diff --git a/docs/autman-plugin-rules.md b/docs/autman-plugin-rules.md index abb86ff..7f8fc46 100644 --- a/docs/autman-plugin-rules.md +++ b/docs/autman-plugin-rules.md @@ -532,7 +532,7 @@ Content-Type: application/json - 优先做“一级菜单 + 二级编号选择”,不要堆很多平铺子命令。 - 账号管理和执行查询分离: - `xxx账号`:只做账号列表、添加/更新、详情/备注、删除、清空。 - - `xxx查询`:只做最近执行情况、单账号执行情况、立即执行等执行侧动作。 + - `xxx查询`:只做最后执行日志/最近记录、单账号最近记录、立即执行等执行侧动作;除非明确实现实时接口,否则不要伪装成实时查询。 - 高频输入可以保留快速通道,例如 `xxxck `,但快速通道只做快速提交,不再混入查询/导出/批量导入。 - 用户选择账号时展示编号列表,让用户回复编号;不要让用户手动复制/输入 UID。 - 添加/更新账号成功后立刻询问是否添加备注;备注修改放在账号详情里,不单独做一个割裂的备注菜单。 @@ -551,8 +551,11 @@ Content-Type: application/json // remark_json { "uid": "主号" } -// last_result_json -{ "uid": "{\"ok\":true,\"msg\":\"成功\",\"time\":\"...\"}" } +// last_result_json:单账号最后执行结果 +{ "uid": "{\"ok\":true,\"msg\":\"成功\",\"time\":\"...\",\"source\":\"cron\",\"runId\":\"...\"}" } + +// last_run_json:最近一次执行批次日志 +{ "source": "cron", "finishedAt": "...", "okCount": 1, "failCount": 0, "total": 1, "items": [] } ``` 这样删除、过滤、展示和权限校验更安全。 @@ -581,6 +584,8 @@ bucketGet("otto", key) ### 13.5 安全与测试 +- `xxx查询` 的数据来源必须写清楚:如果没有实时接口,就命名为“最近签到记录/最后执行日志”,读取 `last_run_json` 和 `last_result_json`,不要让用户误以为是实时查询。 +- 执行动作要同时写批次日志和单账号日志;成功、业务失败、请求异常都要落日志,异常不能只显示在当次回复里。 - 普通回复永远不要输出完整 Cookie;只展示脱敏 UID、备注、Cookie key 摘要或长度。 - 高风险的导出/批量导入功能默认不做,除非 BOSS 明确需要。 - Node VM 测试应模拟:`sender.reply`、`sender.listen`、`sender.getUserID`、`get/set`、`bucketGet/bucketSet`、`request`。 diff --git a/js/qdreader_sign_autman.js b/js/qdreader_sign_autman.js index 791bd5d..04b265a 100644 --- a/js/qdreader_sign_autman.js +++ b/js/qdreader_sign_autman.js @@ -28,6 +28,8 @@ 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 LAST_RESULT_KEY = "qdreader_last_result_json" +var LAST_RUN_KEY = "qdreader_last_run_json" var DEFAULT_SIGN_API = "https://api.120399.xyz" var QIDIAN_BASE = "https://h5.if.qidian.com" var CHECKIN_PATH = "/argus/api/v3/checkin/checkin" @@ -290,46 +292,73 @@ function resultMsg(obj) { if (!obj) return "" return obj.Message || obj.Msg || obj.message || obj.msg || obj.toast || (obj.data && (obj.data.message || obj.data.msg || obj.data.toast)) || "" } -function setLastResult(uid, ok, msg) { - var key = "qdreader_last_result_json" - var obj = cleanStringMap(safeJsonParse(cfgGet(key, "{}"), {})) - obj[String(uid)] = JSON.stringify({ ok: !!ok, msg: String(msg || ""), time: new Date().toISOString() }) - cfgSet(key, JSON.stringify(obj)) +function nowIso() { try { return new Date().toISOString() } catch (e) { return String(new Date()) } } +function setLastResult(uid, result) { + var obj = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {})) + obj[String(uid)] = JSON.stringify(result || {}) + cfgSet(LAST_RESULT_KEY, JSON.stringify(obj)) } function getLastResults() { - var raw = cleanStringMap(safeJsonParse(cfgGet("qdreader_last_result_json", "{}"), {})) + var raw = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {})) var out = {} for (var uid in raw) if (raw.hasOwnProperty(uid)) out[uid] = safeJsonParse(raw[uid], { msg: raw[uid] }) return out } -function doCheckin(uid, cookie) { +function setLastRunLog(log) { cfgSet(LAST_RUN_KEY, JSON.stringify(log || {})) } +function getLastRunLog() { return safeJsonParse(cfgGet(LAST_RUN_KEY, "{}"), {}) || {} } +function sourceName(source) { + if (source === "cron") return "定时任务" + if (source === "menu") return "菜单立即执行" + if (source === "manual") return "手动签到" + return source || "未知" +} +function doCheckin(uid, cookie, source, runId) { + var startedAt = nowIso() var res = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) var ok = resultOk(res) var msg = resultMsg(res) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) - setLastResult(uid, ok, msg) - return { uid: uid, ok: ok, msg: msg, raw: res } + var result = { uid: String(uid), ok: !!ok, msg: String(msg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "" } + setLastResult(uid, result) + return { uid: uid, ok: ok, msg: msg, raw: res, log: result } } -function signAll(targetUid, allUsers) { +function signAll(targetUid, allUsers, source) { + source = source || (allUsers ? "cron" : "manual") + var startedAt = nowIso() + var runId = startedAt.replace(/[-:.TZ]/g, "").slice(0, 14) + "_" + source var store = getStore() var bind = getBindStore() + var remarks = getRemarkStore() var uids = visibleUids(store, bind, !!allUsers) if (targetUid) uids = uids.filter(function (u) { return u === targetUid }) - if (!uids.length) return targetUid ? ("未找到 uid: " + targetUid) : "还没有保存启点读书CK" + if (!uids.length) { + var emptyMsg = targetUid ? ("未找到 uid: " + targetUid) : "还没有保存启点读书CK" + setLastRunLog({ runId: runId, source: source, startedAt: startedAt, finishedAt: nowIso(), targetUid: targetUid || "", total: 0, okCount: 0, failCount: 0, message: emptyMsg, items: [] }) + return emptyMsg + } var okCount = 0 var failCount = 0 + var items = [] var lines = ["启点读书签到结果:"] for (var i = 0; i < uids.length; i++) { var uid = uids[i] try { - var r = doCheckin(uid, store[uid]) + var r = doCheckin(uid, store[uid], source, runId) if (r.ok) okCount++; else failCount++ - lines.push((r.ok ? "✅ " : "❌ ") + maskUid(uid) + ":" + r.msg) + items.push({ uid: String(uid), ok: !!r.ok, msg: String(r.msg || ""), time: r.log.time, remark: remarks[uid] || "" }) + lines.push((r.ok ? "✅ " : "❌ ") + maskUid(uid) + (remarks[uid] ? "(" + remarks[uid] + ")" : "") + ":" + r.msg) } catch (e) { failCount++ - lines.push("❌ " + maskUid(uid) + ":" + (e && e.message ? e.message : String(e))) + var emsg = e && e.message ? e.message : String(e) + var er = { uid: String(uid), ok: false, msg: emsg, time: nowIso(), startedAt: startedAt, source: source, runId: runId, error: true, remark: remarks[uid] || "" } + setLastResult(uid, er) + items.push(er) + lines.push("❌ " + maskUid(uid) + (remarks[uid] ? "(" + remarks[uid] + ")" : "") + ":" + emsg) } } + var finishedAt = nowIso() + setLastRunLog({ runId: runId, source: source, startedAt: startedAt, finishedAt: finishedAt, targetUid: targetUid || "", allUsers: !!allUsers, total: uids.length, okCount: okCount, failCount: failCount, items: items }) lines.push("汇总:成功 " + okCount + " / 失败 " + failCount + " / 总计 " + uids.length) + lines.push("记录时间:" + finishedAt + "(" + sourceName(source) + ")") return lines.join("\n") } @@ -343,6 +372,7 @@ function queryStatus(targetUid) { var store = getStore() var bind = getBindStore() var last = getLastResults() + var run = getLastRunLog() var remarks = getRemarkStore() var uids = visibleUids(store, bind, false) if (targetUid) { @@ -350,7 +380,9 @@ function queryStatus(targetUid) { uids = [targetUid] } if (!uids.length) return "当前用户还没有保存启点读书CK" - var lines = [targetUid ? "启点单账号执行情况:" : "启点执行情况:"] + var title = targetUid ? "启点单账号最近签到记录:" : "启点最近签到记录:" + var lines = [title] + if (!targetUid && run && run.finishedAt) lines.push("最近批次:" + sourceName(run.source) + " @ " + run.finishedAt + ";成功 " + (run.okCount || 0) + " / 失败 " + (run.failCount || 0) + " / 总计 " + (run.total || 0)) for (var i = 0; i < uids.length; i++) lines.push(formatStatusLine(uids[i], last, remarks)) return lines.join("\n") } @@ -373,9 +405,9 @@ function accountMenuText() { } function queryMenuText() { return [ - "启点查询", - "1. 查看最近执行情况", - "2. 查询单个账号", + "启点查询(最近签到记录)", + "1. 查看最近签到记录", + "2. 查询单个账号最近记录", "3. 立即执行签到", "q. 退出", "请回复序号" @@ -490,7 +522,7 @@ function queryMenu() { if (!uid) return reply("已退出") return reply(queryStatus(uid)) } - if (/^(3|执行|签到|立即)$/i.test(op)) return reply(signAll("", false)) + if (/^(3|执行|签到|立即)$/i.test(op)) return reply(signAll("", false, "menu")) return reply("未识别选项,已退出") } function handleDelete(arg, store, bind) { @@ -535,7 +567,7 @@ function main() { var bind = getBindStore() if (cmd === "sign" || /^(签到|sign|run)(?:\s+|$)/i.test(arg)) { var target = trim(arg.replace(/^(签到|sign|run)\s*/i, "")) - var out = signAll(target, isCronRun) + var out = signAll(target, isCronRun, isCronRun ? "cron" : "manual") if (!(isCronRun && getBool(CRON_SILENT_KEY, false) && out === "还没有保存启点读书CK")) reply(out) return } diff --git a/plugins/qdreader/README.md b/plugins/qdreader/README.md index 908487e..48ed091 100644 --- a/plugins/qdreader/README.md +++ b/plugins/qdreader/README.md @@ -45,12 +45,12 @@ q. 退出 请回复序号 ``` -`启点查询` 只负责执行情况和执行动作: +`启点查询` 只负责最后执行日志和执行动作,不做实时接口查询: ```text -启点查询 -1. 查看最近执行情况 -2. 查询单个账号 +启点查询(最近签到记录) +1. 查看最近签到记录 +2. 查询单个账号最近记录 3. 立即执行签到 q. 退出 请回复序号 @@ -91,7 +91,8 @@ hermes_qidian qdreader_cookie_json # uid -> full cookie qdreader_user_bind_json # uid -> sender.getUserID() qdreader_remark_json # uid -> 备注名 -qdreader_last_result_json # uid -> 最近执行结果 JSON 字符串 +qdreader_last_result_json # uid -> 单账号最后执行结果 JSON 字符串 +qdreader_last_run_json # 最近一次执行批次日志、来源和汇总 qdreader_sign_api # 签名网关 qdreader_retry_count # 请求失败重试次数 qdreader_cron_silent # 定时无账号静默 @@ -119,8 +120,20 @@ set(key, value) 1. 从 Cookie 解析 `QDHeader/qdh/uid/guid/qimei/qimei36`。 2. `POST /sign` 获取请求签名头。 3. `POST https://h5.if.qidian.com/argus/api/v3/checkin/checkin` 完成签到。 -4. 写入 `qdreader_last_result_json`。 -5. 汇总每个账号签到结果。 +4. 写入单账号最后结果 `qdreader_last_result_json`,包括 `ok/msg/time/source/runId`。 +5. 写入批次日志 `qdreader_last_run_json`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。 +6. 汇总每个账号签到结果。 + +## 查询数据来源 + +`启点查询` 不请求起点实时接口,只读取最后执行日志: + +```text +qdreader_last_run_json # 最近一次执行批次 +qdreader_last_result_json # 每个 uid 最近一次执行结果 +``` + +因此“查询”语义是“最近签到记录/最后执行日志”。刚添加但从未执行过的账号会显示“暂无执行记录”。 ## 本地验证 diff --git a/plugins/qdreader/qdreader_sign_autman.js b/plugins/qdreader/qdreader_sign_autman.js index 791bd5d..04b265a 100644 --- a/plugins/qdreader/qdreader_sign_autman.js +++ b/plugins/qdreader/qdreader_sign_autman.js @@ -28,6 +28,8 @@ 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 LAST_RESULT_KEY = "qdreader_last_result_json" +var LAST_RUN_KEY = "qdreader_last_run_json" var DEFAULT_SIGN_API = "https://api.120399.xyz" var QIDIAN_BASE = "https://h5.if.qidian.com" var CHECKIN_PATH = "/argus/api/v3/checkin/checkin" @@ -290,46 +292,73 @@ function resultMsg(obj) { if (!obj) return "" return obj.Message || obj.Msg || obj.message || obj.msg || obj.toast || (obj.data && (obj.data.message || obj.data.msg || obj.data.toast)) || "" } -function setLastResult(uid, ok, msg) { - var key = "qdreader_last_result_json" - var obj = cleanStringMap(safeJsonParse(cfgGet(key, "{}"), {})) - obj[String(uid)] = JSON.stringify({ ok: !!ok, msg: String(msg || ""), time: new Date().toISOString() }) - cfgSet(key, JSON.stringify(obj)) +function nowIso() { try { return new Date().toISOString() } catch (e) { return String(new Date()) } } +function setLastResult(uid, result) { + var obj = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {})) + obj[String(uid)] = JSON.stringify(result || {}) + cfgSet(LAST_RESULT_KEY, JSON.stringify(obj)) } function getLastResults() { - var raw = cleanStringMap(safeJsonParse(cfgGet("qdreader_last_result_json", "{}"), {})) + var raw = cleanStringMap(safeJsonParse(cfgGet(LAST_RESULT_KEY, "{}"), {})) var out = {} for (var uid in raw) if (raw.hasOwnProperty(uid)) out[uid] = safeJsonParse(raw[uid], { msg: raw[uid] }) return out } -function doCheckin(uid, cookie) { +function setLastRunLog(log) { cfgSet(LAST_RUN_KEY, JSON.stringify(log || {})) } +function getLastRunLog() { return safeJsonParse(cfgGet(LAST_RUN_KEY, "{}"), {}) || {} } +function sourceName(source) { + if (source === "cron") return "定时任务" + if (source === "menu") return "菜单立即执行" + if (source === "manual") return "手动签到" + return source || "未知" +} +function doCheckin(uid, cookie, source, runId) { + var startedAt = nowIso() var res = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) var ok = resultOk(res) var msg = resultMsg(res) || (ok ? "签到请求成功" : ("签到失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) - setLastResult(uid, ok, msg) - return { uid: uid, ok: ok, msg: msg, raw: res } + var result = { uid: String(uid), ok: !!ok, msg: String(msg || ""), time: nowIso(), startedAt: startedAt, source: source || "manual", runId: runId || "" } + setLastResult(uid, result) + return { uid: uid, ok: ok, msg: msg, raw: res, log: result } } -function signAll(targetUid, allUsers) { +function signAll(targetUid, allUsers, source) { + source = source || (allUsers ? "cron" : "manual") + var startedAt = nowIso() + var runId = startedAt.replace(/[-:.TZ]/g, "").slice(0, 14) + "_" + source var store = getStore() var bind = getBindStore() + var remarks = getRemarkStore() var uids = visibleUids(store, bind, !!allUsers) if (targetUid) uids = uids.filter(function (u) { return u === targetUid }) - if (!uids.length) return targetUid ? ("未找到 uid: " + targetUid) : "还没有保存启点读书CK" + if (!uids.length) { + var emptyMsg = targetUid ? ("未找到 uid: " + targetUid) : "还没有保存启点读书CK" + setLastRunLog({ runId: runId, source: source, startedAt: startedAt, finishedAt: nowIso(), targetUid: targetUid || "", total: 0, okCount: 0, failCount: 0, message: emptyMsg, items: [] }) + return emptyMsg + } var okCount = 0 var failCount = 0 + var items = [] var lines = ["启点读书签到结果:"] for (var i = 0; i < uids.length; i++) { var uid = uids[i] try { - var r = doCheckin(uid, store[uid]) + var r = doCheckin(uid, store[uid], source, runId) if (r.ok) okCount++; else failCount++ - lines.push((r.ok ? "✅ " : "❌ ") + maskUid(uid) + ":" + r.msg) + items.push({ uid: String(uid), ok: !!r.ok, msg: String(r.msg || ""), time: r.log.time, remark: remarks[uid] || "" }) + lines.push((r.ok ? "✅ " : "❌ ") + maskUid(uid) + (remarks[uid] ? "(" + remarks[uid] + ")" : "") + ":" + r.msg) } catch (e) { failCount++ - lines.push("❌ " + maskUid(uid) + ":" + (e && e.message ? e.message : String(e))) + var emsg = e && e.message ? e.message : String(e) + var er = { uid: String(uid), ok: false, msg: emsg, time: nowIso(), startedAt: startedAt, source: source, runId: runId, error: true, remark: remarks[uid] || "" } + setLastResult(uid, er) + items.push(er) + lines.push("❌ " + maskUid(uid) + (remarks[uid] ? "(" + remarks[uid] + ")" : "") + ":" + emsg) } } + var finishedAt = nowIso() + setLastRunLog({ runId: runId, source: source, startedAt: startedAt, finishedAt: finishedAt, targetUid: targetUid || "", allUsers: !!allUsers, total: uids.length, okCount: okCount, failCount: failCount, items: items }) lines.push("汇总:成功 " + okCount + " / 失败 " + failCount + " / 总计 " + uids.length) + lines.push("记录时间:" + finishedAt + "(" + sourceName(source) + ")") return lines.join("\n") } @@ -343,6 +372,7 @@ function queryStatus(targetUid) { var store = getStore() var bind = getBindStore() var last = getLastResults() + var run = getLastRunLog() var remarks = getRemarkStore() var uids = visibleUids(store, bind, false) if (targetUid) { @@ -350,7 +380,9 @@ function queryStatus(targetUid) { uids = [targetUid] } if (!uids.length) return "当前用户还没有保存启点读书CK" - var lines = [targetUid ? "启点单账号执行情况:" : "启点执行情况:"] + var title = targetUid ? "启点单账号最近签到记录:" : "启点最近签到记录:" + var lines = [title] + if (!targetUid && run && run.finishedAt) lines.push("最近批次:" + sourceName(run.source) + " @ " + run.finishedAt + ";成功 " + (run.okCount || 0) + " / 失败 " + (run.failCount || 0) + " / 总计 " + (run.total || 0)) for (var i = 0; i < uids.length; i++) lines.push(formatStatusLine(uids[i], last, remarks)) return lines.join("\n") } @@ -373,9 +405,9 @@ function accountMenuText() { } function queryMenuText() { return [ - "启点查询", - "1. 查看最近执行情况", - "2. 查询单个账号", + "启点查询(最近签到记录)", + "1. 查看最近签到记录", + "2. 查询单个账号最近记录", "3. 立即执行签到", "q. 退出", "请回复序号" @@ -490,7 +522,7 @@ function queryMenu() { if (!uid) return reply("已退出") return reply(queryStatus(uid)) } - if (/^(3|执行|签到|立即)$/i.test(op)) return reply(signAll("", false)) + if (/^(3|执行|签到|立即)$/i.test(op)) return reply(signAll("", false, "menu")) return reply("未识别选项,已退出") } function handleDelete(arg, store, bind) { @@ -535,7 +567,7 @@ function main() { var bind = getBindStore() if (cmd === "sign" || /^(签到|sign|run)(?:\s+|$)/i.test(arg)) { var target = trim(arg.replace(/^(签到|sign|run)\s*/i, "")) - var out = signAll(target, isCronRun) + var out = signAll(target, isCronRun, isCronRun ? "cron" : "manual") if (!(isCronRun && getBool(CRON_SILENT_KEY, false) && out === "还没有保存启点读书CK")) reply(out) return } diff --git a/tests/qdreader_plugin.test.mjs b/tests/qdreader_plugin.test.mjs index cdc67fd..52226cd 100644 --- a/tests/qdreader_plugin.test.mjs +++ b/tests/qdreader_plugin.test.mjs @@ -72,10 +72,15 @@ test('manual sign and cron sign both call sign gateway then qidian checkin', () assert.match(manual.calls[0].url, /api\.120399\.xyz\/sign$/); assert.match(manual.calls[1].url, /h5\.if\.qidian\.com\/argus\/api\/v3\/checkin\/checkin$/); assert.match(manual.replies[0], /✅ 12\*\*\*45/); + assert.match(manual.replies[0], /记录时间:/); + assert.equal(JSON.parse(manual.store.qdreader_last_run_json).source, 'manual'); + assert.equal(JSON.parse(manual.store.qdreader_last_run_json).okCount, 1); + assert.equal(JSON.parse(JSON.parse(manual.store.qdreader_last_result_json)['12345']).source, 'manual'); const cron = runPlugin({ content: '', store: { ...store } }); assert.equal(cron.calls.length, 2); assert.match(cron.replies[0], /启点读书签到结果/); + assert.equal(JSON.parse(cron.store.qdreader_last_run_json).source, 'cron'); }); test('bucketGet/bucketSet fallback works when get/set are unavailable in Autman-like env', () => { @@ -136,19 +141,21 @@ test('account command shows only current user bound accounts without cookies', ( assert.doesNotMatch(r.replies[1], /QDHeader=/); }); -test('query command shows execution status not account management', () => { +test('query command shows last sign log not account management', () => { 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-b' }), qdreader_remark_json: JSON.stringify({ '12345': '主号', '67890': '副号' }), qdreader_last_result_json: JSON.stringify({ - '12345': JSON.stringify({ ok: true, msg: '签到成功', time: '2026-05-17T00:00:00.000Z' }), - '67890': JSON.stringify({ ok: false, msg: '失败', time: '2026-05-17T00:00:00.000Z' }), + '12345': JSON.stringify({ ok: true, msg: '签到成功', time: '2026-05-17T00:00:00.000Z', source: 'cron', runId: 'r1' }), + '67890': JSON.stringify({ ok: false, msg: '失败', time: '2026-05-17T00:00:00.000Z', source: 'cron', runId: 'r1' }), }), + qdreader_last_run_json: JSON.stringify({ source: 'cron', finishedAt: '2026-05-17T00:00:01.000Z', okCount: 1, failCount: 1, total: 2 }), }; const r = runPlugin({ content: '启点查询', store, userId: 'user-a', listens: ['1'] }); assert.match(r.replies[0], /启点查询/); - assert.match(r.replies[1], /启点执行情况/); + assert.match(r.replies[1], /启点最近签到记录/); + assert.match(r.replies[1], /最近批次:定时任务/); assert.match(r.replies[1], /12\*\*\*45/); assert.match(r.replies[1], /主号/); assert.match(r.replies[1], /签到成功/); @@ -162,16 +169,17 @@ test('query menu can inspect a single bound account', () => { qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a', '67890': 'user-a' }), qdreader_remark_json: JSON.stringify({ '12345': '主号', '67890': '副号' }), qdreader_last_result_json: JSON.stringify({ - '12345': JSON.stringify({ ok: true, msg: '签到成功', time: '2026-05-17T00:00:00.000Z' }), - '67890': JSON.stringify({ ok: false, msg: '失败', time: '2026-05-17T00:00:00.000Z' }), + '12345': JSON.stringify({ ok: true, msg: '签到成功', time: '2026-05-17T00:00:00.000Z', source: 'cron', runId: 'r1' }), + '67890': JSON.stringify({ ok: false, msg: '失败', time: '2026-05-17T00:00:00.000Z', source: 'cron', runId: 'r1' }), }), + qdreader_last_run_json: JSON.stringify({ source: 'cron', finishedAt: '2026-05-17T00:00:01.000Z', okCount: 1, failCount: 1, total: 2 }), }; const r = runPlugin({ content: '启点查询', store, userId: 'user-a', listens: ['2', '2'] }); assert.match(r.replies[0], /启点查询/); assert.match(r.replies[1], /请选择要查询的账号/); assert.match(r.replies[1], /1\. 12\*\*\*45/); assert.match(r.replies[1], /2\. 67\*\*\*90/); - assert.match(r.replies[2], /启点单账号执行情况/); + assert.match(r.replies[2], /启点单账号最近签到记录/); assert.match(r.replies[2], /67\*\*\*90/); assert.match(r.replies[2], /副号/); assert.doesNotMatch(r.replies[2], /12\*\*\*45/); @@ -191,6 +199,22 @@ test('account menu prompts remark after add and can edit from detail', () => { assert.equal(JSON.parse(clear.store.qdreader_remark_json)['12345'], undefined); }); + +test('failed sign exceptions are written to last execution log', () => { + const store = { + qdreader_cookie_json: JSON.stringify({ '12345': cookie }), + qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }), + }; + const r = runPlugin({ content: '启点签到', store, userId: 'user-a', requests: [new Error('sign api down'), new Error('sign api down')] }); + assert.match(r.replies[0], /sign api down/); + const lastRun = JSON.parse(r.store.qdreader_last_run_json); + assert.equal(lastRun.failCount, 1); + assert.equal(lastRun.items[0].error, true); + const lastResult = JSON.parse(JSON.parse(r.store.qdreader_last_result_json)['12345']); + assert.equal(lastResult.ok, false); + assert.equal(lastResult.msg, 'sign api down'); +}); + test('legacy ck query points to separated commands', () => { const r = runPlugin({ content: '启点ck 查询' }); assert.match(r.replies[0], /请使用一级菜单:启点账号 或 启点查询/);