fix(qdreader): harden midnight cron dispatch
This commit is contained in:
@@ -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`,绝不执行签到/业务动作。只有在当前 Autman 运行环境确认不可靠支持单插件多 `[cron]` 时,才退而拆成两个插件文件。查询菜单优先展示当日/当前批次;当日日志为空时回退最近归档,按归档结果反馈成功/失败。
|
- 如果插件需要按天判断执行情况,不要把归档和运行任务放在同一个 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:59 都归档),避免 00:00 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`。
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//[author: Hermes]
|
//[author: Hermes]
|
||||||
//[service: BOSS]
|
//[service: BOSS]
|
||||||
//[class: 工具类]
|
//[class: 工具类]
|
||||||
//[version: 2.5.2]
|
//[version: 2.5.3]
|
||||||
//[platform: web,qq,wx,tg,tb,fs,we]
|
//[platform: web,qq,wx,tg,tb,fs,we]
|
||||||
//[public: false]
|
//[public: false]
|
||||||
//[price: 0]
|
//[price: 0]
|
||||||
@@ -261,7 +261,7 @@ function contentText() {
|
|||||||
}
|
}
|
||||||
function usage() {
|
function usage() {
|
||||||
return [
|
return [
|
||||||
"启点读书签到插件 v2.5.2",
|
"启点读书签到插件 v2.5.3",
|
||||||
"【一级菜单】",
|
"【一级菜单】",
|
||||||
"账号管理:启点账号",
|
"账号管理:启点账号",
|
||||||
"执行查询:启点查询",
|
"执行查询:启点查询",
|
||||||
@@ -455,8 +455,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
|
// Autman cron can be delayed by scheduler/load; any system-triggered run during 00:00-00:59 is treated as archive-only.
|
||||||
if (h === 0 && m < 10) return "archive"
|
// This avoids a dangerous midnight sign if the 00:00 archive cron is not executed exactly at minute 0.
|
||||||
|
if (h === 0) return "archive"
|
||||||
return "sign"
|
return "sign"
|
||||||
}
|
}
|
||||||
function formatArchiveReply(ret) {
|
function formatArchiveReply(ret) {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ set(key, value)
|
|||||||
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:`0 0 * * *` 和 `15 7 * * *`。
|
||||||
7. 每日 00:00 触发时只做日志归档:把上一份 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;不请求签名网关、不执行签到。
|
7. 每日 00:00 触发时只做日志归档;为防止调度器延迟,系统触发发生在 00:00-00:59 都按归档处理:把上一份 `qdreader_last_run_json` 按完成日期写入 `qdreader_archive_json`,再清空 `qdreader_last_result_json` / `qdreader_last_run_json`;不请求签名网关、不执行签到。
|
||||||
8. 每日 07:15 触发时只执行签到并反馈结果,不做归档。
|
8. 每日 07:15 触发时只执行签到并反馈结果,不做归档。
|
||||||
9. 汇总每个账号签到结果。
|
9. 汇总每个账号签到结果。
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//[author: Hermes]
|
//[author: Hermes]
|
||||||
//[service: BOSS]
|
//[service: BOSS]
|
||||||
//[class: 工具类]
|
//[class: 工具类]
|
||||||
//[version: 2.5.2]
|
//[version: 2.5.3]
|
||||||
//[platform: web,qq,wx,tg,tb,fs,we]
|
//[platform: web,qq,wx,tg,tb,fs,we]
|
||||||
//[public: false]
|
//[public: false]
|
||||||
//[price: 0]
|
//[price: 0]
|
||||||
@@ -261,7 +261,7 @@ function contentText() {
|
|||||||
}
|
}
|
||||||
function usage() {
|
function usage() {
|
||||||
return [
|
return [
|
||||||
"启点读书签到插件 v2.5.2",
|
"启点读书签到插件 v2.5.3",
|
||||||
"【一级菜单】",
|
"【一级菜单】",
|
||||||
"账号管理:启点账号",
|
"账号管理:启点账号",
|
||||||
"执行查询:启点查询",
|
"执行查询:启点查询",
|
||||||
@@ -455,8 +455,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
|
// Autman cron can be delayed by scheduler/load; any system-triggered run during 00:00-00:59 is treated as archive-only.
|
||||||
if (h === 0 && m < 10) return "archive"
|
// This avoids a dangerous midnight sign if the 00:00 archive cron is not executed exactly at minute 0.
|
||||||
|
if (h === 0) return "archive"
|
||||||
return "sign"
|
return "sign"
|
||||||
}
|
}
|
||||||
function formatArchiveReply(ret) {
|
function formatArchiveReply(ret) {
|
||||||
|
|||||||
@@ -265,6 +265,29 @@ test('midnight cron archives previous run log and clears daily logs without runn
|
|||||||
assert.deepEqual(JSON.parse(r.store.qdreader_last_result_json), {});
|
assert.deepEqual(JSON.parse(r.store.qdreader_last_result_json), {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('delayed midnight cron still archives instead of signing', () => {
|
||||||
|
const previousRun = {
|
||||||
|
runId: '20260518_cron',
|
||||||
|
source: 'cron',
|
||||||
|
finishedAt: '2026-05-18T07:15:02.000Z',
|
||||||
|
total: 1,
|
||||||
|
okCount: 1,
|
||||||
|
failCount: 0,
|
||||||
|
items: [{ uid: '12345', ok: true, msg: '昨日签到成功' }],
|
||||||
|
};
|
||||||
|
const store = {
|
||||||
|
qdreader_cookie_json: JSON.stringify({ '12345': cookie }),
|
||||||
|
qdreader_user_bind_json: JSON.stringify({ '12345': 'user-a' }),
|
||||||
|
qdreader_last_run_json: JSON.stringify(previousRun),
|
||||||
|
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:23:00.000+08:00') } });
|
||||||
|
assert.equal(r.calls.length, 0);
|
||||||
|
assert.match(r.replies[0], /日志归档完成/);
|
||||||
|
assert.equal(JSON.parse(r.store.qdreader_archive_json)['2026-05-18'].runId, '20260518_cron');
|
||||||
|
});
|
||||||
|
|
||||||
test('query falls back to archived sign results after daily log was cleared', () => {
|
test('query falls back to archived sign results after daily log was cleared', () => {
|
||||||
const archiveRun = {
|
const archiveRun = {
|
||||||
source: 'cron',
|
source: 'cron',
|
||||||
|
|||||||
Reference in New Issue
Block a user