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;
+}