fix(schedules): refresh untouched builtin config so checks get new capabilities
Built-in agent checks kept the config stored at first registration, so check.pending never received decision.list / subscription.list added in AG-3 and the patrol could not read pending items (REAL-2 scenario g). Track config_customized (migration 0045); sync replaces config with the code default unless the user PATCHed it. Needs image rebuild. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
96fc089f92
commit
bd0e011748
6 files changed
+52
-5
No files matched your search
@@ -94,7 +94,8 @@ export function runView(r: ScheduledJobRunRow): ScheduledJobRun {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 把代码登记的定义同步进表:新行按默认值插入;已有行刷新名称/分组/默认值,
|
* 把代码登记的定义同步进表:新行按默认值插入;已有行刷新名称/分组/默认值,
|
||||||
* 用户改过(customized)的 enabled/schedule 保留;config 以表内值为准、补上新增的默认键。
|
* 用户改过(customized)的 enabled/schedule 保留;config 用户改过(config_customized)时以表内值为准、补上新增的默认键,
|
||||||
|
* 否则整体换成代码默认值(新增的巡检白名单能力等才能进旧行)。
|
||||||
* 未改过且默认调度变了的行,下次运行改为立即(随后按新周期)。
|
* 未改过且默认调度变了的行,下次运行改为立即(随后按新周期)。
|
||||||
* 同步时代码里已不再登记的内置 task 行标为不可用(不会执行,看板显示"未登记")。
|
* 同步时代码里已不再登记的内置 task 行标为不可用(不会执行,看板显示"未登记")。
|
||||||
*/
|
*/
|
||||||
@@ -121,7 +122,8 @@ export async function syncDefinitions(db: Kysely<DB>, defs: readonly ScheduledJo
|
|||||||
default_schedule = excluded.default_schedule,
|
default_schedule = excluded.default_schedule,
|
||||||
enabled = case when scheduled_job.customized then scheduled_job.enabled else excluded.enabled end,
|
enabled = case when scheduled_job.customized then scheduled_job.enabled else excluded.enabled end,
|
||||||
schedule = case when scheduled_job.customized then scheduled_job.schedule else excluded.schedule end,
|
schedule = case when scheduled_job.customized then scheduled_job.schedule else excluded.schedule end,
|
||||||
config = excluded.config || scheduled_job.config,
|
config = case when scheduled_job.config_customized then excluded.config || scheduled_job.config
|
||||||
|
else excluded.config end,
|
||||||
next_run_at = case
|
next_run_at = case
|
||||||
when not scheduled_job.customized and scheduled_job.schedule is distinct from excluded.schedule then ${now}
|
when not scheduled_job.customized and scheduled_job.schedule is distinct from excluded.schedule then ${now}
|
||||||
else coalesce(scheduled_job.next_run_at, excluded.next_run_at) end`.execute(db);
|
else coalesce(scheduled_job.next_run_at, excluded.next_run_at) end`.execute(db);
|
||||||
@@ -221,6 +223,7 @@ export async function updateJob(
|
|||||||
} else {
|
} else {
|
||||||
set.config = JSON.stringify({ ...((row.config ?? {}) as object), ...patch.config });
|
set.config = JSON.stringify({ ...((row.config ?? {}) as object), ...patch.config });
|
||||||
}
|
}
|
||||||
|
set.config_customized = true;
|
||||||
}
|
}
|
||||||
return await db.updateTable("scheduled_job").set(set).where("id", "=", id).returningAll().executeTakeFirstOrThrow();
|
return await db.updateTable("scheduled_job").set(set).where("id", "=", id).returningAll().executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,22 @@ describe.skipIf(!hasPg)("定时任务注册表(真实 PG)", () => {
|
|||||||
expect(new Date(cron.body.data.nextRunAt).getHours()).toBe(4);
|
expect(new Date(cron.body.data.nextRunAt).getHours()).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("config:没改过的行随代码默认值整体更新;PATCH 过的保留表内值", async () => {
|
||||||
|
const def = (config: Record<string, unknown>) =>
|
||||||
|
counting("test.cfg", {
|
||||||
|
defaults: { name: "c", group: "download", schedule: { type: "interval", everyMs: 60_000 }, config },
|
||||||
|
});
|
||||||
|
registerScheduledJob(ctx, def({ list: ["a"] }));
|
||||||
|
await sched.tick();
|
||||||
|
registerScheduledJob(ctx, def({ list: ["a", "b"] }));
|
||||||
|
await sched.tick();
|
||||||
|
expect((await job("test.cfg")).config).toEqual({ list: ["a", "b"] });
|
||||||
|
expect((await api("PATCH", "/schedules/test.cfg", { config: { list: ["x"] } })).status).toBe(200);
|
||||||
|
registerScheduledJob(ctx, def({ list: ["a", "b", "c"], extra: 1 }));
|
||||||
|
await sched.tick();
|
||||||
|
expect((await job("test.cfg")).config).toEqual({ list: ["x"], extra: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
it("立即执行幂等(合并重复请求)且不改下次运行时间", async () => {
|
it("立即执行幂等(合并重复请求)且不改下次运行时间", async () => {
|
||||||
registerScheduledJob(
|
registerScheduledJob(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
## 登记与用户修改
|
## 登记与用户修改
|
||||||
|
|
||||||
- 首次登记时按默认值插入。之后再登记只刷新名称、说明、分组和默认值。`config` 以表里的值为准,只补上新增的默认键。
|
- 首次登记时按默认值插入。之后再登记只刷新名称、说明、分组和默认值。`config` 用户改过(PATCH `config`,标 `config_customized`)时以表里的值为准、只补上新增的默认键;没改过时整体换成代码默认值(比如内置巡检白名单新增的能力)。
|
||||||
- 用户改过开关或调度后,`customized` 会被置为 true,之后再登记也不覆盖这两项。执行 `PATCH {reset:true}` 可以恢复默认。
|
- 用户改过开关或调度后,`customized` 会被置为 true,之后再登记也不覆盖这两项。执行 `PATCH {reset:true}` 可以恢复默认。
|
||||||
- 如果某个内置 task 在代码里不再登记,它会被标记为 `available=false`,不再执行。
|
- 如果某个内置 task 在代码里不再登记,它会被标记为 `available=false`,不再执行。
|
||||||
- 通过 API 设置间隔时,下限是 10 秒。内置默认值可以更短,比如测试时。
|
- 通过 API 设置间隔时,下限是 10 秒。内置默认值可以更短,比如测试时。
|
||||||
|
|||||||
@@ -108,8 +108,12 @@ const ROWS: Record<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }>
|
|||||||
SlotDeferral: { row: C.SlotDeferral },
|
SlotDeferral: { row: C.SlotDeferral },
|
||||||
SlotUnit: { row: C.SlotUnit },
|
SlotUnit: { row: C.SlotUnit },
|
||||||
SubscriptionHistory: { row: C.SubscriptionHistory },
|
SubscriptionHistory: { row: C.SubscriptionHistory },
|
||||||
// W7-C 定时任务(0028):board_group 在 API 里叫 group;default_enabled 只用于"恢复默认"
|
// W7-C 定时任务(0028):board_group 在 API 里叫 group;default_enabled 只用于"恢复默认";config_customized(0045)只供启动同步
|
||||||
ScheduledJob: { row: C.ScheduledJob, dbOnly: ["boardGroup", "defaultEnabled"], apiOnly: ["group"] },
|
ScheduledJob: {
|
||||||
|
row: C.ScheduledJob,
|
||||||
|
dbOnly: ["boardGroup", "defaultEnabled", "configCustomized"],
|
||||||
|
apiOnly: ["group"],
|
||||||
|
},
|
||||||
ScheduledJobRun: { row: C.ScheduledJobRun },
|
ScheduledJobRun: { row: C.ScheduledJobRun },
|
||||||
// W7-A Pi 上游(0027):secret_ref 不出 API
|
// W7-A Pi 上游(0027):secret_ref 不出 API
|
||||||
// 0044 远程模型表:经 GET /pi/upstreams/{id}/models 单独返回,不进上游行
|
// 0044 远程模型表:经 GET /pi/upstreams/{id}/models 单独返回,不进上游行
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Kysely } from "kysely";
|
||||||
|
import { execDdl } from "../src/migration-helpers.ts";
|
||||||
|
|
||||||
|
// 内置定时任务的 config 只在用户改过(PATCH config)时保留表内值;没改过的行在启动同步时整体换成代码默认值,
|
||||||
|
// 否则代码新增的默认项(如巡检白名单新增的 decision.list / subscription.list)永远进不了旧行。
|
||||||
|
// 已有行:内置巡检的白名单与 0.2.2 默认值不同的视为用户改过。
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await execDdl(
|
||||||
|
db,
|
||||||
|
`alter table scheduled_job add column config_customized boolean not null default false;
|
||||||
|
update scheduled_job set config_customized = true
|
||||||
|
where not builtin
|
||||||
|
or (kind = 'agent_check' and config->'capabilityAllowlist' is distinct from
|
||||||
|
'["catalog.lookup","release.search","release.inspect","inventory.query","task.inspect","identity.resolve"]'::jsonb);`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await execDdl(db, `alter table scheduled_job drop column if exists config_customized;`);
|
||||||
|
}
|
||||||
@@ -1265,6 +1265,7 @@ export interface ScheduledJobTable {
|
|||||||
schedule: Json;
|
schedule: Json;
|
||||||
default_schedule: Json;
|
default_schedule: Json;
|
||||||
customized: Generated<boolean>;
|
customized: Generated<boolean>;
|
||||||
|
config_customized: Generated<boolean>;
|
||||||
config: Generated<Json>;
|
config: Generated<Json>;
|
||||||
last_run_at: Timestamptz | null;
|
last_run_at: Timestamptz | null;
|
||||||
last_status: ScheduledJobLastStatus | null;
|
last_status: ScheduledJobLastStatus | null;
|
||||||
|
|||||||
Reference in new issue
Block a user