抖音评论监控 · 3.0版 (历史记录+导出+自动播放下一个)
// ==UserScript==
// @name 抖音评论监控 · 3.0版 (历史记录+导出+自动播放下一个)
// @namespace https://www.douyin.com/
// @version 3.0.0
// @description 监控评论关键词,右侧悬浮窗始终显示最新两条,历史记录包含评论内容,评论区加载完成后自动播放下一个视频
// @author You
// @match *://*.douyin.com/*
// @grant GM_setClipboard
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ==================== 配置管理 ====================
const STORAGE_KEY = 'dy_monitor_config_v8';
const HISTORY_KEY = 'dy_monitor_history_v8';
const DEFAULT_CONFIG = {
keywords: ['福利', '加我', '微信', 'qq', '进群', '兼职', '赚钱', '秒杀'],
regions: ['新疆', '全部'],
enableRegionFilter: true,
alertMode: 'right',
autoOpenComment: true,
autoScrollComments: true,
scrollInterval: 2000,
maxMarquees: 5,
showVideoLink: true,
showVideoInfo: true,
maxHistoryItems: 100,
maxSideAlerts: 2,
autoNextVideo: false,
nextVideoDelay: 5000,
openCommentDelay: 5000
};
class ConfigManager {
constructor() {
this.config = null;
this.load();
}
load() {
const saved = GM_getValue(STORAGE_KEY);
if (saved) {
try {
this.config = JSON.parse(saved);
// 合并默认配置,确保所有字段都存在
this.config = { ...DEFAULT_CONFIG, ...this.config };
} catch (e) {
this.config = { ...DEFAULT_CONFIG };
}
} else {
this.config = { ...DEFAULT_CONFIG };
}
return this.config;
}
save() {
GM_setValue(STORAGE_KEY, JSON.stringify(this.config));
}
get(key) {
return this.config[key];
}
set(key, value) {
this.config[key] = value;
this.save();
}
getAll() {
return { ...this.config };
}
update(updates) {
Object.assign(this.config, updates);
this.save();
}
getKeywords() {
return new Set(this.config.keywords || []);
}
getRegions() {
return new Set(this.config.regions || []);
}
setKeywords(keywordsSet) {
this.config.keywords = Array.from(keywordsSet);
this.save();
}
setRegions(regionsSet) {
this.config.regions = Array.from(regionsSet);
this.save();
}
}
// ==================== 状态管理 ====================
class StateManager {
constructor() {
this.state = {
isCommentPanelOpen: false,
commentContainer: null,
currentVideoId: null,
isWaitingForNext: false,
lastCommentCount: 0,
noIncreaseCount: 0,
lastScrollTop: 0,
scrollStableCount: 0,
isMinimized: false,
isDragging: false
};
this.triggeredComments = new Set();
this.listeners = [];
this.historyRecords = this.loadHistory();
}
loadHistory() {
try {
return GM_getValue(HISTORY_KEY, []);
} catch (e) {
return [];
}
}
saveHistory() {
try {
GM_setValue(HISTORY_KEY, this.historyRecords);
} catch (e) {
console.error('保存历史记录失败:', e);
}
}
getHistory() {
return this.historyRecords;
}
addHistoryRecord(record) {
this.historyRecords.unshift(record);
const maxItems = 100; // 从配置读取
if (this.historyRecords.length > maxItems) {
this.historyRecords = this.historyRecords.slice(0, maxItems);
}
this.saveHistory();
}
clearHistory() {
this.historyRecords = [];
this.saveHistory();
}
get(key) {
return this.state[key];
}
set(key, value) {
this.state[key] = value;
this.notifyListeners(key, value);
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notifyListeners(key, value) {
this.listeners.forEach(listener => listener(key, value));
}
reset() {
this.state = {
...this.state,
isCommentPanelOpen: false,
commentContainer: null,
isWaitingForNext: false,
lastCommentCount: 0,
noIncreaseCount: 0,
lastScrollTop: 0,
scrollStableCount: 0
};
}
isCommentTriggered(commentId) {
return this.triggeredComments.has(commentId);
}
markCommentTriggered(commentId) {
this.triggeredComments.add(commentId);
// 限制集合大小,防止内存泄漏
if (this.triggeredComments.size > 10000) {
const entries = Array.from(this.triggeredComments);
this.triggeredComments = new Set(entries.slice(-5000));
}
}
getVideoId() {
const url = window.location.href;
const match = url.match(/video\/(\d+)/);
return match ? match[1] : null;
}
}
// ==================== 工具类 ====================
class Utils {
static showToast(msg, duration = 2000, isWarning = false) {
const existing = document.querySelector('#dy-monitor-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'dy-monitor-toast';
Object.assign(toast.style, {
position: 'fixed',
left: '50%',
top: '30%',
transform: 'translateX(-50%)',
background: isWarning ? 'rgba(255, 80, 80, 0.95)' : 'rgba(0,0,0,0.8)',
color: '#fff',
padding: '10px 20px',
borderRadius: '40px',
fontSize: '15px',
fontWeight: '500',
zIndex: 99999999,
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
transition: 'opacity 0.3s ease',
pointerEvents: 'none',
maxWidth: '90%',
textAlign: 'center'
});
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, duration);
}
static formatTime(timestamp) {
if (!timestamp) return '未知时间';
let date;
if (typeof timestamp === 'number') {
date = timestamp > 10000000000 ? new Date(timestamp) : new Date(timestamp * 1000);
} else {
date = new Date(timestamp);
}
if (isNaN(date.getTime())) return '未知时间';
const pad = (n) => n.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
static generateCommentId(comment) {
const text = comment.text || comment.content || '';
const author = comment.user?.nickname || comment.user?.unique_id || comment.author || '';
const time = comment.create_time || comment.time || '';
return `${text}_${author}_${time}`.substring(0, 200);
}
static isRegionMatched(ipLocation, configManager) {
if (!configManager.get('enableRegionFilter')) return true;
if (!ipLocation || ipLocation === '未知') return false;
const regions = configManager.getRegions();
if (regions.has('全部')) return true;
for (let region of regions) {
if (region && region !== '全部' && ipLocation.includes(region)) {
return true;
}
}
return false;
}
static debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
static throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
static copyToClipboard(text) {
if (typeof GM_setClipboard !== 'undefined') {
GM_setClipboard(text, 'text');
return true;
}
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
return true;
} catch (e) {
return false;
}
}
}
// ==================== 监控核心类 ====================
class CommentMonitor {
constructor(configManager, stateManager) {
this.configManager = configManager;
this.stateManager = stateManager;
this.scrollTimer = null;
this.checkInterval = null;
this.nextVideoTimer = null;
this.openCommentTimer = null;
this.marqueeContainer = null;
this.sideAlerts = [];
this.panel = null;
this.isPanelMinimized = false;
this.isDragging = false;
this.dragOffsetX = 0;
this.dragOffsetY = 0;
this.stylesInjected = false;
}
// ====== 初始化 ======
init() {
console.log('抖音评论监控插件 v3.0 已启动');
this.injectStyles();
this.createPanel();
this.setupEventListeners();
if (this.configManager.get('autoOpenComment')) {
this.scheduleOpenComment();
}
// 定期检查视频切换
setInterval(() => {
this.checkVideoChange();
}, 2000);
// 劫持评论接口
this.hijackCommentAPIs();
}
// ====== 样式注入 ======
injectStyles() {
if (this.stylesInjected) return;
const styles = `
@keyframes sideSlideIn-right {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes sideSlideIn-left {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes marqueeFadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes panelPulse {
0%, 100% { box-shadow: 0 8px 20px rgba(0,0,0,0.5); }
50% { box-shadow: 0 8px 30px rgba(76, 175, 80, 0.3); }
}
.video-link {
color: #2196F3;
text-decoration: none;
font-size: 12px;
word-break: break-all;
}
.video-link:hover {
text-decoration: underline;
color: #4CAF50;
}
.video-title {
font-weight: 500;
color: #1a1a1a;
line-height: 1.4;
}
.keyword-tag {
background: #ff4d4d;
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
display: inline-block;
}
.video-link-dark {
color: #64B5F6;
text-decoration: none;
}
.video-link-dark:hover {
text-decoration: underline;
color: #90CAF9;
}
.dy-comment-alert {
animation-duration: 0.3s;
}
#comment-monitor-panel {
transition: width 0.3s ease, height 0.3s ease, box-shadow 0.3s ease;
}
#comment-monitor-panel.minimized {
width: 160px !important;
cursor: pointer;
}
#comment-monitor-panel .panel-content {
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.3s ease;
}
#comment-monitor-panel .panel-content.hidden {
max-height: 0 !important;
opacity: 0;
padding: 0 14px !important;
}
.dy-monitor-scrollbar::-webkit-scrollbar {
width: 4px;
}
.dy-monitor-scrollbar::-webkit-scrollbar-track {
background: #1a1a1a;
}
.dy-monitor-scrollbar::-webkit-scrollbar-thumb {
background: #4CAF50;
border-radius: 2px;
}
`;
const styleEl = document.createElement('style');
styleEl.id = 'dy-monitor-styles';
styleEl.textContent = styles;
document.head.appendChild(styleEl);
this.stylesInjected = true;
}
// ====== 视频信息获取 ======
getVideoInfo() {
const info = {
author: '未知作者',
publishTime: '未知时间',
title: '未知标题',
fullTitle: '未知标题',
videoUrl: window.location.href
};
try {
const authorElement = document.querySelector('[data-e2e="feed-video-nickname"] span, .account-name-text span');
if (authorElement) {
info.author = authorElement.textContent.trim().replace('@', '');
}
const timeElement = document.querySelector('.video-create-time .time, [class*="time"]');
if (timeElement) {
info.publishTime = timeElement.textContent.trim();
}
const titleContainer = document.querySelector('.title .FJhgcCvF, [data-e2e="video-desc"]');
if (titleContainer) {
const titleSpans = titleContainer.querySelectorAll('span');
let fullTitle = '';
titleSpans.forEach(span => {
const text = span.textContent.trim();
if (text && !text.includes('展开') && !text.includes('收起')) {
fullTitle += text + ' ';
}
});
info.fullTitle = fullTitle.trim() || titleContainer.textContent.trim();
info.title = info.fullTitle.length > 80 ? info.fullTitle.substring(0, 80) + '...' : info.fullTitle;
}
info.videoUrl = window.location.href;
} catch (e) {
console.debug('提取视频信息出错:', e);
}
return info;
}
// ====== 评论检测 ======
findCommentContainer() {
const selectors = [
'[class*="comment-list"]',
'[class*="CommentList"]',
'[class*="commentContainer"]',
'[data-e2e="comment-list"]',
'.LWSPvSJk',
'[class*="comments-container"]',
'#comment-container'
];
for (const selector of selectors) {
const el = document.querySelector(selector);
if (el && el.children.length > 0) {
return el;
}
}
return null;
}
getCommentCount() {
const container = this.stateManager.get('commentContainer');
if (!container) return 0;
const items = container.querySelectorAll([
'[class*="comment-item"]',
'[data-e2e="comment-item"]',
'[class*="CommentItem"]'
].join(','));
return items.length;
}
matchKeywords(text) {
const keywords = this.configManager.getKeywords();
if (!keywords || keywords.size === 0) return [];
const matched = [];
for (const kw of keywords) {
if (text.includes(kw)) {
matched.push(kw);
}
}
return matched;
}
// ====== 评论接口劫持 ======
hijackCommentAPIs() {
// 劫持 fetch
const originalFetch = window.fetch;
window.fetch = (...args) => {
const url = args[0] instanceof Request ? args[0].url : args[0];
return originalFetch.apply(this, args).then(response => {
const cloned = response.clone();
cloned.text().then(body => this.inspectResponseBody(url, body)).catch(() => {});
return response;
});
};
// 劫持 XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._monitorUrl = url;
return originalXHROpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(...args) {
if (this._monitorUrl) {
const url = this._monitorUrl;
const originalOnLoad = this.onload;
this.onload = function(e) {
if (this.readyState === 4 && this.status === 200) {
this.inspectResponseBody(url, this.responseText);
}
if (originalOnLoad) originalOnLoad.call(this, e);
};
// 绑定 inspectResponseBody 方法
this.inspectResponseBody = (url, body) => {
// 使用外部的 inspectResponseBody
window._dyMonitorInstance.inspectResponseBody(url, body);
};
}
return originalXHRSend.apply(this, args);
};
// 保存实例引用
window._dyMonitorInstance = this;
}
inspectResponseBody(url, bodyText) {
if (!bodyText || typeof bodyText !== 'string') return;
const commentAPIs = [
'/comment/',
'/v2/comment/',
'/aweme/v1/comment/',
'/aweme/v1/web/comment/list/'
];
if (!commentAPIs.some(api => url.includes(api))) return;
try {
const data = JSON.parse(bodyText);
let comments = [];
if (data.comments) comments = data.comments;
else if (data.data && Array.isArray(data.data)) comments = data.data;
else if (data.data && data.data.comments) comments = data.data.comments;
else if (data.comment_list) comments = data.comment_list;
else return;
comments.forEach(comment => {
this.processComment(comment);
});
} catch (e) {
// 解析失败忽略
}
}
processComment(comment) {
const commentId = Utils.generateCommentId(comment);
if (this.stateManager.isCommentTriggered(commentId)) return;
let text = comment.text || comment.content || '';
if (!text) return;
const user = comment.user?.nickname || comment.user?.unique_id || comment.author || '匿名';
const createTime = comment.create_time || comment.time || null;
let ipLabel = comment.ip_label || comment.ip_location || comment.region || '未知';
if (!ipLabel || ipLabel === '未知') {
ipLabel = comment.user?.ip_location || comment.ip || '未知';
}
const matchedKeywords = this.matchKeywords(text);
if (matchedKeywords.length > 0 && Utils.isRegionMatched(ipLabel, this.configManager)) {
this.stateManager.markCommentTriggered(commentId);
this.showAlert({
text: text,
author: user,
time: createTime,
ipLocation: ipLabel,
matchedKeywords: matchedKeywords
});
}
}
// ====== 显示提醒 ======
showAlert(commentData) {
const alertMode = this.configManager.get('alertMode');
const videoInfo = this.getVideoInfo();
// 添加到历史记录
this.addHistoryRecord(commentData, videoInfo);
// 根据模式显示
switch(alertMode) {
case 'left':
this.showSideAlert(commentData, 'left');
break;
case 'right':
this.showSideAlert(commentData, 'right');
break;
case 'marquee':
this.showMarquee(commentData);
break;
default:
this.showSideAlert(commentData, 'right');
}
// 重置自动切换计时器
if (this.configManager.get('autoNextVideo') && this.stateManager.get('isWaitingForNext')) {
this.resetNextVideoTimer();
}
}
// ====== 侧边弹窗 ======
showSideAlert(commentData, side = 'right') {
const videoInfo = this.getVideoInfo();
const { text, author, time, ipLocation, matchedKeywords } = commentData;
const overlay = document.createElement('div');
overlay.className = 'dy-comment-alert';
overlay.dataset.timestamp = Date.now();
overlay.style.cssText = `
position: fixed;
top: 20px;
${side}: 20px;
width: 380px;
max-height: 60vh;
background: #1e1e1e;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0,0,0,0.5);
color: #fff;
z-index: 999999999;
border: 1px solid #ff4d4d;
animation: sideSlideIn-${side} 0.3s ease-out;
margin-bottom: 10px;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
overflow: hidden;
display: flex;
flex-direction: column;
`;
const card = document.createElement('div');
card.style.cssText = `
padding: 16px;
overflow-y: auto;
flex: 1;
`;
// 头部
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
`;
const title = document.createElement('div');
title.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
`;
title.innerHTML = `
<span style="background:#ff4d4d; width:10px; height:10px; border-radius:50%; display:inline-block;"></span>
🔔 触发 ${matchedKeywords.length} 个关键词
`;
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
closeBtn.style.cssText = `
background: none;
border: none;
color: #aaa;
font-size: 24px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
transition: color 0.2s;
`;
closeBtn.onmouseenter = () => closeBtn.style.color = '#fff';
closeBtn.onmouseleave = () => closeBtn.style.color = '#aaa';
closeBtn.onclick = () => {
overlay.remove();
this.sideAlerts = this.sideAlerts.filter(el => el !== overlay);
};
header.appendChild(title);
header.appendChild(closeBtn);
// 视频信息
if (this.configManager.get('showVideoInfo')) {
const videoInfoDiv = document.createElement('div');
videoInfoDiv.style.cssText = `
margin-bottom: 12px;
padding: 12px;
background: #2a2a2a;
border-radius: 8px;
border-left: 3px solid #4CAF50;
`;
const authorLine = document.createElement('div');
authorLine.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
flex-wrap: wrap;
`;
authorLine.innerHTML = `
<span style="font-weight:600; color:#4CAF50;">👤 ${videoInfo.author}</span>
<span style="color:#aaa; font-size:12px;">🕒 ${videoInfo.publishTime}</span>
`;
const titleLine = document.createElement('div');
titleLine.style.cssText = `
font-size: 13px;
color: #ddd;
margin-bottom: 6px;
line-height: 1.5;
`;
titleLine.textContent = videoInfo.title;
videoInfoDiv.appendChild(authorLine);
videoInfoDiv.appendChild(titleLine);
if (this.configManager.get('showVideoLink')) {
const linkLine = document.createElement('div');
linkLine.style.cssText = `
margin-top: 8px;
font-size: 12px;
`;
linkLine.innerHTML = `
<span style="color:#aaa;">🔗</span>
<a href="${videoInfo.videoUrl}" target="_blank" class="video-link-dark" style="margin-left:4px;">打开视频</a>
`;
videoInfoDiv.appendChild(linkLine);
}
card.appendChild(videoInfoDiv);
}
// 评论内容
const commentDiv = document.createElement('div');
commentDiv.style.cssText = `
margin-bottom: 12px;
padding: 12px;
background: #2a2a2a;
border-radius: 8px;
border-left: 3px solid #ff4d4d;
`;
const metaLine = document.createElement('div');
metaLine.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 8px;
color: #aaa;
font-size: 12px;
`;
metaLine.innerHTML = `
<span>👤 ${author || '匿名'}</span>
<span>🕒 ${Utils.formatTime(time)}</span>
<span style="color: #4CAF50; font-weight: bold;">📍 ${ipLocation || '未知'}</span>
`;
const tagsContainer = document.createElement('div');
tagsContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
`;
matchedKeywords.forEach(kw => {
const tag = document.createElement('span');
tag.style.cssText = `
background: #ff4d4d;
padding: 2px 10px;
border-radius: 12px;
font-size: 11px;
color: #fff;
`;
tag.textContent = `# ${kw}`;
tagsContainer.appendChild(tag);
});
const contentDiv = document.createElement('div');
contentDiv.style.cssText = `
font-size: 13px;
line-height: 1.6;
word-break: break-all;
max-height: 100px;
overflow-y: auto;
color: #eee;
`;
contentDiv.textContent = text;
commentDiv.appendChild(metaLine);
commentDiv.appendChild(tagsContainer);
commentDiv.appendChild(contentDiv);
card.appendChild(commentDiv);
// 操作按钮
const actions = document.createElement('div');
actions.style.cssText = `
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 4px;
`;
const copyBtn = this.createActionButton('📋 复制完整信息', '#4CAF50', () => {
this.copyFullComment(commentData, videoInfo);
});
const closeCardBtn = this.createActionButton('关闭', '#ff4d4d', () => {
overlay.remove();
this.sideAlerts = this.sideAlerts.filter(el => el !== overlay);
});
actions.appendChild(copyBtn);
actions.appendChild(closeCardBtn);
card.appendChild(header);
card.appendChild(actions);
overlay.appendChild(card);
document.body.appendChild(overlay);
this.sideAlerts.push(overlay);
// 限制数量
const maxAlerts = this.configManager.get('maxSideAlerts');
while (this.sideAlerts.length > maxAlerts) {
const oldest = this.sideAlerts.shift();
if (oldest && oldest.parentNode) {
oldest.remove();
}
}
// 更新位置
this.updateSideAlertPositions(side);
// 自动清理(5分钟后)
setTimeout(() => {
if (overlay.parentNode) {
overlay.style.opacity = '0';
setTimeout(() => {
overlay.remove();
this.sideAlerts = this.sideAlerts.filter(el => el !== overlay);
}, 300);
}
}, 300000);
}
createActionButton(text, color, onClick) {
const btn = document.createElement('button');
btn.textContent = text;
btn.style.cssText = `
background: ${color};
border: none;
color: #fff;
padding: 6px 14px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
`;
btn.onmouseenter = () => {
btn.style.transform = 'scale(1.05)';
btn.style.opacity = '0.9';
};
btn.onmouseleave = () => {
btn.style.transform = 'scale(1)';
btn.style.opacity = '1';
};
btn.onclick = onClick;
return btn;
}
updateSideAlertPositions(side) {
const alerts = document.querySelectorAll('.dy-comment-alert');
let currentTop = 20;
const gap = 10;
const sorted = Array.from(alerts).sort((a, b) =>
(parseInt(a.dataset.timestamp) || 0) - (parseInt(b.dataset.timestamp) || 0)
);
// 使用 requestAnimationFrame 优化
requestAnimationFrame(() => {
sorted.forEach((el, index) => {
el.style.top = currentTop + 'px';
// 移除旧的过渡,避免闪烁
if (index === sorted.length - 1) {
el.style.transition = 'top 0.3s ease';
}
currentTop += el.offsetHeight + gap;
});
});
}
// ====== 弹幕模式 ======
showMarquee(commentData) {
const videoInfo = this.getVideoInfo();
const { text, author, time, ipLocation, matchedKeywords } = commentData;
if (!this.marqueeContainer) {
this.marqueeContainer = document.createElement('div');
this.marqueeContainer.id = 'marquee-container';
this.marqueeContainer.style.cssText = `
position: fixed;
top: 70px;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
z-index: 999999999;
pointer-events: none;
`;
document.body.appendChild(this.marqueeContainer);
}
const marquee = document.createElement('div');
marquee.className = 'dy-comment-marquee';
marquee.style.cssText = `
width: 700px;
max-width: 90%;
background: #ffffff;
border-radius: 8px;
padding: 16px 20px;
color: #333;
font-size: 14px;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
border: 1px solid #eaeaea;
pointer-events: auto;
margin-bottom: 4px;
animation: marqueeFadeIn 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.5;
`;
// 视频信息
if (this.configManager.get('showVideoInfo')) {
const videoInfoDiv = document.createElement('div');
videoInfoDiv.style.cssText = `
margin-bottom: 12px;
padding: 10px 12px;
background-color: #f5f9ff;
border-radius: 6px;
border-left: 3px solid #4CAF50;
`;
const authorLine = document.createElement('div');
authorLine.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 6px;
flex-wrap: wrap;
`;
authorLine.innerHTML = `
<span style="font-weight:600; color:#1a1a1a;">👤 ${videoInfo.author}</span>
<span style="color:#999; font-size:12px;">🕒 ${videoInfo.publishTime}</span>
`;
const titleLine = document.createElement('div');
titleLine.className = 'video-title';
titleLine.style.cssText = `
margin-bottom: 4px;
font-size: 13px;
color: #333;
`;
titleLine.innerHTML = `📝 ${videoInfo.title}`;
videoInfoDiv.appendChild(authorLine);
videoInfoDiv.appendChild(titleLine);
if (this.configManager.get('showVideoLink')) {
const linkLine = document.createElement('div');
linkLine.style.cssText = `
margin-top: 6px;
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
`;
linkLine.innerHTML = `
<span style="color:#666;">🔗</span>
<a href="${videoInfo.videoUrl}" target="_blank" class="video-link">打开视频</a>
`;
videoInfoDiv.appendChild(linkLine);
}
marquee.appendChild(videoInfoDiv);
}
// 评论信息
const commentInfoDiv = document.createElement('div');
commentInfoDiv.style.cssText = `
margin-bottom: 12px;
padding: 10px 12px;
background-color: #f8f8f8;
border-radius: 6px;
border-left: 3px solid #ff4d4d;
`;
const metaLine = document.createElement('div');
metaLine.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
flex-wrap: wrap;
`;
metaLine.innerHTML = `
<span style="font-weight:600; color:#1a1a1a;">👤 ${author || '匿名'}</span>
<span style="color:#666;">📍 ${ipLocation || '未知'}</span>
<span style="color:#999; font-size:12px;">🕒 ${Utils.formatTime(time)}</span>
`;
const keywordsBar = document.createElement('div');
keywordsBar.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
flex-wrap: wrap;
`;
matchedKeywords.forEach(kw => {
const tag = document.createElement('span');
tag.className = 'keyword-tag';
tag.textContent = `# ${kw}`;
keywordsBar.appendChild(tag);
});
const contentDiv = document.createElement('div');
contentDiv.style.cssText = `
font-size: 14px;
line-height: 1.6;
word-break: break-all;
padding: 4px 0;
color: #444;
`;
contentDiv.textContent = text;
commentInfoDiv.appendChild(metaLine);
commentInfoDiv.appendChild(keywordsBar);
commentInfoDiv.appendChild(contentDiv);
marquee.appendChild(commentInfoDiv);
// 操作按钮
const actionsBar = document.createElement('div');
actionsBar.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 8px;
`;
const copyBtn = this.createActionButton('📋 复制完整信息', '#4CAF50', () => {
this.copyFullComment(commentData, videoInfo);
});
const closeBtn = this.createActionButton('✕ 关闭', '#ff4d4d', () => {
marquee.remove();
if (this.marqueeContainer && this.marqueeContainer.children.length === 0) {
this.marqueeContainer.remove();
this.marqueeContainer = null;
}
});
actionsBar.appendChild(copyBtn);
actionsBar.appendChild(closeBtn);
marquee.appendChild(actionsBar);
// 点击弹幕复制完整信息
marquee.addEventListener('click', (e) => {
if (e.target.closest('button') || e.target.closest('a')) return;
this.copyFullComment(commentData, videoInfo);
});
this.marqueeContainer.appendChild(marquee);
// 限制数量
const maxMarquees = this.configManager.get('maxMarquees');
while (this.marqueeContainer.children.length > maxMarquees) {
this.marqueeContainer.removeChild(this.marqueeContainer.firstChild);
}
}
// ====== 复制功能 ======
copyFullComment(commentData, videoInfo) {
const { text, author, time, ipLocation, matchedKeywords } = commentData;
const timeStr = Utils.formatTime(time);
const keywordStr = matchedKeywords.length > 0 ? `[关键词: ${matchedKeywords.join('、')}]` : '';
const fullInfo = `👤 评论用户:${author || '匿名'}
📍 评论地区:${ipLocation || '未知'}
🕒 评论时间:${timeStr}
🔑 ${keywordStr}
📹 视频信息:
👤 发布者:${videoInfo.author}
🕒 发布时间:${videoInfo.publishTime}
📝 标题:${videoInfo.fullTitle}
🔗 链接:${videoInfo.videoUrl}
💬 评论内容:${text}
-----
来自抖音评论监控插件 v3.0`;
if (Utils.copyToClipboard(fullInfo)) {
Utils.showToast('✅ 已复制完整信息', 1500);
} else {
Utils.showToast('❌ 复制失败,请手动复制', 1500, true);
}
}
// ====== 历史记录 ======
addHistoryRecord(commentData, videoInfo) {
const record = {
id: `${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
timestamp: Date.now(),
date: new Date().toLocaleString(),
comment: {
text: commentData.text,
author: commentData.author,
time: commentData.time,
timeStr: Utils.formatTime(commentData.time),
ipLocation: commentData.ipLocation,
matchedKeywords: commentData.matchedKeywords
},
video: {
author: videoInfo.author,
publishTime: videoInfo.publishTime,
title: videoInfo.fullTitle,
url: videoInfo.videoUrl
}
};
this.stateManager.addHistoryRecord(record);
}
showHistoryWindow() {
const history = this.stateManager.getHistory();
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 999999999;
backdrop-filter: blur(4px);
animation: fadeIn 0.3s ease;
`;
const windowDiv = document.createElement('div');
windowDiv.style.cssText = `
width: 90%;
max-width: 1300px;
height: 80%;
background: #1e1e1e;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.5);
color: #fff;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid #333;
animation: slideUp 0.3s ease;
`;
// 标题栏
const titleBar = document.createElement('div');
titleBar.style.cssText = `
padding: 16px 20px;
border-bottom: 1px solid #333;
display: flex;
justify-content: space-between;
align-items: center;
background: #252525;
flex-shrink: 0;
`;
const title = document.createElement('h3');
title.style.cssText = `
margin: 0;
font-size: 16px;
font-weight: 500;
`;
title.innerHTML = `📋 历史记录 (${history.length})`;
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
closeBtn.style.cssText = `
background: none;
border: none;
color: #aaa;
font-size: 24px;
cursor: pointer;
padding: 0 8px;
transition: color 0.2s;
`;
closeBtn.onmouseenter = () => closeBtn.style.color = '#fff';
closeBtn.onmouseleave = () => closeBtn.style.color = '#aaa';
closeBtn.onclick = () => overlay.remove();
titleBar.appendChild(title);
titleBar.appendChild(closeBtn);
// 工具栏
const toolbar = document.createElement('div');
toolbar.style.cssText = `
padding: 12px 20px;
border-bottom: 1px solid #333;
display: flex;
gap: 10px;
background: #1a1a1a;
flex-shrink: 0;
flex-wrap: wrap;
`;
const exportBtn = this.createActionButton('📥 导出CSV', '#4CAF50', () => {
this.exportHistoryToCSV();
});
const clearBtn = this.createActionButton('🗑️ 清空记录', '#ff4d4d', () => {
if (confirm('确定要清空所有历史记录吗?')) {
this.stateManager.clearHistory();
Utils.showToast('🗑️ 历史记录已清空', 1500);
overlay.remove();
}
});
toolbar.appendChild(exportBtn);
toolbar.appendChild(clearBtn);
// 表格容器
const tableContainer = document.createElement('div');
tableContainer.style.cssText = `
flex: 1;
overflow: auto;
padding: 20px;
`;
if (history.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.style.cssText = `
text-align: center;
padding: 50px;
color: #666;
font-size: 16px;
`;
emptyMsg.textContent = '暂无历史记录';
tableContainer.appendChild(emptyMsg);
} else {
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
border-collapse: collapse;
font-size: 13px;
min-width: 1000px;
`;
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
headerRow.style.cssText = `
background: #2a2a2a;
`;
const headers = [
'序号', '视频作者', '视频标题', '视频发布时间',
'评论人', '评论时间', '评论地区', '评论内容',
'命中关键词', '视频链接'
];
headers.forEach(text => {
const th = document.createElement('th');
th.textContent = text;
th.style.cssText = `
padding: 12px 8px;
text-align: left;
border-bottom: 2px solid #444;
color: #aaa;
font-weight: 500;
position: sticky;
top: 0;
background: #2a2a2a;
`;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
history.forEach((record, index) => {
const row = document.createElement('tr');
row.style.cssText = `
border-bottom: 1px solid #333;
transition: background 0.2s;
`;
row.onmouseenter = () => row.style.background = '#2a2a2a';
row.onmouseleave = () => row.style.background = 'transparent';
const cells = [
index + 1,
record.video.author,
record.video.title,
record.video.publishTime,
record.comment.author,
record.comment.timeStr,
record.comment.ipLocation,
record.comment.text,
record.comment.matchedKeywords.join('、'),
record.video.url
];
cells.forEach((cell, i) => {
const td = document.createElement('td');
td.style.cssText = `
padding: 10px 8px;
color: ${i === 8 ? '#ffb3b3' : '#e0e0e0'};
max-width: ${i === 2 ? '200px' : (i === 7 ? '250px' : 'none')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
if (i === 9) {
const link = document.createElement('a');
link.href = cell;
link.target = '_blank';
link.textContent = '打开';
link.style.cssText = `
color: #4CAF50;
text-decoration: none;
transition: color 0.2s;
`;
link.onmouseenter = () => link.style.textDecoration = 'underline';
link.onmouseleave = () => link.style.textDecoration = 'none';
td.appendChild(link);
} else {
td.textContent = cell;
if (i === 8 && cell) {
td.style.fontWeight = '500';
}
}
row.appendChild(td);
});
tbody.appendChild(row);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
}
windowDiv.appendChild(titleBar);
windowDiv.appendChild(toolbar);
windowDiv.appendChild(tableContainer);
overlay.appendChild(windowDiv);
document.body.appendChild(overlay);
}
exportHistoryToCSV() {
const history = this.stateManager.getHistory();
if (history.length === 0) {
Utils.showToast('📭 没有历史记录可导出', 1500);
return;
}
const headers = [
'序号', '视频作者', '视频标题', '视频发布时间',
'评论人', '评论时间', '评论地区', '评论内容',
'命中关键词', '视频链接', '记录时间'
];
const rows = history.map((record, index) => {
return [
index + 1,
record.video.author,
record.video.title.replace(/,/g, ','),
record.video.publishTime,
record.comment.author,
record.comment.timeStr,
record.comment.ipLocation,
record.comment.text.replace(/,/g, ','),
record.comment.matchedKeywords.join('、'),
record.video.url,
record.date
];
});
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
].join('\n');
try {
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `抖音评论监控_${new Date().toLocaleDateString()}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
Utils.showToast('📥 历史记录已导出', 1500);
} catch (e) {
Utils.showToast('❌ 导出失败: ' + e.message, 2000, true);
}
}
// ====== 评论区控制 ======
openCommentPanel() {
if (this.stateManager.get('isCommentPanelOpen')) return;
// 模拟 X 键
const event = new KeyboardEvent('keydown', {
key: 'x',
keyCode: 88,
which: 88,
code: 'KeyX',
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
this.stateManager.set('isCommentPanelOpen', true);
Utils.showToast('💬 尝试打开评论区 (模拟X键)', 1500);
setTimeout(() => {
const commentEl = this.findCommentContainer();
if (commentEl) {
Utils.showToast('✅ 评论区已打开', 1000);
this.stateManager.set('commentContainer', commentEl);
this.stateManager.set('lastCommentCount', this.getCommentCount());
if (this.configManager.get('autoScrollComments')) {
this.startCommentScroll();
}
if (this.configManager.get('autoNextVideo')) {
this.startMonitoring();
}
} else {
this.stateManager.set('isCommentPanelOpen', false);
}
}, 2000);
}
scheduleOpenComment() {
if (this.openCommentTimer) {
clearTimeout(this.openCommentTimer);
this.openCommentTimer = null;
}
const delay = this.configManager.get('openCommentDelay');
this.openCommentTimer = setTimeout(() => {
this.openCommentTimer = null;
this.openCommentPanel();
}, delay);
Utils.showToast(`⏰ ${delay/1000}秒后打开评论区`, 1500);
}
// ====== 评论滚动 ======
startCommentScroll() {
if (this.scrollTimer) {
clearInterval(this.scrollTimer);
this.scrollTimer = null;
}
let container = this.stateManager.get('commentContainer');
if (!container) {
container = this.findCommentContainer();
this.stateManager.set('commentContainer', container);
}
if (!container) {
Utils.showToast('⚠️ 未找到评论区', 1500);
return;
}
Utils.showToast('🔄 开始滚动加载评论', 1500);
this.scrollTimer = setInterval(() => {
const currentContainer = this.stateManager.get('commentContainer');
if (currentContainer && this.configManager.get('autoScrollComments') &&
!this.stateManager.get('isWaitingForNext')) {
currentContainer.scrollTop = currentContainer.scrollHeight;
currentContainer.scrollBy(0, 200);
}
}, this.configManager.get('scrollInterval'));
}
stopCommentScroll() {
if (this.scrollTimer) {
clearInterval(this.scrollTimer);
this.scrollTimer = null;
Utils.showToast('⏸️ 停止滚动', 1000);
}
}
// ====== 自动切换视频 ======
startMonitoring() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
this.checkInterval = setInterval(() => {
if (!this.stateManager.get('isCommentPanelOpen')) return;
if (this.stateManager.get('isWaitingForNext')) return;
if (this.shouldSwitchVideo()) {
this.startNextVideoTimer();
}
}, 1500);
}
shouldSwitchVideo() {
if (!this.configManager.get('autoNextVideo')) return false;
if (!this.stateManager.get('isCommentPanelOpen')) return false;
if (this.stateManager.get('isWaitingForNext')) return false;
const container = this.stateManager.get('commentContainer');
if (!container) return false;
// 检查是否滚动到底部
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
if (scrollTop + clientHeight >= scrollHeight - 10) {
const lastScrollTop = this.stateManager.get('lastScrollTop');
const scrollStableCount = this.stateManager.get('scrollStableCount');
if (scrollTop === lastScrollTop) {
this.stateManager.set('scrollStableCount', scrollStableCount + 1);
if (scrollStableCount + 1 >= 2) return true;
} else {
this.stateManager.set('scrollStableCount', 0);
}
this.stateManager.set('lastScrollTop', scrollTop);
}
// 检查评论是否停滞
const currentCount = this.getCommentCount();
const lastCount = this.stateManager.get('lastCommentCount');
const noIncreaseCount = this.stateManager.get('noIncreaseCount');
if (currentCount === lastCount) {
this.stateManager.set('noIncreaseCount', noIncreaseCount + 1);
if (noIncreaseCount + 1 >= 3) return true;
} else {
this.stateManager.set('noIncreaseCount', 0);
}
this.stateManager.set('lastCommentCount', currentCount);
return false;
}
startNextVideoTimer() {
if (!this.configManager.get('autoNextVideo') || this.stateManager.get('isWaitingForNext')) return;
if (this.nextVideoTimer) {
clearTimeout(this.nextVideoTimer);
this.nextVideoTimer = null;
}
const delay = this.configManager.get('nextVideoDelay');
Utils.showToast(`⏰ ${delay/1000}秒后切换视频`, 2000);
this.stateManager.set('isWaitingForNext', true);
this.nextVideoTimer = setTimeout(() => {
this.nextVideoTimer = null;
this.stateManager.set('isWaitingForNext', false);
this.triggerNextVideo();
}, delay);
}
resetNextVideoTimer() {
if (!this.stateManager.get('isWaitingForNext')) return;
if (this.nextVideoTimer) {
clearTimeout(this.nextVideoTimer);
this.nextVideoTimer = null;
}
Utils.showToast('🔄 检测到新评论,重置计时器', 1500);
this.stateManager.set('isWaitingForNext', false);
this.startNextVideoTimer();
}
triggerNextVideo() {
const event = new KeyboardEvent('keydown', {
key: 'ArrowDown',
code: 'ArrowDown',
keyCode: 40,
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
Utils.showToast('⏩ 切换到下一个视频', 1000);
// 重置状态
this.stateManager.reset();
this.stateManager.set('commentContainer', null);
this.stopCommentScroll();
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
// ====== 视频切换检测 ======
checkVideoChange() {
const newId = this.stateManager.getVideoId();
const currentId = this.stateManager.get('currentVideoId');
if (newId && currentId && newId !== currentId) {
console.log('检测到视频切换');
this.stateManager.set('currentVideoId', newId);
// 重置状态
this.stateManager.reset();
this.stateManager.set('commentContainer', null);
this.stopCommentScroll();
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
if (this.nextVideoTimer) {
clearTimeout(this.nextVideoTimer);
this.nextVideoTimer = null;
}
this.stateManager.set('isWaitingForNext', false);
// 重新打开评论区
if (this.configManager.get('autoOpenComment')) {
this.scheduleOpenComment();
}
}
this.stateManager.set('currentVideoId', newId);
}
// ====== 事件监听 ======
setupEventListeners() {
// 键盘快捷键
document.addEventListener('keydown', (e) => {
// Ctrl+Shift+H 打开历史记录
if (e.ctrlKey && e.shiftKey && (e.key === 'h' || e.key === 'H')) {
e.preventDefault();
this.showHistoryWindow();
}
// Alt+X 打开评论区
if (e.altKey && (e.key === 'x' || e.key === 'X')) {
e.preventDefault();
this.openCommentPanel();
}
});
}
// ====== 控制面板 ======
createPanel() {
if (this.panel) {
this.panel.remove();
this.panel = null;
}
const isMinimized = this.isPanelMinimized;
this.panel = document.createElement('div');
this.panel.id = 'comment-monitor-panel';
this.panel.className = isMinimized ? 'minimized' : '';
this.panel.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: ${isMinimized ? '160px' : '420px'};
max-height: 80vh;
background: #1f1f1f;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0,0,0,0.5);
color: #e0e0e0;
font-size: 13px;
z-index: 9999999;
border: 1px solid #333;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
cursor: default;
user-select: none;
display: flex;
flex-direction: column;
`;
// 头部
const header = document.createElement('div');
header.id = 'panel-header';
header.style.cssText = `
padding: 10px 14px;
border-bottom: ${isMinimized ? 'none' : '1px solid #333'};
display: flex;
justify-content: space-between;
align-items: center;
background: #252525;
border-radius: 12px 12px 0 0;
cursor: move;
user-select: none;
flex-shrink: 0;
`;
const titleArea = document.createElement('div');
titleArea.style.cssText = `
display: flex;
align-items: center;
gap: 6px;
`;
const minimizeBtn = document.createElement('span');
minimizeBtn.textContent = isMinimized ? '□' : '—';
minimizeBtn.title = isMinimized ? '展开' : '最小化';
minimizeBtn.style.cssText = `
cursor: pointer;
color: #888;
font-size: 14px;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
`;
minimizeBtn.onmouseenter = () => minimizeBtn.style.color = '#fff';
minimizeBtn.onmouseleave = () => minimizeBtn.style.color = '#888';
minimizeBtn.onclick = (e) => {
e.stopPropagation();
this.isPanelMinimized = !this.isPanelMinimized;
this.createPanel();
};
const title = document.createElement('div');
title.style.cssText = `
font-weight: 500;
font-size: ${isMinimized ? '13px' : '14px'};
`;
title.innerHTML = isMinimized ? '🎯 监控' : '弹幕监控 v3.0';
const historyBtn = document.createElement('span');
historyBtn.textContent = '📋';
historyBtn.title = '查看历史记录 (Ctrl+Shift+H)';
historyBtn.style.cssText = `
cursor: pointer;
color: #4CAF50;
font-size: 14px;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
`;
historyBtn.onmouseenter = () => { historyBtn.style.background = '#333'; };
historyBtn.onmouseleave = () => { historyBtn.style.background = 'transparent'; };
historyBtn.onclick = (e) => {
e.stopPropagation();
this.showHistoryWindow();
};
const saveBtn = document.createElement('span');
saveBtn.textContent = '💾';
saveBtn.title = '保存配置';
saveBtn.style.cssText = `
cursor: pointer;
color: #4CAF50;
font-size: 14px;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.2s;
`;
saveBtn.onmouseenter = () => { saveBtn.style.background = '#333'; };
saveBtn.onmouseleave = () => { saveBtn.style.background = 'transparent'; };
saveBtn.onclick = (e) => {
e.stopPropagation();
this.configManager.save();
Utils.showToast('💾 配置已保存', 1000);
};
titleArea.appendChild(minimizeBtn);
titleArea.appendChild(title);
titleArea.appendChild(historyBtn);
titleArea.appendChild(saveBtn);
const closeBtn = document.createElement('span');
closeBtn.textContent = '×';
closeBtn.style.cssText = `
cursor: pointer;
color: #888;
font-size: 18px;
font-weight: 400;
padding: 0 4px;
border-radius: 4px;
transition: all 0.2s;
`;
closeBtn.onmouseenter = () => { closeBtn.style.background = '#333'; closeBtn.style.color = '#fff'; };
closeBtn.onmouseleave = () => { closeBtn.style.background = 'transparent'; closeBtn.style.color = '#888'; };
closeBtn.onclick = () => {
if (this.panel) {
this.panel.remove();
this.panel = null;
}
};
header.appendChild(titleArea);
header.appendChild(closeBtn);
// 拖拽
header.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
const target = e.target;
if (target === closeBtn || target === minimizeBtn ||
target === historyBtn || target === saveBtn) return;
e.preventDefault();
const rect = this.panel.getBoundingClientRect();
this.dragOffsetX = e.clientX - rect.left;
this.dragOffsetY = e.clientY - rect.top;
this.isDragging = true;
this.panel.style.transition = 'none';
this.panel.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!this.isDragging || !this.panel) return;
e.preventDefault();
const newLeft = e.clientX - this.dragOffsetX;
const newTop = e.clientY - this.dragOffsetY;
const maxX = window.innerWidth - this.panel.offsetWidth;
const maxY = window.innerHeight - this.panel.offsetHeight;
this.panel.style.left = Math.min(Math.max(0, newLeft), maxX) + 'px';
this.panel.style.top = Math.min(Math.max(0, newTop), maxY) + 'px';
this.panel.style.right = 'auto';
this.panel.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => {
if (this.isDragging && this.panel) {
this.isDragging = false;
this.panel.style.cursor = 'default';
this.panel.style.transition = 'width 0.3s ease, height 0.3s ease';
}
});
header.addEventListener('dragstart', (e) => e.preventDefault());
this.panel.appendChild(header);
// 内容区域
if (!isMinimized) {
const body = document.createElement('div');
body.className = 'panel-content dy-monitor-scrollbar';
body.style.cssText = `
padding: 14px;
overflow-y: auto;
flex: 1;
`;
this.buildPanelContent(body);
this.panel.appendChild(body);
}
document.body.appendChild(this.panel);
}
buildPanelContent(container) {
const historyCount = this.stateManager.getHistory().length;
// 状态信息
const statusSection = this.createSection('状态', `
<div style="margin-bottom:8px;font-size:12px;color:#aaa;">📋 历史记录: ${historyCount} 条</div>
<div style="display:flex;gap:8px;">
<button class="dy-panel-btn primary" data-action="openComment">打开评论区 (X)</button>
<button class="dy-panel-btn info" data-action="startScroll">开始滚动</button>
<button class="dy-panel-btn danger" data-action="stopScroll">停止滚动</button>
</div>
`);
container.appendChild(statusSection);
// 弹窗模式
const alertMode = this.configManager.get('alertMode');
const modeSection = this.createSection('🎨 弹窗模式', `
<div style="display:flex;gap:6px;">
<button class="dy-panel-btn ${alertMode === 'left' ? 'primary' : 'secondary'}" data-action="modeLeft">左侧</button>
<button class="dy-panel-btn ${alertMode === 'right' ? 'info' : 'secondary'}" data-action="modeRight">右侧</button>
<button class="dy-panel-btn ${alertMode === 'marquee' ? 'warning' : 'secondary'}" data-action="modeMarquee">弹幕</button>
</div>
`);
container.appendChild(modeSection);
// 显示数量
const maxAlerts = this.configManager.get('maxSideAlerts');
const countSection = this.createSection('📊 右侧弹窗数量', `
<div style="display:flex;align-items:center;gap:8px;">
<input type="number" id="alertCountInput" min="1" max="5" value="${maxAlerts}"
style="width:60px;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;padding:4px;color:#fff;text-align:center;">
<span style="color:#ccc;font-size:11px;">条 (1-5)</span>
<button class="dy-panel-btn primary" style="margin-left:auto;" data-action="applyCount">应用</button>
</div>
`);
container.appendChild(countSection);
// 自动播放下一个
const autoNext = this.configManager.get('autoNextVideo');
const nextDelay = this.configManager.get('nextVideoDelay');
const openDelay = this.configManager.get('openCommentDelay');
const autoSection = this.createSection('⏩ 自动播放下一个', `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<span style="color:#aaa;font-size:12px;">自动播放下一个视频</span>
<label style="position:relative;display:inline-block;width:40px;height:22px;">
<input type="checkbox" id="autoNextToggle" ${autoNext ? 'checked' : ''}
style="opacity:0;width:0;height:0;">
<span style="position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:${autoNext ? '#4CAF50' : '#555'};
transition:0.3s;border-radius:22px;">
<span style="position:absolute;content:'';height:16px;width:16px;left:3px;bottom:3px;
background:white;transition:0.3s;border-radius:50%;transform:${autoNext ? 'translateX(18px)' : 'none'};"></span>
</span>
</label>
</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span style="color:#aaa;font-size:11px;">切换延迟:</span>
<input type="number" id="nextDelayInput" min="1000" max="10000" step="500" value="${nextDelay}"
style="width:80px;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;padding:4px;color:#fff;text-align:center;">
<span style="color:#ccc;font-size:11px;">毫秒</span>
<button class="dy-panel-btn primary" style="margin-left:auto;" data-action="applyDelay">应用</button>
</div>
<div style="display:flex;align-items:center;gap:8px;padding-top:6px;border-top:1px solid #333;">
<span style="color:#aaa;font-size:11px;">打开评论区延迟:</span>
<input type="number" id="openDelayInput" min="1000" max="10000" step="500" value="${openDelay}"
style="width:80px;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;padding:4px;color:#fff;text-align:center;">
<span style="color:#ccc;font-size:11px;">毫秒</span>
<button class="dy-panel-btn info" style="margin-left:auto;" data-action="applyOpenDelay">应用</button>
</div>
`);
container.appendChild(autoSection);
// 视频信息开关
const showVideo = this.configManager.get('showVideoInfo');
const showLink = this.configManager.get('showVideoLink');
const infoSection = this.createSection('📹 显示选项', `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="color:#aaa;font-size:12px;">显示视频信息</span>
<label style="position:relative;display:inline-block;width:40px;height:22px;">
<input type="checkbox" id="showVideoInfo" ${showVideo ? 'checked' : ''}
style="opacity:0;width:0;height:0;">
<span style="position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:${showVideo ? '#4CAF50' : '#555'};
transition:0.3s;border-radius:22px;">
<span style="position:absolute;content:'';height:16px;width:16px;left:3px;bottom:3px;
background:white;transition:0.3s;border-radius:50%;transform:${showVideo ? 'translateX(18px)' : 'none'};"></span>
</span>
</label>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:#aaa;font-size:12px;">显示视频链接</span>
<label style="position:relative;display:inline-block;width:40px;height:22px;">
<input type="checkbox" id="showVideoLink" ${showLink ? 'checked' : ''}
style="opacity:0;width:0;height:0;">
<span style="position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:${showLink ? '#4CAF50' : '#555'};
transition:0.3s;border-radius:22px;">
<span style="position:absolute;content:'';height:16px;width:16px;left:3px;bottom:3px;
background:white;transition:0.3s;border-radius:50%;transform:${showLink ? 'translateX(18px)' : 'none'};"></span>
</span>
</label>
</div>
`);
container.appendChild(infoSection);
// 地区管理
const regions = this.configManager.getRegions();
const enableRegion = this.configManager.get('enableRegionFilter');
const regionSection = this.createSection(`📍 地区 (${regions.size})`, `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="color:#aaa;font-size:12px;">地区筛选</span>
<label style="display:flex;align-items:center;gap:4px;color:#4CAF50;font-size:11px;">
<input type="checkbox" id="enableRegionFilter" ${enableRegion ? 'checked' : ''}>
开启
</label>
</div>
<div id="regionList" style="margin-bottom:8px;max-height:70px;overflow-y:auto;background:#252525;border-radius:4px;padding:4px;"></div>
<div style="display:flex;gap:4px;">
<input type="text" id="regionInput" placeholder="地区"
style="flex:1;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;padding:4px 8px;color:#fff;outline:none;font-size:11px;">
<button class="dy-panel-btn primary" data-action="addRegion" style="width:24px;font-size:14px;">+</button>
</div>
`);
container.appendChild(regionSection);
// 关键词管理
const keywords = this.configManager.getKeywords();
const keywordSection = this.createSection(`🔑 关键词 (${keywords.size})`, `
<div id="keywordList" style="margin-bottom:8px;max-height:80px;overflow-y:auto;background:#252525;border-radius:4px;padding:4px;"></div>
<div style="display:flex;gap:4px;margin-bottom:8px;">
<input type="text" id="keywordInput" placeholder="添加关键词"
style="flex:1;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;padding:4px 8px;color:#fff;outline:none;font-size:11px;">
<button class="dy-panel-btn primary" data-action="addKeyword" style="width:24px;font-size:14px;">+</button>
</div>
<textarea id="batchKeywordInput" placeholder="批量添加(逗号/空格分隔)"
style="width:100%;background:#2a2a2a;border:1px solid #3a3a3a;border-radius:4px;padding:6px;color:#fff;font-size:11px;resize:vertical;min-height:40px;"></textarea>
<button class="dy-panel-btn secondary" data-action="batchAdd" style="width:100%;margin-top:4px;">批量添加</button>
`);
container.appendChild(keywordSection);
// 绑定事件
this.bindPanelEvents(container);
// 渲染列表
this.renderRegionList(container);
this.renderKeywordList(container);
}
createSection(title, content) {
const section = document.createElement('div');
section.style.cssText = `
margin-bottom: 14px;
padding: 10px;
background: #1a1a1a;
border-radius: 8px;
`;
const titleDiv = document.createElement('div');
titleDiv.style.cssText = `
margin-bottom: 8px;
color: #aaa;
font-size: 12px;
`;
titleDiv.textContent = title;
section.appendChild(titleDiv);
const contentDiv = document.createElement('div');
contentDiv.innerHTML = content;
section.appendChild(contentDiv);
return section;
}
bindPanelEvents(container) {
// 按钮事件
container.querySelectorAll('[data-action]').forEach(el => {
el.addEventListener('click', () => {
const action = el.dataset.action;
this.handlePanelAction(action, container);
});
});
// 开关事件
const autoNextToggle = container.querySelector('#autoNextToggle');
if (autoNextToggle) {
autoNextToggle.addEventListener('change', (e) => {
const checked = e.target.checked;
this.configManager.set('autoNextVideo', checked);
Utils.showToast(`⏩ 自动播放下一个: ${checked ? '开启' : '关闭'}`);
if (!checked && this.nextVideoTimer) {
clearTimeout(this.nextVideoTimer);
this.nextVideoTimer = null;
this.stateManager.set('isWaitingForNext', false);
}
if (checked && this.stateManager.get('isCommentPanelOpen')) {
this.startMonitoring();
}
});
}
const showVideoInfo = container.querySelector('#showVideoInfo');
if (showVideoInfo) {
showVideoInfo.addEventListener('change', (e) => {
this.configManager.set('showVideoInfo', e.target.checked);
Utils.showToast(`📹 视频信息: ${e.target.checked ? '显示' : '隐藏'}`);
});
}
const showVideoLink = container.querySelector('#showVideoLink');
if (showVideoLink) {
showVideoLink.addEventListener('change', (e) => {
this.configManager.set('showVideoLink', e.target.checked);
Utils.showToast(`🔗 视频链接: ${e.target.checked ? '显示' : '隐藏'}`);
});
}
const enableRegionFilter = container.querySelector('#enableRegionFilter');
if (enableRegionFilter) {
enableRegionFilter.addEventListener('change', (e) => {
this.configManager.set('enableRegionFilter', e.target.checked);
Utils.showToast(`📍 筛选: ${e.target.checked ? '开启' : '关闭'}`);
});
}
// Enter键支持
const regionInput = container.querySelector('#regionInput');
if (regionInput) {
regionInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this.handlePanelAction('addRegion', container);
}
});
}
const keywordInput = container.querySelector('#keywordInput');
if (keywordInput) {
keywordInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this.handlePanelAction('addKeyword', container);
}
});
}
}
handlePanelAction(action, container) {
switch(action) {
case 'openComment':
this.openCommentPanel();
break;
case 'startScroll':
this.startCommentScroll();
break;
case 'stopScroll':
this.stopCommentScroll();
break;
case 'modeLeft':
this.configManager.set('alertMode', 'left');
Utils.showToast('✅ 已切换到左侧弹窗', 1000);
this.createPanel();
break;
case 'modeRight':
this.configManager.set('alertMode', 'right');
Utils.showToast('✅ 已切换到右侧弹窗', 1000);
this.createPanel();
break;
case 'modeMarquee':
this.configManager.set('alertMode', 'marquee');
Utils.showToast('✅ 已切换到弹幕模式', 1000);
this.createPanel();
break;
case 'applyCount': {
const input = container.querySelector('#alertCountInput');
if (input) {
const val = parseInt(input.value);
if (val >= 1 && val <= 5) {
this.configManager.set('maxSideAlerts', val);
Utils.showToast(`✅ 右侧弹窗数量已设为 ${val}`);
} else {
Utils.showToast('❌ 请输入1-5之间的数字', 1500, true);
}
}
break;
}
case 'applyDelay': {
const input = container.querySelector('#nextDelayInput');
if (input) {
const val = parseInt(input.value);
if (val >= 1000 && val <= 10000) {
this.configManager.set('nextVideoDelay', val);
Utils.showToast(`⏱️ 切换延迟已设为 ${val}ms`);
} else {
Utils.showToast('❌ 请输入1000-10000之间的数字', 1500, true);
}
}
break;
}
case 'applyOpenDelay': {
const input = container.querySelector('#openDelayInput');
if (input) {
const val = parseInt(input.value);
if (val >= 1000 && val <= 10000) {
this.configManager.set('openCommentDelay', val);
Utils.showToast(`⏱️ 打开评论区延迟已设为 ${val}ms`);
} else {
Utils.showToast('❌ 请输入1000-10000之间的数字', 1500, true);
}
}
break;
}
case 'addRegion': {
const input = container.querySelector('#regionInput');
if (input) {
const val = input.value.trim();
if (val) {
const regions = this.configManager.getRegions();
regions.add(val);
this.configManager.setRegions(regions);
this.renderRegionList(container);
input.value = '';
Utils.showToast(`✅ 已添加: ${val}`);
this.createPanel();
}
}
break;
}
case 'addKeyword': {
const input = container.querySelector('#keywordInput');
if (input) {
const val = input.value.trim();
if (val) {
const keywords = this.configManager.getKeywords();
keywords.add(val);
this.configManager.setKeywords(keywords);
this.renderKeywordList(container);
input.value = '';
Utils.showToast(`✅ 已添加: ${val}`);
this.createPanel();
}
}
break;
}
case 'batchAdd': {
const textarea = container.querySelector('#batchKeywordInput');
if (textarea) {
const raw = textarea.value.trim();
if (raw) {
const parts = raw.split(/[,\s]+/).filter(p => p.length > 0);
const keywords = this.configManager.getKeywords();
let added = 0;
parts.forEach(p => {
if (!keywords.has(p)) {
keywords.add(p);
added++;
}
});
if (added > 0) {
this.configManager.setKeywords(keywords);
this.renderKeywordList(container);
Utils.showToast(`✅ 批量添加 ${added} 个`);
}
textarea.value = '';
this.createPanel();
}
}
break;
}
}
}
renderRegionList(container) {
const list = container.querySelector('#regionList');
if (!list) return;
const regions = this.configManager.getRegions();
list.innerHTML = '';
if (regions.size === 0) {
list.innerHTML = '<div style="padding:6px;text-align:center;color:#666;">暂无</div>';
return;
}
regions.forEach(region => {
const item = document.createElement('div');
item.style.cssText = `
display: flex;
justify-content: space-between;
padding: 4px 6px;
border-bottom: 1px solid #333;
font-size: 11px;
`;
item.innerHTML = `<span>${region}</span>`;
const delBtn = document.createElement('span');
delBtn.textContent = '✕';
delBtn.style.cssText = `
cursor: pointer;
color: #ff4d4d;
opacity: 0.6;
padding: 2px 6px;
transition: opacity 0.2s;
`;
delBtn.onmouseenter = () => delBtn.style.opacity = '1';
delBtn.onmouseleave = () => delBtn.style.opacity = '0.6';
delBtn.onclick = () => {
const regionsSet = this.configManager.getRegions();
regionsSet.delete(region);
this.configManager.setRegions(regionsSet);
this.renderRegionList(container);
Utils.showToast(`❌ 已移除: ${region}`);
this.createPanel();
};
item.appendChild(delBtn);
list.appendChild(item);
});
}
renderKeywordList(container) {
const list = container.querySelector('#keywordList');
if (!list) return;
const keywords = this.configManager.getKeywords();
list.innerHTML = '';
if (keywords.size === 0) {
list.innerHTML = '<div style="padding:6px;text-align:center;color:#666;">暂无</div>';
return;
}
keywords.forEach(kw => {
const item = document.createElement('div');
item.style.cssText = `
display: flex;
justify-content: space-between;
padding: 4px 6px;
border-bottom: 1px solid #333;
font-size: 11px;
`;
item.innerHTML = `<span>${kw}</span>`;
const delBtn = document.createElement('span');
delBtn.textContent = '✕';
delBtn.style.cssText = `
cursor: pointer;
color: #ff4d4d;
opacity: 0.6;
padding: 2px 6px;
transition: opacity 0.2s;
`;
delBtn.onmouseenter = () => delBtn.style.opacity = '1';
delBtn.onmouseleave = () => delBtn.style.opacity = '0.6';
delBtn.onclick = () => {
const keywordsSet = this.configManager.getKeywords();
keywordsSet.delete(kw);
this.configManager.setKeywords(keywordsSet);
this.renderKeywordList(container);
Utils.showToast(`❌ 已删除: ${kw}`);
this.createPanel();
};
item.appendChild(delBtn);
list.appendChild(item);
});
}
// ====== 清理资源 ======
cleanup() {
if (this.scrollTimer) {
clearInterval(this.scrollTimer);
this.scrollTimer = null;
}
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
if (this.nextVideoTimer) {
clearTimeout(this.nextVideoTimer);
this.nextVideoTimer = null;
}
if (this.openCommentTimer) {
clearTimeout(this.openCommentTimer);
this.openCommentTimer = null;
}
if (this.panel) {
this.panel.remove();
this.panel = null;
}
if (this.marqueeContainer) {
this.marqueeContainer.remove();
this.marqueeContainer = null;
}
this.sideAlerts.forEach(el => el.remove());
this.sideAlerts = [];
// 移除样式
const styles = document.querySelector('#dy-monitor-styles');
if (styles) styles.remove();
// 移除全局引用
if (window._dyMonitorInstance) {
delete window._dyMonitorInstance;
}
}
}
// ==================== 主程序入口 ====================
class Application {
constructor() {
this.configManager = null;
this.stateManager = null;
this.monitor = null;
this.initialized = false;
}
init() {
if (this.initialized) return;
try {
this.configManager = new ConfigManager();
this.stateManager = new StateManager();
this.monitor = new CommentMonitor(this.configManager, this.stateManager);
this.monitor.init();
this.initialized = true;
// 注册菜单命令
this.registerMenuCommands();
console.log('抖音评论监控插件 v3.0 初始化完成');
} catch (e) {
console.error('初始化失败:', e);
Utils.showToast('❌ 插件初始化失败: ' + e.message, 3000, true);
}
}
registerMenuCommands() {
if (typeof GM_registerMenuCommand === 'undefined') return;
GM_registerMenuCommand('📋 打开监控面板', () => {
if (this.monitor) this.monitor.createPanel();
});
GM_registerMenuCommand('📋 查看历史记录', () => {
if (this.monitor) this.monitor.showHistoryWindow();
});
GM_registerMenuCommand('📥 导出历史记录', () => {
if (this.monitor) this.monitor.exportHistoryToCSV();
});
GM_registerMenuCommand('💬 打开评论区 (X)', () => {
if (this.monitor) this.monitor.openCommentPanel();
});
GM_registerMenuCommand('▶️ 开始滚动', () => {
if (this.monitor) this.monitor.startCommentScroll();
});
GM_registerMenuCommand('⏸️ 停止滚动', () => {
if (this.monitor) this.monitor.stopCommentScroll();
});
GM_registerMenuCommand('⏩ 手动切换视频', () => {
if (this.monitor) this.monitor.triggerNextVideo();
});
GM_registerMenuCommand('💾 保存配置', () => {
if (this.configManager) {
this.configManager.save();
Utils.showToast('💾 配置已保存', 1000);
}
});
}
destroy() {
if (this.monitor) {
this.monitor.cleanup();
}
this.initialized = false;
}
}
// ==================== 启动 ====================
let app = null;
function startApp() {
if (app) {
app.destroy();
}
app = new Application();
app.init();
}
// 等待页面加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(startApp, 1500);
});
} else {
setTimeout(startApp, 1500);
}
// 页面关闭时清理
window.addEventListener('beforeunload', () => {
if (app) {
app.destroy();
app = null;
}
});
// 暴露调试接口
window.__dyMonitorDebug = {
app: () => app,
config: () => app ? app.configManager.getAll() : null,
state: () => app ? app.stateManager.state : null,
history: () => app ? app.stateManager.getHistory() : null,
monitor: () => app ? app.monitor : null
};
})();
发布时间: