fix(qdreader): use single cron for archive and sign

This commit is contained in:
2026-05-19 16:43:05 +08:00
parent 557b5ae9c2
commit 36717c3887
5 changed files with 29 additions and 33 deletions
+1 -1
View File
@@ -619,7 +619,7 @@ QDReader 这次踩坑说明:账号 ID 识别成功,不代表 Autman 会话
- `xxx查询` 的数据来源必须写清楚:如果没有实时接口,就命名为“最近签到记录/最后执行日志”,读取 `last_run_json``last_result_json`,不要让用户误以为是实时查询。 - `xxx查询` 的数据来源必须写清楚:如果没有实时接口,就命名为“最近签到记录/最后执行日志”,读取 `last_run_json``last_result_json`,不要让用户误以为是实时查询。
- 执行动作要同时写批次日志和单账号日志;成功、业务失败、请求异常都要落日志,异常不能只显示在当次回复里。 - 执行动作要同时写批次日志和单账号日志;成功、业务失败、请求异常都要落日志,异常不能只显示在当次回复里。
- 如果插件需要按天判断执行情况,不要把归档和运行任务放在同一个 cron 入口。优先在同一个插件内声明两个独立 cron,并在入口根据触发时间/触发标识分流:业务时间 cron(如 `15 7 * * *`)只执行签到/业务动作并写 `last_run_json` / `last_result_json`;零点归档 cron(如 `0 0 * * *`)只把上一份批次日志按日期写入 `archive_json`,再清空 `last_run_json` / `last_result_json`,绝不执行签到/业务动作。若只能按当前本机时间识别系统触发,归档窗口要覆盖合理调度延迟风险(例如 00:00-00:04 都归档),避免 00:00 cron 短延迟后误走业务签到,同时不要让窗口过长影响业务语义。只有在当前 Autman 运行环境确认不可靠支持单插件多 `[cron]` 时,才退而拆成两个插件文件。查询菜单优先展示当日/当前批次;当日日志为空时回退最近归档,按归档结果反馈成功/失败。 - 如果插件需要按天判断执行情况,不要把归档和运行任务放在同一个 cron 入口。优先一个插件文件;如果两个动作只相差小时、分钟相同,可合并为一个 cron(如 QDReader 用 `1 0,7 * * *`),入口按小时分流:0 点分支只归档上一份批次日志到 `archive_json` 并清空 `last_run_json` / `last_result_json`7 点分支只执行签到/业务动作并写 `last_run_json` / `last_result_json`。如果分钟不同或无法安全按小时分流,再使用多个独立 cron;只有在当前 Autman 运行环境确认不可靠支持单插件多 `[cron]` 时,才退而拆成两个插件文件。查询菜单优先展示当日/当前批次;当日日志为空时回退最近归档,按归档结果反馈成功/失败。
- 普通回复永远不要输出完整 Cookie;只展示脱敏 UID、备注、Cookie key 摘要或长度。 - 普通回复永远不要输出完整 Cookie;只展示脱敏 UID、备注、Cookie key 摘要或长度。
- 高风险的导出/批量导入功能默认不做,除非 BOSS 明确需要。 - 高风险的导出/批量导入功能默认不做,除非 BOSS 明确需要。
- Node VM 测试应模拟:`sender.reply``sender.listen``sender.getUserID``get/set``bucketGet/bucketSet``request` - Node VM 测试应模拟:`sender.reply``sender.listen``sender.getUserID``get/set``bucketGet/bucketSet``request`
+8 -10
View File
@@ -4,18 +4,17 @@
//[author: Hermes] //[author: Hermes]
//[service: BOSS] //[service: BOSS]
//[class: 工具类] //[class: 工具类]
//[version: 2.5.4] //[version: 2.5.5]
//[platform: web,qq,wx,tg,tb,fs,we] //[platform: web,qq,wx,tg,tb,fs,we]
//[public: false] //[public: false]
//[price: 0] //[price: 0]
//[disable: false] //[disable: false]
//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”和每日 07:15 定时签到,每日 00:00 独立归档并清空日志;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。] //[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时使用 `1 0,7 * * *`,每日 00:01 归档并清空日志、每日 07:01 签到;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。]
//[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$] //[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$]
//[rule: ^qdreader(.*)$] //[rule: ^qdreader(.*)$]
//[admin: true] //[admin: true]
//[priority: 600] //[priority: 600]
//[cron: 0 0 * * *] //[cron: 1 0,7 * * *]
//[cron: 15 7 * * *]
var BUCKET = "hermes_qidian" var BUCKET = "hermes_qidian"
@@ -261,7 +260,7 @@ function contentText() {
} }
function usage() { function usage() {
return [ return [
"启点读书签到插件 v2.5.4", "启点读书签到插件 v2.5.5",
"【一级菜单】", "【一级菜单】",
"账号管理:启点账号", "账号管理:启点账号",
"执行查询:启点查询", "执行查询:启点查询",
@@ -271,7 +270,7 @@ function usage() {
"指定签到:启点签到 <uid>", "指定签到:启点签到 <uid>",
"【配置】", "【配置】",
"签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API, "签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
"定时:每日 00:00 只归档并清空日志;每日 07:15 只自动签到;QX 只负责抓 cookie。" "定时:每日 00:01 只归档并清空日志;每日 07:01 只自动签到;QX 只负责抓 cookie。"
].join("\n") ].join("\n")
} }
function stripCommand(text) { function stripCommand(text) {
@@ -455,10 +454,9 @@ function latestArchiveForVisible(uids) {
function cronMode() { function cronMode() {
var d = new Date() var d = new Date()
var h = d.getHours ? d.getHours() : 7 var h = d.getHours ? d.getHours() : 7
var m = d.getMinutes ? d.getMinutes() : 15 // Single cron: 1 0,7 * * *
// Autman cron can be delayed by scheduler/load; allow a 5-minute archive-only window after 00:00. // 00:01 system run archives only; 07:01 system run signs only.
// Outside 00:00-00:04, system-triggered runs follow the business sign path. if (h === 0) return "archive"
if (h === 0 && m < 5) return "archive"
return "sign" return "sign"
} }
function formatArchiveReply(ret) { function formatArchiveReply(ret) {
+5 -5
View File
@@ -1,13 +1,13 @@
# 启点读书 QDReader 签到 Autman 插件 # 启点读书 QDReader 签到 Autman 插件
QDReader/起点读书 Autman 签到插件。用户侧触发词统一使用“启点”,避免平台关键词麻烦;QX 只负责抓取 Cookie。账号管理、执行查询、签到和日志归档都在同一个 Autman 插件内完成;业务上仍保持两个独立定时:每日 00:00 只归档清日志,每日 07:15 只执行签到。 QDReader/起点读书 Autman 签到插件。用户侧触发词统一使用“启点”,避免平台关键词麻烦;QX 只负责抓取 Cookie。账号管理、执行查询、签到和日志归档都在同一个 Autman 插件内完成;业务上使用一个 cron 表达式 `1 0,7 * * *`:每日 00:01 只归档清日志,每日 07:01 只执行签到。
## 文件位置 ## 文件位置
正式插件文件: 正式插件文件:
```text ```text
plugins/qdreader/qdreader_sign_autman.js # 单插件:00:00 归档清日志;07:15 运行签到 plugins/qdreader/qdreader_sign_autman.js # 单插件单 cron00:01 归档清日志;07:01 运行签到
``` ```
实机 JS 导入副本: 实机 JS 导入副本:
@@ -132,9 +132,9 @@ set(key, value)
3. `POST https://h5.if.qidian.com/argus/api/v3/checkin/checkin` 完成签到。 3. `POST https://h5.if.qidian.com/argus/api/v3/checkin/checkin` 完成签到。
4. 写入单账号当日结果 `qdreader_last_result_json`,包括 `ok/msg/time/source/runId` 4. 写入单账号当日结果 `qdreader_last_result_json`,包括 `ok/msg/time/source/runId`
5. 写入当日批次日志 `qdreader_last_run_json`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。 5. 写入当日批次日志 `qdreader_last_run_json`,包括触发来源、开始/结束时间、成功/失败/总数和每账号结果;即使账号执行异常也会记录失败日志。
6. 插件内有两个 cron`0 0 * * *``15 7 * * *` 6. 插件内只有一个 cron`1 0,7 * * *`
7. 每日 00:00 触发时只做日志归档;为防止调度器延迟,系统触发发生在 00:00-00:04 都按归档处理:把上一份 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;不请求签名网关、不执行签到。 7. 每日 00:01 触发时只做日志归档;系统触发按小时分流,0 点分支归档:把上一份 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;不请求签名网关、不执行签到。
8. 每日 07:15 触发时只执行签到并反馈结果,不做归档。 8. 每日 07:01 触发时只执行签到并反馈结果,不做归档。
9. 汇总每个账号签到结果。 9. 汇总每个账号签到结果。
## 查询数据来源 ## 查询数据来源
+8 -10
View File
@@ -4,18 +4,17 @@
//[author: Hermes] //[author: Hermes]
//[service: BOSS] //[service: BOSS]
//[class: 工具类] //[class: 工具类]
//[version: 2.5.4] //[version: 2.5.5]
//[platform: web,qq,wx,tg,tb,fs,we] //[platform: web,qq,wx,tg,tb,fs,we]
//[public: false] //[public: false]
//[price: 0] //[price: 0]
//[disable: false] //[disable: false]
//[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”和每日 07:15 定时签到,每日 00:00 独立归档并清空日志;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。] //[description: 启点读书(QDReader) Autman 签到插件:保存 QX 抓到的 H5 Cookie,支持手动“启点签到”;定时使用 `1 0,7 * * *`,每日 00:01 归档并清空日志、每日 07:01 签到;带独立 bucket、多用户隔离、请求重试、签名响应解析、结果摘要和安全脱敏。]
//[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$] //[rule: ^启点(账号|查询|ck|CK|cookie|Cookie|签到|读书|归档|日志归档)(.*)$]
//[rule: ^qdreader(.*)$] //[rule: ^qdreader(.*)$]
//[admin: true] //[admin: true]
//[priority: 600] //[priority: 600]
//[cron: 0 0 * * *] //[cron: 1 0,7 * * *]
//[cron: 15 7 * * *]
var BUCKET = "hermes_qidian" var BUCKET = "hermes_qidian"
@@ -261,7 +260,7 @@ function contentText() {
} }
function usage() { function usage() {
return [ return [
"启点读书签到插件 v2.5.4", "启点读书签到插件 v2.5.5",
"【一级菜单】", "【一级菜单】",
"账号管理:启点账号", "账号管理:启点账号",
"执行查询:启点查询", "执行查询:启点查询",
@@ -271,7 +270,7 @@ function usage() {
"指定签到:启点签到 <uid>", "指定签到:启点签到 <uid>",
"【配置】", "【配置】",
"签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API, "签名网关:Autman 参数 hermes_qidian.qdreader_sign_api,默认 " + DEFAULT_SIGN_API,
"定时:每日 00:00 只归档并清空日志;每日 07:15 只自动签到;QX 只负责抓 cookie。" "定时:每日 00:01 只归档并清空日志;每日 07:01 只自动签到;QX 只负责抓 cookie。"
].join("\n") ].join("\n")
} }
function stripCommand(text) { function stripCommand(text) {
@@ -455,10 +454,9 @@ function latestArchiveForVisible(uids) {
function cronMode() { function cronMode() {
var d = new Date() var d = new Date()
var h = d.getHours ? d.getHours() : 7 var h = d.getHours ? d.getHours() : 7
var m = d.getMinutes ? d.getMinutes() : 15 // Single cron: 1 0,7 * * *
// Autman cron can be delayed by scheduler/load; allow a 5-minute archive-only window after 00:00. // 00:01 system run archives only; 07:01 system run signs only.
// Outside 00:00-00:04, system-triggered runs follow the business sign path. if (h === 0) return "archive"
if (h === 0 && m < 5) return "archive"
return "sign" return "sign"
} }
function formatArchiveReply(ret) { function formatArchiveReply(ret) {
+7 -7
View File
@@ -266,7 +266,7 @@ test('midnight cron archives previous run log and clears daily logs without runn
}); });
test('five-minute delayed midnight cron still archives instead of signing', () => { test('single cron midnight branch archives without signing', () => {
const previousRun = { const previousRun = {
runId: '20260518_cron', runId: '20260518_cron',
source: 'cron', source: 'cron',
@@ -282,20 +282,20 @@ test('five-minute delayed midnight cron still archives instead of signing', () =
qdreader_last_run_json: JSON.stringify(previousRun), qdreader_last_run_json: JSON.stringify(previousRun),
qdreader_last_result_json: JSON.stringify({ '12345': JSON.stringify({ ok: true, msg: '昨日签到成功' }) }), qdreader_last_result_json: JSON.stringify({ '12345': JSON.stringify({ ok: true, msg: '昨日签到成功' }) }),
}; };
const r = runPlugin({ content: '', store, userId: 'cron', globals: { Date: fixedDateClass('2026-05-19T00:04:00.000+08:00') } }); const r = runPlugin({ content: '', store, userId: 'cron', globals: { Date: fixedDateClass('2026-05-19T00:01:00.000+08:00') } });
assert.equal(r.calls.length, 0); assert.equal(r.calls.length, 0);
assert.match(r.replies[0], /日志归档完成/); assert.match(r.replies[0], /日志归档完成/);
assert.equal(JSON.parse(r.store.qdreader_archive_json)['2026-05-18'].runId, '20260518_cron'); assert.equal(JSON.parse(r.store.qdreader_archive_json)['2026-05-18'].runId, '20260518_cron');
}); });
test('after five-minute midnight window cron follows sign path', () => { test('single cron morning branch signs without archiving', () => {
const store = { const store = {
qdreader_cookie_json: JSON.stringify({ '12345': cookie }), qdreader_cookie_json: JSON.stringify({ '12345': cookie }),
qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }), qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }),
qdreader_last_run_json: JSON.stringify({ source: 'cron', finishedAt: '2026-05-18T07:15:02.000Z', total: 1, okCount: 1, failCount: 0, items: [{ uid: '12345', ok: true }] }), qdreader_last_run_json: JSON.stringify({ source: 'cron', finishedAt: '2026-05-18T07:15:02.000Z', total: 1, okCount: 1, failCount: 0, items: [{ uid: '12345', ok: true }] }),
}; };
const r = runPlugin({ content: '', store, userId: 'cron', globals: { Date: fixedDateClass('2026-05-19T00:05:00.000+08:00') } }); 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, 2);
assert.match(r.replies[0], /启点读书签到结果/); assert.match(r.replies[0], /启点读书签到结果/);
assert.equal(r.store.qdreader_archive_json, undefined); assert.equal(r.store.qdreader_archive_json, undefined);
@@ -346,7 +346,7 @@ test('manual sign and cron sign both call sign gateway then qidian checkin', ()
assert.equal(payload.qdh, 'qdh'); assert.equal(payload.qdh, 'qdh');
assert.equal(payload.qdheader, 'MTIzNDV8eA=='); assert.equal(payload.qdheader, 'MTIzNDV8eA==');
const cron = runPlugin({ content: '', store: { ...store }, globals: { Date: fixedDateClass('2026-05-19T07:15:00.000+08:00') } }); 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, 2);
assert.match(cron.replies[0], /启点读书签到结果/); assert.match(cron.replies[0], /启点读书签到结果/);
assert.equal(JSON.parse(cron.store.qdreader_last_run_json).source, 'cron'); assert.equal(JSON.parse(cron.store.qdreader_last_run_json).source, 'cron');
@@ -527,7 +527,7 @@ test('manual sign only runs current user accounts while cron runs all accounts',
const manual = runPlugin({ content: '启点签到', store: { ...store }, userId: 'user-a' }); const manual = runPlugin({ content: '启点签到', store: { ...store }, userId: 'user-a' });
assert.equal(manual.calls.length, 2); assert.equal(manual.calls.length, 2);
assert.match(manual.replies[0], /总计 1/); assert.match(manual.replies[0], /总计 1/);
const cron = runPlugin({ content: '', store: { ...store }, userId: 'cron', globals: { Date: fixedDateClass('2026-05-19T07:15:00.000+08:00') } }); 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, 4);
assert.match(cron.replies[0], /总计 2/); assert.match(cron.replies[0], /总计 2/);
}); });
@@ -541,7 +541,7 @@ test('unbound global cookies are not visible or usable by normal users', () => {
assert.equal(manual.calls.length, 0); assert.equal(manual.calls.length, 0);
assert.match(manual.replies[0], /还没有保存启点读书CK/); assert.match(manual.replies[0], /还没有保存启点读书CK/);
const cron = runPlugin({ content: '', store: { ...store }, userId: 'cron', globals: { Date: fixedDateClass('2026-05-19T07:15:00.000+08:00') } }); 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, 2);
assert.match(cron.replies[0], /总计 1/); assert.match(cron.replies[0], /总计 1/);
}); });