Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
94 lines
9.7 KiB
Markdown
94 lines
9.7 KiB
Markdown
# 定时任务与看板
|
||
|
||
所有周期任务都登记在 `scheduled_job` 表里,由 io worker 上唯一的调度器按表驱动。控制台日志页的「定时任务」页签(`/console/logs?tab=schedules`,原计划页 `/console/schedules` 跳转过来;分「自动任务」与「Agent 巡检」两节)可以查看和调整,运行记录在日志页按定时任务筛选(见 [logs.md](logs.md))。
|
||
|
||
## 组成
|
||
|
||
| 部分 | 位置 | 说明 |
|
||
| --- | --- | --- |
|
||
| 注册 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`,包括按阈值暂停和恢复(排队中的任务一起暂停)、季包选文件的重试(磁力元数据未就绪时最多等 10 分钟,超时整包下载)。所有下载器都连不上时记为失败;单个下载器连续 10 分钟连不上开 `downloader_offline` 待处理,下载目录读不到容量开 `cache_volume_unknown` 待处理,恢复后都自动关闭。停用的下载器上还有在下的任务时仍同步这些任务,不接管新任务 |
|
||
| `downloads.orchestrate` | 补提交与补入库 | 下载 | 同上 | `sweepReservedPlans` 和补入队 `verifyAndImport` |
|
||
| `slots.reevaluate` | 到期重评 | 下载 | 同上 | 处理 `next_evaluate_at` 已到的收录位 |
|
||
| `subscriptions.metadata` | 总集数刷新 | 整理 | 同上 | `sweepMetadataRefresh` |
|
||
| `library.reconcile` | 资产对账 | 整理 | 6 小时 | 入队 `reconcileLibrary`,幂等键是 `auto:<时间槽>` |
|
||
| `cache.threshold` | 缓存阈值检查 | 维护 | 同 qB 同步 | 超过硬阈值时入队 `cleanupDownloadCache`;读不到容量的下载目录不当 0%,摘要里写明并开待处理。H&R 要求未知的任务缺省不清理,下载器设置打开「H&R 未知也可清理」后保种天数到期即可清理 |
|
||
| `trash.purge` | 回收站清理 | 维护 | 24 小时 | 入队 `purgeTrash` |
|
||
| `pi.token_refresh` | 上游登录刷新 | 维护 | 10 分钟 | 入队 `refreshPiTokens`,幂等键 `refreshPiTokens:<时间槽>`;对 30 分钟内过期的订阅登录上游刷新 token |
|
||
| `auth.cleanup` | 登录会话清理 | 维护 | 1 小时 | 直接执行 `cleanupAuthTables`:删 7 天前的登录尝试、30 天前过期或吊销的会话 |
|
||
| `system.heartbeat_cleanup` | 进程心跳清理 | 维护 | 1 小时 | 删 24 小时没有心跳的进程行、超过 `storage.retentionDays.heartbeatHours`(24 小时)的心跳采样 |
|
||
| `cache.sweep` | 缓存清理 | 维护 | 10 分钟 | 删 kv_cache 过期行 |
|
||
| `cache.images_clean` | 图片缓存清理 | 维护 | 24 小时 | 删超过保留天数的图片 |
|
||
| `cache.images_lru` | 图片缓存容量控制 | 维护 | 30 分钟 | 超过 `storage.budgets.imagesGb` 按最久未访问删到 90% |
|
||
| `storage.retention.*` | 保留清理(观察 / SSE / 任务运行 / Agent 事件 / 审计 / 能力统计 / 流水数据 / 待识别池归档) | 维护 | 1–24 小时(流水数据每天 04:40) | 分批删除超过保留期的流水数据,见 [storage.md](storage.md) |
|
||
| `storage.analyze` | 统计信息更新 | 维护 | 7 天 | 全库 `ANALYZE` |
|
||
| `metadata.backfill_languages` | 元数据语言回刷 | 维护 | 24 小时,跑完自动停用 | 部署后首次启动即运行:对已确认 TMDB 身份的作品按语言回退链 full 刷新一次(TMDB 每秒最多 2 次,走请求缓存),进度写运行记录;跑完把自己停用,可手动再执行。见 [metadata.md](metadata.md) |
|
||
| `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位>`。自定义巡检可以改名,也可以删除;内置任务只能停用。
|
||
|
||
`GET /schedules` 里每个巡检带 `agentRuns`:该巡检最近 100 次会话(`agent_run.created_by = schedule:<id>`,不含子代理)的上次 / 平均费用、近 7 天会话数与费用、最近用的上游与模型、按模型计数、最近 3 次会话。计划页卡片与首页计划带直接用它,不再各自拉会话列表。
|
||
|
||
## 排查
|
||
|
||
- 看板顶部显示「调度器未在线」:说明没有进程持有锁。检查 worker(io 组)是否在运行,以及它的日志里有没有「调度器取得执行权」。
|
||
- 用 SQL 确认谁持有锁:`select pid from pg_locks where locktype='advisory' and classid=1970107507 and objsubid=2;`
|
||
- 任务一直是 `running`:执行体卡住了,调度器不会给执行体设超时。重启 worker 后,状态会被标为中断失败。
|
||
- SSE:运行开始或结束时,只有状态发生变化、手动触发或失败时才推送 `schedule.changed`;用户修改后也会推送。看板另外每 30 秒兜底刷新一次。
|