Greasy Fork

Greasy Fork is available in English.

网页抖音体验增强

自动跳过直播、智能屏蔽关键字(自动不感兴趣)、跳过广告、最高分辨率、分辨率筛选、AI智能筛选(自动点赞)、极速模式

当前为 2025-08-24 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.3
// @description 自动跳过直播、智能屏蔽关键字(自动不感兴趣)、跳过广告、最高分辨率、分辨率筛选、AI智能筛选(自动点赞)、极速模式
// @author Frequenk
// @license GPL-3.0 License
// @run-at document-start
// ==/UserScript==

/*
==== 网页抖音体验增强 ====

核心功能:
1. 跳过直播功能
    - 自动检测并跳过直播内容
    - 通过设置面板开关控制

2. 智能屏蔽关键字 ⭐
    - 自动检测账号名称中的屏蔽关键字(默认:"店"、"甄选")
    - 智能处理:可选择"不感兴趣"(R键)或直接跳过,默认使用R键
    - 按R键后视频直接消失,更符合抖音行为逻辑
    - 📁 导入导出功能:支持txt文件批量管理关键字列表
    - 点击按钮文字打开管理弹窗,支持增删改查

3. 跳过广告功能
    - 自动识别并跳过广告视频
    - 通过设置面板开关控制

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

5. 分辨率筛选 ⭐
    - 只看指定分辨率的视频(4K、2K、1080P、720P、540P)
    - 没有指定分辨率时自动跳过
    - 点击按钮文字选择想要的分辨率

6. AI智能筛选(需本地AI)⭐
    - 🤖 内容识别:自定义想看的内容类型(如"露脸的美女"、"搞笑视频"等)
    - 👍 智能点赞:AI判定喜欢内容时可选择自动点赞,默认开启
    - 🔧 模型支持:支持多种AI模型,默认qwen2.5vl:7b
    - ⚡ 快速决策:多时间点检测(0s、1s、2.5s、4s、6s、8s)
    - 🎯 精准判断:连续1次不符合立即跳过,连续2次符合停止检测
    - 📋 环境要求:需安装Ollama并下载视觉模型

7. 极速模式
    - 每个视频播放指定时间后自动切换到下一个
    - 可自定义播放时间(1-60秒,默认6秒)
    - 适合快速浏览大量内容

8. 用户界面
    - 🎛️ 统一控制:所有功能集成在播放器设置面板中
    - ⚙️ 详细设置:点击按钮文字打开功能专属设置弹窗
    - 📊 状态显示:实时显示各功能开关状态和参数
    - 💾 自动保存:所有设置自动保存到本地存储

安装要求:
- 支持Violentmonkey、Tampermonkey等用户脚本管理器
- AI功能需要本地Ollama环境和视觉模型

版本:2.2 | 作者:Frequenk | 协议:GPL-3.0
*/

