Files
UHD_Media_Tool/docs/runbook/schedules.md
T
rootandClaude Opus 5.5 e681fcedf6 feat(schedules): per-check cost and model summary on GET /schedules (INT-3)
agent_check items carry agentRuns (last 100 sessions: last/avg cost, 7-day count and cost,
models, recent 3); plan page and home plan band use it instead of fetching agent runs.
Migration 0052 indexes agent_run(created_by, created_at).

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

8.6 KiB
Raw Blame History

定时任务与看板

所有周期任务都登记在 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 小时没有心跳的进程行、超过 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 小时 分批删除超过保留期的流水数据,见 storage.md
storage.analyze 统计信息更新 维护 7 天 全库 ANALYZE
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 秒兜底刷新一次。