























@@ -0,0 +1,262 @@
1+import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
2+import { isRecord } from "../../utils.js";
3+4+const CRON_SCHEDULE_KINDS = ["at", "every", "cron"] as const;
5+const CRON_PAYLOAD_KINDS = ["systemEvent", "agentTurn"] as const;
6+const CRON_FLAT_PAYLOAD_KEYS = [
7+"message",
8+"text",
9+"model",
10+"fallbacks",
11+"toolsAllow",
12+"thinking",
13+"timeoutSeconds",
14+"lightContext",
15+"allowUnsafeExternalContent",
16+] as const;
17+const CRON_FLAT_SCHEDULE_KEYS = [
18+"kind",
19+"at",
20+"atMs",
21+"every",
22+"everyMs",
23+"anchorMs",
24+"cron",
25+"expr",
26+"tz",
27+"stagger",
28+"staggerMs",
29+"exact",
30+] as const;
31+const CRON_RECOVERABLE_OBJECT_KEYS: ReadonlySet<string> = new Set([
32+"name",
33+"schedule",
34+"sessionTarget",
35+"wakeMode",
36+"payload",
37+"delivery",
38+"enabled",
39+"description",
40+"deleteAfterRun",
41+"agentId",
42+"sessionKey",
43+"failureAlert",
44+ ...CRON_FLAT_PAYLOAD_KEYS,
45+ ...CRON_FLAT_SCHEDULE_KEYS,
46+]);
47+48+function isCronScheduleKind(value: unknown): value is (typeof CRON_SCHEDULE_KINDS)[number] {
49+return value === "at" || value === "every" || value === "cron";
50+}
51+52+function isCronPayloadKind(value: unknown): value is (typeof CRON_PAYLOAD_KINDS)[number] {
53+return value === "systemEvent" || value === "agentTurn";
54+}
55+56+function isNonEmptyString(value: unknown): value is string {
57+return typeof value === "string" && value.trim().length > 0;
58+}
59+60+function isStringArrayOrNull(value: unknown): boolean {
61+return (
62+value === null || (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
63+);
64+}
65+66+function moveDefinedField(params: {
67+source: Record<string, unknown>;
68+target: Record<string, unknown>;
69+from: string;
70+to?: string;
71+}): boolean {
72+if (params.source[params.from] === undefined) {
73+return false;
74+}
75+params.target[params.to ?? params.from] = params.source[params.from];
76+delete params.source[params.from];
77+return true;
78+}
79+80+function setScheduleAtMs(schedule: Record<string, unknown>, value: unknown): void {
81+const atMs = typeof value === "number" ? value : Number(value);
82+schedule.at = Number.isFinite(atMs) ? (timestampMsToIsoString(Math.floor(atMs)) ?? value) : value;
83+}
84+85+function canonicalizeCronToolSchedule(value: Record<string, unknown>): void {
86+const schedule = isRecord(value.schedule) ? { ...value.schedule } : {};
87+let hasSchedule = isRecord(value.schedule);
88+89+if (schedule.atMs !== undefined) {
90+setScheduleAtMs(schedule, schedule.atMs);
91+delete schedule.atMs;
92+if (!isCronScheduleKind(schedule.kind)) {
93+schedule.kind = "at";
94+}
95+}
96+if (schedule.everyMs === undefined && schedule.every !== undefined) {
97+schedule.everyMs = schedule.every;
98+delete schedule.every;
99+}
100+if (schedule.expr === undefined && schedule.cron !== undefined) {
101+schedule.expr = schedule.cron;
102+delete schedule.cron;
103+}
104+if (schedule.staggerMs === undefined && schedule.stagger !== undefined) {
105+schedule.staggerMs = schedule.stagger;
106+delete schedule.stagger;
107+}
108+if (schedule.exact === true && schedule.staggerMs === undefined) {
109+schedule.staggerMs = 0;
110+}
111+delete schedule.exact;
112+113+if (isCronScheduleKind(value.kind) && !isCronScheduleKind(schedule.kind)) {
114+schedule.kind = value.kind;
115+delete value.kind;
116+hasSchedule = true;
117+}
118+119+const movedAt = moveDefinedField({ source: value, target: schedule, from: "at" });
120+if (movedAt && !isCronScheduleKind(schedule.kind)) {
121+schedule.kind = "at";
122+}
123+124+if (value.atMs !== undefined) {
125+setScheduleAtMs(schedule, value.atMs);
126+delete value.atMs;
127+if (!isCronScheduleKind(schedule.kind)) {
128+schedule.kind = "at";
129+}
130+hasSchedule = true;
131+}
132+133+const movedEveryMs =
134+moveDefinedField({ source: value, target: schedule, from: "everyMs" }) ||
135+moveDefinedField({ source: value, target: schedule, from: "every", to: "everyMs" });
136+if (movedEveryMs && !isCronScheduleKind(schedule.kind)) {
137+schedule.kind = "every";
138+}
139+140+const movedCron =
141+moveDefinedField({ source: value, target: schedule, from: "cron", to: "expr" }) ||
142+moveDefinedField({ source: value, target: schedule, from: "expr" });
143+if (movedCron && !isCronScheduleKind(schedule.kind)) {
144+schedule.kind = "cron";
145+}
146+147+for (const key of ["anchorMs", "tz", "staggerMs"] as const) {
148+hasSchedule = moveDefinedField({ source: value, target: schedule, from: key }) || hasSchedule;
149+}
150+hasSchedule =
151+moveDefinedField({ source: value, target: schedule, from: "stagger", to: "staggerMs" }) ||
152+hasSchedule;
153+154+if (value.exact === true && schedule.staggerMs === undefined) {
155+schedule.staggerMs = 0;
156+hasSchedule = true;
157+}
158+delete value.exact;
159+160+if (!isCronScheduleKind(schedule.kind)) {
161+if (schedule.at !== undefined) {
162+schedule.kind = "at";
163+} else if (schedule.everyMs !== undefined) {
164+schedule.kind = "every";
165+} else if (schedule.expr !== undefined) {
166+schedule.kind = "cron";
167+}
168+}
169+170+if (hasSchedule || Object.keys(schedule).length > 0) {
171+value.schedule = schedule;
172+}
173+}
174+175+function canonicalizeCronToolPayload(value: Record<string, unknown>): void {
176+const payload = isRecord(value.payload) ? { ...value.payload } : {};
177+let hasPayload = isRecord(value.payload);
178+179+for (const key of CRON_FLAT_PAYLOAD_KEYS) {
180+hasPayload = moveDefinedField({ source: value, target: payload, from: key }) || hasPayload;
181+}
182+183+if (isCronPayloadKind(value.kind) && !isCronPayloadKind(payload.kind)) {
184+payload.kind = value.kind;
185+delete value.kind;
186+hasPayload = true;
187+}
188+189+if (!isCronPayloadKind(payload.kind)) {
190+const hasAgentTurnSignal =
191+isNonEmptyString(payload.message) ||
192+isNonEmptyString(payload.model) ||
193+isNonEmptyString(payload.thinking) ||
194+typeof payload.timeoutSeconds === "number" ||
195+typeof payload.lightContext === "boolean" ||
196+typeof payload.allowUnsafeExternalContent === "boolean" ||
197+(payload.fallbacks !== undefined && isStringArrayOrNull(payload.fallbacks)) ||
198+(payload.toolsAllow !== undefined && isStringArrayOrNull(payload.toolsAllow));
199+if (hasAgentTurnSignal) {
200+payload.kind = "agentTurn";
201+} else if (isNonEmptyString(payload.text)) {
202+payload.kind = "systemEvent";
203+}
204+}
205+206+if (hasPayload || Object.keys(payload).length > 0) {
207+value.payload = payload;
208+}
209+}
210+211+export function canonicalizeCronToolObject(
212+value: Record<string, unknown>,
213+): Record<string, unknown> {
214+const unwrapped = isRecord(value.data) ? value.data : isRecord(value.job) ? value.job : value;
215+const next = { ...unwrapped };
216+canonicalizeCronToolSchedule(next);
217+canonicalizeCronToolPayload(next);
218+return next;
219+}
220+221+export function isEmptyRecoveredCronPatch(value: unknown): boolean {
222+if (!isRecord(value)) {
223+return true;
224+}
225+const keys = Object.keys(value);
226+return (
227+keys.length === 0 ||
228+(keys.length === 1 &&
229+keys[0] === "payload" &&
230+isRecord(value.payload) &&
231+Object.keys(value.payload).length === 0)
232+);
233+}
234+235+export function recoverCronObjectFromFlatParams(params: Record<string, unknown>): {
236+found: boolean;
237+value: Record<string, unknown>;
238+} {
239+const value: Record<string, unknown> = {};
240+let found = false;
241+for (const key of Object.keys(params)) {
242+if (CRON_RECOVERABLE_OBJECT_KEYS.has(key) && params[key] !== undefined) {
243+value[key] = params[key];
244+found = true;
245+}
246+}
247+return { found, value: canonicalizeCronToolObject(value) };
248+}
249+250+export function hasCronCreateSignal(value: Record<string, unknown>): boolean {
251+return (
252+value.schedule !== undefined ||
253+value.at !== undefined ||
254+value.atMs !== undefined ||
255+value.everyMs !== undefined ||
256+value.cron !== undefined ||
257+value.expr !== undefined ||
258+value.payload !== undefined ||
259+value.message !== undefined ||
260+value.text !== undefined
261+);
262+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。