From bf4377a5df448bb8f072cf3c518bde5f37e64d5d Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 20 May 2026 17:32:48 +0800 Subject: [PATCH] feat(qdreader): store task switches per account --- docs/autman-plugin-rules.md | 12 ++++ js/qdreader_sign_autman.js | 79 ++++++++++++++++-------- plugins/qdreader/README.md | 43 +++++++------ plugins/qdreader/qdreader_sign_autman.js | 79 ++++++++++++++++-------- tests/qdreader_plugin.test.mjs | 43 ++++++++++--- 5 files changed, 176 insertions(+), 80 deletions(-) diff --git a/docs/autman-plugin-rules.md b/docs/autman-plugin-rules.md index 9c775c0..f1061da 100644 --- a/docs/autman-plugin-rules.md +++ b/docs/autman-plugin-rules.md @@ -624,3 +624,15 @@ QDReader 这次踩坑说明:账号 ID 识别成功,不代表 Autman 会话 - 高风险的导出/批量导入功能默认不做,除非 BOSS 明确需要。 - Node VM 测试应模拟:`sender.reply`、`sender.listen`、`sender.getUserID`、`get/set`、`bucketGet/bucketSet`、`request`。 - 测试必须覆盖:快速提交、菜单添加、备注提示、详情改备注、编号选择删除/查询、旧 bucket 兼容、新 bucket 写入、用户隔离、手动执行只跑当前用户、cron 跑全部账号。 + + +## 12. QDReader 账号级任务开关约定 + +启点/QDReader 这类多账号签到插件的附加任务(广告、抽奖、兑换、章节卡、大咖荐书等)不要再放全局开关里统一控制。BOSS 确认更合适的模型是“直接写进账号里,对每个账号单独管理”。仓库实现约定如下: + +- Cookie、用户绑定、备注仍分别保存;附加任务开关保存为 `qdreader_task_pref_json`。 +- `qdreader_task_pref_json` 结构为 `uid -> { taskKey: boolean }`,例如 `{ "12345": { "adv": true } }`。 +- `启点账号` 菜单内提供“账号任务配置”,先选择账号,再切换该账号的任务开关。 +- 定时/手动签到执行时按 uid 读取自己的任务配置;一个账号开启广告任务不影响其它账号。 +- 删除单个账号或清空当前用户账号时,同步删除该 uid 的任务配置,避免残留配置误作用于重新添加的账号。 +- 旧全局开关 key 可留作历史数据,但不要作为运行依据;测试必须覆盖“一个账号开启,另一个账号不执行”的隔离场景。 diff --git a/js/qdreader_sign_autman.js b/js/qdreader_sign_autman.js index add3cd9..d054473 100644 --- a/js/qdreader_sign_autman.js +++ b/js/qdreader_sign_autman.js @@ -4,7 +4,7 @@ //[author: Hermes] //[service: BOSS] //[class: 工具类] -//[version: 2.6.0] +//[version: 2.7.0] //[platform: web,qq,wx,tg,tb,fs,we] //[public: false] //[price: 0] @@ -24,6 +24,7 @@ var REMARK_KEY = "qdreader_remark_json" var SIGN_API_KEY = "qdreader_sign_api" var RETRY_KEY = "qdreader_retry_count" var CRON_SILENT_KEY = "qdreader_cron_silent" +var TASK_PREF_KEY = "qdreader_task_pref_json" var ADV_JOB_ENABLE_KEY = "qdreader_adv_job_enable" var EXTRA_ADV_JOB_ENABLE_KEY = "qdreader_extra_adv_job_enable" var LOTTERY_ENABLE_KEY = "qdreader_lottery_enable" @@ -275,7 +276,7 @@ function contentText() { } function usage() { return [ - "启点读书签到插件 v2.6.0", + "启点读书签到插件 v2.7.0", "【一级菜单】", "账号管理:启点账号", "执行查询:启点查询", @@ -283,10 +284,10 @@ function usage() { "快速提交CK:启点ck ", "直接签到:启点签到", "指定签到:启点签到 ", - "任务配置:启点任务", + "账号任务:启点账号 → 账号详情/任务配置", "【配置】", "签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API, - "完全体任务:领取奖励默认执行;激励/每日/抽奖/兑换/章节卡/大咖荐书默认关闭,可在 启点任务 菜单开启。", + "完全体任务:领取奖励固定执行;激励/每日/抽奖/兑换/章节卡/大咖荐书按账号单独管理,默认关闭。", "定时:每日 07:01 自动执行签到链路;QX 只负责抓 cookie。" ].join("\n") } @@ -295,7 +296,6 @@ function stripCommand(text) { if (/^启点(?:签到|读书)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:签到|读书)\s*/i, "")) if (/^启点(?:归档|日志归档)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:归档|日志归档)\s*/i, "")) if (/^启点(?:查询)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:查询)\s*/i, "")) - if (/^启点(?:任务|配置)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:任务|配置)\s*/i, "")) if (/^启点(?:账号)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:账号)\s*/i, "")) if (/^启点(?:ck|CK|cookie|Cookie)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:ck|CK|cookie|Cookie)\s*/i, "")) return trim(s.replace(/^qdreader\s*/i, "")) @@ -306,7 +306,6 @@ function commandName(text) { if (/^qdreader\s+sign(?:\s|$)/i.test(text)) return "sign" if (/^qdreader\s+archive(?:\s|$)/i.test(text)) return "archive" if (/^启点(?:查询)(?:\s|$)/i.test(text)) return "status" - if (/^启点(?:任务|配置)(?:\s|$)/i.test(text)) return "task" if (/^启点(?:账号)(?:\s|$)/i.test(text)) return "account" return "ck" } @@ -499,22 +498,36 @@ function sourceName(source) { if (source === "manual") return "手动签到" return source || "未知" } -function taskEnabled(task) { +function getTaskPrefs() { return safeJsonParse(cfgGet(TASK_PREF_KEY, "{}"), {}) || {} } +function setTaskPrefs(obj) { return cfgSet(TASK_PREF_KEY, JSON.stringify(obj || {})) } +function accountTaskPrefs(uid) { + var all = getTaskPrefs() + var prefs = all[String(uid)] + return isObject(prefs) ? prefs : {} +} +function taskEnabledForUid(task, uid) { if (task.defaultEnabled || task.builtin) return true if (task.sundayOnly) { var d = beijingDate(); if (d.getUTCDay() !== 0) return false } - return getBool(task.settingKey, false) + var prefs = accountTaskPrefs(uid) + return prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1" } -function taskStatusLines() { - var lines = ["启点完全体任务开关:"] +function taskEnabled(task) { return taskEnabledForUid(task, "") } +function taskStatusLines(uid) { + var lines = ["启点账号任务开关:" + (uid ? maskUid(uid) : "未选择账号")] + var prefs = uid ? accountTaskPrefs(uid) : {} for (var i = 0; i < QD_TASKS.length; i++) { var t = QD_TASKS[i] - lines.push((i + 1) + ". " + t.label + ":" + (taskEnabled(t) ? "开启" : "关闭") + (t.builtin ? "(固定)" : "")) + var enabled = t.builtin || t.defaultEnabled || prefs[t.key] === true || prefs[t.key] === "true" || prefs[t.key] === 1 || prefs[t.key] === "1" + var suffix = t.builtin ? "(固定)" : "" + if (t.sundayOnly) suffix += "(仅周日执行)" + lines.push((i + 1) + ". " + t.label + ":" + (enabled ? "开启" : "关闭") + suffix) } lines.push("回复:序号 on/off 切换,例如 2 on;q 退出") return lines.join("\n") } -function taskMenu() { - reply(taskStatusLines()) +function taskMenuForUid(uid) { + if (!uid) return reply("未选择账号,已退出") + reply(taskStatusLines(uid)) var op = listenInput(60000) if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出") var m = op.match(/^(\d+)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i) @@ -523,9 +536,14 @@ function taskMenu() { if (idx < 0 || idx >= QD_TASKS.length) return reply("编号无效,已退出") var task = QD_TASKS[idx] if (task.builtin || !task.settingKey) return reply(task.label + " 为固定任务,不能关闭") - var enable = m[2] ? /^(on|开启|启用|1)$/i.test(m[2]) : !getBool(task.settingKey, false) - cfgSet(task.settingKey, enable ? "true" : "false") - return reply(task.label + "已" + (enable ? "开启" : "关闭") + "\n" + taskStatusLines()) + var all = getTaskPrefs() + var prefs = isObject(all[String(uid)]) ? all[String(uid)] : {} + var currently = prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1" + var enable = m[2] ? /^(on|开启|启用|1)$/i.test(m[2]) : !currently + prefs[task.key] = !!enable + all[String(uid)] = prefs + setTaskPrefs(all) + return reply(task.label + "已" + (enable ? "开启" : "关闭") + "(账号 " + maskUid(uid) + ")\n" + taskStatusLines(uid)) } function doExtraTask(task, cookie) { var data = task.body || task.params || {} @@ -543,7 +561,7 @@ function doTaskChain(uid, cookie, source, runId) { details.push({ name: "签到结果", ok: !!ok, msg: String(msg || ""), path: CHECKIN_PATH }) for (var i = 0; i < QD_TASKS.length; i++) { var task = QD_TASKS[i] - if (!taskEnabled(task)) continue + if (!taskEnabledForUid(task, uid)) continue try { details.push(doExtraTask(task, cookie)) } catch (e) { details.push({ name: task.label, ok: false, msg: e && e.message ? e.message : String(e), path: task.path }) } } @@ -679,8 +697,9 @@ function accountMenuText() { "1. 查看账号", "2. 添加/更新账号", "3. 账号详情/修改备注", - "4. 删除账号", - "5. 清空账号", + "4. 账号任务配置", + "5. 删除账号", + "6. 清空账号", "q. 退出", "请回复序号" ].join("\n") @@ -691,6 +710,7 @@ function queryMenuText() { "1. 查看最近签到记录", "2. 查询单个账号最近记录", "3. 立即执行签到", + "说明:附加任务开关在 启点账号 → 账号任务配置 中按账号管理", "q. 退出", "请回复序号" ].join("\n") @@ -752,6 +772,7 @@ function accountDetailText(uid, store, bind) { "uid: " + maskUid(uid), "备注: " + (remarks[uid] || "未设置"), "cookie: " + maskCookie(store[uid]), + taskStatusLines(uid).replace(/\n回复:[\s\S]*$/, ""), formatResultLine(uid, last[uid], remarks, "暂无执行记录"), "修改备注请回复新备注名,发送 清空 可删除备注,发送 q 退出" ].join("\n") @@ -783,12 +804,17 @@ function accountMenu(store, bind) { return reply(save.text + remarkMsg) } if (/^(3|详情|备注|改名|名称)$/i.test(op)) return accountDetailMenu(store, bind) - if (/^(4|删除)$/i.test(op)) { + if (/^(4|任务|配置|开关)$/i.test(op)) { + var taskUid = chooseUid(store, bind, "请选择要配置任务的账号:") + if (!taskUid) return reply("已退出") + return taskMenuForUid(taskUid) + } + if (/^(5|删除)$/i.test(op)) { var uid = chooseUid(store, bind, "请选择要删除的账号:") if (!uid) return reply("已退出") return reply(handleDelete("删除 " + uid, store, bind)) } - if (/^(5|清空|全部)$/i.test(op)) return reply(handleDelete("删除 all", store, bind)) + if (/^(6|清空|全部)$/i.test(op)) return reply(handleDelete("删除 all", store, bind)) return reply("未识别选项,已退出") } function queryMenu() { @@ -814,13 +840,15 @@ function handleDelete(arg, store, bind) { if (/^(all|全部|所有)$/i.test(duid)) { for (var i = 0; i < uids.length; i++) { delete store[uids[i]]; delete bind[uids[i]] } var remarks = remarksForStore(store); for (var j = 0; j < uids.length; j++) delete remarks[uids[j]] - setStore(store); setBindStore(bind); setRemarkStore(remarks) + var allPrefs = getTaskPrefs(); for (var p = 0; p < uids.length; p++) delete allPrefs[uids[p]] + setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(allPrefs) return "已删除全部当前用户绑定的启点读书CK,共 " + uids.length + " 个账号" } if (uids.indexOf(duid) < 0) return store[duid] ? ("无权删除 uid: " + duid) : ("未找到 uid: " + duid) delete store[duid]; delete bind[duid] var remarks = remarksForStore(store); delete remarks[duid] - setStore(store); setBindStore(bind); setRemarkStore(remarks) + var prefs = getTaskPrefs(); delete prefs[duid] + setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(prefs) return "已删除 uid: " + duid } function normalizeCookieInput(arg) { @@ -886,7 +914,7 @@ function main() { var isCronRun = !text var cmd = isCronRun ? cronMode() : commandName(text) var arg = isCronRun ? "" : trim(stripCommand(text).replace(/^(sign|archive)\s*/i, "")) - if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status" && cmd !== "task") { reply(usage()); return } + if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status") { reply(usage()); return } if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return } var store = getStore() @@ -903,8 +931,7 @@ function main() { } if (cmd === "account") { accountMenu(store, bind); return } if (cmd === "status") { queryMenu(); return } - if (cmd === "task") { taskMenu(); return } - if (cmd === "ck" && /^(查询|状态|status|list|账号|管理|任务|配置)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck "); return } + if (cmd === "ck" && /^(查询|状态|status|list|账号|管理|任务|配置)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;任务配置在 启点账号 → 账号任务配置;快速提交CK:启点ck "); return } var delMsg = handleDelete(arg, store, bind) if (delMsg !== null) { reply(delMsg); return } if (cmd === "ck") { diff --git a/plugins/qdreader/README.md b/plugins/qdreader/README.md index 53cda12..c2ca7ca 100644 --- a/plugins/qdreader/README.md +++ b/plugins/qdreader/README.md @@ -30,21 +30,9 @@ https://gitea.chickliu.fun/Hermes/autman-plugins/raw/branch/main/js/qdreader_sig ```text 启点账号 启点查询 -启点任务 ``` -`启点账号` 只负责账号管理: - -```text -启点账号管理 -1. 查看账号 -2. 添加/更新账号 -3. 账号详情/修改备注 -4. 删除账号 -5. 清空账号 -q. 退出 -请回复序号 -``` +`启点账号` 负责账号管理和单账号任务配置: `启点查询` 只负责最后执行日志和执行动作,不做实时接口查询: @@ -57,21 +45,35 @@ q. 退出 请回复序号 ``` -`启点任务` 负责完整任务模块开关: +`启点账号` 内的“账号任务配置”负责完整任务模块开关,开关直接写入对应账号: ```text -启点完全体任务开关: +启点账号管理 +1. 查看账号 +2. 添加/更新账号 +3. 账号详情/修改备注 +4. 账号任务配置 +5. 删除账号 +6. 清空账号 +q. 退出 +请回复序号 +``` + +选择账号后显示该账号自己的任务开关: + +```text +启点账号任务开关:12***45 1. 领取奖励:开启(固定) 2. 激励任务:关闭 3. 每日任务:关闭 4. 每日抽奖:关闭 -5. 每周兑换:关闭 +5. 每周兑换:关闭(仅周日执行) 6. 章节卡片:关闭 7. 大咖荐书:关闭 回复:序号 on/off 切换,例如 2 on;q 退出 ``` -默认只执行签到 + 领取奖励;其余任务按上游完全体模块接入但默认关闭,避免广告/抽奖/兑换类接口在未确认前自动运行。开启后,签到链路会按账号依次追加对应模块,单个模块失败会记录为该账号“部分任务失败”,不会暴露 Cookie。 +默认只执行签到 + 领取奖励;其余任务按上游完全体模块接入但默认关闭,避免广告/抽奖/兑换类接口在未确认前自动运行。开启后只对该账号生效,签到链路会按账号读取自己的任务配置,单个模块失败会记录为该账号“部分任务失败”,不会暴露 Cookie。 ### 快速通道 @@ -84,7 +86,7 @@ qdreader sign qdreader archive ``` -`启点ck` 只保留快速提交 Cookie,不再承担查询/导出/批量导入等管理功能。 +`启点ck` 只保留快速提交 Cookie,不再承担查询/导出/批量导入等管理功能。`启点任务` 入口已取消,附加任务开关统一放在 `启点账号` 的单账号配置里。 ## 账号与备注 @@ -123,7 +125,8 @@ qdreader_extra_adv_job_enable # 每日任务开关,默认 false qdreader_lottery_enable # 每日抽奖开关,默认 false qdreader_weekly_exchange_enable # 每周兑换开关,默认 false qdreader_chapter_card_enable # 章节卡片开关,默认 false -qdreader_message_box_enable # 大咖荐书开关,默认 false +qdreader_message_box_enable # 旧全局大咖荐书开关,v2.7 起不再作为运行依据 +qdreader_task_pref_json # uid -> { taskKey: boolean },每个账号自己的附加任务开关 ``` 读取顺序: @@ -153,7 +156,7 @@ set(key, value) - 同时提取 `qdh/QDHeader/guid/qimei/qimei36` 供签名网关使用,其中 `qdh` 只取 Cookie 的 `qdh/QDH`,不再用 `QDHeader` 顶替,避免签名网关 `decodeQDHAuto` 解码失败。 2. `POST /sign` 获取请求签名头。默认签名网关基址是 `https://api.120399.xyz/qdreader`。 3. `POST https://h5.if.qidian.com/argus/api/v3/checkin/checkin` 完成签到。 -4. 追加完全体任务链路: +4. 按账号读取 `qdreader_task_pref_json` 后追加完全体任务链路: - 固定执行领取奖励状态查询:`GET /argus/api/v3/checkin/getcurrentweekcheckininfo`; - 可选激励任务:`GET /argus/api/v2/video/adv/mainPage`; - 可选每日任务:`GET /argus/api/v2/video/adv/packageList`; diff --git a/plugins/qdreader/qdreader_sign_autman.js b/plugins/qdreader/qdreader_sign_autman.js index add3cd9..d054473 100644 --- a/plugins/qdreader/qdreader_sign_autman.js +++ b/plugins/qdreader/qdreader_sign_autman.js @@ -4,7 +4,7 @@ //[author: Hermes] //[service: BOSS] //[class: 工具类] -//[version: 2.6.0] +//[version: 2.7.0] //[platform: web,qq,wx,tg,tb,fs,we] //[public: false] //[price: 0] @@ -24,6 +24,7 @@ var REMARK_KEY = "qdreader_remark_json" var SIGN_API_KEY = "qdreader_sign_api" var RETRY_KEY = "qdreader_retry_count" var CRON_SILENT_KEY = "qdreader_cron_silent" +var TASK_PREF_KEY = "qdreader_task_pref_json" var ADV_JOB_ENABLE_KEY = "qdreader_adv_job_enable" var EXTRA_ADV_JOB_ENABLE_KEY = "qdreader_extra_adv_job_enable" var LOTTERY_ENABLE_KEY = "qdreader_lottery_enable" @@ -275,7 +276,7 @@ function contentText() { } function usage() { return [ - "启点读书签到插件 v2.6.0", + "启点读书签到插件 v2.7.0", "【一级菜单】", "账号管理:启点账号", "执行查询:启点查询", @@ -283,10 +284,10 @@ function usage() { "快速提交CK:启点ck ", "直接签到:启点签到", "指定签到:启点签到 ", - "任务配置:启点任务", + "账号任务:启点账号 → 账号详情/任务配置", "【配置】", "签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API, - "完全体任务:领取奖励默认执行;激励/每日/抽奖/兑换/章节卡/大咖荐书默认关闭,可在 启点任务 菜单开启。", + "完全体任务:领取奖励固定执行;激励/每日/抽奖/兑换/章节卡/大咖荐书按账号单独管理,默认关闭。", "定时:每日 07:01 自动执行签到链路;QX 只负责抓 cookie。" ].join("\n") } @@ -295,7 +296,6 @@ function stripCommand(text) { if (/^启点(?:签到|读书)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:签到|读书)\s*/i, "")) if (/^启点(?:归档|日志归档)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:归档|日志归档)\s*/i, "")) if (/^启点(?:查询)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:查询)\s*/i, "")) - if (/^启点(?:任务|配置)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:任务|配置)\s*/i, "")) if (/^启点(?:账号)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:账号)\s*/i, "")) if (/^启点(?:ck|CK|cookie|Cookie)(?:\s|$)/i.test(s)) return trim(s.replace(/^启点(?:ck|CK|cookie|Cookie)\s*/i, "")) return trim(s.replace(/^qdreader\s*/i, "")) @@ -306,7 +306,6 @@ function commandName(text) { if (/^qdreader\s+sign(?:\s|$)/i.test(text)) return "sign" if (/^qdreader\s+archive(?:\s|$)/i.test(text)) return "archive" if (/^启点(?:查询)(?:\s|$)/i.test(text)) return "status" - if (/^启点(?:任务|配置)(?:\s|$)/i.test(text)) return "task" if (/^启点(?:账号)(?:\s|$)/i.test(text)) return "account" return "ck" } @@ -499,22 +498,36 @@ function sourceName(source) { if (source === "manual") return "手动签到" return source || "未知" } -function taskEnabled(task) { +function getTaskPrefs() { return safeJsonParse(cfgGet(TASK_PREF_KEY, "{}"), {}) || {} } +function setTaskPrefs(obj) { return cfgSet(TASK_PREF_KEY, JSON.stringify(obj || {})) } +function accountTaskPrefs(uid) { + var all = getTaskPrefs() + var prefs = all[String(uid)] + return isObject(prefs) ? prefs : {} +} +function taskEnabledForUid(task, uid) { if (task.defaultEnabled || task.builtin) return true if (task.sundayOnly) { var d = beijingDate(); if (d.getUTCDay() !== 0) return false } - return getBool(task.settingKey, false) + var prefs = accountTaskPrefs(uid) + return prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1" } -function taskStatusLines() { - var lines = ["启点完全体任务开关:"] +function taskEnabled(task) { return taskEnabledForUid(task, "") } +function taskStatusLines(uid) { + var lines = ["启点账号任务开关:" + (uid ? maskUid(uid) : "未选择账号")] + var prefs = uid ? accountTaskPrefs(uid) : {} for (var i = 0; i < QD_TASKS.length; i++) { var t = QD_TASKS[i] - lines.push((i + 1) + ". " + t.label + ":" + (taskEnabled(t) ? "开启" : "关闭") + (t.builtin ? "(固定)" : "")) + var enabled = t.builtin || t.defaultEnabled || prefs[t.key] === true || prefs[t.key] === "true" || prefs[t.key] === 1 || prefs[t.key] === "1" + var suffix = t.builtin ? "(固定)" : "" + if (t.sundayOnly) suffix += "(仅周日执行)" + lines.push((i + 1) + ". " + t.label + ":" + (enabled ? "开启" : "关闭") + suffix) } lines.push("回复:序号 on/off 切换,例如 2 on;q 退出") return lines.join("\n") } -function taskMenu() { - reply(taskStatusLines()) +function taskMenuForUid(uid) { + if (!uid) return reply("未选择账号,已退出") + reply(taskStatusLines(uid)) var op = listenInput(60000) if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出") var m = op.match(/^(\d+)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i) @@ -523,9 +536,14 @@ function taskMenu() { if (idx < 0 || idx >= QD_TASKS.length) return reply("编号无效,已退出") var task = QD_TASKS[idx] if (task.builtin || !task.settingKey) return reply(task.label + " 为固定任务,不能关闭") - var enable = m[2] ? /^(on|开启|启用|1)$/i.test(m[2]) : !getBool(task.settingKey, false) - cfgSet(task.settingKey, enable ? "true" : "false") - return reply(task.label + "已" + (enable ? "开启" : "关闭") + "\n" + taskStatusLines()) + var all = getTaskPrefs() + var prefs = isObject(all[String(uid)]) ? all[String(uid)] : {} + var currently = prefs[task.key] === true || prefs[task.key] === "true" || prefs[task.key] === 1 || prefs[task.key] === "1" + var enable = m[2] ? /^(on|开启|启用|1)$/i.test(m[2]) : !currently + prefs[task.key] = !!enable + all[String(uid)] = prefs + setTaskPrefs(all) + return reply(task.label + "已" + (enable ? "开启" : "关闭") + "(账号 " + maskUid(uid) + ")\n" + taskStatusLines(uid)) } function doExtraTask(task, cookie) { var data = task.body || task.params || {} @@ -543,7 +561,7 @@ function doTaskChain(uid, cookie, source, runId) { details.push({ name: "签到结果", ok: !!ok, msg: String(msg || ""), path: CHECKIN_PATH }) for (var i = 0; i < QD_TASKS.length; i++) { var task = QD_TASKS[i] - if (!taskEnabled(task)) continue + if (!taskEnabledForUid(task, uid)) continue try { details.push(doExtraTask(task, cookie)) } catch (e) { details.push({ name: task.label, ok: false, msg: e && e.message ? e.message : String(e), path: task.path }) } } @@ -679,8 +697,9 @@ function accountMenuText() { "1. 查看账号", "2. 添加/更新账号", "3. 账号详情/修改备注", - "4. 删除账号", - "5. 清空账号", + "4. 账号任务配置", + "5. 删除账号", + "6. 清空账号", "q. 退出", "请回复序号" ].join("\n") @@ -691,6 +710,7 @@ function queryMenuText() { "1. 查看最近签到记录", "2. 查询单个账号最近记录", "3. 立即执行签到", + "说明:附加任务开关在 启点账号 → 账号任务配置 中按账号管理", "q. 退出", "请回复序号" ].join("\n") @@ -752,6 +772,7 @@ function accountDetailText(uid, store, bind) { "uid: " + maskUid(uid), "备注: " + (remarks[uid] || "未设置"), "cookie: " + maskCookie(store[uid]), + taskStatusLines(uid).replace(/\n回复:[\s\S]*$/, ""), formatResultLine(uid, last[uid], remarks, "暂无执行记录"), "修改备注请回复新备注名,发送 清空 可删除备注,发送 q 退出" ].join("\n") @@ -783,12 +804,17 @@ function accountMenu(store, bind) { return reply(save.text + remarkMsg) } if (/^(3|详情|备注|改名|名称)$/i.test(op)) return accountDetailMenu(store, bind) - if (/^(4|删除)$/i.test(op)) { + if (/^(4|任务|配置|开关)$/i.test(op)) { + var taskUid = chooseUid(store, bind, "请选择要配置任务的账号:") + if (!taskUid) return reply("已退出") + return taskMenuForUid(taskUid) + } + if (/^(5|删除)$/i.test(op)) { var uid = chooseUid(store, bind, "请选择要删除的账号:") if (!uid) return reply("已退出") return reply(handleDelete("删除 " + uid, store, bind)) } - if (/^(5|清空|全部)$/i.test(op)) return reply(handleDelete("删除 all", store, bind)) + if (/^(6|清空|全部)$/i.test(op)) return reply(handleDelete("删除 all", store, bind)) return reply("未识别选项,已退出") } function queryMenu() { @@ -814,13 +840,15 @@ function handleDelete(arg, store, bind) { if (/^(all|全部|所有)$/i.test(duid)) { for (var i = 0; i < uids.length; i++) { delete store[uids[i]]; delete bind[uids[i]] } var remarks = remarksForStore(store); for (var j = 0; j < uids.length; j++) delete remarks[uids[j]] - setStore(store); setBindStore(bind); setRemarkStore(remarks) + var allPrefs = getTaskPrefs(); for (var p = 0; p < uids.length; p++) delete allPrefs[uids[p]] + setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(allPrefs) return "已删除全部当前用户绑定的启点读书CK,共 " + uids.length + " 个账号" } if (uids.indexOf(duid) < 0) return store[duid] ? ("无权删除 uid: " + duid) : ("未找到 uid: " + duid) delete store[duid]; delete bind[duid] var remarks = remarksForStore(store); delete remarks[duid] - setStore(store); setBindStore(bind); setRemarkStore(remarks) + var prefs = getTaskPrefs(); delete prefs[duid] + setStore(store); setBindStore(bind); setRemarkStore(remarks); setTaskPrefs(prefs) return "已删除 uid: " + duid } function normalizeCookieInput(arg) { @@ -886,7 +914,7 @@ function main() { var isCronRun = !text var cmd = isCronRun ? cronMode() : commandName(text) var arg = isCronRun ? "" : trim(stripCommand(text).replace(/^(sign|archive)\s*/i, "")) - if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status" && cmd !== "task") { reply(usage()); return } + if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status") { reply(usage()); return } if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return } var store = getStore() @@ -903,8 +931,7 @@ function main() { } if (cmd === "account") { accountMenu(store, bind); return } if (cmd === "status") { queryMenu(); return } - if (cmd === "task") { taskMenu(); return } - if (cmd === "ck" && /^(查询|状态|status|list|账号|管理|任务|配置)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck "); return } + if (cmd === "ck" && /^(查询|状态|status|list|账号|管理|任务|配置)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;任务配置在 启点账号 → 账号任务配置;快速提交CK:启点ck "); return } var delMsg = handleDelete(arg, store, bind) if (delMsg !== null) { reply(delMsg); return } if (cmd === "ck") { diff --git a/tests/qdreader_plugin.test.mjs b/tests/qdreader_plugin.test.mjs index bf969d5..1f72ce6 100644 --- a/tests/qdreader_plugin.test.mjs +++ b/tests/qdreader_plugin.test.mjs @@ -509,7 +509,7 @@ test('delete account uses numbered selection instead of uid input', () => { qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a', '67890': 'user-a' }), qdreader_remark_json: JSON.stringify({ '12345': '主号', '67890': '副号' }), }; - const r = runPlugin({ content: '启点账号', store, userId: 'user-a', listens: ['4', '2'] }); + const r = runPlugin({ content: '启点账号', store, userId: 'user-a', listens: ['5', '2'] }); assert.match(r.replies[1], /请选择要删除的账号/); assert.match(r.replies[1], /1\. 12\*\*\*45/); assert.match(r.replies[1], /2\. 67\*\*\*90/); @@ -523,7 +523,7 @@ test('delete all command clears only current user saved cookies', () => { qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a', '67890': 'user-b' }), qdreader_remark_json: JSON.stringify({ '12345': '主号', '67890': '副号' }), }; - const r = runPlugin({ content: '启点账号', store, userId: 'user-a', listens: ['5'] }); + const r = runPlugin({ content: '启点账号', store, userId: 'user-a', listens: ['6'] }); assert.match(r.replies[0], /启点账号管理/); assert.match(r.replies[1], /已删除全部/); assert.deepEqual(JSON.parse(r.store.qdreader_cookie_json), { '67890': cookie.replace(/12345/g, '67890') }); @@ -570,19 +570,46 @@ test('user cannot overwrite another user bound cookie', () => { assert.equal(JSON.parse(r.store.qdreader_user_bind_json)['12345'], '987654321'); }); -test('task menu toggles full task switches and enabled tasks are called', () => { +test('account task menu stores full task switches per account and sign uses selected account prefs', () => { const store = { qdreader_cookie_json: JSON.stringify({ '12345': cookie }), qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }), }; - const menu = runPlugin({ content: '启点任务', store: { ...store }, userId: 'user-a', listens: ['2 on'] }); - assert.match(menu.replies[0], /激励任务:关闭/); - assert.match(menu.replies[1], /激励任务已开启/); - assert.equal(menu.store.qdreader_adv_job_enable, 'true'); + const menu = runPlugin({ content: '启点账号', store: { ...store }, userId: 'user-a', listens: ['4', '1', '2 on'] }); + assert.match(menu.replies[0], /账号任务配置/); + assert.match(menu.replies[1], /请选择要配置任务的账号/); + assert.match(menu.replies[2], /激励任务:关闭/); + assert.match(menu.replies[3], /激励任务已开启/); + assert.equal(JSON.parse(menu.store.qdreader_task_pref_json)['12345'].adv, true); + assert.equal(menu.store.qdreader_adv_job_enable, undefined); - const sign = runPlugin({ content: '启点签到', store: { ...store, qdreader_adv_job_enable: 'true' }, userId: 'user-a' }); + const sign = runPlugin({ content: '启点签到', store: { ...store, qdreader_task_pref_json: JSON.stringify({ '12345': { adv: true } }) }, userId: 'user-a' }); assert.equal(sign.calls.length, 6); assert.match(sign.calls[3].url, /getcurrentweekcheckininfo$/); assert.match(sign.calls[5].url, /video\/adv\/mainPage$/); assert.match(sign.replies[0], /激励任务/); }); + + +test('per-account task switches do not affect other accounts', () => { + const store = { + qdreader_cookie_json: JSON.stringify({ '12345': cookie, '67890': cookie.replace(/12345/g, '67890') }), + qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a', '67890': 'user-a' }), + qdreader_task_pref_json: JSON.stringify({ '12345': { adv: true }, '67890': {} }), + }; + const sign = runPlugin({ content: '启点签到', store, userId: 'user-a' }); + // account 12345: sign + reward + adv = 6 calls; account 67890: sign + reward = 4 calls + assert.equal(sign.calls.length, 10); + assert.equal(sign.calls.filter((c) => /video\/adv\/mainPage$/.test(c.url)).length, 1); +}); + +test('delete account also clears per-account task prefs', () => { + const store = { + qdreader_cookie_json: JSON.stringify({ '12345': cookie }), + qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }), + qdreader_task_pref_json: JSON.stringify({ '12345': { adv: true } }), + }; + const r = runPlugin({ content: '启点账号', store, userId: 'user-a', listens: ['5', '1'] }); + assert.match(r.replies[2], /已删除 uid: 12345/); + assert.deepEqual(JSON.parse(r.store.qdreader_task_pref_json), {}); +});