适用版本:ChengOS v0.1.0+ | 最后核对:2026-08-12 | 来源:
crates/cheng-core/src/domain/scheduled_task.rs、crates/cheng-core/src/services/tiered_scheduler.rs、crates/cheng-nodes/src/nodes/builtin/tools/schedule.rs、crates/cheng-api/src/rest/handlers/schedule/mod.rs
工作流有四种启动方式:用户手动运行、渠道消息到达、另一个工作流把它当子流程调用,或者时钟到点。本页讲时钟。
分层调度器
ChengOS 的定时不是一个 cron 进程,而是一个分层调度器,其设计目标是在多节点和进程重启下依然正确:
遥远的未来 即将到来 马上就到
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ PostgreSQL │ ───► │ Redis(温层) │ ───► │ 热轮 │
│ (唯一真相) │ │ warm_window │ │ DelayQueue │
│ │ │ 默认 300 秒 │ │ 默认 60 秒 │
└──────────────────┘ └──────────────────┘ └──────────────────┘
▲ │
│ PG 清扫器把滞留任务重新入队 ▼
└────────────────────────────────────────── 执行工作流
PostgreSQL 是唯一真相。Redis 承载温层窗口,并提供分布式执行锁。热轮是一个进程内的 DelayQueue,只装即将触发的任务。
任务 id 是 i64(PostgreSQL BIGSERIAL)而不是 UUID,正是为了能直接用作 DelayQueue 的键。
配置
| 设置 | 默认 | 开发 | 生产 |
|---|---|---|---|
hot_window_secs |
60 | 10 | 60 |
warm_window_secs |
300 | 60 | 300 |
load_batch_size |
100 | 20 | 500 |
sweeper_interval_secs |
30 | 10 | 30 |
stale_threshold_secs |
30 | 15 | 30 |
lock_timeout_secs |
60 | 30 | 120 |
max_concurrency |
16 | 4 | 64 |
heartbeat_interval_secs |
5 | 3 | 5 |
execution_timeout_secs |
300 | 60 | 300 |
crash_recovery_interval_secs |
60 | 20 | 60 |
warm_window_secs 必须大于等于 hot_window_secs;配置会校验这一点,而不是留下一个能让任务掉进去的缝隙。
可靠性
三套机制,各自覆盖一类故障:
- PG 清扫器把逾期超过
stale_threshold_secs的任务重新入队——覆盖 Redis 条目丢失。 - 节点心跳每
heartbeat_interval_secs一次;沉默超过node_stale_threshold_secs的节点被判定为死亡。 - 崩溃恢复(仅 Leader 执行)找出卡在
running且超过execution_timeout_secs的任务,或重试或永久判失败。
认领任务使用乐观锁;冲突的认领会以锁冲突失败,而不是重复执行。
任务生命周期
┌─────────┐ claim() ┌─────────┐
│ Pending │ ──────────► │ Running │
└─────────┘ └─────────┘
▲ │
│ schedule_retry() ┌────┴──────────────┐
│ ▼ ▼
│ ┌──────────┐ ┌────────┐
└──────────────│ Pending │ │ Failed │ (超过最大重试)
└──────────┘ └────────┘
另有:Completed、Cancelled、Paused
成功完成的周期性任务会被自动重新排到下一个 cron 时间;一次性任务在 completed 处结束。
周期规则与时区
{ "timezone": "Asia/Shanghai", "cron": "0 8 * * *" }
timezone 是 IANA 标识符,对周期性任务是必填的。它不是装饰:调度器需要它才能在夏令时切换时正确算出下一次触发。「每天 08:00」在夏天和冬天对应不同的 UTC 时刻,只用 UTC 的 cron 每年会错两次。
载荷还携带 params(交给工作流)、可选的 label,以及创建该定时任务的 user_id / tenant_id / workspace_id 作用域。
定时节点
tools/schedule 让你在工作流内部管理定时任务——包括从智能体里管理,这正是「每天早上提醒我」能以对话方式实现的原因。
动作:list_tasks、get_task、get_execution_result、create_task、update_task、pause_task、resume_task、cancel_task、delete_task。
模式:manual(用户填参数)或 llm(模型填参数)。
run_at 接受三种格式
| 格式 | 例子 | 结果 |
|---|---|---|
| 相对延迟 | 2m、30m、1h、1d |
等待之后运行一次 |
| cron 表达式 | 0 9 * * *、0 9 * * 1、0 9 * * 0,6 |
周期运行 |
| 本地墙钟时间 | 2026-06-30T09:00 |
在该时刻运行一次 |
日期时间格式不带时区也不带偏移——节点自己套用服务器时区。一次性日期时间必须在未来,如果不是,校验错误会报出当前服务器时间,让调用方据此纠正。这个细节的存在是因为:一个被要求安排日程的模型,否则会去猜今天是几号,然后猜错。
timezone 被刻意设为不可由 LLM 填写的字段:节点自动使用宿主的系统时区,于是用户只需说出墙钟时间,模型永远不必去挑时区。它仍作为可选的手动覆盖项,供界面和其他非 LLM 调用方使用。
frequency 是比裸 cron 更友好的替代:once、every_minute、hourly、daily、weekly、monthly,或 custom 配 custom_cron。
输入与名称——务必避开的错误
有三个字段很容易混淆:
| 字段 | 是 | 不是 |
|---|---|---|
task_text |
工作流的输入,纯文本——对以对话打头的工作流会成为 raw_input |
名称 |
params |
显式的输入对象,如 {"raw_input": "hello"}——恰是入口节点收到的东西 |
名称 |
label |
在列表与日志中显示的简短描述性名称 | 任务内容 |
内容用 task_text 或 params。务必设置 label,控制在 20 字以内并写得有描述性——一份全是 任务1、test、123 的任务列表,会逼你按 id 逐个查询才能找到目标。
其他选项:priority(默认 0)、max_retries(默认 3)、用于把 task_text 路由到特定入口节点的 target_node_id,以及 list_tasks 用的 limit(默认 50,最大 500)/offset/status。
REST 接口
GET|POST /schedules 列出、创建
GET /schedules/metrics 调度器指标
GET|PATCH|DELETE /schedules/:id 获取、更新、取消(软)
GET /schedules/:id/executions 该任务的执行历史
DELETE /schedules/:id/hard 硬删除
POST /schedules/:id/pause
POST /schedules/:id/resume
DELETE /schedules/:id 是取消,/hard 才删除记录行。cron 表达式在创建和更新时校验——非法表达式在那时就被拒绝,而不是等到下一次滴答才悄悄失败。
选择模式
| 你想要 | 这样做 |
|---|---|
| 今天晚些时候跑一次 | run_at: "2h" 或一个本地日期时间 |
| 每天早上 09:00 | run_at: "0 9 * * *" 并带上时区 |
| 用户在对话里创建的提醒 | 一个挂了 tools/schedule、模式为 llm 的智能体 |
| 周期性重建索引 | 一个指向你索引工作流的定时任务——按版本幂等 |
| 对外部事件做出反应 | 那不是定时任务——用渠道 |
下一步
- 执行模型——任务触发之后发生了什么。
- 轨迹、日志与回放——查看一次定时运行。
- 存储、Redis 与向量配置——调度器所需的 Redis。

暂无评论内容