This commit is contained in:
Fu Diwei 2025-09-05 21:18:14 +08:00
parent 1b0f652de3
commit 299e2202fd
3 changed files with 98 additions and 9 deletions

View File

@ -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 <em>crontab</em> 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. <br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Lets Encrypt (ACME) client run at a random time?</a>",

View File

@ -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": "五段式表达式。<br>支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式。",
"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": "五段式表达式,使用 <em>crontab</em> 标准语法规则。<br>支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式。",
"workflow_node.start.form.trigger_cron.help": "预计最近 5 次运行时间(实际时区以服务器设置为准):",
"workflow_node.start.form.trigger_cron.guide": "如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>",

View File

@ -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<number> {
const slots = new Set<number>();
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;
}