首页
文章导航
留言板
友链
更多
关于
Search
1
常用安装脚本知识 [24年10月27日更新]
70 阅读
2
Win10怎么默认开启数字小键盘
68 阅读
3
网页制作常用代码 不断增加 [2025年3月31更新]
66 阅读
4
总结白嫖】DeepSeek R1 671B满血版-网页版+API版
55 阅读
5
自建不蒜子
54 阅读
默认
日常
学习
技术
登录
Search
标签搜索
安装
cloudflare
白嫖
脚本
CF
壁纸
图片
docker
Linux
Caddy
代码
哪吒
域名
节点
桌面壁纸
手机壁纸
NAT
LXC
优选
HTML
ws01
累计撰写
108
篇文章
累计收到
61
条评论
首页
栏目
默认
日常
学习
技术
页面
文章导航
留言板
友链
关于
搜索到
83
篇与
的结果
2025-06-08
在nodeloc.cc上的帖子使用,让chrome浏览器自动向下滑动刷时间
在nodeloc.cc上的帖子使用,让 chrome浏览器自动向下滑动刷时间 在Tampermonkey使用的代码,功能是开启后,让chrome浏览器自动向下滑动,模仿人在看帖子找一个回帖长的页面,如 2200多个回帖页 ,Tampermonkey自动刷时间开始,期间可以页面最小化后做其它事。3、NodeLOC控制面板【2025年7月2日更新,添加控制面板】// ==UserScript== // @name NodeLOC控制面板 // @namespace http://tampermonkey.net/ // @version v 0.3 // @description 自动向下、上滚动页面,模拟人在浏览NodeLOC上的帖子,并增加控制面板 // @author You // @match https://nodeloc.cc/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; // 配置参数 const config = { scrollInterval: 3000, // 滚动间隔(毫秒) scrollAmount: window.innerHeight * 1.8, // 每次滚动距离(像素) maxScrolls: 100 // 设置最大滚动次数,1次6个回帖左右,根据回帖多少设置 }; let scrollCount = 0; let scrollDirection = 1; // 滚动方向:1为向下,-1为向上 let isScrolling = false; // 创建控制UI function createControlUI() { // 创建控制面板元素 const controlPanel = document.createElement('div'); controlPanel.id = 'autoScrollControl'; document.body.appendChild(controlPanel); // 创建UI元素 controlPanel.innerHTML = ` <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); })(); {dotted startColor="#ff6c6c" endColor="#1989fa"/}坛友做的1.自动滚动辅助器,还行,有不足// ==UserScript== // @name 自动滚动辅助器 // @namespace https://example.com/ // @version 1.6 // @description 自动向下/向上滚动页面,到达底部时自动反向滚动 // @author DeepSeek-R1 // @match *://*/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function () { 'use strict'; // 配置参数 const defaultSettings = { scrollSpeed: 1, // 每次滚动的像素数 scrollInterval: 25, // 滚动间隔(ms) threshold: 100, // 底部/顶部检测阈值(px) enableUI: true // 是否显示控制UI }; // 加载用户设置 let settings = Object.assign({}, defaultSettings); if (GM_getValue('autoScrollSettings')) { settings = Object.assign(settings, GM_getValue('autoScrollSettings')); } // 状态变量 let scrollDirection = 1; // 1=向下, -1=向上 let scrollIntervalId = null; let isScrolling = false; // 创建控制UI function createControlUI() { // 创建控制面板元素 const controlPanel = document.createElement('div'); controlPanel.id = 'autoScrollControl'; document.body.appendChild(controlPanel); // 创建UI元素 controlPanel.innerHTML = ` <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 }); }); })();
2025年06月08日
16 阅读
0 评论
0 点赞
2025-06-08
移除NodeSeek推广
移除NodeSeek推广一、论坛扩展里面加入下面的代码就行了a.promotation-item:not([href*="nodeseek.com"]):not([href*="github.com"]):not([href*="nodequality.com"]) { display: none; }二、Adguard 推荐抄酒神的作业nodeseek.com###nsk-right-panel-container > div > a.promotation-item:matches-attr("href"=/^(?!^[^?#]*[nN][oO][dD][eE][sS][eE][eE][kK])(?!^[^?#]*[nN][oO][dD][eE][qQ][uU][aA][lL][iI][tT][yY]).*$/)三、油猴脚本 推荐抄酒神的作业// ==UserScript== // @name RemoveNodeSeekPromotions // @namespace http://tampermonkey.net/ // @version 1.0 // @description 移除右侧推广 // @author malibu // @match https://www.nodeseek.com/* // @grant none // @run-at document-start // ==/UserScript== (function() { 'use strict'; const removePromotionsByLinkPath = () => { const selector = '#nsk-right-panel-container a.promotation-item'; const adLinks = document.querySelectorAll(selector); if (adLinks.length === 0) { return; } adLinks.forEach(link => { if (link.style.display === 'none') { return; } const href = link.getAttribute('href'); if (!href || href.startsWith('javascript:')) { return; } try { const linkUrl = new URL(href, location.origin); const linkPart = (linkUrl.origin + linkUrl.pathname).toLowerCase(); const shouldKeep = linkPart.includes('nodeseek') || linkPart.includes('nodequality'); if (!shouldKeep) { link.style.setProperty('display', 'none', 'important'); } } catch (e) { } }); }; const observer = new MutationObserver(removePromotionsByLinkPath); observer.observe(document.documentElement, { childList: true, subtree: true }); })();
2025年06月08日
15 阅读
0 评论
0 点赞
2025-06-05
★★★用更轻量的 Caddy + PHP 快速部署 动、静态网站
★★★用更轻量的 Caddy + PHP 快速部署 动、静态网站 Caddy 是一个现代、高性能、自动 HTTPS 的 Web 服务器,比 Nginx 更简单,特别适合快速部署 PHP 网站。这是一个一步到位的自动化脚本,包括:安装 Caddy(来自官方仓库)安装 PHP-FPM 及扩展创建网站目录配置 Caddy 支持 PHP可自动启用 HTTPS(如果绑定域名){dotted startColor="#ff6c6c" endColor="#1989fa"/}一、在 Debian 11 上用 Caddy + PHP 快速部署动态 PHP 或静态html网站1、更新系统sudo apt update && sudo apt upgrade -y2、安装 PHP 和常用扩展sudo apt install -y php php-fpm php-mysql php-cli php-curl php-gd php-mbstring php-xml php-zip unzip{dotted startColor="#ff6c6c" endColor="#1989fa"/}3、安装 Caddya、必要的软件包1sudo apt install -y debian-keyring debian-archive-keyring curl必要的软件包2sudo apt install -y debian-keyring debian-archive-keyring apt-transport-httpsb、Caddy的安全密钥curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpgc、下载 Caddy密钥文件curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.listd、更新软件包列表sudo apt updatee、升级或安装Caddy2sudo apt install -y caddy{dotted startColor="#ff6c6c" endColor="#1989fa"/}4、创建网站目录,并上传静态html或动态php文件【网站文件在 home/html/web/】网站1sudo mkdir -p /home/html/web/web1 sudo chown -R www-data:www-data /home/html/web/web1 sudo chmod -R 755 /home/html/web/web1网站2sudo mkdir -p /home/html/web/web2 sudo chown -R www-data:www-data /home/html/web/web2 sudo chmod -R 755 /home/html/web/web2网站3sudo mkdir -p /home/html/web/web3 sudo chown -R www-data:www-data /home/html/web/web3 sudo chmod -R 755 /home/html/web/web3网站4sudo mkdir -p /home/html/web/web4 sudo chown -R www-data:www-data /home/html/web/web4 sudo chmod -R 755 /home/html/web/web4网站5sudo mkdir -p /home/html/web/web5 sudo chown -R www-data:www-data /home/html/web/web5 sudo chmod -R 755 /home/html/web/web5网站6sudo mkdir -p /home/html/web/web6 sudo chown -R www-data:www-data /home/html/web/web6 sudo chmod -R 755 /home/html/web/web6可以类似的创建更多网站5、配置 Caddy 支持 PHP【以两个网站为例,多个网站类似配置,可以不开cf小黃云】example1.com { root * /home/html/web/web1 #安装路径,备注什么网站 php_fastcgi unix//run/php/php-fpm.sock file_server } example2.com { root * /home/html/web/web2 #安装路径,备注什么网站 php_fastcgi unix//run/php/php-fpm.sock file_server } example3.com { reverse_proxy 127.0.0.1:8880 #反代,这里输入你想反代的服务器IP和端口 encode gzip } example4.com { redir https://naiyous.com{uri} #重定向,这里输入你想设置的重定向 }6、启动并启用 php*-fpm和Caddy,并设置开机自启【后面7、8不用】sudo systemctl enable php*-fpm sudo systemctl restart php*-fpm sudo systemctl enable caddy sudo systemctl restart caddy7、开机自启systemctl enable caddy重启Caddy2systemctl restart caddy8、停止Caddy2systemctl stop caddy{dotted startColor="#f70808" endColor="#f818fb"/}二、【不用数据库可不安装,比如 WordPress、Typecho要用到】Debian 11 默认的软件源中没有 mysql-server 包,或者没有启用适当的源。Debian 通常推荐使用 MariaDB 替代 MySQLA、使用 MariaDB(兼容 MySQL) 1、 升级等sudo apt update sudo apt install -y mariadb-server sudo systemctl enable mariadb sudo systemctl start mariadb设置 root 密码【一定要修改默认的rootpass123为强密码,至少18位英文、数字加字符】sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'rootpass123'; FLUSH PRIVILEGES;"2、登录 MariaDB:sudo mysql -u root -p # 然后输入密码:rootpass1233、创建数据库:CREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;你也可以:CREATE DATABASE mysite;4、给 root 用户授权(MariaDB 默认 root 用户已拥有所有权限)CREATE USER 'myuser'@'localhost' IDENTIFIED BY 'mypassword'; GRANT ALL PRIVILEGES ON mydb.* TO 'myuser'@'localhost'; FLUSH PRIVILEGES;5、示例配置(适用于 WordPress/phpMyAdmin 等)配置项 值数据库名 mydb数据库用户名 root(或 myuser)数据库密码 rootpass123(或 mypassword)数据库主机 localhost6、如何在安装程序中填写(例如 WordPress/phpMyAdmin),安装时根据提示进行数据库主机:localhost表前缀:wp_ (或保留默认)数据库用户名: root 数据库名: mydb 数据库密码: rootpass123 B、Oracle 官方源安装了 MySQL Server 8.0 ,和上面的MariaDB(兼容 MySQL)一样1、升级等sudo apt update sudo apt install -y wget lsb-release gnupg2、下载官方源配置包# 下载官方源配置包 wget https://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb3、安装该配置包【安装过程中要设置的 root 密码,记好,后面用到】# 安装该配置包 sudo dpkg -i mysql-apt-config_0.8.29-1_all.deb4、在弹出的菜单中,默认选中 MySQL 8.0,按回车确认5、再更新软件源# 再更新软件源 sudo apt update6、安装 MySQL Server# 安装 MySQL Server sudo apt install -y mysql-server7、登录 MySQL,然后输入你在安装时设置的 root 密码。sudo mysql -u root -p8、创建数据库,mydb 是你的网站或程序要使用的数据库名,可以根据需要修改。CREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;9、创建独立的数据库用户并授权CREATE USER 'myuser'@'localhost' IDENTIFIED BY 'mypassword'; GRANT ALL PRIVILEGES ON mydb.* TO 'myuser'@'localhost'; FLUSH PRIVILEGES;myuser 是你新建的数据库用户名mypassword 是你设置的密码localhost 表示只允许本地连接数据库10、(可选):查看创建情况SHOW DATABASES; SELECT user, host FROM mysql.user; SHOW GRANTS FOR 'myuser'@'localhost';数据库名 mydb数据库用户名 myuser密码 mypassword主机 localhost11、退出命令,随便一个都行exit; #退出quit; #退出{dotted startColor="#ff6c6c" endColor="#1989fa"/}一键部署脚本(保存为 setup_caddy_php.sh)#!/bin/bash set -e green() { echo -e "\033[32m$1\033[0m"; } green "▶ 更新系统..." sudo apt update && sudo apt upgrade -y green "▶ 安装 PHP 和常用扩展..." sudo apt install -y php php-fpm php-mysql php-cli php-curl php-gd php-mbstring php-xml php-zip unzip green "▶ 安装 Caddy..." sudo apt install -y debian-keyring debian-archive-keyring curl curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update sudo apt install -y caddy green "▶ 创建网站目录..." sudo mkdir -p /var/www/html/mysite sudo chown -R www-data:www-data /var/www/html/mysite green "▶ 写入 PHP 测试页..." cat <<EOF | sudo tee /var/www/html/mysite/index.php <?php phpinfo(); ?> EOF green "▶ 配置 Caddy 支持 PHP..." cat <<EOF | sudo tee /etc/caddy/Caddyfile :80 { root * /var/www/html/mysite php_fastcgi unix//run/php/php-fpm.sock file_server } EOF green "▶ 启动并启用 Caddy..." sudo systemctl enable php*-fpm sudo systemctl restart php*-fpm sudo systemctl restart caddy green "✅ 成功部署!请访问 http://你的-VPS-IP 查看 PHP 测试页。"二、使用方法步骤 1:上传并运行脚本nano setup_caddy_php.sh # 粘贴脚本内容 chmod +x setup_caddy_php.sh ./setup_caddy_php.sh或一键命令bash <(curl -fsSL https://raw.githubusercontent.com/wszx123/gongjuxiang/refs/heads/main/setup_caddy_php.sh)或bash <(wget -qO- https://raw.githubusercontent.com/wszx123/gongjuxiang/refs/heads/main/setup_caddy_php.sh)三、使用域名 + 自动 HTTPS(可选)如果你有域名(如 example.com)指向 VPS:编辑 /etc/caddy/Caddyfile 为:example.com { root * /var/www/html/mysite php_fastcgi unix//run/php/php-fpm.sock file_server }重启caddysudo systemctl reload caddyCaddy 会自动申请并配置 Let's Encrypt 证书(HTTPS)。四、总结组件说明Caddy替代 Nginx,自动 HTTPS,配置更简单PHP-FPM处理 PHP 动态请求运行目录/var/www/html/mysite/配置文件/etc/caddy/Caddyfile
2025年06月05日
15 阅读
1 评论
0 点赞
2025-06-02
cf 部署 订阅转换工具
部署 订阅 转换工具 github项目 一、🚀 快速开始Fork本项目,点击上方Deploy to Cloudflare按钮在导入储存库栏选择你的仓库(你需要绑定Github账户)更改部署命令如下,选择保存并部署即可使用【构建命令,必须】npm run deploy二、功能特点1、支持协议:ShadowSocksVMessVLESSHysteria2TrojanTUIC2、核心功能支持导入 Base64 的 http/https 订阅链接以及多种协议的分享URL纯JavaScript + Cloudflare Worker实现,一键部署,开箱即用支持固定/随机短链接生成(基于 KV)浅色/深色主题切换灵活的 API,支持脚本化操作中文,英语,波斯语三语言支持3、客户端支持Sing-BoxClashXray/V2Ray4、Web 界面特性用户友好的操作界面提供多种预定义规则集可自建关于 geo-site、geo-ip、ip-cidr 和 domain-suffix 的自定义策略组主要端点/singbox - 生成 Sing-Box 配置/clash - 生成 Clash 配置/xray - 生成 Xray 配置/shorten - 生成短链接
2025年06月02日
13 阅读
1 评论
0 点赞
2025-05-28
Sub-Hub 基于 Cloudflare Worker+D1数据库的简洁订阅管理器,摒弃第三方订阅转换!
Sub-Hub 基于 Cloudflare Worker+D1数据库的简洁订阅管理器,摒弃第三方订阅转换! github项目地址 Sub-Hub 是一个基于 Cloudflare Workers 的代理节点订阅管理系统。它提供了一个直观的 Web 界面,让您可以轻松管理多个订阅和节点基于Cloudflare Worker 搭建,不需要借助VPS进行部署支持原始格式,BASE64格式,Surge格式、Clash格式(内置模板,需要使用自己的规则可以按需修改)支持SS、VMess、VLESS(除Surge)、Trojan、SOCKS5、Snell(仅Surge)、Hysteria2、Tuic 格式节点的托管本项目不使用任何第三方订阅转换,所以可能有部分协议转换不完整,目前支持的协议经过测试没发现太大问题基于Cursor纯AI代码 越来越屎山了,有问题可以提,但不一定能解决功能特点 🚀 支持多种代理协议SS(Shadowsocks)SS2022(Shadowsocks 2022)VMessTrojanVLESS(除 Surge 外)SOCKS5Snell(仅 Surge)Hysteria2Tuic订阅管理 创建多个独立订阅自定义订阅路径支持批量导入节点节点拖拽排序多种订阅格式原始格式(适用于大多数客户端)Base64 编码格式(/v2ray 路径)Surge 配置格式(/surge 路径)Clash 配置格式(/clash 路径)(内置Clash模板)安全特性管理面板登录认证会话管理安全的 Cookie 设置🎨 现代化界面响应式设计直观的操作界面支持移动设备{dotted startColor="#ff6c6c" endColor="#1989fa"/}创建项目创建名为“sub-hub”新的 Workers 项目创建名为“sub-hub” 的D1 数据库将D1数据库与Cloudflare Workers绑定变量名称 = "DB" 数据库名称 = "sub-hub"初始化数据库,在名为“sub-hub” 的D1 数据库“控制台中执行如下代码”-- 创建订阅表CREATE TABLE IF NOT EXISTS subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL UNIQUE );-- 创建节点表CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, subscription_id INTEGER NOT NULL, name TEXT NOT NULL, original_link TEXT NOT NULL, node_order INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE );-- 创建会话表CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at INTEGER NOT NULL );-- 创建索引CREATE INDEX IF NOT EXISTS idx_subscriptions_path ON subscriptions(path); CREATE INDEX IF NOT EXISTS idx_nodes_subscription_order ON nodes(subscription_id, node_order); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);配置环境变量在 Cloudflare Dashboard 中设置以下环境变量:ADMIN_PATH: 管理面板路径(默认:admin)ADMIN_USERNAME: 管理员用户名(默认:admin)ADMIN_PASSWORD: 管理员密码(默认:password)部署代码将 “worker.js” 文件内容复制到Cloudflare Workers保存【20250529更新】// 通用的路径验证和节点名称提取函数 function validateSubscriptionPath(path) { return /^[a-z0-9-]{5,50}$/.test(path); } // 节点类型常量定义 const NODE_TYPES = { SS: 'ss://', VMESS: 'vmess://', TROJAN: 'trojan://', VLESS: 'vless://', SOCKS: 'socks://', HYSTERIA2: 'hysteria2://', TUIC: 'tuic://', SNELL: 'snell,' }; function extractNodeName(nodeLink) { if (!nodeLink) return '未命名节点'; // 处理snell节点 if(nodeLink.includes(NODE_TYPES.SNELL)) { const name = nodeLink.split('=')[0].trim(); return name || '未命名节点'; } // 处理 VMess 链接 if (nodeLink.toLowerCase().startsWith(NODE_TYPES.VMESS)) { try { const config = JSON.parse(safeBase64Decode(nodeLink.substring(8))); if (config.ps) { return safeUtf8Decode(config.ps); } } catch {} return '未命名节点'; } // 处理其他使用哈希标记名称的链接类型(SS、TROJAN、VLESS、SOCKS、Hysteria2、TUIC等) const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } export default { async fetch(request, env) { // 解析请求路径和参数 const url = new URL(request.url); const pathname = url.pathname; const method = request.method; // 检查是否有查询参数 if (url.search && !pathname.startsWith('/admin')) { return new Response('Not Found', { status: 404 }); } // 从环境变量获取配置路径,如果未设置则使用默认值 const adminPath = env.ADMIN_PATH || 'admin'; // 从环境变量获取登录凭据 const adminUsername = env.ADMIN_USERNAME || 'admin'; const adminPassword = env.ADMIN_PASSWORD || 'password'; // 处理登录页面请求 if (pathname === `/${adminPath}/login`) { if (method === "GET") { return serveLoginPage(adminPath); } else if (method === "POST") { return handleLogin(request, env, adminUsername, adminPassword, adminPath); } } // 处理登出请求 if (pathname === `/${adminPath}/logout`) { return handleLogout(request, env, adminPath); } // 处理管理面板请求 if (pathname === `/${adminPath}`) { const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return Response.redirect(`${url.origin}/${adminPath}/login`, 302); } return serveAdminPanel(env, adminPath); } // 处理API请求 if (pathname.startsWith(`/${adminPath}/api/`)) { // 验证会话 const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return new Response(JSON.stringify({ success: false, message: '未授权访问' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // 处理节点管理API请求 const nodeApiMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)/nodes(?:/([^/]+|reorder))?$`)); if (nodeApiMatch) { const subscriptionPath = nodeApiMatch[1]; const nodeId = nodeApiMatch[2]; try { // 更新节点顺序 if (nodeId === 'reorder' && method === 'POST') { const { orders } = await request.json(); if (!Array.isArray(orders) || orders.length === 0) { return new Response(JSON.stringify({ success: false, message: '无效的排序数据' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return new Response(JSON.stringify({ success: false, message: '订阅不存在' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } const subscriptionId = subResults[0].id; // 使用事务来确保数据一致性 const statements = []; // 准备更新语句 for (const { id, order } of orders) { statements.push(env.DB.prepare( "UPDATE nodes SET node_order = ? WHERE id = ? AND subscription_id = ?" ).bind(order, id, subscriptionId)); } // 执行批量更新 const result = await env.DB.batch(statements); return new Response(JSON.stringify({ success: true, message: '节点顺序已更新' }), { headers: { 'Content-Type': 'application/json' } }); } // 获取节点列表 if (!nodeId && method === 'GET') { return handleGetNodes(env, subscriptionPath); } // 创建新节点 if (!nodeId && method === 'POST') { return handleCreateNode(request, env, subscriptionPath); } // 更新节点 if (nodeId && nodeId !== 'reorder' && method === 'PUT') { return handleUpdateNode(request, env, subscriptionPath, nodeId); } // 删除节点 if (nodeId && nodeId !== 'reorder' && method === 'DELETE') { return handleDeleteNode(env, subscriptionPath, nodeId); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('API请求处理失败:', error); return new Response(JSON.stringify({ success: false, message: error.message || '服务器内部错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 处理订阅管理API请求 if (pathname.startsWith(`/${adminPath}/api/subscriptions`)) { // 获取单个订阅内容 const getOneMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (getOneMatch && method === 'GET') { return handleGetSubscription(env, getOneMatch[1]); } // 获取订阅列表 if (pathname === `/${adminPath}/api/subscriptions` && method === 'GET') { return handleGetSubscriptions(env); } // 创建新订阅 if (pathname === `/${adminPath}/api/subscriptions` && method === 'POST') { try { const { name, path } = await request.json(); if (!name || !validateSubscriptionPath(path)) { return createErrorResponse('无效的参数', 400); } // 检查路径是否已存在 const { results } = await env.DB.prepare( "SELECT COUNT(*) as count FROM subscriptions WHERE path = ?" ).bind(path).all(); if (results[0].count > 0) { return createErrorResponse('该路径已被使用', 400); } // 创建订阅 const result = await env.DB.prepare( "INSERT INTO subscriptions (name, path) VALUES (?, ?)" ).bind(name, path).run(); if (!result.success) { throw new Error('创建订阅失败'); } return createSuccessResponse(null, '订阅创建成功'); } catch (error) { console.error('创建订阅失败:', error); return createErrorResponse('创建订阅失败: ' + error.message); } } // 更新订阅信息 const updateMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (updateMatch && method === 'PUT') { const data = await request.json(); return handleUpdateSubscriptionInfo(env, updateMatch[1], data); } // 删除订阅 const deleteMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (deleteMatch && method === 'DELETE') { return handleDeleteSubscription(env, deleteMatch[1]); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ success: false, message: 'Not Found' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } // 处理订阅请求 if (pathname.startsWith('/')) { // 检查路径格式是否合法(只允许一级或两级路径,如 /path 或 /path/surge 或 /path/v2ray 或 /path/clash) const pathParts = pathname.split('/').filter(Boolean); if (pathParts.length > 2) { return new Response('Not Found', { status: 404 }); } if (pathParts.length === 2 && !['surge', 'v2ray', 'clash'].includes(pathParts[1])) { return new Response('Not Found', { status: 404 }); } try { // 获取基本路径 let basePath = pathname; if (pathname.endsWith('/surge')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/surge } else if (pathname.endsWith('/v2ray')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/v2ray } else if (pathname.endsWith('/clash')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/clash } else { basePath = pathname.slice(1); // 只移除开头的/ } // 获取订阅信息 const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(basePath).all(); const subscription = results[0]; if (subscription) { // 生成订阅内容 const content = await generateSubscriptionContent(env, basePath); // 根据请求路径返回不同格式的内容 if (pathname.endsWith('/surge')) { // 返回 Surge 格式 const surgeContent = convertToSurge(content); return new Response(surgeContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/v2ray')) { // 返回 Base64 编码格式,排除 snell 节点,包括 VLESS 节点 const filteredContent = filterSnellNodes(content); const base64Content = safeBase64Encode(filteredContent); return new Response(base64Content, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/clash')) { // 返回 Clash 格式 const clashContent = convertToClash(content); return new Response(clashContent, { headers: { 'Content-Type': 'text/yaml; charset=utf-8' }, }); } // 返回普通订阅内容,排除 snell 节点 const filteredContent = filterSnellNodes(content); return new Response(filteredContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } } catch (error) { console.error('处理订阅请求失败:', error); return new Response('Internal Server Error', { status: 500 }); } // 如果没有找到匹配的订阅,返回404 return new Response('Not Found', { status: 404 }); } // 其他所有路径返回 404 return new Response('Not Found', { status: 404 }); }, }; // 添加获取单个订阅的处理函数 async function handleGetSubscription(env, path) { try { const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(path).all(); if (!results || results.length === 0) { return createErrorResponse('订阅不存在', 404); } return createSuccessResponse(results[0]); } catch (error) { console.error('获取订阅内容失败:', error); return createErrorResponse('获取订阅内容失败: ' + error.message); } } // 提供登录页面HTML function serveLoginPage(adminPath) { const html = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sub-Hub - 登录</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css"> <style> :root { --primary-color: #4e73df; --success-color: #1cc88a; --danger-color: #e74a3b; --transition-timing: 0.25s cubic-bezier(0.4, 0, 0.2, 1); --box-shadow-light: 0 2px 10px rgba(0,0,0,0.05); --box-shadow-medium: 0 4px 15px rgba(0,0,0,0.08); --font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; --text-color: #2d3748; --text-color-secondary: #444; --border-radius-sm: 8px; --border-radius-md: 12px; --border-radius-lg: 16px; } * { transition: all var(--transition-timing); font-family: var(--font-family); } html { scrollbar-gutter: stable; } /* 防止模态框打开时页面偏移 */ .modal-open { padding-right: 0 !important; } /* 修复模态框背景遮罩的宽度 */ .modal-backdrop { width: 100vw !important; } /* 优化模态框布局 */ .modal-dialog { margin-right: auto !important; margin-left: auto !important; padding-right: 0 !important; } /* 标题和重要文字样式 */ .navbar-brand, .subscription-name, .modal-title, .form-label { font-weight: 600; color: var(--text-color); } /* 按钮统一样式 */ .btn, .logout-btn { font-weight: 500; } /* 次要文字样式 */ .link-label, .form-text { color: var(--text-color-secondary); } /* 链接标签文字加粗 */ .link-label small > span > span { font-weight: 600; } body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; } .login-container { background-color: #fff; border-radius: 16px; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); overflow: hidden; width: 360px; max-width: 90%; } .login-header { background: linear-gradient(120deg, var(--primary-color), #224abe); padding: 2rem 1.5rem; text-align: center; color: white; } .login-icon { background-color: white; color: var(--primary-color); width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); } .login-icon i { font-size: 2.5rem; } .login-title { font-weight: 600; margin-bottom: 0.5rem; } .login-subtitle { opacity: 0.8; font-size: 0.9rem; } .login-form { padding: 2rem; } .form-floating { margin-bottom: 1.5rem; } .form-floating input { border-radius: 8px; height: 56px; border: 2px solid #e7eaf0; box-shadow: none; } .form-floating input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 0.25rem rgba(78, 115, 223, 0.15); } .form-floating label { color: #7e7e7e; padding-left: 1rem; } .btn-login { background: linear-gradient(120deg, var(--primary-color), #224abe); border: none; border-radius: 8px; padding: 0.75rem 1.5rem; font-weight: 600; width: 100%; margin-top: 1rem; box-shadow: 0 4px 10px rgba(78, 115, 223, 0.35); transition: all 0.2s ease; } .btn-login:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(78, 115, 223, 0.4); } .alert { border-radius: 8px; font-size: 0.9rem; margin-bottom: 1.5rem; display: none; } </style> </head> <body> <div class="login-container"> <div class="login-header"> <div class="login-icon"> <i class="fas fa-cube"></i> </div> <h3 class="login-title">Sub-Hub</h3> <p class="login-subtitle">请登录以继续使用</p> </div> <div class="login-form"> <div class="alert alert-danger" id="loginAlert" role="alert"> <i class="fas fa-exclamation-triangle me-2"></i> <span id="alertMessage">用户名或密码错误</span> </div> <form id="loginForm"> <div class="form-floating"> <input type="text" class="form-control" id="username" name="username" placeholder="用户名" required> <label for="username"><i class="fas fa-user me-2"></i>用户名</label> </div> <div class="form-floating"> <input type="password" class="form-control" id="password" name="password" placeholder="密码" required> <label for="password"><i class="fas fa-lock me-2"></i>密码</label> </div> <button type="submit" class="btn btn-primary btn-login"> <i class="fas fa-sign-in-alt me-2"></i>登录 </button> </form> </div> </div> <script> document.getElementById('loginForm').addEventListener('submit', async function(e) { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const response = await fetch('/${adminPath}/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }), }); const data = await response.json(); if (data.success) { // 登录成功,重定向到管理面板 window.location.href = data.redirect; } else { // 显示错误消息 document.getElementById('alertMessage').textContent = data.message; document.getElementById('loginAlert').style.display = 'block'; } } catch (error) { // 显示错误消息 document.getElementById('alertMessage').textContent = '登录请求失败,请重试'; document.getElementById('loginAlert').style.display = 'block'; } }); </script> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 验证会话 async function verifySession(request, env) { const sessionId = getSessionFromCookie(request); if (!sessionId) return false; const now = Date.now(); const { results } = await env.DB.prepare(` UPDATE sessions SET expires_at = ? WHERE session_id = ? AND expires_at > ? RETURNING * `).bind(now + 24 * 60 * 60 * 1000, sessionId, now).all(); return results.length > 0; } // 从Cookie中获取会话ID function getSessionFromCookie(request) { const cookieHeader = request.headers.get('Cookie') || ''; const sessionCookie = cookieHeader.split(';') .find(cookie => cookie.trim().startsWith('session=')); return sessionCookie ? sessionCookie.trim().substring(8) : null; } // 生成安全的会话令牌 async function generateSecureSessionToken(username, env) { // 清理过期会话和用户旧会话 const now = Date.now(); await env.DB.batch([ env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(now), env.DB.prepare("DELETE FROM sessions WHERE username = ?").bind(username) ]); const sessionId = crypto.randomUUID(); const expiresAt = now + 24 * 60 * 60 * 1000; // 24小时后过期 await env.DB.prepare(` INSERT INTO sessions (session_id, username, expires_at) VALUES (?, ?, ?) `).bind(sessionId, username, expiresAt).run(); return sessionId; } // 处理登录请求 async function handleLogin(request, env, adminUsername, adminPassword, adminPath) { const { username, password } = await request.json(); if (!username || !password || username !== adminUsername || password !== adminPassword) { return new Response(JSON.stringify({ success: false, message: '用户名或密码错误' }), { headers: { 'Content-Type': 'application/json' }, status: 401 }); } const sessionId = await generateSecureSessionToken(username, env); const headers = new Headers({ 'Content-Type': 'application/json', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400; Secure` }); return new Response(JSON.stringify({ success: true, message: '登录成功', redirect: `/${adminPath}` }), { headers }); } // 处理登出请求 async function handleLogout(request, env, adminPath) { const sessionId = getSessionFromCookie(request); if (sessionId) { await env.DB.prepare("DELETE FROM sessions WHERE session_id = ?").bind(sessionId).run(); } const headers = new Headers({ 'Set-Cookie': `session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0; Secure` }); return Response.redirect(`${new URL(request.url).origin}/${adminPath}/login`, 302); } // 修改管理面板HTML生成函数 function serveAdminPanel(env, adminPath) { const html = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sub-Hub</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css"> <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script> <style> :root { --primary-color: #4e73df; --success-color: #1cc88a; --danger-color: #e74a3b; --transition-timing: 0.25s cubic-bezier(0.4, 0, 0.2, 1); --box-shadow-light: 0 2px 10px rgba(0,0,0,0.05); --box-shadow-medium: 0 4px 15px rgba(0,0,0,0.08); --font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; --text-color: #2d3748; --text-color-secondary: #444; --border-radius-sm: 8px; --border-radius-md: 12px; --border-radius-lg: 16px; } * { transition: all var(--transition-timing); font-family: var(--font-family); } html { scrollbar-gutter: stable; } body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; background-color: #f8f9fc; padding: 20px 0; overflow-y: scroll; } /* 防止模态框打开时页面偏移 */ .modal-open { padding-right: 0 !important; } /* 修复模态框背景遮罩的宽度 */ .modal-backdrop { width: 100vw !important; } /* 优化模态框布局 */ .modal-dialog { margin-right: auto !important; margin-left: auto !important; padding-right: 0 !important; } /* 标题和重要文字样式 */ .navbar-brand, .subscription-name, .modal-title, .form-label { font-weight: 600; color: var(--text-color); } /* 按钮统一样式 */ .btn, .logout-btn { font-weight: 500; } /* 次要文字样式 */ .subscription-meta, .link-label, .form-text { color: var(--text-color-secondary); } /* 链接标签文字加粗 */ .link-label small > span > span { font-weight: 600; } .navbar { background-color: white; box-shadow: var(--box-shadow-light); padding: 1rem 1.5rem; margin-bottom: 0.5rem; height: 80px; display: flex; align-items: center; transform: translateY(0); } .navbar:hover { box-shadow: var(--box-shadow-medium); } .navbar-brand { font-weight: bold; color: #000; display: flex; align-items: center; font-size: 1.8rem; } .navbar-brand i { font-size: 2.2rem; margin-right: 0.8rem; color: #000; } .navbar-user { display: flex; align-items: center; } .navbar-user .user-name { font-weight: 500; } .logout-btn { color: #4a4a4a; background: none; border: none; margin-left: 1rem; padding: 0.375rem 0.75rem; border-radius: 0.25rem; text-decoration: none; font-weight: 500; } .logout-btn:hover { background-color: #f8f9fa; color: var(--danger-color); text-decoration: none; } .logout-btn:focus { text-decoration: none; outline: none; } .container { max-width: 1100px; } textarea { border: 1px solid #e5e7eb; border-radius: 8px; resize: vertical; min-height: 300px; max-height: 800px; height: 300px; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.9rem; line-height: 1.5; padding: 12px; } .link-label { display: flex; flex-direction: column; margin-top: 1rem; color: #444; font-size: 0.9rem; } .link-label small { display: flex; flex-direction: column; gap: 0.75rem; } .link-label small > span { display: flex; align-items: center; gap: 0.25rem; font-size: 0.9rem; } .link-label small > span > span { display: inline-block; width: 90px; color: var(--text-color); font-weight: 600; text-align: justify; text-align-last: justify; font-size: 0.9rem; } .link-label a { margin-left: 0; color: var(--primary-color); text-decoration: none; word-break: break-all; font-weight: 700; font-size: 0.9rem; } .link-label .link:hover { text-decoration: underline; } .btn-group { display: flex; gap: 0.5rem; margin-top: 1rem; } .toast-container { position: fixed; top: 1rem; right: 1rem; z-index: 1050; } @media (max-width: 767px) { .btn-group { flex-direction: column; } } .subscription-list { margin-bottom: 2rem; } .subscription-item { background: #fff; border-radius: var(--border-radius-md); padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: var(--box-shadow-light); border: 1px solid #eaeaea; overflow: hidden; transition: margin-bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } .edit-button { position: absolute; top: 1rem; right: 1rem; z-index: 1; } .edit-button .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; } .subscription-item:hover { box-shadow: var(--box-shadow-medium); } .subscription-edit-area { max-height: 0; overflow: hidden; opacity: 0; transform: translateY(-10px); padding: 0; margin: 0; will-change: max-height, opacity, transform, padding, margin; } .subscription-edit-area.active { max-height: 300px; opacity: 1; transform: translateY(0); padding: 1rem; margin-top: 0.5rem; } .subscription-edit-area textarea { width: 100%; height: 200px; min-height: 200px; max-height: 200px; margin-bottom: 1rem; padding: 1rem; border: 1px solid #e5e7eb; border-radius: var(--border-radius-sm); font-size: 0.9rem; line-height: 1.5; resize: none; box-shadow: var(--box-shadow-light); opacity: 0; transform: translateY(5px); } .subscription-edit-area.active textarea { opacity: 1; transform: translateY(0); } .subscription-edit-actions { display: flex; gap: 0.5rem; justify-content: flex-end; opacity: 0; transform: translateY(5px); } .subscription-edit-area.active .subscription-edit-actions { opacity: 1; transform: translateY(0); transition-delay: 0.05s; } .toast { transform: translateY(0); } .toast.hide { transform: translateY(-100%); } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .subscription-item { animation: fadeIn 0.5s ease-out; } .path-error { color: #dc3545; font-size: 0.875em; margin-top: 0.25rem; display: none; } .form-control.is-invalid { border-color: #dc3545; padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .form-control.is-valid { border-color: #198754; padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .subscription-name { font-size: 1.5rem; font-weight: 600; color: #2d3748; margin-bottom: 0.75rem; line-height: 1.2; } .node-count { display: inline-flex; align-items: center; color: var(--text-color-secondary); font-size: 1rem; margin-left: 0.75rem; } .node-count i { margin-right: 0.35rem; font-size: 1rem; } .subscription-actions { margin-top: 1.25rem; padding-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; } .subscription-actions .btn { width: 100%; padding: 0.5rem 1.25rem; font-weight: 500; border-radius: 8px; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; display: flex; align-items: center; justify-content: center; } .subscription-actions .btn i { margin-right: 0.5rem; } /* 操作按钮样式 */ .node-actions { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; gap: 4px; flex-shrink: 0; } .node-actions .btn { padding: 4px 8px !important; min-width: 32px !important; height: 28px !important; display: inline-flex !important; align-items: center; justify-content: center; margin: 0 !important; } .node-actions .btn i { font-size: 14px; line-height: 1; margin: 0 !important; } /* 移动端适配样式 */ @media (max-width: 767px) { /* 基础布局调整 */ .container { padding: 0 15px; } .navbar { margin-bottom: 2rem; padding: 0.8rem 1rem; } .btn-group { flex-direction: column; } /* 订阅项样式调整 */ .subscription-item { padding: 1rem; } .subscription-item > div { flex-direction: column; align-items: flex-start !important; } .subscription-item .link-label { margin: 1rem 0 0; width: 100%; } .subscription-actions { margin-top: 1rem; width: 100%; flex-direction: row !important; gap: 0.5rem; } .subscription-actions .btn { flex: 1; font-size: 0.875rem; padding: 0.5rem; } .subscription-actions .btn i { margin-right: 0.25rem; font-size: 0.875rem; } /* 模态框调整 */ .modal-dialog { margin: 0.5rem; } .modal-content { border-radius: 1rem; } /* Toast提示调整 */ .toast-container { right: 0; left: 0; bottom: 1rem; top: auto; margin: 0 1rem; } .toast { width: 100%; } /* 节点列表移动端优化 */ .table { table-layout: fixed; width: 100%; margin-bottom: 0 !important; } .table thead th:first-child { padding-left: 1rem !important; width: 100% !important; } .table thead th:last-child { display: none !important; } .node-row td { padding: 0.5rem !important; } .node-row td:first-child { font-size: 0.85rem; padding-right: 0 !important; width: calc(100% - 90px) !important; } .node-row td:first-child .text-truncate { max-width: 100% !important; width: 100% !important; padding-right: 8px !important; } .node-row td:last-child { padding-left: 0 !important; width: 90px !important; position: relative !important; } .node-row td:last-child .text-truncate { display: none !important; } /* 节点操作按钮移动端优化 */ .node-actions { margin: 0 !important; gap: 2px !important; justify-content: flex-end !important; width: 90px !important; position: absolute !important; right: 4px !important; flex-wrap: nowrap !important; align-items: center !important; } .node-actions .btn { padding: 2px 4px !important; min-width: 28px !important; height: 28px !important; margin: 0 !important; } .node-actions .btn i { font-size: 12px; line-height: 1; display: flex !important; align-items: center !important; justify-content: center !important; } } /* 模态框按钮样式 */ .modal .btn-secondary { background-color: #e2e8f0; border-color: #e2e8f0; color: var(--text-color); } .modal .btn-secondary:hover { background-color: #cbd5e0; border-color: #cbd5e0; } /* 添加通用按钮样式 */ /* 按钮基础样式 */ .action-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.625rem 1.25rem; font-size: 1rem; font-weight: 500; border-radius: 8px; transition: all 0.2s ease; min-width: 140px; height: 42px; gap: 0.5rem; line-height: 1; } .action-btn i { font-size: 1rem; display: inline-flex; align-items: center; justify-content: center; width: 1rem; height: 1rem; } /* 按钮颜色变体 */ .action-btn.btn-primary, .btn-primary { background-color: var(--primary-color); border-color: var(--primary-color); color: white; } .action-btn.btn-success, .btn-success { background-color: var(--success-color); border-color: var(--success-color); color: white; } .btn-edit { background-color: #1cc88a; border-color: #1cc88a; color: white; } /* 按钮悬停效果 */ .action-btn:hover, .btn-edit:hover { transform: translateY(-1px); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); } .btn-edit:hover { background-color: #19b57c; border-color: #19b57c; } .btn-edit:focus { background-color: #19b57c; border-color: #19b57c; box-shadow: 0 0 0 0.25rem rgba(28, 200, 138, 0.25); } .action-btn:active { transform: translateY(0); } /* 调整容器样式 */ .container { max-width: 1100px; padding: 2rem 1rem; } .d-flex.justify-content-between.align-items-center.mb-4 { margin-bottom: 2rem !important; } /* 节点列表区域样式 */ .node-list-area { max-height: 0; overflow: hidden; transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1); background: #fff; border-radius: 0 0 var(--border-radius-md) var(--border-radius-md); will-change: max-height; font-size: 0.875rem; color: var(--text-color); } .node-list-area.expanded { max-height: 2000px; } .node-list-area .table-responsive { transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); transform-origin: top; } .node-list-area .link { font-size: 0.875rem; color: var(--text-color-secondary); text-decoration: none; } .node-list-area .link:hover { text-decoration: underline; } /* 订阅项样式 */ .subscription-item { background: #fff; border-radius: var(--border-radius-md); padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: var(--box-shadow-light); border: 1px solid #eaeaea; overflow: hidden; transition: margin-bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } .subscription-item:has(.node-list-area.expanded) { margin-bottom: 2rem; } .subscription-content { margin-left: 1rem; } .subscription-content .d-flex.align-items-center { margin-left: -0.1rem; } /* 编辑按钮样式 */ .edit-button { position: absolute; top: 1rem; right: 1rem; z-index: 1; } .edit-button .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; } /* 节点行样式 */ .node-row { cursor: move; } .node-row td { vertical-align: middle; font-family: var(--font-family); } .node-row .text-truncate, .node-row .text-nowrap { font-family: var(--font-family); font-weight: 500; } .node-row .form-check { display: flex; align-items: center; margin: 0; padding: 0; min-height: auto; } .node-row .form-check-input { margin: 0; position: static; } .node-row.sortable-ghost { opacity: 0.5; background-color: #f8f9fa; } .node-row.sortable-drag { background-color: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } </style> </head> <body> <nav class="navbar navbar-expand navbar-light bg-white"> <div class="container"> <a class="navbar-brand" href="#"> <i class="fas fa-cube"></i> Sub-Hub </a> <div class="navbar-user ms-auto"> <a href="/${adminPath}/logout" class="logout-btn"> <i class="fas fa-sign-out-alt me-1"></i> 退出 </a> </div> </div> </nav> <div class="container"> <div class="subscription-item" style="padding-bottom: 0; border: none; box-shadow: none; margin-bottom: 1rem; background: transparent;"> <div class="d-flex justify-content-end"> <button class="btn btn-primary action-btn" data-bs-toggle="modal" data-bs-target="#addSubscriptionModal"> <i class="fas fa-plus"></i> <span>添加订阅</span> </button> </div> </div> <!-- 订阅列表 --> <div class="subscription-list" id="subscriptionList"> <!-- 订阅项将通过JavaScript动态添加 --> </div> </div> <!-- 添加订阅模态框 --> <div class="modal fade" id="addSubscriptionModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">添加新订阅</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="addSubscriptionForm" onsubmit="return false;"> <div class="mb-3"> <label class="form-label">订阅名称 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="name" required> <div class="invalid-feedback">请输入订阅名称</div> </div> <div class="mb-3"> <label class="form-label">订阅路径 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="path" required pattern="^[a-z0-9-]+$" minlength="5" maxlength="50"> <div class="form-text">路径只能包含小写字母、数字和连字符,长度在5-50个字符之间</div> <div class="path-error text-danger"></div> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="createSubscription()">创建</button> </div> </div> </div> </div> <!-- 修改添加节点模态框 --> <div class="modal fade" id="addNodeModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">添加节点</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="addNodeForm" onsubmit="return false;"> <input type="hidden" name="subscriptionPath" value=""> <div class="mb-3"> <label class="form-label">节点内容 <span class="text-danger">*</span></label> <textarea class="form-control" name="content" rows="6" required placeholder="请输入节点内容,支持以下格式: 1. ss://... 2. vless://...(除Surge) 3. vmess://... 4. trojan://... 5. socks://... 6. hysteria2://... 7. tuic://... 8. snell格式(仅Surge) 9. Base64编码格式"></textarea> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="createNode()"> <i class="fas fa-plus me-1"></i>添加 </button> </div> </div> </div> </div> <!-- 编辑名称和路径模态框 --> <div class="modal fade" id="editNameModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">编辑订阅信息</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="editNameForm" onsubmit="return false;"> <input type="hidden" name="originalPath"> <div class="mb-3"> <label class="form-label">订阅名称 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="name" required> <div class="invalid-feedback">请输入订阅名称</div> </div> <div class="mb-3"> <label class="form-label">订阅路径 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="path" required> <div class="form-text">路径只能包含小写字母、数字和连字符,长度在5-50个字符之间</div> <div class="path-error text-danger"></div> </div> </form> </div> <div class="modal-footer justify-content-between"> <button type="button" class="btn btn-danger" onclick="confirmDelete()"> <i class="fas fa-trash"></i> 删除订阅 </button> <div> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="updateSubscriptionInfo()">保存</button> </div> </div> </div> </div> </div> <!-- 编辑节点模态框 --> <div class="modal fade" id="editNodeModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">编辑节点</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="editNodeForm" onsubmit="return false;"> <input type="hidden" name="subscriptionPath" value=""> <input type="hidden" name="nodeId" value=""> <div class="mb-3"> <label class="form-label">节点内容 <span class="text-danger">*</span></label> <textarea class="form-control" name="content" rows="6" required placeholder="请输入节点内容,支持以下格式: 1. ss://... 2. vless://...(除Surge) 3. vmess://... 4. trojan://... 5. socks://... 6. hysteria2://... 7. tuic://... 8. snell格式(仅Surge) 9. Base64编码格式"></textarea> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="updateNode()">保存</button> </div> </div> </div> </div> <!-- Toast提示 --> <div class="toast-container"> <div id="toast" class="toast align-items-center text-white bg-success" role="alert" aria-live="assertive" aria-atomic="true"> <div class="d-flex"> <div class="toast-body" id="toastMessage"> 操作成功! </div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script> // 定义 adminPath 变量 const adminPath = '${adminPath}'; // 优化的loadSubscriptions函数 async function loadSubscriptions() { try { const response = await fetch('/' + adminPath + '/api/subscriptions'); if (!response.ok) throw new Error('加载失败'); const result = await response.json(); if (!result.success) { throw new Error(result.message || '加载失败'); } const subscriptions = result.data || []; const listElement = document.getElementById('subscriptionList'); listElement.innerHTML = ''; const fragment = document.createDocumentFragment(); for (const sub of subscriptions) { const item = document.createElement('div'); item.className = 'subscription-item'; item.innerHTML = \` <div class="d-flex justify-content-between align-items-start"> <button class="btn btn-sm btn-link text-primary edit-button" onclick="showEditNameModal('\${sub.path}', '\${sub.name}')"> <i class="fas fa-edit"></i> </button> <div class="subscription-content"> <div class="d-flex align-items-center"> <h5 class="subscription-name mb-1">\${sub.name}</h5> <span class="node-count ms-2"><i class="fas fa-server"></i>\${sub.nodeCount}</span> </div> <div class="link-label"> <small> <span> <span class="address-label">订阅地址1:</span> <a href="/\${sub.path}" target="_blank">/\${sub.path}</a> </span> <span> <span class="address-label">订阅地址2:</span> <a href="/\${sub.path}/v2ray" target="_blank">/\${sub.path}/v2ray</a> </span> <span> <span class="address-label">订阅地址3:</span> <a href="/\${sub.path}/surge" target="_blank">/\${sub.path}/surge</a> </span> <span> <span class="address-label">订阅地址4:</span> <a href="/\${sub.path}/clash" target="_blank">/\${sub.path}/clash</a> </span> </small> </div> </div> <div class="subscription-actions"> <button class="btn btn-success action-btn" onclick="showAddNodeModal('\${sub.path}')"> <i class="fas fa-plus"></i> <span>添加节点</span> </button> <button class="btn btn-primary action-btn" onclick="showNodeList('\${sub.path}')"> <i class="fas fa-list"></i> <span>节点列表</span> </button> </div> </div> <div class="node-list-area" id="node-list-\${sub.path}"> <div class="table-responsive mt-3"> <table class="table"> <thead> <tr> <th style="min-width: 250px; width: 30%; padding-left: 1rem;">名称</th> <th style="padding-left: 4.5rem;">节点链接</th> </tr> </thead> <tbody></tbody> </table> <div class="d-flex justify-content-between align-items-center mt-3 px-2 pb-2"> <button class="btn btn-danger btn-sm rounded-3" onclick="toggleBatchDelete('\${sub.path}')" id="batch-delete-btn-\${sub.path}" style="padding: 0.5rem 1rem;"> <i class="fas fa-trash-alt me-1"></i>批量删除 </button> <div class="batch-delete-actions" id="batch-delete-actions-\${sub.path}" style="display: none;"> <button class="btn btn-danger btn-sm me-2 rounded-3" onclick="executeBatchDelete('\${sub.path}')" style="padding: 0.5rem 1rem;"> <i class="fas fa-check me-1"></i>确认删除 </button> <button class="btn btn-secondary btn-sm rounded-3" onclick="cancelBatchDelete('\${sub.path}')" style="padding: 0.5rem 1rem;"> <i class="fas fa-times me-1"></i>取消 </button> </div> </div> </div> </div> \`; fragment.appendChild(item); } listElement.appendChild(fragment); } catch (error) { showToast('加载订阅列表失败: ' + error.message, 'danger'); } } // 页面加载完成后,加载订阅列表 window.addEventListener('load', loadSubscriptions); // 切换批量删除模式 function toggleBatchDelete(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkboxes = nodeListArea.querySelectorAll('.form-check'); const deleteBtn = document.getElementById('batch-delete-btn-' + subscriptionPath); const deleteActions = document.getElementById('batch-delete-actions-' + subscriptionPath); const isEnteringBatchMode = checkboxes[0].style.display === 'none'; // 切换勾选框显示 checkboxes.forEach(checkbox => { checkbox.style.display = isEnteringBatchMode ? 'block' : 'none'; }); // 切换按钮和操作区域 deleteBtn.style.display = isEnteringBatchMode ? 'none' : 'block'; deleteActions.style.display = isEnteringBatchMode ? 'flex' : 'none'; nodeListArea.classList.toggle('batch-delete-mode', isEnteringBatchMode); // 重置所有勾选框 nodeListArea.querySelectorAll('.node-checkbox').forEach(cb => { cb.checked = false; }); } // 取消批量删除 function cancelBatchDelete(subscriptionPath) { toggleBatchDelete(subscriptionPath); } // 执行批量删除 async function executeBatchDelete(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkedNodes = nodeListArea.querySelectorAll('.node-checkbox:checked'); if (checkedNodes.length === 0) { showToast('请选择要删除的节点', 'warning'); return; } if (!confirm(\`确定要删除选中的 \${checkedNodes.length} 个节点吗?\`)) { return; } try { let successCount = 0; let failCount = 0; // 显示进度提示 showToast('正在删除节点...', 'info'); // 批量删除所选节点 for (const checkbox of checkedNodes) { const nodeId = checkbox.value; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (response.ok) { successCount++; } else { failCount++; } } catch (error) { console.error('删除节点失败:', error); failCount++; } } // 显示删除结果 if (failCount === 0) { showToast(\`成功删除 \${successCount} 个节点\`, 'success'); } else { showToast(\`删除完成:成功 \${successCount} 个,失败 \${failCount} 个\`, 'warning'); } // 退出批量删除模式 toggleBatchDelete(subscriptionPath); // 重新加载节点列表和订阅信息 await loadNodeList(subscriptionPath); await loadSubscriptions(); } catch (error) { console.error('批量删除失败:', error); showToast('批量删除失败: ' + error.message, 'danger'); } }; // 显示添加节点模态框 function showAddNodeModal(subscriptionPath) { const modal = document.getElementById('addNodeModal'); const form = document.getElementById('addNodeForm'); // 重置表单 form.reset(); // 设置订阅路径 const pathInput = form.querySelector('[name="subscriptionPath"]'); if (pathInput) { pathInput.value = subscriptionPath; } // 显示模态框 const modalInstance = new bootstrap.Modal(modal); modalInstance.show(); } // 修改创建节点函数 async function createNode() { try { const form = document.getElementById('addNodeForm'); if (!form) { throw new Error('找不到表单元素'); } const formData = new FormData(form); const subscriptionPath = formData.get('subscriptionPath'); const name = formData.get('name')?.trim(); let content = formData.get('content')?.trim(); if (!subscriptionPath) { throw new Error('缺少订阅路径'); } if (!content) { throw new Error('请填写节点内容'); } // 分割成行 const lines = content.split(/\\r?\\n/); const validNodes = []; // 处理每一行 for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // 检查是否是Base64编码的完整配置 try { const decodedContent = safeBase64DecodeFrontend(trimmedLine); // 如果解码成功,检查是否包含多个节点 const decodedLines = decodedContent.split(/\\r?\\n/); for (const decodedLine of decodedLines) { if (decodedLine.trim() && isValidNodeLink(decodedLine.trim())) { validNodes.push({ name: name || extractNodeNameFrontend(decodedLine.trim()), content: decodedLine.trim() }); } } } catch (e) { // 如果不是Base64编码,直接检查是否是有效的节点链接 if (isValidNodeLink(trimmedLine)) { validNodes.push({ name: name || extractNodeNameFrontend(trimmedLine), content: trimmedLine }); } } } if (validNodes.length === 0) { throw new Error('未找到有效的节点链接'); } // 批量创建节点,按顺序添加 const results = []; const timestamp = Date.now(); // 使用时间戳作为基础序号 for (let i = 0; i < validNodes.length; i++) { const node = validNodes[i]; try { // 创建节点,使用时间戳+索引作为顺序值 const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name: node.name, content: node.content, type: getNodeType(node.content), order: timestamp + i // 使用时间戳确保顺序唯一且递增 }) }); const result = await response.json(); results.push({ success: response.ok, message: result.message, link: node.content, order: i }); } catch (error) { results.push({ success: false, message: error.message, link: node.content, order: i }); } } // 按原始顺序排序结果 results.sort((a, b) => a.order - b.order); // 统计结果 const successful = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; // 显示结果 if (validNodes.length === 1) { showToast(successful > 0 ? '节点添加成功' : '节点添加失败', successful > 0 ? 'success' : 'danger'); } else { showToast(\`添加完成:成功 \${successful} 个,失败 \${failed} 个\`, successful > 0 ? 'success' : 'warning'); } // 关闭模态框 const modal = document.getElementById('addNodeModal'); if (modal) { const modalInstance = bootstrap.Modal.getInstance(modal); if (modalInstance) { modalInstance.hide(); } } // 重置表单 form.reset(); // 刷新订阅列表 await loadSubscriptions(); if (successful > 0) { await loadNodeList(subscriptionPath); } } catch (error) { console.error('添加节点失败:', error); showToast('添加节点失败: ' + error.message, 'danger'); } } // 提取节点名称(前端版本) function extractNodeNameFrontend(nodeLink) { if (!nodeLink) return '未命名节点'; // 处理snell节点 if(nodeLink.includes('snell,')) { const name = nodeLink.split('=')[0].trim(); return name || '未命名节点'; } // 处理 SOCKS 链接 if (nodeLink.toLowerCase().startsWith('socks://')) { const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } // 处理 VLESS 链接 if (nodeLink.toLowerCase().startsWith('vless://')) { const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } // 处理 VMess 链接 if (nodeLink.toLowerCase().startsWith('vmess://')) { try { const config = JSON.parse(safeBase64DecodeFrontend(nodeLink.substring(8))); if (config.ps) { return safeUtf8DecodeFrontend(config.ps); } } catch {} return '未命名节点'; } // 处理其他类型的链接 const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } // 节点类型配置(前端版本) const NODE_TYPES_FRONTEND = { 'ss://': 'ss', 'vmess://': 'vmess', 'trojan://': 'trojan', 'vless://': 'vless', 'socks://': 'socks', 'hysteria2://': 'hysteria2', 'tuic://': 'tuic', 'snell,': 'snell' }; // 检查是否是有效的节点链接 function isValidNodeLink(link) { const lowerLink = link.toLowerCase(); // 检查snell格式 if(lowerLink.includes('=') && lowerLink.includes('snell,')) { const parts = link.split('=')[1]?.trim().split(','); return parts && parts.length >= 4 && parts[0].trim() === 'snell'; } return Object.keys(NODE_TYPES_FRONTEND).some(prefix => lowerLink.startsWith(prefix)); } // 获取节点类型 function getNodeType(link) { const lowerLink = link.toLowerCase(); if(lowerLink.includes('=') && lowerLink.includes('snell,')) { return 'snell'; } return Object.entries(NODE_TYPES_FRONTEND).find(([prefix]) => lowerLink.startsWith(prefix) )?.[1] || ''; } // 安全的UTF-8字符串解码函数(前端版本) function safeUtf8DecodeFrontend(str) { if (!str) return str; try { // 方法1:使用escape + decodeURIComponent return decodeURIComponent(escape(str)); } catch (e1) { try { // 方法2:直接使用decodeURIComponent return decodeURIComponent(str); } catch (e2) { try { // 方法3:使用TextDecoder(如果支持) if (typeof TextDecoder !== 'undefined') { const encoder = new TextEncoder(); const decoder = new TextDecoder('utf-8'); return decoder.decode(encoder.encode(str)); } } catch (e3) { // 如果所有方法都失败,返回原始字符串 return str; } return str; } } } // 显示节点列表 async function showNodeList(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); if (!nodeListArea) { console.error('找不到节点列表区域'); return; } const isHidden = !nodeListArea.classList.contains('expanded'); let expandedSubs = JSON.parse(localStorage.getItem('expandedSubscriptions') || '[]'); if (isHidden) { // 立即添加展开类名触发动画 nodeListArea.classList.add('expanded'); // 更新展开状态 if (!expandedSubs.includes(subscriptionPath)) { expandedSubs.push(subscriptionPath); localStorage.setItem('expandedSubscriptions', JSON.stringify(expandedSubs)); } // 检查是否需要滚动 const rect = nodeListArea.getBoundingClientRect(); const isVisible = ( rect.top >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) ); if (!isVisible) { const scrollTarget = nodeListArea.offsetTop - window.innerHeight + rect.height + 100; window.scrollTo({ top: scrollTarget, behavior: 'smooth' }); } // 同时开始加载数据 loadNodeList(subscriptionPath).catch(error => { console.error('加载节点列表失败:', error); showToast('加载节点列表失败: ' + error.message, 'danger'); }); } else { nodeListArea.classList.remove('expanded'); expandedSubs = expandedSubs.filter(path => path !== subscriptionPath); localStorage.setItem('expandedSubscriptions', JSON.stringify(expandedSubs)); } } // 修改加载节点列表函数 async function loadNodeList(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); if (!nodeListArea) { throw new Error('找不到节点列表区域'); } const tbody = nodeListArea.querySelector('tbody'); // 先显示加载中的提示 tbody.innerHTML = '<tr><td colspan="2" class="text-center py-4"><i class="fas fa-spinner fa-spin me-2"></i>加载中...</td></tr>'; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes'); if (!response.ok) throw new Error('加载失败'); const result = await response.json(); if (!result.success) { throw new Error(result.message || '加载失败'); } const nodes = result.data || []; // 构建节点列表HTML const nodesHtml = nodes.map(node => { const nodeLink = node.original_link; const escapedNodeLink = nodeLink .replace(/&/g, '&') .replace(/'/g, '\\\'') .replace(/"/g, '\\"'); return \` <tr class="node-row" data-id="\${node.id}" data-order="\${node.node_order}"> <td> <div class="d-flex align-items-center"> <div class="form-check me-2" style="display: none;"> <input class="form-check-input node-checkbox" type="checkbox" value="\${node.id}" data-subscription="\${subscriptionPath}" style="margin-top: 0;"> </div> <div class="text-nowrap text-truncate" style="max-width: 300px; padding-left: 0.5rem;" title="\${node.name}"> \${node.name} </div> </div> </td> <td> <div class="d-flex justify-content-between align-items-center" style="gap: 8px;"> <div class="text-nowrap text-truncate" style="max-width: 400px; margin-left: 4rem;" title="\${nodeLink}"> \${nodeLink} </div> <div class="node-actions d-flex" style="flex-shrink: 0; gap: 4px;"> <button class="btn btn-sm btn-edit" onclick="showEditNodeModal('\${subscriptionPath}', '\${node.id}', '\${escapedNodeLink}')" title="编辑节点"> <i class="fas fa-edit"></i> </button> <button class="btn btn-sm btn-primary" onclick="copyToClipboard('\${escapedNodeLink}')" title="复制链接"> <i class="fas fa-copy"></i> </button> <button class="btn btn-sm btn-danger" onclick="deleteNode('\${subscriptionPath}', \${node.id})" title="删除节点"> <i class="fas fa-trash"></i> </button> </div> </div> </td> </tr> \`; }).join(''); // 更新节点列表内容 tbody.innerHTML = nodesHtml || '<tr><td colspan="2" class="text-center py-4">暂无节点</td></tr>'; // 初始化拖拽排序 if (nodes.length > 0) { // 检查是否为移动设备 const isMobile = window.innerWidth <= 767; // 只在非移动设备上初始化排序 if (!isMobile) { initializeSortable(tbody, subscriptionPath); } } } catch (error) { tbody.innerHTML = \`<tr><td colspan="2" class="text-center py-4 text-danger"> <i class="fas fa-exclamation-circle me-2"></i>\${error.message} </td></tr>\`; throw error; } } // 初始化拖拽排序功能 function initializeSortable(tbody, subscriptionPath) { new Sortable(tbody, { animation: 150, handle: '.node-row', ghostClass: 'sortable-ghost', dragClass: 'sortable-drag', onEnd: async function(evt) { try { const rows = Array.from(tbody.querySelectorAll('.node-row')); const newOrders = rows.map((row, index) => ({ id: parseInt(row.dataset.id), order: index })); await updateNodeOrder(subscriptionPath, newOrders); showToast('节点排序已更新'); } catch (error) { console.error('更新节点排序失败:', error); showToast('更新节点排序失败: ' + error.message, 'danger'); // 重新加载列表以恢复原始顺序 await loadNodeList(subscriptionPath); } } }); } // 删除节点 async function deleteNode(subscriptionPath, nodeId) { if (!confirm('确定要删除这个节点吗?')) return; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '删除失败'); } showToast('节点已删除'); await loadNodeList(subscriptionPath); await loadSubscriptions(); } catch (error) { showToast('删除节点失败: ' + error.message, 'danger'); } } // 添加创建订阅函数 async function createSubscription() { const form = document.getElementById('addSubscriptionForm'); const formData = new FormData(form); const name = formData.get('name').trim(); const path = formData.get('path').trim(); if (!name) { showToast('请输入订阅名称', 'danger'); form.querySelector('[name="name"]').focus(); return; } if (!path || !validateSubscriptionPathFrontend(path)) { const pathInput = form.querySelector('[name="path"]'); pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); form.querySelector('.path-error').textContent = '路径格式不正确'; form.querySelector('.path-error').style.display = 'block'; pathInput.focus(); return; } try { const response = await fetch('/' + adminPath + '/api/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name, path }) }); const result = await response.json(); if (!response.ok) throw new Error(result.message || '创建失败'); showToast('订阅创建成功'); bootstrap.Modal.getInstance(document.getElementById('addSubscriptionModal')).hide(); form.reset(); await loadSubscriptions(); } catch (error) { showToast('创建失败: ' + error.message, 'danger'); } } // 添加显示Toast提示函数 function showToast(message, type = 'success') { const toast = document.getElementById('toast'); const toastMessage = document.getElementById('toastMessage'); // 设置消息 toastMessage.textContent = message; // 设置类型 toast.className = toast.className.replace(/bg-\w+/, ''); toast.classList.add('bg-' + type); // 显示Toast const bsToast = new bootstrap.Toast(toast); bsToast.show(); } // 显示编辑名称模态框 function showEditNameModal(path, name) { const form = document.getElementById('editNameForm'); form.querySelector('input[name="originalPath"]').value = path; form.querySelector('input[name="name"]').value = name; form.querySelector('input[name="path"]').value = path; const modal = new bootstrap.Modal(document.getElementById('editNameModal')); modal.show(); } // 更新订阅信息 async function updateSubscriptionInfo() { const form = document.getElementById('editNameForm'); const originalPath = form.querySelector('input[name="originalPath"]').value; const nameInput = form.querySelector('input[name="name"]'); const pathInput = form.querySelector('input[name="path"]'); const pathError = form.querySelector('.path-error'); try { // 验证输入 if (!nameInput.value.trim()) { showToast('请输入订阅名称', 'danger'); nameInput.focus(); return; } // 验证路径格式 const path = pathInput.value.trim(); if (!validateSubscriptionPathFrontend(path)) { pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); pathError.textContent = '路径格式不正确'; pathError.style.display = 'block'; pathInput.focus(); return; } // 如果路径被修改,检查新路径是否已存在 if (path !== originalPath) { const checkResponse = await fetch('/' + adminPath + '/api/subscriptions/' + path); if (checkResponse.ok) { pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); pathError.textContent = '该路径已被使用'; pathError.style.display = 'block'; pathInput.focus(); return; } } const response = await fetch('/' + adminPath + '/api/subscriptions/' + originalPath, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name: nameInput.value.trim(), path: path, action: 'update_info' }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '更新失败'); } showToast('订阅信息已更新'); bootstrap.Modal.getInstance(document.getElementById('editNameModal')).hide(); await loadSubscriptions(); } catch (error) { console.error('更新订阅信息失败:', error); showToast('更新失败: ' + error.message, 'danger'); } } // 添加确认删除函数 async function confirmDelete() { try { const form = document.getElementById('editNameForm'); const path = form.querySelector('input[name="originalPath"]').value; if (!path) { showToast('无效的订阅路径', 'danger'); return; } if (!confirm('确定要删除这个订阅吗?')) { return; } const response = await fetch('/' + adminPath + '/api/subscriptions/' + path, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { const result = await response.json(); throw new Error(result.message || '删除失败'); } // 关闭编辑模态框 const editModal = bootstrap.Modal.getInstance(document.getElementById('editNameModal')); if (editModal) { editModal.hide(); } showToast('订阅已删除'); await loadSubscriptions(); } catch (error) { console.error('删除失败:', error); showToast('删除失败: ' + error.message, 'danger'); } } // 表单验证工具函数(前端版本) function validateSubscriptionPathFrontend(path) { return /^[a-z0-9-]{5,50}$/.test(path); } // 添加复制到剪贴板函数 function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { showToast('已复制到剪贴板'); }).catch(() => { showToast('复制失败', 'danger'); }); } // 显示编辑节点模态框 function showEditNodeModal(subscriptionPath, nodeId, nodeContent) { const modal = document.getElementById('editNodeModal'); const form = document.getElementById('editNodeForm'); if (!modal || !form) { showToast('显示编辑模态框失败:找不到必要的页面元素', 'danger'); return; } // 设置表单值 form.querySelector('[name="subscriptionPath"]').value = subscriptionPath; form.querySelector('[name="nodeId"]').value = nodeId; form.setAttribute('data-original-content', nodeContent); form.querySelector('[name="content"]').value = nodeContent; // 显示模态框 new bootstrap.Modal(modal).show(); } // 更新节点 async function updateNode() { const form = document.getElementById('editNodeForm'); const formData = new FormData(form); const subscriptionPath = formData.get('subscriptionPath'); const nodeId = formData.get('nodeId'); const content = formData.get('content')?.trim(); const originalContent = form.getAttribute('data-original-content'); if (!subscriptionPath || !nodeId || !content) { showToast('请填写完整的节点信息', 'danger'); return; } // 检查内容是否被修改 if (content === originalContent) { showToast('节点内容未修改'); bootstrap.Modal.getInstance(document.getElementById('editNodeModal'))?.hide(); return; } try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ content }) }); const result = await response.json(); if (!response.ok) throw new Error(result.message || '更新失败'); showToast('节点更新成功'); bootstrap.Modal.getInstance(document.getElementById('editNodeModal'))?.hide(); form.reset(); // 刷新数据 await Promise.all([ loadSubscriptions(), loadNodeList(subscriptionPath) ]); } catch (error) { showToast('更新节点失败: ' + error.message, 'danger'); } } // 修改前端的排序处理代码 async function updateNodeOrder(subscriptionPath, orders) { try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ orders }) }); const result = await response.json(); if (!result.success) { throw new Error(result.message || '保存排序失败'); } return result; } catch (error) { console.error('更新节点排序失败:', error); } finally { // 无论成功还是失败都重新加载节点列表 await loadNodeList(subscriptionPath); } } // 安全的Base64解码函数(前端版本) function safeBase64DecodeFrontend(str) { try { // 方法1:使用atob解码,然后正确处理UTF-8编码 const decoded = atob(str); // 将解码结果转换为UTF-8字符串 const bytes = []; for (let i = 0; i < decoded.length; i++) { bytes.push(decoded.charCodeAt(i)); } // 使用TextDecoder正确解码UTF-8字符 if (typeof TextDecoder !== 'undefined') { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(bytes)); } else { // 如果没有TextDecoder,使用escape + decodeURIComponent let utf8String = ''; for (let i = 0; i < bytes.length; i++) { utf8String += String.fromCharCode(bytes[i]); } return decodeURIComponent(escape(utf8String)); } } catch (e) { try { // 方法2:直接使用atob,可能适用于简单ASCII字符 return atob(str); } catch (e2) { // 如果Base64解码失败,返回原字符串 return str; } } } </script> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 通用响应头 const JSON_HEADERS = { 'Content-Type': 'application/json' }; // 通用响应创建函数 function createResponse(success, message, data = null, status = success ? 200 : 500) { return new Response( JSON.stringify({ success, message, ...(data && { data }) }), { headers: JSON_HEADERS, status } ); } // 错误响应函数 const createErrorResponse = (message, status = 500) => createResponse(false, message, null, status); // 成功响应函数 const createSuccessResponse = (data = null, message = '操作成功') => createResponse(true, message, data); // 修改获取订阅列表函数 async function handleGetSubscriptions(env) { const { results } = await env.DB.prepare(` SELECT s.path, s.name, COUNT(n.id) as nodeCount FROM subscriptions s LEFT JOIN nodes n ON s.id = n.subscription_id GROUP BY s.id ORDER BY s.id ASC `).all(); const subscriptions = results.map(item => ({ name: item.name, path: item.path, nodeCount: item.nodeCount || 0 })); return createSuccessResponse(subscriptions); } // 添加获取节点列表的函数 async function handleGetNodes(env, subscriptionPath) { const { results } = await env.DB.prepare(` SELECT n.id, n.name, n.original_link, n.node_order FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? ORDER BY n.node_order ASC `).bind(subscriptionPath).all(); return createSuccessResponse(results || []); } // 添加创建节点的函数 async function handleCreateNode(request, env, subscriptionPath) { const nodeData = await request.json(); if (!nodeData.content) { return createErrorResponse('缺少节点内容', 400); } const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.trim(); // 尝试Base64解码 try { const decodedContent = safeBase64Decode(originalLink); // 检查解码后的内容是否是有效的节点链接 if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.trim(); } } catch (e) { // 不是Base64格式,继续使用原始内容 } // 验证节点类型 const lowerContent = originalLink.toLowerCase(); const isSnell = lowerContent.includes('=') && lowerContent.includes('snell,'); if (!['ss://', 'vmess://', 'trojan://', 'vless://', 'socks://', 'hysteria2://', 'tuic://'].some(prefix => lowerContent.startsWith(prefix)) && !isSnell) { return createErrorResponse('不支持的节点格式', 400); } // 从节点链接中提取名称 let nodeName = extractNodeName(originalLink); // 直接使用提供的order值 const nodeOrder = nodeData.order; // 直接插入新节点,不更新其他节点的顺序 await env.DB.prepare(` INSERT INTO nodes (subscription_id, name, original_link, node_order) VALUES (?, ?, ?, ?) `).bind(subscriptionId, nodeName, originalLink, nodeOrder).run(); return createSuccessResponse(null, '节点创建成功'); } // 添加删除节点的函数 async function handleDeleteNode(env, subscriptionPath, nodeId) { try { const result = await env.DB.prepare(` DELETE FROM nodes WHERE id = ? AND subscription_id = ( SELECT id FROM subscriptions WHERE path = ? LIMIT 1 ) `).bind(nodeId, subscriptionPath).run(); return createSuccessResponse(null, '节点已删除'); } catch (error) { return createErrorResponse('删除节点失败: ' + error.message); } } // 生成订阅内容 async function generateSubscriptionContent(env, path) { if (!path?.trim()) return ''; const { results } = await env.DB.prepare(` SELECT GROUP_CONCAT(n.original_link, CHAR(10)) as content FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? AND n.original_link IS NOT NULL GROUP BY s.id ORDER BY n.node_order ASC `).bind(path).all(); return results?.[0]?.content || ''; } // 解析SIP002格式 function parseSIP002Format(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; const nodeName = name ? decodeURIComponent(name) : '未命名节点'; return `${nodeName} = ss, ${server}, ${port}, encrypt-method=${method}, password=${password}`; } catch { return null; } } // 解析Vmess链接为Surge格式 function parseVmessLink(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; // 正确处理UTF-8编码的中文字符 const name = config.ps ? safeUtf8Decode(config.ps) : '未命名节点'; const configParts = [ `${name} = vmess`, config.add, config.port, `username=${config.id}`, 'vmess-aead=true', `tls=${config.tls === 'tls'}`, `sni=${config.add}`, 'skip-cert-verify=true', 'tfo=false' ]; if (config.tls === 'tls' && config.alpn) { configParts.push(`alpn=${config.alpn.replace(/,/g, ':')}`); } if (config.net === 'ws') { configParts.push('ws=true'); if (config.path) configParts.push(`ws-path=${config.path}`); configParts.push(`ws-headers=Host:${config.host || config.add}`); } return configParts.join(', '); } catch { return null; } } // 解析Trojan链接为Surge格式 function parseTrojanLink(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const configParts = [ `${nodeName} = trojan`, url.hostname, url.port, `password=${url.username}`, 'tls=true', `sni=${url.hostname}`, 'skip-cert-verify=true', 'tfo=false' ]; const alpn = params.get('alpn'); if (alpn) { configParts.push(`alpn=${safeDecodeURIComponent(alpn).replace(/,/g, ':')}`); } if (params.get('type') === 'ws') { configParts.push('ws=true'); const path = params.get('path'); if (path) { configParts.push(`ws-path=${safeDecodeURIComponent(path)}`); } const host = params.get('host'); configParts.push(`ws-headers=Host:${host ? safeDecodeURIComponent(host) : url.hostname}`); } return configParts.join(', '); } catch { return null; } } // 解析SOCKS链接为Surge格式 function parseSocksLink(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { // 处理标准格式:socks://username:password@server:port#name // 或者 socks://base64encoded@server:port#name const url = new URL(socksLink); if (!url.hostname || !url.port) return null; // 提取节点名称 const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; // 处理认证信息 let username = '', password = ''; // 专门处理 socks://base64auth@server:port 这样的格式 if (url.username) { // 首先对username进行URL解码 let decodedUsername = safeDecodeURIComponent(url.username); // 特殊处理 dXNlcm5hbWUxMjM6cGFzc3dvcmQxMjM= 这样的Base64编码认证信息 try { // 尝试Base64解码 const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { // 成功解码为 username:password 格式 const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } else { username = decodedUsername; } } else { // 不是预期的格式,使用原始值 username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } // 构建Surge格式 const configParts = [ nodeName + " = socks5", url.hostname, url.port ]; // 如果有用户名密码,添加到配置中 if (username) configParts.push(username); if (password) configParts.push(password); return configParts.join(', '); } catch (error) { return null; } } // 添加更新订阅信息的函数 async function handleUpdateSubscriptionInfo(env, path, data) { const name = data.name?.trim(); const newPath = data.path?.trim(); // 基本验证 if (!name) { return createErrorResponse('订阅名称不能为空', 400); } if (!validateSubscriptionPath(newPath)) { return createErrorResponse('无效的订阅路径格式', 400); } try { // 如果路径被修改,检查新路径是否已存在 if (newPath !== path) { const { results } = await env.DB.prepare(` SELECT 1 FROM subscriptions WHERE path = ? LIMIT 1 `).bind(newPath).all(); if (results.length > 0) { return createErrorResponse('该路径已被使用', 400); } } // 使用事务确保数据一致性 const statements = [ env.DB.prepare( "UPDATE subscriptions SET name = ?, path = ? WHERE path = ?" ).bind(name, newPath, path), env.DB.prepare( "SELECT id, name, path FROM subscriptions WHERE path = ?" ).bind(newPath) ]; const [updateResult, { results }] = await env.DB.batch(statements); if (!results?.[0]) { return createErrorResponse('更新失败:找不到订阅', 404); } return createSuccessResponse(results[0], '订阅信息已更新'); } catch (error) { return createErrorResponse('更新订阅信息失败: ' + error.message); } } // 添加删除订阅的处理函数 async function handleDeleteSubscription(env, path) { // 使用事务确保数据一致性 const statements = [ // 首先删除该订阅下的所有节点 env.DB.prepare( "DELETE FROM nodes WHERE subscription_id IN (SELECT id FROM subscriptions WHERE path = ?)" ).bind(path), // 然后删除订阅 env.DB.prepare( "DELETE FROM subscriptions WHERE path = ?" ).bind(path) ]; // 执行事务 await env.DB.batch(statements); return createSuccessResponse(null, '订阅已删除'); } // 添加更新节点的处理函数 async function handleUpdateNode(request, env, subscriptionPath, nodeId) { const nodeData = await request.json(); // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.replace(/[\r\n\s]+$/, ''); // 尝试base64解码 try { const decodedContent = safeBase64Decode(originalLink); if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.replace(/[\r\n\s]+$/, ''); } } catch (e) {} // 不是base64格式,继续使用原始内容 // 使用通用的节点名称提取函数 const nodeName = extractNodeName(originalLink); // 更新节点内容和名称 await env.DB.prepare(` UPDATE nodes SET original_link = ?, name = ? WHERE id = ? AND subscription_id = ? `).bind(originalLink, nodeName || '未命名节点', nodeId, subscriptionId).run(); return createSuccessResponse(null, '节点更新成功'); } // 将订阅内容转换为surge格式 function convertToSurge(content) { if (!content?.trim()) return ''; // 使用Map来映射节点类型和处理函数,提高性能 const nodeParserMap = new Map([ [NODE_TYPES.SS, parseSIP002Format], [NODE_TYPES.VMESS, parseVmessLink], [NODE_TYPES.TROJAN, parseTrojanLink], [NODE_TYPES.SOCKS, parseSocksLink], [NODE_TYPES.HYSTERIA2, parseHysteria2ToSurge], [NODE_TYPES.TUIC, parseTuicToSurge] ]); return content .split(/\r?\n/) .map(line => { const trimmedLine = line.trim(); if (!trimmedLine) return null; // 如果已经是snell格式,格式化并返回 if (trimmedLine.includes(NODE_TYPES.SNELL)) { return formatSnellConfig(trimmedLine); } // 跳过 VLESS 节点 if (trimmedLine.toLowerCase().startsWith(NODE_TYPES.VLESS)) { return null; } // 检查是否有匹配的解析器 for (const [prefix, parser] of nodeParserMap.entries()) { if (trimmedLine.startsWith(prefix)) { return parser(trimmedLine); } } return null; }) .filter(Boolean) .join('\n'); } // 格式化snell配置 function formatSnellConfig(snellConfig) { if (!snellConfig) return null; // 分割配置字符串,保持格式一致 const parts = snellConfig.split(',').map(part => part.trim()); return parts.join(', '); } // 安全的URL解码辅助函数 function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); } catch { return str; } } // 安全的Base64编码辅助函数,支持Unicode字符 function safeBase64Encode(str) { try { return btoa(unescape(encodeURIComponent(str))); } catch (e) { return str; } } // 安全的Base64解码辅助函数 function safeBase64Decode(str) { try { // 方法1:使用atob解码,然后正确处理UTF-8编码 const decoded = atob(str); // 将解码结果转换为UTF-8字符串 const bytes = []; for (let i = 0; i < decoded.length; i++) { bytes.push(decoded.charCodeAt(i)); } // 使用TextDecoder正确解码UTF-8字符 if (typeof TextDecoder !== 'undefined') { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(bytes)); } else { // 如果没有TextDecoder,使用escape + decodeURIComponent let utf8String = ''; for (let i = 0; i < bytes.length; i++) { utf8String += String.fromCharCode(bytes[i]); } return decodeURIComponent(escape(utf8String)); } } catch (e) { try { // 方法2:直接使用atob,可能适用于简单ASCII字符 return atob(str); } catch (e2) { // 如果Base64解码失败,返回原字符串 return str; } } } // 安全的UTF-8字符串解码函数 function safeUtf8Decode(str) { if (!str) return str; try { // 方法1:使用escape + decodeURIComponent return decodeURIComponent(escape(str)); } catch (e1) { try { // 方法2:直接使用decodeURIComponent return decodeURIComponent(str); } catch (e2) { try { // 方法3:使用TextDecoder(如果支持) if (typeof TextDecoder !== 'undefined') { const encoder = new TextEncoder(); const decoder = new TextDecoder('utf-8'); return decoder.decode(encoder.encode(str)); } } catch (e3) { // 如果所有方法都失败,返回原始字符串 return str; } return str; } } } // 过滤掉snell节点的函数 function filterSnellNodes(content) { if (!content?.trim()) return ''; return content .split(/\r?\n/) .filter(line => { const trimmedLine = line.trim(); if (!trimmedLine) return false; // 过滤掉snell节点 return !trimmedLine.includes(NODE_TYPES.SNELL); }) .join('\n'); } // 将订阅内容转换为 Clash 格式 function convertToClash(content) { if (!content?.trim()) { return generateEmptyClashConfig(); } const nodes = content .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) .map(parseNodeToClash) .filter(Boolean); return generateClashConfig(nodes); } // 解析单个节点为 Clash 格式 function parseNodeToClash(nodeLink) { if (!nodeLink) return null; const lowerLink = nodeLink.toLowerCase(); // 跳过 snell 节点,Clash 不支持 if (nodeLink.includes(NODE_TYPES.SNELL)) { return null; } // 解析 SS 节点 if (lowerLink.startsWith(NODE_TYPES.SS)) { return parseSSToClash(nodeLink); } // 解析 VMess 节点 if (lowerLink.startsWith(NODE_TYPES.VMESS)) { return parseVmessToClash(nodeLink); } // 解析 Trojan 节点 if (lowerLink.startsWith(NODE_TYPES.TROJAN)) { return parseTrojanToClash(nodeLink); } // 解析 VLESS 节点 if (lowerLink.startsWith(NODE_TYPES.VLESS)) { return parseVlessToClash(nodeLink); } // 解析 SOCKS 节点 if (lowerLink.startsWith(NODE_TYPES.SOCKS)) { return parseSocksToClash(nodeLink); } // 解析 Hysteria2 节点 if (lowerLink.startsWith(NODE_TYPES.HYSTERIA2)) { return parseHysteria2ToClash(nodeLink); } // 解析 TUIC 节点 if (lowerLink.startsWith(NODE_TYPES.TUIC)) { return parseTuicToClash(nodeLink); } return null; } // 解析 SS 节点为 Clash 格式 function parseSSToClash(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; return { name: name ? decodeURIComponent(name) : '未命名节点', type: 'ss', server: server, port: parseInt(port), cipher: method, password: password }; } catch { return null; } } // 解析 VMess 节点为 Clash 格式 function parseVmessToClash(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; const node = { name: config.ps ? safeUtf8Decode(config.ps) : '未命名节点', type: 'vmess', server: config.add, port: parseInt(config.port), uuid: config.id, alterId: parseInt(config.aid) || 0, cipher: 'auto', tls: config.tls === 'tls' }; // 添加网络类型配置 if (config.net === 'ws') { node.network = 'ws'; if (config.path) { node['ws-opts'] = { path: config.path, headers: { Host: config.host || config.add } }; } } else if (config.net === 'grpc') { node.network = 'grpc'; if (config.path) { node['grpc-opts'] = { 'grpc-service-name': config.path }; } } // TLS 配置 if (config.tls === 'tls') { node['skip-cert-verify'] = true; if (config.sni) { node.servername = config.sni; } } return node; } catch { return null; } } // 解析 Trojan 节点为 Clash 格式 function parseTrojanToClash(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'trojan', server: url.hostname, port: parseInt(url.port), password: url.username, 'skip-cert-verify': true }; // 添加网络类型配置 if (params.get('type') === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (params.get('type') === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 VLESS 节点为 Clash 格式 function parseVlessToClash(vlessLink) { if (!vlessLink.startsWith(NODE_TYPES.VLESS)) return null; try { const url = new URL(vlessLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'vless', server: url.hostname, port: parseInt(url.port), uuid: url.username, tls: params.get('security') === 'tls' || params.get('security') === 'reality', 'skip-cert-verify': true, tfo: false, udp: true, 'ip-version': 'dual' }; // 添加网络类型配置 const type = params.get('type'); if (type === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (type === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } else { node.network = 'tcp'; } // Reality 配置 if (params.get('security') === 'reality') { node.reality = true; const publicKey = params.get('pbk'); if (publicKey) { node['reality-opts'] = { 'public-key': publicKey }; } } // 添加 flow 参数 const flow = params.get('flow'); if (flow) { node.flow = flow; } // 添加 client-fingerprint const fp = params.get('fp'); if (fp) { node['client-fingerprint'] = fp; } // SNI 配置 const sni = params.get('sni'); if (sni) { node.servername = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 SOCKS 节点为 Clash 格式 function parseSocksToClash(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { const url = new URL(socksLink); if (!url.hostname || !url.port) return null; const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'socks5', server: url.hostname, port: parseInt(url.port) }; // 处理认证信息 if (url.username) { let username = '', password = ''; let decodedUsername = safeDecodeURIComponent(url.username); try { const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } } else { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } if (username) node.username = username; if (password) node.password = password; } return node; } catch { return null; } } // 生成 Clash 配置文件 function generateClashConfig(proxies) { const proxyNames = proxies.map(proxy => proxy.name); const config = { // 用于下载订阅时指定UA 'global-ua': 'clash', // 全局配置 mode: 'rule', 'mixed-port': 7890, 'allow-lan': true, // 控制面板 'external-controller': '0.0.0.0:9090', // 如果有代理节点,则包含代理节点配置 proxies: proxies.length > 0 ? proxies : [], // 策略组 'proxy-groups': [ { name: '节点选择', type: 'select', proxies: ['DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Proxy.png' }, { name: '媒体服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Netflix.png' }, { name: '微软服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Microsoft.png' }, { name: '苹果服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Apple.png' }, { name: 'CDN服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/OneDrive.png' }, { name: 'AI服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/ChatGPT.png' }, { name: 'Telegram', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Telegram.png' }, ], // 分流规则 rules: [ 'RULE-SET,reject_non_ip,REJECT', 'RULE-SET,reject_domainset,REJECT', 'RULE-SET,reject_extra_domainset,REJECT', 'RULE-SET,reject_non_ip_drop,REJECT-DROP', 'RULE-SET,reject_non_ip_no_drop,REJECT', // 域名类规则 'RULE-SET,telegram_non_ip,Telegram', 'RULE-SET,apple_cdn,苹果服务', 'RULE-SET,apple_cn_non_ip,苹果服务', 'RULE-SET,microsoft_cdn_non_ip,微软服务', 'RULE-SET,apple_services,苹果服务', 'RULE-SET,microsoft_non_ip,微软服务', 'RULE-SET,download_domainset,CDN服务', 'RULE-SET,download_non_ip,CDN服务', 'RULE-SET,cdn_domainset,CDN服务', 'RULE-SET,cdn_non_ip,CDN服务', 'RULE-SET,stream_non_ip,媒体服务', 'RULE-SET,ai_non_ip,AI服务', 'RULE-SET,global_non_ip,节点选择', 'RULE-SET,domestic_non_ip,DIRECT', 'RULE-SET,direct_non_ip,DIRECT', 'RULE-SET,lan_non_ip,DIRECT', 'GEOSITE,CN,DIRECT', // IP 类规则 'RULE-SET,reject_ip,REJECT', 'RULE-SET,telegram_ip,Telegram', 'RULE-SET,stream_ip,媒体服务', 'RULE-SET,lan_ip,DIRECT', 'RULE-SET,domestic_ip,DIRECT', 'RULE-SET,china_ip,DIRECT', 'GEOIP,LAN,DIRECT', 'GEOIP,CN,DIRECT', // 兜底规则 'MATCH,节点选择' ], // 规则提供者 'rule-providers': { reject_non_ip_no_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-no-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_no_drop.txt' }, reject_non_ip_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_drop.txt' }, reject_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip.txt' }, reject_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject.txt', path: './rule_set/sukkaw_ruleset/reject_domainset.txt' }, reject_extra_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject_extra.txt', path: './sukkaw_ruleset/reject_domainset_extra.txt' }, reject_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_ip.txt' }, cdn_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_domainset.txt' }, cdn_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_non_ip.txt' }, stream_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_non_ip.txt' }, stream_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_ip.txt' }, ai_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/ai.txt', path: './rule_set/sukkaw_ruleset/ai_non_ip.txt' }, telegram_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_non_ip.txt' }, telegram_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_ip.txt' }, apple_cdn: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/apple_cdn.txt', path: './rule_set/sukkaw_ruleset/apple_cdn.txt' }, apple_services: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_services.txt', path: './rule_set/sukkaw_ruleset/apple_services.txt' }, apple_cn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_cn.txt', path: './rule_set/sukkaw_ruleset/apple_cn_non_ip.txt' }, microsoft_cdn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft_cdn.txt', path: './rule_set/sukkaw_ruleset/microsoft_cdn_non_ip.txt' }, microsoft_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft.txt', path: './rule_set/sukkaw_ruleset/microsoft_non_ip.txt' }, download_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/download.txt', path: './rule_set/sukkaw_ruleset/download_domainset.txt' }, download_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/download.txt', path: './rule_set/sukkaw_ruleset/download_non_ip.txt' }, lan_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_non_ip.txt' }, lan_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_ip.txt' }, domestic_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_non_ip.txt' }, direct_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/direct.txt', path: './rule_set/sukkaw_ruleset/direct_non_ip.txt' }, global_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/global.txt', path: './rule_set/sukkaw_ruleset/global_non_ip.txt' }, domestic_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_ip.txt' }, china_ip: { type: 'http', behavior: 'ipcidr', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/china_ip.txt', path: './rule_set/sukkaw_ruleset/china_ip.txt' } } }; return `# Clash 配置文件 - Sub-Hub 自动生成 # 生成时间: ${new Date().toISOString()} ${convertToYaml(config)}`; } // 生成空的 Clash 配置 function generateEmptyClashConfig() { return generateClashConfig([]); } // 简化的对象转 YAML 函数,针对 Clash 配置优化 function convertToYaml(obj, indent = 0) { const spaces = ' '.repeat(indent); let yaml = ''; for (const [key, value] of Object.entries(obj)) { // 处理键名,只对真正需要的情况加引号 let yamlKey = key; if (key.includes(' ') || key.includes('@') || key.includes('&') || key.includes('*') || key.includes('?') || key.includes('>') || key.includes('<') || key.includes('!') || key.includes('%') || key.includes('^') || key.includes('`') || /^\d/.test(key) || key === '' || /^(true|false|null|yes|no|on|off)$/i.test(key)) { yamlKey = `"${key.replace(/"/g, '\\"')}"`; } if (value === null || value === undefined) { yaml += `${spaces}${yamlKey}: null\n`; } else if (typeof value === 'boolean') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'number') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'string') { // 对字符串值更宽松的引号判断,主要针对真正会导致 YAML 解析问题的字符 const needsQuotes = value.includes(':') || value.includes('#') || value.includes('"') || value.includes('\n') || value.includes('&') || value.includes('*') || value.includes('[') || value.includes(']') || value.includes('{') || value.includes('}') || value.includes('@') || value.includes('`') || /^\s/.test(value) || /\s$/.test(value) || value === '' || /^(true|false|null|yes|no|on|off)$/i.test(value) || (/^\d+$/.test(value) && value.length > 1) || (/^\d+\.\d+$/.test(value) && value.length > 1); if (needsQuotes) { const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces}${yamlKey}: "${escapedValue}"\n`; } else { yaml += `${spaces}${yamlKey}: ${value}\n`; } } else if (Array.isArray(value)) { if (value.length === 0) { yaml += `${spaces}${yamlKey}: []\n`; } else { yaml += `${spaces}${yamlKey}:\n`; for (const item of value) { if (typeof item === 'object' && item !== null) { yaml += `${spaces} -\n`; const itemYaml = convertToYaml(item, 0); yaml += itemYaml.split('\n').map(line => line.trim() ? `${spaces} ${line}` : '' ).filter(line => line).join('\n') + '\n'; } else if (typeof item === 'string') { // 对数组中的字符串项(如节点名称)更宽松的引号判断 const needsQuotes = item.includes(':') || item.includes('#') || item.includes('"') || item.includes('\n') || item.includes('&') || item.includes('*') || item.includes('[') || item.includes(']') || item.includes('{') || item.includes('}') || item.includes('@') || item.includes('`') || /^\s/.test(item) || /\s$/.test(item) || item === '' || /^(true|false|null|yes|no|on|off)$/i.test(item) || (/^\d+$/.test(item) && item.length > 1) || (/^\d+\.\d+$/.test(item) && item.length > 1); if (needsQuotes) { const escapedItem = item.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces} - "${escapedItem}"\n`; } else { yaml += `${spaces} - ${item}\n`; } } else { yaml += `${spaces} - ${item}\n`; } } } } else if (typeof value === 'object' && value !== null) { yaml += `${spaces}${yamlKey}:\n`; yaml += convertToYaml(value, indent + 1); } } return yaml; } // 解析 Hysteria2 节点为 Clash 格式 function parseHysteria2ToClash(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'hysteria2', server: url.hostname, port: parseInt(url.port), password: url.username || params.get('password') || '', 'skip-cert-verify': true }; // 上传和下载速度配置 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) node.up = upMbps; if (downMbps) node.down = downMbps; // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // 混淆配置 const obfs = params.get('obfs'); if (obfs) { node.obfs = safeDecodeURIComponent(obfs); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { node['obfs-password'] = safeDecodeURIComponent(obfsPassword); } } // 拥塞控制算法 const cc = params.get('cc'); if (cc) { node.cc = cc; } return node; } catch { return null; } } // 解析 TUIC 节点为 Clash 格式 function parseTuicToClash(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'tuic', server: url.hostname, port: parseInt(url.port), uuid: url.username || params.get('uuid') || '', password: url.password || params.get('password') || '', 'skip-cert-verify': true }; // TUIC 版本 const version = params.get('version') || params.get('v'); if (version) { node.version = parseInt(version); } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // UDP Relay 模式 const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) { node['udp-relay-mode'] = udpRelayMode; } // 拥塞控制算法 const cc = params.get('congestion_control') || params.get('cc'); if (cc) { node['congestion-control'] = cc; } // 禁用 SNI const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { node['disable-sni'] = true; } // 减少 RTT const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { node['reduce-rtt'] = true; } return node; } catch { return null; } } // 解析 Hysteria2 链接为 Surge 格式 function parseHysteria2ToSurge(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const password = url.username || params.get('password') || ''; // 构建 Surge 格式的 Hysteria2 配置 const configParts = [ `${nodeName} = hysteria2`, url.hostname, url.port, `password=${password}` ]; // 添加可选参数 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) configParts.push(`up=${upMbps}`); if (downMbps) configParts.push(`down=${downMbps}`); const sni = params.get('sni'); if (sni) configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); const obfs = params.get('obfs'); if (obfs) { configParts.push(`obfs=${safeDecodeURIComponent(obfs)}`); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { configParts.push(`obfs-password=${safeDecodeURIComponent(obfsPassword)}`); } } const cc = params.get('cc'); if (cc) configParts.push(`cc=${cc}`); // 默认跳过证书验证 configParts.push('skip-cert-verify=true'); return configParts.join(', '); } catch (error) { return null; } } // 解析 TUIC 链接为 Surge 格式 function parseTuicToSurge(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const uuid = url.username || params.get('uuid') || ''; const password = url.password || params.get('password') || ''; // 构建 Surge 格式的 TUIC 配置 const configParts = [ `${nodeName} = tuic`, url.hostname, url.port, `uuid=${uuid}`, `password=${password}` ]; // TUIC 版本 - 如果没有指定版本,默认使用版本 5 const version = params.get('version') || params.get('v') || '5'; configParts.push(`version=${version}`); // SNI 配置 - 如果没有指定 SNI,使用服务器地址作为 SNI const sni = params.get('sni') || url.hostname; configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); // 处理 allow_insecure 参数 const allowInsecure = params.get('allow_insecure') || params.get('allowInsecure'); if (allowInsecure === 'true' || allowInsecure === '1') { configParts.push('skip-cert-verify=true'); } else { // 如果没有明确设置 allow_insecure,默认为 false configParts.push('skip-cert-verify=false'); } const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) configParts.push(`udp-relay-mode=${udpRelayMode}`); const cc = params.get('congestion_control') || params.get('congestion-control') || params.get('cc'); if (cc) configParts.push(`congestion-control=${cc}`); const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { configParts.push('disable-sni=true'); } const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { configParts.push('reduce-rtt=true'); } return configParts.join(', '); } catch (error) { return null; } } {dotted startColor="#ff6c6c" endColor="#1989fa"/}访问系统访问管理面板:https://你的域名/ADMIN_PATH订阅地址格式:原始格式:https://你的域名/订阅路径Base64 格式:https://你的域名/订阅路径/v2raySurge 格式:https://你的域名/订阅路径/surgeClash 格式:https://你的域名/订阅路径/clash{dotted startColor="#ff6c6c" endColor="#1989fa"/}使用说明创建订阅登录管理面板点击"添加订阅"按钮输入订阅名称和路径(路径只能包含小写字母、数字和连字符)点击"创建"按钮管理节点在订阅列表中找到目标订阅点击"添加节点"按钮添加新节点支持以下格式:单个节点链接多个节点链接(每行一个)Base64 编码的节点列表节点排序点击"节点列表"按钮查看节点拖拽节点行可以调整顺序顺序会自动保存批量操作点击"批量删除"按钮进入批量模式勾选要删除的节点点击"确认删除"执行删除操作注意事项首次部署后请立即修改默认的管理员密码定期备份数据库内容妥善保管管理面板地址和登录信息建议使用强密码提高安全性
2025年05月28日
15 阅读
1 评论
0 点赞
1
2
3
...
17