mirror of https://github.com/openclaw/openclaw.git
222 lines
5.6 KiB
TypeScript
222 lines
5.6 KiB
TypeScript
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
|
import { sanitizeAgentId } from "../routing/session-key.js";
|
|
import { parseAbsoluteTimeMs } from "./parse.js";
|
|
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
|
|
|
type UnknownRecord = Record<string, unknown>;
|
|
|
|
type NormalizeOptions = {
|
|
applyDefaults?: boolean;
|
|
};
|
|
|
|
const DEFAULT_OPTIONS: NormalizeOptions = {
|
|
applyDefaults: false,
|
|
};
|
|
|
|
function isRecord(value: unknown): value is UnknownRecord {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function coerceSchedule(schedule: UnknownRecord) {
|
|
const next: UnknownRecord = { ...schedule };
|
|
const kind = typeof schedule.kind === "string" ? schedule.kind : undefined;
|
|
const atMsRaw = schedule.atMs;
|
|
const atRaw = schedule.at;
|
|
const parsedAtMs =
|
|
typeof atMsRaw === "string"
|
|
? parseAbsoluteTimeMs(atMsRaw)
|
|
: typeof atRaw === "string"
|
|
? parseAbsoluteTimeMs(atRaw)
|
|
: null;
|
|
|
|
if (!kind) {
|
|
if (
|
|
typeof schedule.atMs === "number" ||
|
|
typeof schedule.at === "string" ||
|
|
typeof schedule.atMs === "string"
|
|
) {
|
|
next.kind = "at";
|
|
} else if (typeof schedule.everyMs === "number") {
|
|
next.kind = "every";
|
|
} else if (typeof schedule.expr === "string") {
|
|
next.kind = "cron";
|
|
}
|
|
}
|
|
|
|
if (typeof schedule.atMs !== "number" && parsedAtMs !== null) {
|
|
next.atMs = parsedAtMs;
|
|
}
|
|
|
|
if ("at" in next) {
|
|
delete next.at;
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
function coercePayload(payload: UnknownRecord) {
|
|
const next: UnknownRecord = { ...payload };
|
|
// Back-compat: older configs used `provider` for delivery channel.
|
|
migrateLegacyCronPayload(next);
|
|
return next;
|
|
}
|
|
|
|
function coerceDelivery(delivery: UnknownRecord) {
|
|
const next: UnknownRecord = { ...delivery };
|
|
if (typeof delivery.mode === "string") {
|
|
next.mode = delivery.mode.trim().toLowerCase();
|
|
}
|
|
if (typeof delivery.channel === "string") {
|
|
const trimmed = delivery.channel.trim().toLowerCase();
|
|
if (trimmed) {
|
|
next.channel = trimmed;
|
|
} else {
|
|
delete next.channel;
|
|
}
|
|
}
|
|
if (typeof delivery.to === "string") {
|
|
const trimmed = delivery.to.trim();
|
|
if (trimmed) {
|
|
next.to = trimmed;
|
|
} else {
|
|
delete next.to;
|
|
}
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function hasLegacyDeliveryHints(payload: UnknownRecord) {
|
|
if (typeof payload.deliver === "boolean") {
|
|
return true;
|
|
}
|
|
if (typeof payload.bestEffortDeliver === "boolean") {
|
|
return true;
|
|
}
|
|
if (typeof payload.to === "string" && payload.to.trim()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function unwrapJob(raw: UnknownRecord) {
|
|
if (isRecord(raw.data)) {
|
|
return raw.data;
|
|
}
|
|
if (isRecord(raw.job)) {
|
|
return raw.job;
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
export function normalizeCronJobInput(
|
|
raw: unknown,
|
|
options: NormalizeOptions = DEFAULT_OPTIONS,
|
|
): UnknownRecord | null {
|
|
if (!isRecord(raw)) {
|
|
return null;
|
|
}
|
|
const base = unwrapJob(raw);
|
|
const next: UnknownRecord = { ...base };
|
|
|
|
if ("agentId" in base) {
|
|
const agentId = base.agentId;
|
|
if (agentId === null) {
|
|
next.agentId = null;
|
|
} else if (typeof agentId === "string") {
|
|
const trimmed = agentId.trim();
|
|
if (trimmed) {
|
|
next.agentId = sanitizeAgentId(trimmed);
|
|
} else {
|
|
delete next.agentId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ("enabled" in base) {
|
|
const enabled = base.enabled;
|
|
if (typeof enabled === "boolean") {
|
|
next.enabled = enabled;
|
|
} else if (typeof enabled === "string") {
|
|
const trimmed = enabled.trim().toLowerCase();
|
|
if (trimmed === "true") {
|
|
next.enabled = true;
|
|
}
|
|
if (trimmed === "false") {
|
|
next.enabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isRecord(base.schedule)) {
|
|
next.schedule = coerceSchedule(base.schedule);
|
|
}
|
|
|
|
if (isRecord(base.payload)) {
|
|
next.payload = coercePayload(base.payload);
|
|
}
|
|
|
|
if (isRecord(base.delivery)) {
|
|
next.delivery = coerceDelivery(base.delivery);
|
|
}
|
|
|
|
if (options.applyDefaults) {
|
|
if (!next.wakeMode) {
|
|
next.wakeMode = "next-heartbeat";
|
|
}
|
|
if (!next.sessionTarget && isRecord(next.payload)) {
|
|
const kind = typeof next.payload.kind === "string" ? next.payload.kind : "";
|
|
if (kind === "systemEvent") {
|
|
next.sessionTarget = "main";
|
|
}
|
|
if (kind === "agentTurn") {
|
|
next.sessionTarget = "isolated";
|
|
}
|
|
}
|
|
if (
|
|
"schedule" in next &&
|
|
isRecord(next.schedule) &&
|
|
next.schedule.kind === "at" &&
|
|
!("deleteAfterRun" in next)
|
|
) {
|
|
next.deleteAfterRun = true;
|
|
}
|
|
const hasDelivery = "delivery" in next && next.delivery !== undefined;
|
|
const payload = isRecord(next.payload) ? next.payload : null;
|
|
const payloadKind = payload && typeof payload.kind === "string" ? payload.kind : "";
|
|
const sessionTarget = typeof next.sessionTarget === "string" ? next.sessionTarget : "";
|
|
const hasLegacyIsolation = isRecord(next.isolation);
|
|
const hasLegacyDelivery = payload ? hasLegacyDeliveryHints(payload) : false;
|
|
if (
|
|
!hasDelivery &&
|
|
!hasLegacyIsolation &&
|
|
!hasLegacyDelivery &&
|
|
sessionTarget === "isolated" &&
|
|
payloadKind === "agentTurn"
|
|
) {
|
|
next.delivery = { mode: "announce" };
|
|
}
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function normalizeCronJobCreate(
|
|
raw: unknown,
|
|
options?: NormalizeOptions,
|
|
): CronJobCreate | null {
|
|
return normalizeCronJobInput(raw, {
|
|
applyDefaults: true,
|
|
...options,
|
|
}) as CronJobCreate | null;
|
|
}
|
|
|
|
export function normalizeCronJobPatch(
|
|
raw: unknown,
|
|
options?: NormalizeOptions,
|
|
): CronJobPatch | null {
|
|
return normalizeCronJobInput(raw, {
|
|
applyDefaults: false,
|
|
...options,
|
|
}) as CronJobPatch | null;
|
|
}
|