在nodeloc.cc上的帖子使用,让chrome浏览器自动向下滑动刷时间
在nodeloc.cc上的帖子使用,让 chrome浏览器自动向下滑动刷时间 在Tampermonkey使用的代码，功能是开启后，让chrome浏览器自动向下滑动，模仿人在看帖子找一个回帖长的页面，如 2200多个回帖页 ，Tampermonkey自动刷时间开始，期间可以页面最小化后做其它事。3、NodeLOC控制面板【2025年7月2日更新，添加控制面板】// ==UserScript== // @name NodeLOC控制面板 // @namespace http://tampermonkey.net/ // @version v 0.3 // @description 自动向下、上滚动页面，模拟人在浏览NodeLOC上的帖子，并增加控制面板 // @author You // @match https://nodeloc.cc/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; // 配置参数 const config = { scrollInterval: 3000, // 滚动间隔（毫秒） scrollAmount: window.innerHeight * 1.8, // 每次滚动距离（像素） maxScrolls: 100 // 设置最大滚动次数,1次6个回帖左右，根据回帖多少设置 }; let scrollCount = 0; let scrollDirection = 1; // 滚动方向：1为向下，-1为向上 let isScrolling = false; // 创建控制UI function createControlUI() { // 创建控制面板元素 const controlPanel = document.createElement('div'); controlPanel.id = 'autoScrollControl'; document.body.appendChild(controlPanel); // 创建UI元素 controlPanel.innerHTML = ` 自动滚动控制 × 开始滚动 停止滚动 `; // 添加样式 GM_addStyle(` #autoScrollControl { position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: rgba(30, 30, 50, 0.9); border-radius: 10px; padding: 15px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); border: 1px solid #444; backdrop-filter: blur(5px); min-width: 250px; color: #fff; font-family: Arial, sans-serif; transition: transform 0.3s ease; } #autoScrollControl:hover { transform: translateY(-5px); } #autoScrollControl .control-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #444; } #autoScrollControl .control-title { font-size: 18px; font-weight: bold; color: #ff8a00; } #autoScrollControl .close-btn { background: none; border: none; color: #aaa; font-size: 20px; cursor: pointer; transition: color 0.3s; } #autoScrollControl .close-btn:hover { color: #fff; } #autoScrollControl .buttons { display: flex; gap: 10px; margin-top: 10px; } #autoScrollControl .btn { flex: 1; padding: 8px 15px; border: none; border-radius: 6px; font-size: 14px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } #autoScrollControl .btn-start { background: linear-gradient(to right, #22c1c3, #1a9c9e); color: white; } #autoScrollControl .btn-stop { background: linear-gradient(to right, #e52e71, #c41c5c); color: white; }`); // 获取UI元素 const startBtn = controlPanel.querySelector('#startBtn'); const stopBtn = controlPanel.querySelector('#stopBtn'); const closeBtn = controlPanel.querySelector('#closeControl'); // 按钮事件 startBtn.addEventListener('click', startAutoScroll); stopBtn.addEventListener('click', stopAutoScroll); // 关闭按钮 closeBtn.addEventListener('click', function () { controlPanel.style.display = 'none'; }); } // 开始滚动 function startAutoScroll() { if (isScrolling) return; isScrolling = true; startBtn.disabled = true; stopBtn.disabled = false; setTimeout(startAutoScrollInternal, config.scrollInterval); } function startAutoScrollInternal() { if (!isScrolling) return; if (scrollCount >= config.maxScrolls) { console.log('已达到最大滚动次数，将在45秒后反向滚动'); setTimeout(() => { scrollCount = 0; // 重置滚动计数器 scrollDirection *= -1; // 切换滚动方向 startAutoScrollInternal(); // 重新开始滚动 }, 45000); // 45秒后重启 return; } window.scrollBy({ top: config.scrollAmount * scrollDirection, left: 0, behavior: 'smooth' }); scrollCount++; console.log(`第${scrollCount}次滚动`); setTimeout(startAutoScrollInternal, config.scrollInterval); } // 停止滚动 function stopAutoScroll() { isScrolling = false; startBtn.disabled = false; stopBtn.disabled = true; } // 初始化 window.addEventListener('load', function () { createControlUI(); }); })(); 2、循环自动滚动NodeLOC【2025年6月9日更新】// ==UserScript== // @name 循环自动滚动NodeLOC // @namespace http://tampermonkey.net/ // @version 0.1 // @description 自动向下滚动页面，模拟人在浏览NodeLOC上的帖子 // @author You // @match https://nodeloc.cc/* // @grant none // ==/UserScript== (function() { 'use strict'; // 配置参数 const config = { scrollInterval: 5000, // 滚动间隔（毫秒） scrollAmount: window.innerHeight * 0.8, // 每次滚动距离（像素） maxScrolls: 60 // 最大滚动次数，10次约为30个回帖，60次约为180个回帖，找一个180个回帖以上的 }; let scrollCount = 0; let scrollDirection = 1; // 滚动方向：1为向下，-1为向上 // 开始自动滚动 function startAutoScroll() { if (scrollCount >= config.maxScrolls) { console.log('已达到最大滚动次数，将在45秒后反向滚动'); setTimeout(() => { scrollCount = 0; // 重置滚动计数器 scrollDirection *= -1; // 切换滚动方向 startAutoScroll(); // 重新开始滚动 }, 45000); // 45秒后重启 return; } window.scrollBy({ top: config.scrollAmount * scrollDirection, left: 0, behavior: 'smooth' }); scrollCount++; console.log(`第${scrollCount}次滚动`); setTimeout(startAutoScroll, config.scrollInterval); } // 初始化 setTimeout(startAutoScroll, config.scrollInterval); })();1、单向自动滚动NodeLOC【2025年6月8日】// ==UserScript== // @name 自动滚动NodeLOC // @namespace http://tampermonkey.net/ // @version 0.1 // @description 自动向下滚动页面，模拟人在浏览NodeLOC上的帖子 // @author You // @match https://nodeloc.cc/* // @grant none // ==/UserScript== (function() { 'use strict'; // 配置参数 const config = { scrollInterval: 5000, // 滚动间隔（毫秒） scrollAmount: window.innerHeight * 0.8, // 每次滚动距离（像素） maxScrolls: 200 // 最大滚动次数 }; let scrollCount = 0; // 开始自动滚动 function startAutoScroll() { if (scrollCount >= config.maxScrolls) { console.log('已达到最大滚动次数，停止自动滚动'); return; } window.scrollBy({ top: config.scrollAmount, left: 0, behavior: 'smooth' }); scrollCount++; console.log(`第${scrollCount}次滚动`); setTimeout(startAutoScroll, config.scrollInterval); } // 初始化 setTimeout(startAutoScroll, config.scrollInterval); })(); {dotted startColor="#ff6c6c" endColor="#1989fa"/}坛友做的1.自动滚动辅助器，还行，有不足// ==UserScript== // @name 自动滚动辅助器 // @namespace https://example.com/ // @version 1.6 // @description 自动向下/向上滚动页面，到达底部时自动反向滚动 // @author DeepSeek-R1 // @match *://*/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; // 配置参数 const defaultSettings = { scrollSpeed: 1, // 每次滚动的像素数 scrollInterval: 25, // 滚动间隔(ms) threshold: 100, // 底部/顶部检测阈值(px) enableUI: true // 是否显示控制UI }; // 加载用户设置 let settings = Object.assign({}, defaultSettings); if (GM_getValue('autoScrollSettings')) { settings = Object.assign(settings, GM_getValue('autoScrollSettings')); } // 状态变量 let scrollDirection = 1; // 1=向下, -1=向上 let scrollIntervalId = null; let isScrolling = false; // 创建控制UI function createControlUI() { // 创建控制面板元素 const controlPanel = document.createElement('div'); controlPanel.id = 'autoScrollControl'; document.body.appendChild(controlPanel); // 创建UI元素 controlPanel.innerHTML = ` 自动滚动控制 × 滚动速度: px/帧 ${settings.scrollSpeed} 刷新间隔: ms ${settings.scrollInterval} 开始滚动 停止滚动 `; // 添加样式 GM_addStyle(` #autoScrollControl { position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: rgba(30, 30, 50, 0.9); border-radius: 10px; padding: 15px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); border: 1px solid #444; backdrop-filter: blur(5px); min-width: 250px; color: #fff; font-family: Arial, sans-serif; transition: transform 0.3s ease; } #autoScrollControl:hover { transform: translateY(-5px); } #autoScrollControl .control-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #444; } #autoScrollControl .control-title { font-size: 18px; font-weight: bold; color: #ff8a00; } #autoScrollControl .close-btn { background: none; border: none; color: #aaa; font-size: 20px; cursor: pointer; transition: color 0.3s; } #autoScrollControl .close-btn:hover { color: #fff; } #autoScrollControl .control-group { margin-bottom: 15px; } #autoScrollControl .control-label { display: block; margin-bottom: 8px; font-size: 14px; color: #a0b3ff; } #autoScrollControl .slider-container { display: flex; align-items: center; gap: 10px; } #autoScrollControl .slider { flex: 1; height: 6px; -webkit-appearance: none; background: rgba(255, 255, 255, 0.1); outline: none; border-radius: 3px; } #autoScrollControl .slider::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: #ff8a00; cursor: pointer; box-shadow: 0 0 5px rgba(255, 138, 0, 0.7); } #autoScrollControl .value-display { min-width: 40px; text-align: center; font-size: 14px; font-weight: bold; color: #ff8a00; } #autoScrollControl .buttons { display: flex; gap: 10px; margin-top: 10px; } #autoScrollControl .btn { flex: 1; padding: 8px 15px; border: none; border-radius: 6px; font-size: 14px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } #autoScrollControl .btn-start { background: linear-gradient(to right, #22c1c3, #1a9c9e); color: white; } #autoScrollControl .btn-stop { background: linear-gradient(to right, #e52e71, #c41c5c); color: white; }`); // 获取UI元素 const speedSlider = controlPanel.querySelector('#speedSlider'); const speedDisplay = controlPanel.querySelector('#speedDisplay'); const intervalSlider = controlPanel.querySelector('#intervalSlider'); const intervalDisplay = controlPanel.querySelector('#intervalDisplay'); const startBtn = controlPanel.querySelector('#startBtn'); const stopBtn = controlPanel.querySelector('#stopBtn'); const closeBtn = controlPanel.querySelector('#closeControl'); // 滑块事件 speedSlider.addEventListener('input', function () { settings.scrollSpeed = parseInt(this.value); speedDisplay.textContent = settings.scrollSpeed; GM_setValue('autoScrollSettings', settings); if (isScrolling) { stopAutoScroll(); startAutoScroll(); } }); intervalSlider.addEventListener('input', function () { settings.scrollInterval = parseInt(this.value); intervalDisplay.textContent = settings.scrollInterval; GM_setValue('autoScrollSettings', settings); if (isScrolling) { stopAutoScroll(); startAutoScroll(); } }); // 按钮事件 startBtn.addEventListener('click', startAutoScroll); stopBtn.addEventListener('click', stopAutoScroll); // 关闭按钮 closeBtn.addEventListener('click', function () { controlPanel.style.display = 'none'; settings.enableUI = false; GM_setValue('autoScrollSettings', settings); }); } // 开始滚动 function startAutoScroll() { if (scrollIntervalId) return; isScrolling = true; scrollIntervalId = setInterval(() => { // 获取页面当前滚动位置和最大滚动位置 const currentPos = window.pageYOffset || document.documentElement.scrollTop; const maxPos = document.documentElement.scrollHeight - window.innerHeight; // 检查是否到达底部或顶部 if (scrollDirection === 1 && currentPos >= maxPos - settings.threshold) { scrollDirection = -1; // 到达底部，改为向上滚动 } else if (scrollDirection === -1 && currentPos <= settings.threshold) { scrollDirection = 1; // 到达顶部，改为向下滚动 } // 执行滚动 window.scrollBy({ top: settings.scrollSpeed * scrollDirection, behavior: 'instant' }); }, settings.scrollInterval); } // 停止滚动 function stopAutoScroll() { if (scrollIntervalId) { clearInterval(scrollIntervalId); scrollIntervalId = null; isScrolling = false; } } // 注册菜单命令 GM_registerMenuCommand('开始自动滚动', startAutoScroll); GM_registerMenuCommand('停止自动滚动', stopAutoScroll); GM_registerMenuCommand('显示控制面板', function () { document.getElementById('autoScrollControl').style.display = 'block'; settings.enableUI = true; GM_setValue('autoScrollSettings', settings); }); // 初始化 window.addEventListener('load', function () { createControlUI(); if (!settings.enableUI) { document.getElementById('autoScrollControl').style.display = 'none'; } }); })(); 坛友做的2.智能助手， 功能多用不上，电脑配置低卡，不推荐// ==UserScript== // @name 智能论坛助手 Pro // @namespace http://tampermonkey.net/ // @version 2.6.0 // @description NodeLoc智能论坛助手 - 自动阅读/点赞/回复，升级进度追踪，弹窗检测，限制监控，数据统计，位置记忆等全方位功能 // @author Enhanced by AI // @match https://meta.discourse.org/* // @match https://meta.appinn.net/* // @match https://community.openai.com/* // @match https://nodeloc.cc/* // @match https://bbs.tampermonkey.net.cn/* // @match https://greasyfork.org/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_notification // @grant GM_openInTab // @grant GM_xmlhttpRequest // @grant unsafeWindow // @license MIT // @icon https://www.google.com/s2/favicons?domain=discourse.org // @downloadURL https://update.greasyfork.org/scripts/enhanced-forum-assistant/Enhanced%20Forum%20Assistant.user.js // @updateURL https://update.greasyfork.org/scripts/enhanced-forum-assistant/Enhanced%20Forum%20Assistant.meta.js // ==/UserScript== (function () { "use strict"; // ===== 配置管理类 ===== class ConfigManager { constructor() { this.defaultConfig = { // 基础配置 possibleBaseURLs: [ "https://linux.do", "https://meta.discourse.org", "https://meta.appinn.net", "https://community.openai.com", "https://nodeloc.cc", "https://bbs.tampermonkey.net.cn", "https://greasyfork.org" ], // 行为参数 commentLimit: 1000, topicListLimit: 100, likeLimit: 55, stuckTimeout: 15000, minScrollDelta: 50, maxIdleTime: 60000, maxLogEntries: 200, minTopicChangeDelay: 3000, maxTopicChangeDelay: 8000, minReadTimeLower: 45000, minReadTimeUpper: 120000, fetchRetryDelay: 30000, // 滚动参数 scrollSegmentDistanceMin: 300, scrollSegmentDistanceMax: 1000, scrollSegmentDurationMin: 2000, scrollSegmentDurationMax: 6000, randomPauseProbability: 0.2, randomPauseDurationMin: 100, randomPauseDurationMax: 800, // 贝塞尔曲线参数 bezierP1Min: 0.1, bezierP1Max: 0.4, bezierP2Min: 0.6, bezierP2Max: 0.9, // 新增功能配置 enableMouseSimulation: true, enableAdvancedBehavior: true, enableDataAnalysis: true, enableSafetyFeatures: true, autoReplyEnabled: false, keywordMonitoring: false, proxyEnabled: false, // UI配置 theme: 'dark', language: 'zh-CN', showStatistics: true, compactMode: false }; this.config = this.loadConfig(); } loadConfig() { try { const saved = GM_getValue('forumAssistantConfig', '{}'); return { ...this.defaultConfig, ...JSON.parse(saved) }; } catch (e) { console.warn('配置加载失败，使用默认配置:', e); return { ...this.defaultConfig }; } } saveConfig() { try { GM_setValue('forumAssistantConfig', JSON.stringify(this.config)); return true; } catch (e) { console.error('配置保存失败:', e); return false; } } get(key) { return this.config[key]; } set(key, value) { this.config[key] = value; this.saveConfig(); } reset() { this.config = { ...this.defaultConfig }; this.saveConfig(); } exportConfig() { return JSON.stringify(this.config, null, 2); } importConfig(configStr) { try { const imported = JSON.parse(configStr); this.config = { ...this.defaultConfig, ...imported }; this.saveConfig(); return true; } catch (e) { console.error('配置导入失败:', e); return false; } } } // ===== 工具类 ===== class Utils { static getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } static getRandomFloat(min, max) { return Math.random() * (max - min) + min; } static sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 安全的DOM元素创建 static safeCreateElement(tagName, options = {}) { try { const element = document.createElement(tagName); if (!element) { console.error(`无法创建 ${tagName} 元素`); return null; } // 设置属性 if (options.id) element.id = options.id; if (options.className) element.className = options.className; if (options.textContent) element.textContent = options.textContent; if (options.innerHTML) element.innerHTML = options.innerHTML; // 设置样式 if (options.style && typeof options.style === 'object') { Object.assign(element.style, options.style); } return element; } catch (e) { console.error(`创建 ${tagName} 元素失败:`, e); return null; } } // 安全的DOM元素查找 static safeQuerySelector(selector, parent = document) { try { if (!parent || typeof parent.querySelector !== 'function') { console.warn('无效的父元素'); return null; } return parent.querySelector(selector); } catch (e) { console.error(`查找元素失败 (${selector}):`, e); return null; } } // 安全的DOM元素添加 static safeAppendChild(parent, child) { try { if (!parent || !child) { console.warn('父元素或子元素为空'); return false; } if (typeof parent.appendChild !== 'function') { console.warn('父元素不支持appendChild'); return false; } parent.appendChild(child); return true; } catch (e) { console.error('添加子元素失败:', e); return false; } } // 安全的样式设置 static safeSetStyle(element, property, value) { try { if (!element || !element.style) { console.warn('元素或样式对象不存在'); return false; } element.style[property] = value; return true; } catch (e) { console.error(`设置样式失败 (${property}):`, e); return false; } } // 安全的内容设置 static safeSetContent(element, content, useInnerHTML = false) { try { if (!element) { console.warn('元素不存在'); return false; } if (useInnerHTML) { element.innerHTML = content; } else { element.textContent = content; } return true; } catch (e) { console.error('设置内容失败:', e); return false; } } static formatTime(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } static formatNumber(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); } 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() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } static generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } static detectForumType(url) { if (url.includes('discourse')) return 'discourse'; if (url.includes('linux.do')) return 'discourse'; if (url.includes('nodeloc.cc')) return 'discourse'; if (url.includes('greasyfork.org')) return 'greasyfork'; return 'unknown'; } static cubicBezier(t, p0, p1, p2, p3) { const u = 1 - t; const tt = t * t; const uu = u * u; const uuu = uu * u; const ttt = tt * t; return uuu * p0 + 3 * uu * t * p1 + 3 * u * tt * p2 + ttt * p3; } } // ===== 日志管理类 ===== class LogManager { constructor(config) { this.config = config; this.entries = []; this.logWindow = null; this.logContent = null; // 从存储中恢复统计数据，如果不存在则使用默认值 const savedStats = GM_getValue('statistics', null); if (savedStats) { this.statistics = { totalActions: savedStats.totalActions || 0, totalReadTime: savedStats.totalReadTime || 0, topicsRead: savedStats.topicsRead || 0, likesGiven: savedStats.likesGiven || 0, errorsCount: savedStats.errorsCount || 0, startTime: savedStats.startTime || Date.now() }; } else { this.statistics = { totalActions: 0, totalReadTime: 0, topicsRead: 0, likesGiven: 0, errorsCount: 0, startTime: Date.now() }; } } createLogWindow() { if (this.logWindow) return; const isDark = this.config.get('theme') === 'dark'; const isCompact = this.config.get('compactMode'); this.logWindow = Utils.safeCreateElement("div", { id: "forum-assistant-log", style: { position: "fixed", top: "10px", right: "10px", width: isCompact ? "280px" : "350px", maxHeight: isCompact ? "300px" : "500px", backgroundColor: isDark ? "#2d3748" : "#fff", color: isDark ? "#e2e8f0" : "#2d3748", border: `1px solid ${isDark ? "#4a5568" : "#e2e8f0"}`, borderRadius: "8px", boxShadow: "0 4px 12px rgba(0,0,0,0.15)", zIndex: "10000", display: "flex", flexDirection: "column", fontFamily: "'Consolas', 'Monaco', monospace", fontSize: isCompact ? "11px" : "12px", backdropFilter: "blur(10px)", resize: "both", overflow: "hidden" } }); if (!this.logWindow) { console.error('无法创建日志窗口'); return; } this.createLogHeader(isDark); this.createLogContent(isDark); this.createLogControls(isDark); if (!Utils.safeAppendChild(document.body, this.logWindow)) { console.error('无法添加日志窗口到页面'); return; } this.makeResizable(); } createLogHeader(isDark) { const header = document.createElement("div"); Object.assign(header.style, { backgroundColor: isDark ? "#4a5568" : "#f7fafc", padding: "8px 12px", borderBottom: `1px solid ${isDark ? "#718096" : "#e2e8f0"}`, fontWeight: "bold", display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "move" }); const title = document.createElement("span"); title.textContent = "智能论坛助手 Pro"; const controls = document.createElement("div"); controls.style.display = "flex"; controls.style.gap = "5px"; // 最小化按钮 const minimizeBtn = this.createControlButton("−", () => this.toggleMinimize()); // 关闭按钮 const closeBtn = this.createControlButton("×", () => this.toggleVisibility()); controls.appendChild(minimizeBtn); controls.appendChild(closeBtn); header.appendChild(title); header.appendChild(controls); this.logWindow.appendChild(header); // 使窗口可拖拽 this.makeDraggable(header); } createLogContent(isDark) { this.logContent = Utils.safeCreateElement("pre", { style: { margin: "0", padding: "10px", overflowY: "auto", flex: "1", fontSize: "inherit", lineHeight: "1.4", whiteSpace: "pre-wrap", wordBreak: "break-word" } }); if (this.logContent && this.logWindow) { Utils.safeAppendChild(this.logWindow, this.logContent); } } createLogControls(isDark) { const controls = document.createElement("div"); Object.assign(controls.style, { padding: "8px", borderTop: `1px solid ${isDark ? "#718096" : "#e2e8f0"}`, display: "flex", gap: "5px", flexWrap: "wrap" }); const clearBtn = this.createActionButton("清空", () => this.clear()); const exportBtn = this.createActionButton("导出", () => this.exportLogs()); const statsBtn = this.createActionButton("统计", () => this.showStatistics()); controls.appendChild(clearBtn); controls.appendChild(exportBtn); controls.appendChild(statsBtn); this.logWindow.appendChild(controls); } createControlButton(text, onClick) { const btn = document.createElement("button"); btn.textContent = text; Object.assign(btn.style, { background: "none", border: "none", color: "inherit", cursor: "pointer", padding: "2px 6px", borderRadius: "3px", fontSize: "14px", fontWeight: "bold" }); btn.addEventListener("click", onClick); btn.addEventListener("mouseenter", () => { btn.style.backgroundColor = "rgba(255,255,255,0.1)"; }); btn.addEventListener("mouseleave", () => { btn.style.backgroundColor = "transparent"; }); return btn; } createActionButton(text, onClick) { const btn = document.createElement("button"); btn.textContent = text; Object.assign(btn.style, { padding: "4px 8px", border: "1px solid currentColor", backgroundColor: "transparent", color: "inherit", cursor: "pointer", borderRadius: "4px", fontSize: "11px" }); btn.addEventListener("click", onClick); return btn; } makeDraggable(header) { if (!header || !this.logWindow) return; let isDragging = false; let currentX, currentY, initialX, initialY; header.addEventListener("mousedown", (e) => { if (!this.logWindow) return; isDragging = true; initialX = e.clientX - this.logWindow.offsetLeft; initialY = e.clientY - this.logWindow.offsetTop; }); document.addEventListener("mousemove", (e) => { if (isDragging && this.logWindow) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; this.logWindow.style.left = currentX + "px"; this.logWindow.style.top = currentY + "px"; this.logWindow.style.right = "auto"; } }); document.addEventListener("mouseup", () => { isDragging = false; }); } makeResizable() { if (!this.logWindow) return; // 简单的调整大小功能 const resizer = document.createElement("div"); Object.assign(resizer.style, { position: "absolute", bottom: "0", right: "0", width: "10px", height: "10px", cursor: "se-resize", backgroundColor: "rgba(128,128,128,0.3)" }); this.logWindow.appendChild(resizer); let isResizing = false; resizer.addEventListener("mousedown", (e) => { isResizing = true; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (isResizing && this.logWindow) { const rect = this.logWindow.getBoundingClientRect(); const newWidth = e.clientX - rect.left; const newHeight = e.clientY - rect.top; if (newWidth > 200) this.logWindow.style.width = newWidth + "px"; if (newHeight > 150) this.logWindow.style.height = newHeight + "px"; } }); document.addEventListener("mouseup", () => { isResizing = false; }); } log(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const entry = { timestamp, message, type, id: Utils.generateUUID() }; this.entries.push(entry); this.statistics.totalActions++; if (type === 'error') this.statistics.errorsCount++; // 限制日志条目数量 const maxEntries = this.config.get('maxLogEntries'); if (this.entries.length > maxEntries) { this.entries = this.entries.slice(-maxEntries); } this.updateLogDisplay(); // 控制台输出 const consoleMethod = type === 'error' ? 'error' : type === 'warn' ? 'warn' : 'log'; console[consoleMethod](`[论坛助手] [${timestamp}] ${message}`); // 重要消息通知 if (type === 'error' || (type === 'info' && message.includes('完成'))) { this.showNotification(message, type); } } updateLogDisplay() { if (!this.logContent) { console.warn('日志内容元素不存在'); return; } try { const displayEntries = this.entries.map(entry => { const typeIcon = this.getTypeIcon(entry.type); return `${typeIcon} [${entry.timestamp}] ${entry.message}`; }); this.logContent.textContent = displayEntries.join('\n'); this.logContent.scrollTop = this.logContent.scrollHeight; } catch (e) { console.error('更新日志显示失败:', e); } } getTypeIcon(type) { const icons = { info: '📘', warn: '⚠️', error: '❌', success: '✅', action: '🔄' }; return icons[type] || '📘'; } showNotification(message, type) { if (typeof GM_notification !== 'undefined') { GM_notification({ text: message, title: '智能论坛助手', timeout: 3000, onclick: () => { window.focus(); this.logWindow?.scrollIntoView(); } }); } } clear() { this.entries = []; this.updateLogDisplay(); this.log('日志已清空', 'action'); } exportLogs() { const exportData = { timestamp: new Date().toISOString(), statistics: this.statistics, logs: this.entries, config: this.config.config }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `forum-assistant-logs-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.log('日志已导出', 'success'); } showStatistics() { const runtime = Date.now() - this.statistics.startTime; const stats = ` 📊 运行统计 ━━━━━━━━━━━━━━━━━━━━ ⏱️ 运行时间: ${Utils.formatTime(runtime)} 📖 已读话题: ${this.statistics.topicsRead} 👍 点赞数量: ${this.statistics.likesGiven} 🔄 总操作数: ${this.statistics.totalActions} ❌ 错误次数: ${this.statistics.errorsCount} 📈 效率: ${(this.statistics.topicsRead / (runtime / 3600000)).toFixed(2)} 话题/小时 ━━━━━━━━━━━━━━━━━━━━`; alert(stats); } toggleMinimize() { if (!this.logWindow) return; const content = this.logWindow.querySelector('pre'); const controls = this.logWindow.querySelector('div:last-child'); if (!content || !controls) return; if (content.style.display === 'none') { content.style.display = 'block'; controls.style.display = 'flex'; this.logWindow.style.height = 'auto'; } else { content.style.display = 'none'; controls.style.display = 'none'; this.logWindow.style.height = 'auto'; } } toggleVisibility() { if (!this.logWindow) return; this.logWindow.style.display = this.logWindow.style.display === 'none' ? 'flex' : 'none'; } updateStatistics(key, value = 1) { if (this.statistics.hasOwnProperty(key)) { this.statistics[key] += value; // 保存统计数据到存储 GM_setValue('statistics', this.statistics); } } // 重置统计数据 resetStatistics() { this.statistics = { totalActions: 0, totalReadTime: 0, topicsRead: 0, likesGiven: 0, errorsCount: 0, startTime: Date.now() }; GM_setValue('statistics', this.statistics); this.log('统计数据已重置', 'info'); } } // ===== 配置面板类 ===== class ConfigPanel { constructor(config, logger) { this.config = config; this.logger = logger; this.panel = null; this.isVisible = false; } create() { if (this.panel) return; const isDark = this.config.get('theme') === 'dark'; this.panel = document.createElement('div'); this.panel.id = 'forum-assistant-config'; Object.assign(this.panel.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '600px', maxHeight: '80vh', backgroundColor: isDark ? '#2d3748' : '#ffffff', color: isDark ? '#e2e8f0' : '#2d3748', border: `1px solid ${isDark ? '#4a5568' : '#e2e8f0'}`, borderRadius: '12px', boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', zIndex: '10001', display: 'none', flexDirection: 'column', fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif", backdropFilter: 'blur(10px)' }); this.createHeader(); this.createContent(); this.createFooter(); document.body.appendChild(this.panel); this.createOverlay(); } createHeader() { const header = document.createElement('div'); Object.assign(header.style, { padding: '20px', borderBottom: `1px solid ${this.config.get('theme') === 'dark' ? '#4a5568' : '#e2e8f0'}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }); const title = document.createElement('h2'); title.textContent = '🛠️ 高级配置'; title.style.margin = '0'; title.style.fontSize = '20px'; title.style.fontWeight = '600'; const closeBtn = document.createElement('button'); closeBtn.innerHTML = '✕'; Object.assign(closeBtn.style, { background: 'none', border: 'none', fontSize: '18px', cursor: 'pointer', color: 'inherit', padding: '5px', borderRadius: '4px' }); closeBtn.addEventListener('click', () => this.hide()); header.appendChild(title); header.appendChild(closeBtn); this.panel.appendChild(header); } createContent() { const content = document.createElement('div'); Object.assign(content.style, { padding: '20px', overflowY: 'auto', flex: '1' }); // 创建标签页 const tabs = this.createTabs(); content.appendChild(tabs); // 创建配置区域 const configArea = document.createElement('div'); configArea.id = 'config-area'; content.appendChild(configArea); this.panel.appendChild(content); // 默认显示基础配置 this.showBasicConfig(); } createTabs() { const tabContainer = document.createElement('div'); Object.assign(tabContainer.style, { display: 'flex', marginBottom: '20px', borderBottom: `1px solid ${this.config.get('theme') === 'dark' ? '#4a5568' : '#e2e8f0'}` }); const tabs = [ { id: 'basic', name: '基础设置', icon: '⚙️' }, { id: 'behavior', name: '行为配置', icon: '🤖' }, { id: 'advanced', name: '高级功能', icon: '🚀' }, { id: 'ui', name: '界面设置', icon: '🎨' }, { id: 'data', name: '数据管理', icon: '📊' } ]; tabs.forEach(tab => { const tabBtn = document.createElement('button'); tabBtn.innerHTML = `${tab.icon} ${tab.name}`; Object.assign(tabBtn.style, { padding: '10px 15px', border: 'none', background: 'none', color: 'inherit', cursor: 'pointer', borderBottom: '2px solid transparent', fontSize: '14px', fontWeight: '500' }); tabBtn.addEventListener('click', () => { // 移除所有活动状态 tabContainer.querySelectorAll('button').forEach(btn => { btn.style.borderBottomColor = 'transparent'; btn.style.opacity = '0.7'; }); // 设置当前活动状态 tabBtn.style.borderBottomColor = '#3182ce'; tabBtn.style.opacity = '1'; // 显示对应配置 this.showConfigSection(tab.id); }); tabContainer.appendChild(tabBtn); }); // 默认激活第一个标签 tabContainer.firstChild.click(); return tabContainer; } showConfigSection(sectionId) { const configArea = document.getElementById('config-area'); if (!configArea) { console.error('配置区域元素未找到'); return; } configArea.innerHTML = ''; switch (sectionId) { case 'basic': this.showBasicConfig(); break; case 'behavior': this.showBehaviorConfig(); break; case 'advanced': this.showAdvancedConfig(); break; case 'ui': this.showUIConfig(); break; case 'data': this.showDataConfig(); break; } } showBasicConfig() { const configArea = document.getElementById('config-area'); if (!configArea) { console.error('配置区域元素未找到 (showBasicConfig)'); return; } const basicConfigs = [ { key: 'commentLimit', label: '评论数限制', type: 'number', min: 100, max: 5000, step: 100, desc: '跳过评论数超过此值的帖子' }, { key: 'topicListLimit', label: '话题缓存数量', type: 'number', min: 50, max: 500, step: 50, desc: '一次获取并缓存的话题数量' }, { key: 'likeLimit', label: '每日点赞限制', type: 'number', min: 10, max: 200, step: 5, desc: '每日自动点赞的最大次数' }, { key: 'minReadTimeLower', label: '最小阅读时间(秒)', type: 'number', min: 30, max: 300, step: 5, desc: '每个帖子的最小阅读时间', transform: v => v / 1000, reverseTransform: v => v * 1000 }, { key: 'minReadTimeUpper', label: '最大阅读时间(秒)', type: 'number', min: 60, max: 600, step: 10, desc: '每个帖子的最大阅读时间', transform: v => v / 1000, reverseTransform: v => v * 1000 }, { key: 'stuckTimeout', label: '卡住检测超时(秒)', type: 'number', min: 5, max: 60, step: 5, desc: '检测页面卡住的超时时间', transform: v => v / 1000, reverseTransform: v => v * 1000 } ]; basicConfigs.forEach(config => { const group = this.createConfigGroup(config); if (group && configArea) { configArea.appendChild(group); } }); } showBehaviorConfig() { const configArea = document.getElementById('config-area'); if (!configArea) { console.error('配置区域元素未找到 (showBehaviorConfig)'); return; } const behaviorConfigs = [ { key: 'scrollSegmentDistanceMin', label: '滚动最小距离(px)', type: 'number', min: 100, max: 1000, step: 50, desc: '每段滚动的最小距离' }, { key: 'scrollSegmentDistanceMax', label: '滚动最大距离(px)', type: 'number', min: 500, max: 2000, step: 100, desc: '每段滚动的最大距离' }, { key: 'scrollSegmentDurationMin', label: '滚动最小时长(ms)', type: 'number', min: 1000, max: 5000, step: 250, desc: '每段滚动的最小持续时间' }, { key: 'scrollSegmentDurationMax', label: '滚动最大时长(ms)', type: 'number', min: 3000, max: 10000, step: 500, desc: '每段滚动的最大持续时间' }, { key: 'randomPauseProbability', label: '随机暂停概率', type: 'range', min: 0, max: 1, step: 0.05, desc: '滚动过程中随机暂停的概率' }, { key: 'minTopicChangeDelay', label: '话题切换最小延迟(ms)', type: 'number', min: 1000, max: 10000, step: 500, desc: '切换话题的最小延迟时间' }, { key: 'maxTopicChangeDelay', label: '话题切换最大延迟(ms)', type: 'number', min: 3000, max: 20000, step: 1000, desc: '切换话题的最大延迟时间' } ]; behaviorConfigs.forEach(config => { const group = this.createConfigGroup(config); if (group && configArea) { configArea.appendChild(group); } }); } showAdvancedConfig() { const configArea = document.getElementById('config-area'); if (!configArea) { console.error('配置区域元素未找到 (showAdvancedConfig)'); return; } const advancedConfigs = [ { key: 'enableMouseSimulation', label: '启用鼠标模拟', type: 'checkbox', desc: '模拟真实的鼠标移动轨迹' }, { key: 'enableAdvancedBehavior', label: '启用高级行为', type: 'checkbox', desc: '包括随机停留、页面交互等' }, { key: 'enableDataAnalysis', label: '启用数据分析', type: 'checkbox', desc: '收集和分析使用统计数据' }, { key: 'enableSafetyFeatures', label: '启用安全功能', type: 'checkbox', desc: '包括请求头随机化、行为混淆等' }, { key: 'autoReplyEnabled', label: '启用自动回复', type: 'checkbox', desc: '自动回复特定类型的帖子' }, { key: 'keywordMonitoring', label: '关键词监控', type: 'checkbox', desc: '监控特定关键词并执行操作' }, { key: 'proxyEnabled', label: '启用代理', type: 'checkbox', desc: '通过代理服务器发送请求' } ]; advancedConfigs.forEach(config => { const group = this.createConfigGroup(config); if (group && configArea) { configArea.appendChild(group); } }); // 添加关键词设置区域 if (this.config.get('keywordMonitoring')) { const keywordSection = this.createKeywordSection(); if (keywordSection && configArea) { configArea.appendChild(keywordSection); } } } showUIConfig() { const configArea = document.getElementById('config-area'); if (!configArea) { console.error('配置区域元素未找到 (showUIConfig)'); return; } const uiConfigs = [ { key: 'theme', label: '主题', type: 'select', options: [ { value: 'light', label: '浅色主题' }, { value: 'dark', label: '深色主题' }, { value: 'auto', label: '跟随系统' } ], desc: '选择界面主题' }, { key: 'language', label: '语言', type: 'select', options: [ { value: 'zh-CN', label: '简体中文' }, { value: 'zh-TW', label: '繁体中文' }, { value: 'en-US', label: 'English' } ], desc: '选择界面语言' }, { key: 'showStatistics', label: '显示统计信息', type: 'checkbox', desc: '在界面中显示运行统计' }, { key: 'compactMode', label: '紧凑模式', type: 'checkbox', desc: '使用更紧凑的界面布局' }, { key: 'maxLogEntries', label: '最大日志条目', type: 'number', min: 50, max: 1000, step: 50, desc: '日志窗口保留的最大条目数' } ]; uiConfigs.forEach(config => { const group = this.createConfigGroup(config); if (group && configArea) { configArea.appendChild(group); } }); } showDataConfig() { const configArea = document.getElementById('config-area'); if (!configArea) { console.error('配置区域元素未找到 (showDataConfig)'); return; } // 数据管理区域 const dataSection = document.createElement('div'); if (!dataSection) { console.error('无法创建数据管理区域'); return; } dataSection.innerHTML = ` 📊 数据管理 配置管理 导出配置 导入配置 重置配置 数据清理 清空日志 清空缓存 清空所有数据 `; try { configArea.appendChild(dataSection); // 绑定事件 this.bindDataManagementEvents(); } catch (e) { console.error('添加数据管理区域失败:', e); } } createConfigGroup(config) { const group = document.createElement('div'); Object.assign(group.style, { marginBottom: '20px', padding: '15px', border: `1px solid ${this.config.get('theme') === 'dark' ? '#4a5568' : '#e2e8f0'}`, borderRadius: '8px', backgroundColor: this.config.get('theme') === 'dark' ? '#374151' : '#f8fafc' }); const label = document.createElement('label'); label.style.display = 'block'; label.style.marginBottom = '8px'; label.style.fontWeight = '500'; label.innerHTML = `${config.label} ${config.desc ? `(${config.desc})` : ''}`; let input; const currentValue = config.transform ? config.transform(this.config.get(config.key)) : this.config.get(config.key); switch (config.type) { case 'number': input = document.createElement('input'); input.type = 'number'; input.min = config.min; input.max = config.max; input.step = config.step; input.value = currentValue; break; case 'range': const container = document.createElement('div'); container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.gap = '10px'; input = document.createElement('input'); input.type = 'range'; input.min = config.min; input.max = config.max; input.step = config.step; input.value = currentValue; const valueDisplay = document.createElement('span'); valueDisplay.textContent = currentValue; valueDisplay.style.minWidth = '50px'; valueDisplay.style.textAlign = 'center'; valueDisplay.style.fontSize = '14px'; input.addEventListener('input', () => { valueDisplay.textContent = input.value; }); container.appendChild(input); container.appendChild(valueDisplay); group.appendChild(label); group.appendChild(container); input.addEventListener('change', () => { this.config.set(config.key, parseFloat(input.value)); this.logger.log(`配置已更新: ${config.label} = ${input.value}`, 'action'); }); return group; case 'checkbox': input = document.createElement('input'); input.type = 'checkbox'; input.checked = currentValue; input.style.marginRight = '8px'; const checkboxLabel = document.createElement('label'); checkboxLabel.style.display = 'flex'; checkboxLabel.style.alignItems = 'center'; checkboxLabel.style.cursor = 'pointer'; checkboxLabel.appendChild(input); checkboxLabel.appendChild(document.createTextNode(config.label)); if (config.desc) { const desc = document.createElement('div'); desc.style.fontSize = '12px'; desc.style.opacity = '0.7'; desc.style.marginTop = '4px'; desc.textContent = config.desc; group.appendChild(checkboxLabel); group.appendChild(desc); } else { group.appendChild(checkboxLabel); } input.addEventListener('change', () => { this.config.set(config.key, input.checked); this.logger.log(`配置已更新: ${config.label} = ${input.checked}`, 'action'); // 特殊处理某些配置变更 if (config.key === 'theme') { this.applyTheme(); } }); return group; case 'select': input = document.createElement('select'); config.options.forEach(option => { const optionElement = document.createElement('option'); optionElement.value = option.value; optionElement.textContent = option.label; optionElement.selected = option.value === currentValue; input.appendChild(optionElement); }); break; default: input = document.createElement('input'); input.type = 'text'; input.value = currentValue; } // 通用样式 if (input && config.type !== 'checkbox') { Object.assign(input.style, { width: '100%', padding: '8px 12px', border: `1px solid ${this.config.get('theme') === 'dark' ? '#4a5568' : '#d1d5db'}`, borderRadius: '6px', backgroundColor: this.config.get('theme') === 'dark' ? '#1f2937' : '#ffffff', color: 'inherit', fontSize: '14px' }); // 通用事件处理 input.addEventListener('change', () => { let value = input.value; if (config.type === 'number') { value = parseFloat(value); if (config.reverseTransform) { value = config.reverseTransform(value); } } this.config.set(config.key, value); this.logger.log(`配置已更新: ${config.label} = ${input.value}`, 'action'); // 特殊处理 if (config.key === 'theme') { this.applyTheme(); } }); } if (config.type !== 'checkbox') { group.appendChild(label); group.appendChild(input); } return group; } bindDataManagementEvents() { // 导出配置 document.getElementById('export-config')?.addEventListener('click', () => { const configStr = this.config.exportConfig(); const blob = new Blob([configStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `forum-assistant-config-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.logger.log('配置已导出', 'success'); }); // 导入配置 document.getElementById('import-config')?.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { try { const success = this.config.importConfig(e.target.result); if (success) { this.logger.log('配置导入成功', 'success'); this.hide(); setTimeout(() => location.reload(), 1000); } else { this.logger.log('配置导入失败', 'error'); } } catch (error) { this.logger.log(`配置导入错误: ${error.message}`, 'error'); } }; reader.readAsText(file); } }); input.click(); }); // 重置配置 document.getElementById('reset-config')?.addEventListener('click', () => { if (confirm('确定要重置所有配置吗？此操作不可撤销。')) { this.config.reset(); this.logger.log('配置已重置', 'action'); this.hide(); setTimeout(() => location.reload(), 1000); } }); // 清空日志 document.getElementById('clear-logs')?.addEventListener('click', () => { this.logger.clear(); }); // 清空缓存 document.getElementById('clear-cache')?.addEventListener('click', () => { if (confirm('确定要清空所有缓存数据吗？')) { GM_listValues().forEach(key => { if (key.startsWith('topicList') || key.startsWith('latestPage')) { GM_deleteValue(key); } }); this.logger.log('缓存已清空', 'action'); } }); // 清空所有数据 document.getElementById('clear-all')?.addEventListener('click', () => { if (confirm('确定要清空所有数据吗？这将删除所有配置、日志和缓存数据。')) { GM_listValues().forEach(key => GM_deleteValue(key)); this.logger.log('所有数据已清空', 'action'); setTimeout(() => location.reload(), 1000); } }); } createKeywordSection() { const section = document.createElement('div'); section.style.marginTop = '20px'; section.innerHTML = ` 🔍 关键词监控设置 监控关键词 (每行一个): 触发动作: 自动点赞 自动回复 仅通知 收藏帖子 `; // 加载已保存的关键词 const savedKeywords = this.config.get('monitorKeywords') || []; const keywordsInput = section.querySelector('#keywords-input'); keywordsInput.value = savedKeywords.join('\n'); // 保存关键词 keywordsInput.addEventListener('blur', () => { const keywords = keywordsInput.value.split('\n').filter(k => k.trim()); this.config.set('monitorKeywords', keywords); this.logger.log(`关键词已更新: ${keywords.length} 个`, 'action'); }); return section; } createFooter() { const footer = document.createElement('div'); Object.assign(footer.style, { padding: '20px', borderTop: `1px solid ${this.config.get('theme') === 'dark' ? '#4a5568' : '#e2e8f0'}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }); const version = document.createElement('span'); version.textContent = 'v2.0.0'; version.style.opacity = '0.6'; version.style.fontSize = '12px'; const buttons = document.createElement('div'); buttons.style.display = 'flex'; buttons.style.gap = '10px'; const saveBtn = document.createElement('button'); saveBtn.textContent = '保存设置'; Object.assign(saveBtn.style, { padding: '8px 16px', backgroundColor: '#3182ce', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', fontWeight: '500' }); saveBtn.addEventListener('click', () => { this.config.saveConfig(); this.logger.log('配置已保存', 'success'); this.hide(); }); const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; Object.assign(cancelBtn.style, { padding: '8px 16px', backgroundColor: 'transparent', color: 'inherit', border: '1px solid currentColor', borderRadius: '6px', cursor: 'pointer', fontSize: '14px' }); cancelBtn.addEventListener('click', () => this.hide()); buttons.appendChild(cancelBtn); buttons.appendChild(saveBtn); footer.appendChild(version); footer.appendChild(buttons); this.panel.appendChild(footer); } createOverlay() { this.overlay = document.createElement('div'); Object.assign(this.overlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: '10000', display: 'none' }); this.overlay.addEventListener('click', () => this.hide()); document.body.appendChild(this.overlay); } show() { if (!this.panel) this.create(); this.panel.style.display = 'flex'; this.overlay.style.display = 'block'; this.isVisible = true; } hide() { if (this.panel) this.panel.style.display = 'none'; if (this.overlay) this.overlay.style.display = 'none'; this.isVisible = false; } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); } } applyTheme() { // 重新创建面板以应用新主题 if (this.panel) { this.panel.remove(); this.panel = null; } if (this.isVisible) { this.create(); this.show(); } } } // ===== 统计面板类 ===== class StatisticsPanel { constructor(config, logger) { this.config = config; this.logger = logger; this.panel = null; this.isVisible = false; this.updateInterval = null; this.energyValue = '加载中...'; } create() { if (this.panel) return; const isDark = this.config.get('theme') === 'dark'; this.panel = document.createElement('div'); this.panel.id = 'forum-assistant-stats'; Object.assign(this.panel.style, { position: 'fixed', bottom: '10px', left: '10px', width: '280px', backgroundColor: isDark ? '#2d3748' : '#ffffff', color: isDark ? '#e2e8f0' : '#2d3748', border: `1px solid ${isDark ? '#4a5568' : '#e2e8f0'}`, borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: '9999', display: 'none', flexDirection: 'column', fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif", fontSize: '12px', backdropFilter: 'blur(10px)' }); this.createHeader(); this.createContent(); document.body.appendChild(this.panel); } createHeader() { const header = document.createElement('div'); Object.assign(header.style, { padding: '10px 12px', borderBottom: `1px solid ${this.config.get('theme') === 'dark' ? '#4a5568' : '#e2e8f0'}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontWeight: '600' }); const title = document.createElement('span'); title.textContent = '📊 运行统计'; const toggleBtn = document.createElement('button'); toggleBtn.innerHTML = '−'; Object.assign(toggleBtn.style, { background: 'none', border: 'none', color: 'inherit', cursor: 'pointer', fontSize: '16px', padding: '2px 6px', borderRadius: '3px' }); toggleBtn.addEventListener('click', () => this.toggleMinimize()); header.appendChild(title); header.appendChild(toggleBtn); this.panel.appendChild(header); } createContent() { this.content = document.createElement('div'); Object.assign(this.content.style, { padding: '12px', display: 'grid', gap: '8px' }); this.panel.appendChild(this.content); this.updateContent(); } updateContent() { if (!this.content) { console.warn('统计内容元素不存在'); return; } try { const stats = this.logger.statistics; const runtime = Date.now() - stats.startTime; const items = [ { label: '⏱️ 运行时间', value: Utils.formatTime(runtime) }, { label: '📖 已读话题', value: stats.topicsRead }, { label: '👍 点赞数量', value: stats.likesGiven }, { label: '🔄 总操作数', value: stats.totalActions }, { label: '❌ 错误次数', value: stats.errorsCount }, { label: '📈 阅读效率', value: `${(stats.topicsRead / Math.max(runtime / 3600000, 0.1)).toFixed(1)} 话题/小时` } ]; this.content.innerHTML = items.map(item => ` ${item.label} ${item.value} `).join('') + ` ⚡ 能量值 ${this.energyValue} 🔄 重置统计 `; } catch (e) { console.error('更新统计内容失败:', e); } } // 更新能量显示 updateEnergyDisplay() { if (window.location.hostname !== 'nodeloc.cc') { this.energyValue = 'N/A'; return; } GM_xmlhttpRequest({ method: 'GET', url: 'https://nodeloc.cc/leaderboard/1.json', onload: (response) => { try { const data = JSON.parse(response.responseText); let energy = '--'; if (data && data.personal && data.personal.user && typeof data.personal.user.total_score !== 'undefined') { energy = data.personal.user.total_score.toLocaleString(); } this.energyValue = energy; } catch (e) { this.energyValue = '错误'; console.error('[Energy Display] Error parsing data:', e); } }, onerror: (error) => { this.energyValue = '失败'; console.error('[Energy Display] Error fetching data:', error); } }); } show() { if (!this.panel) this.create(); this.panel.style.display = 'flex'; this.isVisible = true; // 立即更新一次 this.updateContent(); this.updateEnergyDisplay(); // 设置定时器 if (this.updateInterval) clearInterval(this.updateInterval); this.updateInterval = setInterval(() => { this.updateContent(); this.updateEnergyDisplay(); }, 5000); // 5秒刷新一次 // 绑定重置按钮事件 setTimeout(() => { const resetBtn = document.getElementById('reset-stats'); if (resetBtn) { resetBtn.addEventListener('click', () => { if (confirm('确定要重置所有统计数据吗？此操作不可撤销。')) { this.logger.resetStatistics(); this.updateContent(); } }); } }, 100); } hide() { if (this.panel) this.panel.style.display = 'none'; this.isVisible = false; // 停止更新 if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; } } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); } } toggleMinimize() { if (!this.content || !this.panel) return; const toggleBtn = this.panel.querySelector('button'); if (!toggleBtn) return; if (this.content.style.display === 'none') { this.content.style.display = 'grid'; toggleBtn.innerHTML = '−'; } else { this.content.style.display = 'none'; toggleBtn.innerHTML = '+'; } } } // ===== 升级进度面板类 ===== class UpgradeProgressPanel { constructor(config, logger) { this.config = config; this.logger = logger; this.panel = null; this.isVisible = false; this.updateInterval = null; this.upgradeProgress = '加载中...'; this.unmetConditions = null; this.customUsername = GM_getValue('customUsername', ''); this.currentUsername = null; } create() { if (this.panel) return; const isDark = this.config.get('theme') === 'dark'; this.panel = document.createElement('div'); this.panel.id = 'forum-assistant-upgrade'; // 获取保存的位置，默认为左上角 const savedPosition = GM_getValue('upgradeProgressPosition', { top: '10px', left: '10px' }); Object.assign(this.panel.style, { position: 'fixed', top: savedPosition.top, left: savedPosition.left, width: '300px', backgroundColor: isDark ? '#2d3748' : '#ffffff', color: isDark ? '#e2e8f0' : '#2d3748', border: `1px solid ${isDark ? '#4a5568' : '#e2e8f0'}`, borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: '9998', display: 'none', flexDirection: 'column', fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif", fontSize: '12px', backdropFilter: 'blur(10px)' }); this.createHeader(); this.createContent(); document.body.appendChild(this.panel); } createHeader() { const header = document.createElement('div'); Object.assign(header.style, { padding: '10px 12px', borderBottom: `1px solid ${this.config.get('theme') === 'dark' ? '#4a5568' : '#e2e8f0'}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontWeight: '600', cursor: 'move', userSelect: 'none', background: this.config.get('theme') === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)' }); const title = document.createElement('span'); title.textContent = '📈 升级进度'; const toggleBtn = document.createElement('button'); toggleBtn.innerHTML = '−'; Object.assign(toggleBtn.style, { background: 'none', border: 'none', color: 'inherit', cursor: 'pointer', fontSize: '16px', padding: '2px 6px', borderRadius: '3px' }); toggleBtn.addEventListener('click', () => this.hide()); header.appendChild(title); header.appendChild(toggleBtn); this.panel.appendChild(header); // 使窗口可拖拽 this.makeDraggable(header); } createContent() { this.content = document.createElement('div'); Object.assign(this.content.style, { padding: '15px', display: 'flex', flexDirection: 'column', gap: '12px' }); this.panel.appendChild(this.content); this.updateContent(); } updateContent() { if (!this.content) return; const username = this.getEffectiveUsername(); this.content.innerHTML = ` 👤 用户名: ${username || '未设置'} 📊 当前进度: ${this.upgradeProgress} ${this.unmetConditions ? ` ⚠️ 未完成条件: ${this.unmetConditions.map(condition => `• ${condition}`).join(' ')} ` : ''} 保存用户名 刷新进度 🔧 测试API 📍 重置位置 `; // 绑定事件 this.bindEvents(); } bindEvents() { const saveBtn = this.content.querySelector('#save-username'); const refreshBtn = this.content.querySelector('#refresh-progress'); const testBtn = this.content.querySelector('#test-api'); const resetPosBtn = this.content.querySelector('#reset-position'); const usernameInput = this.content.querySelector('#custom-username'); if (saveBtn) { saveBtn.addEventListener('click', () => { const username = usernameInput.value.trim(); this.customUsername = username; GM_setValue('customUsername', username); this.logger.log(`自定义用户名已保存: ${username || '(清空)'}`, 'success'); this.updateContent(); this.updateUpgradeProgress(); }); } if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.updateUpgradeProgress(); this.logger.log('手动刷新升级进度', 'action'); }); } if (testBtn) { testBtn.addEventListener('click', () => { this.testApiConnection(); }); } if (resetPosBtn) { resetPosBtn.addEventListener('click', () => { this.resetPosition(); }); } } makeDraggable(header) { if (!header || !this.panel) return; let isDragging = false; let currentX, currentY, initialX, initialY; header.addEventListener("mousedown", (e) => { if (!this.panel) return; isDragging = true; initialX = e.clientX - this.panel.offsetLeft; initialY = e.clientY - this.panel.offsetTop; header.style.cursor = 'grabbing'; }); document.addEventListener("mousemove", (e) => { if (isDragging && this.panel) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; // 限制在屏幕范围内 const maxX = window.innerWidth - this.panel.offsetWidth; const maxY = window.innerHeight - this.panel.offsetHeight; currentX = Math.max(0, Math.min(currentX, maxX)); currentY = Math.max(0, Math.min(currentY, maxY)); this.panel.style.left = currentX + "px"; this.panel.style.top = currentY + "px"; this.panel.style.right = "auto"; this.panel.style.bottom = "auto"; } }); document.addEventListener("mouseup", () => { if (isDragging && this.panel) { isDragging = false; header.style.cursor = 'move'; // 保存当前位置 const position = { top: this.panel.style.top, left: this.panel.style.left }; GM_setValue('upgradeProgressPosition', position); console.log('[Upgrade Progress] 位置已保存:', position); } }); } getEffectiveUsername() { // 优先使用自定义用户名 if (this.customUsername) { return this.customUsername; } // 其次使用自动检测的用户名 if (!this.currentUsername) { this.currentUsername = this.getCurrentUsername(); } return this.currentUsername; } resetPosition() { // 重置到默认位置（左上角） const defaultPosition = { top: '10px', left: '10px' }; if (this.panel) { this.panel.style.top = defaultPosition.top; this.panel.style.left = defaultPosition.left; this.panel.style.right = 'auto'; this.panel.style.bottom = 'auto'; } // 保存重置后的位置 GM_setValue('upgradeProgressPosition', defaultPosition); this.logger.log('升级进度窗口位置已重置到左上角', 'success'); } testApiConnection() { const username = this.getEffectiveUsername(); if (!username) { alert('请先设置用户名'); return; } this.logger.log('开始测试API连接...', 'action'); // 测试基本的用户信息API GM_xmlhttpRequest({ method: 'GET', url: `https://nodeloc.cc/u/${username}.json`, headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Referer': window.location.href }, onload: (response) => { console.log(`[API Test] 用户信息API响应状态: ${response.status}`); console.log(`[API Test] 用户信息API响应内容:`, response.responseText); if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (data.user) { this.logger.log(`✅ 用户信息API正常，用户ID: ${data.user.id}`, 'success'); // 继续测试升级进度API this.testUpgradeProgressApi(username); } else { this.logger.log('❌ 用户信息API返回数据异常', 'error'); } } catch (e) { this.logger.log('❌ 用户信息API数据解析失败', 'error'); } } else { this.logger.log(`❌ 用户信息API请求失败 (${response.status})`, 'error'); } }, onerror: (error) => { this.logger.log('❌ 用户信息API网络错误', 'error'); console.error('[API Test] 用户信息API网络错误:', error); } }); } testUpgradeProgressApi(username) { GM_xmlhttpRequest({ method: 'GET', url: `https://nodeloc.cc/u/${username}/upgrade-progress.json`, headers: { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest', 'Referer': window.location.href }, onload: (response) => { console.log(`[API Test] 升级进度API响应状态: ${response.status}`); console.log(`[API Test] 升级进度API响应内容:`, response.responseText); if (response.status === 200) { try { const data = JSON.parse(response.responseText); console.log('[API Test] 升级进度API完整响应:', JSON.stringify(data, null, 2)); if (data) { if (data.next_level) { const nextLevel = data.next_level; const nextLevelKeys = Object.keys(nextLevel); this.logger.log(`✅ 升级进度API正常，next_level字段: ${nextLevelKeys.join(', ')}`, 'success'); console.log('[API Test] next_level详细数据:', nextLevel); console.log('[API Test] next_level类型:', typeof nextLevel); // 分析next_level的具体内容 if (typeof nextLevel === 'object') { Object.entries(nextLevel).forEach(([key, value]) => { console.log(`[API Test] next_level.${key}:`, value, `(类型: ${typeof value})`); }); } } else { const keys = Object.keys(data); this.logger.log(`⚠️ 升级进度API无next_level，可用字段: ${keys.join(', ')}`, 'warning'); console.log('[API Test] 可用数据字段:', keys); console.log('[API Test] 完整数据:', data); } } else { this.logger.log('❌ 升级进度API返回空数据', 'error'); } } catch (e) { this.logger.log('❌ 升级进度API数据解析失败', 'error'); console.error('[API Test] 解析错误:', e); console.error('[API Test] 原始响应:', response.responseText); } } else if (response.status === 403) { this.logger.log('❌ 升级进度API权限不足，可能需要登录', 'error'); } else if (response.status === 404) { this.logger.log('❌ 升级进度API不存在或用户不存在', 'error'); } else { this.logger.log(`❌ 升级进度API请求失败 (${response.status})`, 'error'); } }, onerror: (error) => { this.logger.log('❌ 升级进度API网络错误', 'error'); console.error('[API Test] 升级进度API网络错误:', error); } }); } getCurrentUsername() { // 方法1: 从用户菜单按钮获取 const userMenuButton = document.querySelector('.header-dropdown-toggle.current-user'); if (userMenuButton) { const img = userMenuButton.querySelector('img'); if (img && img.alt) { console.log(`[Username] 从用户菜单获取到用户名: ${img.alt}`); return img.alt; } } // 方法2: 从用户链接获取 const userLinks = document.querySelectorAll('a[href*="/u/"]'); for (const userLink of userLinks) { const match = userLink.href.match(/\/u\/([^\/]+)/); if (match && match[1]) { console.log(`[Username] 从用户链接获取到用户名: ${match[1]}`); return match[1]; } } // 方法3: 从当前页面URL获取（如果在用户页面） if (window.location.pathname.includes('/u/')) { const match = window.location.pathname.match(/\/u\/([^\/]+)/); if (match && match[1]) { console.log(`[Username] 从URL获取到用户名: ${match[1]}`); return match[1]; } } // 方法4: 从页面标题获取 const titleMatch = document.title.match(/(.+?)\s*-\s*NodeLoc/); if (titleMatch && titleMatch[1] && !titleMatch[1].includes('NodeLoc')) { console.log(`[Username] 从页面标题获取到用户名: ${titleMatch[1]}`); return titleMatch[1]; } // 方法5: 从meta标签获取 const metaUser = document.querySelector('meta[name="discourse-username"]'); if (metaUser && metaUser.content) { console.log(`[Username] 从meta标签获取到用户名: ${metaUser.content}`); return metaUser.content; } console.log('[Username] 无法自动获取用户名'); return null; } updateUpgradeProgress() { if (window.location.hostname !== 'nodeloc.cc') { this.upgradeProgress = 'N/A (非NodeLoc站点)'; this.updateContent(); return; } const username = this.getEffectiveUsername(); if (!username) { this.upgradeProgress = '未设置用户名'; this.updateContent(); return; } this.upgradeProgress = '获取中...'; this.updateContent(); console.log(`[Upgrade Progress] 正在获取用户 ${username} 的升级进度`); GM_xmlhttpRequest({ method: 'GET', url: `https://nodeloc.cc/u/${username}/upgrade-progress.json`, headers: { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest', 'Referer': window.location.href }, onload: (response) => { console.log(`[Upgrade Progress] 响应状态: ${response.status}`); console.log(`[Upgrade Progress] 响应内容:`, response.responseText); try { if (response.status === 200) { const data = JSON.parse(response.responseText); let progress = '获取不到信息'; console.log(`[Upgrade Progress] 解析的数据:`, data); // 检查数据结构 if (data) { console.log(`[Upgrade Progress] 完整数据结构:`, JSON.stringify(data, null, 2)); // 方案1: 检查升级进度数据（基于真实的NodeLoc API结构） if (data.next_level !== undefined && data.next_level_name && data.met_count !== undefined && data.total_conditions !== undefined) { // NodeLoc的真实数据结构 const nextLevelName = data.next_level_name; const metCount = data.met_count; const totalConditions = data.total_conditions; const percentage = Math.round((metCount / totalConditions) * 100); progress = `${nextLevelName} ${percentage}% (${metCount}/${totalConditions})`; // 保存未完成条件 if (data.unmet_conditions && Array.isArray(data.unmet_conditions)) { this.unmetConditions = data.unmet_conditions; } else { this.unmetConditions = null; } console.log(`[Upgrade Progress] 解析成功: ${progress}`); } // 方案2: 如果有next_level但结构不同 else if (data.next_level) { const nextLevel = data.next_level; console.log(`[Upgrade Progress] next_level详细信息:`, nextLevel); console.log(`[Upgrade Progress] next_level类型:`, typeof nextLevel); if (typeof nextLevel === 'number') { // next_level是数字，查找等级名称 if (data.next_level_name) { progress = `下一等级: ${data.next_level_name} (等级${nextLevel})`; } else { const levelNames = ['新手', '基础会员', '会员', '资深会员', '钻石会员', '领导者']; progress = `下一等级: ${levelNames[nextLevel] || `等级${nextLevel}`}`; } } else if (typeof nextLevel === 'object') { // 检查各种可能的字段组合 if (nextLevel.name && nextLevel.progress !== undefined && nextLevel.total !== undefined) { const percentage = Math.round((nextLevel.progress / nextLevel.total) * 100); progress = `${nextLevel.name} ${percentage}%`; } else if (nextLevel.name && nextLevel.current !== undefined && nextLevel.required !== undefined) { const percentage = Math.round((nextLevel.current / nextLevel.required) * 100); progress = `${nextLevel.name} ${percentage}%`; } else if (nextLevel.name) { progress = `下一等级: ${nextLevel.name}`; } else { // 显示对象的所有键值对 const entries = Object.entries(nextLevel).map(([key, value]) => `${key}: ${value}`); progress = `next_level: {${entries.join(', ')}}`; } } else if (typeof nextLevel === 'string') { progress = `下一等级: ${nextLevel}`; } else { progress = `next_level: ${JSON.stringify(nextLevel)}`; } } // 方案2: 检查用户信任等级 else if (data.user && data.user.trust_level !== undefined) { const trustLevel = data.user.trust_level; const levelNames = ['新手', '基础会员', '会员', '资深会员', '领导者']; progress = `信任等级 ${trustLevel} (${levelNames[trustLevel] || '未知'})`; } // 方案3: 检查其他可能的数据结构 else if (data.upgrade_progress) { progress = `进度: ${JSON.stringify(data.upgrade_progress)}`; } else if (data.level_info) { progress = `等级信息: ${JSON.stringify(data.level_info)}`; } else if (data.progress) { progress = `进度: ${JSON.stringify(data.progress)}`; } // 方案4: 显示所有可用的键 else { const keys = Object.keys(data); progress = `可用字段: ${keys.join(', ')}`; console.warn('[Upgrade Progress] 未识别的数据结构，可用字段:', keys); console.warn('[Upgrade Progress] 完整数据:', data); } } else { progress = '数据为空'; } this.upgradeProgress = progress; } else if (response.status === 403) { this.upgradeProgress = '权限不足'; } else if (response.status === 404) { this.upgradeProgress = '用户不存在'; } else { this.upgradeProgress = `请求失败 (${response.status})`; } this.updateContent(); } catch (e) { this.upgradeProgress = '数据解析错误'; this.updateContent(); console.error('[Upgrade Progress] 解析错误:', e); console.error('[Upgrade Progress] 原始响应:', response.responseText); } }, onerror: (error) => { this.upgradeProgress = '网络请求失败'; this.updateContent(); console.error('[Upgrade Progress] 网络错误:', error); }, ontimeout: () => { this.upgradeProgress = '请求超时'; this.updateContent(); console.error('[Upgrade Progress] 请求超时'); }, timeout: 10000 // 10秒超时 }); } show() { if (!this.panel) this.create(); this.panel.style.display = 'flex'; this.isVisible = true; // 保存显示状态 GM_setValue('upgradeProgressVisible', true); // 立即更新一次 this.updateContent(); this.updateUpgradeProgress(); // 设置定时器 if (this.updateInterval) clearInterval(this.updateInterval); this.updateInterval = setInterval(() => { this.updateUpgradeProgress(); }, 10000); // 10秒刷新一次 } hide() { if (this.panel) this.panel.style.display = 'none'; this.isVisible = false; // 保存隐藏状态 GM_setValue('upgradeProgressVisible', false); // 停止更新 if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; } } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); } } } // ===== 控制面板类 ===== class ControlPanel { constructor(config, logger, statsPanel, configPanel, upgradePanel) { this.config = config; this.logger = logger; this.statsPanel = statsPanel; this.configPanel = configPanel; this.upgradePanel = upgradePanel; this.panel = null; this.buttons = {}; } create() { if (this.panel) return; const isDark = this.config.get('theme') === 'dark'; this.panel = document.createElement('div'); this.panel.id = 'forum-assistant-controls'; Object.assign(this.panel.style, { position: 'fixed', bottom: '10px', right: '10px', display: 'flex', flexDirection: 'column', gap: '8px', zIndex: '9999' }); // 创建控制按钮 this.createControlButtons(); document.body.appendChild(this.panel); } createControlButtons() { const buttons = [ { id: 'toggle-reading', text: '开始阅读', icon: '📖', action: () => this.toggleReading() }, { id: 'toggle-like', text: '启用点赞', icon: '👍', action: () => this.toggleAutoLike() }, { id: 'show-stats', text: '显示统计', icon: '📊', action: () => this.statsPanel.toggle() }, { id: 'show-upgrade', text: '升级进度', icon: '📈', action: () => this.toggleUpgradeProgress() }, { id: 'show-config', text: '打开设置', icon: '⚙️', action: () => this.configPanel.toggle() }, { id: 'show-logs', text: '显示日志', icon: '📋', action: () => this.logger.toggleVisibility() } ]; buttons.forEach(btn => { const button = this.createButton(btn); this.buttons[btn.id] = button; this.panel.appendChild(button); }); this.updateButtonStates(); } createButton(config) { const isDark = this.config.get('theme') === 'dark'; const button = document.createElement('button'); button.innerHTML = `${config.icon} ${config.text}`; Object.assign(button.style, { padding: '10px 15px', backgroundColor: isDark ? '#4a5568' : '#ffffff', color: isDark ? '#e2e8f0' : '#2d3748', border: `1px solid ${isDark ? '#718096' : '#d1d5db'}`, borderRadius: '8px', cursor: 'pointer', fontSize: '13px', fontWeight: '500', display: 'flex', alignItems: 'center', gap: '6px', minWidth: '120px', justifyContent: 'flex-start', transition: 'all 0.2s ease', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }); button.addEventListener('click', config.action); // 悬停效果 button.addEventListener('mouseenter', () => { button.style.transform = 'translateY(-1px)'; button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)'; }); button.addEventListener('mouseleave', () => { button.style.transform = 'translateY(0)'; button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'; }); return button; } toggleReading() { const isReading = GM_getValue('isReading', false); GM_setValue('isReading', !isReading); this.updateButtonStates(); if (!isReading) { this.logger.log('开始自动阅读', 'action'); // 调用主程序的开始阅读逻辑 if (window.forumAssistant && typeof window.forumAssistant.start === 'function') { window.forumAssistant.start(); } } else { this.logger.log('停止自动阅读', 'action'); // 调用主程序的停止阅读逻辑 if (window.forumAssistant && typeof window.forumAssistant.stop === 'function') { window.forumAssistant.stop(); } } } toggleAutoLike() { const rateLimitInfo = GM_getValue('rateLimitInfo', null); // 检查是否仍在限制期内 if (rateLimitInfo && this.isRateLimitActive(rateLimitInfo)) { const remainingTime = this.getRemainingTime(rateLimitInfo); const canLikeTime = this.getCanLikeTime(rateLimitInfo); this.logger.log(`点赞仍在限制期内，剩余时间: ${remainingTime}`, 'warning'); this.logger.log(`📅 预计可点赞时间: ${canLikeTime}`, 'info'); this.logger.showNotification(`点赞受限，剩余时间: ${remainingTime}`, 'warning'); return; } // 如果限制已过期，清除限制信息 if (rateLimitInfo && !this.isRateLimitActive(rateLimitInfo)) { GM_setValue('rateLimitInfo', null); this.logger.log('点赞限制已过期，已清除限制信息', 'success'); } const isEnabled = GM_getValue('autoLikeEnabled', false); const newState = !isEnabled; GM_setValue('autoLikeEnabled', newState); this.updateButtonStates(); if (newState) { // 启用时显示详细信息 this.logger.log(`🎯 自动点赞已启用`, 'success'); this.showLikeStatusInfo(); } else { this.logger.log(`⏸️ 自动点赞已禁用`, 'action'); } } // 显示点赞状态信息 showLikeStatusInfo() { const rateLimitInfo = GM_getValue('rateLimitInfo', null); if (rateLimitInfo && this.isRateLimitActive(rateLimitInfo)) { // 仍在限制期内 const remainingTime = this.getRemainingTime(rateLimitInfo); const canLikeTime = this.getCanLikeTime(rateLimitInfo); this.logger.log(`⚠️ 当前状态: 点赞受限`, 'warning'); this.logger.log(`⏰ 剩余等待时间: ${remainingTime}`, 'warning'); this.logger.log(`📅 预计可点赞时间: ${canLikeTime}`, 'info'); this.logger.log(`💡 限制原因: ${rateLimitInfo.message || '未知'}`, 'info'); } else { // 可以正常点赞 const currentTime = new Date().toLocaleString('zh-CN'); this.logger.log(`✅ 当前状态: 可以正常点赞`, 'success'); this.logger.log(`🕐 当前时间: ${currentTime}`, 'info'); // 显示今日点赞统计 const dailyLikeCount = GM_getValue('dailyLikeCount', 0); const maxLikes = this.siteAdapter?.getLimit('maxLikes') || 100; this.logger.log(`📊 今日点赞: ${dailyLikeCount}/${maxLikes}`, 'info'); if (dailyLikeCount >= maxLikes) { this.logger.log(`⚠️ 今日点赞已达上限`, 'warning'); } else { const remaining = maxLikes - dailyLikeCount; this.logger.log(`💪 剩余可点赞: ${remaining} 次`, 'success'); } } } // 获取可以点赞的具体时间 getCanLikeTime(rateLimitInfo) { if (!rateLimitInfo || !rateLimitInfo.waitSeconds || !rateLimitInfo.timestamp) { return '未知'; } const canLikeTimestamp = rateLimitInfo.timestamp + (rateLimitInfo.waitSeconds * 1000); const canLikeDate = new Date(canLikeTimestamp); return canLikeDate.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } // 获取剩余限制时间 getRemainingTime(rateLimitInfo) { if (!rateLimitInfo || !rateLimitInfo.waitSeconds || !rateLimitInfo.timestamp) { return '未知'; } const now = Date.now(); const limitEndTime = rateLimitInfo.timestamp + (rateLimitInfo.waitSeconds * 1000); const remainingMs = limitEndTime - now; if (remainingMs <= 0) { return '已过期'; } const hours = Math.floor(remainingMs / (1000 * 60 * 60)); const minutes = Math.floor((remainingMs % (1000 * 60 * 60)) / (1000 * 60)); if (hours > 0) { return `${hours}小时${minutes}分钟`; } else { return `${minutes}分钟`; } } toggleUpgradeProgress() { this.upgradePanel.toggle(); this.updateButtonStates(); this.logger.log(`升级进度面板已${this.upgradePanel.isVisible ? '显示' : '隐藏'}`, 'action'); } updateButtonStates() { const isReading = GM_getValue('isReading', false); const isLikeEnabled = GM_getValue('autoLikeEnabled', false); const isUpgradeVisible = this.upgradePanel.isVisible; const rateLimitInfo = GM_getValue('rateLimitInfo', null); if (this.buttons && this.buttons['toggle-reading']) { const btn = this.buttons['toggle-reading']; if (btn) { btn.innerHTML = `📖 ${isReading ? '停止阅读' : '开始阅读'}`; btn.style.backgroundColor = isReading ? '#dc3545' : (this.config.get('theme') === 'dark' ? '#4a5568' : '#ffffff'); btn.style.color = isReading ? '#ffffff' : (this.config.get('theme') === 'dark' ? '#e2e8f0' : '#2d3748'); } } if (this.buttons && this.buttons['toggle-like']) { const btn = this.buttons['toggle-like']; if (btn) { // 检查是否有限制信息 if (rateLimitInfo && this.isRateLimitActive(rateLimitInfo)) { btn.innerHTML = `🚫 点赞受限`; btn.style.backgroundColor = '#dc3545'; btn.style.color = '#ffffff'; btn.title = `点赞已达限制，剩余等待时间: ${rateLimitInfo.timeLeft}`; } else { btn.innerHTML = `👍 ${isLikeEnabled ? '禁用点赞' : '启用点赞'}`; btn.style.backgroundColor = isLikeEnabled ? '#28a745' : (this.config.get('theme') === 'dark' ? '#4a5568' : '#ffffff'); btn.style.color = isLikeEnabled ? '#ffffff' : (this.config.get('theme') === 'dark' ? '#e2e8f0' : '#2d3748'); btn.title = isLikeEnabled ? '点击禁用自动点赞' : '点击启用自动点赞'; } } } if (this.buttons && this.buttons['show-upgrade']) { const btn = this.buttons['show-upgrade']; if (btn) { btn.innerHTML = `📈 ${isUpgradeVisible ? '隐藏进度' : '升级进度'}`; btn.style.backgroundColor = isUpgradeVisible ? '#8b5cf6' : (this.config.get('theme') === 'dark' ? '#4a5568' : '#ffffff'); btn.style.color = isUpgradeVisible ? '#ffffff' : (this.config.get('theme') === 'dark' ? '#e2e8f0' : '#2d3748'); } } } // 检查限制是否仍然有效 isRateLimitActive(rateLimitInfo) { if (!rateLimitInfo || !rateLimitInfo.waitSeconds || !rateLimitInfo.timestamp) { return false; } const now = Date.now(); const limitEndTime = rateLimitInfo.timestamp + (rateLimitInfo.waitSeconds * 1000); return now < limitEndTime; } show() { if (!this.panel) this.create(); this.panel.style.display = 'flex'; } hide() { if (this.panel) this.panel.style.display = 'none'; } } // ===== 状态指示器类 ===== class StatusIndicator { constructor(config, logger) { this.config = config; this.logger = logger; this.indicator = null; this.statusText = null; this.progressBar = null; } create() { if (this.indicator) return; const isDark = this.config.get('theme') === 'dark'; this.indicator = document.createElement('div'); this.indicator.id = 'forum-assistant-status'; Object.assign(this.indicator.style, { position: 'fixed', top: '10px', left: '50%', transform: 'translateX(-50%)', backgroundColor: isDark ? '#2d3748' : '#ffffff', color: isDark ? '#e2e8f0' : '#2d3748', border: `1px solid ${isDark ? '#4a5568' : '#e2e8f0'}`, borderRadius: '20px', padding: '8px 16px', fontSize: '12px', fontWeight: '500', zIndex: '9998', display: 'none', alignItems: 'center', gap: '8px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', backdropFilter: 'blur(10px)', minWidth: '200px', justifyContent: 'center' }); // 状态图标 this.statusIcon = document.createElement('span'); this.statusIcon.textContent = '⏸️'; // 状态文本 this.statusText = document.createElement('span'); this.statusText.textContent = '待机中'; // 进度条容器 const progressContainer = document.createElement('div'); Object.assign(progressContainer.style, { width: '60px', height: '4px', backgroundColor: isDark ? '#4a5568' : '#e2e8f0', borderRadius: '2px', overflow: 'hidden' }); // 进度条 this.progressBar = document.createElement('div'); Object.assign(this.progressBar.style, { width: '0%', height: '100%', backgroundColor: '#3182ce', borderRadius: '2px', transition: 'width 0.3s ease' }); progressContainer.appendChild(this.progressBar); this.indicator.appendChild(this.statusIcon); this.indicator.appendChild(this.statusText); this.indicator.appendChild(progressContainer); document.body.appendChild(this.indicator); } updateStatus(status, progress = 0) { if (!this.indicator) this.create(); const statusConfig = { idle: { icon: '⏸️', text: '待机中', color: '#6b7280' }, reading: { icon: '📖', text: '阅读中', color: '#3182ce' }, scrolling: { icon: '🔄', text: '滚动中', color: '#10b981' }, liking: { icon: '👍', text: '点赞中', color: '#f59e0b' }, switching: { icon: '🔀', text: '切换话题', color: '#8b5cf6' }, error: { icon: '❌', text: '出现错误', color: '#dc3545' }, loading: { icon: '⏳', text: '加载中', color: '#6366f1' } }; const config = statusConfig[status] || statusConfig.idle; if (this.statusIcon) this.statusIcon.textContent = config.icon; if (this.statusText) this.statusText.textContent = config.text; if (this.progressBar) { this.progressBar.style.backgroundColor = config.color; this.progressBar.style.width = `${Math.max(0, Math.min(100, progress))}%`; } // 自动显示/隐藏 if (status !== 'idle') { this.show(); } else { setTimeout(() => this.hide(), 2000); } } show() { if (!this.indicator) this.create(); this.indicator.style.display = 'flex'; } hide() { if (this.indicator) this.indicator.style.display = 'none'; } } // ===== 智能行为模拟类 ===== class BehaviorSimulator { constructor(config, logger, statusIndicator) { this.config = config; this.logger = logger; this.statusIndicator = statusIndicator; this.mouseTracker = null; this.behaviorQueue = []; this.isSimulating = false; } // 鼠标移动模拟 simulateMouseMovement() { if (!this.config.get('enableMouseSimulation')) return; const startX = Math.random() * window.innerWidth; const startY = Math.random() * window.innerHeight; const endX = Math.random() * window.innerWidth; const endY = Math.random() * window.innerHeight; const duration = Utils.getRandomInt(1000, 3000); const steps = 30; const stepDuration = duration / steps; let currentStep = 0; const moveInterval = setInterval(() => { if (currentStep >= steps) { clearInterval(moveInterval); return; } const progress = currentStep / steps; const easeProgress = this.easeInOutCubic(progress); const currentX = startX + (endX - startX) * easeProgress; const currentY = startY + (endY - startY) * easeProgress; // 创建鼠标移动事件 const event = new MouseEvent('mousemove', { clientX: currentX, clientY: currentY, bubbles: true }); document.dispatchEvent(event); currentStep++; }, stepDuration); } // 随机页面交互 async simulatePageInteraction() { if (!this.config.get('enableAdvancedBehavior')) return; const interactions = [ () => this.simulateTextSelection(), () => this.simulateScrollPause(), () => this.simulateElementHover(), () => this.simulateKeyboardActivity(), () => this.simulateWindowResize() ]; const randomInteraction = interactions[Math.floor(Math.random() * interactions.length)]; await randomInteraction(); } // 文本选择模拟 simulateTextSelection() { const textElements = document.querySelectorAll('p, div, span, h1, h2, h3, h4, h5, h6'); if (textElements.length === 0) return; const randomElement = textElements[Math.floor(Math.random() * textElements.length)]; const text = randomElement.textContent; if (text.length > 10) { const startIndex = Math.floor(Math.random() * (text.length - 10)); const endIndex = startIndex + Utils.getRandomInt(5, 20); try { const range = document.createRange(); const textNode = randomElement.firstChild; if (textNode && textNode.nodeType === Node.TEXT_NODE) { range.setStart(textNode, Math.min(startIndex, textNode.textContent.length)); range.setEnd(textNode, Math.min(endIndex, textNode.textContent.length)); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); // 短暂保持选择状态 setTimeout(() => { selection.removeAllRanges(); }, Utils.getRandomInt(500, 2000)); this.logger.log('模拟文本选择', 'action'); } } catch (e) { // 忽略选择错误 } } } // 滚动暂停模拟 async simulateScrollPause() { const pauseDuration = Utils.getRandomInt(1000, 5000); this.statusIndicator.updateStatus('reading', 50); await Utils.sleep(pauseDuration); this.logger.log(`模拟阅读暂停 ${pauseDuration}ms`, 'action'); } // 元素悬停模拟 simulateElementHover() { const hoverableElements = document.querySelectorAll('a, button, .btn, [role="button"]'); if (hoverableElements.length === 0) return; const randomElement = hoverableElements[Math.floor(Math.random() * hoverableElements.length)]; // 触发悬停事件 const mouseEnter = new MouseEvent('mouseenter', { bubbles: true }); const mouseOver = new MouseEvent('mouseover', { bubbles: true }); randomElement.dispatchEvent(mouseEnter); randomElement.dispatchEvent(mouseOver); // 短暂悬停后移开 setTimeout(() => { const mouseLeave = new MouseEvent('mouseleave', { bubbles: true }); const mouseOut = new MouseEvent('mouseout', { bubbles: true }); randomElement.dispatchEvent(mouseLeave); randomElement.dispatchEvent(mouseOut); }, Utils.getRandomInt(500, 2000)); this.logger.log('模拟元素悬停', 'action'); } // 键盘活动模拟 simulateKeyboardActivity() { const keys = ['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End']; const randomKey = keys[Math.floor(Math.random() * keys.length)]; const keyEvent = new KeyboardEvent('keydown', { key: randomKey, code: randomKey, bubbles: true }); document.dispatchEvent(keyEvent); this.logger.log(`模拟按键: ${randomKey}`, 'action'); } // 窗口大小调整模拟 simulateWindowResize() { // 模拟窗口大小变化事件 const resizeEvent = new Event('resize'); window.dispatchEvent(resizeEvent); this.logger.log('模拟窗口调整', 'action'); } // 随机行为触发 async triggerRandomBehavior() { if (!this.isSimulating) return; const behaviors = [ { action: () => this.simulateMouseMovement(), weight: 3 }, { action: () => this.simulatePageInteraction(), weight: 2 }, { action: () => this.simulateScrollPause(), weight: 1 } ]; // 权重随机选择 const totalWeight = behaviors.reduce((sum, b) => sum + b.weight, 0); let random = Math.random() * totalWeight; for (const behavior of behaviors) { random -= behavior.weight; if (random <= 0) { await behavior.action(); break; } } // 随机间隔后再次触发 const nextDelay = Utils.getRandomInt(5000, 15000); setTimeout(() => this.triggerRandomBehavior(), nextDelay); } // 缓动函数 easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } // 开始行为模拟 start() { if (this.isSimulating) return; this.isSimulating = true; this.logger.log('开始智能行为模拟', 'action'); // 启动随机行为 setTimeout(() => this.triggerRandomBehavior(), Utils.getRandomInt(2000, 5000)); } // 停止行为模拟 stop() { this.isSimulating = false; this.logger.log('停止智能行为模拟', 'action'); } } // ===== 增强滚动系统类 ===== class EnhancedScrollSystem { constructor(config, logger, statusIndicator, behaviorSimulator) { this.config = config; this.logger = logger; this.statusIndicator = statusIndicator; this.behaviorSimulator = behaviorSimulator; this.scrollInterval = null; this.checkScrollTimeout = null; this.lastScrollY = 0; this.scrollDirection = 'down'; this.isScrolling = false; this.scrollStartTime = 0; } // 贝塞尔曲线分段滚动 (向下) async scrollWithBezier() { if (this.scrollInterval !== null) { clearInterval(this.scrollInterval); } const startY = window.scrollY; const documentHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); const windowHeight = window.innerHeight; const pageBottom = documentHeight - windowHeight; // 检查是否已接近底部 if (startY >= pageBottom - 10) { this.logger.log("已接近页面底部，停止向下滚动", 'info'); this.statusIndicator.updateStatus('idle'); return false; } this.isScrolling = true; this.scrollStartTime = Date.now(); this.statusIndicator.updateStatus('scrolling', 0); // 计算滚动参数 const segmentDistance = Utils.getRandomInt( this.config.get('scrollSegmentDistanceMin'), this.config.get('scrollSegmentDistanceMax') ); const targetY = Math.min(startY + segmentDistance, pageBottom); const totalDuration = Utils.getRandomInt( this.config.get('scrollSegmentDurationMin'), this.config.get('scrollSegmentDurationMax') ); const steps = Math.max(20, Math.round(totalDuration / 50)); const stepDuration = totalDuration / steps; // 随机贝塞尔控制点 const p0 = 0, p3 = 1; const p1 = Utils.getRandomFloat(this.config.get('bezierP1Min'), this.config.get('bezierP1Max')); const p2 = Utils.getRandomFloat(this.config.get('bezierP2Min'), this.config.get('bezierP2Max')); this.logger.log(`向下滚动: ${Math.round(startY)} → ${Math.round(targetY)} (${Math.round(targetY - startY)}px, ${totalDuration}ms)`, 'action'); let currentStep = 0; return new Promise((resolve) => { this.scrollInterval = setInterval(async () => { if (currentStep >= steps) { clearInterval(this.scrollInterval); this.scrollInterval = null; this.isScrolling = false; window.scrollTo(0, targetY); this.statusIndicator.updateStatus('idle'); this.logger.log("向下滚动完成", 'success'); // 随机触发行为模拟 if (Math.random() < 0.3) { await this.behaviorSimulator.simulatePageInteraction(); } resolve(true); return; } const t = currentStep / steps; const progress = Utils.cubicBezier(t, p0, p1, p2, p3); const scrollPosition = startY + (targetY - startY) * progress; window.scrollTo(0, scrollPosition); // 更新进度 const scrollProgress = (currentStep / steps) * 100; this.statusIndicator.updateStatus('scrolling', scrollProgress); currentStep++; // 随机暂停 if (Math.random() < this.config.get('randomPauseProbability')) { const pauseDuration = Utils.getRandomInt( this.config.get('randomPauseDurationMin'), this.config.get('randomPauseDurationMax') ); await Utils.sleep(pauseDuration); } }, stepDuration); }); } // 贝塞尔曲线分段滚动 (向上) async scrollUpWithBezier() { if (this.scrollInterval !== null) { clearInterval(this.scrollInterval); } const startY = window.scrollY; // 检查是否已接近顶部 if (startY <= 10) { this.logger.log("已接近页面顶部，停止向上滚动", 'info'); this.statusIndicator.updateStatus('idle'); return false; } this.isScrolling = true; this.scrollStartTime = Date.now(); this.statusIndicator.updateStatus('scrolling', 0); // 计算滚动参数 const segmentDistance = Utils.getRandomInt( this.config.get('scrollSegmentDistanceMin'), this.config.get('scrollSegmentDistanceMax') ); const targetY = Math.max(startY - segmentDistance, 0); const totalDuration = Utils.getRandomInt( this.config.get('scrollSegmentDurationMin'), this.config.get('scrollSegmentDurationMax') ); const steps = Math.max(20, Math.round(totalDuration / 50)); const stepDuration = totalDuration / steps; // 随机贝塞尔控制点 const p0 = 0, p3 = 1; const p1 = Utils.getRandomFloat(this.config.get('bezierP1Min'), this.config.get('bezierP1Max')); const p2 = Utils.getRandomFloat(this.config.get('bezierP2Min'), this.config.get('bezierP2Max')); this.logger.log(`向上滚动: ${Math.round(startY)} → ${Math.round(targetY)} (${Math.round(startY - targetY)}px, ${totalDuration}ms)`, 'action'); let currentStep = 0; return new Promise((resolve) => { this.scrollInterval = setInterval(async () => { if (currentStep >= steps) { clearInterval(this.scrollInterval); this.scrollInterval = null; this.isScrolling = false; window.scrollTo(0, targetY); this.statusIndicator.updateStatus('idle'); this.logger.log("向上滚动完成", 'success'); // 随机触发行为模拟 if (Math.random() < 0.3) { await this.behaviorSimulator.simulatePageInteraction(); } resolve(true); return; } const t = currentStep / steps; const progress = Utils.cubicBezier(t, p0, p1, p2, p3); const scrollPosition = startY - (startY - targetY) * progress; window.scrollTo(0, scrollPosition); // 更新进度 const scrollProgress = (currentStep / steps) * 100; this.statusIndicator.updateStatus('scrolling', scrollProgress); currentStep++; // 随机暂停 if (Math.random() < this.config.get('randomPauseProbability')) { const pauseDuration = Utils.getRandomInt( this.config.get('randomPauseDurationMin'), this.config.get('randomPauseDurationMax') ); await Utils.sleep(pauseDuration); } }, stepDuration); }); } // 智能滚动决策 async performIntelligentScroll() { if (this.isScrolling) return false; const documentHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); const windowHeight = window.innerHeight; const currentY = window.scrollY; const isAtBottom = currentY + windowHeight >= documentHeight - 10; const isAtTop = currentY <= 10; let scrollSuccess = false; if (this.scrollDirection === 'down') { if (isAtBottom) { this.logger.log("到达底部，切换为向上滚动", 'info'); this.scrollDirection = 'up'; scrollSuccess = await this.scrollUpWithBezier(); } else { scrollSuccess = await this.scrollWithBezier(); } } else { // scrollDirection === 'up' if (isAtTop) { this.logger.log("到达顶部，切换为向下滚动", 'info'); this.scrollDirection = 'down'; scrollSuccess = await this.scrollWithBezier(); } else { scrollSuccess = await this.scrollUpWithBezier(); } } return scrollSuccess; } // 停止所有滚动 stopScrolling() { if (this.scrollInterval) { clearInterval(this.scrollInterval); this.scrollInterval = null; } if (this.checkScrollTimeout) { clearTimeout(this.checkScrollTimeout); this.checkScrollTimeout = null; } this.isScrolling = false; this.statusIndicator.updateStatus('idle'); } // 检查是否卡住 isStuck() { const currentScrollY = window.scrollY; const scrollDelta = Math.abs(currentScrollY - this.lastScrollY); const timeSinceLastScroll = Date.now() - this.scrollStartTime; const isStuck = !this.isScrolling && scrollDelta < this.config.get('minScrollDelta') && timeSinceLastScroll > this.config.get('stuckTimeout'); if (!this.isScrolling) { this.lastScrollY = currentScrollY; } return isStuck; } } // ===== 数据分析类 ===== class DataAnalyzer { constructor(config, logger) { this.config = config; this.logger = logger; this.sessionData = { startTime: Date.now(), topicsVisited: [], scrollEvents: [], likeEvents: [], errorEvents: [], behaviorEvents: [] }; this.historicalData = this.loadHistoricalData(); } // 加载历史数据 loadHistoricalData() { try { const data = GM_getValue('analyticsData', null); const parsedData = data ? JSON.parse(data) : {}; const defaults = { totalSessions: 0, totalTopicsRead: 0, totalLikesGiven: 0, totalReadingTime: 0, dailyStats: {}, weeklyStats: {}, monthlyStats: {}, topicCategories: {}, readingPatterns: [], efficiencyMetrics: [] }; return { ...defaults, ...parsedData }; } catch (e) { return { totalSessions: 0, totalTopicsRead: 0, totalLikesGiven: 0, totalReadingTime: 0, dailyStats: {}, weeklyStats: {}, monthlyStats: {}, topicCategories: {}, readingPatterns: [], efficiencyMetrics: [] }; } } // 保存历史数据 saveHistoricalData() { try { GM_setValue('analyticsData', JSON.stringify(this.historicalData)); } catch (e) { this.logger.log('保存分析数据失败', 'error'); } } // 记录话题访问 recordTopicVisit(topicId, topicTitle, category, readTime) { const visitData = { topicId, topicTitle, category: category || 'unknown', readTime, timestamp: Date.now(), scrollCount: this.sessionData.scrollEvents.length, likeGiven: this.sessionData.likeEvents.some(e => e.topicId === topicId) }; this.sessionData.topicsVisited.push(visitData); this.updateHistoricalStats(visitData); this.logger.log(`记录话题访问: ${topicTitle} (${Utils.formatTime(readTime)})`, 'action'); } // 记录滚动事件 recordScrollEvent(direction, distance, duration) { const scrollData = { direction, distance, duration, timestamp: Date.now(), scrollSpeed: distance / duration }; this.sessionData.scrollEvents.push(scrollData); } // 记录点赞事件 recordLikeEvent(topicId, postId) { const likeData = { topicId, postId, timestamp: Date.now() }; this.sessionData.likeEvents.push(likeData); this.historicalData.totalLikesGiven++; } // 记录错误事件 recordErrorEvent(errorType, errorMessage, context) { const errorData = { type: errorType, message: errorMessage, context, timestamp: Date.now() }; this.sessionData.errorEvents.push(errorData); } // 记录行为事件 recordBehaviorEvent(behaviorType, details) { const behaviorData = { type: behaviorType, details, timestamp: Date.now() }; this.sessionData.behaviorEvents.push(behaviorData); } // 更新历史统计 updateHistoricalStats(visitData) { const today = new Date().toISOString().split('T')[0]; const thisWeek = this.getWeekKey(new Date()); const thisMonth = new Date().toISOString().substring(0, 7); // 更新总计 this.historicalData.totalTopicsRead++; this.historicalData.totalReadingTime += visitData.readTime; // 更新日统计 if (!this.historicalData.dailyStats[today]) { this.historicalData.dailyStats[today] = { topicsRead: 0, readingTime: 0, likesGiven: 0, categories: {} }; } this.historicalData.dailyStats[today].topicsRead++; this.historicalData.dailyStats[today].readingTime += visitData.readTime; // 更新周统计 if (!this.historicalData.weeklyStats[thisWeek]) { this.historicalData.weeklyStats[thisWeek] = { topicsRead: 0, readingTime: 0, likesGiven: 0 }; } this.historicalData.weeklyStats[thisWeek].topicsRead++; this.historicalData.weeklyStats[thisWeek].readingTime += visitData.readTime; // 更新月统计 if (!this.historicalData.monthlyStats[thisMonth]) { this.historicalData.monthlyStats[thisMonth] = { topicsRead: 0, readingTime: 0, likesGiven: 0 }; } this.historicalData.monthlyStats[thisMonth].topicsRead++; this.historicalData.monthlyStats[thisMonth].readingTime += visitData.readTime; // 更新分类统计 if (!this.historicalData.topicCategories[visitData.category]) { this.historicalData.topicCategories[visitData.category] = { count: 0, totalTime: 0, avgTime: 0 }; } const categoryStats = this.historicalData.topicCategories[visitData.category]; if (!categoryStats) { this.historicalData.topicCategories[visitData.category] = { count: 0, totalTime: 0, avgTime: 0 }; } categoryStats.count++; categoryStats.totalTime += visitData.readTime; categoryStats.avgTime = categoryStats.totalTime / categoryStats.count; } // 生成效率报告 generateEfficiencyReport() { const sessionDuration = Date.now() - this.sessionData.startTime; const topicsPerHour = (this.sessionData.topicsVisited.length / (sessionDuration / 3600000)).toFixed(2); const avgReadTime = this.sessionData.topicsVisited.length > 0 ? this.sessionData.topicsVisited.reduce((sum, t) => sum + t.readTime, 0) / this.sessionData.topicsVisited.length : 0; const report = { session: { duration: sessionDuration, topicsRead: this.sessionData.topicsVisited.length, topicsPerHour: parseFloat(topicsPerHour), avgReadTime, scrollEvents: this.sessionData.scrollEvents.length, likeEvents: this.sessionData.likeEvents.length, errorEvents: this.sessionData.errorEvents.length }, historical: { totalSessions: this.historicalData.totalSessions, totalTopicsRead: this.historicalData.totalTopicsRead, totalReadingTime: this.historicalData.totalReadingTime, avgTopicsPerSession: this.historicalData.totalSessions > 0 ? (this.historicalData.totalTopicsRead / this.historicalData.totalSessions).toFixed(2) : 0 }, trends: this.analyzeTrends(), recommendations: this.generateRecommendations() }; return report; } // 分析趋势 analyzeTrends() { const recentDays = Object.keys(this.historicalData.dailyStats) .sort() .slice(-7); if (recentDays.length < 2) return null; const firstDay = this.historicalData.dailyStats[recentDays[0]]; const lastDay = this.historicalData.dailyStats[recentDays[recentDays.length - 1]]; return { topicsReadTrend: lastDay.topicsRead - firstDay.topicsRead, readingTimeTrend: lastDay.readingTime - firstDay.readingTime, period: `${recentDays[0]} 到 ${recentDays[recentDays.length - 1]}` }; } // 生成建议 generateRecommendations() { const recommendations = []; const avgReadTime = this.sessionData.topicsVisited.length > 0 ? this.sessionData.topicsVisited.reduce((sum, t) => sum + t.readTime, 0) / this.sessionData.topicsVisited.length : 0; if (avgReadTime < 30000) { recommendations.push('建议增加每个话题的阅读时间，提高内容理解度'); } if (this.sessionData.errorEvents.length > 5) { recommendations.push('检测到较多错误，建议检查网络连接或调整配置'); } const scrollToReadRatio = this.sessionData.scrollEvents.length / Math.max(this.sessionData.topicsVisited.length, 1); if (scrollToReadRatio > 20) { recommendations.push('滚动频率较高，建议调整滚动参数以提高效率'); } return recommendations; } // 获取周键 getWeekKey(date) { const year = date.getFullYear(); const week = this.getWeekNumber(date); return `${year}-W${week.toString().padStart(2, '0')}`; } // 获取周数 getWeekNumber(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); } // 结束会话 endSession() { this.historicalData.totalSessions++; this.saveHistoricalData(); const report = this.generateEfficiencyReport(); this.logger.log(`会话结束 - 阅读 ${report.session.topicsRead} 个话题，用时 ${Utils.formatTime(report.session.duration)}`, 'success'); return report; } // 导出分析数据 exportAnalyticsData() { const exportData = { sessionData: this.sessionData, historicalData: this.historicalData, report: this.generateEfficiencyReport(), exportTime: new Date().toISOString() }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `forum-assistant-analytics-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.logger.log('分析数据已导出', 'success'); } } // ===== 安全和隐私增强类 ===== class SecurityManager { constructor(config, logger) { this.config = config; this.logger = logger; this.userAgents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15' ]; this.proxyList = []; this.currentFingerprint = this.generateFingerprint(); } // 生成随机指纹 generateFingerprint() { return { userAgent: this.getRandomUserAgent(), language: this.getRandomLanguage(), timezone: this.getRandomTimezone(), screen: this.getRandomScreen(), colorDepth: this.getRandomColorDepth(), platform: this.getRandomPlatform() }; } // 获取随机User-Agent getRandomUserAgent() { return this.userAgents[Math.floor(Math.random() * this.userAgents.length)]; } // 获取随机语言 getRandomLanguage() { const languages = ['zh-CN', 'zh-TW', 'en-US', 'en-GB', 'ja-JP', 'ko-KR']; return languages[Math.floor(Math.random() * languages.length)]; } // 获取随机时区 getRandomTimezone() { const timezones = [ 'Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Taipei', 'Asia/Tokyo', 'Asia/Seoul', 'America/New_York', 'Europe/London', 'Australia/Sydney' ]; return timezones[Math.floor(Math.random() * timezones.length)]; } // 获取随机屏幕分辨率 getRandomScreen() { const screens = [ { width: 1920, height: 1080 }, { width: 1366, height: 768 }, { width: 1440, height: 900 }, { width: 1536, height: 864 }, { width: 2560, height: 1440 } ]; return screens[Math.floor(Math.random() * screens.length)]; } // 获取随机颜色深度 getRandomColorDepth() { const depths = [24, 32]; return depths[Math.floor(Math.random() * depths.length)]; } // 获取随机平台 getRandomPlatform() { const platforms = ['Win32', 'MacIntel', 'Linux x86_64']; return platforms[Math.floor(Math.random() * platforms.length)]; } // 随机化请求头 randomizeHeaders(baseHeaders = {}) { const randomHeaders = { 'User-Agent': this.currentFingerprint.userAgent, 'Accept-Language': `${this.currentFingerprint.language},en;q=0.9`, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'DNT': Math.random() > 0.5 ? '1' : '0', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Cache-Control': Math.random() > 0.5 ? 'no-cache' : 'max-age=0' }; // 随机添加一些可选头 if (Math.random() > 0.7) { randomHeaders['Sec-CH-UA'] = this.generateSecChUa(); } if (Math.random() > 0.8) { randomHeaders['Sec-CH-UA-Mobile'] = '?0'; randomHeaders['Sec-CH-UA-Platform'] = `"${this.currentFingerprint.platform}"`; } return { ...randomHeaders, ...baseHeaders }; } // 生成Sec-CH-UA头 generateSecChUa() { const brands = [ '"Google Chrome";v="120", "Chromium";v="120", "Not_A Brand";v="99"', '"Google Chrome";v="119", "Chromium";v="119", "Not_A Brand";v="99"', '"Microsoft Edge";v="120", "Chromium";v="120", "Not_A Brand";v="99"' ]; return brands[Math.floor(Math.random() * brands.length)]; } // 增强的fetch请求 async secureRequest(url, options = {}) { if (!this.config.get('enableSafetyFeatures')) { return fetch(url, options); } // 随机延迟 const delay = Utils.getRandomInt(100, 500); await Utils.sleep(delay); // 随机化请求头 const secureHeaders = this.randomizeHeaders(options.headers || {}); const secureOptions = { ...options, headers: secureHeaders }; // 如果启用代理 if (this.config.get('proxyEnabled') && this.proxyList.length > 0) { // 这里可以添加代理逻辑 this.logger.log('使用代理发送请求', 'action'); } try { const response = await fetch(url, secureOptions); // 记录请求 this.logger.log(`安全请求: ${url.substring(0, 50)}...`, 'action'); return response; } catch (error) { this.logger.log(`安全请求失败: ${error.message}`, 'error'); throw error; } } // 行为指纹混淆 obfuscateBehavior() { if (!this.config.get('enableSafetyFeatures')) return; // 随机改变指纹 if (Math.random() < 0.1) { this.currentFingerprint = this.generateFingerprint(); this.logger.log('更新行为指纹', 'action'); } // 随机添加噪声事件 this.addNoiseEvents(); } // 添加噪声事件 addNoiseEvents() { const noiseEvents = [ () => this.simulateRandomClick(), () => this.simulateRandomKeyPress(), () => this.simulateRandomMouseMove(), () => this.simulateRandomScroll() ]; // 随机执行1-2个噪声事件 const eventCount = Utils.getRandomInt(1, 2); for (let i = 0; i < eventCount; i++) { const randomEvent = noiseEvents[Math.floor(Math.random() * noiseEvents.length)]; setTimeout(randomEvent, Utils.getRandomInt(100, 1000)); } } // 模拟随机点击 simulateRandomClick() { const x = Math.random() * window.innerWidth; const y = Math.random() * window.innerHeight; const clickEvent = new MouseEvent('click', { clientX: x, clientY: y, bubbles: false // 不冒泡，避免触发实际功能 }); // 创建一个不可见的元素来接收点击 const dummyElement = document.createElement('div'); dummyElement.style.position = 'absolute'; dummyElement.style.left = x + 'px'; dummyElement.style.top = y + 'px'; dummyElement.style.width = '1px'; dummyElement.style.height = '1px'; dummyElement.style.opacity = '0'; dummyElement.style.pointerEvents = 'none'; document.body.appendChild(dummyElement); dummyElement.dispatchEvent(clickEvent); setTimeout(() => { document.body.removeChild(dummyElement); }, 100); } // 模拟随机按键 simulateRandomKeyPress() { const keys = ['Tab', 'Shift', 'Control', 'Alt']; const randomKey = keys[Math.floor(Math.random() * keys.length)]; const keyEvent = new KeyboardEvent('keydown', { key: randomKey, bubbles: false }); document.dispatchEvent(keyEvent); } // 模拟随机鼠标移动 simulateRandomMouseMove() { const x = Math.random() * window.innerWidth; const y = Math.random() * window.innerHeight; const moveEvent = new MouseEvent('mousemove', { clientX: x, clientY: y, bubbles: false }); document.dispatchEvent(moveEvent); } // 模拟随机滚动 simulateRandomScroll() { const scrollEvent = new WheelEvent('wheel', { deltaY: Utils.getRandomInt(-100, 100), bubbles: false }); document.dispatchEvent(scrollEvent); } // 检测反爬虫机制 detectAntiBot() { const indicators = []; // 检测常见的反爬虫脚本 if (window.navigator.webdriver) { indicators.push('webdriver detected'); } if (window.chrome && window.chrome.runtime && window.chrome.runtime.onConnect) { // 可能是自动化浏览器 indicators.push('automation detected'); } // 检测异常的性能指标 if (performance.timing) { const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart; if (loadTime < 100) { indicators.push('suspicious load time'); } } if (indicators.length > 0) { this.logger.log(`检测到反爬虫指标: ${indicators.join(', ')}`, 'warn'); return true; } return false; } // 启动安全监控 startSecurityMonitoring() { if (!this.config.get('enableSafetyFeatures')) return; // 定期检测反爬虫 setInterval(() => { if (this.detectAntiBot()) { this.obfuscateBehavior(); } }, 30000); // 定期混淆行为 setInterval(() => { this.obfuscateBehavior(); }, Utils.getRandomInt(60000, 120000)); this.logger.log('安全监控已启动', 'success'); } // 清理痕迹 cleanupTraces() { try { // 清理localStorage中的敏感数据 const sensitiveKeys = ['topicList', 'latestPage', 'clickCounter']; sensitiveKeys.forEach(key => { if (Math.random() < 0.1) { // 10%概率清理 localStorage.removeItem(key); } }); // 清理sessionStorage if (Math.random() < 0.05) { // 5%概率清理 sessionStorage.clear(); } this.logger.log('执行痕迹清理', 'action'); } catch (e) { // 忽略清理错误 } } } // ===== 多站点适配器类 ===== class SiteAdapter { constructor(config, logger) { this.config = config; this.logger = logger; this.currentSite = this.detectSite(); this.siteConfigs = this.initializeSiteConfigs(); } // 检测当前站点 detectSite() { const url = window.location.href; const hostname = window.location.hostname; const siteMap = { 'linux.do': 'linuxdo', 'meta.discourse.org': 'discourse-meta', 'meta.appinn.net': 'appinn', 'community.openai.com': 'openai', 'nodeloc.cc': 'nodeloc', 'bbs.tampermonkey.net.cn': 'tampermonkey', 'greasyfork.org': 'greasyfork' }; for (const [domain, siteId] of Object.entries(siteMap)) { if (hostname.includes(domain)) { this.logger.log(`检测到站点: ${siteId}`, 'info'); return siteId; } } // 尝试检测Discourse论坛 if (this.isDiscourse()) { this.logger.log('检测到Discourse论坛', 'info'); return 'discourse-generic'; } this.logger.log('未知站点类型', 'warn'); return 'unknown'; } // 检测是否为Discourse论坛 isDiscourse() { return !!( document.querySelector('meta[name="discourse_theme_ids"]') || document.querySelector('meta[name="discourse_current_homepage"]') || document.querySelector('#discourse-modal') || window.Discourse || document.body.classList.contains('discourse') ); } // 初始化站点配置 initializeSiteConfigs() { return { 'linuxdo': { name: 'Linux.do', type: 'discourse', apiEndpoint: '/latest.json', topicUrlPattern: '/t/topic/{id}/{post}', selectors: { likeButton: '.discourse-reactions-reaction-button:not(.my-reaction)', topicTitle: '.fancy-title', postContent: '.cooked', userAvatar: '.avatar', replyButton: '.reply' }, features: { reactions: true, categories: true, tags: true, privateMessages: true }, limits: { maxComments: 1000, maxLikes: 50, readTimeMin: 45000, readTimeMax: 120000 } }, 'nodeloc': { name: 'NodeLoc', type: 'discourse', apiEndpoint: '/latest.json', topicUrlPattern: '/t/topic/{id}/{post}', selectors: { likeButton: '.discourse-reactions-reaction-button:not(.my-reaction)', topicTitle: '.fancy-title', postContent: '.cooked' }, features: { reactions: true, categories: true }, limits: { maxComments: 800, maxLikes: 40, readTimeMin: 30000, readTimeMax: 90000 } }, 'discourse-meta': { name: 'Discourse Meta', type: 'discourse', apiEndpoint: '/latest.json', topicUrlPattern: '/t/{slug}/{id}/{post}', selectors: { likeButton: '.like-button:not(.has-like)', topicTitle: '.fancy-title', postContent: '.cooked' }, features: { likes: true, categories: true, tags: true }, limits: { maxComments: 1500, maxLikes: 60, readTimeMin: 60000, readTimeMax: 150000 } }, 'greasyfork': { name: 'Greasy Fork', type: 'custom', apiEndpoint: null, selectors: { scriptLink: 'a[href*="/scripts/"]', scriptTitle: '.script-name', scriptDescription: '.script-description' }, features: { scripts: true, reviews: true }, limits: { maxScripts: 100, readTimeMin: 20000, readTimeMax: 60000 } }, 'unknown': { name: 'Unknown Site', type: 'generic', apiEndpoint: null, selectors: { likeButton: '.like, .upvote, [data-action="like"]', content: 'article, .post, .content' }, features: {}, limits: { maxComments: 500, maxLikes: 20, readTimeMin: 30000, readTimeMax: 90000 } } }; } // 获取当前站点配置 getSiteConfig() { return this.siteConfigs[this.currentSite] || this.siteConfigs['unknown']; } // 获取API端点 getApiEndpoint(endpoint) { const siteConfig = this.getSiteConfig(); const baseUrl = `${window.location.protocol}//${window.location.hostname}`; switch (endpoint) { case 'latest': return siteConfig.apiEndpoint ? `${baseUrl}${siteConfig.apiEndpoint}` : null; case 'topics': return `${baseUrl}/topics.json`; case 'categories': return `${baseUrl}/categories.json`; default: return null; } } // 构建话题URL buildTopicUrl(topicId, postNumber = 1) { const siteConfig = this.getSiteConfig(); const baseUrl = `${window.location.protocol}//${window.location.hostname}`; if (siteConfig.topicUrlPattern) { return `${baseUrl}${siteConfig.topicUrlPattern .replace('{id}', topicId) .replace('{post}', postNumber) .replace('{slug}', 'topic')}`; } return `${baseUrl}/t/${topicId}`; } // 获取选择器 getSelector(type) { const siteConfig = this.getSiteConfig(); return siteConfig.selectors[type] || null; } // 检查功能支持 hasFeature(feature) { const siteConfig = this.getSiteConfig(); return siteConfig.features[feature] || false; } // 获取限制配置 getLimit(type) { const siteConfig = this.getSiteConfig(); return siteConfig.limits[type] || 0; } // 站点特定的话题获取 async fetchTopics(page = 0) { const apiUrl = this.getApiEndpoint('latest'); if (!apiUrl) { this.logger.log('当前站点不支持API获取话题', 'warn'); return []; } try { const url = `${apiUrl}?no_definitions=true&page=${page}`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); const topics = data?.topic_list?.topics || []; // 站点特定的过滤逻辑 return this.filterTopics(topics); } catch (error) { this.logger.log(`获取话题失败: ${error.message}`, 'error'); return []; } } // 过滤话题 filterTopics(topics) { const maxComments = this.getLimit('maxComments'); return topics.filter(topic => { if (!topic || !topic.id) return false; if (topic.posts_count > maxComments) return false; if (topic.closed || topic.archived) return false; // 站点特定过滤 switch (this.currentSite) { case 'linuxdo': // 过滤掉某些分类 if (topic.category_id && [1, 2].includes(topic.category_id)) return false; break; case 'nodeloc': // 只保留特定分类 if (topic.category_id && ![5, 6, 7].includes(topic.category_id)) return false; break; } return true; }); } // 站点特定的点赞逻辑 async performLike(element) { const siteConfig = this.getSiteConfig(); if (!this.hasFeature('reactions') && !this.hasFeature('likes')) { return false; } try { // 检查元素是否可点击 if (element.classList.contains('my-reaction') || element.classList.contains('has-like') || element.disabled) { return false; } // 模拟点击 const event = new MouseEvent('click', { bubbles: true, cancelable: true }); element.dispatchEvent(event); // 等待响应 await Utils.sleep(Utils.getRandomInt(500, 1500)); return true; } catch (error) { this.logger.log(`点赞失败: ${error.message}`, 'error'); return false; } } // 获取页面信息 getPageInfo() { const siteConfig = this.getSiteConfig(); return { site: this.currentSite, siteName: siteConfig.name, type: siteConfig.type, url: window.location.href, title: document.title, isTopicPage: this.isTopicPage(), topicId: this.extractTopicId(), hasLikeButtons: this.hasLikeButtons() }; } // 检查是否为话题页面 isTopicPage() { const patterns = [ /\/t\/[^/]+\/\d+/, // Discourse pattern /\/topic\/\d+/, // Generic topic pattern /\/thread\/\d+/, // Forum thread pattern /\/scripts\/\d+/ // Greasyfork script pattern ]; return patterns.some(pattern => pattern.test(window.location.pathname)); } // 提取话题ID extractTopicId() { const path = window.location.pathname; const matches = path.match(/\/(?:t|topic|thread|scripts)\/(?:[^/]+\/)?(\d+)/); return matches ? parseInt(matches[1]) : null; } // 检查是否有点赞按钮 hasLikeButtons() { const selector = this.getSelector('likeButton'); return selector ? document.querySelectorAll(selector).length > 0 : false; } } // ===== 主程序类 ===== class ForumAssistant { constructor() { this.config = new ConfigManager(); this.logger = new LogManager(this.config); this.statusIndicator = new StatusIndicator(this.config, this.logger); this.behaviorSimulator = new BehaviorSimulator(this.config, this.logger, this.statusIndicator); this.scrollSystem = new EnhancedScrollSystem(this.config, this.logger, this.statusIndicator, this.behaviorSimulator); this.dataAnalyzer = new DataAnalyzer(this.config, this.logger); this.securityManager = new SecurityManager(this.config, this.logger); this.siteAdapter = new SiteAdapter(this.config, this.logger); this.automationEngine = new AutomationEngine(this.config, this.logger, this.siteAdapter); // UI组件 this.statsPanel = new StatisticsPanel(this.config, this.logger); this.configPanel = new ConfigPanel(this.config, this.logger); this.upgradePanel = new UpgradeProgressPanel(this.config, this.logger); this.controlPanel = new ControlPanel(this.config, this.logger, this.statsPanel, this.configPanel, this.upgradePanel); // 状态变量 this.isRunning = false; this.currentTopic = null; this.topicStartTime = Date.now(); this.requiredReadTime = 0; this.mainLoop = null; this.stuckCheckInterval = null; this.initialize(); } // 初始化 async initialize() { try { this.logger.log('智能论坛助手 Pro 初始化中...', 'info'); // 等待DOM完全准备好 await this.waitForDOM(); // 创建UI (使用延迟确保DOM稳定) setTimeout(() => { try { this.logger.createLogWindow(); this.statusIndicator.create(); this.controlPanel.create(); if (this.config.get('showStatistics')) { this.statsPanel.show(); } // 恢复升级进度窗口状态 const upgradeProgressVisible = GM_getValue('upgradeProgressVisible', false); if (upgradeProgressVisible) { this.upgradePanel.show(); } } catch (uiError) { console.error('UI创建失败:', uiError); this.logger.log(`UI创建失败: ${uiError.message}`, 'error'); } }, 500); // 启动安全监控 this.securityManager.startSecurityMonitoring(); // 启动弹窗监控 this.startPopupMonitoring(); // 启动网络请求监控 this.startNetworkMonitoring(); // 检查站点兼容性 const pageInfo = this.siteAdapter.getPageInfo(); this.logger.log(`当前站点: ${pageInfo.siteName} (${pageInfo.type})`, 'info'); // 恢复运行状态 const wasRunning = GM_getValue('isReading', false); if (wasRunning) { await Utils.sleep(2000); // 等待页面完全加载 this.start(); } this.logger.log('初始化完成', 'success'); this.setupNavigationListener(); } catch (error) { console.error('初始化错误:', error); this.logger.log(`初始化失败: ${error.message}`, 'error'); } } // 设置导航监听器 setupNavigationListener() { this.lastUrl = window.location.href; const observer = new MutationObserver(() => { const currentUrl = window.location.href; if (currentUrl !== this.lastUrl) { this.handleUrlChange(currentUrl); } }); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('popstate', () => { this.handleUrlChange(window.location.href); }); this.logger.log('导航监听器已设置', 'info'); } // 处理URL变化 handleUrlChange(newUrl) { const newPath = new URL(newUrl).pathname; const oldPath = new URL(this.lastUrl).pathname; // 只有当pathname的主体部分发生变化时，才认为是真正的导航 const newPathBase = newPath.split('/').slice(0, 3).join('/'); const oldPathBase = oldPath.split('/').slice(0, 3).join('/'); if (newPathBase !== oldPathBase) { this.logger.log(`URL 路径发生变化: ${newPath}`, 'info'); this.lastUrl = newUrl; // 停止当前的活动 if (this.isRunning) { this.stop(); } // 延迟后重新启动，以确保页面内容已加载 setTimeout(() => { const pageInfo = this.siteAdapter.getPageInfo(); if (pageInfo.isTopicPage) { this.start(); } else { this.navigateToNextTopic(); } }, 2000); } } // 等待DOM准备就绪 async waitForDOM() { return new Promise((resolve) => { if (document.body && document.head) { resolve(); return; } const observer = new MutationObserver(() => { if (document.body && document.head) { observer.disconnect(); resolve(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); // 超时保护 setTimeout(() => { observer.disconnect(); resolve(); }, 5000); }); } // 开始运行 async start() { if (this.isRunning) return; this.isRunning = true; GM_setValue('isReading', true); // 安全更新控制面板状态 try { if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } } catch (e) { console.error('更新按钮状态失败:', e); } this.logger.log('开始自动阅读', 'success'); this.statusIndicator.updateStatus('loading'); // 启动行为模拟 if (this.config.get('enableAdvancedBehavior')) { this.behaviorSimulator.start(); } // 启动自动化引擎 if (this.config.get('autoReplyEnabled') || this.config.get('keywordMonitoring')) { this.automationEngine.start(); } // 开始主循环 this.startMainLoop(); // 启动卡住检测 this.startStuckDetection(); // 如果不在话题页面，导航到话题 const pageInfo = this.siteAdapter.getPageInfo(); if (!pageInfo.isTopicPage) { await this.navigateToNextTopic(); } else { this.startTopicReading(); } } // 停止运行 stop() { if (!this.isRunning) return; this.isRunning = false; GM_setValue('isReading', false); // 安全更新控制面板状态 try { if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } } catch (e) { console.error('更新按钮状态失败:', e); } this.logger.log('停止自动阅读', 'action'); this.statusIndicator.updateStatus('idle'); // 停止所有组件 this.scrollSystem.stopScrolling(); this.behaviorSimulator.stop(); this.automationEngine.stop(); // 清理定时器 if (this.mainLoop) { clearTimeout(this.mainLoop); this.mainLoop = null; } if (this.stuckCheckInterval) { clearInterval(this.stuckCheckInterval); this.stuckCheckInterval = null; } // 结束会话分析 try { const report = this.dataAnalyzer.endSession(); this.logger.log(`会话报告: 阅读${report.session.topicsRead}个话题`, 'info'); } catch (e) { console.error('生成会话报告失败:', e); } } // 开始主循环 startMainLoop() { const checkInterval = 3000; // 3秒检查一次 const mainCheck = async () => { if (!this.isRunning) return; try { await this.performMainCheck(); } catch (error) { this.logger.log(`主循环错误: ${error.message}`, 'error'); this.dataAnalyzer.recordErrorEvent('main_loop', error.message, 'main_check'); } // 安排下次检查 this.mainLoop = setTimeout(mainCheck, checkInterval); }; mainCheck(); } // 执行主要检查 async performMainCheck() { const now = Date.now(); const readDuration = now - this.topicStartTime; const maxIdleTime = Math.max(this.config.get('maxIdleTime'), this.requiredReadTime); // 检查是否需要切换话题 if (this.shouldSwitchTopic(readDuration, maxIdleTime)) { await this.navigateToNextTopic(); return; } // 执行滚动 if (!this.scrollSystem.isScrolling) { const scrollSuccess = await this.scrollSystem.performIntelligentScroll(); if (scrollSuccess) { this.dataAnalyzer.recordScrollEvent( this.scrollSystem.scrollDirection, this.config.get('scrollSegmentDistanceMin'), this.config.get('scrollSegmentDurationMin') ); } } // 执行点赞 if (GM_getValue('autoLikeEnabled', false)) { await this.performAutoLike(); } // 安全混淆 if (Math.random() < 0.1) { this.securityManager.obfuscateBehavior(); } } // 判断是否应该切换话题 shouldSwitchTopic(readDuration, maxIdleTime) { const documentHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); const windowHeight = window.innerHeight; const currentY = window.scrollY; const isAtBottom = currentY + windowHeight >= documentHeight - 10; const isAtTop = currentY <= 10; // 超时切换 if (readDuration > maxIdleTime) { this.logger.log(`阅读超时 (${Utils.formatTime(readDuration)})，切换话题`, 'info'); return true; } // 达到最小阅读时间且在页面边界 if (readDuration >= this.requiredReadTime && (isAtBottom || isAtTop)) { this.logger.log(`阅读完成 (${Utils.formatTime(readDuration)})，切换话题`, 'info'); return true; } return false; } // 导航到下一个话题 async navigateToNextTopic() { this.statusIndicator.updateStatus('switching'); try { // 记录当前话题的阅读数据 if (this.currentTopic) { const readTime = Date.now() - this.topicStartTime; this.dataAnalyzer.recordTopicVisit( this.currentTopic.id, this.currentTopic.title || document.title, this.currentTopic.category, readTime ); this.logger.updateStatistics('topicsRead'); } // 获取下一个话题 const nextTopic = await this.getNextTopic(); if (!nextTopic) { this.logger.log('没有更多话题，停止运行', 'warn'); this.stop(); return; } // 随机延迟 const delay = Utils.getRandomInt( this.config.get('minTopicChangeDelay'), this.config.get('maxTopicChangeDelay') ); this.logger.log(`等待 ${Utils.formatTime(delay)} 后切换到话题: ${nextTopic.title || nextTopic.id}`, 'info'); await Utils.sleep(delay); // 导航到新话题 const topicUrl = this.siteAdapter.buildTopicUrl(nextTopic.id, nextTopic.last_read_post_number || 1); // 在当前标签页中导航 window.location.href = topicUrl; } catch (error) { this.logger.log(`话题切换失败: ${error.message}`, 'error'); this.dataAnalyzer.recordErrorEvent('topic_switch', error.message, 'navigation'); // 重试 setTimeout(() => this.navigateToNextTopic(), 10000); } } // 开始话题阅读 startTopicReading() { const pageInfo = this.siteAdapter.getPageInfo(); this.currentTopic = { id: pageInfo.topicId, title: pageInfo.title, url: pageInfo.url }; this.topicStartTime = Date.now(); this.requiredReadTime = Utils.getRandomInt( this.config.get('minReadTimeLower'), this.config.get('minReadTimeUpper') ); this.scrollSystem.scrollDirection = 'down'; this.statusIndicator.updateStatus('reading'); this.logger.log(`开始阅读话题: ${this.currentTopic.title} (需要 ${Utils.formatTime(this.requiredReadTime)})`, 'info'); } // 获取下一个话题 async getNextTopic() { // 尝试从缓存获取 let topicList = GM_getValue('topicList', '[]'); try { topicList = JSON.parse(topicList); } catch (e) { topicList = []; } if (topicList.length === 0) { // 获取新话题 this.logger.log('话题列表为空，获取新话题...', 'info'); const newTopics = await this.fetchTopics(); if (newTopics.length === 0) { return null; } topicList = newTopics; GM_setValue('topicList', JSON.stringify(topicList)); } // 返回第一个话题并从列表中移除 const nextTopic = topicList.shift(); GM_setValue('topicList', JSON.stringify(topicList)); return nextTopic; } // 获取话题列表 async fetchTopics() { const topics = []; const maxRetries = 3; let page = GM_getValue('latestPage', 0); for (let retry = 0; retry < maxRetries; retry++) { try { page++; const pageTopics = await this.siteAdapter.fetchTopics(page); if (pageTopics.length > 0) { topics.push(...pageTopics); GM_setValue('latestPage', page); if (topics.length >= this.config.get('topicListLimit')) { break; } } else { // 没有更多话题，重置页码 GM_setValue('latestPage', 0); break; } } catch (error) { this.logger.log(`获取话题失败 (页面 ${page}): ${error.message}`, 'error'); await Utils.sleep(1000); } } this.logger.log(`获取到 ${topics.length} 个话题`, 'success'); return topics.slice(0, this.config.get('topicListLimit')); } // 检测是否有弹窗出现 checkForPopups() { // 需要忽略的弹窗类型（信息性弹窗，不应停止点赞） const ignoredPopupSelectors = [ '.user-card', // 用户信息卡片 '.user-tooltip', // 用户提示框 '.topic-entrance', // 话题入口 '.quote-button', // 引用按钮 '.post-menu', // 帖子菜单 '.dropdown-menu', // 下拉菜单 '.autocomplete', // 自动完成 '.suggestion-menu', // 建议菜单 '.emoji-picker', // 表情选择器 '.preview-popup' // 预览弹窗 ]; // 需要检测的弹窗选择器（会阻止操作的弹窗） const criticalPopupSelectors = [ '.modal', '.popup', '.dialog', '.overlay', '.alert', '.notification', '.toast', '[role="dialog"]', '[role="alertdialog"]', '.swal2-container', '.sweetalert2-container', '.layui-layer', '.layer-shade', '.ant-modal', '.el-dialog', '.v-dialog', '.q-dialog', // NodeLoc 特定的重要弹窗 '.fancybox-container', '.fancybox-overlay', '.ui-dialog', '.ui-widget-overlay', '.bootbox', '.confirm-dialog', '.error-dialog', '.warning-dialog' ]; // 首先检查是否是应该忽略的弹窗 for (const selector of ignoredPopupSelectors) { const popup = document.querySelector(selector); if (popup && this.isElementVisible(popup)) { // 这是信息性弹窗，不需要停止点赞 return null; } } // 检查关键弹窗 for (const selector of criticalPopupSelectors) { const popup = document.querySelector(selector); if (popup && this.isElementVisible(popup)) { return popup; } } // 检查是否有遮罩层（但排除用户卡片相关的） const overlays = document.querySelectorAll('div[style*="position: fixed"], div[style*="position:fixed"]'); for (const overlay of overlays) { // 跳过用户卡片相关的遮罩 if (overlay.id === 'user-card' || overlay.classList.contains('user-card') || overlay.closest('.user-card')) { continue; } const style = getComputedStyle(overlay); if (style.zIndex > 1000 && (style.backgroundColor !== 'rgba(0, 0, 0, 0)' || style.background !== 'none')) { return overlay; } } return null; } // 检查元素是否可见 isElementVisible(element) { if (!element) return false; const style = getComputedStyle(element); return element.style.display !== 'none' && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; } // 启动弹窗监控 startPopupMonitoring() { // 使用 MutationObserver 监控 DOM 变化 const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { this.checkNewElementForPopup(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); // 定期检查弹窗 setInterval(() => { this.performPopupCheck(); }, 3000); // 每3秒检查一次 this.logger.log('弹窗监控已启动', 'info'); } // 检查新元素是否为弹窗 checkNewElementForPopup(element) { const popupClasses = [ 'modal', 'popup', 'dialog', 'overlay', 'alert', 'notification', 'toast', 'swal2-container', 'sweetalert2-container', 'layui-layer', 'layer-shade', 'ant-modal', 'el-dialog', 'v-dialog', 'q-dialog', 'fancybox-container', 'fancybox-overlay', 'ui-dialog', 'ui-widget-overlay' ]; const hasPopupClass = popupClasses.some(className => element.classList && element.classList.contains(className) ); const hasPopupRole = element.getAttribute && (element.getAttribute('role') === 'dialog' || element.getAttribute('role') === 'alertdialog'); if (hasPopupClass || hasPopupRole) { setTimeout(() => { this.handlePopupDetected(element); }, 500); // 延迟500ms确保弹窗完全显示 } } // 定期执行弹窗检查 performPopupCheck() { const popup = this.checkForPopups(); if (popup) { this.handlePopupDetected(popup); } } // 处理检测到的弹窗 handlePopupDetected(popup) { const isLikeEnabled = GM_getValue('autoLikeEnabled', false); if (isLikeEnabled) { this.logger.log('🚨 检测到弹窗，自动停止点赞功能', 'warning'); GM_setValue('autoLikeEnabled', false); // 更新控制面板按钮状态 if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } // 显示通知 this.logger.showNotification('检测到弹窗，已自动停止点赞功能', 'warning'); // 记录弹窗信息 const popupInfo = { className: popup.className || '', id: popup.id || '', tagName: popup.tagName || '', timestamp: new Date().toLocaleString() }; this.logger.log(`弹窗详情: ${JSON.stringify(popupInfo)}`, 'info'); } } // 启动网络请求监控 startNetworkMonitoring() { // 监控 XMLHttpRequest const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url, ...args) { this._method = method; this._url = url; return originalXHROpen.apply(this, [method, url, ...args]); }; XMLHttpRequest.prototype.send = function(data) { const xhr = this; // 监听响应 xhr.addEventListener('readystatechange', () => { if (xhr.readyState === 4) { this.handleNetworkResponse(xhr._method, xhr._url, xhr.status, xhr.responseText); } }); return originalXHRSend.apply(this, [data]); }.bind(this); // 监控 fetch 请求 const originalFetch = window.fetch; window.fetch = async function(url, options = {}) { const method = options.method || 'GET'; try { const response = await originalFetch.apply(this, [url, options]); // 检查响应 if (response.status === 429) { const responseText = await response.clone().text(); this.handleNetworkResponse(method, url, response.status, responseText); } return response; } catch (error) { return Promise.reject(error); } }.bind(this); this.logger.log('网络请求监控已启动', 'info'); } // 处理网络响应 handleNetworkResponse(method, url, status, responseText) { // 检查是否是点赞相关的请求 const isLikeRequest = url && ( url.includes('discourse-reactions') || url.includes('custom-reactions') || url.includes('/like') || url.includes('/heart') || url.includes('/toggle') ); if (isLikeRequest && status === 429) { this.handleRateLimitError(responseText); } } // 处理点赞限制错误 handleRateLimitError(responseText) { try { const errorData = JSON.parse(responseText); if (errorData.errors && errorData.errors.length > 0) { const errorMessage = errorData.errors[0]; const timeLeft = errorData.extras?.time_left || '未知'; const waitSeconds = errorData.extras?.wait_seconds || 0; this.logger.log(`🚫 点赞限制触发`, 'error'); this.logger.log(`📝 限制详情: ${errorMessage}`, 'error'); this.logger.log(`⏰ 剩余等待时间: ${timeLeft}`, 'warning'); // 计算具体可点赞时间 if (waitSeconds > 0) { const canLikeTimestamp = Date.now() + (waitSeconds * 1000); const canLikeTime = new Date(canLikeTimestamp).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); this.logger.log(`📅 预计可点赞时间: ${canLikeTime}`, 'info'); } // 自动停止点赞功能 GM_setValue('autoLikeEnabled', false); // 保存限制信息 const rateLimitInfo = { message: errorMessage, timeLeft: timeLeft, waitSeconds: waitSeconds, timestamp: Date.now() }; GM_setValue('rateLimitInfo', rateLimitInfo); // 更新控制面板状态 if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } // 显示通知 this.logger.showNotification(`点赞已达限制，需等待 ${timeLeft}`, 'error'); this.logger.log('🛑 已自动停止点赞功能，避免继续触发限制', 'action'); // 显示详细的限制信息摘要 this.showRateLimitSummary(rateLimitInfo); } } catch (parseError) { this.logger.log('解析限制错误信息失败', 'error'); // 即使解析失败，也要停止点赞 GM_setValue('autoLikeEnabled', false); if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } this.logger.showNotification('检测到点赞限制，已自动停止', 'warning'); } } // 显示限制信息摘要 showRateLimitSummary(rateLimitInfo) { this.logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'info'); this.logger.log('📊 点赞限制信息摘要', 'info'); this.logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'info'); const currentTime = new Date().toLocaleString('zh-CN'); this.logger.log(`🕐 触发时间: ${currentTime}`, 'info'); if (rateLimitInfo.waitSeconds > 0) { const canLikeTime = this.getCanLikeTime(rateLimitInfo); this.logger.log(`📅 恢复时间: ${canLikeTime}`, 'info'); // 计算具体的小时和分钟 const hours = Math.floor(rateLimitInfo.waitSeconds / 3600); const minutes = Math.floor((rateLimitInfo.waitSeconds % 3600) / 60); if (hours > 0) { this.logger.log(`⏳ 等待时长: ${hours}小时${minutes}分钟`, 'info'); } else { this.logger.log(`⏳ 等待时长: ${minutes}分钟`, 'info'); } } this.logger.log(`💡 建议: 请在恢复时间后重新启用点赞功能`, 'info'); this.logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'info'); } // 获取可以点赞的具体时间 getCanLikeTime(rateLimitInfo) { if (!rateLimitInfo || !rateLimitInfo.waitSeconds || !rateLimitInfo.timestamp) { return '未知'; } const canLikeTimestamp = rateLimitInfo.timestamp + (rateLimitInfo.waitSeconds * 1000); const canLikeDate = new Date(canLikeTimestamp); return canLikeDate.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } // 执行自动点赞 async performAutoLike() { if (!this.siteAdapter.hasFeature('reactions') && !this.siteAdapter.hasFeature('likes')) { return; } // 检查是否启用了自动点赞 if (!GM_getValue('autoLikeEnabled', false)) { return; } // 检查是否在限制期内 const rateLimitInfo = GM_getValue('rateLimitInfo', null); if (rateLimitInfo && this.controlPanel.isRateLimitActive(rateLimitInfo)) { this.logger.log('点赞仍在限制期内，跳过自动点赞', 'warning'); return; } // 检查是否有弹窗出现 const popup = this.checkForPopups(); if (popup) { this.logger.log('检测到弹窗，自动停止点赞功能', 'warning'); GM_setValue('autoLikeEnabled', false); // 更新控制面板按钮状态 if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } return; } const likeSelector = this.siteAdapter.getSelector('likeButton'); if (!likeSelector) return; const likeButtons = document.querySelectorAll(likeSelector); const maxLikes = this.siteAdapter.getLimit('maxLikes'); const currentLikes = GM_getValue('dailyLikeCount', 0); if (currentLikes >= maxLikes) { return; } for (const button of likeButtons) { if (currentLikes >= maxLikes) break; // 在每次点赞前再次检查弹窗 const popupCheck = this.checkForPopups(); if (popupCheck) { this.logger.log('点赞过程中检测到弹窗，停止点赞', 'warning'); GM_setValue('autoLikeEnabled', false); // 更新控制面板按钮状态 if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } break; } const success = await this.siteAdapter.performLike(button); if (success) { GM_setValue('dailyLikeCount', currentLikes + 1); this.dataAnalyzer.recordLikeEvent(this.currentTopic?.id, null); this.logger.updateStatistics('likesGiven'); this.logger.log(`点赞成功 (${currentLikes + 1}/${maxLikes})`, 'success'); // 随机延迟 await Utils.sleep(Utils.getRandomInt(2000, 5000)); } } } // 启动卡住检测 startStuckDetection() { this.stuckCheckInterval = setInterval(() => { if (!this.isRunning) return; if (this.scrollSystem.isStuck()) { this.logger.log('检测到页面卡住，强制切换话题', 'warn'); this.navigateToNextTopic(); } }, this.config.get('stuckTimeout')); } } // ===== 自动化引擎类 ===== class AutomationEngine { constructor(config, logger, siteAdapter) { this.config = config; this.logger = logger; this.siteAdapter = siteAdapter; this.isRunning = false; this.keywordPatterns = []; this.autoReplyTemplates = []; this.monitoringInterval = null; this.initializeTemplates(); } // 初始化模板 initializeTemplates() { this.autoReplyTemplates = [ { trigger: ['谢谢', '感谢', 'thanks', 'thank you'], responses: ['不客气！', '很高兴能帮到你！', '互相帮助！', '🤝'], weight: 0.3 }, { trigger: ['问题', '求助', 'help', 'question'], responses: ['我来看看能不能帮到你', '这个问题很有意思', '让我想想...', '可以详细说说吗？'], weight: 0.2 }, { trigger: ['好的', '不错', 'good', 'nice', '赞'], responses: ['👍', '同感！', '确实如此', '说得对'], weight: 0.4 }, { trigger: ['教程', 'tutorial', '指南', 'guide'], responses: ['很实用的教程！', '学到了，感谢分享', '收藏了！', '这个教程很详细'], weight: 0.5 } ]; // 加载用户自定义关键词 const customKeywords = this.config.get('monitorKeywords') || []; this.keywordPatterns = customKeywords.map(keyword => ({ pattern: new RegExp(keyword, 'i'), keyword: keyword, action: this.config.get('keywordAction') || 'notify' })); } // 启动自动化引擎 start() { if (this.isRunning) return; this.isRunning = true; this.logger.log('自动化引擎已启动', 'success'); // 启动关键词监控 if (this.config.get('keywordMonitoring')) { this.startKeywordMonitoring(); } // 启动自动回复监控 if (this.config.get('autoReplyEnabled')) { this.startAutoReplyMonitoring(); } } // 停止自动化引擎 stop() { if (!this.isRunning) return; this.isRunning = false; this.logger.log('自动化引擎已停止', 'action'); if (this.monitoringInterval) { clearInterval(this.monitoringInterval); this.monitoringInterval = null; } } // 启动关键词监控 startKeywordMonitoring() { this.monitoringInterval = setInterval(() => { this.scanForKeywords(); }, 5000); // 每5秒扫描一次 this.logger.log(`关键词监控已启动，监控 ${this.keywordPatterns.length} 个关键词`, 'info'); } // 扫描关键词 scanForKeywords() { if (!this.isRunning || this.keywordPatterns.length === 0) return; // 获取页面文本内容 const contentSelector = this.siteAdapter.getSelector('postContent') || 'article, .post, .content'; const contentElements = document.querySelectorAll(contentSelector); contentElements.forEach((element, index) => { // 避免重复扫描 if (element.dataset.scanned) return; element.dataset.scanned = 'true'; const text = element.textContent.toLowerCase(); this.keywordPatterns.forEach(({ pattern, keyword, action }) => { if (pattern.test(text)) { this.handleKeywordMatch(keyword, action, element, text); } }); }); } // 处理关键词匹配 async handleKeywordMatch(keyword, action, element, text) { this.logger.log(`检测到关键词: ${keyword}`, 'success'); switch (action) { case 'like': await this.performAutoLike(element); break; case 'reply': await this.performAutoReply(element, keyword); break; case 'notify': this.showKeywordNotification(keyword, text.substring(0, 100)); break; case 'collect': await this.collectPost(element); break; } } // 执行自动点赞（关键词触发） async performAutoLike(element) { // 检查是否启用了自动点赞 if (!GM_getValue('autoLikeEnabled', false)) { return; } // 检查是否有弹窗出现 const popup = this.checkForPopups(); if (popup) { this.logger.log('检测到弹窗，停止关键词触发的点赞', 'warning'); GM_setValue('autoLikeEnabled', false); // 更新控制面板按钮状态 if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } return; } const likeButton = element.querySelector(this.siteAdapter.getSelector('likeButton')); if (likeButton) { const success = await this.siteAdapter.performLike(likeButton); if (success) { this.logger.log('关键词触发自动点赞', 'success'); } } } // 检测是否有弹窗出现（AutomationEngine版本） checkForPopups() { // 需要忽略的弹窗类型（信息性弹窗，不应停止点赞） const ignoredPopupSelectors = [ '.user-card', // 用户信息卡片 '.user-tooltip', // 用户提示框 '.topic-entrance', // 话题入口 '.quote-button', // 引用按钮 '.post-menu', // 帖子菜单 '.dropdown-menu', // 下拉菜单 '.autocomplete', // 自动完成 '.suggestion-menu', // 建议菜单 '.emoji-picker', // 表情选择器 '.preview-popup' // 预览弹窗 ]; // 需要检测的弹窗选择器（会阻止操作的弹窗） const criticalPopupSelectors = [ '.modal', '.popup', '.dialog', '.overlay', '.alert', '.notification', '.toast', '[role="dialog"]', '[role="alertdialog"]', '.swal2-container', '.sweetalert2-container', '.layui-layer', '.layer-shade', '.ant-modal', '.el-dialog', '.v-dialog', '.q-dialog', // NodeLoc 特定的重要弹窗 '.fancybox-container', '.fancybox-overlay', '.ui-dialog', '.ui-widget-overlay', '.bootbox', '.confirm-dialog', '.error-dialog', '.warning-dialog' ]; // 首先检查是否是应该忽略的弹窗 for (const selector of ignoredPopupSelectors) { const popup = document.querySelector(selector); if (popup && this.isElementVisible(popup)) { // 这是信息性弹窗，不需要停止点赞 return null; } } // 检查关键弹窗 for (const selector of criticalPopupSelectors) { const popup = document.querySelector(selector); if (popup && this.isElementVisible(popup)) { return popup; } } // 检查是否有遮罩层（但排除用户卡片相关的） const overlays = document.querySelectorAll('div[style*="position: fixed"], div[style*="position:fixed"]'); for (const overlay of overlays) { // 跳过用户卡片相关的遮罩 if (overlay.id === 'user-card' || overlay.classList.contains('user-card') || overlay.closest('.user-card')) { continue; } const style = getComputedStyle(overlay); if (style.zIndex > 1000 && (style.backgroundColor !== 'rgba(0, 0, 0, 0)' || style.background !== 'none')) { return overlay; } } return null; } // 检查元素是否可见 isElementVisible(element) { if (!element) return false; const style = getComputedStyle(element); return element.style.display !== 'none' && style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; } // 执行自动回复 async performAutoReply(element, keyword) { if (!this.siteAdapter.hasFeature('replies')) return; const replyButton = element.querySelector(this.siteAdapter.getSelector('replyButton')); if (!replyButton) return; // 生成回复内容 const replyContent = this.generateReplyContent(keyword); if (!replyContent) return; try { // 点击回复按钮 replyButton.click(); // 等待回复框出现 await Utils.sleep(1000); // 查找回复输入框 const replyInput = document.querySelector('textarea[placeholder*="回复"], .reply-area textarea, .composer-input'); if (replyInput) { // 模拟输入 await this.simulateTyping(replyInput, replyContent); // 查找发送按钮 const sendButton = document.querySelector('.btn-primary[title*="发送"], .submit-reply, .create'); if (sendButton && !sendButton.disabled) { // 随机延迟后发送 await Utils.sleep(Utils.getRandomInt(2000, 5000)); sendButton.click(); this.logger.log(`自动回复: ${replyContent}`, 'success'); } } } catch (error) { this.logger.log(`自动回复失败: ${error.message}`, 'error'); } } // 生成回复内容 generateReplyContent(keyword) { const matchingTemplates = this.autoReplyTemplates.filter(template => template.trigger.some(trigger => keyword.toLowerCase().includes(trigger.toLowerCase())) ); if (matchingTemplates.length === 0) { // 使用通用回复 const genericReplies = ['👍', '有道理', '学习了', '感谢分享']; return genericReplies[Math.floor(Math.random() * genericReplies.length)]; } // 根据权重选择模板 const totalWeight = matchingTemplates.reduce((sum, t) => sum + t.weight, 0); let random = Math.random() * totalWeight; for (const template of matchingTemplates) { random -= template.weight; if (random <= 0) { const responses = template.responses; return responses[Math.floor(Math.random() * responses.length)]; } } return null; } // 模拟打字 async simulateTyping(input, text) { input.focus(); input.value = ''; // 逐字符输入 for (let i = 0; i < text.length; i++) { input.value += text[i]; // 触发输入事件 const inputEvent = new Event('input', { bubbles: true }); input.dispatchEvent(inputEvent); // 随机延迟 await Utils.sleep(Utils.getRandomInt(50, 150)); } // 触发最终事件 const changeEvent = new Event('change', { bubbles: true }); input.dispatchEvent(changeEvent); } // 显示关键词通知 showKeywordNotification(keyword, context) { if (typeof GM_notification !== 'undefined') { GM_notification({ text: `检测到关键词: ${keyword}\n${context}...`, title: '智能论坛助手', timeout: 5000, onclick: () => { window.focus(); } }); } } // 收藏帖子 async collectPost(element) { // 查找收藏按钮 const bookmarkButton = element.querySelector('.bookmark, [title*="收藏"], [data-action="bookmark"]'); if (bookmarkButton && !bookmarkButton.classList.contains('bookmarked')) { bookmarkButton.click(); this.logger.log('自动收藏帖子', 'success'); await Utils.sleep(1000); } } // 启动自动回复监控 startAutoReplyMonitoring() { // 监控新帖子 const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { this.scanNewContent(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); this.logger.log('自动回复监控已启动', 'info'); } // 扫描新内容 scanNewContent(element) { if (!this.isRunning) return; // 检查是否为帖子内容 const postSelectors = ['.post', '.topic-post', '.cooked', 'article']; const isPost = postSelectors.some(selector => element.matches && element.matches(selector) ); if (isPost) { const text = element.textContent.toLowerCase(); // 检查是否应该自动回复 const shouldReply = this.autoReplyTemplates.some(template => template.trigger.some(trigger => text.includes(trigger.toLowerCase())) ); if (shouldReply && Math.random() < 0.3) { // 30%概率回复 setTimeout(() => { this.performAutoReply(element, text); }, Utils.getRandomInt(5000, 15000)); // 5-15秒后回复 } } } // 智能互动 async performSmartInteraction() { const interactions = [ () => this.likeRandomPosts(), () => this.followInterestingUsers(), () => this.bookmarkUsefulContent(), () => this.shareContent() ]; const randomInteraction = interactions[Math.floor(Math.random() * interactions.length)]; await randomInteraction(); } // 点赞随机帖子 async likeRandomPosts() { // 检查是否启用了自动点赞 if (!GM_getValue('autoLikeEnabled', false)) { return; } // 检查是否有弹窗出现 const popup = this.checkForPopups(); if (popup) { this.logger.log('检测到弹窗，停止随机点赞', 'warning'); GM_setValue('autoLikeEnabled', false); // 更新控制面板按钮状态 if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } return; } const likeButtons = document.querySelectorAll(this.siteAdapter.getSelector('likeButton')); const maxLikes = Math.min(3, likeButtons.length); for (let i = 0; i < maxLikes; i++) { // 在每次点赞前检查弹窗 const popupCheck = this.checkForPopups(); if (popupCheck) { this.logger.log('随机点赞过程中检测到弹窗，停止点赞', 'warning'); GM_setValue('autoLikeEnabled', false); // 更新控制面板按钮状态 if (this.controlPanel && typeof this.controlPanel.updateButtonStates === 'function') { this.controlPanel.updateButtonStates(); } break; } const randomButton = likeButtons[Math.floor(Math.random() * likeButtons.length)]; if (randomButton) { await this.siteAdapter.performLike(randomButton); await Utils.sleep(Utils.getRandomInt(2000, 5000)); } } } // 关注有趣的用户 async followInterestingUsers() { const userLinks = document.querySelectorAll('a[href*="/u/"], .user-link'); if (userLinks.length > 0 && Math.random() < 0.1) { // 10%概率 const randomUser = userLinks[Math.floor(Math.random() * userLinks.length)]; // 这里可以添加关注逻辑 this.logger.log('发现有趣用户', 'info'); } } // 收藏有用内容 async bookmarkUsefulContent() { const usefulKeywords = ['教程', '指南', '技巧', 'tutorial', 'guide', 'tip']; const posts = document.querySelectorAll('.post, .topic-post'); posts.forEach(async (post) => { const text = post.textContent.toLowerCase(); const isUseful = usefulKeywords.some(keyword => text.includes(keyword)); if (isUseful && Math.random() < 0.2) { // 20%概率收藏 await this.collectPost(post); } }); } // 分享内容 async shareContent() { // 查找分享按钮 const shareButtons = document.querySelectorAll('.share, [title*="分享"], [data-action="share"]'); if (shareButtons.length > 0 && Math.random() < 0.05) { // 5%概率分享 const randomShare = shareButtons[Math.floor(Math.random() * shareButtons.length)]; randomShare.click(); this.logger.log('分享有趣内容', 'action'); } } } // ===== 每日任务管理器 ===== class DailyTaskManager { constructor(config, logger, forumAssistant) { this.config = config; this.logger = logger; this.forumAssistant = forumAssistant; this.checkInterval = null; } // 启动每日任务检查 start() { // 立即检查一次 this.checkDailyTasks(); // 每分钟检查一次 this.checkInterval = setInterval(() => { this.checkDailyTasks(); }, 60000); this.logger.log('每日任务管理器已启动', 'info'); } // 停止每日任务检查 stop() { if (this.checkInterval) { clearInterval(this.checkInterval); this.checkInterval = null; } } // 检查每日任务 checkDailyTasks() { const now = new Date(); const today = now.toISOString().split('T')[0]; const lastRunDate = GM_getValue('dailyTaskLastRunDate', ''); // 检查是否为新的一天 if (lastRunDate !== today) { this.resetDailyCounters(); GM_setValue('dailyTaskLastRunDate', today); } // 检查是否在自动启动时间范围内 (00:10-00:15) if (now.getHours() === 0 && now.getMinutes() >= 10 && now.getMinutes() < 15) { const autoStarted = GM_getValue('autoStartedToday', false); if (!autoStarted) { this.performDailyAutoStart(); GM_setValue('autoStartedToday', true); } } // 重置自动启动标志（在非启动时间） if (now.getHours() !== 0 || now.getMinutes() < 10 || now.getMinutes() >= 15) { GM_setValue('autoStartedToday', false); } } // 重置每日计数器 resetDailyCounters() { GM_setValue('dailyLikeCount', 0); GM_setValue('dailyTopicsRead', 0); GM_setValue('dailyErrors', 0); this.logger.log('每日计数器已重置', 'action'); } // 执行每日自动启动 performDailyAutoStart() { this.logger.log('执行每日自动启动任务', 'success'); try { // 启用自动点赞 GM_setValue('autoLikeEnabled', true); // 启动论坛助手 if (!this.forumAssistant.isRunning) { setTimeout(() => { this.forumAssistant.start(); }, Utils.getRandomInt(5000, 15000)); // 5-15秒后启动 } this.logger.log('每日自动启动完成', 'success'); } catch (error) { this.logger.log(`每日自动启动失败: ${error.message}`, 'error'); } } // 获取每日统计 getDailyStats() { return { likesGiven: GM_getValue('dailyLikeCount', 0), topicsRead: GM_getValue('dailyTopicsRead', 0), errors: GM_getValue('dailyErrors', 0), date: new Date().toISOString().split('T')[0] }; } } // ===== 全局变量和初始化 ===== let forumAssistant = null; let dailyTaskManager = null; // ===== 安全检查函数 ===== function safeElementAccess(element, property, defaultValue = null) { try { if (!element || typeof element !== 'object') return defaultValue; return element[property] || defaultValue; } catch (e) { console.warn('安全访问元素属性失败:', e); return defaultValue; } } function safeStyleAccess(element, property, value) { try { if (!element || !element.style) return false; element.style[property] = value; return true; } catch (e) { console.warn('安全设置样式失败:', e); return false; } } // ===== 主初始化函数 ===== function initializeForumAssistant() { // 防止重复初始化 if (window.forumAssistantInitialized) { console.log('智能论坛助手已经初始化，跳过重复初始化'); return; } try { console.log('开始初始化智能论坛助手 Pro...'); // 检查必要的API if (typeof GM_getValue === 'undefined') { console.warn('GM_getValue 不可用，使用 localStorage 作为备用'); window.GM_getValue = (key, defaultValue) => { try { const value = localStorage.getItem('gm_' + key); return value !== null ? JSON.parse(value) : defaultValue; } catch (e) { return defaultValue; } }; window.GM_setValue = (key, value) => { try { localStorage.setItem('gm_' + key, JSON.stringify(value)); } catch (e) { console.error('存储失败:', e); } }; window.GM_deleteValue = (key) => { try { localStorage.removeItem('gm_' + key); } catch (e) { console.error('删除失败:', e); } }; window.GM_listValues = () => { try { return Object.keys(localStorage).filter(key => key.startsWith('gm_')).map(key => key.substring(3)); } catch (e) { return []; } }; } // 创建主程序实例 forumAssistant = new ForumAssistant(); // 创建每日任务管理器 dailyTaskManager = new DailyTaskManager( forumAssistant.config, forumAssistant.logger, forumAssistant ); // 启动每日任务管理器 dailyTaskManager.start(); // 绑定全局事件 bindGlobalEvents(); // 将实例暴露到全局，供控制面板调用 window.forumAssistant = forumAssistant; // 标记初始化完成 window.forumAssistantInitialized = true; console.log('智能论坛助手 Pro 初始化完成'); } catch (error) { console.error('智能论坛助手初始化失败:', error); // 显示错误通知 if (typeof GM_notification !== 'undefined') { GM_notification({ text: `初始化失败: ${error.message}`, title: '智能论坛助手', timeout: 5000 }); } else { // 备用通知方式 setTimeout(() => { alert(`智能论坛助手初始化失败: ${error.message}`); }, 1000); } } } // ===== 绑定全局事件 ===== function bindGlobalEvents() { // 页面卸载时保存数据 window.addEventListener('beforeunload', () => { if (forumAssistant && forumAssistant.isRunning) { forumAssistant.dataAnalyzer.endSession(); } }); // 页面可见性变化 document.addEventListener('visibilitychange', () => { if (document.hidden) { // 页面隐藏时暂停某些功能 if (forumAssistant) { forumAssistant.behaviorSimulator.stop(); } } else { // 页面显示时恢复功能 if (forumAssistant && forumAssistant.isRunning) { forumAssistant.behaviorSimulator.start(); } } }); // 键盘快捷键 document.addEventListener('keydown', (e) => { // Ctrl+Shift+F: 开启/关闭论坛助手 if (e.ctrlKey && e.shiftKey && e.key === 'F') { e.preventDefault(); if (forumAssistant) { if (forumAssistant.isRunning) { forumAssistant.stop(); } else { forumAssistant.start(); } } } // Ctrl+Shift+C: 打开配置面板 if (e.ctrlKey && e.shiftKey && e.key === 'C') { e.preventDefault(); if (forumAssistant) { forumAssistant.configPanel.toggle(); } } // Ctrl+Shift+S: 显示/隐藏统计面板 if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); if (forumAssistant) { forumAssistant.statsPanel.toggle(); } } }); // 错误处理 window.addEventListener('error', (e) => { try { console.error('全局错误捕获:', e.message, e.filename, e.lineno); if (forumAssistant && forumAssistant.logger) { forumAssistant.logger.log(`全局错误: ${e.message} (${e.filename}:${e.lineno})`, 'error'); if (forumAssistant.dataAnalyzer) { forumAssistant.dataAnalyzer.recordErrorEvent('global', e.message, e.filename); } } } catch (err) { console.error('错误处理器本身出错:', err); } }); // 未处理的Promise拒绝 window.addEventListener('unhandledrejection', (e) => { try { console.error('未处理的Promise拒绝:', e.reason); if (forumAssistant && forumAssistant.logger) { forumAssistant.logger.log(`未处理的Promise拒绝: ${e.reason}`, 'error'); if (forumAssistant.dataAnalyzer) { forumAssistant.dataAnalyzer.recordErrorEvent('promise', e.reason, 'unhandled_rejection'); } } // 防止控制台错误 e.preventDefault(); } catch (err) { console.error('Promise拒绝处理器出错:', err); } }); } // ===== 脚本启动 ===== // 安全启动函数 function safeInitialize() { try { // 检查环境 if (typeof document === 'undefined') { console.error('document 对象不可用'); return; } // 检查是否在支持的页面 const supportedDomains = ['linux.do', 'meta.discourse.org', 'meta.appinn.net', 'community.openai.com', 'nodeloc.cc', 'bbs.tampermonkey.net.cn', 'greasyfork.org']; const currentDomain = window.location.hostname; const isSupported = supportedDomains.some(domain => currentDomain.includes(domain)); if (!isSupported) { console.log('当前域名不在支持列表中:', currentDomain); // 仍然尝试初始化，可能是新的Discourse站点 } console.log('开始安全初始化...'); initializeForumAssistant(); } catch (error) { console.error('安全初始化失败:', error); // 延迟重试 setTimeout(() => { console.log('尝试重新初始化...'); initializeForumAssistant(); }, 3000); } } // 多种启动方式确保脚本能够运行 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', safeInitialize); } else if (document.readyState === 'interactive' || document.readyState === 'complete') { // DOM已经加载完成，立即初始化 setTimeout(safeInitialize, 100); } // 备用启动方式1（window.load事件） window.addEventListener('load', () => { if (!window.forumAssistantInitialized) { console.log('备用启动方式1触发 (window.load)'); setTimeout(safeInitialize, 1000); } }); // 备用启动方式2（延迟启动） setTimeout(() => { if (!window.forumAssistantInitialized) { console.log('备用启动方式2触发 (延迟启动)'); safeInitialize(); } }, 5000); // 备用启动方式3（用户交互后启动） const interactionEvents = ['click', 'scroll', 'keydown']; const startOnInteraction = () => { if (!window.forumAssistantInitialized) { console.log('备用启动方式3触发 (用户交互)'); safeInitialize(); // 移除事件监听器 interactionEvents.forEach(event => { document.removeEventListener(event, startOnInteraction); }); } }; interactionEvents.forEach(event => { document.addEventListener(event, startOnInteraction, { once: true, passive: true }); }); })();