网页抖音体验增强

自动跳过直播、屏蔽账号关键字、跳过广告、最高分辨率、AI喜好模式(可自定义)、极速模式、AI判定内容自动点赞

// ==UserScript==
// @name 网页抖音体验增强
// @namespace Violentmonkey Scripts
// @match https://www.douyin.com/?*
// @match *://*.douyin.com/*
// @match *://*.iesdouyin.com/*
// @exclude *://lf-zt.douyin.com*
// @grant none
// @version 2.1
// @description 自动跳过直播、屏蔽账号关键字、跳过广告、最高分辨率、AI喜好模式(可自定义)、极速模式、AI判定内容自动点赞
// @author Frequenk
// @license GPL-3.0 License
// @run-at document-start
// ==/UserScript==

/*
功能说明:
1. 跳过直播功能
    - 自动检测并跳过直播内容
    - 可通过界面按钮开启/关闭

2. 屏蔽账号关键字
    - 自动检测账号名称是否包含屏蔽关键字
    - 点击按钮文字可自定义关键字列表(默认:"店"、"甄选")
    - 关键字保存在本地存储中

3. 跳过广告功能
    - 自动检测并跳过广告视频
    - 可通过界面按钮开启/关闭

4. 自动最高分辨率
    - 自动选择最高可用分辨率(优先级:4K > 2K > 1080P > 720P > 540P > 智能)
    - 找到4K后自动关闭该功能

5. AI喜好模式(需本地AI)
    - 点击按钮文字可自定义想看的内容类型
    - 支持选择或输入自定义AI模型
    - 快速决策:0秒、1秒、2.5秒、4秒、6秒、8秒时检测
    - 连续1次不符合立即跳过,连续2次符合停止检测
    - 判定为喜好内容后自动点赞(Z键)
    - 需要安装Ollama并下载视觉模型

6. 极速模式
    - 每个视频播放指定秒数后自动切换
    - 点击按钮文字可设置时间(1-60秒,默认6秒)
    - 适合快速浏览大量内容

7. 界面控制
    - 所有功能通过播放器设置面板的开关按钮控制
    - 实时显示各功能状态
    - 支持动态开关切换
*/

(function() {
    'use strict';

    // ========== 配置管理模块 ==========
    class ConfigManager {
        constructor() {
            this.config = {
                skipLive: { enabled: true, key: 'skipLive' },
                autoHighRes: { enabled: true, key: 'autoHighRes' },
                blockKeywords: { 
                    enabled: true, 
                    key: 'blockKeywords',
                    keywords: this.loadKeywords()
                },
                skipAd: { enabled: true, key: 'skipAd' },
                aiPreference: { 
                    enabled: false, 
                    key: 'aiPreference',
                    content: this.loadAiContent(),
                    model: this.loadAiModel()
                },
                speedMode: { 
                    enabled: false, 
                    key: 'speedMode',
                    seconds: this.loadSpeedSeconds()
                }
            };
        }

        loadKeywords() {
            return JSON.parse(localStorage.getItem('douyin_blocked_keywords') || '["店", "甄选"]');
        }

        loadSpeedSeconds() {
            return parseInt(localStorage.getItem('douyin_speed_mode_seconds') || '6');
        }

        loadAiContent() {
            return localStorage.getItem('douyin_ai_content') || '露脸的美女';
        }

        loadAiModel() {
            return localStorage.getItem('douyin_ai_model') || 'qwen2.5vl:7b';
        }

        saveKeywords(keywords) {
            this.config.blockKeywords.keywords = keywords;
            localStorage.setItem('douyin_blocked_keywords', JSON.stringify(keywords));
        }

        saveSpeedSeconds(seconds) {
            this.config.speedMode.seconds = seconds;
            localStorage.setItem('douyin_speed_mode_seconds', seconds.toString());
        }

        saveAiContent(content) {
            this.config.aiPreference.content = content;
            localStorage.setItem('douyin_ai_content', content);
        }

        saveAiModel(model) {
            this.config.aiPreference.model = model;
            localStorage.setItem('douyin_ai_model', model);
        }

        get(key) {
            return this.config[key];
        }

        setEnabled(key, value) {
            if (this.config[key]) {
                this.config[key].enabled = value;
            }
        }

        isEnabled(key) {
            return this.config[key]?.enabled || false;
        }
    }

    // ========== DOM选择器常量 ==========
    const SELECTORS = {
        activeVideo: "[data-e2e='feed-active-video']",
        resolutionOptions: ".xgplayer-playing div.virtual > div.item",
        accountName: '[data-e2e="feed-video-nickname"]',
        settingsPanel: 'xg-icon.xgplayer-autoplay-setting',
        adIndicator: 'svg[viewBox="0 0 30 16"]',
        videoElement: 'video'
    };

    // ========== 视频控制器 ==========
    class VideoController {
        constructor() {
            this.skipCheckInterval = null;
            this.skipAttemptCount = 0;
            this.MAX_SKIP_ATTEMPTS = 20;
        }

        skip() {
            console.log('跳过视频');
            if (!document.body) return;

            const videoBefore = this.getCurrentVideoUrl();
            this.sendKeyEvent('ArrowDown');
            
            this.clearSkipCheck();
            this.startSkipCheck(videoBefore);
        }

        like() {
            console.log('【自动点赞】喜好内容');
            this.sendKeyEvent('z', 'KeyZ', 90);
        }

        sendKeyEvent(key, code = null, keyCode = null) {
            try {
                const event = new KeyboardEvent('keydown', {
                    key: key,
                    code: code || (key === 'ArrowDown' ? 'ArrowDown' : code),
                    keyCode: keyCode || (key === 'ArrowDown' ? 40 : keyCode),
                    which: keyCode || (key === 'ArrowDown' ? 40 : keyCode),
                    bubbles: true,
                    cancelable: true
                });
                document.body.dispatchEvent(event);
            } catch (error) {
                console.log('发送键盘事件失败:', error);
            }
        }

        getCurrentVideoUrl() {
            const videoEl = document.querySelector(`${SELECTORS.activeVideo} ${SELECTORS.videoElement}`);
            return videoEl?.src || '';
        }

        clearSkipCheck() {
            if (this.skipCheckInterval) {
                clearInterval(this.skipCheckInterval);
                this.skipCheckInterval = null;
            }
            this.skipAttemptCount = 0;
        }

        startSkipCheck(urlBefore) {
            this.skipCheckInterval = setInterval(() => {
                this.skipAttemptCount++;
                const urlAfter = this.getCurrentVideoUrl();

                if (urlAfter && urlAfter !== urlBefore) {
                    console.log('视频已成功切换');
                    this.clearSkipCheck();
                    return;
                }

                if (this.skipAttemptCount >= this.MAX_SKIP_ATTEMPTS) {
                    console.log('达到最大尝试次数,停止跳过');
                    this.clearSkipCheck();
                    return;
                }

                console.log(`视频未切换,第${this.skipAttemptCount + 1}次尝试跳过`);
                this.sendKeyEvent('ArrowDown');
            }, 300);
        }
    }

    // ========== UI组件工厂 ==========
    class UIFactory {
        static createDialog(className, title, content, onSave, onCancel) {
            const existingDialog = document.querySelector(`.${className}`);
            if (existingDialog) {
                existingDialog.remove();
                return;
            }

            const dialog = document.createElement('div');
            dialog.className = className;
            Object.assign(dialog.style, {
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                background: 'rgba(0, 0, 0, 0.9)',
                border: '1px solid rgba(255, 255, 255, 0.2)',
                borderRadius: '8px',
                padding: '20px',
                zIndex: '10000',
                minWidth: '250px'
            });

            dialog.innerHTML = `
                <div style="color: white; margin-bottom: 15px; font-size: 14px;">${title}</div>
                ${content}
                <div style="display: flex; gap: 10px; margin-top: 15px;">
                    <button class="dialog-confirm" style="flex: 1; padding: 5px; background: #fe2c55; 
                            color: white; border: none; border-radius: 4px; cursor: pointer;">确定</button>
                    <button class="dialog-cancel" style="flex: 1; padding: 5px; background: rgba(255, 255, 255, 0.1); 
                            color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; cursor: pointer;">取消</button>
                </div>
            `;

            document.body.appendChild(dialog);

            dialog.querySelector('.dialog-confirm').addEventListener('click', () => {
                if (onSave()) dialog.remove();
            });

            dialog.querySelector('.dialog-cancel').addEventListener('click', () => {
                dialog.remove();
                if (onCancel) onCancel();
            });

            setTimeout(() => {
                document.addEventListener('click', function closeDialog(e) {
                    if (!dialog.contains(e.target)) {
                        dialog.remove();
                        document.removeEventListener('click', closeDialog);
                    }
                });
            }, 100);

            return dialog;
        }

        static createToggleButton(text, className, isEnabled, onToggle, onClick = null) {
            const btnContainer = document.createElement('xg-icon');
            btnContainer.className = `xgplayer-autoplay-setting ${className}`;
            
            btnContainer.innerHTML = `
                <div class="xgplayer-icon">
                    <div class="xgplayer-setting-label">
                        <button aria-checked="${isEnabled}" class="xg-switch ${isEnabled ? 'xg-switch-checked' : ''}">
                            <span class="xg-switch-inner"></span>
                        </button>
                        <span class="xgplayer-setting-title" style="${onClick ? 'cursor: pointer; text-decoration: underline;' : ''}">${text}</span>
                    </div>
                </div>`;

            btnContainer.querySelector('button').addEventListener('click', (e) => {
                const newState = e.currentTarget.getAttribute('aria-checked') === 'false';
                UIManager.updateToggleButtons(className, newState);
                onToggle(newState);
            });

            if (onClick) {
                btnContainer.querySelector('.xgplayer-setting-title').addEventListener('click', (e) => {
                    e.stopPropagation();
                    onClick();
                });
            }

            return btnContainer;
        }

        static showErrorDialog() {
            const dialog = document.createElement('div');
            dialog.className = 'error-dialog-' + Date.now();
            dialog.style.cssText = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: rgba(0, 0, 0, 0.95);
                border: 2px solid rgba(254, 44, 85, 0.8);
                color: white;
                padding: 20px;
                border-radius: 8px;
                z-index: 10001;
                max-width: 400px;
                text-align: center;
                font-size: 14px;
            `;
            dialog.innerHTML = `
                <div style="margin-bottom: 20px;">
                    <div style="color: #fe2c55; font-size: 40px; margin-bottom: 15px;">⚠️</div>
                    <div style="text-align: left; line-height: 1.6;">
                        <div style="margin-bottom: 12px;">
                            <strong>请检查以下配置:</strong>
                        </div>
                        <div style="margin-bottom: 8px;">
                            1. 安装 <a href="https://ollama.com/" target="_blank" style="color: #fe2c55; text-decoration: underline;">Ollama</a> 
                            并下载视觉模型(默认:qwen2.5vl:7b)
                        </div>
                        <div>
                            2. 开启Ollama跨域模式,设置环境变量:
                            <div style="margin-left: 20px; margin-top: 5px; font-family: monospace; background: rgba(255, 255, 255, 0.1); padding: 5px; border-radius: 4px;">
                                OLLAMA_HOST=0.0.0.0<br>
                                OLLAMA_ORIGINS=*
                            </div>
                            <div style="margin-top: 8px;">
                                参考配置教程:<a href="https://lobehub.com/zh/docs/self-hosting/examples/ollama" target="_blank" 
                                   style="color: #fe2c55; text-decoration: underline;">Ollama跨域设置指南</a>
                            </div>
                        </div>
                    </div>
                </div>
                <button class="error-dialog-confirm" style="padding: 8px 20px; background: #fe2c55; color: white; 
                        border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">确定</button>
            `;
            document.body.appendChild(dialog);
            
            dialog.querySelector('.error-dialog-confirm').addEventListener('click', () => {
                dialog.remove();
            });
        }
    }

    // ========== UI管理器 ==========
    class UIManager {
        constructor(config, videoController) {
            this.config = config;
            this.videoController = videoController;
            this.initButtons();
        }

        initButtons() {
            this.buttonConfigs = [
                {
                    text: '跳过直播',
                    className: 'skip-live-button',
                    configKey: 'skipLive'
                },
                {
                    text: '寻找最高分辨率',
                    className: 'auto-high-resolution-button',
                    configKey: 'autoHighRes'
                },
                {
                    text: '屏蔽账号关键字',
                    className: 'block-account-keyword-button',
                    configKey: 'blockKeywords',
                    onClick: () => this.showKeywordDialog()
                },
                {
                    text: '跳过广告',
                    className: 'skip-ad-button',
                    configKey: 'skipAd'
                },
                {
                    text: 'AI喜好模式',
                    className: 'ai-preference-button',
                    configKey: 'aiPreference',
                    onClick: () => this.showAiPreferenceDialog()
                },
                {
                    text: `极速模式(${this.config.get('speedMode').seconds}秒)`,
                    className: 'speed-mode-button',
                    configKey: 'speedMode',
                    onClick: () => this.showSpeedDialog()
                }
            ];
        }

        insertButtons() {
            document.querySelectorAll(SELECTORS.settingsPanel).forEach(panel => {
                const parent = panel.parentNode;
                if (!parent) return;

                let lastButton = panel;
                this.buttonConfigs.forEach(config => {
                    let button = parent.querySelector(`.${config.className}`);
                    if (!button) {
                        button = UIFactory.createToggleButton(
                            config.text,
                            config.className,
                            this.config.isEnabled(config.configKey),
                            (state) => this.config.setEnabled(config.configKey, state),
                            config.onClick
                        );
                        parent.insertBefore(button, lastButton.nextSibling);
                    }
                    lastButton = button;
                });
            });
        }

        static updateToggleButtons(className, isEnabled) {
            document.querySelectorAll(`.${className} .xg-switch`).forEach(sw => {
                sw.classList.toggle('xg-switch-checked', isEnabled);
                sw.setAttribute('aria-checked', String(isEnabled));
            });
        }

        updateSpeedModeText() {
            const seconds = this.config.get('speedMode').seconds;
            document.querySelectorAll('.speed-mode-button .xgplayer-setting-title').forEach(el => {
                el.textContent = `极速模式(${seconds}秒)`;
            });
        }

        showSpeedDialog() {
            const seconds = this.config.get('speedMode').seconds;
            const content = `
                <input type="number" class="speed-input" min="1" max="60" value="${seconds}" 
                    style="width: 100%; padding: 5px; margin-bottom: 15px; background: rgba(255, 255, 255, 0.1); 
                           color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px;">
            `;

            UIFactory.createDialog('speed-mode-time-dialog', '设置极速模式时间(秒)', content, () => {
                const input = document.querySelector('.speed-input');
                const value = parseInt(input.value);
                if (value >= 1 && value <= 60) {
                    this.config.saveSpeedSeconds(value);
                    this.updateSpeedModeText();
                    return true;
                }
                return false;
            });
        }

        showAiPreferenceDialog() {
            const currentContent = this.config.get('aiPreference').content;
            const currentModel = this.config.get('aiPreference').model;
            
            const content = `
                <div style="margin-bottom: 15px;">
                    <label style="color: rgba(255, 255, 255, 0.7); font-size: 12px; display: block; margin-bottom: 5px;">
                        想看什么内容?(例如:露脸的美女、搞笑视频、猫咪)
                    </label>
                    <input type="text" class="ai-content-input" value="${currentContent}" placeholder="输入你想看的内容"
                        style="width: 100%; padding: 8px; background: rgba(255, 255, 255, 0.1); 
                               color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px;">
                </div>
                
                <div style="margin-bottom: 15px;">
                    <label style="color: rgba(255, 255, 255, 0.7); font-size: 12px; display: block; margin-bottom: 5px;">
                        AI模型选择
                    </label>
                    <div style="position: relative;">
                        <select class="ai-model-select" 
                            style="width: 100%; padding: 8px; background: rgba(255, 255, 255, 0.1); 
                                   color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px;
                                   appearance: none; cursor: pointer;">
                            <option value="qwen2.5vl:7b" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentModel === 'qwen2.5vl:7b' ? 'selected' : ''}>qwen2.5vl:7b (推荐)</option>
                            <option value="custom" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentModel !== 'qwen2.5vl:7b' ? 'selected' : ''}>自定义模型</option>
                        </select>
                        <span style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); 
                                   pointer-events: none; color: rgba(255, 255, 255, 0.5);">▼</span>
                    </div>
                    <input type="text" class="ai-model-input" value="${currentModel !== 'qwen2.5vl:7b' ? currentModel : ''}" 
                        placeholder="输入自定义模型名称"
                        style="width: 100%; padding: 8px; margin-top: 10px; background: rgba(255, 255, 255, 0.1); 
                               color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px;
                               display: ${currentModel !== 'qwen2.5vl:7b' ? 'block' : 'none'};">
                </div>
                
                <div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-bottom: 10px;">
                    提示:需要安装 <a href="https://ollama.com/" target="_blank" style="color: #fe2c55;">Ollama</a> 并下载视觉模型
                </div>
            `;

            const dialog = UIFactory.createDialog('ai-preference-dialog', '设置AI喜好', content, () => {
                const contentInput = dialog.querySelector('.ai-content-input');
                const modelSelect = dialog.querySelector('.ai-model-select');
                const modelInput = dialog.querySelector('.ai-model-input');
                
                const content = contentInput.value.trim();
                let model = modelSelect.value === 'custom' 
                    ? modelInput.value.trim() 
                    : modelSelect.value;
                
                if (!content) {
                    alert('请输入想看的内容');
                    return false;
                }
                
                if (!model) {
                    alert('请选择或输入模型名称');
                    return false;
                }
                
                this.config.saveAiContent(content);
                this.config.saveAiModel(model);
                console.log('AI喜好设置已更新:', { content, model });
                return true;
            });

            // 处理模型选择切换
            const modelSelect = dialog.querySelector('.ai-model-select');
            const modelInput = dialog.querySelector('.ai-model-input');
            
            modelSelect.addEventListener('change', (e) => {
                if (e.target.value === 'custom') {
                    modelInput.style.display = 'block';
                } else {
                    modelInput.style.display = 'none';
                    modelInput.value = '';
                }
            });
        }

        showKeywordDialog() {
            const keywords = this.config.get('blockKeywords').keywords;
            let tempKeywords = [...keywords];

            const updateList = () => {
                const container = document.querySelector('.keyword-list');
                if (!container) return;
                
                container.innerHTML = tempKeywords.length === 0 
                    ? '<div style="color: rgba(255, 255, 255, 0.5); text-align: center;">暂无关键字</div>'
                    : tempKeywords.map((keyword, index) => `
                        <div style="display: flex; align-items: center; margin-bottom: 8px;">
                            <span style="flex: 1; color: white; padding: 5px 10px; background: rgba(255, 255, 255, 0.1); 
                                   border-radius: 4px; margin-right: 10px;">${keyword}</span>
                            <button data-index="${index}" class="delete-keyword" style="padding: 5px 10px; background: #ff4757; 
                                    color: white; border: none; border-radius: 4px; cursor: pointer;">删除</button>
                        </div>
                    `).join('');

                container.querySelectorAll('.delete-keyword').forEach(btn => {
                    btn.addEventListener('click', (e) => {
                        tempKeywords.splice(parseInt(e.target.dataset.index), 1);
                        updateList();
                    });
                });
            };

            const content = `
                <div style="color: rgba(255, 255, 255, 0.7); margin-bottom: 15px; font-size: 12px;">
                    包含这些关键字的账号将被自动跳过
                </div>
                <div style="display: flex; gap: 10px; margin-bottom: 10px;">
                    <input type="text" class="keyword-input" placeholder="输入新关键字" 
                        style="flex: 1; padding: 8px; background: rgba(255, 255, 255, 0.1); 
                               color: white; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px;">
                    <button class="add-keyword" style="padding: 8px 15px; background: #00d639; 
                            color: white; border: none; border-radius: 4px; cursor: pointer;">添加</button>
                </div>
                <div class="keyword-list" style="margin-bottom: 15px; max-height: 200px; overflow-y: auto;"></div>
            `;

            const dialog = UIFactory.createDialog('keyword-setting-dialog', '管理屏蔽关键字', content, () => {
                this.config.saveKeywords(tempKeywords);
                console.log('屏蔽关键字已更新:', tempKeywords);
                return true;
            });

            const addKeyword = () => {
                const input = dialog.querySelector('.keyword-input');
                const keyword = input.value.trim();
                if (keyword && !tempKeywords.includes(keyword)) {
                    tempKeywords.push(keyword);
                    updateList();
                    input.value = '';
                }
            };

            dialog.querySelector('.add-keyword').addEventListener('click', addKeyword);
            dialog.querySelector('.keyword-input').addEventListener('keypress', (e) => {
                if (e.key === 'Enter') addKeyword();
            });

            updateList();
        }
    }

    // ========== AI检测器 ==========
    class AIDetector {
        constructor(videoController, config) {
            this.videoController = videoController;
            this.config = config;
            this.API_URL = 'http://localhost:11434/api/generate';
            this.checkSchedule = [0, 1000, 2500, 4000, 6000, 8000];
            this.reset();
        }

        reset() {
            this.currentCheckIndex = 0;
            this.checkResults = [];
            this.consecutiveYes = 0;
            this.consecutiveNo = 0;
            this.hasSkipped = false;
            this.stopChecking = false;
            this.hasLiked = false;
            this.isProcessing = false;
        }

        shouldCheck(videoPlayTime) {
            return !this.isProcessing && 
                   !this.stopChecking && 
                   !this.hasSkipped && 
                   this.currentCheckIndex < this.checkSchedule.length &&
                   videoPlayTime >= this.checkSchedule[this.currentCheckIndex];
        }

        async processVideo(videoEl) {
            if (this.isProcessing || this.stopChecking || this.hasSkipped) return;
            this.isProcessing = true;

            try {
                const base64Image = await this.captureVideoFrame(videoEl);
                const aiResponse = await this.callAI(base64Image);
                this.handleResponse(aiResponse);
                this.currentCheckIndex++;
            } catch (error) {
                console.error('AI判断功能出错:', error);
                // 显示错误提示
                UIFactory.showErrorDialog();
                // 关闭AI喜好模式
                this.config.setEnabled('aiPreference', false);
                UIManager.updateToggleButtons('ai-preference-button', false);
                this.stopChecking = true;
            } finally {
                this.isProcessing = false;
            }
        }

        async captureVideoFrame(videoEl) {
            const canvas = document.createElement('canvas');
            const maxSize = 500;
            const aspectRatio = videoEl.videoWidth / videoEl.videoHeight;
            
            let targetWidth, targetHeight;
            if (videoEl.videoWidth > videoEl.videoHeight) {
                targetWidth = Math.min(videoEl.videoWidth, maxSize);
                targetHeight = Math.round(targetWidth / aspectRatio);
            } else {
                targetHeight = Math.min(videoEl.videoHeight, maxSize);
                targetWidth = Math.round(targetHeight * aspectRatio);
            }

            canvas.width = targetWidth;
            canvas.height = targetHeight;
            
            const ctx = canvas.getContext('2d');
            ctx.drawImage(videoEl, 0, 0, targetWidth, targetHeight);
            
            return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];
        }

        async callAI(base64Image) {
            const content = this.config.get('aiPreference').content;
            const model = this.config.get('aiPreference').model;
            
            const response = await fetch(this.API_URL, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    model: model,
                    prompt: `这是${content}吗?回答『是』或者『不是』,不要说任何多余的字符`,
                    images: [base64Image],
                    stream: false
                })
            });

            if (!response.ok) {
                throw new Error(`AI请求失败: ${response.status}`);
            }

            const result = await response.json();
            return result.response?.trim();
        }

        handleResponse(aiResponse) {
            const content = this.config.get('aiPreference').content;
            this.checkResults.push(aiResponse);
            console.log(`AI检测结果[${this.checkResults.length}]:${aiResponse}`);

            if (aiResponse === '是') {
                this.consecutiveYes++;
                this.consecutiveNo = 0;
            } else {
                this.consecutiveYes = 0;
                this.consecutiveNo++;
            }

            if (this.consecutiveNo >= 1) {
                console.log(`【立即跳过】判定为非${content}`);
                this.hasSkipped = true;
                this.stopChecking = true;
                this.videoController.skip();
            } else if (this.consecutiveYes >= 2) {
                console.log(`【停止检测】连续2次判定为${content},安心观看`);
                this.stopChecking = true;
                if (!this.hasLiked) {
                    this.videoController.like();
                    this.hasLiked = true;
                }
            }
        }
    }

    // ========== 视频检测策略 ==========
    class VideoDetectionStrategies {
        constructor(config, videoController) {
            this.config = config;
            this.videoController = videoController;
        }

        checkAd(container) {
            if (!this.config.isEnabled('skipAd')) return false;
            
            const adIndicator = container.querySelector(SELECTORS.adIndicator);
            if (adIndicator) {
                console.log("检测到广告,已跳过");
                this.videoController.skip();
                return true;
            }
            return false;
        }

        checkBlockedAccount(container) {
            if (!this.config.isEnabled('blockKeywords')) return false;
            
            const accountEl = container.querySelector(SELECTORS.accountName);
            const accountName = accountEl?.textContent.trim();
            const keywords = this.config.get('blockKeywords').keywords;
            
            if (accountName && keywords.some(kw => accountName.includes(kw))) {
                console.log(`检测到屏蔽关键字,已跳过账号: ${accountName}`);
                this.videoController.skip();
                return true;
            }
            return false;
        }

        checkResolution(container) {
            if (!this.config.isEnabled('autoHighRes')) return false;

            const priorityOrder = ["4K", "2K", "1080P", "720P", "540P", "智能"];
            const options = Array.from(container.querySelectorAll(SELECTORS.resolutionOptions))
                .map(el => {
                    const text = el.textContent.trim().toUpperCase();
                    return { 
                        element: el, 
                        text, 
                        priority: priorityOrder.findIndex(p => text.includes(p)) 
                    };
                })
                .filter(opt => opt.priority !== -1)
                .sort((a, b) => a.priority - b.priority);

            if (options.length > 0 && !options[0].element.classList.contains("selected")) {
                const bestOption = options[0];
                bestOption.element.click();
                console.log(`已切换至最高分辨率: ${bestOption.element.textContent}`);

                if (bestOption.text.includes("4K")) {
                    this.config.setEnabled('autoHighRes', false);
                    UIManager.updateToggleButtons('auto-high-resolution-button', false);
                    console.log("已找到4K分辨率,自动关闭功能");
                }
                return true;
            }
            return false;
        }
    }

    // ========== 主应用程序 ==========
    class DouyinEnhancer {
        constructor() {
            this.config = new ConfigManager();
            this.videoController = new VideoController();
            this.uiManager = new UIManager(this.config, this.videoController);
            this.aiDetector = new AIDetector(this.videoController, this.config);
            this.strategies = new VideoDetectionStrategies(this.config, this.videoController);
            
            this.lastVideoUrl = '';
            this.videoStartTime = 0;
            this.speedModeSkipped = false;
            
            this.init();
        }

        init() {
            setInterval(() => this.mainLoop(), 300);
        }

        mainLoop() {
            this.uiManager.insertButtons();

            const activeContainer = document.querySelector(SELECTORS.activeVideo);
            if (!activeContainer) {
                if (this.config.isEnabled('skipLive')) {
                    this.videoController.skip();
                }
                return;
            }

            const videoEl = activeContainer.querySelector(SELECTORS.videoElement);
            if (!videoEl || !videoEl.src) return;

            const currentVideoUrl = videoEl.src;
            
            if (this.handleNewVideo(currentVideoUrl)) {
                return;
            }

            if (this.handleSpeedMode()) {
                return;
            }

            if (this.handleAIDetection(videoEl)) {
                return;
            }

            if (this.strategies.checkAd(activeContainer)) return;
            if (this.strategies.checkBlockedAccount(activeContainer)) return;
            this.strategies.checkResolution(activeContainer);
        }

        handleNewVideo(currentVideoUrl) {
            if (currentVideoUrl !== this.lastVideoUrl) {
                this.lastVideoUrl = currentVideoUrl;
                this.videoStartTime = Date.now();
                this.speedModeSkipped = false;
                this.aiDetector.reset();
                
                console.log('===== 新视频开始 =====');
                if (this.config.isEnabled('speedMode')) {
                    const seconds = this.config.get('speedMode').seconds;
                    console.log(`【极速模式】已开启,${seconds}秒后自动切换`);
                }
                if (this.config.isEnabled('aiPreference')) {
                    const content = this.config.get('aiPreference').content;
                    console.log(`【AI喜好模式】已开启,筛选:${content}`);
                }
                return true;
            }
            return false;
        }

        handleSpeedMode() {
            if (!this.config.isEnabled('speedMode') || this.speedModeSkipped || this.aiDetector.hasSkipped) {
                return false;
            }

            const videoPlayTime = Date.now() - this.videoStartTime;
            const seconds = this.config.get('speedMode').seconds;
            
            if (videoPlayTime >= seconds * 1000) {
                console.log(`【极速模式】视频已播放${seconds}秒,自动切换`);
                this.speedModeSkipped = true;
                this.videoController.skip();
                return true;
            }
            return false;
        }

        handleAIDetection(videoEl) {
            if (!this.config.isEnabled('aiPreference')) return false;

            const videoPlayTime = Date.now() - this.videoStartTime;
            
            if (this.aiDetector.shouldCheck(videoPlayTime)) {
                if (videoEl.readyState >= 2 && !videoEl.paused) {
                    const timeInSeconds = (this.aiDetector.checkSchedule[this.aiDetector.currentCheckIndex] / 1000).toFixed(1);
                    console.log(`【AI检测】第${this.aiDetector.currentCheckIndex + 1}次检测,时间点:${timeInSeconds}秒`);
                    this.aiDetector.processVideo(videoEl);
                    return true;
                }
            }
            
            if (videoPlayTime >= 10000 && !this.aiDetector.stopChecking) {
                console.log('【超时停止】视频播放已超过10秒,停止AI检测');
                this.aiDetector.stopChecking = true;
            }
            
            return false;
        }
    }

    // 启动应用
    const app = new DouyinEnhancer();

})();