(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(),
                    pressR: this.loadPressRSetting()
                },
                skipAd: { enabled: true, key: 'skipAd' },
                onlyResolution: { 
                    enabled: false, 
                    key: 'onlyResolution',
                    resolution: this.loadTargetResolution()
                },
                aiPreference: { 
                    enabled: false, 
                    key: 'aiPreference',
                    content: this.loadAiContent(),
                    model: this.loadAiModel(),
                    autoLike: this.loadAutoLikeSetting()
                },
                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';
        }

        loadTargetResolution() {
            return localStorage.getItem('douyin_target_resolution') || '4K';
        }

        loadPressRSetting() {
            return localStorage.getItem('douyin_press_r_enabled') !== 'false'; // 默认开启
        }

        loadAutoLikeSetting() {
            return localStorage.getItem('douyin_auto_like_enabled') !== 'false'; // 默认开启
        }

        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);
        }

        saveTargetResolution(resolution) {
            this.config.onlyResolution.resolution = resolution;
            localStorage.setItem('douyin_target_resolution', resolution);
        }

        savePressRSetting(enabled) {
            this.config.blockKeywords.pressR = enabled;
            localStorage.setItem('douyin_press_r_enabled', enabled.toString());
        }

        saveAutoLikeSetting(enabled) {
            this.config.aiPreference.autoLike = enabled;
            localStorage.setItem('douyin_auto_like_enabled', enabled.toString());
        }

        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;
        }

        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);
        }

        pressR() {
            console.log('【不感兴趣】屏蔽关键字内容');
            this.sendKeyEvent('r', 'KeyR', 82);
        }

        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;
                }


                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: `分辨率筛选(${this.config.get('onlyResolution').resolution})`,
                    className: 'resolution-filter-button',
                    configKey: 'onlyResolution',
                    onClick: () => this.showResolutionDialog()
                },
                {
                    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}秒)`;
            });
        }

        updateResolutionText() {
            const resolution = this.config.get('onlyResolution').resolution;
            document.querySelectorAll('.resolution-filter-button .xgplayer-setting-title').forEach(el => {
                el.textContent = `分辨率筛选(${resolution})`;
            });
        }

        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 autoLikeEnabled = this.config.get('aiPreference').autoLike;
            
            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="margin-bottom: 15px; padding: 10px; background: rgba(255, 255, 255, 0.05); border-radius: 6px;">
                    <label style="display: flex; align-items: center; cursor: pointer; color: white; font-size: 13px;">
                        <input type="checkbox" class="auto-like-checkbox" ${autoLikeEnabled ? 'checked' : ''} 
                               style="margin-right: 8px; transform: scale(1.2);">
                        AI判定为喜欢的内容将自动点赞(Z键)
                    </label>
                    <div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-top: 5px; margin-left: 24px;">
                        帮助抖音算法了解你喜欢此类内容
                    </div>
                </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 autoLikeCheckbox = dialog.querySelector('.auto-like-checkbox');
                
                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);
                this.config.saveAutoLikeSetting(autoLikeCheckbox.checked);
                
                console.log('AI喜好设置已更新:', { content, model, autoLike: autoLikeCheckbox.checked });
                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 = '';
                }
            });
            
            // 防止复选框点击时关闭弹窗
            dialog.querySelector('.auto-like-checkbox').addEventListener('click', (e) => {
                e.stopPropagation();
            });
        }

        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.onclick = (e) => {
                    if (e.target.classList.contains('delete-keyword')) {
                        e.stopPropagation(); // 阻止事件冒泡,防止触发弹窗关闭
                        const index = parseInt(e.target.dataset.index);
                        tempKeywords.splice(index, 1);
                        updateList();
                    }
                };
            };

            const pressREnabled = this.config.get('blockKeywords').pressR;
            
            const content = `
                <div style="color: rgba(255, 255, 255, 0.7); margin-bottom: 15px; font-size: 12px;">
                    包含这些关键字的账号将被自动跳过
                </div>
                
                <div style="margin-bottom: 15px; padding: 10px; background: rgba(255, 255, 255, 0.05); border-radius: 6px;">
                    <label style="display: flex; align-items: center; cursor: pointer; color: white; font-size: 13px;">
                        <input type="checkbox" class="press-r-checkbox" ${pressREnabled ? 'checked' : ''} 
                               style="margin-right: 8px; transform: scale(1.2);">
                        跳过时自动按R键(不感兴趣)
                    </label>
                    <div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-top: 5px; margin-left: 24px;">
                        帮助抖音算法了解你不喜欢此类内容
                    </div>
                </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 style="display: flex; gap: 10px; margin-bottom: 10px;">
                    <button class="import-keywords" style="flex: 1; padding: 8px 12px; background: rgba(52, 152, 219, 0.8); 
                            color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
                        📁 导入关键字
                    </button>
                    <button class="export-keywords" style="flex: 1; padding: 8px 12px; background: rgba(155, 89, 182, 0.8); 
                            color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
                        💾 导出关键字
                    </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, () => {
                const pressRCheckbox = dialog.querySelector('.press-r-checkbox');
                
                this.config.saveKeywords(tempKeywords);
                this.config.savePressRSetting(pressRCheckbox.checked);
                
                console.log('屏蔽关键字已更新:', tempKeywords);
                console.log('自动按R键设置已更新:', pressRCheckbox.checked);
                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', (e) => {
                e.stopPropagation(); // 阻止事件冒泡,防止触发弹窗关闭
                addKeyword();
            });
            dialog.querySelector('.keyword-input').addEventListener('keypress', (e) => {
                if (e.key === 'Enter') {
                    e.stopPropagation(); // 阻止事件冒泡
                    addKeyword();
                }
            });
            
            // 防止在输入框内点击时关闭弹窗
            dialog.querySelector('.keyword-input').addEventListener('click', (e) => {
                e.stopPropagation();
            });
            
            // 防止复选框点击时关闭弹窗
            dialog.querySelector('.press-r-checkbox').addEventListener('click', (e) => {
                e.stopPropagation();
            });

            // 导出功能
            const exportKeywords = () => {
                const content = tempKeywords.join('\n');
                const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `抖音屏蔽关键字_${new Date().toISOString().split('T')[0]}.txt`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                console.log('关键字已导出:', tempKeywords);
            };

            dialog.querySelector('.export-keywords').addEventListener('click', (e) => {
                e.stopPropagation();
                exportKeywords();
            });

            // 导入功能
            const importKeywords = () => {
                const input = document.createElement('input');
                input.type = 'file';
                input.accept = '.txt';
                input.addEventListener('change', (e) => {
                    const file = e.target.files[0];
                    if (file) {
                        const reader = new FileReader();
                        reader.onload = (e) => {
                            const content = e.target.result;
                            const importedKeywords = content.split('\n')
                                .map(line => line.trim())
                                .filter(line => line.length > 0);
                            
                            if (importedKeywords.length > 0) {
                                // 合并关键字,去重
                                const allKeywords = [...new Set([...tempKeywords, ...importedKeywords])];
                                tempKeywords.splice(0, tempKeywords.length, ...allKeywords);
                                updateList();
                                console.log('关键字导入完成:', importedKeywords);
                                console.log('当前关键字列表:', tempKeywords);
                            } else {
                                alert('文件内容为空或格式不正确!');
                            }
                        };
                        reader.onerror = () => {
                            alert('文件读取失败!');
                        };
                        reader.readAsText(file, 'utf-8');
                    }
                });
                input.click();
            };

            dialog.querySelector('.import-keywords').addEventListener('click', (e) => {
                e.stopPropagation();
                importKeywords();
            });

            updateList();
        }

        showResolutionDialog() {
            const currentResolution = this.config.get('onlyResolution').resolution;
            const resolutions = ['4K', '2K', '1080P', '720P', '540P'];
            
            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>
                    <div style="position: relative;">
                        <select class="resolution-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;">
                            ${resolutions.map(res => 
                                `<option value="${res}" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentResolution === res ? 'selected' : ''}>${res}</option>`
                            ).join('')}
                        </select>
                        <span style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); 
                                   pointer-events: none; color: rgba(255, 255, 255, 0.5);">▼</span>
                    </div>
                </div>
                
                <div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-bottom: 10px;">
                    提示:只播放包含所选分辨率关键字的视频,没有找到则自动跳过
                </div>
            `;

            const dialog = UIFactory.createDialog('resolution-dialog', '分辨率筛选设置', content, () => {
                const resolutionSelect = dialog.querySelector('.resolution-select');
                const resolution = resolutionSelect.value;
                
                this.config.saveTargetResolution(resolution);
                this.updateResolutionText();
                console.log('分辨率筛选已更新:', resolution);
                return true;
            });
        }
    }

    // ========== 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;
                
                // 检查是否开启了自动点赞功能
                const autoLikeEnabled = this.config.get('aiPreference').autoLike;
                if (!this.hasLiked && autoLikeEnabled) {
                    this.videoController.like();
                    this.hasLiked = true;
                } else if (!autoLikeEnabled) {
                    console.log('【自动点赞】功能已关闭,跳过点赞');
                }
            }
        }
    }

    // ========== 视频检测策略 ==========
    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;
            const pressREnabled = this.config.get('blockKeywords').pressR;
            
            if (accountName && keywords.some(kw => accountName.includes(kw))) {
                console.log(`检测到屏蔽关键字,已跳过账号: ${accountName}`);
                
                // 如果开启了按R键功能,只按R键(视频会直接消失)
                if (pressREnabled) {
                    this.videoController.pressR();
                } else {
                    // 如果没开启R键功能,则使用下键跳过
                    this.videoController.skip();
                }
                return true;
            }
            return false;
        }

        checkResolution(container) {
            if (!this.config.isEnabled('autoHighRes') && !this.config.isEnabled('onlyResolution')) 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 (this.config.isEnabled('onlyResolution')) {
                const targetResolution = this.config.get('onlyResolution').resolution.toUpperCase();
                const hasTarget = options.some(opt => opt.text.includes(targetResolution));
                if (!hasTarget) {
                    console.log(`【分辨率筛选】未找到${targetResolution}分辨率,跳过视频`);
                    this.videoController.skip();
                    return true;
                }
                const targetOption = options.find(opt => opt.text.includes(targetResolution));
                if (targetOption && !targetOption.element.classList.contains("selected")) {
                    targetOption.element.click();
                    console.log(`【分辨率筛选】已切换至${targetResolution}分辨率`);
                    return true;
                }
                return false;
            }

            // 原有的最高分辨率逻辑
            if (this.config.isEnabled('autoHighRes')) {
                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();

    // 动态调整底部栏高度
    function adjustBottomBarHeight() {
        const bottomBars = document.querySelectorAll('.xg-right-grid');
        bottomBars.forEach(bar => {
            // 移除固定高度限制,让容器根据内容自适应
            bar.style.height = 'auto';
            bar.style.maxHeight = 'none';
            bar.style.overflow = 'visible';
            
            // 确保按钮容器可以正常换行
            const buttonContainers = bar.querySelectorAll('.xgplayer-controls');
            buttonContainers.forEach(container => {
                container.style.flexWrap = 'wrap';
                container.style.height = 'auto';
            });
        });
    }

    // 监听DOM变化,动态调整高度
    const observer = new MutationObserver(() => {
        adjustBottomBarHeight();
    });

    // 监听整个文档的变化
    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class', 'style']
    });

    // 初始调整
    setTimeout(adjustBottomBarHeight, 1000);
    setInterval(adjustBottomBarHeight, 2000);

})();