Files
YuerKIng/auto.js
2025-10-17 21:29:39 +08:00

2058 lines
76 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==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();
}
}
})();
})();