diff --git a/CHANGELOG.md b/CHANGELOG.md index be74c157d0..254b20cc2f 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index f5fd784a8e..0749d5a0c0 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -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 diff --git a/docs/guide/scheduled-tasks.md b/docs/guide/scheduled-tasks.md index c5ca3b9e6c..e70c3cf951 100644 --- a/docs/guide/scheduled-tasks.md +++ b/docs/guide/scheduled-tasks.md @@ -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 diff --git a/frontend/src/core/scheduled-tasks/cron.ts b/frontend/src/core/scheduled-tasks/cron.ts index 23de102898..b03bf30ed5 100644 --- a/frontend/src/core/scheduled-tasks/cron.ts +++ b/frontend/src/core/scheduled-tasks/cron.ts @@ -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]), }, }; diff --git a/frontend/tests/unit/core/scheduled-tasks/cron.test.ts b/frontend/tests/unit/core/scheduled-tasks/cron.test.ts index 0dcc0ed703..e6ea1b5a6e 100644 --- a/frontend/tests/unit/core/scheduled-tasks/cron.test.ts +++ b/frontend/tests/unit/core/scheduled-tasks/cron.test.ts @@ -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"),