18.2 Cron 作业执行

生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~210k tokens,输出 ~6k tokens(本节)


上一节介绍了 Cron 系统的调度设计。本节深入作业的实际执行:如何区分主会话作业和隔离作业、投递计划如何决定消息去向、运行日志如何记录、以及作业存储的持久化机制。


18.2.1 隔离 Agent 执行(src/cron/isolated-agent.ts

两种执行模式

Cron 作业有两种执行模式,由 sessionTarget 字段决定:

模式

sessionTarget

负载类型

工作方式

主会话

"main"

systemEvent

向主会话注入系统事件文本,触发心跳

隔离会话

"isolated"

agentTurn

启动独立的 Agent 会话执行任务

// src/cron/service/timer.ts — executeJobCore(简化版)

async function executeJobCore(state, job) {
  // 模式一:主会话执行
  if (job.sessionTarget === "main") {
    const text = resolveJobPayloadTextForMain(job);
    if (!text) return { status: "skipped", error: "requires non-empty text" };

    // 注入系统事件
    state.deps.enqueueSystemEvent(text, { agentId: job.agentId });

    // 唤醒 Agent 处理
    if (job.wakeMode === "now" && state.deps.runHeartbeatOnce) {
      // 同步模式:等待心跳完成(最多 2 分钟)
      for (;;) {
        const result = await state.deps.runHeartbeatOnce({ reason: `cron:${job.id}` });
        if (result.status !== "skipped" || result.reason !== "requests-in-flight") break;
        if (state.deps.nowMs() - waitStartedAt > 2 * 60_000) {
          state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
          return { status: "ok", summary: text };
        }
        await delay(250);
      }
    } else {
      // 异步模式:仅请求心跳,不等待
      state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
    }
    return { status: "ok", summary: text };
  }

  // 模式二:隔离会话执行
  if (job.payload.kind !== "agentTurn") {
    return { status: "skipped", error: "isolated job requires agentTurn" };
  }
  const res = await state.deps.runIsolatedAgentJob({
    job,
    message: job.payload.message,
  });

  // 将摘要回传到主会话(如果配置了投递)
  const deliveryPlan = resolveCronDeliveryPlan(job);
  if (res.summary?.trim() && deliveryPlan.requested) {
    state.deps.enqueueSystemEvent(`Cron: ${res.summary}`, { agentId: job.agentId });
    if (job.wakeMode === "now") {
      state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
    }
  }

  return { status: res.status, error: res.error, summary: res.summary };
}

衍生解释——心跳(Heartbeat):在 OpenClaw 中,心跳是 Agent 的"处理循环"。Agent 不是一直运行的——它在收到消息或系统事件时被唤醒,处理完后回到休眠状态。requestHeartbeatNow 请求 Agent 立即唤醒并处理队列中的事件,runHeartbeatOnce 则同步等待一次心跳完成。Cron 的主会话模式通过注入系统事件 + 触发心跳来让 Agent "看到"定时消息。

隔离 Agent 的运行细节

runCronIsolatedAgentTurn(位于 src/cron/isolated-agent/run.ts)是隔离执行的核心函数。它完成以下工作:

  1. 会话管理——为每次 Cron 运行创建或复用会话,会话键格式为 agent:{agentId}:cron:{jobId}

  2. 模型选择——优先使用作业指定的模型,回退到 Agent 默认模型

  3. 思考级别——支持 thinking 覆盖(low/medium/high/xhigh),xhigh 不支持的模型自动降级

  4. 安全包装——外部钩子(如 Gmail)的内容会被安全包装,防止提示注入

  5. 投递处理——Agent 完成后,根据投递计划将结果发送到指定通道

作业生命周期

完整的作业执行生命周期:

关键状态转换:

  • at 类型成功后:如果 deleteAfterRun=true 则删除作业,否则禁用(enabled=false

  • every/cron 类型:基于结束时间计算下次运行时间,重新 arm 定时器

  • 失败的作业:记录 lastError,正常计算下次运行时间(不会因为失败就停止调度)


18.2.2 投递计划(Delivery Plan)(src/cron/delivery.ts

隔离 Cron 作业完成后,Agent 的回复文本需要投递到用户——例如发送到 WhatsApp、Telegram 或者仅仅注入到主会话。投递计划(Delivery Plan)决定了投递的目标和方式。

投递配置

mode

行为

"announce"

将 Agent 回复投递到指定通道

"none"

不投递(仅记录运行结果)

投递计划解析

resolveCronDeliveryPlan 处理新旧两种配置格式:

channel: "last" 是一个特殊值——表示"使用用户最近活跃的消息通道"。例如用户最近在 WhatsApp 上和 Agent 对话,那么 Cron 的投递就会发到 WhatsApp。


18.2.3 运行日志(src/cron/run-log.ts

每次 Cron 作业执行都会记录到一个 JSONL 格式的运行日志中。

日志文件组织

路径解析:

日志条目

写入:顺序化 + 自动裁剪

自动裁剪防止日志文件无限增长。当文件超过 2MB 时,只保留最近 2000 行。裁剪通过"写入临时文件 → 原子重命名"实现,避免裁剪过程中的数据丢失。

读取:倒序扫描

从末尾扫描确保了即使文件很大,也只需要读取最近的 N 条记录。


18.2.4 Cron 存储与迁移(src/cron/store.ts

存储格式

所有 Cron 作业存储在一个 JSON 文件中:

存储路径

加载与保存

衍生解释——JSON5:JSON5 是 JSON 的超集,允许注释(///* */)、尾逗号、单引号字符串、多行字符串等。OpenClaw 使用 JSON5 解析 Cron 存储,允许用户手动编辑 jobs.json 文件时使用更友好的语法。

原子写入通过"写入临时文件 → rename"实现。rename 在同一文件系统上是原子操作,确保即使写入中途进程崩溃,也不会损坏原始文件。每次保存后还会创建一个 .bak 备份。

Gateway 集成

Cron 服务在 Gateway 启动时初始化:

关键配置:

  • OPENCLAW_SKIP_CRON=1——环境变量可以完全禁用 Cron(不加载、不调度)

  • cron.enabled: false——配置文件禁用(作业保存但不调度)

  • onEvent 广播——每次作业状态变更都会通过 WebSocket 广播到所有连接的客户端

  • lane: "cron"——Cron 作业运行在专用的命令车道上,与主会话隔离


本节小结

  1. Cron 作业有两种执行模式:主会话(注入系统事件 + 触发心跳)和隔离会话(独立 Agent 运行)

  2. 隔离 Agent 执行涉及会话管理、模型选择、思考级别控制、安全包装和投递处理

  3. 投递计划(Delivery Plan)决定隔离作业结果的去向,支持新版 delivery 对象和旧版 payload 字段两种配置

  4. 运行日志使用 JSONL 格式,每个作业一个文件,写入顺序化防止竞争,自动裁剪防止无限增长

  5. 作业存储使用 JSON5 文件~/.openclaw/cron/jobs.json),支持手动编辑

  6. 存储操作通过原子写入(临时文件 + rename)保障数据安全,每次保存自动创建 .bak 备份

  7. Gateway 集成时将 Cron 事件通过 WebSocket 广播给客户端,并追加运行日志

Last updated