Files
UHD_Media_Tool/docs/runbook/schedules.md
T
rootandClaude Opus 5.5 baf2d6ea04 feat(web): split plan page from tasks (FE-6)
- Nav overview group: 首页 / 任务 / 计划 / Agent 会话; tasks page drops the plan tab
- /console/tasks?tab=plan[&job=] redirects to /console/schedules[?job=]
- Plan page: 自动任务 compact grouped list (switch, interval preset/custom, last result,
  countdown, failure reason, run now/skip/run records) and Agent 巡检 cards (goal,
  allowlist, budget, upstream/model, trigger, cost per run, recent sessions); split stats
- Home plan band: two counts linking to #auto / #checks
- Docs: console-ia §7.4a, glossary, schedules runbook; smoke routes

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 17:13:54 +08:00

86 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 定时任务与看板
所有周期任务都登记在 `scheduled_job` 表里,由 io worker 上唯一的调度器按表驱动。控制台「计划」页(`/console/schedules`,分「自动任务」与「Agent 巡检」两节)可以查看和调整。
## 组成
| 部分 | 位置 | 说明 |
| --- | --- | --- |
| 注册 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` |
| `pi.token_refresh` | 上游登录刷新 | 维护 | 10 分钟 | 入队 `refreshPiTokens`,幂等键 `refreshPiTokens:<时间槽>`;对 30 分钟内过期的订阅登录上游刷新 token |
| `auth.cleanup` | 登录会话清理 | 维护 | 1 小时 | 直接执行 `cleanupAuthTables`:删 7 天前的登录尝试、30 天前过期或吊销的会话 |
| `system.heartbeat_cleanup` | 进程心跳清理 | 维护 | 1 小时 | 删 24 小时没有心跳的进程行、7 天前的心跳采样 |
| `maintain.backup` | 备份 | 维护 | 每天 03:30(占位) | 未配置。备份目前由宿主机上的 `scripts/backup.sh` 执行 |
| `check.pending` | 待处理巡检 | Agent | 6 小时,默认关闭 | 只读。有未关闭事项时才会运行 |
| `check.downloads` | 下载诊断 | Agent | 2 小时,默认关闭 | 默认只读。打开 `allowRecover` 后,在授权范围内重试或重新校验 |
| `check.weekly` | 订阅与站点周报 | Agent | 7 天,默认关闭 | 只读。结果会写成一条 info 级待处理事项 |
补充几点:
- 自动化总开关关闭,或处于只读对账模式时,编排类任务会记为 `skipped`,不执行。
- 各项默认周期和改造前的计时器一致。测试时可以通过模块参数调短,比如 `syncIntervalMs`、`schedulerIntervalMs`。
- 所有周期任务都已登记,模块里不再有自建计时器(W8-A 把 `pi.token_refresh`、`auth.cleanup` 也迁了进来)。
## 调度语义
- **到期**:满足 `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` 用户改过(PATCH `config`,标 `config_customized`)时以表里的值为准、只补上新增的默认键;没改过时整体换成代码默认值(比如内置巡检白名单新增的能力)。
- 用户改过开关或调度后,`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 秒兜底刷新一次。