diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81b98d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production builds +dist/ +build/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# Local cache +.cache/ + +# Temporary folders +tmp/ +temp/ + +.github diff --git a/BIN文件上传提取Token工具-0.5.user.js b/BIN文件上传提取Token工具-0.5.user.js new file mode 100644 index 0000000..96f8095 --- /dev/null +++ b/BIN文件上传提取Token工具-0.5.user.js @@ -0,0 +1,507 @@ +// ==UserScript== +// @name BIN文件上传提取Token工具 +// @namespace http://tampermonkey.net/ +// @version 0.5 +// @description 上传BIN文件提取RoleToken并生成WSS链接 +// @author 豆包编程助手 +// @match *://*/* +// @grant GM_xmlhttpRequest +// @grant GM_setClipboard +// ==/UserScript== + +(function() { + 'use strict'; + + // 界面状态变量 + let toolContainer = null; + let isToolVisible = false; + + // 创建工具界面 + function createToolUI() { + // 检查是否已存在界面 + if (document.getElementById('bin-token-extractor')) { + toolContainer = document.getElementById('bin-token-extractor'); + return toolContainer; + } + + // 创建容器(固定宽度380px) + const container = document.createElement('div'); + container.id = 'bin-token-extractor'; + container.style.cssText = ` + position: fixed; + top: 50%; + right: 20px; + transform: translateY(-50%); + background: linear-gradient(180deg, #ffffff 0%, #f9fbfd 100%); + border-radius: 16px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12); + width: 380px; + max-height: 80vh; + overflow-y: auto; + padding: 25px; + z-index: 99999; + display: none; /* 默认隐藏 */ + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + `; + + // 优化后的HTML结构 + container.innerHTML = ` +
+

BIN文件Token提取工具

+

上传文件,提取RoleToken并生成WSS链接

+
+ +
+
📂
+

点击或拖放BIN文件

+

仅支持 .bin 格式文件

