From 93d529ec5fa6b0a09f74305df3c30526f72f19c0 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 20 May 2026 17:13:23 +0800 Subject: [PATCH] feat(qdreader): add full task switches --- README.md | 3 +- js/qdreader_sign_autman.js | 94 +++++++++++++++++++++--- plugins/qdreader/README.md | 43 +++++++++-- plugins/qdreader/qdreader_sign_autman.js | 94 +++++++++++++++++++++--- tests/qdreader_plugin.test.mjs | 39 ++++++++-- 5 files changed, 236 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9c21a6e..8a73aed 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ GET /api/sms/stats - 定时运行:`cron: 1 7 * * *`,每日 07:01 自动签到;签到结束后立即归档并清空最近运行日志 - 手动运行:`启点签到` / `启点签到 ` - Cookie 维护:`启点账号` / `启点ck ` / `启点查询` -- v2.5.9:保持单插件;定时只负责 07:01 签到,每次签到结束后立即归档运行日志;查询结果按批次/来源/汇总/明细排版并截断过长原始消息;所有运行日志和归档日期统一使用北京时间。 +- 完全体任务:`启点任务` 菜单按需开启激励任务、每日任务、每日抽奖、每周兑换、章节卡片、大咖荐书;默认只执行签到 + 领取奖励 +- v2.6.0:适配上游 QDReader 完全体任务框架,保留 Autman 单插件、独立 bucket、多用户隔离、手动只跑当前用户、cron 跑全部账号、运行后立即归档清日志。 Raw 导入: diff --git a/js/qdreader_sign_autman.js b/js/qdreader_sign_autman.js index cf28707..add3cd9 100644 --- a/js/qdreader_sign_autman.js +++ b/js/qdreader_sign_autman.js @@ -4,7 +4,7 @@ //[author: Hermes] //[service: BOSS] //[class: 工具类] -//[version: 2.5.9] +//[version: 2.6.0] //[platform: web,qq,wx,tg,tb,fs,we] //[public: false] //[price: 0] @@ -24,12 +24,27 @@ 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 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" +var WEEKLY_EXCHANGE_ENABLE_KEY = "qdreader_weekly_exchange_enable" +var CHAPTER_CARD_ENABLE_KEY = "qdreader_chapter_card_enable" +var MESSAGE_BOX_ENABLE_KEY = "qdreader_message_box_enable" var LAST_RESULT_KEY = "qdreader_last_result_json" var LAST_RUN_KEY = "qdreader_last_run_json" var ARCHIVE_KEY = "qdreader_archive_json" var DEFAULT_SIGN_API = "https://api.120399.xyz/qdreader" var QIDIAN_BASE = "https://h5.if.qidian.com" var CHECKIN_PATH = "/argus/api/v3/checkin/checkin" +var QD_TASKS = [ + { key: "reward", label: "领取奖励", path: "/argus/api/v3/checkin/getcurrentweekcheckininfo", method: "GET", defaultEnabled: true, builtin: true }, + { key: "adv", label: "激励任务", path: "/argus/api/v2/video/adv/mainPage", method: "GET", settingKey: ADV_JOB_ENABLE_KEY }, + { key: "extraAdv", label: "每日任务", path: "/argus/api/v2/video/adv/packageList", method: "GET", settingKey: EXTRA_ADV_JOB_ENABLE_KEY }, + { key: "lottery", label: "每日抽奖", path: "/argus/api/v2/video/lottery", method: "POST", settingKey: LOTTERY_ENABLE_KEY, body: { source: "autman" } }, + { key: "weeklyExchange", label: "每周兑换", path: "/argus/api/v2/video/exchange/page", method: "GET", settingKey: WEEKLY_EXCHANGE_ENABLE_KEY, sundayOnly: true }, + { key: "chapterCard", label: "章节卡片", path: "/argus/api/v2/video/chapterCard/info", method: "GET", settingKey: CHAPTER_CARD_ENABLE_KEY }, + { key: "messageBox", label: "大咖荐书", path: "/ajax/message/getMessageBox", method: "GET", settingKey: MESSAGE_BOX_ENABLE_KEY, params: { type: "qdreader" } } +] function defined(name) { try { return typeof this[name] !== "undefined" } catch (e) { return false } } function globalValue(name) { try { return this[name] } catch (e) { return undefined } } @@ -260,7 +275,7 @@ function contentText() { } function usage() { return [ - "启点读书签到插件 v2.5.9", + "启点读书签到插件 v2.6.0", "【一级菜单】", "账号管理:启点账号", "执行查询:启点查询", @@ -268,9 +283,11 @@ function usage() { "快速提交CK:启点ck ", "直接签到:启点签到", "指定签到:启点签到 ", + "任务配置:启点任务", "【配置】", "签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API, - "定时:每日 00:01 只归档并清空日志;每日 07:01 只自动签到;QX 只负责抓 cookie。" + "完全体任务:领取奖励默认执行;激励/每日/抽奖/兑换/章节卡/大咖荐书默认关闭,可在 启点任务 菜单开启。", + "定时:每日 07:01 自动执行签到链路;QX 只负责抓 cookie。" ].join("\n") } function stripCommand(text) { @@ -278,6 +295,7 @@ 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, "")) @@ -288,6 +306,7 @@ 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" } @@ -480,14 +499,66 @@ function sourceName(source) { if (source === "manual") return "手动签到" return source || "未知" } -function doCheckin(uid, cookie, source, runId) { - var startedAt = nowIso() - var res = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) +function taskEnabled(task) { + if (task.defaultEnabled || task.builtin) return true + if (task.sundayOnly) { var d = beijingDate(); if (d.getUTCDay() !== 0) return false } + return getBool(task.settingKey, false) +} +function taskStatusLines() { + var lines = ["启点完全体任务开关:"] + for (var i = 0; i < QD_TASKS.length; i++) { + var t = QD_TASKS[i] + lines.push((i + 1) + ". " + t.label + ":" + (taskEnabled(t) ? "开启" : "关闭") + (t.builtin ? "(固定)" : "")) + } + lines.push("回复:序号 on/off 切换,例如 2 on;q 退出") + return lines.join("\n") +} +function taskMenu() { + reply(taskStatusLines()) + var op = listenInput(60000) + if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出") + var m = op.match(/^(\d+)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i) + if (!m) return reply("未识别选项,已退出") + var idx = parseInt(m[1], 10) - 1 + 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()) +} +function doExtraTask(task, cookie) { + var data = task.body || task.params || {} + var res = qidianRequest(task.path, task.method || "GET", data, cookie) var ok = resultOk(res) - var msg = resultMsg(res) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) - var result = { uid: String(uid), ok: !!ok, msg: String(msg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "" } + var msg = resultMsg(res) || (ok ? "完成" : ("失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) + return { name: task.label, ok: !!ok, msg: String(msg || ""), path: task.path } +} +function doTaskChain(uid, cookie, source, runId) { + var startedAt = nowIso() + var details = [] + var checkin = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) + var ok = resultOk(checkin) + var msg = resultMsg(checkin) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(checkin) !== undefined ? " code=" + resultCode(checkin) : ""))) + 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 + 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 }) } + } + var failed = [] + for (var j = 0; j < details.length; j++) if (!details[j].ok) failed.push(details[j].name + ":" + details[j].msg) + var finalOk = failed.length === 0 + var detailMsg = [] + for (var k = 0; k < details.length; k++) detailMsg.push(details[k].name + (details[k].ok ? "✅" : "❌") + (details[k].msg ? "(" + displayMsg(details[k].msg) + ")" : "")) + var finalMsg = (finalOk ? "完全体任务完成" : "部分任务失败") + ":" + detailMsg.join(";") + var result = { uid: String(uid), ok: !!finalOk, msg: String(finalMsg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "", tasks: details } setLastResult(uid, result) - return { uid: uid, ok: ok, msg: msg, raw: res, log: result } + return { uid: uid, ok: finalOk, msg: finalMsg, raw: checkin, log: result } +} +function doCheckin(uid, cookie, source, runId) { + return doTaskChain(uid, cookie, source, runId) } function signAll(targetUid, allUsers, source, archiveAfterRun) { source = source || (allUsers ? "cron" : "manual") @@ -815,7 +886,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") { reply(usage()); return } + if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status" && cmd !== "task") { reply(usage()); return } if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return } var store = getStore() @@ -832,7 +903,8 @@ function main() { } if (cmd === "account") { accountMenu(store, bind); return } if (cmd === "status") { queryMenu(); return } - if (cmd === "ck" && /^(查询|状态|status|list|账号|管理)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck "); return } + if (cmd === "task") { taskMenu(); 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 b889103..53cda12 100644 --- a/plugins/qdreader/README.md +++ b/plugins/qdreader/README.md @@ -30,6 +30,7 @@ https://gitea.chickliu.fun/Hermes/autman-plugins/raw/branch/main/js/qdreader_sig ```text 启点账号 启点查询 +启点任务 ``` `启点账号` 只负责账号管理: @@ -56,6 +57,22 @@ q. 退出 请回复序号 ``` +`启点任务` 负责完整任务模块开关: + +```text +启点完全体任务开关: +1. 领取奖励:开启(固定) +2. 激励任务:关闭 +3. 每日任务:关闭 +4. 每日抽奖:关闭 +5. 每周兑换:关闭 +6. 章节卡片:关闭 +7. 大咖荐书:关闭 +回复:序号 on/off 切换,例如 2 on;q 退出 +``` + +默认只执行签到 + 领取奖励;其余任务按上游完全体模块接入但默认关闭,避免广告/抽奖/兑换类接口在未确认前自动运行。开启后,签到链路会按账号依次追加对应模块,单个模块失败会记录为该账号“部分任务失败”,不会暴露 Cookie。 + ### 快速通道 ```text @@ -101,6 +118,12 @@ qdreader_archive_json # yyyy-mm-dd -> 已归档批次日志,用于判断 qdreader_sign_api # 签名网关 qdreader_retry_count # 请求失败重试次数 qdreader_cron_silent # 定时无账号静默 +qdreader_adv_job_enable # 激励任务开关,默认 false +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 ``` 读取顺序: @@ -130,12 +153,20 @@ 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. 写入单账号当日结果 `qdreader_last_result_json`,包括 `ok/msg/time/source/runId`。 -5. 写入当日批次日志 `qdreader_last_run_json`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。 -6. 插件内只有一个 cron:`1 7 * * *`。 -7. 每日 07:01 触发时执行签到。 -8. 签到运行结束后,马上把本次 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;手动签到同样会立即归档。 -9. 汇总每个账号签到结果,并在回复中提示归档日期。 +4. 追加完全体任务链路: + - 固定执行领取奖励状态查询:`GET /argus/api/v3/checkin/getcurrentweekcheckininfo`; + - 可选激励任务:`GET /argus/api/v2/video/adv/mainPage`; + - 可选每日任务:`GET /argus/api/v2/video/adv/packageList`; + - 可选每日抽奖:`POST /argus/api/v2/video/lottery`; + - 可选每周兑换:`GET /argus/api/v2/video/exchange/page`,仅北京时间周日执行; + - 可选章节卡片:`GET /argus/api/v2/video/chapterCard/info`; + - 可选大咖荐书:`GET /ajax/message/getMessageBox`。 +5. 写入单账号当日结果 `qdreader_last_result_json`,包括 `ok/msg/time/source/runId/tasks`。 +6. 写入当日批次日志 `qdreader_last_run_json`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。 +7. 插件内只有一个 cron:`1 7 * * *`。 +8. 每日 07:01 触发时执行签到链路。 +9. 签到运行结束后,马上把本次 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;手动签到同样会立即归档。 +10. 汇总每个账号签到/完整任务结果,并在回复中提示归档日期。 ## 查询数据来源 diff --git a/plugins/qdreader/qdreader_sign_autman.js b/plugins/qdreader/qdreader_sign_autman.js index cf28707..add3cd9 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.5.9] +//[version: 2.6.0] //[platform: web,qq,wx,tg,tb,fs,we] //[public: false] //[price: 0] @@ -24,12 +24,27 @@ 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 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" +var WEEKLY_EXCHANGE_ENABLE_KEY = "qdreader_weekly_exchange_enable" +var CHAPTER_CARD_ENABLE_KEY = "qdreader_chapter_card_enable" +var MESSAGE_BOX_ENABLE_KEY = "qdreader_message_box_enable" var LAST_RESULT_KEY = "qdreader_last_result_json" var LAST_RUN_KEY = "qdreader_last_run_json" var ARCHIVE_KEY = "qdreader_archive_json" var DEFAULT_SIGN_API = "https://api.120399.xyz/qdreader" var QIDIAN_BASE = "https://h5.if.qidian.com" var CHECKIN_PATH = "/argus/api/v3/checkin/checkin" +var QD_TASKS = [ + { key: "reward", label: "领取奖励", path: "/argus/api/v3/checkin/getcurrentweekcheckininfo", method: "GET", defaultEnabled: true, builtin: true }, + { key: "adv", label: "激励任务", path: "/argus/api/v2/video/adv/mainPage", method: "GET", settingKey: ADV_JOB_ENABLE_KEY }, + { key: "extraAdv", label: "每日任务", path: "/argus/api/v2/video/adv/packageList", method: "GET", settingKey: EXTRA_ADV_JOB_ENABLE_KEY }, + { key: "lottery", label: "每日抽奖", path: "/argus/api/v2/video/lottery", method: "POST", settingKey: LOTTERY_ENABLE_KEY, body: { source: "autman" } }, + { key: "weeklyExchange", label: "每周兑换", path: "/argus/api/v2/video/exchange/page", method: "GET", settingKey: WEEKLY_EXCHANGE_ENABLE_KEY, sundayOnly: true }, + { key: "chapterCard", label: "章节卡片", path: "/argus/api/v2/video/chapterCard/info", method: "GET", settingKey: CHAPTER_CARD_ENABLE_KEY }, + { key: "messageBox", label: "大咖荐书", path: "/ajax/message/getMessageBox", method: "GET", settingKey: MESSAGE_BOX_ENABLE_KEY, params: { type: "qdreader" } } +] function defined(name) { try { return typeof this[name] !== "undefined" } catch (e) { return false } } function globalValue(name) { try { return this[name] } catch (e) { return undefined } } @@ -260,7 +275,7 @@ function contentText() { } function usage() { return [ - "启点读书签到插件 v2.5.9", + "启点读书签到插件 v2.6.0", "【一级菜单】", "账号管理:启点账号", "执行查询:启点查询", @@ -268,9 +283,11 @@ function usage() { "快速提交CK:启点ck ", "直接签到:启点签到", "指定签到:启点签到 ", + "任务配置:启点任务", "【配置】", "签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API, - "定时:每日 00:01 只归档并清空日志;每日 07:01 只自动签到;QX 只负责抓 cookie。" + "完全体任务:领取奖励默认执行;激励/每日/抽奖/兑换/章节卡/大咖荐书默认关闭,可在 启点任务 菜单开启。", + "定时:每日 07:01 自动执行签到链路;QX 只负责抓 cookie。" ].join("\n") } function stripCommand(text) { @@ -278,6 +295,7 @@ 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, "")) @@ -288,6 +306,7 @@ 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" } @@ -480,14 +499,66 @@ function sourceName(source) { if (source === "manual") return "手动签到" return source || "未知" } -function doCheckin(uid, cookie, source, runId) { - var startedAt = nowIso() - var res = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) +function taskEnabled(task) { + if (task.defaultEnabled || task.builtin) return true + if (task.sundayOnly) { var d = beijingDate(); if (d.getUTCDay() !== 0) return false } + return getBool(task.settingKey, false) +} +function taskStatusLines() { + var lines = ["启点完全体任务开关:"] + for (var i = 0; i < QD_TASKS.length; i++) { + var t = QD_TASKS[i] + lines.push((i + 1) + ". " + t.label + ":" + (taskEnabled(t) ? "开启" : "关闭") + (t.builtin ? "(固定)" : "")) + } + lines.push("回复:序号 on/off 切换,例如 2 on;q 退出") + return lines.join("\n") +} +function taskMenu() { + reply(taskStatusLines()) + var op = listenInput(60000) + if (!op || /^(q|退出|取消)$/i.test(op)) return reply("已退出") + var m = op.match(/^(\d+)\s*(on|off|开启|关闭|启用|禁用|1|0)?$/i) + if (!m) return reply("未识别选项,已退出") + var idx = parseInt(m[1], 10) - 1 + 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()) +} +function doExtraTask(task, cookie) { + var data = task.body || task.params || {} + var res = qidianRequest(task.path, task.method || "GET", data, cookie) var ok = resultOk(res) - var msg = resultMsg(res) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) - var result = { uid: String(uid), ok: !!ok, msg: String(msg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "" } + var msg = resultMsg(res) || (ok ? "完成" : ("失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) + return { name: task.label, ok: !!ok, msg: String(msg || ""), path: task.path } +} +function doTaskChain(uid, cookie, source, runId) { + var startedAt = nowIso() + var details = [] + var checkin = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) + var ok = resultOk(checkin) + var msg = resultMsg(checkin) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(checkin) !== undefined ? " code=" + resultCode(checkin) : ""))) + 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 + 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 }) } + } + var failed = [] + for (var j = 0; j < details.length; j++) if (!details[j].ok) failed.push(details[j].name + ":" + details[j].msg) + var finalOk = failed.length === 0 + var detailMsg = [] + for (var k = 0; k < details.length; k++) detailMsg.push(details[k].name + (details[k].ok ? "✅" : "❌") + (details[k].msg ? "(" + displayMsg(details[k].msg) + ")" : "")) + var finalMsg = (finalOk ? "完全体任务完成" : "部分任务失败") + ":" + detailMsg.join(";") + var result = { uid: String(uid), ok: !!finalOk, msg: String(finalMsg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "", tasks: details } setLastResult(uid, result) - return { uid: uid, ok: ok, msg: msg, raw: res, log: result } + return { uid: uid, ok: finalOk, msg: finalMsg, raw: checkin, log: result } +} +function doCheckin(uid, cookie, source, runId) { + return doTaskChain(uid, cookie, source, runId) } function signAll(targetUid, allUsers, source, archiveAfterRun) { source = source || (allUsers ? "cron" : "manual") @@ -815,7 +886,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") { reply(usage()); return } + if (!arg && cmd !== "sign" && cmd !== "archive" && cmd !== "account" && cmd !== "status" && cmd !== "task") { reply(usage()); return } if (/^(帮助|help|h)$/i.test(arg)) { reply(usage()); return } var store = getStore() @@ -832,7 +903,8 @@ function main() { } if (cmd === "account") { accountMenu(store, bind); return } if (cmd === "status") { queryMenu(); return } - if (cmd === "ck" && /^(查询|状态|status|list|账号|管理)$/i.test(arg)) { reply("请使用一级菜单:启点账号 或 启点查询;快速提交CK:启点ck "); return } + if (cmd === "task") { taskMenu(); 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 89e5c82..bf969d5 100644 --- a/tests/qdreader_plugin.test.mjs +++ b/tests/qdreader_plugin.test.mjs @@ -244,7 +244,8 @@ test('cron signs then immediately archives and clears recent logs', () => { qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }), }; const r = runPlugin({ content: '', store, userId: 'cron', globals: { Date: fixedDateClass('2026-05-19T07:01:00.000+08:00') } }); - assert.equal(r.calls.length, 2); + assert.equal(r.calls.length, 4); + assert.match(r.calls[3].url, /getcurrentweekcheckininfo$/); assert.match(r.replies[0], /启点读书签到结果/); assert.match(r.replies[0], /日志已归档:2026-05-19/); const archive = JSON.parse(r.store.qdreader_archive_json); @@ -261,7 +262,8 @@ test('manual sign also archives immediately after run', () => { qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }), }; const r = runPlugin({ content: '启点签到', store, userId: 'user-a', globals: { Date: fixedDateClass('2026-05-19T08:01:00.000+08:00') } }); - assert.equal(r.calls.length, 2); + assert.equal(r.calls.length, 4); + assert.match(r.calls[3].url, /getcurrentweekcheckininfo$/); assert.match(r.replies[0], /日志已归档:2026-05-19/); assert.equal(JSON.parse(r.store.qdreader_archive_json)['2026-05-19'].source, 'manual'); assert.deepEqual(JSON.parse(r.store.qdreader_last_run_json), {}); @@ -337,9 +339,10 @@ test('manual sign and cron sign both call sign gateway then qidian checkin', () qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }), }; const manual = runPlugin({ content: '启点签到', store: { ...store } }); - assert.equal(manual.calls.length, 2); + assert.equal(manual.calls.length, 4); assert.match(manual.calls[0].url, /api\.120399\.xyz\/qdreader\/sign$/); assert.match(manual.calls[1].url, /h5\.if\.qidian\.com\/argus\/api\/v3\/checkin\/checkin$/); + assert.match(manual.calls[3].url, /getcurrentweekcheckininfo$/); assert.match(manual.replies[0], /✅ 12\*\*\*45/); assert.match(manual.replies[0], /记录时间:/); assert.equal(JSON.parse(manual.store.qdreader_archive_json)['2026-05-20'].source, 'manual'); @@ -351,7 +354,8 @@ test('manual sign and cron sign both call sign gateway then qidian checkin', () assert.equal(payload.qdheader, 'MTIzNDV8eA=='); const cron = runPlugin({ content: '', store: { ...store }, globals: { Date: fixedDateClass('2026-05-19T07:01:00.000+08:00') } }); - assert.equal(cron.calls.length, 2); + assert.equal(cron.calls.length, 4); + assert.match(cron.calls[3].url, /getcurrentweekcheckininfo$/); assert.match(cron.replies[0], /启点读书签到结果/); assert.equal(JSON.parse(cron.store.qdreader_archive_json)['2026-05-19'].source, 'cron'); assert.deepEqual(JSON.parse(cron.store.qdreader_last_run_json), {}); @@ -394,9 +398,11 @@ test('qidian request retries transient request errors', () => { { body: { code: 0, data: { headers: { 'QD-Sign': 'sig' } } } }, new Error('timeout'), { statusCode: 200, body: { code: 0, msg: '重试后成功' } }, + { body: { code: 0, data: { headers: { 'QD-Sign': 'sig' } } } }, + { statusCode: 200, body: { code: 0, msg: '奖励完成' } }, ], }); - assert.equal(r.calls.length, 3); + assert.equal(r.calls.length, 5); assert.match(r.replies[0], /重试后成功/); }); @@ -531,10 +537,10 @@ test('manual sign only runs current user accounts while cron runs all accounts', qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a', '67890': 'user-b' }), }; const manual = runPlugin({ content: '启点签到', store: { ...store }, userId: 'user-a' }); - assert.equal(manual.calls.length, 2); + assert.equal(manual.calls.length, 4); assert.match(manual.replies[0], /总计 1/); const cron = runPlugin({ content: '', store: { ...store }, userId: 'cron', globals: { Date: fixedDateClass('2026-05-19T07:01:00.000+08:00') } }); - assert.equal(cron.calls.length, 4); + assert.equal(cron.calls.length, 8); assert.match(cron.replies[0], /总计 2/); }); @@ -548,7 +554,7 @@ test('unbound global cookies are not visible or usable by normal users', () => { assert.match(manual.replies[0], /还没有保存启点读书CK/); const cron = runPlugin({ content: '', store: { ...store }, userId: 'cron', globals: { Date: fixedDateClass('2026-05-19T07:01:00.000+08:00') } }); - assert.equal(cron.calls.length, 2); + assert.equal(cron.calls.length, 4); assert.match(cron.replies[0], /总计 1/); }); @@ -563,3 +569,20 @@ test('user cannot overwrite another user bound cookie', () => { assert.equal(JSON.parse(r.store.qdreader_cookie_json)['12345'], 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', () => { + 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 sign = runPlugin({ content: '启点签到', store: { ...store, qdreader_adv_job_enable: '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], /激励任务/); +});