

























1-import { execFile } from "node:child_process";
2-import { promisify } from "node:util";
31import { normalizeOptionalString } from "../../../../packages/normalization-core/src/string-coerce.js";
42import { note } from "../../../../packages/terminal-core/src/note.js";
53import { formatCliCommand } from "../../../cli/command-format.js";
6-import { resolveAgentModelPrimaryValue } from "../../../config/model-input.js";
74import type { OpenClawConfig } from "../../../config/types.openclaw.js";
8-import { migrateLegacyNotifyFallback } from "../../../cron/migrations/legacy-notify.js";
95import {
106loadCronQuarantineFile,
11-loadCronStoreWithConfigJobs,
7+loadCronJobsStoreWithConfigJobs,
128resolveCronQuarantinePath,
13-resolveCronStorePath,
9+resolveCronJobsStorePath,
1410saveCronQuarantineFile,
15-saveCronStore,
11+saveCronJobsStore,
1612} from "../../../cron/store.js";
1713import type { CronJob } from "../../../cron/types.js";
1814import { shortenHomePath } from "../../../utils.js";
@@ -21,6 +17,7 @@ import {
2117countStaleDreamingJobs,
2218migrateLegacyDreamingPayloadShape,
2319} from "./dreaming-payload-migration.js";
20+import { migrateLegacyNotifyFallback } from "./legacy-notify.js";
2421import {
2522legacyCronRunLogFilesExist,
2623migrateLegacyCronRunLogsToSqlite,
@@ -37,13 +34,12 @@ import {
3734needsSqliteProjectionBackfill,
3835} from "./repair-plan.js";
3936import { normalizeStoredCronJobs } from "./store-migration.js";
37+import { noteCronModelOverrides } from "./warnings.js";
403841-type CrontabReader = () => Promise<{ stdout?: unknown; stderr?: unknown }>;
42-43-const execFileAsync = promisify(execFile);
44-const LEGACY_WHATSAPP_HEALTH_SCRIPT_RE =
45-/(?:^|\s)(?:"[^"]*ensure-whatsapp\.sh"|'[^']*ensure-whatsapp\.sh'|[^\s#;|&]*ensure-whatsapp\.sh)\b/u;
46-const CRON_MODEL_OVERRIDE_EXAMPLE_LIMIT = 3;
39+export {
40+collectLegacyWhatsAppCrontabHealthWarning,
41+noteLegacyWhatsAppCrontabHealthCheck,
42+} from "./warnings.js";
47434844function pluralize(count: number, noun: string) {
4945return `${count} ${noun}${count === 1 ? "" : "s"}`;
@@ -55,197 +51,22 @@ function formatRunLogMigrationNote(importedFiles: number): string {
5551 : "";
5652}
575358-function normalizeModelProvider(value: unknown): string | undefined {
59-const raw = normalizeOptionalString(value);
60-if (!raw) {
61-return undefined;
62-}
63-const slash = raw.indexOf("/");
64-if (slash <= 0 || slash >= raw.length - 1) {
65-return undefined;
66-}
67-return raw.slice(0, slash).trim().toLowerCase() || undefined;
68-}
69-70-function normalizeModelRef(value: unknown): string | undefined {
71-const raw = normalizeOptionalString(value);
72-if (!raw) {
73-return undefined;
74-}
75-const slash = raw.indexOf("/");
76-if (slash <= 0 || slash >= raw.length - 1) {
77-return undefined;
78-}
79-const provider = raw.slice(0, slash).trim().toLowerCase();
80-const model = raw.slice(slash + 1).trim();
81-return provider && model ? `${provider}/${model}` : undefined;
82-}
83-84-function normalizeModelMismatchKey(value: unknown): string | undefined {
85-return normalizeModelRef(value) ?? normalizeOptionalString(value)?.toLowerCase();
86-}
87-88-function getRecord(value: unknown): Record<string, unknown> | null {
89-return value && typeof value === "object" && !Array.isArray(value)
90- ? (value as Record<string, unknown>)
91- : null;
92-}
93-94-function formatProviderCounts(counts: Map<string, number>): string {
95-return [...counts.entries()]
96-.toSorted(([left], [right]) => left.localeCompare(right))
97-.map(([provider, count]) => `${provider}=${count}`)
98-.join(", ");
99-}
100-101-function noteCronModelOverrides(params: {
102-cfg: OpenClawConfig;
103-jobs: Array<Record<string, unknown>>;
104-storePath: string;
105-}) {
106-const defaultModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model);
107-const defaultKey = normalizeModelMismatchKey(defaultModel);
108-const providerCounts = new Map<string, number>();
109-const mismatchExamples: string[] = [];
110-let overrideCount = 0;
111-let mismatchCount = 0;
112-113-for (const rawJob of params.jobs) {
114-const payload = getRecord(rawJob.payload);
115-const kind = normalizeOptionalString(payload?.kind)?.toLowerCase();
116-if (kind && kind !== "agentturn") {
117-continue;
118-}
119-const model = normalizeOptionalString(payload?.model);
120-if (!model) {
121-continue;
122-}
123-overrideCount += 1;
124-const provider = normalizeModelProvider(model) ?? "bare/alias";
125-providerCounts.set(provider, (providerCounts.get(provider) ?? 0) + 1);
126-const modelKey = normalizeModelMismatchKey(model);
127-if (defaultKey && modelKey && modelKey !== defaultKey) {
128-mismatchCount += 1;
129-if (mismatchExamples.length < CRON_MODEL_OVERRIDE_EXAMPLE_LIMIT) {
130-const id = normalizeOptionalString(rawJob.id) ?? normalizeOptionalString(rawJob.jobId);
131-const name = normalizeOptionalString(rawJob.name);
132-mismatchExamples.push(`${id ?? name ?? "<unnamed>"} -> ${model}`);
133-}
134-}
135-}
136-137-if (overrideCount === 0) {
138-return;
139-}
140-141-const lines = [
142-`Cron model overrides detected at ${shortenHomePath(params.storePath)}.`,
143-`- ${pluralize(overrideCount, "job")} set \`payload.model\` and will not inherit \`agents.defaults.model\`${defaultModel ? ` (${defaultModel})` : ""}`,
144-`- Provider namespaces: ${formatProviderCounts(providerCounts)}`,
145-];
146-if (mismatchCount > 0) {
147-lines.push(
148-`- ${pluralize(mismatchCount, "job")} ${mismatchCount === 1 ? "uses" : "use"} a different model than \`agents.defaults.model\`${defaultModel ? ` (${defaultModel})` : ""}`,
149-);
150-lines.push(`- Examples: ${mismatchExamples.join(", ")}`);
151-}
152-lines.push(
153-`Review with ${formatCliCommand("openclaw cron list")} and ${formatCliCommand("openclaw cron show <job-id>")}; remove \`payload.model\` from jobs that should inherit the default.`,
154-);
155-156-note(lines.join("\n"), "Cron");
157-}
158-159-async function readUserCrontab(): Promise<{ stdout: string; stderr?: string }> {
160-const result = await execFileAsync("crontab", ["-l"], {
161-encoding: "utf8",
162-windowsHide: true,
163-});
164-return {
165-stdout: result.stdout,
166-stderr: result.stderr,
167-};
168-}
169-170-function coerceCrontabText(crontab: unknown): string {
171-if (typeof crontab === "string") {
172-return crontab;
173-}
174-if (crontab == null) {
175-return "";
176-}
177-if (typeof crontab === "number" || typeof crontab === "boolean" || typeof crontab === "bigint") {
178-return String(crontab);
179-}
180-return "";
181-}
182-183-function findLegacyWhatsAppHealthCrontabLines(crontab: unknown): string[] {
184-return coerceCrontabText(crontab)
185-.split(/\r?\n/u)
186-.map((line) => line.trim())
187-.filter((line) => line.length > 0 && !line.startsWith("#"))
188-.filter((line) => LEGACY_WHATSAPP_HEALTH_SCRIPT_RE.test(line));
189-}
190-191-export async function collectLegacyWhatsAppCrontabHealthWarning(
192-params: {
193-platform?: NodeJS.Platform;
194-readCrontab?: CrontabReader;
195-} = {},
196-): Promise<string | null> {
197-if ((params.platform ?? process.platform) !== "linux") {
198-return null;
199-}
200-201-let crontab: unknown;
202-try {
203-crontab = (await (params.readCrontab ?? readUserCrontab)()).stdout;
204-} catch {
205-return null;
206-}
207-208-const legacyLines = findLegacyWhatsAppHealthCrontabLines(crontab);
209-if (legacyLines.length === 0) {
210-return null;
211-}
212-213-return [
214-"Legacy WhatsApp crontab health check detected.",
215-"`~/.openclaw/bin/ensure-whatsapp.sh` is not maintained by current OpenClaw and can misreport `Gateway inactive` from cron when the systemd user bus environment is missing.",
216-`Remove the stale crontab entry with ${formatCliCommand("crontab -e")}; use ${formatCliCommand("openclaw channels status --probe")}, ${formatCliCommand("openclaw doctor")}, and ${formatCliCommand("openclaw gateway status")} for current health checks.`,
217-`Matched ${pluralize(legacyLines.length, "entry")}.`,
218-].join("\n");
219-}
220-221-export async function noteLegacyWhatsAppCrontabHealthCheck(
222-params: {
223-platform?: NodeJS.Platform;
224-readCrontab?: CrontabReader;
225-} = {},
226-): Promise<void> {
227-const warning = await collectLegacyWhatsAppCrontabHealthWarning(params);
228-if (warning) {
229-note(warning, "Cron");
230-}
231-}
232-23354export async function maybeRepairLegacyCronStore(params: {
23455cfg: OpenClawConfig;
23556options: DoctorOptions;
23657prompter: Pick<DoctorPrompter, "confirm">;
23758}) {
238-const storePath = resolveCronStorePath(params.cfg.cron?.store);
59+const storePath = resolveCronJobsStorePath(params.cfg.cron?.store);
23960const quarantinePath = resolveCronQuarantinePath(storePath);
240-let store: Awaited<ReturnType<typeof loadCronStoreWithConfigJobs>>["store"];
61+let store: Awaited<ReturnType<typeof loadCronJobsStoreWithConfigJobs>>["store"];
24162let legacyStoreDetected = false;
24263let legacyRunLogDetected = false;
24364let legacyImportCount = 0;
24465let sqliteProjectionBackfillCount = 0;
24566try {
24667legacyStoreDetected = await legacyCronStoreFilesExist(storePath);
24768legacyRunLogDetected = await legacyCronRunLogFilesExist(storePath);
248-const loaded = await loadCronStoreWithConfigJobs(storePath);
69+const loaded = await loadCronJobsStoreWithConfigJobs(storePath);
24970const currentJobs =
25071loaded.configJobs.length > 0
25172 ? loaded.configJobs.map((job, index) =>
@@ -336,7 +157,7 @@ export async function maybeRepairLegacyCronStore(params: {
336157return;
337158}
338159if (legacyStoreDetected) {
339-await saveCronStore(storePath, { version: 1, jobs: [] });
160+await saveCronJobsStore(storePath, { version: 1, jobs: [] });
340161await archiveLegacyCronStoreForMigration(storePath);
341162}
342163const runLogMigration = legacyRunLogDetected
@@ -436,7 +257,7 @@ export async function maybeRepairLegacyCronStore(params: {
436257})),
437258});
438259}
439-await saveCronStore(storePath, {
260+await saveCronJobsStore(storePath, {
440261version: 1,
441262jobs: rawJobs as unknown as CronJob[],
442263});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。