mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-15 07:41:20 +00:00
fix(frontend): treat invalid cron fields as non-scheduled (#8020)
* fix(frontend): keep invalid cron schedules as custom * test(frontend): cover inclusive cron bounds * docs(scheduled-tasks): note cron preset bounds The form keeps an out-of-range cron as custom; the guide now says which numeric ranges a preset covers and that saving such an expression returns 422. CHANGELOG entries cover the fix. * docs(scheduler): clarify invalid cron save validation --------- Co-authored-by: Nan Gao <thunderggnn@gmail.com>
This commit is contained in:
parent
4f3a8b790d
commit
35d33bea75
10
CHANGELOG.md
10
CHANGELOG.md
@ -68,6 +68,15 @@ This section accumulates work toward the **2.3.0** milestone
|
||||
|
||||
### Fixed
|
||||
|
||||
- **frontend:** Cron presets in the task form now require minute 0-59, hour
|
||||
0-23 and day of month 1-31, so an out-of-range expression such as
|
||||
`0 24 * * *` stays a custom schedule and keeps its original text. Previously,
|
||||
it was parsed as a preset and could be clamped to a different time when
|
||||
serialized for submission. Creating or duplicating a task with an invalid
|
||||
expression, or updating its schedule configuration while the expression is
|
||||
invalid, returns the Gateway's `422`; edits to other fields do not revalidate
|
||||
an unchanged schedule. ([#8020])
|
||||
|
||||
- **gateway:** Creating a thread with checkpoint-owned keys such as `step`,
|
||||
`parents` or `counters_since_delta_snapshot` in its metadata no longer
|
||||
leaves the thread broken. Those keys used to land on the thread's first
|
||||
@ -16415,6 +16424,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#7998]: https://github.com/bytedance/deer-flow/issues/7998
|
||||
[#8006]: https://github.com/bytedance/deer-flow/pull/8006
|
||||
[#8019]: https://github.com/bytedance/deer-flow/pull/8019
|
||||
[#8020]: https://github.com/bytedance/deer-flow/pull/8020
|
||||
[#8043]: https://github.com/bytedance/deer-flow/pull/8043
|
||||
[#8046]: https://github.com/bytedance/deer-flow/pull/8046
|
||||
[#8057]: https://github.com/bytedance/deer-flow/pull/8057
|
||||
|
||||
@ -33,6 +33,12 @@
|
||||
|
||||
### 修复
|
||||
|
||||
- **前端:** 任务表单的 Cron 预设现在要求分钟 0-59、小时 0-23、日期 1-31,
|
||||
超出范围的表达式(例如 `0 24 * * *`)会保持为自定义并保留原始文本。此前它会被识别为预设,
|
||||
在序列化以供提交时可能被限制到合法范围内的另一个时间。使用非法表达式创建或复制任务,
|
||||
或在表达式仍非法时更新调度配置,会由 Gateway 返回 `422`;仅修改其他字段不会重新校验
|
||||
未变化的调度配置。([#8020])
|
||||
|
||||
- **网关:** 创建对话时在元数据中带上 `step`、`parents`、`counters_since_delta_snapshot`
|
||||
等由检查点自身维护的键,不再让该对话损坏。此前这些键会写入对话的首个检查点,错误的 `parents`
|
||||
会让该对话的历史记录永久返回 500,错误的 `step` 或增量计数器会让之后每一轮对话都失败;
|
||||
@ -12903,6 +12909,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#7998]: https://github.com/bytedance/deer-flow/issues/7998
|
||||
[#8006]: https://github.com/bytedance/deer-flow/pull/8006
|
||||
[#8019]: https://github.com/bytedance/deer-flow/pull/8019
|
||||
[#8020]: https://github.com/bytedance/deer-flow/pull/8020
|
||||
[#8043]: https://github.com/bytedance/deer-flow/pull/8043
|
||||
[#8046]: https://github.com/bytedance/deer-flow/pull/8046
|
||||
[#8064]: https://github.com/bytedance/deer-flow/pull/8064
|
||||
|
||||
@ -48,6 +48,8 @@ Enable background polling with `config.yaml -> scheduler.enabled`. Manual trigge
|
||||
|
||||
In the task form, switching an hourly, daily, weekly, or monthly schedule to **Custom cron** starts from its current timing, including selected weekdays or day of the month. Switching back to Custom cron after preset edits uses those edits rather than an older expression.
|
||||
|
||||
Presets cover minute 0-59, hour 0-23 and day of month 1-31. An expression outside those ranges stays Custom and keeps its original text. Creating or duplicating a task with an invalid expression, or updating its schedule configuration while the expression is invalid, returns `422` from the Gateway. Editing only other fields, such as the title, leaves the schedule configuration out of the update request and does not revalidate the unchanged expression.
|
||||
|
||||
## Control proactive IM notifications
|
||||
|
||||
Open **Settings → Notifications → IM notifications** to turn scheduled-task
|
||||
|
||||
@ -239,6 +239,12 @@ function isStar(field: string): boolean {
|
||||
return field === "*";
|
||||
}
|
||||
|
||||
function cronNumber(field: string, min: number, max: number): number | null {
|
||||
if (!/^\d+$/.test(field)) return null;
|
||||
const value = Number(field);
|
||||
return value >= min && value <= max ? value : null;
|
||||
}
|
||||
|
||||
function isSimpleDowList(field: string): boolean {
|
||||
return field.split(",").every((tok) => /^[0-7]$/.test(tok));
|
||||
}
|
||||
@ -258,26 +264,23 @@ export function parseCron(cron: string): {
|
||||
const domF = fields[2]!;
|
||||
const monF = fields[3]!;
|
||||
const dowF = fields[4]!;
|
||||
const numMinute = /^\d+$/.test(mF);
|
||||
const numHour = /^\d+$/.test(hF);
|
||||
const numDom = /^\d+$/.test(domF);
|
||||
const minute = cronNumber(mF, 0, 59);
|
||||
const hour = cronNumber(hF, 0, 23);
|
||||
const dayOfMonth = cronNumber(domF, 1, 31);
|
||||
const stars = isStar(domF) && isStar(monF);
|
||||
|
||||
// hourly: "M * * * *"
|
||||
if (numMinute && isStar(hF) && stars && isStar(dowF)) {
|
||||
return { preset: "hourly", parts: { minute: Number(mF) } };
|
||||
if (minute !== null && isStar(hF) && stars && isStar(dowF)) {
|
||||
return { preset: "hourly", parts: { minute } };
|
||||
}
|
||||
// daily: "M H * * *"
|
||||
if (numMinute && numHour && stars && isStar(dowF)) {
|
||||
return {
|
||||
preset: "daily",
|
||||
parts: { minute: Number(mF), hour: Number(hF) },
|
||||
};
|
||||
if (minute !== null && hour !== null && stars && isStar(dowF)) {
|
||||
return { preset: "daily", parts: { minute, hour } };
|
||||
}
|
||||
// weekly: "M H * * DOW" (simple comma list of single digits only)
|
||||
if (
|
||||
numMinute &&
|
||||
numHour &&
|
||||
minute !== null &&
|
||||
hour !== null &&
|
||||
isStar(domF) &&
|
||||
isStar(monF) &&
|
||||
!isStar(dowF) &&
|
||||
@ -291,15 +294,21 @@ export function parseCron(cron: string): {
|
||||
if (ordered.length > 0) {
|
||||
return {
|
||||
preset: "weekly",
|
||||
parts: { minute: Number(mF), hour: Number(hF), weekdays: ordered },
|
||||
parts: { minute, hour, weekdays: ordered },
|
||||
};
|
||||
}
|
||||
}
|
||||
// monthly: "M H DOM * *"
|
||||
if (numMinute && numHour && numDom && isStar(monF) && isStar(dowF)) {
|
||||
if (
|
||||
minute !== null &&
|
||||
hour !== null &&
|
||||
dayOfMonth !== null &&
|
||||
isStar(monF) &&
|
||||
isStar(dowF)
|
||||
) {
|
||||
return {
|
||||
preset: "monthly",
|
||||
parts: { minute: Number(mF), hour: Number(hF), dayOfMonth: Number(domF) },
|
||||
parts: { minute, hour, dayOfMonth },
|
||||
};
|
||||
}
|
||||
return { preset: "custom", parts: { raw: expr } };
|
||||
@ -395,13 +404,14 @@ function parseDisplayCron(cron: string): {
|
||||
}
|
||||
const fields = cron.split(/\s+/);
|
||||
const [m, h, dom, mon, dow] = fields;
|
||||
if (fields.length !== 5 || !m || !h || !dow) {
|
||||
return parsed;
|
||||
}
|
||||
const minute = cronNumber(m, 0, 59);
|
||||
const hour = cronNumber(h, 0, 23);
|
||||
if (
|
||||
fields.length !== 5 ||
|
||||
!m ||
|
||||
!h ||
|
||||
!dow ||
|
||||
!/^\d+$/.test(m) ||
|
||||
!/^\d+$/.test(h) ||
|
||||
minute === null ||
|
||||
hour === null ||
|
||||
dom !== "*" ||
|
||||
mon !== "*" ||
|
||||
!/^[0-7](-[0-7])?(,[0-7](-[0-7])?)*$/.test(dow)
|
||||
@ -422,8 +432,8 @@ function parseDisplayCron(cron: string): {
|
||||
return {
|
||||
preset: "weekly",
|
||||
parts: {
|
||||
minute: Number(m),
|
||||
hour: Number(h),
|
||||
minute,
|
||||
hour,
|
||||
weekdays: orderedWeekdays([...days]),
|
||||
},
|
||||
};
|
||||
|
||||
@ -132,6 +132,21 @@ describe("parseCron", () => {
|
||||
expect(parseCron("garbage").preset).toBe("custom");
|
||||
expect(parseCron("garbage").parts.raw).toBe("garbage");
|
||||
});
|
||||
|
||||
test.each([
|
||||
"60 * * * *",
|
||||
"99 9 * * *",
|
||||
"0 24 * * *",
|
||||
"0 9 0 * *",
|
||||
"0 9 32 * *",
|
||||
])("keeps the out-of-range cron %j as custom", (cron) => {
|
||||
expect(parseCron(cron)).toEqual({ preset: "custom", parts: { raw: cron } });
|
||||
});
|
||||
|
||||
test("accepts inclusive upper bounds for numeric fields", () => {
|
||||
expect(parseCron("59 23 * * *").preset).toBe("daily");
|
||||
expect(parseCron("0 9 31 * *").preset).toBe("monthly");
|
||||
});
|
||||
});
|
||||
|
||||
describe("describeSchedule", () => {
|
||||
@ -601,6 +616,13 @@ describe("describeTaskSchedule", () => {
|
||||
).toBe("工作日 08:30 (Asia/Shanghai)");
|
||||
});
|
||||
|
||||
test("max valid weekdays still read as weekdays", () => {
|
||||
expect(describeTaskSchedule(cronTask("59 23 * * 1-5"), "en-US")).toEqual({
|
||||
text: "Weekdays at 23:59 (Asia/Shanghai)",
|
||||
raw: "59 23 * * 1-5",
|
||||
});
|
||||
});
|
||||
|
||||
test("daily reuses the preset wording", () => {
|
||||
expect(describeTaskSchedule(cronTask("0 9 * * *"), "en-US").text).toBe(
|
||||
"Every day at 09:00 (Asia/Shanghai)",
|
||||
@ -610,6 +632,16 @@ describe("describeTaskSchedule", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test.each(["60 9 * * 1-5", "0 24 * * 1-5"])(
|
||||
"does not describe the out-of-range cron %j as a preset",
|
||||
(cron) => {
|
||||
expect(describeTaskSchedule(cronTask(cron), "en-US")).toEqual({
|
||||
text: "Custom schedule (Asia/Shanghai)",
|
||||
raw: cron,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("a custom cron never shows the expression in text", () => {
|
||||
const { text, raw } = describeTaskSchedule(
|
||||
cronTask("*/15 9-17 * * 1-5"),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user