生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~200k tokens,输出 ~6k tokens(本节)
AI Agent 不应该只在用户主动对话时才工作。OpenClaw 的 Cron 系统让 Agent 可以按时间计划自动执行任务——每天早上发送新闻摘要、每小时检查邮箱、在指定时间提醒用户等。本节分析 Cron 系统的核心设计:服务类、调度引擎、表达式解析和作业规范化。
18.1.1 Cron 服务(src/cron/service.ts)
CronService 是 Cron 系统的对外门面(Facade),它封装了所有定时任务的管理操作:
// src/cron/service.ts
export class CronService {
private readonly state;
constructor(deps: CronServiceDeps) {
this.state = createCronServiceState(deps);
}
async start() { await ops.start(this.state); }
stop() { ops.stop(this.state); }
async status() { return await ops.status(this.state); }
async list(opts?: { includeDisabled?: boolean }) { ... }
async add(input: CronJobCreate) { return await ops.add(this.state, input); }
async update(id: string, patch: CronJobPatch) { ... }
async remove(id: string) { ... }
async run(id: string, mode?: "due" | "force") { ... }
wake(opts: { mode: "now" | "next-heartbeat"; text: string }) {
return ops.wakeNow(this.state, opts);
}
}
所有实际逻辑都委托给 service/ops.ts 中的纯函数,CronService 本身只是一个薄包装。这种设计便于测试——可以直接测试 ops 函数而不必实例化整个服务。
CronServiceDeps 定义了 Cron 服务所需的所有外部依赖:
在隔离会话中运行 Agent(用于独立的 Cron 任务)
事件回调,Gateway 用它向客户端广播 Cron 事件
store 字段是关键——所有作业数据都以 JSON 文件存储在磁盘上(默认 ~/.openclaw/cron/jobs.json),启动时加载到内存,修改后写回磁盘。这是一种"文件即数据库"的简单持久化方案,适合单用户场景。
启动流程中有两个重要的恢复机制:
清除残留标记——如果进程在作业执行期间崩溃,runningAtMs 标记会残留,需要在启动时清除
补跑错过的作业——runMissedJobs 会找出所有 nextRunAtMs 已过期的启用作业并立即执行
所有操作通过 locked() 函数序列化,防止并发修改导致数据竞争:
衍生解释——操作序列化:在单线程的 Node.js 中,虽然没有真正的线程并发问题,但 async/await 可以导致交错执行。例如,两个并发的 add() 调用可能同时读取存储、同时修改、同时写回,导致一个操作的修改被覆盖。locked() 通过 Promise 链确保操作按顺序执行,相当于一个异步互斥锁(Mutex)。
18.1.2 调度引擎(src/cron/schedule.ts)与 Croner 库
OpenClaw 支持三种定时调度方式:
computeNextRunAtMs:下次运行时间计算
every 类型的锚点机制:anchorMs 是间隔的参考起始点。下次运行时间通过 anchor + N × everyMs 计算(取大于 nowMs 的最小 N)。这确保了即使进程重启,间隔也是从原始锚点计算,而非从重启时刻开始。
衍生解释——Croner:Croner 是一个轻量级的 Cron 表达式解析库(纯 JavaScript,无依赖)。它支持标准的 5 字段 Cron 表达式(分 时 日 月 周),并提供 nextRun() 方法计算下次匹配时间。OpenClaw 使用它来解析 Cron 表达式,但不使用它的作业调度功能——调度逻辑由 OpenClaw 自己的 armTimer 实现。
Cron 表达式的时区默认为系统时区,用户可通过 tz 字段指定 IANA 时区标识(如 "America/New_York"、"Asia/Shanghai")。
armTimer 设置下一次唤醒的 setTimeout:
为什么限制 1 分钟最大延迟? 注释说明了原因:"避免调度偏移,并在进程被暂停或系统时钟跳变时快速恢复"。JavaScript 的 setTimeout 在系统休眠(如笔记本合盖)后可能偏移很大,每分钟唤醒一次可以检测到这种情况并及时触发到期的作业。
18.1.3 Cron 表达式解析(src/cron/parse.ts)
parse.ts 负责解析各种时间格式为 Unix 毫秒时间戳:
支持的时间格式:
补全为 UTC(2026-03-01T09:00:00Z)
"2026-03-01T09:00:00+08:00"
18.1.4 Cron 规范化(src/cron/normalize.ts)
Cron 作业通常由 Agent 通过工具调用创建。LLM 生成的参数可能:
大小写不一致("SystemEvent" vs "systemEvent")
使用旧版字段名(atMs vs at,deliver vs delivery)
字段位置不对(model 放在顶层而非 payload 内)
normalize.ts 的职责就是将这些"脏输入"规范化为统一的内部格式。
normalizeCronJobCreate 和 normalizeCronJobPatch 的区别在于是否应用默认值:
关键默认值:
"main"(systemEvent)/ "isolated"(agentTurn)
CronService 是 Cron 系统的门面类,所有逻辑委托给 ops 模块的纯函数
通过依赖注入实现可测试性——时间源、日志、Agent 执行器均可注入
所有操作通过 Promise 链序列化,避免异步并发导致的数据竞争
支持三种调度类型:一次性(at)、固定间隔(every)、Cron 表达式(cron)
调度引擎使用 Croner 库解析 Cron 表达式,定时器限制最大 1 分钟延迟以应对系统休眠
时间解析支持 Unix 毫秒、ISO 日期、ISO 日期时间等多种格式,缺失时区默认 UTC
规范化模块处理 LLM 生成的不可靠输入——大小写修正、旧版字段迁移、自动推断类型、默认值填充
create 和 patch 操作使用不同的规范化策略——create 应用默认值,patch 只修改显式字段