feat(server): unified scheduled-job registry and scheduler; module timers moved onto it

- modules/schedules: registerScheduledJob({key, defaults, run}) registry; defaults are
  synced into scheduled_job without overriding user-changed enabled/schedule
- scheduler runs only on the io worker, one instance via pg_try_advisory_lock held on a
  dedicated connection; due = enabled and next_run_at reached, or run requested;
  jobs run in parallel, never re-entrant; interval next = finish + period, cron next match;
  run-now does not move next_run_at and merges duplicate requests; skip advances one period;
  history trimmed to the last 50 runs; interrupted runs marked failed on takeover
- agent_check jobs: trigger condition checked first, goal template rendered, runAgent
  enqueued via startRun(trigger=schedule) with allowlist and budget, one run per time slot;
  allowRecover uses the autoInvestigate grant check; results harvested back into the run
  history and optionally reported as an info decision; 3 built-in checks, off by default
- sources-identity (feeds.poll, sites.snapshot, identity.review), acquisition-organize
  (downloads.sync, cache.threshold, downloads.orchestrate, slots.reevaluate,
  subscriptions.search, subscriptions.metadata, library.reconcile) and file-manager
  (trash.purge) register jobs instead of running their own setInterval; default
  intervals unchanged; backup registered as an unconfigured placeholder
- endpoints under /schedules (admin, console session only)

TODO: pi refreshPiTokens and auth-settings session cleanup still own their timers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 02:32:33 +08:00
1 parent f8f70d8e09
commit a68fcea13b
16 files changed
+1732 -127

No files matched your search

+2 -1
View File
@@ -43,7 +43,7 @@ const pingModule: ServerModule = {
};
describe("ServerModule 骨架", () => {
it("清单里是四个 W2 模块 + 文件管理 + Pi 上游,名字唯一", () => {
it("清单里是四个 W2 模块 + 文件管理 + Pi 上游 + 定时任务(末尾),名字唯一", () => {
expect(SERVER_MODULES.map((m) => m.name)).toEqual([
"auth-settings",
"sources-identity",
@@ -51,6 +51,7 @@ describe("ServerModule 骨架", () => {
"agent",
"file-manager",
"pi",
"schedules",
]);
});