docs(runbook): scheduled jobs registry, board and agent checks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d800987403
commit
a6402c3aed
1 file changed
+83
@@ -0,0 +1,83 @@
|
||||
# 定时任务与看板
|
||||
|
||||
所有周期任务都登记在 `scheduled_job` 表里,由 io worker 上唯一的调度器按表驱动。控制台「定时任务」页(`/console/schedules`)可以查看和调整。
|
||||
|
||||
## 组成
|
||||
|
||||
| 部分 | 位置 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 注册 API | `apps/server/src/modules/schedules/registry.ts` | 模块在 `start()` 里调用 `registerScheduledJob(ctx, { key, defaults, run })` |
|
||||
| 调度器 | `modules/schedules/scheduler.ts` | 只在 `worker`(io 组)或 `all` 角色运行。多个实例靠 `pg_try_advisory_lock` 选出一个执行者,锁挂在一条专用连接上 |
|
||||
| 表 | 迁移 `0028_scheduled_jobs` | `scheduled_job`(定义、状态、计数)和 `scheduled_job_run`(运行历史,每个任务保留最近 50 条) |
|
||||
| 端点 | `Api.schedules.*` | 全部是 admin scope,只接受控制台会话,API Key 调用返回 403 |
|
||||
|
||||
`modules/schedules` 必须排在 `SERVER_MODULES` 最后。不过定义在任何时候登记都行,调度器每一轮发现注册表版本变了,会重新同步一次。
|
||||
|
||||
## 已登记的任务
|
||||
|
||||
| key | 名称 | 分组 | 默认周期 | 执行内容 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `feeds.poll` | Feed 轮询 | 采集 | 30 秒 | 扫描到期的 Feed,发 `feed.due`。每个 Feed 仍按自己的 `poll_interval_sec` 判断是否到期 |
|
||||
| `sites.snapshot` | 站点快照 | 采集 | 30 秒 | 扫描到周期的站点,入队 `snapshotSite` |
|
||||
| `identity.review` | 待识别复查 | 采集 | 30 秒 | 对 `next_review_at` 已到的条目入队 `resolveObservation` |
|
||||
| `subscriptions.search` | 订阅周期搜索 | 采集 | 同 qB 同步 | `sweepSubscriptionSearch` |
|
||||
| `downloads.sync` | qB 同步 | 下载 | `cache.checkIntervalSec`(60 秒) | `syncDownloads`,包括按阈值暂停和恢复。所有下载器都连不上时记为失败 |
|
||||
| `downloads.orchestrate` | 补提交与补入库 | 下载 | 同上 | `sweepReservedPlans` 和补入队 `verifyAndImport` |
|
||||
| `slots.reevaluate` | 到期重评 | 下载 | 同上 | 处理 `next_evaluate_at` 已到的收录位 |
|
||||
| `subscriptions.metadata` | 总集数刷新 | 整理 | 同上 | `sweepMetadataRefresh` |
|
||||
| `library.reconcile` | 资产对账 | 整理 | 6 小时 | 入队 `reconcileLibrary`,幂等键是 `auto:<时间槽>` |
|
||||
| `cache.threshold` | 缓存阈值检查 | 维护 | 同 qB 同步 | 超过硬阈值时入队 `cleanupDownloadCache` |
|
||||
| `trash.purge` | 回收站清理 | 维护 | 24 小时 | 入队 `purgeTrash` |
|
||||
| `maintain.backup` | 备份 | 维护 | 每天 03:30(占位) | 未配置。备份目前由宿主机上的 `scripts/backup.sh` 执行 |
|
||||
| `check.pending` | 待处理巡检 | Agent | 6 小时,默认关闭 | 只读。有未关闭事项时才会运行 |
|
||||
| `check.downloads` | 下载诊断 | Agent | 2 小时,默认关闭 | 默认只读。打开 `allowRecover` 后,在授权范围内重试或重新校验 |
|
||||
| `check.weekly` | 订阅与站点周报 | Agent | 7 天,默认关闭 | 只读。结果会写成一条 info 级待处理事项 |
|
||||
|
||||
补充几点:
|
||||
|
||||
- 自动化总开关关闭,或处于只读对账模式时,编排类任务会记为 `skipped`,不执行。
|
||||
- 各项默认周期和改造前的计时器一致。测试时可以通过模块参数调短,比如 `syncIntervalMs`、`schedulerIntervalMs`。
|
||||
- auth-settings 的会话清理(每小时)目前还在模块里自建计时器,尚未登记。
|
||||
- Pi 的 token 刷新(`refreshPiTokens`)目前也还是自建计时器,待 W7-A 登记为 `pi.token_refresh`。
|
||||
|
||||
## 调度语义
|
||||
|
||||
- **到期**:满足 `enabled and next_run_at <= now()`,或者 `run_requested_at` 不为空。同一个任务不会重入,不同任务并行执行。
|
||||
- **下次运行时间**:interval 类型取「本次结束时间 + 周期」,和改造前的自调度循环一样;cron 类型取下一个匹配时间。cron 支持 5 段(分 时 日 月 周),按服务器本地时区计算。
|
||||
- **立即执行**:只设置 `run_requested_at`。如果任务已在排队或正在运行,请求会被合并,接口返回 `queued:false`。立即执行不改变下次运行时间。
|
||||
- **跳过本次**:把下次运行时间顺延一个周期,并记一条 `trigger=skip` 的历史。
|
||||
- **时间槽**:run 拿到的 `slot` 在 interval 类型下是 `floor(now / everyMs)`,在 cron 类型下是计划时间。入队幂等键用它拼接;立即执行时用 `manual:<runId>`。
|
||||
- **中断恢复**:新的执行者拿到锁时,会把上一个执行者留下的 `running` 状态改为 `failed`,错误信息是「调度器重启,上次运行中断」。
|
||||
|
||||
## 登记与用户修改
|
||||
|
||||
- 首次登记时按默认值插入。之后再登记只刷新名称、说明、分组和默认值。`config` 以表里的值为准,只补上新增的默认键。
|
||||
- 用户改过开关或调度后,`customized` 会被置为 true,之后再登记也不覆盖这两项。执行 `PATCH {reset:true}` 可以恢复默认。
|
||||
- 如果某个内置 task 在代码里不再登记,它会被标记为 `available=false`,不再执行。
|
||||
- 通过 API 设置间隔时,下限是 10 秒。内置默认值可以更短,比如测试时。
|
||||
|
||||
## Agent 巡检(kind = agent_check)
|
||||
|
||||
`config` 使用 `AgentCheckConfig` 结构:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `goalTemplate` | 目标模板。可用占位符:`{{now}}` `{{date}}` `{{openDecisions}}` `{{stalledDownloads}}` `{{activeSubscriptions}}` |
|
||||
| `capabilityAllowlist` | 能力白名单。只读模式下,非 read/preview/propose 类能力会被剔除;留空表示只读类全部可用 |
|
||||
| `budget` | 传给 `agent_run.budget` |
|
||||
| `triggerCondition` | 可选 `always`、`open_decisions`、`stalled_downloads`、`subscriptions_active`。计划触发时条件不满足就记为 `skipped`,不唤醒模型;立即执行时忽略条件 |
|
||||
| `allowRecover` | 走和 autoInvestigate 相同的判定:存在覆盖 `task.retry` / `download.recheck` 的有效持续授权时,用 execute 模式;否则降级为只读 |
|
||||
| `reportAsDecision` | 为 true 时,运行成功后把摘要写成一条 `kind=other`、`severity=info` 的待处理事项 |
|
||||
|
||||
执行过程是 `startRun(trigger=schedule, createdBy=schedule:<id>)`,然后入队 `runAgent`。`dedupe_key` 是 `schedule:<id>:<时间槽>`,所以同一时间槽只会建一个 run。
|
||||
|
||||
agent_run 结束后,调度器每 15 秒回填一次,把 `scheduled_job_run.agent_status` 和 `summary` 写回。
|
||||
|
||||
用户可以通过 `POST /schedules` 新建自定义巡检,id 形如 `check.custom.<8位>`。自定义巡检可以改名,也可以删除;内置任务只能停用。
|
||||
|
||||
## 排查
|
||||
|
||||
- 看板顶部显示「调度器未在线」:说明没有进程持有锁。检查 worker(io 组)是否在运行,以及它的日志里有没有「调度器取得执行权」。
|
||||
- 用 SQL 确认谁持有锁:`select pid from pg_locks where locktype='advisory' and classid=1970107507 and objsubid=2;`
|
||||
- 任务一直是 `running`:执行体卡住了,调度器不会给执行体设超时。重启 worker 后,状态会被标为中断失败。
|
||||
- SSE:运行开始或结束时,只有状态发生变化、手动触发或失败时才推送 `schedule.changed`;用户修改后也会推送。看板另外每 30 秒兜底刷新一次。
|
||||
Reference in new issue
Block a user