Files
YuerKIng/auto.js

2058 lines
76 KiB
JavaScript
Raw Permalink Normal View History

2025-10-17 21:29:39 +08:00
// ==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 = `
<div class="panel-wrap">
<div class="panel-header">
<div class="panel-title">自动化控制面板</div>
<div class="panel-actions">
<button type="button" class="collapse-btn">折叠</button>
</div>
</div>
<div class="panel-body">
<div class="row"><span class="label">状态</span><span class="value" data-field="status">-</span></div>
<div class="row"><span class="label">配置</span><span class="value" data-field="profile">-</span></div>
<div class="row"><span class="label">队列</span><span class="value" data-field="queue">0/0</span></div>
<div class="row"><span class="label">页面</span><span class="value" data-field="page">-</span></div>
<div class="row"><span class="label">当前角色</span><span class="value" data-field="role">-</span></div>
<div class="row"><span class="label">任务进度</span><span class="value" data-field="task-progress">-</span></div>
<div class="row"><span class="label">动作等待(ms)</span><span class="value" data-field="delay">-</span></div>
<div class="row"><span class="label">下次启动间隔(分钟)</span><span class="value" data-field="restart-interval">-</span></div>
<div class="row"><span class="label">日志行数</span><span class="value" data-field="limit">-</span></div>
<div class="row"><span class="label">任务选择</span>
<span class="value" data-field="tasks-summary">-</span>
</div>
<div class="tasks-config" data-field="tasks" style="display:none;">
<label><input type="checkbox" data-task="dailyFix"> 一键补差</label>
<label><input type="checkbox" data-task="answer"> 一键答题</label>
<label><input type="checkbox" data-task="claimReward"> 领取奖励</label>
<label><input type="checkbox" data-task="addClock"> 加钟</label>
<label><input type="checkbox" data-task="signIn"> 立即签到</label>
<label><input type="checkbox" data-task="startService"> 启动/重启服务</label>
<div class="tasks-actions">
<button type="button" data-action="tasks-all">全选</button>
<button type="button" data-action="tasks-none">全不选</button>
<button type="button" data-action="tasks-toggle">收起</button>
</div>
</div>
<div class="config-manager" data-field="configs" style="display:none;">
<div class="config-list" data-field="config-list"></div>
<div class="config-actions">
<button type="button" data-action="config-create">新建配置</button>
<button type="button" data-action="config-close">收起</button>
</div>
</div>
<div class="row" style="gap:6px; justify-content:flex-end;">
<button type="button" data-action="start">开始</button>
<button type="button" data-action="pause">暂停</button>
<button type="button" data-action="stop">停止</button>
</div>
<div class="row" style="gap:6px;">
<button type="button" data-action="set-delay">设置等待</button>
<button type="button" data-action="set-restart-interval">设置间隔</button>
<button type="button" data-action="set-limit">设置行数</button>
<button type="button" data-action="clear-logs">清空日志</button>
<button type="button" data-action="open-tasks">设置任务</button>
<button type="button" data-action="open-config">配置管理</button>
</div>
<div class="row" style="gap:6px;">
<button type="button" data-action="reset-queue">清除队列进度</button>
<button type="button" data-action="clear-task-progress">清除任务进度</button>
</div>
<div class="log-box" data-field="logs"></div>
</div>
</div>`;
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const typeLabel = (t) =>
({
info: "INFO",
warning: "WARN",
warn: "WARN",
error: "ERROR",
success: "OK",
}[t] || String(t || "").toUpperCase());
const html = logs
.map(
(l) =>
`<div class="log-item ${esc(l.type)}">${esc(l.time)} [${esc(
typeLabel(l.type)
)}] ${esc(l.message)}</div>`
)
.join("");
if (box.innerHTML !== html) {
const atBottom =
box.scrollTop + box.clientHeight >= box.scrollHeight - 4;
box.innerHTML = html || '<div class="log-item">(无日志)</div>';
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();
}
}
})();
})();