diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index 3ceff0654..a96f2951d 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -229,35 +229,49 @@ export class Runtime { // 如果有nextruntime,则加入重试队列 this.joinRetryList(script); this.crontabSripts.push(script); - let flag = false; + + const ERROR_MESSAGES: Record = { + 0: "crontabScript: cron expression failed", + 2: "crontabScript: onTick creation failed", + 4: "crontabScript: create cronjob failed", + 6: "crontabScript: cronjob start failed", + }; + + const logError = (ok: number, val: string, e: unknown) => + this.logger.error( + ERROR_MESSAGES[ok] ?? "crontabScript: execution failed", + { uuid: script.uuid, crontab: val }, + Logger.E(e) + ); + const cronJobList: Array = []; script.metadata.crontab.forEach((val) => { - const { cronExpr, oncePos } = extractCronExpr(val); + let ok = 0; try { - const cron = new CronJob(cronExpr, this.crontabExec(script, oncePos)); + const { cronExpr, oncePos } = extractCronExpr(val); + ok = 2; + const onTick = this.crontabExec(script, oncePos); + ok = 4; + const cron = new CronJob(cronExpr, onTick); + ok = 6; cron.start(); + ok = 8; cronJobList.push(cron); } catch (e) { - flag = true; - this.logger.error( - "create cronjob failed", - { - uuid: script.uuid, - crontab: val, - }, - Logger.E(e) - ); + logError(ok, val, e); } }); - if (cronJobList.length !== script.metadata.crontab.length) { + + const allSucceeded = cronJobList.length === script.metadata.crontab.length; + if (allSucceeded) { + this.cronJob.set(script.uuid, cronJobList); + } else { // 有表达式失败了 for (const crontab of cronJobList) { crontab.stop(); } - } else { - this.cronJob.set(script.uuid, cronJobList); } - return !flag; + return allSucceeded; } crontabExec(script: ScriptLoadInfo, oncePos: number) { diff --git a/src/pkg/utils/cron.ts b/src/pkg/utils/cron.ts index 35bdf3091..01ca7a405 100644 --- a/src/pkg/utils/cron.ts +++ b/src/pkg/utils/cron.ts @@ -81,12 +81,18 @@ type NextTimeResult = { * 对外展示用方法。 * * - 若为 once cron,返回「下次在 xx 执行一次」的国际化文案 + * - 若表达式无效,返回本地化的错误提示文案 * - 否则直接返回下一次执行时间字符串 */ export const nextTimeDisplay = (crontab: string, date = new Date()): string => { - const res = nextTimeInfo(crontab, date); - const nextTimeFormatted = res.next.toFormat(res.format); - return res.once ? t(`cron_oncetype.${res.once}`, { next: nextTimeFormatted }) : nextTimeFormatted; + try { + const res = nextTimeInfo(crontab, date); + const nextTimeFormatted = res.next.toFormat(res.format); + return res.once ? t(`cron_oncetype.${res.once}`, { next: nextTimeFormatted }) : nextTimeFormatted; + } catch (e) { + console.error(`nextTimeDisplay: Invalid cron expression "${crontab}"`, e); + return t("cron_invalid_expr"); + } }; /** diff --git a/src/pkg/utils/script.ts b/src/pkg/utils/script.ts index 76a5a5600..590a2e0ed 100644 --- a/src/pkg/utils/script.ts +++ b/src/pkg/utils/script.ts @@ -12,7 +12,7 @@ import { } from "@App/app/repo/scripts"; import type { Subscribe } from "@App/app/repo/subscribe"; import { SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe"; -import { nextTimeDisplay } from "./cron"; +import { extractCronExpr } from "./cron"; import { parseUserConfig } from "./yaml"; import { t as i18n_t } from "@App/locales/locales"; import { readBlobContent } from "@App/pkg/utils/encoding"; @@ -81,7 +81,7 @@ export function parseScriptFromCode(code: string, origin: string, uuid?: string) if (metadata.crontab !== undefined) { type = SCRIPT_TYPE_CRONTAB; try { - nextTimeDisplay(metadata.crontab[0]); + extractCronExpr(metadata.crontab[0]); } catch { throw new Error(i18n_t("error_cron_invalid", { expr: metadata.crontab[0] })); } @@ -136,7 +136,7 @@ export async function prepareScriptByCode( dao?: ScriptDAO, options?: { byEditor?: boolean; // 是否通过编辑器导入 - byWebRequest?: boolean; // 是否通过網頁連結安裝或更新 + byWebRequest?: boolean; // 是否通过网页连结安装或更新 } ): Promise<{ script: Script; oldScript?: Script; oldScriptCode?: string }> { dao = dao ?? new ScriptDAO(); diff --git a/src/pkg/utils/utils.test.ts b/src/pkg/utils/utils.test.ts index a1ea5627c..80cd69f57 100644 --- a/src/pkg/utils/utils.test.ts +++ b/src/pkg/utils/utils.test.ts @@ -62,6 +62,23 @@ const assertNextTimeInfo = (expr: string, date: Date, expected: any) => { .toEqual(expected); }; +describe.concurrent("nextTimeDisplay ERROR SAFE", () => { + it.concurrent.each([ + ["* * * once * once"], + ["* * once * once"], + ["* once(2,4) once(4-5) * *"], + ["* * 1 A *"], + ["* once 1.2 * *"], + ["* 3 1**2 * *"], + ["* 1^2 F * *"], + ["1 1 * *"], + ["* 3"], + ])("错误Cron表达式: %s", (expr) => { + // 确保无效表达式不会抛出异常 + expect(() => nextTimeDisplay(expr)).not.toThrow(); + }); +}); + describe.concurrent("nextTimeInfo1", () => { const date = new Date("2025-12-17T11:47:17.629"); // 2025-12-17 11:47:17.629 (本地时区)