在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 = `
<div class="control-header">
<div class="control-title">自动滚动控制</div>
<button class="close-btn" id="closeControl">×</button>
</div>
<div class="buttons">
<button class="btn btn-start" id="startBtn">开始滚动</button>
<button class="btn btn-stop" id="stopBtn">停止滚动</button>
</div>`;
// 添加样式
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);
})();
坛友做的
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 = `
<div class="control-header">
<div class="control-title">自动滚动控制</div>
<button class="close-btn" id="closeControl">×</button>
</div>
<div class="control-group">
<label class="control-label">滚动速度: px/帧</label>
<div class="slider-container">
<input type="range" id="speedSlider" class="slider" min="1" max="10" value="${settings.scrollSpeed}">
<div class="value-display" id="speedDisplay">${settings.scrollSpeed}</div>
</div>
</div>
<div class="control-group">
<label class="control-label">刷新间隔: ms</label>
<div class="slider-container">
<input type="range" id="intervalSlider" class="slider" min="5" max="100" value="${settings.scrollInterval}">
<div class="value-display" id="intervalDisplay">${settings.scrollInterval}</div>
</div>
</div>
<div class="buttons">
<button class="btn btn-start" id="startBtn">开始滚动</button>
<button class="btn btn-stop" id="stopBtn">停止滚动</button>
</div>`;
// 添加样式
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 = `
<h3 style="margin-top: 0; color: inherit;">📊 数据管理</h3>
<div style="display: grid; gap: 15px;">
<div style="padding: 15px; border: 1px solid currentColor; border-radius: 8px; opacity: 0.8;">
<h4 style="margin: 0 0 10px 0;">配置管理</h4>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button id="export-config" style="padding: 8px 16px; border: 1px solid currentColor; background: transparent; color: inherit; border-radius: 4px; cursor: pointer;">导出配置</button>
<button id="import-config" style="padding: 8px 16px; border: 1px solid currentColor; background: transparent; color: inherit; border-radius: 4px; cursor: pointer;">导入配置</button>
<button id="reset-config" style="padding: 8px 16px; border: 1px solid #dc3545; background: transparent; color: #dc3545; border-radius: 4px; cursor: pointer;">重置配置</button>
</div>
</div>
<div style="padding: 15px; border: 1px solid currentColor; border-radius: 8px; opacity: 0.8;">
<h4 style="margin: 0 0 10px 0;">数据清理</h4>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button id="clear-logs" style="padding: 8px 16px; border: 1px solid currentColor; background: transparent; color: inherit; border-radius: 4px; cursor: pointer;">清空日志</button>
<button id="clear-cache" style="padding: 8px 16px; border: 1px solid currentColor; background: transparent; color: inherit; border-radius: 4px; cursor: pointer;">清空缓存</button>
<button id="clear-all" style="padding: 8px 16px; border: 1px solid #dc3545; background: transparent; color: #dc3545; border-radius: 4px; cursor: pointer;">清空所有数据</button>
</div>
</div>
</div>
`;
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 ? `<small style="opacity: 0.7; font-weight: normal;">(${config.desc})</small>` : ''}`;
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 = `
<h4 style="margin: 0 0 15px 0; color: inherit;">🔍 关键词监控设置</h4>
<div style="display: grid; gap: 10px;">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500;">监控关键词 (每行一个):</label>
<textarea id="keywords-input" style="width: 100%; height: 100px; padding: 8px; border: 1px solid currentColor; border-radius: 4px; background: transparent; color: inherit; resize: vertical;" placeholder="输入要监控的关键词,每行一个"></textarea>
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: 500;">触发动作:</label>
<select id="keyword-action" style="width: 100%; padding: 8px; border: 1px solid currentColor; border-radius: 4px; background: transparent; color: inherit;">
<option value="like">自动点赞</option>
<option value="reply">自动回复</option>
<option value="notify">仅通知</option>
<option value="collect">收藏帖子</option>
</select>
</div>
</div>
`;
// 加载已保存的关键词
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 => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0;">
<span style="opacity: 0.8;">${item.label}</span>
<span style="font-weight: 600;">${item.value}</span>
</div>
`).join('') + `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0;">
<span style="opacity: 0.8;">⚡ 能量值</span>
<span style="font-weight: 600;">${this.energyValue}</span>
</div>
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.1);">
<button id="reset-stats" style="width: 100%; padding: 6px 12px; border: 1px solid #dc3545;
background: #dc3545; color: white; border-radius: 4px; cursor: pointer; font-size: 11px;">
🔄 重置统计
</button>
</div>
`;
} 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 = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="opacity: 0.8;">👤 用户名:</span>
<span style="font-weight: 600;">${username || '未设置'}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="opacity: 0.8;">📊 当前进度:</span>
<span style="font-weight: 600;">${this.upgradeProgress}</span>
</div>
${this.unmetConditions ? `
<div style="margin-top: 8px; padding: 8px; background: rgba(255,193,7,0.1); border-radius: 4px; border-left: 3px solid #ffc107;">
<div style="font-size: 11px; font-weight: 600; margin-bottom: 4px; color: #f59e0b;">⚠️ 未完成条件:</div>
<div style="font-size: 10px; line-height: 1.4; opacity: 0.9;">
${this.unmetConditions.map(condition => `• ${condition}`).join('<br>')}
</div>
</div>
` : ''}
<div style="margin-top: 8px;">
<input type="text" id="custom-username" placeholder="输入自定义用户名"
value="${this.customUsername}"
style="width: 100%; padding: 6px 8px; border: 1px solid currentColor; border-radius: 4px;
background: transparent; color: inherit; font-size: 12px;">
</div>
<div style="margin-top: 8px; display: flex; gap: 8px;">
<button id="save-username" style="flex: 1; padding: 6px 12px; border: 1px solid #3182ce;
background: #3182ce; color: white; border-radius: 4px; cursor: pointer; font-size: 11px;">
保存用户名
</button>
<button id="refresh-progress" style="flex: 1; padding: 6px 12px; border: 1px solid currentColor;
background: transparent; color: inherit; border-radius: 4px; cursor: pointer; font-size: 11px;">
刷新进度
</button>
</div>
<div style="margin-top: 8px; display: flex; gap: 8px;">
<button id="test-api" style="flex: 1; padding: 6px 12px; border: 1px solid #e53e3e;
background: #e53e3e; color: white; border-radius: 4px; cursor: pointer; font-size: 11px;">
🔧 测试API
</button>
<button id="reset-position" style="flex: 1; padding: 6px 12px; border: 1px solid #6b7280;
background: #6b7280; color: white; border-radius: 4px; cursor: pointer; font-size: 11px;">
📍 重置位置
</button>
</div>
`;
// 绑定事件
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 });
});
})();
评论 (0)