diff --git a/docs/autman-plugin-rules.md b/docs/autman-plugin-rules.md index e96eb08..7bed255 100644 --- a/docs/autman-plugin-rules.md +++ b/docs/autman-plugin-rules.md @@ -636,3 +636,10 @@ QDReader 这次踩坑说明:账号 ID 识别成功,不代表 Autman 会话 - 定时/手动签到执行时按 uid 读取自己的任务配置;一个账号开启广告任务不影响其它账号。 - 删除单个账号或清空当前用户账号时,同步删除该 uid 的任务配置,避免残留配置误作用于重新添加的账号。 - 旧全局开关 key 可留作历史数据,但不要作为运行依据;测试必须覆盖“一个账号开启,另一个账号不执行”的隔离场景。 + + +QDReader 签到结果判定补充: + +- 上游返回“今日已签到/已经签到/重复签到”应视为成功,不应把账号计入失败。 +- 签名网关返回 `url 不在允许的接口列表内` 说明本地可选任务接口尚未被网关白名单支持;对这类可选任务应显示为“已跳过”,不要让整个账号失败。 +- 保留原始错误到任务对象的 `rawMsg` 即可,聊天摘要只显示短的跳过原因,避免刷屏。 diff --git a/js/qdreader_sign_autman.js b/js/qdreader_sign_autman.js index e31f142..f17438b 100644 --- a/js/qdreader_sign_autman.js +++ b/js/qdreader_sign_autman.js @@ -4,7 +4,7 @@ //[author: Hermes] //[service: BOSS] //[class: 工具类] -//[version: 2.8.0] +//[version: 2.9.0] //[platform: web,qq,wx,tg,tb,fs,we] //[public: false] //[price: 0] @@ -276,7 +276,7 @@ function contentText() { } function usage() { return [ - "启点读书签到插件 v2.8.0", + "启点读书签到插件 v2.9.0", "【一级菜单】", "账号管理:启点账号", "执行查询:启点查询", @@ -413,6 +413,17 @@ 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 successLikeMsg(msg) { + msg = String(msg || "") + return /今日已签到|已经签到|已签到|重复签到|已领取|已经领取|已完成|完成/.test(msg) +} +function taskResultOk(obj) { + return resultOk(obj) || successLikeMsg(resultMsg(obj)) +} +function isWhitelistBlockedError(msg) { + msg = String(msg || "") + return /url\s*不在允许的接口列表内|不在允许的接口列表|参数异常.*允许的接口/.test(msg) +} function pad2(n) { n = String(n); return n.length < 2 ? "0" + n : n } function beijingDate(dateLike) { var d = dateLike ? new Date(dateLike) : new Date() @@ -562,7 +573,7 @@ function taskMenuForUid(uid) { 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 ok = taskResultOk(res) var msg = resultMsg(res) || (ok ? "完成" : ("失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) return { name: task.label, ok: !!ok, msg: String(msg || ""), path: task.path } } @@ -570,20 +581,24 @@ function doTaskChain(uid, cookie, source, runId) { var startedAt = nowIso() var details = [] var checkin = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) - var ok = resultOk(checkin) + var ok = taskResultOk(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 (!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 }) } + catch (e) { + var tmsg = e && e.message ? e.message : String(e) + if (isWhitelistBlockedError(tmsg)) details.push({ name: task.label, ok: true, skipped: true, msg: "签名网关暂不支持该接口,已跳过", rawMsg: tmsg, path: task.path }) + else details.push({ name: task.label, ok: false, msg: tmsg, 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) + for (var j = 0; j < details.length; j++) if (!details[j].ok && !details[j].skipped) 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) + ")" : "")) + for (var k = 0; k < details.length; k++) detailMsg.push(details[k].name + (details[k].skipped ? "⏭" : (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) diff --git a/plugins/qdreader/README.md b/plugins/qdreader/README.md index a7db210..7402b50 100644 --- a/plugins/qdreader/README.md +++ b/plugins/qdreader/README.md @@ -156,7 +156,8 @@ 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_task_pref_json` 后追加完全体任务链路: +4. “今日已签到/已经签到/重复签到”等签到响应按成功处理,避免重复执行时误计失败。 +5. 按账号读取 `qdreader_task_pref_json` 后追加完全体任务链路: - 固定执行领取奖励状态查询:`GET /argus/api/v3/checkin/getcurrentweekcheckininfo`; - 可选激励任务:`GET /argus/api/v2/video/adv/mainPage`; - 可选每日任务:`GET /argus/api/v2/video/adv/packageList`; @@ -164,12 +165,13 @@ set(key, value) - 可选每周兑换:`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. 汇总每个账号签到/完整任务结果,并在回复中提示归档日期。 +6. 对签名网关返回 `url 不在允许的接口列表内` 的可选任务,记为“已跳过”而不是账号失败,用于兼容网关白名单尚未覆盖的上游接口。 +7. 写入单账号当日结果 `qdreader_last_result_json`,包括 `ok/msg/time/source/runId/tasks`。 +8. 写入当日批次日志 `qdreader_last_run_json`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。 +9. 插件内只有一个 cron:`1 7 * * *`。 +10. 每日 07:01 触发时执行签到链路。 +11. 签到运行结束后,马上把本次 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;手动签到同样会立即归档。 +12. 汇总每个账号签到/完整任务结果,并在回复中提示归档日期。 ## 查询数据来源 diff --git a/plugins/qdreader/qdreader_sign_autman.js b/plugins/qdreader/qdreader_sign_autman.js index e31f142..f17438b 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.8.0] +//[version: 2.9.0] //[platform: web,qq,wx,tg,tb,fs,we] //[public: false] //[price: 0] @@ -276,7 +276,7 @@ function contentText() { } function usage() { return [ - "启点读书签到插件 v2.8.0", + "启点读书签到插件 v2.9.0", "【一级菜单】", "账号管理:启点账号", "执行查询:启点查询", @@ -413,6 +413,17 @@ 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 successLikeMsg(msg) { + msg = String(msg || "") + return /今日已签到|已经签到|已签到|重复签到|已领取|已经领取|已完成|完成/.test(msg) +} +function taskResultOk(obj) { + return resultOk(obj) || successLikeMsg(resultMsg(obj)) +} +function isWhitelistBlockedError(msg) { + msg = String(msg || "") + return /url\s*不在允许的接口列表内|不在允许的接口列表|参数异常.*允许的接口/.test(msg) +} function pad2(n) { n = String(n); return n.length < 2 ? "0" + n : n } function beijingDate(dateLike) { var d = dateLike ? new Date(dateLike) : new Date() @@ -562,7 +573,7 @@ function taskMenuForUid(uid) { 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 ok = taskResultOk(res) var msg = resultMsg(res) || (ok ? "完成" : ("失败" + (resultCode(res) !== undefined ? " code=" + resultCode(res) : ""))) return { name: task.label, ok: !!ok, msg: String(msg || ""), path: task.path } } @@ -570,20 +581,24 @@ function doTaskChain(uid, cookie, source, runId) { var startedAt = nowIso() var details = [] var checkin = qidianRequest(CHECKIN_PATH, "POST", {}, cookie) - var ok = resultOk(checkin) + var ok = taskResultOk(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 (!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 }) } + catch (e) { + var tmsg = e && e.message ? e.message : String(e) + if (isWhitelistBlockedError(tmsg)) details.push({ name: task.label, ok: true, skipped: true, msg: "签名网关暂不支持该接口,已跳过", rawMsg: tmsg, path: task.path }) + else details.push({ name: task.label, ok: false, msg: tmsg, 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) + for (var j = 0; j < details.length; j++) if (!details[j].ok && !details[j].skipped) 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) + ")" : "")) + for (var k = 0; k < details.length; k++) detailMsg.push(details[k].name + (details[k].skipped ? "⏭" : (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) diff --git a/tests/qdreader_plugin.test.mjs b/tests/qdreader_plugin.test.mjs index 151208c..26c0430 100644 --- a/tests/qdreader_plugin.test.mjs +++ b/tests/qdreader_plugin.test.mjs @@ -640,3 +640,61 @@ test('account task menu supports one-click enable and disable all optional tasks assert.equal(disabled.chapterCard, false); assert.equal(disabled.messageBox, false); }); + + +test('already signed checkin is treated as success', () => { + 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: [ + { body: { code: 0, data: { headers: { 'QD-Sign': 'sig' } } } }, + { statusCode: 200, body: { code: 1, msg: '今日已签到' } }, + { body: { code: 0, data: { headers: { 'QD-Sign': 'sig' } } } }, + { statusCode: 200, body: { code: 0, msg: '完成' } }, + ], + }); + assert.match(r.replies[0], /签到结果✅\(今日已签到\)/); + assert.match(r.replies[0], /汇总:成功 1 \/ 失败 0 \/ 总计 1/); + const archive = JSON.parse(r.store.qdreader_archive_json); + const log = archive[Object.keys(archive)[0]]; + assert.equal(log.okCount, 1); + assert.equal(log.failCount, 0); +}); + +test('sign gateway whitelist blocked optional tasks are skipped not counted as account failure', () => { + 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': { extraAdv: true, lottery: true, chapterCard: true, messageBox: true } }), + }; + const blocked = { body: { code: 444, message: '参数异常: url 不在允许的接口列表内' } }; + const r = runPlugin({ + content: '启点签到', + store, + userId: 'user-a', + requests: [ + { body: { code: 0, data: { headers: { 'QD-Sign': 'sig' } } } }, + { statusCode: 200, body: { code: 1, msg: '今日已签到' } }, + { body: { code: 0, data: { headers: { 'QD-Sign': 'sig' } } } }, + { statusCode: 200, body: { code: 0, msg: '完成' } }, + blocked, + blocked, + blocked, + blocked, + ], + }); + assert.match(r.replies[0], /每日任务⏭\(签名网关暂不支持该接口,已跳过\)/); + assert.match(r.replies[0], /每日抽奖⏭/); + assert.match(r.replies[0], /章节卡片⏭/); + assert.match(r.replies[0], /大咖荐书⏭/); + assert.match(r.replies[0], /汇总:成功 1 \/ 失败 0 \/ 总计 1/); + const archive = JSON.parse(r.store.qdreader_archive_json); + const log = archive[Object.keys(archive)[0]]; + assert.equal(log.okCount, 1); + assert.equal(log.failCount, 0); +});