2058 lines
76 KiB
JavaScript
2058 lines
76 KiB
JavaScript
|
|
// ==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, "&")
|
|||
|
|
.replace(/</g, "<")
|
|||
|
|
.replace(/>/g, ">");
|
|||
|
|
const typeLabel = (t) =>
|
|||
|
|
({
|
|||
|
|
info: "INFO",
|
|||
|
|
warning: "WARN",
|
|||
|
|
warn: "WARN",
|
|||
|
|
error: "ERROR",
|
|||
|
|
success: "OK",
|
|||
|
|
}[t] || String(t || "").toUpperCase());
|
|||
|
|
const html = logs
|
|||
|
|
.map(
|
|||
|
|
(l) =>
|
|||
|
|
`<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();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})();
|
|||
|
|
})();
|