diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 03aa09be..60ed9ca9 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -11,10 +11,10 @@ "workflow_node.start.form.trigger.placeholder": "Please select trigger", "workflow_node.start.form.trigger.option.scheduled.label": "Scheduled", "workflow_node.start.form.trigger.option.manual.label": "Manual", - "workflow_node.start.form.trigger_cron.label": "Cron expression", - "workflow_node.start.form.trigger_cron.placeholder": "Please enter cron expression", - "workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression", - "workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments.", + "workflow_node.start.form.trigger_cron.label": "CRON expression", + "workflow_node.start.form.trigger_cron.placeholder": "Please enter CRON expression", + "workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid CRON expression", + "workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments, in standard crontab rules.", "workflow_node.start.form.trigger_cron.help": "Expected execution time for the last 5 times (the actual time zone is based on the server):", "workflow_node.start.form.trigger_cron.guide": "If you have multiple workflows, it is recommended to set them to run at different times of the day instead of always running at a specific time. And please don't always set it to midnight every day to avoid spikes in traffic.

Reference links:
1. Let’s Encrypt rate limits
2. Why should my Let’s Encrypt (ACME) client run at a random time?", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 242a1957..e214f6e9 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -11,10 +11,10 @@ "workflow_node.start.form.trigger.placeholder": "请选择触发方式", "workflow_node.start.form.trigger.option.scheduled.label": "定时触发", "workflow_node.start.form.trigger.option.manual.label": "手动触发", - "workflow_node.start.form.trigger_cron.label": "Cron 表达式", - "workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式", - "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式", - "workflow_node.start.form.trigger_cron.tooltip": "五段式表达式。
支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式。", + "workflow_node.start.form.trigger_cron.label": "CRON 表达式", + "workflow_node.start.form.trigger_cron.placeholder": "请输入 CRON 表达式", + "workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 CRON 表达式", + "workflow_node.start.form.trigger_cron.tooltip": "五段式表达式,使用 crontab 标准语法规则。
支持使用任意值(即 *)、值列表分隔符(即 ,)、值的范围(即 -)、步骤值(即 /)等四种表达式。", "workflow_node.start.form.trigger_cron.help": "预计最近 5 次运行时间(实际时区以服务器设置为准):", "workflow_node.start.form.trigger_cron.guide": "如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", diff --git a/ui/src/utils/cron.ts b/ui/src/utils/cron.ts index 46d806e3..8e3dd7e3 100644 --- a/ui/src/utils/cron.ts +++ b/ui/src/utils/cron.ts @@ -4,7 +4,16 @@ export const validCronExpression = (expr: string): boolean => { try { CronExpressionParser.parse(expr); - if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式 + // pocketbase 后端仅支持标准 crontab 形式的表达式 + // 这里转译了来自 pocketbase 的 golang 代码来验证 + const segments = expr.trim().split(" "); + if (segments.length !== 5) return false; + parseCronSegment(segments[0], 0, 59); + parseCronSegment(segments[1], 0, 23); + parseCronSegment(segments[2], 1, 31); + parseCronSegment(segments[3], 1, 12); + parseCronSegment(segments[4], 0, 6); + return true; } catch { return false; @@ -19,3 +28,83 @@ export const getNextCronExecutions = (expr: string, times = 1): Date[] => { return cron.take(times).map((date) => date.toDate()); }; + +// transpile from: +// https://github.com/pocketbase/pocketbase/blob/5d964c1b1d020f425299b32df03ecf44e0a0502e/tools/cron/schedule.go#L141-L218 +function parseCronSegment(segment: string, min: number, max: number): Set { + const slots = new Set(); + + const list = segment.split(","); + for (const p of list) { + const stepParts = p.split("/"); + + let step: number; + switch (stepParts.length) { + case 1: + { + step = 1; + } + break; + case 2: + { + const parsedStep = parseInt(stepParts[1], 10); + if (isNaN(parsedStep) || parsedStep < 1 || parsedStep > max) { + throw new Error(`invalid segment step boundary - the step must be between 1 and the ${max}`); + } + step = parsedStep; + } + break; + + default: + throw new Error("invalid segment step format - must be in the format */n or 1-30/n"); + } + + let rangeMin: number, rangeMax: number; + if (stepParts[0] === "*") { + rangeMin = min; + rangeMax = max; + } else { + const rangeParts = stepParts[0].split("-"); + switch (rangeParts.length) { + case 1: + { + if (step !== 1) { + throw new Error("invalid segment step - step > 1 could be used only with the wildcard or range format"); + } + const parsed = parseInt(rangeParts[0], 10); + if (isNaN(parsed) || parsed < min || parsed > max) { + throw new Error("invalid segment value - must be between the min and max of the segment"); + } + rangeMin = parsed; + rangeMax = rangeMin; + } + break; + + case 2: + { + const parsedMin = parseInt(rangeParts[0], 10); + if (isNaN(parsedMin) || parsedMin < min || parsedMin > max) { + throw new Error(`invalid segment range minimum - must be between ${min} and ${max}`); + } + rangeMin = parsedMin; + + const parsedMax = parseInt(rangeParts[1], 10); + if (isNaN(parsedMax) || parsedMax < rangeMin || parsedMax > max) { + throw new Error(`invalid segment range maximum - must be between ${rangeMin} and ${max}`); + } + rangeMax = parsedMax; + } + break; + + default: + throw new Error("invalid segment range format - the range must have 1 or 2 parts"); + } + } + + for (let i = rangeMin; i <= rangeMax; i += step) { + slots.add(i); + } + } + + return slots; +}