- 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]>
7.6 KiB
7.6 KiB
定时任务与看板
所有周期任务都登记在 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用户改过(PATCHconfig,标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 秒兜底刷新一次。