+ +
+ + + + + + + + + + + `; + + document.body.appendChild(container); + toolContainer = container; + return container; + } + + // 切换工具显示/隐藏状态 + function toggleTool() { + if (!toolContainer) { + createToolUI(); + } + + isToolVisible = !isToolVisible; + + if (isToolVisible) { + // 显示工具并添加淡入动画 + toolContainer.style.display = 'block'; + setTimeout(() => { + toolContainer.style.opacity = '1'; + toolContainer.style.transform = 'translateY(-50%) scale(1)'; + }, 10); + // 初始化工具功能 + initToolFunctions(); + } else { + // 添加淡出动画后隐藏 + toolContainer.style.opacity = '0'; + toolContainer.style.transform = 'translateY(-50%) scale(0.95)'; + setTimeout(() => { + toolContainer.style.display = 'none'; + }, 300); + } + } + + // 初始化工具功能 + function initToolFunctions() { + if (!toolContainer) return; + + // 获取DOM元素 + const uploadArea = toolContainer.querySelector('#uploadArea'); + const fileInput = toolContainer.querySelector('#fileInput'); + const fileInfo = toolContainer.querySelector('#fileInfo'); + const uploadBtn = toolContainer.querySelector('#uploadBtn'); + const progressContainer = toolContainer.querySelector('#progressContainer'); + const progressBar = toolContainer.querySelector('#progressBar'); + const progressText = toolContainer.querySelector('#progressText'); + const statusMessage = toolContainer.querySelector('#statusMessage'); + const resultContainer = toolContainer.querySelector('#resultContainer'); + const wssLinkDisplay = toolContainer.querySelector('#wssLinkDisplay'); + const copyWssLinkBtn = toolContainer.querySelector('#copyWssLinkBtn'); + + let selectedFile = null; + let extractedToken = null; + + // 点击上传区域触发文件选择 + uploadArea.addEventListener('click', function(e) { + if (e.target !== fileInput) { + fileInput.click(); + } + }); + + // 拖放功能 + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + uploadArea.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + ['dragenter', 'dragover'].forEach(eventName => { + uploadArea.addEventListener(eventName, highlight, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + uploadArea.addEventListener(eventName, unhighlight, false); + }); + + function highlight() { + uploadArea.style.borderColor = '#3b82f6'; + uploadArea.style.backgroundColor = 'rgba(59, 130, 246, 0.05)'; + uploadArea.style.transform = 'scale(1.02)'; + } + + function unhighlight() { + uploadArea.style.borderColor = '#d1d9e6'; + uploadArea.style.backgroundColor = '#f8fafc'; + uploadArea.style.transform = 'scale(1)'; + } + + // 文件拖放处理 + uploadArea.addEventListener('drop', handleDrop, false); + + function handleDrop(e) { + const dt = e.dataTransfer; + const files = dt.files; + + if (files.length) { + handleFiles(files); + } + } + + // 文件选择处理 + fileInput.addEventListener('change', function() { + if (this.files.length) { + handleFiles(this.files); + } + }); + + function handleFiles(files) { + const file = files[0]; + + // 检查文件类型 + if (!file.name.toLowerCase().endsWith('.bin')) { + showStatus('请选择.bin格式的文件', 'error'); + return; + } + + selectedFile = file; + updateFileInfo(file); + uploadBtn.disabled = false; + uploadBtn.style.opacity = '1'; + uploadBtn.style.transform = 'translateY(0)'; + } + + function updateFileInfo(file) { + fileInfo.textContent = `已选择: ${file.name} (${formatFileSize(file.size)})`; + fileInfo.style.display = 'block'; + // 添加淡入动画 + fileInfo.style.opacity = '0'; + setTimeout(() => { + fileInfo.style.opacity = '1'; + }, 10); + } + + function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + // 上传按钮点击事件 + uploadBtn.addEventListener('click', function() { + if (!selectedFile) { + showStatus('请先选择文件', 'error'); + return; + } + + // 禁用上传按钮,防止重复点击 + uploadBtn.disabled = true; + uploadBtn.style.opacity = '0.7'; + uploadBtn.style.transform = 'translateY(2px)'; + + // 隐藏之前的结果 + resultContainer.style.display = 'none'; + + // 显示进度条 + progressContainer.style.display = 'block'; + progressBar.style.width = '0%'; + progressText.textContent = '0%'; + + // 直接上传文件 + uploadFile(selectedFile); + }); + + function uploadFile(file) { + showStatus('正在上传文件...', ''); + + // 读取文件内容 + const reader = new FileReader(); + reader.onload = function(e) { + const arrayBuffer = e.target.result; + + // 配置请求 + GM_xmlhttpRequest({ + method: 'POST', + url: 'https://xxz-xyzw.hortorgames.com/login/authuser?_seq=1', + data: arrayBuffer, + headers: { + 'Content-Type': 'application/octet-stream' + }, + responseType: 'arraybuffer', + onprogress: function(e) { + if (e.lengthComputable) { + const percentComplete = (e.loaded / e.total) * 100; + progressBar.style.width = percentComplete + '%'; + progressText.textContent = Math.round(percentComplete) + '%'; + } + }, + onload: function(response) { + if (response.status >= 200 && response.status < 300) { + try { + // 处理二进制响应 + const arrayBuffer = response.response; + if (arrayBuffer) { + // 提取RoleToken + extractRoleToken(arrayBuffer); + showStatus('Token提取成功!', 'success'); + } else { + showStatus('上传成功,但响应为空', 'error'); + } + } catch (e) { + showStatus('处理响应时出错: ' + e.message, 'error'); + } + } else { + showStatus('上传失败: ' + response.statusText, 'error'); + } + + // 重新启用上传按钮 + uploadBtn.disabled = false; + uploadBtn.style.opacity = '1'; + uploadBtn.style.transform = 'translateY(0)'; + }, + onerror: function() { + showStatus('上传过程中发生错误', 'error'); + uploadBtn.disabled = false; + uploadBtn.style.opacity = '1'; + uploadBtn.style.transform = 'translateY(0)'; + } + }); + }; + + reader.onerror = function() { + showStatus('读取文件失败', 'error'); + uploadBtn.disabled = false; + uploadBtn.style.opacity = '1'; + uploadBtn.style.transform = 'translateY(0)'; + }; + + reader.readAsArrayBuffer(file); + } + + function extractRoleToken(arrayBuffer) { + try { + // 将ArrayBuffer转换为Uint8Array以便处理 + const bytes = new Uint8Array(arrayBuffer); + + // 转换为ASCII字符串以便搜索 + let asciiString = ''; + for (let i = 0; i < bytes.length; i++) { + // 只转换可打印的ASCII字符(32-126) + if (bytes[i] >= 32 && bytes[i] <= 126) { + asciiString += String.fromCharCode(bytes[i]); + } else { + asciiString += '.'; // 用点号表示不可打印字符 + } + } + + // 搜索Token的位置 - 查找 "Token" 字符串 + const tokenIndex = asciiString.indexOf('Token'); + + if (tokenIndex !== -1) { + // 找到Token标记,提取Token值 + let tokenStart = tokenIndex + 5; // "Token"长度为5 + + // 跳过可能的非Base64字符,直到找到Base64字符 + while (tokenStart < asciiString.length) { + const char = asciiString[tokenStart]; + if (isBase64Char(char)) { + break; + } + tokenStart++; + } + + // 提取Base64 Token + let tokenEnd = tokenStart; + while (tokenEnd < asciiString.length && isBase64Char(asciiString[tokenEnd])) { + tokenEnd++; + } + + const tokenValue = asciiString.substring(tokenStart, tokenEnd); + + if (tokenValue.length > 0) { + extractedToken = tokenValue; + resultContainer.style.display = 'block'; + + // 触发动画 + setTimeout(() => { + resultContainer.style.transform = 'translateY(0)'; + resultContainer.style.opacity = '1'; + }, 10); + + // 生成并显示完整的WSS链接 + generateAndDisplayWssLink(extractedToken); + + // 平滑滚动到结果区域 + resultContainer.scrollIntoView({ behavior: 'smooth' }); + } else { + showStatus('找到Token标记但未找到Token值', 'error'); + } + } else { + showStatus('在响应中未找到Token标记', 'error'); + } + } catch (error) { + showStatus('提取Token时发生错误: ' + error.message, 'error'); + } + } + + function isBase64Char(char) { + // Base64字符集: A-Z, a-z, 0-9, +, /, = + return /[A-Za-z0-9+/=]/.test(char); + } + + function showStatus(message, type) { + statusMessage.textContent = message; + statusMessage.className = ''; + statusMessage.style.backgroundColor = ''; + statusMessage.style.color = ''; + statusMessage.style.boxShadow = 'none'; + + if (type === 'success') { + statusMessage.style.backgroundColor = 'rgba(16, 185, 129, 0.1)'; + statusMessage.style.color = '#059669'; + statusMessage.style.borderLeft = '3px solid #10b981'; + } else if (type === 'error') { + statusMessage.style.backgroundColor = 'rgba(239, 68, 68, 0.1)'; + statusMessage.style.color = '#dc2626'; + statusMessage.style.borderLeft = '3px solid #ef4444'; + } else { + statusMessage.style.backgroundColor = 'rgba(59, 130, 246, 0.1)'; + statusMessage.style.color = '#2563eb'; + statusMessage.style.borderLeft = '3px solid #3b82f6'; + } + + statusMessage.style.display = 'block'; + statusMessage.style.opacity = '0'; + statusMessage.style.transform = 'translateY(10px)'; + + // 触发淡入动画 + setTimeout(() => { + statusMessage.style.opacity = '1'; + statusMessage.style.transform = 'translateY(0)'; + }, 10); + + // 3秒后自动隐藏非错误状态 + if (type !== 'error') { + setTimeout(() => { + statusMessage.style.opacity = '0'; + statusMessage.style.transform = 'translateY(10px)'; + setTimeout(() => { + statusMessage.style.display = 'none'; + }, 300); + }, 3000); + } + } + + // 复制完整WSS链接按钮事件 + copyWssLinkBtn.addEventListener('click', function() { + const wssLink = wssLinkDisplay.textContent; + + if (!wssLink) return; + + GM_setClipboard(wssLink); + // 按钮点击反馈 + this.style.backgroundColor = '#059669'; + setTimeout(() => { + this.style.backgroundColor = '#10b981'; + }, 200); + showStatus('WSS链接已复制', 'success'); + }); + + // 生成并显示完整的WSS链接 + function generateAndDisplayWssLink(token) { + // 生成随机的会话ID和连接ID + const currentTime = Date.now(); + const sessId = currentTime * 100 + Math.floor(Math.random() * 100); + const connId = currentTime + Math.floor(Math.random() * 10); + + // 构建WebSocket参数 + const wssParams = `{"roleToken":"${token}","sessId":${sessId},"connId":${connId},"isRestore":0}`; + + // 显示完整的WSS链接参数 + wssLinkDisplay.textContent = wssParams; + } + } + + // 创建切换按钮(兼具显示和关闭功能) + function createToggleButton() { + // 创建按钮 + const toggleBtn = document.createElement('button'); + toggleBtn.innerHTML = `🔑BIN Token提取`; + toggleBtn.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%); + color: white; + border: none; + padding: 10px 18px; + border-radius: 50px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3); + white-space: nowrap; + z-index: 99998; + transition: all 0.3s; + `; + + // 添加悬停效果 + toggleBtn.addEventListener('mouseenter', () => { + toggleBtn.style.transform = 'translateY(-2px)'; + toggleBtn.style.boxShadow = '0 6px 20px rgba(59, 130, 246, 0.4)'; + }); + + toggleBtn.addEventListener('mouseleave', () => { + toggleBtn.style.transform = 'translateY(0)'; + toggleBtn.style.boxShadow = '0 4px 15px rgba(59, 130, 246, 0.3)'; + }); + + // 添加点击事件 - 切换显示/隐藏 + toggleBtn.addEventListener('click', toggleTool); + + document.body.appendChild(toggleBtn); + } + + // 页面加载完成后创建切换按钮 + window.addEventListener('load', createToggleButton); +})(); \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5fe8aaa --- /dev/null +++ b/LICENSE @@ -0,0 +1,42 @@ +Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License + +Copyright (c) 2024 XYZW Team + +This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. + +You are free to: +- Share — copy and redistribute the material in any medium or format +- Adapt — remix, transform, and build upon the material + +The licensor cannot revoke these freedoms as long as you follow the license terms. + +Under the following terms: +- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. +- NonCommercial — You may not use the material for commercial purposes. +- ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. +- No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. + +Notices: +You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. + +No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. + +For the full legal text of this license, please visit: +https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode + +--- + +ADDITIONAL TERMS FOR THIS SOFTWARE: + +This software is specifically designed for educational and personal use only. +Commercial use, including but not limited to: +- Selling this software or derivative works +- Using this software in commercial gaming operations +- Integrating this software into commercial products or services +- Using this software to generate revenue in any form + +is strictly prohibited without explicit written permission from the copyright holders. + +The software is provided "AS IS", without warranty of any kind, express or implied, +including but not limited to the warranties of merchantability, fitness for a +particular purpose and noninfringement. \ No newline at end of file diff --git a/auto.js b/auto.js new file mode 100644 index 0000000..b3194da --- /dev/null +++ b/auto.js @@ -0,0 +1,2057 @@ +// ==UserScript== +// @name 管理系统自动化-本地 +// @namespace xyzw-helper-auto +// @version 0.1.0 +// @description 在 tokens 与 game-features 页,自动循环每个角色执行:一键补差→启动服务→领取奖励→加钟→立即签到→一键答题。通过正则匹配名称并点击最近的可点击控件。 +// @author you +// @match http://localhost:3001/tokens +// @match http://localhost:3001/game-features +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_registerMenuCommand +// @grant GM_addStyle +// @run-at document-end +// ==/UserScript== + +(function () { + "use strict"; + + // -------------- 基础工具 -------------- + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + const nowStr = () => new Date().toLocaleTimeString("zh-CN"); + + // -------------- 运行日志 -------------- + const LOG_KEY = "automation_logs"; + const LOG_LIMIT_KEY = "automation_log_limit"; + const ACTION_DELAY_KEY = "automation_action_delay_ms"; + const TASKS_KEY = "automation_tasks_config"; + const TASKS_UI_OPEN_KEY = "automation_tasks_ui_open"; + const TASK_PROGRESS_KEY = "automation_tasks_progress"; + const RESTART_INTERVAL_KEY = "automation_restart_interval_minutes"; + const PAGE_READY_DELAY_MS = 2000; + const CONFIG_STORE_KEY = "automation_config_store_v1"; + const DEFAULT_PROFILE_NAME = "default"; + + let configCache = null; + let configManagerOpen = false; + + function clampNumber(value, min, max, fallback) { + const num = Number(value); + if (!Number.isFinite(num)) return fallback; + if (min !== undefined && num < min) return min; + if (max !== undefined && num > max) return max; + return num; + } + + function deepClone(value) { + try { + return JSON.parse(JSON.stringify(value)); + } catch (_) { + return value; + } + } + + function defaultProfileSettings() { + return { + logLimit: 200, + actionDelayMs: 3000, + restartIntervalMinutes: 0, + tasks: defaultTasksConfig(), + tasksUiOpen: false, + description: "", + }; + } + + const TASK_NAME_MAP = { + dailyFix: "一键补差", + answer: "一键答题", + claimReward: "领取奖励", + addClock: "加钟", + signIn: "立即签到", + startService: "启动/重启服务", + }; + + function normalizeTasks(tasks) { + const base = defaultTasksConfig(); + const input = tasks && typeof tasks === "object" ? tasks : {}; + const next = { ...base }; + for (const key in base) { + next[key] = !!input[key]; + } + return next; + } + + function normalizeProfile(profile) { + const base = defaultProfileSettings(); + const input = profile && typeof profile === "object" ? profile : {}; + const next = { + ...base, + ...deepClone(input), + }; + next.logLimit = clampNumber(next.logLimit, 20, 2000, base.logLimit); + next.actionDelayMs = clampNumber( + next.actionDelayMs, + 0, + 600000, + base.actionDelayMs + ); + next.restartIntervalMinutes = clampNumber( + next.restartIntervalMinutes, + 0, + 1440, + base.restartIntervalMinutes + ); + next.tasks = normalizeTasks(next.tasks); + next.tasksUiOpen = !!next.tasksUiOpen; + if (typeof next.description !== "string") next.description = ""; + return next; + } + + function createEmptyStore() { + const profile = normalizeProfile(null); + const now = Date.now(); + return { + version: 1, + active: DEFAULT_PROFILE_NAME, + profiles: { + [DEFAULT_PROFILE_NAME]: profile, + }, + createdAt: now, + updatedAt: now, + }; + } + + function normalizeConfigStore(raw) { + const base = createEmptyStore(); + const input = raw && typeof raw === "object" ? raw : {}; + const store = { + version: Number.isFinite(input.version) ? input.version : base.version, + active: + typeof input.active === "string" && input.active.trim() + ? input.active.trim() + : base.active, + profiles: {}, + createdAt: Number.isFinite(input.createdAt) + ? input.createdAt + : base.createdAt, + updatedAt: Number.isFinite(input.updatedAt) + ? input.updatedAt + : base.updatedAt, + }; + + const rawProfiles = input.profiles && typeof input.profiles === "object" + ? input.profiles + : {}; + + for (const [name, profile] of Object.entries(rawProfiles)) { + if (!name) continue; + store.profiles[name] = normalizeProfile(profile); + } + + if (!Object.keys(store.profiles).length) { + store.profiles[DEFAULT_PROFILE_NAME] = normalizeProfile(null); + } + + if (!store.profiles[store.active]) { + store.active = Object.keys(store.profiles)[0] || DEFAULT_PROFILE_NAME; + } + + return store; + } + + function loadConfigStoreFromStorage() { + try { + const stored = gmGet(CONFIG_STORE_KEY, null); + if (stored && typeof stored === "object" && stored.profiles) { + return stored; + } + } catch (_) { } + return null; + } + + function migrateLegacySettings() { + const store = createEmptyStore(); + const profile = store.profiles[DEFAULT_PROFILE_NAME]; + + const legacyLogLimit = gmGet(LOG_LIMIT_KEY, null); + if (legacyLogLimit != null) { + profile.logLimit = clampNumber(legacyLogLimit, 20, 2000, profile.logLimit); + } + + const legacyDelay = gmGet(ACTION_DELAY_KEY, null); + if (legacyDelay != null) { + profile.actionDelayMs = clampNumber( + legacyDelay, + 0, + 600000, + profile.actionDelayMs + ); + } + + const legacyInterval = gmGet(RESTART_INTERVAL_KEY, null); + if (legacyInterval != null) { + profile.restartIntervalMinutes = clampNumber( + legacyInterval, + 0, + 1440, + profile.restartIntervalMinutes + ); + } + + const legacyTasks = gmGet(TASKS_KEY, null); + if (legacyTasks && typeof legacyTasks === "object") { + profile.tasks = normalizeTasks(legacyTasks); + } + + const legacyTasksUiOpen = gmGet(TASKS_UI_OPEN_KEY, null); + if (legacyTasksUiOpen != null) { + profile.tasksUiOpen = !!legacyTasksUiOpen; + } + + store.updatedAt = Date.now(); + return store; + } + + function saveConfigStore(store) { + configCache = normalizeConfigStore(store); + configCache.updatedAt = Date.now(); + gmSet(CONFIG_STORE_KEY, configCache); + } + + function getConfigStore() { + if (configCache) return configCache; + let raw = loadConfigStoreFromStorage(); + if (!raw) { + raw = migrateLegacySettings(); + } + configCache = normalizeConfigStore(raw); + saveConfigStore(configCache); + return configCache; + } + + function getActiveProfileName(store = getConfigStore()) { + return store.active && store.profiles[store.active] + ? store.active + : DEFAULT_PROFILE_NAME; + } + + function setActiveProfileName(name) { + const store = getConfigStore(); + const key = typeof name === "string" ? name.trim() : ""; + if (!key || !store.profiles[key]) return false; + if (store.active !== key) { + store.active = key; + saveConfigStore(store); + } + return true; + } + + function getActiveProfile(store = getConfigStore()) { + const name = getActiveProfileName(store); + if (!store.profiles[name]) { + store.profiles[name] = normalizeProfile(null); + saveConfigStore(store); + } + return store.profiles[name]; + } + + function updateActiveProfileConfig(updater) { + const store = getConfigStore(); + const name = getActiveProfileName(store); + const current = getActiveProfile(store); + const draft = { ...current, tasks: { ...current.tasks } }; + const nextRaw = + typeof updater === "function" + ? updater(draft) || draft + : { ...draft, ...(updater || {}) }; + store.profiles[name] = normalizeProfile(nextRaw); + saveConfigStore(store); + return store.profiles[name]; + } + + function listProfileNames() { + const store = getConfigStore(); + return Object.keys(store.profiles); + } + + function ensureUniqueProfileName(name) { + const trimmed = (name || "").trim(); + if (!trimmed) return null; + return trimmed; + } + + function hasProfile(name) { + const store = getConfigStore(); + return !!store.profiles[name]; + } + + function createProfile(name, baseProfile) { + const store = getConfigStore(); + const key = ensureUniqueProfileName(name); + if (!key || store.profiles[key]) return false; + const source = baseProfile && typeof baseProfile === "object" + ? baseProfile + : defaultProfileSettings(); + store.profiles[key] = normalizeProfile(source); + saveConfigStore(store); + return true; + } + + function deleteProfile(name) { + const store = getConfigStore(); + const key = ensureUniqueProfileName(name); + if (!key || key === DEFAULT_PROFILE_NAME || !store.profiles[key]) { + return false; + } + delete store.profiles[key]; + if (store.active === key) { + store.active = DEFAULT_PROFILE_NAME; + if (!store.profiles[store.active]) { + const fallback = Object.keys(store.profiles)[0]; + store.active = fallback || DEFAULT_PROFILE_NAME; + } + } + saveConfigStore(store); + return true; + } + + function renameProfile(oldName, newName) { + const store = getConfigStore(); + const from = ensureUniqueProfileName(oldName); + const to = ensureUniqueProfileName(newName); + if ( + !from || + !to || + from === to || + !store.profiles[from] || + store.profiles[to] || + from === DEFAULT_PROFILE_NAME + ) { + return false; + } + store.profiles[to] = store.profiles[from]; + delete store.profiles[from]; + if (store.active === from) { + store.active = to; + } + saveConfigStore(store); + return true; + } + + function summarizeTasksForProfile(profile) { + if (!profile) return "无"; + const enabled = Object.entries(profile.tasks || {}) + .filter(([, flag]) => flag) + .map(([key]) => TASK_NAME_MAP[key] || key); + return enabled.length ? enabled.join("、") : "无"; + } + + function isConfigManagerOpen() { + return configManagerOpen; + } + + function setConfigManagerOpen(v) { + configManagerOpen = !!v; + } + + function toggleConfigManagerPanel() { + setConfigManagerOpen(!isConfigManagerOpen()); + updatePanel(); + } + + function openConfigManager() { + setConfigManagerOpen(true); + updatePanel(); + notify("已打开配置管理,请在面板中操作。", "info"); + } + + function handleConfigUse(name) { + if (!name || !hasProfile(name)) { + alert(`配置「${name}」不存在`); + return; + } + if (getActiveProfileName() === name) { + return; + } + if (setActiveProfileName(name)) { + notify(`已切换到配置「${name}」`, "success"); + updatePanel(); + } + } + + function handleConfigRename(name) { + if (!name || !hasProfile(name)) { + alert(`配置「${name}」不存在`); + return; + } + if (name === DEFAULT_PROFILE_NAME) { + alert("默认配置不可重命名"); + return; + } + const input = prompt("输入新的配置名称", name); + if (input == null) return; + const trimmed = input.trim(); + if (!trimmed) { + alert("名称不能为空"); + return; + } + if (hasProfile(trimmed)) { + alert(`配置「${trimmed}」已存在`); + return; + } + if (renameProfile(name, trimmed)) { + notify(`已将配置「${name}」重命名为「${trimmed}」`, "success"); + updatePanel(); + } else { + alert("重命名失败"); + } + } + + function handleConfigDelete(name) { + if (!name || !hasProfile(name)) { + alert(`配置「${name}」不存在`); + return; + } + if (name === DEFAULT_PROFILE_NAME) { + alert("默认配置不可删除"); + return; + } + if (!confirm(`确定要删除配置「${name}」吗?该操作不可恢复。`)) { + return; + } + if (deleteProfile(name)) { + notify(`已删除配置「${name}」`, "success"); + updatePanel(); + } else { + alert("删除配置失败"); + } + } + + function handleConfigCreate() { + const input = prompt("输入新配置名称"); + if (input == null) return; + const name = input.trim(); + if (!name) { + alert("名称不能为空"); + return; + } + if (hasProfile(name)) { + alert(`配置「${name}」已存在`); + return; + } + const useCurrent = confirm("是否复制当前配置?选择“取消”则使用默认配置。"); + const base = useCurrent ? deepClone(getActiveProfile()) : defaultProfileSettings(); + if (!createProfile(name, base)) { + alert("创建配置失败"); + return; + } + setActiveProfileName(name); + notify(`已创建并切换到配置「${name}」`, "success"); + updatePanel(); + } + + function renderConfigList(listEl) { + if (!listEl) return; + const store = getConfigStore(); + const active = getActiveProfileName(store); + const names = listProfileNames(); + + while (listEl.firstChild) listEl.removeChild(listEl.firstChild); + + if (!names.length) { + const empty = document.createElement("div"); + empty.className = "config-empty"; + empty.textContent = "暂无配置"; + listEl.appendChild(empty); + return; + } + + names.forEach((name) => { + const profile = store.profiles[name]; + const item = document.createElement("div"); + item.className = "config-item"; + item.dataset.profileName = name; + if (name === active) item.classList.add("active"); + + const header = document.createElement("div"); + header.className = "config-item-header"; + const title = document.createElement("span"); + title.textContent = name; + header.appendChild(title); + if (name === active) { + const badge = document.createElement("span"); + badge.className = "config-badge"; + badge.textContent = "当前"; + header.appendChild(badge); + } + + const meta = document.createElement("div"); + meta.className = "config-item-meta"; + meta.textContent = `任务:${summarizeTasksForProfile(profile)}|等待:${profile.actionDelayMs}ms|间隔:${profile.restartIntervalMinutes}分|日志:${profile.logLimit}`; + + const buttons = document.createElement("div"); + buttons.className = "config-item-buttons"; + + const useBtn = document.createElement("button"); + useBtn.type = "button"; + useBtn.dataset.configAction = "use"; + useBtn.dataset.profileName = name; + useBtn.textContent = name === active ? "已启用" : "启用"; + if (name === active) useBtn.disabled = true; + + const renameBtn = document.createElement("button"); + renameBtn.type = "button"; + renameBtn.dataset.configAction = "rename"; + renameBtn.dataset.profileName = name; + renameBtn.textContent = "重命名"; + if (name === DEFAULT_PROFILE_NAME) renameBtn.disabled = true; + + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.dataset.configAction = "delete"; + deleteBtn.dataset.profileName = name; + deleteBtn.textContent = "删除"; + if (name === DEFAULT_PROFILE_NAME) deleteBtn.disabled = true; + + buttons.append(useBtn, renameBtn, deleteBtn); + item.append(header, meta, buttons); + listEl.appendChild(item); + }); + } + + function getLogLimit() { + const profile = getActiveProfile(); + return profile?.logLimit ?? defaultProfileSettings().logLimit; + } + function setLogLimit(n) { + const v = parseInt(n, 10); + if (!Number.isFinite(v) || v <= 0) return false; + updateActiveProfileConfig({ logLimit: clampNumber(v, 20, 2000, 200) }); + return true; + } + + // 动作等待配置(默认 3000ms,可自定义) + function getActionDelayMs() { + const profile = getActiveProfile(); + const v = parseInt(profile?.actionDelayMs, 10); + if (!Number.isFinite(v) || v < 0) return defaultProfileSettings().actionDelayMs; + return Math.min(Math.max(v, 0), 600000); // 0ms ~ 10分钟 + } + function setActionDelayMs(n) { + const v = parseInt(n, 10); + if (!Number.isFinite(v) || v < 0) return false; + updateActiveProfileConfig({ + actionDelayMs: clampNumber(v, 0, 600000, defaultProfileSettings().actionDelayMs), + }); + return true; + } + async function waitBetweenSteps() { + const ms = getActionDelayMs(); + if (ms > 0) await sleep(ms); + } + + // 下次启动间隔配置(默认 0 分钟,可自定义) + function getRestartIntervalMinutes() { + const profile = getActiveProfile(); + const v = parseInt(profile?.restartIntervalMinutes, 10); + if (!Number.isFinite(v) || v < 0) { + return defaultProfileSettings().restartIntervalMinutes; + } + return Math.min(Math.max(v, 0), 1440); // 0分钟 ~ 24小时 + } + function setRestartIntervalMinutes(n) { + const v = parseInt(n, 10); + if (!Number.isFinite(v) || v < 0) return false; + updateActiveProfileConfig({ + restartIntervalMinutes: clampNumber( + v, + 0, + 1440, + defaultProfileSettings().restartIntervalMinutes + ), + }); + return true; + } + async function waitForRestartInterval() { + const minutes = getRestartIntervalMinutes(); + if (minutes > 0) { + const ms = minutes * 60 * 1000; + const nextTime = new Date(Date.now() + ms); + const nextTimeStr = nextTime.toLocaleTimeString("zh-CN"); + notify(`等待下次启动间隔 ${minutes} 分钟,将在 ${nextTimeStr} 开始下一轮...`, "info"); + await sleep(ms); + } + } + + // 任务选择配置(默认全启用) + function defaultTasksConfig() { + return { + dailyFix: true, // 一键补差 + answer: true, // 一键答题 + claimReward: true, // 领取奖励 + addClock: true, // 加钟 + signIn: true, // 立即签到 + startService: true, // 启动/重启服务 + }; + } + function getTasksConfig() { + const profile = getActiveProfile(); + const cfg = profile?.tasks && typeof profile.tasks === "object" + ? profile.tasks + : defaultTasksConfig(); + return { ...defaultTasksConfig(), ...deepClone(cfg) }; + } + function setTasksConfig(cfg) { + const base = defaultTasksConfig(); + const next = { ...base }; + for (const k in base) { + next[k] = !!(cfg && typeof cfg === "object" && cfg[k]); + } + updateActiveProfileConfig({ tasks: next }); + return next; + } + function isTasksUIOpen() { + const profile = getActiveProfile(); + return !!profile?.tasksUiOpen; + } + function setTasksUIOpen(v) { + updateActiveProfileConfig({ tasksUiOpen: !!v }); + } + + // 任务进度记录 + // 状态:pending | ok | fail | skip + function defaultTaskProgress(cfg) { + const base = { + dailyFix: "skip", + answer: "skip", + claimReward: "skip", + addClock: "skip", + signIn: "skip", + startService: "skip", + }; + const c = cfg || getTasksConfig(); + const res = { ...base }; + for (const k in base) res[k] = c[k] ? "pending" : "skip"; + return res; + } + function getTaskProgress() { + const p = gmGet(TASK_PROGRESS_KEY, null); + if (!p || typeof p !== "object") return defaultTaskProgress(); + // 兼容缺失字段 + return { ...defaultTaskProgress(), ...p }; + } + function setTaskProgress(patch) { + const cur = getTaskProgress(); + const next = { ...cur, ...(patch || {}) }; + gmSet(TASK_PROGRESS_KEY, next); + try { + updatePanel(); + } catch (_) { } + return next; + } + function clearTaskProgress() { + gmSet(TASK_PROGRESS_KEY, defaultTaskProgress()); + try { + updatePanel(); + } catch (_) { } + } + function initTaskProgressFromConfig() { + const next = defaultTaskProgress(); + gmSet(TASK_PROGRESS_KEY, next); + return next; + } + function summarizeTaskProgress() { + const p = getTaskProgress(); + const order = [ + ["dailyFix", "补差"], + ["answer", "答题"], + ["claimReward", "领奖"], + ["addClock", "加钟"], + ["signIn", "签到"], + ["startService", "启动"], + ]; + const icon = (s) => + s === "ok" ? "✓" : s === "fail" ? "✗" : s === "skip" ? "—" : "…"; + let okCnt = 0, + totalCnt = 0; + order.forEach(([k]) => { + if (p[k] !== "skip") totalCnt += 1; + if (p[k] === "ok") okCnt += 1; + }); + const parts = order.map(([k, name]) => `${name}${icon(p[k])}`); + return { text: parts.join(" "), okCnt, totalCnt }; + } + + function getLogs() { + const arr = gmGet(LOG_KEY, []); + return Array.isArray(arr) ? arr : []; + } + function setLogs(arr) { + gmSet(LOG_KEY, Array.isArray(arr) ? arr : []); + } + function clearLogs() { + setLogs([]); + } + + function addLog(type, message) { + const limit = getLogLimit(); + const list = getLogs(); + const item = { + ts: Date.now(), + time: nowStr(), + type: String(type || "info"), + message: String(message || ""), + }; + list.push(item); + if (list.length > limit) { + list.splice(0, list.length - limit); + } + setLogs(list); + // 实时刷新面板日志视图(若存在) + try { + renderLogsToPanel(); + } catch (_) { } + } + + function notify(text, type = "info") { + // 仅写入控制台与面板日志,不触发浏览器通知 + console.log(`[自动化][${nowStr()}] ${text}`); + addLog(type, text); + } + + function gmGet(key, defVal) { + try { + if (typeof GM_getValue === "function") + return GM_getValue(key, defVal); + } catch (_) { } + try { + const raw = localStorage.getItem("__tm__" + key); + return raw ? JSON.parse(raw) : defVal; + } catch (_) { + return defVal; + } + } + + function gmSet(key, val) { + try { + if (typeof GM_setValue === "function") return GM_setValue(key, val); + } catch (_) { } + try { + localStorage.setItem("__tm__" + key, JSON.stringify(val)); + } catch (_) { } + } + + function isVisible(el) { + if (!el) return false; + const rect = el.getBoundingClientRect(); + const style = getComputedStyle(el); + return ( + rect.width > 0 && + rect.height > 0 && + style.visibility !== "hidden" && + style.display !== "none" && + style.opacity !== "0" + ); + } + + function isClickable(el) { + if (!el || !isVisible(el)) return false; + const style = getComputedStyle(el); + const tag = (el.tagName || "").toLowerCase(); + if (tag === "button") return !el.disabled; + if (tag === "input" && /^(button|submit)$/i.test(el.type)) + return !el.disabled; + if (tag === "a" && (el.href || el.getAttribute("href"))) return true; + if (el.getAttribute && el.getAttribute("role") === "button") + return true; + if ( + el.classList && + (el.classList.contains("action-button") || + el.classList.contains("execute-button")) + ) { + return !el.disabled; + } + if ( + style.pointerEvents !== "none" && + (el.onclick || el.getAttribute("onclick")) + ) + return true; + const innerBtn = el.querySelector && el.querySelector("button"); + if (innerBtn && isVisible(innerBtn) && !innerBtn.disabled) return true; + return false; + } + + function findNearestClickableFrom(el, root = document.body) { + let cur = el; + const excludeSelector = "#automation-control-panel"; + const isInExcluded = (el2) => { + try { + return !!(el2 && el2.closest && el2.closest(excludeSelector)); + } catch (_) { + return false; + } + }; + if (isInExcluded(cur)) return null; + for ( + let depth = 0; + depth < 6 && cur && cur !== root && cur !== document.body; + depth++ + ) { + if (isClickable(cur)) return cur; + if (cur.querySelector) { + const found = cur.querySelector( + 'button, a[href], [role="button"], .action-button, .execute-button, .n-button, .n-button button' + ); + if (found && !isInExcluded(found) && isClickable(found)) + return found; + } + cur = cur.parentElement; + } + if (el.parentElement && !isInExcluded(el.parentElement)) { + const candidates = el.parentElement.querySelectorAll( + 'button, a[href], [role="button"], .action-button, .execute-button, .n-button, .n-button button' + ); + for (const c of candidates) { + if (!isInExcluded(c) && isClickable(c)) return c; + } + } + return null; + } + + function findClickableByText(pattern, root = document.body) { + const regex = + pattern instanceof RegExp ? pattern : new RegExp(pattern, "i"); + const excludeSelector = "#automation-control-panel"; + + const isInExcluded = (el) => { + try { + return !!(el && el.closest && el.closest(excludeSelector)); + } catch (_) { + return false; + } + }; + const isTextNodeVisible = (nodeEl) => { + try { + let el = nodeEl; + let hops = 0; + while (el && el !== document.body && hops++ < 20) { + const style = getComputedStyle(el); + if ( + style.display === "none" || + style.visibility === "hidden" || + style.opacity === "0" + ) + return false; + el = el.parentElement; + } + return true; + } catch (_) { + return true; + } + }; + + // 1) 按钮/可点击控件文本直配 + const clickableSelectors = + 'button, a[href], [role="button"], .action-button, .execute-button, .n-button, .n-button button'; + const direct = root.querySelectorAll(clickableSelectors); + for (const btn of direct) { + if (isInExcluded(btn)) continue; + const text = (btn.innerText || btn.textContent || "").trim(); + if (regex.test(text) && isClickable(btn)) return btn; + } + + // 2) 文本节点匹配 -> 邻近可点击控件 + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + const s = (node.nodeValue || "").trim(); + if (!s) return NodeFilter.FILTER_SKIP; + const parent = node.parentElement; + if (parent && isInExcluded(parent)) + return NodeFilter.FILTER_SKIP; + if (parent && !isTextNodeVisible(parent)) + return NodeFilter.FILTER_SKIP; + return regex.test(s) + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP; + }, + }); + let node; + while ((node = walker.nextNode())) { + const parent = node.parentElement; + if (isInExcluded(parent)) continue; + const candidate = findNearestClickableFrom(parent, root); + if (candidate) return candidate; + } + return null; + } + + async function clickByText(pattern, opts = {}) { + const { retries = 40, interval = 500, waitAfter = 600 } = opts; + for (let i = 0; i < retries; i++) { + if (!isRunning()) return false; + const btn = findClickableByText(pattern); + if (btn) { + try { + btn.scrollIntoView({ behavior: "smooth", block: "center" }); + await sleep(200 + Math.random() * 150); + const innerBtn = + btn.querySelector && btn.querySelector("button"); + (innerBtn || btn).click(); + await sleep(waitAfter); + return true; + } catch (e) { } + } + await sleep(interval); + } + return false; + } + + // 在限定超时时间内尝试点击文本按钮(默认5秒) + async function clickByTextWithTimeout( + pattern, + timeoutMs = 5000, + intervalMs = 250, + waitAfterMs = 600 + ) { + const retries = Math.max(1, Math.floor(timeoutMs / intervalMs)); + return clickByText(pattern, { + retries, + interval: intervalMs, + waitAfter: waitAfterMs, + }); + } + + function waitForText( + pattern, + { root = document.body, timeout = 120000 } = {} + ) { + const regex = + pattern instanceof RegExp ? pattern : new RegExp(pattern, "i"); + return new Promise((resolve, reject) => { + const initial = ( + root.innerText || + root.textContent || + "" + ).toString(); + if (regex.test(initial)) return resolve(true); + const obs = new MutationObserver(() => { + const txt = ( + root.innerText || + root.textContent || + "" + ).toString(); + if (regex.test(txt)) { + clearTimeout(timer); + obs.disconnect(); + resolve(true); + } + }); + obs.observe(root, { + childList: true, + subtree: true, + characterData: true, + }); + const timer = setTimeout(() => { + obs.disconnect(); + reject(new Error("等候文本超时: " + regex)); + }, timeout); + }); + } + + // 等待指定正则文本对应的可点击控件出现 + async function waitForClickableByText( + pattern, + { timeout = 120000, interval = 400 } = {} + ) { + const start = Date.now(); + while (Date.now() - start < timeout) { + if (!isRunning()) return false; + const btn = findClickableByText(pattern); + if (btn && isClickable(btn)) return true; + await sleep(interval); + } + return false; + } + + // 等待文本(排除指定容器中的文本,例如面板日志),默认排除自动化面板 + function waitForTextExcluding( + pattern, + { + root = document.body, + timeout = 120000, + exclude = ["#automation-control-panel"], + } = {} + ) { + const regex = + pattern instanceof RegExp ? pattern : new RegExp(pattern, "i"); + const excludeSelector = Array.isArray(exclude) + ? exclude.join(",") + : exclude || ""; + + const hasMatch = () => { + try { + const walker = document.createTreeWalker( + root, + NodeFilter.SHOW_TEXT, + { + acceptNode(node) { + const s = (node.nodeValue || "").trim(); + if (!s) return NodeFilter.FILTER_SKIP; + if ( + excludeSelector && + node.parentElement && + node.parentElement.closest && + node.parentElement.closest(excludeSelector) + ) { + return NodeFilter.FILTER_SKIP; + } + return regex.test(s) + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP; + }, + } + ); + return !!walker.nextNode(); + } catch (_) { + // 兜底:如果出错则不匹配 + return false; + } + }; + + return new Promise((resolve, reject) => { + if (hasMatch()) return resolve(true); + const obs = new MutationObserver(() => { + if (hasMatch()) { + clearTimeout(timer); + obs.disconnect(); + resolve(true); + } + }); + obs.observe(root, { + childList: true, + subtree: true, + characterData: true, + }); + const timer = setTimeout(() => { + obs.disconnect(); + reject(new Error("等候文本超时: " + regex)); + }, timeout); + }); + } + + async function waitForConnected({ timeout = 20000 } = {}) { + try { + await waitForText(/已连接/, { timeout }); + await sleep(800); + return true; + } catch { + return false; + } + } + + // -------------- 状态管理 -------------- + const STATE_KEY = "automationState"; + function getState() { + return gmGet(STATE_KEY, { + running: false, + total: 0, + current: 0, + }); + } + function setState(s) { + gmSet(STATE_KEY, s); + } + function clearState() { + setState({ running: false, total: 0, current: 0 }); + } + function resetQueueProgress() { + const st = getState(); + const next = { + ...st, + current: 0, + currentIndex: 0, + currentRoleName: null, + }; + setState(next); + try { + updatePanel(); + } catch (_) { } + notify("已重置队列进度,当前索引回到 0", "success"); + } + function isRunning() { + try { + const s = getState(); + return !!(s && s.running); + } catch (_) { + return false; + } + } + function updateState(patch) { + try { + const cur = getState() || {}; + const next = { ...cur, ...patch }; + setState(next); + return next; + } catch (_) { + setState(patch || {}); + return patch || {}; + } + } + + // -------------- 页面判断 -------------- + function isTokensPage() { + return location.pathname.replace(/\/+$/, "") === "/tokens"; + } + function isFeaturesPage() { + return location.pathname.replace(/\/+$/, "") === "/game-features"; + } + + // -------------- tokens 页:角色卡片 -------------- + function getTokenCards() { + return Array.from(document.querySelectorAll(".token-card")); + } + + function probeCurrentRoleNameFromDom() { + try { + const el = document.querySelector(".page-subtitle"); + const t = (el?.innerText || "").trim(); + if (t && !/未选择Token/.test(t)) return t; + } catch (_) { } + return null; + } + + function getCardName(card) { + try { + return ( + card.querySelector(".token-name")?.innerText || + card.innerText || + "" + ).trim(); + } catch (_) { + return ""; + } + } + + async function waitForActiveCardByName( + targetName, + { timeout = 8000 } = {} + ) { + const start = Date.now(); + while (Date.now() - start < timeout) { + if (!isRunning()) return false; + const active = document.querySelector( + ".token-card.active .token-name" + ); + const name = (active?.innerText || "").trim(); + if (name && targetName && name === targetName) return true; + await sleep(200); + } + return false; + } + async function waitForTokenCards({ timeout = 15000 } = {}) { + const start = Date.now(); + while (Date.now() - start < timeout) { + if (!isRunning()) return []; + const cards = getTokenCards(); + if (cards.length > 0) return cards; + await sleep(300); + } + return []; + } + + async function processNextOnTokensPage() { + const st = getState(); + if (!st.running) return; + + const cards = await waitForTokenCards(); + if (!isRunning()) return; + // 持久化总数到状态,便于面板回显 + if (cards && cards.length) updateState({ total: cards.length }); + if (!cards.length) { + notify("未找到任何角色卡片,停止。", "error"); + clearState(); + return; + } + if (st.current >= cards.length) { + notify("所有角色已处理完成,稍后将重新开始下一轮。", "success"); + const loopDelay = Math.max(0, getActionDelayMs()); + updateState({ + running: true, + total: cards.length, + current: 0, + currentIndex: 0, + currentRoleName: null, + }); + try { + updatePanel(); + } catch (_) { } + if (!isRunning()) return; + // 应用下次启动间隔延迟 + await waitForRestartInterval(); + if (!isRunning()) return; + if (loopDelay > 0) await sleep(loopDelay); + if (!isRunning()) return; + return processNextOnTokensPage(); + } + + const card = cards[st.current]; + try { + if (!isRunning()) return; + const targetName = getCardName(card); + updateState({ + currentRoleName: targetName, + currentIndex: st.current, + }); + card.scrollIntoView({ behavior: "smooth", block: "center" }); + await sleep(200); + if (!isRunning()) return; + card.click(); + await sleep(600); + // 等待该卡片成为 active,确保已选中正确角色 + const ok = await waitForActiveCardByName(targetName, { + timeout: 8000, + }); + if (!ok) { + // 再尝试一次点击 + notify( + `未检测到已选中目标卡片「${targetName}」,重试点击一次...`, + "warning" + ); + card.click(); + await sleep(800); + await waitForActiveCardByName(targetName, { timeout: 5000 }); + } + } catch (e) { + notify( + `点击第 ${st.current + 1} 个角色失败,尝试继续。`, + "warning" + ); + } + + if (!isRunning()) return; + notify( + `前往任务页处理:第 ${st.current + 1}/${cards.length} 个角色`, + "info" + ); + await sleep(500); + if (isRunning()) location.assign(`${location.origin}/game-features`); + } + + // -------------- 任务页:执行流程 -------------- + async function runTaskFlowOnFeaturesPage() { + const st = getState(); + if (!st.running) return; + + await waitForConnected({ timeout: 20000 }); + if (!isRunning()) return; + + const tasks = getTasksConfig(); + + // 初始化本角色任务进度 + initTaskProgressFromConfig(); + + // 尝试从页面头部回显当前角色名称 + if (!st.currentRoleName) { + const n = probeCurrentRoleNameFromDom(); + if (n) updateState({ currentRoleName: n }); + } + + // 1. 一键补差(点击后等待日志:最终角色信息刷新完成;排除面板日志) + if (tasks.dailyFix) { + if (!isRunning()) return; + notify("开始:一键补差", "info"); + const fixClicked = await clickByText(/一键补差/); + if (fixClicked) { + // 点击后一秒再开始任务“停止”检查与按钮恢复检查 + await sleep(1000); + if (!isRunning()) return; + let ok = false; + try { + await waitForTextExcluding(/最终角色信息刷新完成/, { + timeout: 18000, + exclude: ["#automation-control-panel"], + }); + ok = true; + } catch (e) { + ok = false; + } + if (ok) { + notify( + "一键补差完成(检测到“最终角色信息刷新完成”)", + "success" + ); + setTaskProgress({ dailyFix: "ok" }); + } else { + notify( + "等待“最终角色信息刷新完成”超时,继续后续步骤。", + "warning" + ); + // 已经点击但未检测到完成,也视为已尝试成功 + setTaskProgress({ dailyFix: "ok" }); + } + } else { + notify("未找到“一键补差”按钮,继续后续步骤。", "warning"); + setTaskProgress({ dailyFix: "fail" }); + } + await waitBetweenSteps(); + if (!isRunning()) return; + } else { + setTaskProgress({ dailyFix: "skip" }); + } + + // 2. 一键答题 + if (tasks.answer) { + if (!isRunning()) return; + notify("尝试:一键答题", "info"); + const ok = await clickByTextWithTimeout(/一键答题/, 2000, 250, 300); + setTaskProgress({ answer: ok ? "ok" : "fail" }); + await waitBetweenSteps(); + if (!isRunning()) return; + } else { + setTaskProgress({ answer: "skip" }); + } + + // 3. 领取奖励 + if (tasks.claimReward) { + if (!isRunning()) return; + notify("尝试:领取奖励", "info"); + const ok = await clickByTextWithTimeout(/领取奖励/, 2000, 250, 300); + setTaskProgress({ claimReward: ok ? "ok" : "fail" }); + await waitBetweenSteps(); + if (!isRunning()) return; + } else { + setTaskProgress({ claimReward: "skip" }); + } + + // 4. 加钟 + if (tasks.addClock) { + if (!isRunning()) return; + notify("尝试:加钟", "info"); + const ok = await clickByTextWithTimeout(/加钟/, 2000, 250, 300); + setTaskProgress({ addClock: ok ? "ok" : "fail" }); + await waitBetweenSteps(); + if (!isRunning()) return; + } else { + setTaskProgress({ addClock: "skip" }); + } + + // 5. 立即签到 + if (tasks.signIn) { + if (!isRunning()) return; + // 检查是否已签到 + const alreadySignedIn = findClickableByText(/已签到/); + if (alreadySignedIn) { + notify("检测到已签到,跳过立即签到任务", "info"); + setTaskProgress({ signIn: "skip" }); + } else { + notify("尝试:立即签到", "info"); + const ok = await clickByTextWithTimeout(/立即签到/, 2000, 250, 300); + setTaskProgress({ signIn: ok ? "ok" : "fail" }); + } + await waitBetweenSteps(); + if (!isRunning()) return; + } else { + setTaskProgress({ signIn: "skip" }); + } + + // 6. 启动/重启服务 + if (tasks.startService) { + if (!isRunning()) return; + notify("尝试:启动/重启服务", "info"); + const ok = await clickByTextWithTimeout( + /启动服务|重启服务/, + 2000, + 250, + 300 + ); + setTaskProgress({ startService: ok ? "ok" : "fail" }); + await waitBetweenSteps(); + if (!isRunning()) return; + } else { + setTaskProgress({ startService: "skip" }); + } + + // 末尾再等待一次,确保后续状态稳定 + await waitBetweenSteps(); + if (!isRunning()) return; + + const nextState = updateState({ current: st.current + 1 }); + + notify( + `当前角色处理完成,返回 Token 管理页(下一位:${(nextState.current ?? st.current) + 1 + }/${nextState.total ?? "-"})`, + "info" + ); + // 根据需求:跳转到 tokens 页前固定等待 2 秒 + if (!isRunning()) return; + await sleep(PAGE_READY_DELAY_MS); + if (!isRunning()) return; + location.assign(`${location.origin}/tokens`); + } + + // -------------- 启停与菜单 -------------- + function startAllRoles() { + const cards = getTokenCards(); + const total = cards.length; + if (!isTokensPage() || total === 0) { + notify("请在 Token 管理页启动,且确保已有角色!", "warning"); + return; + } + const prev = getState() || {}; + let current = Number.isFinite(prev.current) ? prev.current : 0; + // 边界保护:若上次索引超出范围,则从 0 开始 + if (total <= 0) current = 0; + else if (current >= total) current = 0; + updateState({ + running: true, + total, + current, + currentRoleName: null, + currentIndex: current, + }); + notify( + `开始循环自动做任务,共 ${total} 个角色(从第 ${current + 1 + } 个继续)`, + "info" + ); + processNextOnTokensPage(); + } + + function stopAll() { + // 仅停止运行,保留队列与任务进度 + updateState({ running: false }); + notify("已停止(保留队列进度)。", "info"); + } + + // 暂停(与停止一致:不清空进度,仅置为非运行态) + function pauseRun() { + updateState({ running: false }); + notify("已暂停(保留队列进度)。", "info"); + try { + updatePanel(); + } catch (_) { } + } + + // 恢复(从上次队列索引继续) + function resumeRun() { + updateState({ running: true }); + notify("已恢复(从上次进度继续)。", "success"); + try { + updatePanel(); + } catch (_) { } + if (isTokensPage()) { + startAllRoles(); + } else { + location.assign(`${location.origin}/tokens`); + } + } + + try { + GM_registerMenuCommand("开始:循环所有角色自动做任务", startAllRoles); + GM_registerMenuCommand("停止:暂停运行", stopAll); + GM_registerMenuCommand("设置日志最大行数", () => { + const cur = getLogLimit(); + const input = prompt("输入日志最大行数(20~2000)", String(cur)); + if (input == null) return; + if (setLogLimit(input)) { + notify("日志最大行数已更新为 " + getLogLimit(), "success"); + } else { + notify("输入无效,未更新日志行数", "warning"); + } + }); + GM_registerMenuCommand("清空运行日志", () => { + clearLogs(); + renderLogsToPanel(); + notify("已清空运行日志", "success"); + }); + GM_registerMenuCommand("设置动作等待时间(ms)", () => { + const cur = getActionDelayMs(); + const input = prompt( + "输入每步动作之间的等待时间(毫秒,0~600000)", + String(cur) + ); + if (input == null) return; + if (setActionDelayMs(input)) { + notify( + "动作等待时间已更新为 " + getActionDelayMs() + "ms", + "success" + ); + updatePanel(); + } else { + notify("输入无效,未更新动作等待时间", "warning"); + } + }); + GM_registerMenuCommand("设置下次启动间隔(分钟)", () => { + const cur = getRestartIntervalMinutes(); + const input = prompt( + "输入下次启动间隔时间(分钟,0~1440)", + String(cur) + ); + if (input == null) return; + if (setRestartIntervalMinutes(input)) { + notify( + "下次启动间隔已更新为 " + getRestartIntervalMinutes() + " 分钟", + "success" + ); + updatePanel(); + } else { + notify("输入无效,未更新下次启动间隔", "warning"); + } + }); + GM_registerMenuCommand("配置管理", openConfigManager); + } catch (_) { } + + // -------------- 管理面板(可折叠,10px 字体) -------------- + const PANEL_ID = "automation-control-panel"; + const PANEL_COLLAPSE_KEY = "automation_panel_collapsed"; + + function ensurePanelStyles() { + const css = ` + #${PANEL_ID} { position: fixed; right: 12px; bottom: 12px; z-index: 999999; font-size: 10px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial; color: #111; } + #${PANEL_ID} .panel-wrap { background: rgba(255,255,255,.98); border: 1px solid #e5e7eb; box-shadow: 0 6px 18px rgba(0,0,0,.12); border-radius: 8px; overflow: hidden; min-width: 200px; } + #${PANEL_ID} .panel-header { display:flex; align-items:center; justify-content:space-between; gap:6px; padding:6px 8px; background:#f3f4f6; border-bottom:1px solid #e5e7eb; } + #${PANEL_ID} .panel-title { font-weight:600; } + #${PANEL_ID} .panel-actions { display:flex; align-items:center; gap:6px; } + #${PANEL_ID} button { font-size:10px; line-height:1; padding:4px 6px; border-radius:4px; border:1px solid #d1d5db; background:#fff; cursor:pointer; } + #${PANEL_ID} button:hover { background:#f9fafb; } + #${PANEL_ID} .collapse-btn { color:#374151; } + #${PANEL_ID} .panel-body { padding:8px; display:grid; gap:6px; } + #${PANEL_ID} .row { display:flex; align-items:center; justify-content:space-between; gap:8px; } + #${PANEL_ID} .label { color:#6b7280; } + #${PANEL_ID} .value { color:#111827; font-weight:600; } + #${PANEL_ID} .tasks-config { display:grid; grid-template-columns: 1fr 1fr; gap:4px 8px; padding:6px; border:1px dashed #d1d5db; border-radius:6px; background:#fff; } + #${PANEL_ID} .tasks-config label { display:flex; align-items:center; gap:6px; font-weight:500; } + #${PANEL_ID} .tasks-actions { grid-column: 1 / -1; display:flex; gap:6px; justify-content:flex-end; margin-top:4px; } + #${PANEL_ID} .config-manager { display:grid; gap:6px; padding:6px; border:1px dashed #d1d5db; border-radius:6px; background:#fff; } + #${PANEL_ID} .config-list { display:flex; flex-direction:column; gap:4px; max-height:180px; overflow:auto; } + #${PANEL_ID} .config-item { border:1px solid #e5e7eb; border-radius:6px; padding:6px; background:#fff; display:flex; flex-direction:column; gap:4px; } + #${PANEL_ID} .config-item.active { border-color:#2563eb; box-shadow:0 0 0 1px rgba(37,99,235,.35); } + #${PANEL_ID} .config-item-header { display:flex; align-items:center; justify-content:space-between; gap:6px; font-weight:600; } + #${PANEL_ID} .config-item-meta { font-size:9px; color:#6b7280; } + #${PANEL_ID} .config-item-buttons { display:flex; gap:4px; flex-wrap:wrap; } + #${PANEL_ID} .config-item-buttons button { flex:0 0 auto; } + #${PANEL_ID} .config-actions { display:flex; gap:6px; justify-content:flex-end; flex-wrap:wrap; } + #${PANEL_ID} .config-badge { font-size:9px; color:#2563eb; background:rgba(37,99,235,.12); padding:2px 4px; border-radius:4px; } + #${PANEL_ID} .config-empty { font-size:10px; color:#6b7280; text-align:center; padding:6px 0; } + #${PANEL_ID} .log-box { max-height: 200px; overflow:auto; background:#f9fafb; border:1px solid #e5e7eb; border-radius:6px; padding:6px; font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace; line-height:1.3; } + #${PANEL_ID} .log-item { margin:1px 0; white-space: pre-wrap; word-break: break-word; } + #${PANEL_ID} .log-item.info { color:#111827; } + #${PANEL_ID} .log-item.warn, #${PANEL_ID} .log-item.warning { color:#b45309; } + #${PANEL_ID} .log-item.error { color:#b91c1c; } + #${PANEL_ID} .log-item.success, #${PANEL_ID} .log-item.ok { color:#065f46; } + @media (prefers-color-scheme: dark) { + #${PANEL_ID} .panel-wrap { background: rgba(31,41,55,.98); border-color:#374151; color:#e5e7eb; } + #${PANEL_ID} .panel-header { background:#111827; border-color:#374151; } + #${PANEL_ID} .label { color:#9ca3af; } + #${PANEL_ID} .value { color:#e5e7eb; } + #${PANEL_ID} button { background:#111827; color:#e5e7eb; border-color:#374151; } + #${PANEL_ID} button:hover { background:#1f2937; } + #${PANEL_ID} .tasks-config { background:#0b1220; border-color:#374151; } + #${PANEL_ID} .config-manager { background:#0b1220; border-color:#374151; } + #${PANEL_ID} .config-item { background:#111827; border-color:#374151; } + #${PANEL_ID} .config-item.active { border-color:#3b82f6; box-shadow:0 0 0 1px rgba(59,130,246,.35); } + #${PANEL_ID} .config-item-meta { color:#9ca3af; } + #${PANEL_ID} .config-empty { color:#9ca3af; } + #${PANEL_ID} .config-badge { color:#bfdbfe; background:rgba(59,130,246,.25); } + #${PANEL_ID} .log-box { background:#111827; border-color:#374151; } + } + `; + try { + if (typeof GM_addStyle === "function") GM_addStyle(css); + } catch (_) { + const style = document.createElement("style"); + style.textContent = css; + document.head.appendChild(style); + } + } + + function getCollapsed() { + return !!gmGet(PANEL_COLLAPSE_KEY, false); + } + function setCollapsed(v) { + gmSet(PANEL_COLLAPSE_KEY, !!v); + } + + function currentPageTag() { + if (isTokensPage()) return "tokens"; + if (isFeaturesPage()) return "game-features"; + return location.pathname.replace(/\/+$/, "") || "/"; + } + + function renderPanel() { + ensurePanelStyles(); + let root = document.getElementById(PANEL_ID); + if (!root) { + root = document.createElement("div"); + root.id = PANEL_ID; + root.innerHTML = ` +
+
+
自动化控制面板
+
+ +
+
+
+
状态-
+
配置-
+
队列0/0
+
页面-
+
当前角色-
+
任务进度-
+
动作等待(ms)-
+
下次启动间隔(分钟)-
+
日志行数-
+
任务选择 + - +
+ + +
+ + + +
+
+ + + + + + +
+
+ + +
+
+
+
`; + document.body.appendChild(root); + + // 事件绑定 + const startBtn = root.querySelector('button[data-action="start"]'); + const stopBtn = root.querySelector('button[data-action="stop"]'); + const pauseBtn = root.querySelector('button[data-action="pause"]'); + const collapseBtn = root.querySelector(".collapse-btn"); + const setLimitBtn = root.querySelector( + 'button[data-action="set-limit"]' + ); + const setDelayBtn = root.querySelector( + 'button[data-action="set-delay"]' + ); + const setRestartIntervalBtn = root.querySelector( + 'button[data-action="set-restart-interval"]' + ); + const clearLogsBtn = root.querySelector( + 'button[data-action="clear-logs"]' + ); + const openTasksBtn = root.querySelector( + 'button[data-action="open-tasks"]' + ); + const openConfigBtn = root.querySelector( + 'button[data-action="open-config"]' + ); + const configCreateBtn = root.querySelector( + 'button[data-action="config-create"]' + ); + const configCloseBtn = root.querySelector( + 'button[data-action="config-close"]' + ); + const configListEl = root.querySelector('[data-field="config-list"]'); + const resetQueueBtn = root.querySelector( + 'button[data-action="reset-queue"]' + ); + const clearTaskProgBtn = root.querySelector( + 'button[data-action="clear-task-progress"]' + ); + const tasksBox = root.querySelector('[data-field="tasks"]'); + const tasksAllBtn = root.querySelector( + 'button[data-action="tasks-all"]' + ); + const tasksNoneBtn = root.querySelector( + 'button[data-action="tasks-none"]' + ); + const tasksToggleBtn = root.querySelector( + 'button[data-action="tasks-toggle"]' + ); + startBtn.addEventListener("click", () => { + // 允许任意页面启动:若不在 tokens 页则先设置状态后跳转 + if (!isTokensPage()) { + const cardsHint = "(将跳转到 tokens 页)"; + // 保持上次队列进度,仅设置为运行中 + updateState({ running: true }); + notify("已开始 " + cardsHint, "info"); + location.assign(`${location.origin}/tokens`); + } else { + startAllRoles(); + } + }); + stopBtn.addEventListener("click", () => stopAll()); + pauseBtn.addEventListener("click", () => { + const s = getState(); + if (s && s.running) { + pauseRun(); + } else { + resumeRun(); + } + }); + collapseBtn.addEventListener("click", toggleCollapse); + setDelayBtn.addEventListener("click", () => { + const cur = getActionDelayMs(); + const input = prompt( + "输入每步动作之间的等待时间(毫秒,0~600000)", + String(cur) + ); + if (input == null) return; + if (setActionDelayMs(input)) { + notify( + "动作等待时间已更新为 " + getActionDelayMs() + "ms", + "success" + ); + updatePanel(); + } else { + notify("输入无效,未更新动作等待时间", "warning"); + } + }); + setRestartIntervalBtn.addEventListener("click", () => { + const cur = getRestartIntervalMinutes(); + const input = prompt( + "输入下次启动间隔时间(分钟,0~1440)", + String(cur) + ); + if (input == null) return; + if (setRestartIntervalMinutes(input)) { + notify( + "下次启动间隔已更新为 " + getRestartIntervalMinutes() + " 分钟", + "success" + ); + updatePanel(); + } else { + notify("输入无效,未更新下次启动间隔", "warning"); + } + }); + setLimitBtn.addEventListener("click", () => { + const cur = getLogLimit(); + const input = prompt( + "输入日志最大行数(20~2000)", + String(cur) + ); + if (input == null) return; + if (setLogLimit(input)) { + notify("日志最大行数已更新为 " + getLogLimit(), "success"); + updatePanel(); + } else { + notify("输入无效,未更新日志行数", "warning"); + } + }); + clearLogsBtn.addEventListener("click", () => { + clearLogs(); + renderLogsToPanel(); + notify("已清空运行日志", "success"); + }); + resetQueueBtn.addEventListener("click", () => { + resetQueueProgress(); + }); + clearTaskProgBtn.addEventListener("click", () => { + clearTaskProgress(); + notify("已清空任务进度", "success"); + }); + + // 任务选择 UI + openTasksBtn.addEventListener("click", () => { + const open = tasksBox.style.display !== "none"; + const nextOpen = !open; + tasksBox.style.display = nextOpen ? "grid" : "none"; + setTasksUIOpen(nextOpen); + // 初始化勾选状态 + if (nextOpen) syncTasksUI(); + }); + if (openConfigBtn && !openConfigBtn.dataset.listenerAttached) { + openConfigBtn.dataset.listenerAttached = "true"; + openConfigBtn.addEventListener("click", () => { + toggleConfigManagerPanel(); + }); + } + if (configCreateBtn && !configCreateBtn.dataset.listenerAttached) { + configCreateBtn.dataset.listenerAttached = "true"; + configCreateBtn.addEventListener("click", () => { + handleConfigCreate(); + }); + } + if (configCloseBtn && !configCloseBtn.dataset.listenerAttached) { + configCloseBtn.dataset.listenerAttached = "true"; + configCloseBtn.addEventListener("click", () => { + setConfigManagerOpen(false); + updatePanel(); + }); + } + if (configListEl && !configListEl.dataset.listenerAttached) { + configListEl.dataset.listenerAttached = "true"; + configListEl.addEventListener("click", (event) => { + const targetButton = event.target.closest('button[data-config-action]'); + if (targetButton) { + event.preventDefault(); + event.stopPropagation(); + const action = targetButton.dataset.configAction; + const profileName = targetButton.dataset.profileName; + if (!profileName) return; + if (action === "use") { + handleConfigUse(profileName); + } else if (action === "rename") { + handleConfigRename(profileName); + } else if (action === "delete") { + handleConfigDelete(profileName); + } + return; + } + const item = event.target.closest('[data-profile-name]'); + if (item) { + const profileName = item.dataset.profileName; + if (profileName) { + handleConfigUse(profileName); + } + } + }); + } + tasksAllBtn.addEventListener("click", () => { + const cfg = setTasksConfig({ + dailyFix: true, + answer: true, + claimReward: true, + addClock: true, + signIn: true, + startService: true, + }); + syncTasksUI(cfg); + updatePanel(); + notify("已全选所有任务", "success"); + }); + tasksNoneBtn.addEventListener("click", () => { + const cfg = setTasksConfig({ + dailyFix: false, + answer: false, + claimReward: false, + addClock: false, + signIn: false, + startService: false, + }); + syncTasksUI(cfg); + updatePanel(); + notify("已取消所有任务", "success"); + }); + tasksToggleBtn.addEventListener("click", () => { + const open = tasksBox.style.display !== "none"; + const nextOpen = !open; + tasksBox.style.display = nextOpen ? "grid" : "none"; + setTasksUIOpen(nextOpen); + }); + + // 复选框勾选事件(即改即存) + tasksBox + .querySelectorAll('input[type="checkbox"]') + .forEach((cb) => { + cb.addEventListener("change", () => { + const cfg = getTasksConfig(); + const key = cb.getAttribute("data-task"); + cfg[key] = cb.checked; + setTasksConfig(cfg); + updatePanel(); + notify("任务选择已更新", "info"); + }); + }); + } + + // 折叠状态 + applyCollapse(); + updatePanel(); + } + + function applyCollapse() { + const root = document.getElementById(PANEL_ID); + if (!root) return; + const body = root.querySelector(".panel-body"); + const btn = root.querySelector(".collapse-btn"); + const collapsed = getCollapsed(); + if (collapsed) { + body.style.display = "none"; + btn.textContent = "展开"; + } else { + body.style.display = "grid"; + btn.textContent = "折叠"; + } + } + + function toggleCollapse() { + setCollapsed(!getCollapsed()); + applyCollapse(); + } + + function updatePanel() { + const root = document.getElementById(PANEL_ID); + if (!root) return; + // 强制按照持久化的折叠状态渲染,避免被外部样式或渲染干扰导致“自动折叠” + applyCollapse(); + const st = getState(); + const tasksCfg = getTasksConfig(); + const statusEl = root.querySelector('[data-field="status"]'); + const profileEl = root.querySelector('[data-field="profile"]'); + const queueEl = root.querySelector('[data-field="queue"]'); + const pageEl = root.querySelector('[data-field="page"]'); + const roleEl = root.querySelector('[data-field="role"]'); + const taskProgEl = root.querySelector('[data-field="task-progress"]'); + const delayEl = root.querySelector('[data-field="delay"]'); + const restartIntervalEl = root.querySelector('[data-field="restart-interval"]'); + const limitEl = root.querySelector('[data-field="limit"]'); + const tasksSummaryEl = root.querySelector( + '[data-field="tasks-summary"]' + ); + const tasksBox = root.querySelector('[data-field="tasks"]'); + + const statusText = st.running ? "运行中" : "空闲"; + statusEl.textContent = statusText; + if (profileEl) profileEl.textContent = getActiveProfileName(); + queueEl.textContent = `${Math.min(st.current, st.total)}/${st.total}`; + pageEl.textContent = currentPageTag(); + if (roleEl) + roleEl.textContent = + (st.currentRoleName || "-") + + (Number.isFinite(st.currentIndex) + ? ` (#${st.currentIndex + 1})` + : ""); + if (taskProgEl) { + const { text, okCnt, totalCnt } = summarizeTaskProgress(); + taskProgEl.textContent = + totalCnt > 0 ? `${text}(${okCnt}/${totalCnt})` : text; + } + if (delayEl) delayEl.textContent = String(getActionDelayMs()); + if (restartIntervalEl) restartIntervalEl.textContent = String(getRestartIntervalMinutes()); + if (limitEl) limitEl.textContent = String(getLogLimit()); + if (tasksSummaryEl) { + const names = []; + if (tasksCfg.dailyFix) names.push("补差"); + if (tasksCfg.answer) names.push("答题"); + if (tasksCfg.claimReward) names.push("领奖"); + if (tasksCfg.addClock) names.push("加钟"); + if (tasksCfg.signIn) names.push("签到"); + if (tasksCfg.startService) names.push("启动"); + tasksSummaryEl.textContent = names.length + ? names.join("、") + : "未选择"; + } + // 同步 UI 勾选与展开状态 + if (tasksBox) { + tasksBox.style.display = isTasksUIOpen() ? "grid" : "none"; + syncTasksUI(tasksCfg); + } + const configBox = root.querySelector('[data-field="configs"]'); + const configListEl = root.querySelector('[data-field="config-list"]'); + const openConfigBtn = root.querySelector('button[data-action="open-config"]'); + if (configBox) { + configBox.style.display = isConfigManagerOpen() ? "grid" : "none"; + if (isConfigManagerOpen() && configListEl) { + renderConfigList(configListEl); + } + } + if (openConfigBtn) { + openConfigBtn.textContent = isConfigManagerOpen() ? "收起配置" : "配置管理"; + } + // 更新暂停按钮的文案:运行中->“暂停”,非运行->“恢复” + const pauseBtn = root.querySelector('button[data-action="pause"]'); + if (pauseBtn) { + pauseBtn.textContent = st.running ? "暂停" : "恢复"; + } + renderLogsToPanel(); + } + + function syncTasksUI(cfg) { + const root = document.getElementById(PANEL_ID); + if (!root) return; + const tasks = cfg || getTasksConfig(); + root.querySelectorAll( + '[data-field="tasks"] input[type="checkbox"]' + ).forEach((cb) => { + const key = cb.getAttribute("data-task"); + cb.checked = !!tasks[key]; + }); + } + + // 初始化面板与定时刷新 + function initPanel() { + renderPanel(); + // 定时刷新显示数据 + setInterval(updatePanel, 1000); + } + + // 渲染日志到面板 + function renderLogsToPanel() { + const root = document.getElementById(PANEL_ID); + if (!root) return; + const box = root.querySelector('[data-field="logs"]'); + if (!box) return; + const logs = getLogs(); + const esc = (s) => + String(s) + .replace(/&/g, "&") + .replace(//g, ">"); + const typeLabel = (t) => + ({ + info: "INFO", + warning: "WARN", + warn: "WARN", + error: "ERROR", + success: "OK", + }[t] || String(t || "").toUpperCase()); + const html = logs + .map( + (l) => + `
${esc(l.time)} [${esc( + typeLabel(l.type) + )}] ${esc(l.message)}
` + ) + .join(""); + if (box.innerHTML !== html) { + const atBottom = + box.scrollTop + box.clientHeight >= box.scrollHeight - 4; + box.innerHTML = html || '
(无日志)
'; + if (atBottom) { + box.scrollTop = box.scrollHeight; + } + } + } + + // -------------- 页面入口 -------------- + (async function main() { + initPanel(); + await sleep(PAGE_READY_DELAY_MS); + if (isTokensPage()) { + if (getState().running) { + await processNextOnTokensPage(); + } + } + if (isFeaturesPage()) { + if (getState().running) { + await runTaskFlowOnFeaturesPage(); + } + } + })(); +})(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..fdb2e77 --- /dev/null +++ b/index.html @@ -0,0 +1,42 @@ + + + + + + XYZW 游戏管理系统 + + + + + +
+
正在加载应用...
+
+ + + \ No newline at end of file