自动跳过直播、智能屏蔽关键字(自动不感兴趣)、跳过广告、最高分辨率、分辨率筛选、AI智能筛选(支持智谱/Ollama)、极速模式
// ==UserScript==
// @name 网页抖音体验增强
// @namespace Violentmonkey Scripts
// @match https://www.douyin.com/?*
// @match *://*.douyin.com/*
// @match *://*.iesdouyin.com/*
// @exclude *://lf-zt.douyin.com*
// @grant none
// @version 3.5
// @changelog 新增智谱AI支持(免费在线视觉模型),无需本地部署,准确度相比Ollama开源模型有显著提升
// @description 自动跳过直播、智能屏蔽关键字(自动不感兴趣)、跳过广告、最高分辨率、分辨率筛选、AI智能筛选(支持智谱/Ollama)、极速模式
// @author Frequenk
// @license GPL-3.0 License
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
function isElementInViewport(el, text = "") {
if (!el) return false;
const rect = el.getBoundingClientRect();
return (
rect.width > 0 &&
rect.height > 0 &&
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < window.innerHeight &&
rect.left < window.innerWidth
);
}
function getBestVisibleElement(elements) {
if (!elements || elements.length === 0) {
return null;
}
const visibleElements = Array.from(elements).filter(isElementInViewport);
if (visibleElements.length === 0) {
return null;
}
if (visibleElements.length === 1) {
return visibleElements[0];
}
let bestCandidate = null;
let minDistance = Infinity;
for (const el of visibleElements) {
const rect = el.getBoundingClientRect();
const distance = Math.abs(rect.top);
if (distance < minDistance) {
minDistance = distance;
bestCandidate = el;
}
}
return bestCandidate;
}
// ========== 通知管理器 ==========
class NotificationManager {
constructor() {
this.container = null;
}
createContainer() {
if (this.container && document.body.contains(this.container)) return;
this.container = document.createElement('div');
Object.assign(this.container.style, {
position: 'fixed',
top: '100px',
left: '50%',
transform: 'translateX(-50%)',
zIndex: '10001',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px'
});
document.body.appendChild(this.container);
}
showMessage(message, duration = 2000) {
this.createContainer();
const messageElement = document.createElement('div');
messageElement.textContent = message;
Object.assign(messageElement.style, {
background: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: '10px 20px',
borderRadius: '6px',
fontSize: '14px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
opacity: '0',
transition: 'opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
transform: 'translateY(-20px)'
});
this.container.appendChild(messageElement);
// Animate in
setTimeout(() => {
messageElement.style.opacity = '1';
messageElement.style.transform = 'translateY(0)';
}, 10);
// Animate out and remove
setTimeout(() => {
messageElement.style.opacity = '0';
messageElement.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (messageElement.parentElement) {
messageElement.remove();
}
if (this.container && this.container.childElementCount === 0) {
this.container.remove();
this.container = null;
}
}, 300);
}, duration);
}
}
// ========== 配置管理模块 ==========
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(),
blockName: this.loadBlockNameSetting(),
blockDesc: this.loadBlockDescSetting(),
blockTags: this.loadBlockTagsSetting()
},
skipAd: { enabled: true, key: 'skipAd' },
onlyResolution: {
enabled: false,
key: 'onlyResolution',
resolution: this.loadTargetResolution()
},
aiPreference: {
enabled: false,
key: 'aiPreference',
content: this.loadAiContent(),
provider: this.loadAiProvider(),
// Ollama 配置
model: this.loadAiModel(),
// 智谱配置
zhipuApiKey: this.loadZhipuApiKey(),
zhipuModel: this.loadZhipuModel(),
autoLike: this.loadAutoLikeSetting()
},
speedMode: {
enabled: false,
key: 'speedMode',
seconds: this.loadSpeedSeconds(),
mode: this.loadSpeedModeType(),
minSeconds: this.loadSpeedMinSeconds(),
maxSeconds: this.loadSpeedMaxSeconds()
}
};
}
loadKeywords() {
return JSON.parse(localStorage.getItem('douyin_blocked_keywords') || '["店", "甄选"]');
}
loadSpeedSeconds() {
const value = parseInt(localStorage.getItem('douyin_speed_mode_seconds') || '6', 10);
return Number.isFinite(value) ? Math.min(Math.max(value, 1), 3600) : 6;
}
loadSpeedModeType() {
const mode = localStorage.getItem('douyin_speed_mode_type') || 'fixed';
return mode === 'random' ? 'random' : 'fixed';
}
loadSpeedMinSeconds() {
const value = parseInt(localStorage.getItem('douyin_speed_mode_min_seconds') || '5', 10);
return Number.isFinite(value) ? Math.min(Math.max(value, 1), 3600) : 5;
}
loadSpeedMaxSeconds() {
const value = parseInt(localStorage.getItem('douyin_speed_mode_max_seconds') || '10', 10);
return Number.isFinite(value) ? Math.min(Math.max(value, 1), 3600) : 10;
}
loadAiContent() {
return localStorage.getItem('douyin_ai_content') || '露脸的美女';
}
loadAiProvider() {
// 默认 ollama,保持向后兼容
return localStorage.getItem('douyin_ai_provider') || 'ollama';
}
loadAiModel() {
return localStorage.getItem('douyin_ai_model') || 'qwen3-vl:8b';
}
loadZhipuApiKey() {
return localStorage.getItem('douyin_zhipu_api_key') || '';
}
loadZhipuModel() {
return localStorage.getItem('douyin_zhipu_model') || 'glm-4.6v-flash';
}
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'; // 默认开启
}
loadBlockNameSetting() {
return localStorage.getItem('douyin_block_name_enabled') !== 'false'; // 默认开启
}
loadBlockDescSetting() {
return localStorage.getItem('douyin_block_desc_enabled') !== 'false'; // 默认开启
}
loadBlockTagsSetting() {
return localStorage.getItem('douyin_block_tags_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());
}
saveSpeedModeType(mode) {
this.config.speedMode.mode = mode;
localStorage.setItem('douyin_speed_mode_type', mode);
}
saveSpeedModeRange(minSeconds, maxSeconds) {
this.config.speedMode.minSeconds = minSeconds;
this.config.speedMode.maxSeconds = maxSeconds;
localStorage.setItem('douyin_speed_mode_min_seconds', minSeconds.toString());
localStorage.setItem('douyin_speed_mode_max_seconds', maxSeconds.toString());
}
saveAiContent(content) {
this.config.aiPreference.content = content;
localStorage.setItem('douyin_ai_content', content);
}
saveAiProvider(provider) {
this.config.aiPreference.provider = provider;
localStorage.setItem('douyin_ai_provider', provider);
}
saveAiModel(model) {
this.config.aiPreference.model = model;
localStorage.setItem('douyin_ai_model', model);
}
saveZhipuApiKey(apiKey) {
this.config.aiPreference.zhipuApiKey = apiKey;
localStorage.setItem('douyin_zhipu_api_key', apiKey);
}
saveZhipuModel(model) {
this.config.aiPreference.zhipuModel = model;
localStorage.setItem('douyin_zhipu_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());
}
saveBlockNameSetting(enabled) {
this.config.blockKeywords.blockName = enabled;
localStorage.setItem('douyin_block_name_enabled', enabled.toString());
}
saveBlockDescSetting(enabled) {
this.config.blockKeywords.blockDesc = enabled;
localStorage.setItem('douyin_block_desc_enabled', enabled.toString());
}
saveBlockTagsSetting(enabled) {
this.config.blockKeywords.blockTags = enabled;
localStorage.setItem('douyin_block_tags_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']:has(video[src])",
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[src]',
videoDesc: '[data-e2e="video-desc"]'
};
// ========== 视频控制器 ==========
class VideoController {
constructor(notificationManager) {
this.skipCheckInterval = null;
this.skipAttemptCount = 0;
this.notificationManager = notificationManager;
}
skip(reason) {
const tip = `跳过视频,原因:${reason}`;
if (reason) {
this.notificationManager.showMessage(tip);
}
console.log(tip);
if (!document.body) return;
const videoBefore = this.getCurrentVideoUrl();
this.sendKeyEvent('ArrowDown');
this.clearSkipCheck();
this.startSkipCheck(videoBefore);
}
like() {
this.notificationManager.showMessage('AI喜好: ❤️ 自动点赞');
this.sendKeyEvent('z', 'KeyZ', 90);
}
pressR() {
this.notificationManager.showMessage('屏蔽账号: 🚫 不感兴趣');
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 activeContainers = document.querySelectorAll(SELECTORS.activeVideo);
const lastActiveContainer = getBestVisibleElement(activeContainers);
if (!lastActiveContainer) return '';
const videoEl = lastActiveContainer.querySelector(SELECTORS.videoElement);
return videoEl?.src || '';
}
clearSkipCheck() {
if (this.skipCheckInterval) {
clearInterval(this.skipCheckInterval);
this.skipCheckInterval = null;
}
this.skipAttemptCount = 0;
}
startSkipCheck(urlBefore) {
this.skipCheckInterval = setInterval(() => {
if (this.skipAttemptCount >= 5) {
this.notificationManager.showMessage('⚠️ 跳过失败,请手动操作');
this.clearSkipCheck();
return;
}
this.skipAttemptCount++;
const urlAfter = this.getCurrentVideoUrl();
if (urlAfter && urlAfter !== urlBefore) {
console.log('视频已成功切换');
this.clearSkipCheck();
return;
}
const attemptMessage = `跳过失败,正在重试 (${this.skipAttemptCount}/5)`;
this.notificationManager.showMessage(attemptMessage, 1000);
console.log(attemptMessage);
this.sendKeyEvent('ArrowDown');
}, 500);
}
}
// ========== 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, shortcut = null) {
const btnContainer = document.createElement('xg-icon');
btnContainer.className = `xgplayer-autoplay-setting ${className}`;
const shortcutHint = shortcut
? `<div class="xgTips"><span>${text.replace(/<[^>]*>/g, '')}</span><span class="shortcutKey">${shortcut}</span></div>`
: '';
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>${shortcutHint}`;
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 showZhipuGuideDialog() {
// 移除已存在的引导弹窗
const existingGuide = document.querySelector('.zhipu-guide-dialog');
if (existingGuide) {
existingGuide.remove();
return;
}
const dialog = document.createElement('div');
dialog.className = 'zhipu-guide-dialog';
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: 25px;
border-radius: 12px;
z-index: 10002;
max-width: 420px;
max-height: 85vh;
overflow-y: auto;
text-align: left;
font-size: 14px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
`;
const stepStyle = `background: rgba(255, 255, 255, 0.05); padding: 15px; border-radius: 8px; margin-bottom: 12px; border-left: 3px solid #fe2c55;`;
const stepTitleStyle = `color: #fe2c55; font-size: 15px; font-weight: bold; margin-bottom: 8px;`;
dialog.innerHTML = `
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 24px; margin-bottom: 8px;">🔑 如何获取智谱 API Key</div>
<p style="color: #aaa; font-size: 12px; margin: 0;">免费注册,无需本地部署,即可使用 AI 视觉筛选</p>
</div>
<div style="${stepStyle}">
<div style="${stepTitleStyle}">步骤一:注册账号</div>
<div style="color: rgba(255,255,255,0.8); line-height: 1.6;">
访问 <a href="https://www.bigmodel.cn/invite?icode=GrgfvImGKwdq1i6nWogBXQZ3c5owLmCCcMQXWcJRS8E%3D" target="_blank" style="color: #fe2c55; text-decoration: underline;">智谱开放平台</a>,点击右上角「注册/登录」<br>
使用手机号或微信扫码完成注册
</div>
</div>
<div style="${stepStyle}">
<div style="${stepTitleStyle}">步骤二:获取 API Key</div>
<div style="color: rgba(255,255,255,0.8); line-height: 1.6;">
登录后进入「个人中心」→「API Keys」<br>
点击「添加新的 API Key」按钮,复制生成的 Key
</div>
</div>
<div style="background: rgba(254, 44, 85, 0.1); padding: 12px; border-radius: 8px; margin-bottom: 15px;">
<div style="color: #fe2c55; font-size: 13px; margin-bottom: 5px;">💡 推荐使用免费模型</div>
<div style="color: rgba(255,255,255,0.7); font-size: 12px; line-height: 1.5;">
<strong>GLM-4.6V-Flash</strong> - 视觉推理能力强,速度快
</div>
</div>
<div style="text-align: center;">
<button class="zhipu-guide-close" style="
background: #fe2c55;
color: white;
border: none;
padding: 10px 30px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
">我知道了</button>
</div>
`;
document.body.appendChild(dialog);
dialog.querySelector('.zhipu-guide-close').addEventListener('click', (e) => {
e.stopPropagation();
dialog.remove();
});
// 阻止弹窗内部点击事件冒泡,避免关闭设置弹窗
dialog.addEventListener('click', (e) => {
e.stopPropagation();
});
}
// 错误提示弹窗,根据服务商显示不同内容
static showErrorDialog(provider = 'ollama') {
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: 25px;
border-radius: 12px;
z-index: 10001;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
text-align: left;
font-size: 14px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
`;
if (provider === 'zhipu') {
// 智谱错误提示
dialog.innerHTML = `
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 32px; margin-bottom: 10px;">⚠️ 智谱 API 调用失败</div>
<p style="color: #aaa; font-size: 13px;">请检查以下可能的原因</p>
</div>
<div style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<div style="color: #fe2c55; font-size: 15px; margin-bottom: 10px; font-weight: bold;">常见问题排查</div>
<ul style="padding-left: 20px; margin: 0; line-height: 1.8; color: rgba(255,255,255,0.8);">
<li>检查 API Key 是否正确复制(无多余空格)</li>
<li>确认账户已完成实名认证</li>
<li>检查是否触发速率限制(免费用户并发上限为3)</li>
</ul>
</div>
<div style="text-align: center;">
<button class="zhipu-guide-btn" style="
background: transparent;
color: #fe2c55;
border: 1px solid #fe2c55;
padding: 8px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
margin-right: 10px;
">查看注册教程</button>
<button class="error-dialog-close" style="
background: #fe2c55;
color: white;
border: none;
padding: 8px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
">关闭</button>
</div>
`;
} else {
// Ollama 错误提示(原有逻辑)
const commonStyle = `background: rgba(255, 255, 255, 0.1); padding: 8px; border-radius: 4px; font-family: monospace; margin: 5px 0; display: block; user-select: text;`;
const h3Style = `color: #fe2c55; margin: 15px 0 8px 0; font-size: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 5px;`;
dialog.innerHTML = `
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 32px; margin-bottom: 10px;">⚠️ 连接失败</div>
<p style="color: #aaa; font-size: 13px;">请确保 <a href="https://ollama.com/" target="_blank" style="color: #fe2c55;">Ollama</a> 已运行并配置跨域访问</p>
</div>
<div style="background: rgba(0,0,0,0.3); padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<h3 style="${h3Style}">🖥️ Windows 配置</h3>
<ol style="padding-left: 20px; margin: 0; line-height: 1.6;">
<li>打开 <strong>控制面板</strong> -> 系统 -> 高级系统设置 -> 环境变量</li>
<li>在 <strong>用户变量</strong> 点击新建,添加两个变量:
<div style="${commonStyle}">
OLLAMA_HOST = 0.0.0.0<br>
OLLAMA_ORIGINS = *
</div>
</li>
<li>点击确定保存,重启 Ollama</li>
</ol>
<h3 style="${h3Style}">🍎 macOS 配置</h3>
<div style="margin-bottom: 5px;">打开终端运行以下命令,然后重启 Ollama:</div>
<code style="${commonStyle}">
launchctl setenv OLLAMA_HOST "0.0.0.0"<br>
launchctl setenv OLLAMA_ORIGINS "*"
</code>
<h3 style="${h3Style}">🐧 Linux (systemd) 配置</h3>
<div style="margin-bottom: 5px;">1. 编辑服务配置: <code style="background:rgba(255,255,255,0.1); px-1">sudo systemctl edit ollama.service</code></div>
<div style="margin-bottom: 5px;">2. 在 <code style="color:#aaa">[Service]</code> 下方添加:</div>
<code style="${commonStyle}">
[Service]<br>
Environment="OLLAMA_HOST=0.0.0.0"<br>
Environment="OLLAMA_ORIGINS=*"
</code>
<div style="margin-top: 5px;">3. 重启服务: <code style="background:rgba(255,255,255,0.1); px-1">sudo systemctl daemon-reload && sudo systemctl restart ollama</code></div>
</div>
<div style="text-align: center;">
<div class="error-dialog-close" style="margin-top: 10px; font-size: 14px; color: #fe2c55; cursor: pointer; text-decoration: underline;">关闭</div>
</div>
`;
}
document.body.appendChild(dialog);
// 点击关闭按钮
dialog.querySelector('.error-dialog-close').addEventListener('click', () => {
dialog.remove();
});
// 智谱错误弹窗中的"查看注册教程"按钮
const guideBtn = dialog.querySelector('.zhipu-guide-btn');
if (guideBtn) {
guideBtn.addEventListener('click', () => {
dialog.remove();
UIFactory.showZhipuGuideDialog();
});
}
// 点击弹窗外部关闭
dialog.addEventListener('click', (e) => {
if (e.target === dialog) dialog.remove();
});
}
}
// ========== UI管理器 ==========
class UIManager {
constructor(config, videoController, notificationManager) {
this.config = config;
this.videoController = videoController;
this.notificationManager = notificationManager;
this.initButtons();
}
initButtons() {
this.buttonConfigs = [
{
text: '跳直播',
className: 'skip-live-button',
configKey: 'skipLive',
shortcut: '='
},
{
text: '跳广告',
className: 'skip-ad-button',
configKey: 'skipAd'
},
{
text: '账号屏蔽',
className: 'block-account-keyword-button',
configKey: 'blockKeywords',
onClick: () => this.showKeywordDialog()
},
{
text: '最高清',
className: 'auto-high-resolution-button',
configKey: 'autoHighRes'
},
{
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.getSpeedModeLabel(),
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);
if (config.configKey === 'skipLive') {
this.notificationManager.showMessage(`功能开关: 跳过直播已 ${state ? '✅' : '❌'}`);
} else if (config.configKey === 'speedMode') {
document.dispatchEvent(new CustomEvent('douyin-speed-mode-updated'));
}
},
config.onClick,
config.shortcut
);
parent.insertBefore(button, lastButton.nextSibling);
}
const isEnabled = this.config.isEnabled(config.configKey);
const switchEl = button.querySelector('.xg-switch');
if (switchEl) {
switchEl.classList.toggle('xg-switch-checked', isEnabled);
switchEl.setAttribute('aria-checked', String(isEnabled));
}
const titleEl = button.querySelector('.xgplayer-setting-title');
if (titleEl && typeof config.text === 'string') {
titleEl.textContent = config.text;
}
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 label = this.getSpeedModeLabel();
const speedButtonConfig = this.buttonConfigs?.find(config => config.configKey === 'speedMode');
if (speedButtonConfig) {
speedButtonConfig.text = label;
}
document.querySelectorAll('.speed-mode-button .xgplayer-setting-title').forEach(el => {
el.textContent = label;
});
}
getSpeedModeLabel() {
const speedConfig = this.config.get('speedMode');
console.log('speedConfig', speedConfig)
if (speedConfig.mode === 'random') {
return `随机${speedConfig.minSeconds}-${speedConfig.maxSeconds}秒`;
}
return `${speedConfig.seconds}秒切`;
}
updateResolutionText() {
const resolution = this.config.get('onlyResolution').resolution;
const resolutionButtonConfig = this.buttonConfigs?.find(config => config.configKey === 'onlyResolution');
if (resolutionButtonConfig) {
resolutionButtonConfig.text = `${resolution}筛选`;
}
document.querySelectorAll('.resolution-filter-button .xgplayer-setting-title').forEach(el => {
el.textContent = `${resolution}筛选`;
});
}
showSpeedDialog() {
const speedConfig = this.config.get('speedMode');
const isRandom = speedConfig.mode === 'random';
const content = `
<div style="margin-bottom: 15px; color: rgba(255, 255, 255, 0.8); font-size: 13px;">
<label style="display: flex; align-items: center; margin-bottom: 8px; cursor: pointer;">
<input type="radio" name="speed-mode-type" value="fixed" ${isRandom ? '' : 'checked'}
style="margin-right: 8px;">
固定时间模式
</label>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="speed-mode-type" value="random" ${isRandom ? 'checked' : ''}
style="margin-right: 8px;">
随机时间模式
</label>
</div>
<div class="speed-fixed-wrapper" style="display: ${isRandom ? 'none' : 'block'};">
<input type="number" class="speed-input" min="1" max="3600" value="${speedConfig.seconds}"
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 class="speed-random-wrapper" style="display: ${isRandom ? 'flex' : 'none'}; gap: 10px; align-items: center;">
<input type="number" class="speed-min-input" min="1" max="3600" value="${speedConfig.minSeconds}"
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;">
<span style="color: rgba(255, 255, 255, 0.6);">—</span>
<input type="number" class="speed-max-input" min="1" max="3600" value="${speedConfig.maxSeconds}"
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;">
</div>
<div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-top: 12px;">
范围需在 1-3600 秒之间,随机模式将在区间内为每个视频生成一个等待时间
</div>
`;
const dialog = UIFactory.createDialog('speed-mode-time-dialog', '设置极速模式', content, () => {
const modeInput = dialog.querySelector('input[name="speed-mode-type"]:checked');
const mode = modeInput ? modeInput.value : 'fixed';
if (mode === 'fixed') {
const input = dialog.querySelector('.speed-input');
const value = parseInt(input.value, 10);
if (!Number.isFinite(value) || value < 1 || value > 3600) {
alert('请输入 1 - 3600 秒之间的整数');
return false;
}
this.config.saveSpeedModeType('fixed');
this.config.saveSpeedSeconds(value);
this.notificationManager.showMessage(`⚙️ 极速模式: 播放时间已设为 ${value} 秒`);
} else {
const minInput = dialog.querySelector('.speed-min-input');
const maxInput = dialog.querySelector('.speed-max-input');
const minValue = parseInt(minInput.value, 10);
const maxValue = parseInt(maxInput.value, 10);
if (!Number.isFinite(minValue) || minValue < 1 || minValue > 3600 ||
!Number.isFinite(maxValue) || maxValue < 1 || maxValue > 3600) {
alert('随机范围需在 1 - 3600 秒之间');
return false;
}
if (minValue > maxValue) {
alert('最小时间不能大于最大时间');
return false;
}
this.config.saveSpeedModeType('random');
this.config.saveSpeedModeRange(minValue, maxValue);
this.notificationManager.showMessage(`⚙️ 极速模式: 已设为随机 ${minValue}-${maxValue} 秒`);
}
this.updateSpeedModeText();
document.dispatchEvent(new CustomEvent('douyin-speed-mode-updated'));
return true;
});
if (!dialog) return;
const toggleVisibility = () => {
const modeInput = dialog.querySelector('input[name="speed-mode-type"]:checked');
const isRandomMode = modeInput && modeInput.value === 'random';
dialog.querySelector('.speed-fixed-wrapper').style.display = isRandomMode ? 'none' : 'block';
dialog.querySelector('.speed-random-wrapper').style.display = isRandomMode ? 'flex' : 'none';
};
dialog.querySelectorAll('input[name="speed-mode-type"]').forEach(radio => {
radio.addEventListener('change', toggleVisibility);
});
}
showAiPreferenceDialog() {
const aiConfig = this.config.get('aiPreference');
const currentContent = aiConfig.content;
const currentProvider = aiConfig.provider;
const currentOllamaModel = aiConfig.model;
const currentZhipuApiKey = aiConfig.zhipuApiKey;
const currentZhipuModel = aiConfig.zhipuModel;
const autoLikeEnabled = aiConfig.autoLike;
// 智谱免费模型列表
const zhipuModels = [
{ value: 'glm-4.6v-flash', label: 'GLM-4.6V-Flash (免费)', desc: '视觉推理,速度快' }
];
const isZhipuCustomModel = !zhipuModels.some(m => m.value === currentZhipuModel);
// Ollama 模型列表
const ollamaModels = ['qwen3-vl:8b', 'qwen2.5vl:7b'];
const isOllamaCustomModel = !ollamaModels.includes(currentOllamaModel);
const selectStyle = `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;`;
const inputStyle = `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;`;
const labelStyle = `color: rgba(255, 255, 255, 0.7); font-size: 12px; display: block; margin-bottom: 5px;`;
const content = `
<!-- 想看的内容 -->
<div style="margin-bottom: 15px;">
<label style="${labelStyle}">想看什么内容?(例如:露脸的美女、猫咪)</label>
<input type="text" class="ai-content-input" value="${currentContent}" placeholder="输入你想看的内容" style="${inputStyle}">
</div>
<!-- 服务商选择 -->
<div style="margin-bottom: 15px;">
<label style="${labelStyle}">AI服务商 <span style="color: #fe2c55; font-weight: bold;">✨ 新增智谱AI</span></label>
<div style="position: relative;">
<select class="ai-provider-select" style="${selectStyle}">
<option value="ollama" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentProvider === 'ollama' ? 'selected' : ''}>Ollama (本地部署)</option>
<option value="zhipu" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentProvider === 'zhipu' ? 'selected' : ''}>智谱AI (免费在线) ⭐</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>
</div>
<!-- Ollama 配置区域 -->
<div class="ollama-config-section" style="display: ${currentProvider === 'ollama' ? 'block' : 'none'}; padding: 15px; background: rgba(255, 255, 255, 0.03); border-radius: 8px; margin-bottom: 15px;">
<label style="${labelStyle}">Ollama 模型选择</label>
<div style="position: relative;">
<select class="ollama-model-select" style="${selectStyle}">
<option value="qwen3-vl:8b" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentOllamaModel === 'qwen3-vl:8b' ? 'selected' : ''}>qwen3-vl:8b (推荐)</option>
<option value="qwen2.5vl:7b" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentOllamaModel === 'qwen2.5vl:7b' ? 'selected' : ''}>qwen2.5vl:7b</option>
<option value="custom" style="background: rgba(0, 0, 0, 0.9); color: white;" ${isOllamaCustomModel ? '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="ollama-model-input" value="${isOllamaCustomModel ? currentOllamaModel : ''}" placeholder="输入自定义模型名称"
style="${inputStyle} margin-top: 10px; display: ${isOllamaCustomModel ? 'block' : 'none'};">
<div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-top: 10px;">
提示:需要安装 <a href="https://ollama.com/" target="_blank" style="color: #fe2c55;">Ollama</a> 并下载视觉模型
</div>
</div>
<!-- 智谱配置区域 -->
<div class="zhipu-config-section" style="display: ${currentProvider === 'zhipu' ? 'block' : 'none'}; padding: 15px; background: rgba(255, 255, 255, 0.03); border-radius: 8px; margin-bottom: 15px;">
<label style="${labelStyle}">API Key</label>
<input type="password" class="zhipu-apikey-input" value="${currentZhipuApiKey}" placeholder="输入智谱 API Key" style="${inputStyle}">
<div style="color: rgba(255, 255, 255, 0.5); font-size: 11px; margin-top: 8px;">
前往 <a href="https://www.bigmodel.cn/invite?icode=GrgfvImGKwdq1i6nWogBXQZ3c5owLmCCcMQXWcJRS8E%3D" target="_blank" style="color: #fe2c55; text-decoration: underline;">智谱</a> 注册获取免费 API Key,
<span class="zhipu-guide-trigger" style="color: #fe2c55; cursor: pointer; text-decoration: underline;">查看教程</span>
</div>
<label style="${labelStyle} margin-top: 15px;">模型选择</label>
<div style="position: relative;">
<select class="zhipu-model-select" style="${selectStyle}">
${zhipuModels.map(m => `<option value="${m.value}" style="background: rgba(0, 0, 0, 0.9); color: white;" ${currentZhipuModel === m.value ? 'selected' : ''}>${m.label}</option>`).join('')}
<option value="custom" style="background: rgba(0, 0, 0, 0.9); color: white;" ${isZhipuCustomModel ? '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="zhipu-model-input" value="${isZhipuCustomModel ? currentZhipuModel : ''}" placeholder="输入自定义模型名称"
style="${inputStyle} margin-top: 10px; display: ${isZhipuCustomModel ? '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>
`;
const dialog = UIFactory.createDialog('ai-preference-dialog', '设置AI喜好', content, () => {
const contentInput = dialog.querySelector('.ai-content-input');
const providerSelect = dialog.querySelector('.ai-provider-select');
const autoLikeCheckbox = dialog.querySelector('.auto-like-checkbox');
const contentValue = contentInput.value.trim();
const providerValue = providerSelect.value;
if (!contentValue) {
alert('请输入想看的内容');
return false;
}
// 根据服务商验证和保存配置
if (providerValue === 'zhipu') {
const apiKeyInput = dialog.querySelector('.zhipu-apikey-input');
const zhipuModelSelect = dialog.querySelector('.zhipu-model-select');
const zhipuModelInput = dialog.querySelector('.zhipu-model-input');
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
alert('请输入智谱 API Key\n\n👉 前往智谱开放平台免费注册获取');
UIFactory.showZhipuGuideDialog();
return false;
}
let zhipuModel = zhipuModelSelect.value === 'custom'
? zhipuModelInput.value.trim()
: zhipuModelSelect.value;
if (!zhipuModel) {
alert('请选择或输入模型名称');
return false;
}
this.config.saveZhipuApiKey(apiKey);
this.config.saveZhipuModel(zhipuModel);
} else {
const ollamaModelSelect = dialog.querySelector('.ollama-model-select');
const ollamaModelInput = dialog.querySelector('.ollama-model-input');
let ollamaModel = ollamaModelSelect.value === 'custom'
? ollamaModelInput.value.trim()
: ollamaModelSelect.value;
if (!ollamaModel) {
alert('请选择或输入模型名称');
return false;
}
this.config.saveAiModel(ollamaModel);
}
this.config.saveAiContent(contentValue);
this.config.saveAiProvider(providerValue);
this.config.saveAutoLikeSetting(autoLikeCheckbox.checked);
const providerName = providerValue === 'zhipu' ? '智谱AI' : 'Ollama';
this.notificationManager.showMessage(`🤖 AI喜好: 已切换到 ${providerName}`);
return true;
});
if (!dialog) return;
// 服务商切换事件
const providerSelect = dialog.querySelector('.ai-provider-select');
const ollamaSection = dialog.querySelector('.ollama-config-section');
const zhipuSection = dialog.querySelector('.zhipu-config-section');
providerSelect.addEventListener('change', (e) => {
const isZhipu = e.target.value === 'zhipu';
ollamaSection.style.display = isZhipu ? 'none' : 'block';
zhipuSection.style.display = isZhipu ? 'block' : 'none';
// 切换到智谱且 API Key 为空时,弹出引导
if (isZhipu) {
const apiKeyInput = dialog.querySelector('.zhipu-apikey-input');
if (!apiKeyInput.value.trim()) {
UIFactory.showZhipuGuideDialog();
}
}
});
// Ollama 模型选择切换
const ollamaModelSelect = dialog.querySelector('.ollama-model-select');
const ollamaModelInput = dialog.querySelector('.ollama-model-input');
ollamaModelSelect.addEventListener('change', (e) => {
ollamaModelInput.style.display = e.target.value === 'custom' ? 'block' : 'none';
if (e.target.value !== 'custom') ollamaModelInput.value = '';
});
// 智谱模型选择切换
const zhipuModelSelect = dialog.querySelector('.zhipu-model-select');
const zhipuModelInput = dialog.querySelector('.zhipu-model-input');
zhipuModelSelect.addEventListener('change', (e) => {
zhipuModelInput.style.display = e.target.value === 'custom' ? 'block' : 'none';
if (e.target.value !== 'custom') zhipuModelInput.value = '';
});
// 智谱引导教程触发
const guideTrigger = dialog.querySelector('.zhipu-guide-trigger');
if (guideTrigger) {
guideTrigger.addEventListener('click', (e) => {
e.stopPropagation();
UIFactory.showZhipuGuideDialog();
});
}
// 防止复选框点击时关闭弹窗
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 blockNameEnabled = this.config.get('blockKeywords').blockName;
const blockDescEnabled = this.config.get('blockKeywords').blockDesc;
const blockTagsEnabled = this.config.get('blockKeywords').blockTags;
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;">
勾选:告诉抖音你不喜欢,优化推荐算法<br>
不勾:仅跳到下一个视频
</div>
</div>
<div style="margin-bottom: 15px; padding: 10px; background: rgba(255, 255, 255, 0.05); border-radius: 6px;">
<div style="color: rgba(255, 255, 255, 0.7); font-size: 12px; margin-bottom: 8px;">检测范围:</div>
<label style="display: flex; align-items: center; cursor: pointer; color: white; font-size: 13px; margin-bottom: 6px;">
<input type="checkbox" class="block-name-checkbox" ${blockNameEnabled ? 'checked' : ''}
style="margin-right: 8px; transform: scale(1.2);">
屏蔽名称(账号昵称)
</label>
<label style="display: flex; align-items: center; cursor: pointer; color: white; font-size: 13px; margin-bottom: 6px;">
<input type="checkbox" class="block-desc-checkbox" ${blockDescEnabled ? 'checked' : ''}
style="margin-right: 8px; transform: scale(1.2);">
屏蔽简介(视频描述文案)
</label>
<label style="display: flex; align-items: center; cursor: pointer; color: white; font-size: 13px;">
<input type="checkbox" class="block-tags-checkbox" ${blockTagsEnabled ? 'checked' : ''}
style="margin-right: 8px; transform: scale(1.2);">
屏蔽标签(#话题标签)
</label>
</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');
const blockNameCheckbox = dialog.querySelector('.block-name-checkbox');
const blockDescCheckbox = dialog.querySelector('.block-desc-checkbox');
const blockTagsCheckbox = dialog.querySelector('.block-tags-checkbox');
this.config.saveKeywords(tempKeywords);
this.config.savePressRSetting(pressRCheckbox.checked);
this.config.saveBlockNameSetting(blockNameCheckbox.checked);
this.config.saveBlockDescSetting(blockDescCheckbox.checked);
this.config.saveBlockTagsSetting(blockTagsCheckbox.checked);
this.notificationManager.showMessage('🚫 屏蔽关键字: 设置已更新');
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();
});
dialog.querySelector('.block-name-checkbox').addEventListener('click', (e) => {
e.stopPropagation();
});
dialog.querySelector('.block-desc-checkbox').addEventListener('click', (e) => {
e.stopPropagation();
});
dialog.querySelector('.block-tags-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);
this.notificationManager.showMessage('💾 屏蔽账号: 关键字已导出');
};
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();
this.notificationManager.showMessage('📁 屏蔽账号: 关键字导入成功');
} 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();
this.notificationManager.showMessage(`⚙️ 分辨率筛选: 已设为 ${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);
// 显示错误提示,根据服务商类型显示不同内容
const provider = this.config.get('aiPreference').provider;
UIFactory.showErrorDialog(provider);
// 关闭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 provider = this.config.get('aiPreference').provider;
if (provider === 'zhipu') {
return await this.callZhipuAI(base64Image);
} else {
return await this.callOllamaAI(base64Image);
}
}
// Ollama 本地 API 调用
async callOllamaAI(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(`Ollama请求失败: ${response.status}`);
}
const result = await response.json();
return result.response?.trim();
}
// 智谱 API 调用
async callZhipuAI(base64Image) {
const content = this.config.get('aiPreference').content;
const zhipuModel = this.config.get('aiPreference').zhipuModel;
const apiKey = this.config.get('aiPreference').zhipuApiKey;
if (!apiKey) {
throw new Error('智谱 API Key 未配置');
}
const response = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: zhipuModel,
messages: [{
role: 'user',
content: [
{ type: 'text', text: `这是${content}吗?回答『是』或者『不是』,不要说任何多余的字符` },
{ type: 'image_url', image_url: { url: base64Image } }
]
}],
stream: false
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`智谱请求失败: ${response.status} - ${errorText}`);
}
const result = await response.json();
// 智谱返回格式: { choices: [{ message: { content: '是' } }] }
let answer = result.choices?.[0]?.message?.content?.trim() || '';
// 清理可能存在的 <think> 标签
answer = answer.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
return answer;
}
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) {
this.hasSkipped = true;
this.stopChecking = true;
this.videoController.skip(`🤖 AI筛选: 非'${content}'`);
} 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, notificationManager) {
this.config = config;
this.videoController = videoController;
this.notificationManager = notificationManager;
this.resolutionSkipped = false;
}
reset() {
this.resolutionSkipped = false;
}
checkAd(container) {
if (!this.config.isEnabled('skipAd')) return false;
const adIndicator = container.querySelector(SELECTORS.adIndicator);
if (adIndicator) {
this.videoController.skip('⏭️ 自动跳过: 广告视频');
return true;
}
return false;
}
checkBlockedAccount(container) {
if (!this.config.isEnabled('blockKeywords')) return false;
const blockConfig = this.config.get('blockKeywords');
const keywords = blockConfig.keywords;
const pressREnabled = blockConfig.pressR;
const blockName = blockConfig.blockName;
const blockDesc = blockConfig.blockDesc;
const blockTags = blockConfig.blockTags;
// 如果三个检测选项都没开启,直接返回
if (!blockName && !blockDesc && !blockTags) return false;
let matchedKeyword = null;
let matchType = '';
// 检测名称(账号昵称)
if (blockName && !matchedKeyword) {
const accountEl = container.querySelector(SELECTORS.accountName);
const accountName = accountEl?.textContent.trim();
if (accountName) {
matchedKeyword = keywords.find(kw => accountName.includes(kw));
if (matchedKeyword) matchType = '名称';
}
}
// 检测简介(视频描述文案,排除标签)
if (blockDesc && !matchedKeyword) {
const descEl = container.querySelector(SELECTORS.videoDesc);
if (descEl) {
// 获取纯文本,然后移除 #xxx 标签
const descText = descEl.textContent.replace(/#\S+/g, '').trim();
if (descText) {
matchedKeyword = keywords.find(kw => descText.includes(kw));
if (matchedKeyword) matchType = '简介';
}
}
}
// 检测标签(#话题标签)
if (blockTags && !matchedKeyword) {
const descEl = container.querySelector(SELECTORS.videoDesc);
if (descEl) {
// 提取所有 #xxx 标签
const tags = descEl.textContent.match(/#\S+/g) || [];
const tagsText = tags.join(' ');
if (tagsText) {
matchedKeyword = keywords.find(kw => tagsText.includes(kw));
if (matchedKeyword) matchType = '标签';
}
}
}
// 如果匹配到关键字,执行跳过操作
if (matchedKeyword) {
if (pressREnabled) {
// 如果开启了按R键功能,按R键(视频会直接消失)
this.videoController.pressR();
} else {
// 如果没开启R键功能,则使用下键跳过
this.videoController.skip(`🚫 屏蔽${matchType}: 关键字"${matchedKeyword}"`);
}
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) {
if (!this.resolutionSkipped) {
this.videoController.skip(`📺 分辨率筛选:非 ${targetResolution} 分辨率`);
this.resolutionSkipped = true;
}
return true;
}
const targetOption = options.find(opt => opt.text.includes(targetResolution));
if (targetOption && !targetOption.element.classList.contains("selected")) {
targetOption.element.click();
this.notificationManager.showMessage(`📺 分辨率: 已切换至 ${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();
const resolutionText = bestOption.element.textContent.trim();
this.notificationManager.showMessage(`📺 分辨率: 已切换至最高档 ${resolutionText}`);
if (bestOption.text.includes("4K")) {
this.config.setEnabled('autoHighRes', false);
UIManager.updateToggleButtons('auto-high-resolution-button', false);
this.notificationManager.showMessage("📺 分辨率: 已锁定4K,自动切换已关闭");
}
return true;
}
}
return false;
}
}
// ========== 主应用程序 ==========
class DouyinEnhancer {
constructor() {
this.notificationManager = new NotificationManager();
this.config = new ConfigManager();
this.videoController = new VideoController(this.notificationManager);
this.uiManager = new UIManager(this.config, this.videoController, this.notificationManager);
this.aiDetector = new AIDetector(this.videoController, this.config);
this.strategies = new VideoDetectionStrategies(this.config, this.videoController, this.notificationManager);
this.lastVideoUrl = '';
this.videoStartTime = 0;
this.speedModeSkipped = false;
this.lastSkippedLiveUrl = '';
this.isCurrentlySkipping = false;
this.currentSpeedDuration = null;
this.currentSpeedMode = this.config.get('speedMode').mode;
this.init();
}
init() {
this.injectStyles();
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
return;
}
if (e.key === '=') {
const isEnabled = !this.config.isEnabled('skipLive');
this.config.setEnabled('skipLive', isEnabled);
UIManager.updateToggleButtons('skip-live-button', isEnabled);
this.notificationManager.showMessage(`功能开关: 跳过直播已 ${isEnabled ? '✅' : '❌'}`);
}
});
document.addEventListener('douyin-speed-mode-updated', () => {
this.assignSpeedModeDuration(false);
this.speedModeSkipped = false;
this.videoStartTime = Date.now();
});
setInterval(() => this.mainLoop(), 300);
}
assignSpeedModeDuration(isNewVideo) {
const speedConfig = this.config.get('speedMode');
if (!this.config.isEnabled('speedMode')) {
this.currentSpeedDuration = null;
this.currentSpeedMode = speedConfig.mode;
return;
}
if (speedConfig.mode === 'random') {
const min = Math.min(speedConfig.minSeconds, speedConfig.maxSeconds);
const max = Math.max(speedConfig.minSeconds, speedConfig.maxSeconds);
const randomValue = Math.floor(Math.random() * (max - min + 1)) + min;
this.currentSpeedDuration = randomValue;
this.currentSpeedMode = 'random';
} else {
this.currentSpeedDuration = speedConfig.seconds;
this.currentSpeedMode = 'fixed';
}
}
injectStyles() {
const style = document.createElement('style');
style.innerHTML = `
/* 让右侧按钮容器高度自适应,防止按钮换行时被隐藏 */
.xg-right-grid {
height: auto !important;
max-height: none !important;
overflow: visible !important;
}
/* 确保按钮容器可以正确换行显示 */
.xg-right-grid xg-icon {
display: inline-block !important;
margin: -12px 0 !important;
}
/* 防止父容器限制高度导致内容被裁剪 */
.xgplayer-controls {
overflow: visible !important;
}
/* 让控制栏底部区域高度自适应 */
.xgplayer-controls-bottom {
height: auto !important;
min-height: 50px !important;
}
`;
document.head.appendChild(style);
}
mainLoop() {
this.uiManager.insertButtons();
const elementsWithText = Array.from(document.querySelectorAll('div,span'))
.filter(el => el.textContent.includes('进入直播间'));
const innermostElements = elementsWithText.filter(el => {
return !elementsWithText.some(otherEl => el !== otherEl && el.contains(otherEl));
});
const isLive = innermostElements.some(el => isElementInViewport(el));
if (isLive) {
this.lastVideoUrl = "直播";
if (this.config.isEnabled('skipLive')) {
if (!this.isCurrentlySkipping) {
this.videoController.skip('⏭️ 自动跳过: 直播间');
this.isCurrentlySkipping = true;
}
}
return;
}
this.isCurrentlySkipping = false;
const activeContainers = document.querySelectorAll(SELECTORS.activeVideo);
const activeContainer = getBestVisibleElement(activeContainers);
if (!activeContainer) {
return;
}
const videoEl = activeContainer.querySelector(SELECTORS.videoElement);
if (!videoEl || !videoEl.src) return;
const currentVideoUrl = videoEl.src;
if (this.handleNewVideo(currentVideoUrl)) {
return;
}
if (this.handleSpeedMode(videoEl)) {
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();
this.strategies.reset();
this.assignSpeedModeDuration(true);
console.log('===== 新视频开始 =====');
return true;
}
return false;
}
handleSpeedMode(videoEl) {
if (!this.config.isEnabled('speedMode') || this.speedModeSkipped || this.aiDetector.hasSkipped) {
return false;
}
const speedConfig = this.config.get('speedMode');
if (this.currentSpeedMode !== speedConfig.mode) {
this.assignSpeedModeDuration(false);
}
if (speedConfig.mode === 'fixed') {
if (this.currentSpeedDuration !== speedConfig.seconds) {
this.currentSpeedDuration = speedConfig.seconds;
}
} else if (speedConfig.mode === 'random') {
if (this.currentSpeedDuration === null) {
this.assignSpeedModeDuration(false);
}
}
const playbackTime = Number.isFinite(videoEl.currentTime) ? videoEl.currentTime : 0;
const targetSeconds = this.currentSpeedDuration ?? speedConfig.seconds;
if (playbackTime >= targetSeconds) {
this.speedModeSkipped = true;
this.videoController.skip(`⚡️ 极速模式: ${targetSeconds}秒已到`);
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();
})();