Fork me on GitHub

2026年5月

让 OpenClaw 自己动起来:Cron 与 Heartbeat

前面几篇我们把小龙虾接进了 Telegram 和飞书,跑通了通道侧的整套准入逻辑。但到目前为止它都是个 被动型选手,我们发它才回。昨天写飞书那篇结尾时我提了一个例子:让小龙虾在每天 9:30 给我发一条 DM,把昨天的 GitHub Trending、最新的新闻或订阅的博客汇总发给我。要让小龙虾反过来主动找你,就得给它装上自己启动的发条。

OpenClaw 在 Automation & tasks 这篇官方文档中,把这件事拆成了 6 种机制:Scheduled Tasks(Cron)HeartbeatHooksStanding OrdersBackground TasksTask Flow。其中最基础也是最容易搞混的两个就是 CronHeartbeat —— 一个负责按精确时间表干活,一个负责主会话隔一会儿自己看一眼。今天我们就把这两个机制各自跑一遍,再用一个完整的工作日早报场景把它们组合起来。

Background Tasks、Task Flow、Hooks、Standing Orders 这几块下一篇再讲。

Cron 和 Heartbeat 的分工

动手之前先用一张表把两者的边界讲清楚,免得后面用错了场景:

维度CronHeartbeat
触发精度精确(cron 表达式、一次性时间戳)近似(默认每 30 分钟一次)
会话上下文fresh isolated 或显式指定主会话完整上下文
任务记录每次运行都创建 task 条目不创建
投递方式频道、Webhook 或静默主会话内联,可选定向投递
适用场景日报、周报、定时提醒、后台批处理收件箱轮询、日历扫描、轻量通知

简单说,Cron 是有名有姓的定时任务,每个 job 都会写进 ~/.openclaw/cron/jobs.json 持久化,Gateway 重启不会丢;Heartbeat 是主会话每隔一会儿自己醒一次,不留 task 记录、也不上 jobs 列表。两者并不互斥,而是经常搭配使用:Heartbeat 负责轻量轮询,Cron 负责精确报表。

这里要先澄清一个常见误会:OpenClaw 的 Cron 不是 Linux 那个 cron,它是 Gateway 进程内置的调度器,不会去 /etc/cron.d/crontab 里写任何东西。Gateway 进程没起来,所有 cron job 都不会触发。

Cron:让小龙虾按时间表干活

OpenClaw 的 Cron CLI 入口是 openclaw cron add。我们先做一个最小可跑的例子,20 分钟后在 main 会话里提醒我喝水:

$ openclaw cron add \
    --name "喝水提醒" \
    --at "20m" \
    --session session:agent:main:main \
    --message "该喝水了,提醒一下用户起身倒杯水。" \
    --wake now

这条命令各个 flag 的含义如下:

  • --name:任务的展示名,方便 cron list 查看,可以中文
  • --at "20m":一次性触发的时间点。支持 ISO 8601 绝对时间(如 2026-05-05T16:00:00Z),也支持 20m / 1h 这种相对时间
  • --session session:agent:main:main:直接落到默认 agent(main)的 main 会话(main)里跑一轮 agent turn。其中 session key 的格式为 session:<session-id>session-idagent: 开头;第一个 main 是 agent id,默认 agent 就叫 main(DEFAULT_AGENT_ID = "main"),如果你跑了其它 agent,这里会是 work / personal 之类;第二个 main 表示会话槽位,main 是该 agent 的主会话(Main Session),如果是其他渠道或 subagent 触发的会话,其格式可能是这的:

    • agent:main:telegram:direct:user123 — Telegram 私聊
    • agent:main:discord:group:guild-chan — Discord 群聊
    • agent:main:cron:daily-briefing-uuid — cron 触发
    • agent:main:subagent:<uuid> — 子 agent
  • --message:这次 agent turn 的初始 prompt,会被当作用户输入直接喂给模型
  • --wake now:立即唤醒一次心跳来执行;另一个值是 next-heartbeat,等到下一轮心跳再执行

加完之后 openclaw cron list 能看到这个 job 的下一次触发时间,也可以到 Web UI 中的定时任务列表中查看:

cron-list.png

20 分钟到点之后,小龙虾就会在 main 会话里直接冒出一条 agent 回复,提醒你喝水:

cron-message.png

一次性 job(--at)在成功跑完之后默认会自己删除,如果要保留该任务记录的话可以加 --keep-after-run 参数。

遭遇 scope upgrade 报错

我自己在跑这条命令时遇到了一个报错,估计有不少同学也会遇到:

gateway connect failed: GatewayClientRequestError: scope upgrade pending approval
  (requestId: 1397fa2b-53be-4d4d-a00d-cc8842c61f7c)
GatewayTransportError: gateway closed (1008): pairing required:
  device is asking for more scopes than currently approved
Gateway target: ws://127.0.0.1:18789

报错原因不在 cron 命令本身,而是底层 Gateway 的 scope upgrade 还没审批。前几篇 onboard 和接通道时这台设备已经跟 Gateway 配对过了,但 openclaw cron add 需要的 scope 比当前已批准的范围更大,Gateway 就把这次请求挂成 pending 升级,同时断开本次握手。

报错里那串 requestId 就是 Gateway 为这次升级生成的 pending 请求号。我们运行下面的命令,看一下待审批的请求,确认就是这一条:

$ openclaw devices list

运行结果如下:

devices-list.png

可以看到之前的 scopesoperator.pairing, operator.read, operator.write,现在的 cron 命令需要 operator.admin,使用下面的命令批准这次 scope upgrade:

$ openclaw devices approve 1397fa2b-53be-4d4d-a00d-cc8842c61f7c

也可以在 Gateway 的 Web UI 的节点页面里点同意,效果一样:

devices-approve.png

批准之后再次运行 openclaw cron add ... 即可。只要 scope 不再扩大,后续 cron 命令不会再次触发审批。

--system-event 还是 --message

我第一次写这个例子时跟着官方文档抄的不是上面那条命令,而是下面这样:

$ openclaw cron add \
    --name "喝水提醒" \
    --at "20m" \
    --session main \
    --system-event "该喝水了,提醒一下用户起身倒杯水。" \
    --wake now

结果时间到了,在任务的执行历史里看 job 状态显示 success,但 main 会话里完全没动静。这就不得不提 --system-event--message 这两个容易混淆的参数了:

参数payload 类型触发时实际做的事
--system-eventsystemEvent往目标主会话里 enqueue 一条系统事件,并按 --wake 唤起一次 heartbeat
--messageagentTurn直接在目标会话里跑一轮 agent turn,把这段文本当作 prompt

另外这两个参数跟 --session 取值是 强绑定 的:

  • --session main 必须--system-event
  • --session isolated / current / session:<id> 必须--message

回到那条官方文档的例子,--system-event 写的"该喝水了..."并不是要直接展示给我的一条聊天消息,它是一个 塞给 agent 上下文的系统信号。Gateway 收到之后按 --wake now 唤起 heartbeat,heartbeat 跑的是 它自己那条默认 prompt("看一眼 HEARTBEAT.md,没事就回 HEARTBEAT_OK"),并不一定会把那段系统事件复述出来。如果 agent 判断"这条系统事件没必要打断用户"或者直接回了 HEARTBEAT_OK,main 会话里就什么都不会出现 —— 但 cron list 依然记成 success,因为 cron 端的工作确实做完了。

要让到点的时候用户在 main 会话里 直接 看到那条提醒,应该走 agent turn 这条路:用 --message 把要说的话当作 prompt 直接起一轮,agent 的回复就是用户看到的那条消息。--system-event 更适合"通知 agent 该看某件事了,但具体说不说、怎么说交给它判断"的场景。参考下面 Heartbeat 的章节。

三种调度类型

openclaw cron add 支持三种调度方式,正好覆盖一次性、固定间隔、复杂表达式三个场景:

KindCLI flag说明
at--at一次性触发,ISO 8601 时间戳或相对时间(如 20m
every--every固定间隔触发(如 --every 10m--every 1h
cron--cron5 段或 6 段 cron 表达式,配合 --tz 指定时区

要注意的是,不写时区的话,--cron 默认走 Gateway 主机时区,--at 不写时区直接当 UTC。生产环境建议都强制写 --tz "Asia/Shanghai",省得调试半天发现是时差的锅。

另外 OpenClaw 对整点表达式做了 自动错峰:默认会在原始触发时间附近随机抖动最多 5 分钟,避免一堆任务挤在 0 秒同时启动。要严格准点可以加 --exact 参数,要自定义抖动窗口用 --stagger 30s

四种执行风格

光会调度还不够,Cron 在哪个会话里跑 直接决定了它能看到多少上下文。OpenClaw 的执行风格分四种:

Style--session运行环境适用场景
主会话main下一次心跳轮次提醒、系统事件
隔离会话isolated全新 cron:<jobId> 会话报表、后台杂务
当前会话current创建时绑定的会话上下文敏感的循环任务
自定义命名会话session:<id>持久化命名会话需要在历史上累积上下文的工作流

主会话模式只是给主会话发一个系统事件,任务执行时仍然是用户自己的会话;隔离模式则会开一个全新的 cron:<jobId> 会话,跟主会话完全隔离,适合那种不希望污染聊天上下文的后台报表;自定义会话介于两者之间,同一个 --session session:daily-standup 可以在多次运行里累积上下文,比如每天的站会摘要可以基于昨天的摘要生成。

一次 cron 触发到底发生了什么

光看 CLI 不够直观,我们扒一眼源码。cron 的对外入口在 src/cron/service.ts

export class CronService implements CronServiceContract {
  private readonly state;
  constructor(deps: CronServiceDeps) {
    this.state = createCronServiceState(deps);
  }

  async start() {
    await ops.start(this.state);
  }

  async add(input: CronJobCreate) {
    return await ops.add(this.state, input);
  }

  async run(id: string, mode?: "due" | "force"): Promise<CronServiceRunResult> {
    return await ops.run(this.state, id, mode);
  }
}

这里的代码做了简化,只保留了主要部分。可以看到 CronService 本身只是一层薄壳,真正的状态拆在了 src/cron/service/state.ts,下一层的 ops.tsjobs.tstimer.tsstagger.ts 各自负责调度、持久化、定时器和错峰。其中 timer.ts 是触发的核心,它会按 nextRunAtMs 排定真实定时器,到点之后调用 markCronJobActive 写入 active 标记,再由 delivery-plan.ts 把投递目标解析成 announce / webhook / none,最后走 isolated-agent.ts 拉起一次真正的 agent turn。

整个链路有三个关键点:

  1. 持久化:所有 job 写在 ~/.openclaw/cron/jobs.json 里,Gateway 重启不会丢。运行态信息单独放在同目录的 jobs-state.json。文档里特别提到这两个文件拆开是为了让定义可入 git、运行态不入 git
  2. 错峰:所有整点表达式默认会被 stagger.ts 自动错开 0~5 分钟,避免一堆 job 在 9:00:00 同时触发
  3. 重试与熔断:transient 错误(rate_limit、overloaded、network、server_error)会指数退避重试;permanent 错误直接 disable

三种投递方式

openclaw cron add 支持三种投递模式,对应不同的输出策略:

Mode行为
announceagent 没主动发消息时,runner 把最终回复兜底发到 --to 目标
webhook把最终的处理结果 POST 到一个 URL
nonerunner 不做任何兜底投递

我们把开头那个早报场景用 announce 实现一遍 —— 让小龙虾每天 8:30 在隔离会话里汇总 GitHub 通知和今日日程,然后通过 Telegram 投递到我自己的私聊:

$ openclaw cron add \
    --name "morning-brief" \
    --cron "30 8 * * *" \
    --tz "Asia/Shanghai" \
    --session isolated \
    --message "汇总过去 24 小时的 GitHub 通知和今天的日程,按重要性排序。" \
    --announce \
    --channel telegram \
    --to "12345678"

跟前面喝水提醒例子比起来,多出来的三个 flag:

  • --announce:开启 runner 兜底投递;其中 --session isolated 表示隔离会话;
  • --channel telegram:投递通道。可选 telegram / slack / discord / feishu / whatsapp 等,默认 last(最近一次用过的通道);
  • --to "12345678":目标地址,格式跟通道挂钩。Telegram 直接给数字 chat_id:私聊是用户 ID,群组带 -100 前缀(如 -1001234567890),如果是话题群再加一个 --thread-id 42 参数;

到 8:30 触发时,OpenClaw 会开一个全新的 cron:<jobId> isolated session 跑一轮 agent turn。等 agent 执行结束,runner 拿这段文本通过 Telegram 适配器 POST 到 chat_id 12345678。整个 isolated session 跟主会话完全隔开,main 会话不会被早报刷屏。

要注意的是,对 isolated job 来说 chat 路由是共享的:如果在 prompt 里让 agent 显式调 message 工具发到了 --to 指定的目标,OpenClaw 会按 通道 + 目标 做去重判断,匹配就自动跳过 announce 兜底,避免重复消息。但只要通道或目标 ID 写法对不上,就可能出现重复发送的情况。最干净的写法是二选一:要么靠 --announce 兜底、prompt 里别让它主动发;要么 prompt 显式调工具发,cron 加 --no-deliver 把兜底关掉。

Heartbeat:让小龙虾自己醒一下

讲完 Cron,我们再来看 Heartbeat。这是两个很容易混淆的概念。文档里的一句话定义最准确:

Heartbeat runs periodic agent turns in the main session so the model can surface anything that needs attention without spamming you.

也就是说,Heartbeat 不是另一个 cron,它是主会话里 隔一会儿自己跑一轮 agent turn,看一下有没有什么需要冒泡给你的。默认间隔是 30 分钟,可以通过 agents.defaults.heartbeat.every 参数改,写 0m 完全关掉。

整个机制设计得比 Cron 轻量很多:没有 jobs 列表、不会创建 task 记录、连出现在哪都不强求。它在后台跑得勤快,但是却不张扬,专门用来做收件箱轮询、日历扫描、轻量通知这类不急但得有人盯着的事。

默认行为

Heartbeat 默认是开的,一个全新装好的 OpenClaw 每 30 分钟会自动跑一轮,prompt 是写死的一句话(可以在 agents.defaults.heartbeat.prompt 里覆盖):

Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.

翻译过来就是:去读 workspace 里的 HEARTBEAT.md(如果有的话),按它说的做;别从老聊天里翻出陈年的 task 出来重复一遍;没事的话就回 HEARTBEAT_OK

最关键的是 响应契约

  • 没事时回 HEARTBEAT_OK:这串字符是 OpenClaw 跟模型约定的暗号,只有出现在回复的开头或结尾才算数。识别成暗号之后,OpenClaw 会把它从回复里抠掉,再看剩下的内容有多长,如果 不到 300 字符(默认值,可通过 ackMaxChars 调)就把整条回复静默吃掉,主会话里啥都看不到。比如模型回 HEARTBEAT_OK 邮箱里没有紧急邮件 会被直接丢弃;但如果它写了 800 字的真正邮件摘要再带个 HEARTBEAT_OK,还是会照常投递
  • 如果有需要提醒你的事(比如收件箱里有紧急邮件),返回 alert 文本,不要HEARTBEAT_OK
  • 如果 HEARTBEAT_OK 出现在回复中间不算暗号,避免误伤模型的正常消息,也会照常投递

这个契约的好处很直接:你不会被一堆 "✅ 心跳正常,无事可做" 之类的废话淹没;只有当 agent 真有事情要跟你说,你才会在主会话里看到它的消息。

HEARTBEAT.md:心跳清单

如果你不写 HEARTBEAT.md,agent 每次心跳都得自己想"我应该看点啥",结果就是每次问的事情都不太一样,token 烧得也比较随机。OpenClaw 的推荐做法是在 workspace 里放一份小巧、稳定、安全的 HEARTBEAT.md,把每次心跳要做的事写成清单:

# Heartbeat checklist

- 快速扫一眼:收件箱里有没有紧急的邮件?
- 如果是白天,闲着没事时可以来打个招呼
- 如果某个任务卡住了,在这里写下到底缺什么,记得下次问

每次心跳 OpenClaw 都会把这份文件作为 workspace bootstrap 注入到 system prompt 里。要让它真正小,OpenClaw 还做了一个优化:如果 HEARTBEAT.md 内容是 effectively empty(只有空行、Markdown 标题、空列表、围栏标记),OpenClaw 会直接 skip 这次心跳,省掉一次模型调用,运行日志里会看到 reason=empty-heartbeat-file

tasks:

HEARTBEAT.md 有一个特别好用的功能,叫 tasks: 块,用来在心跳里跑不同间隔的子检查:

tasks:

- name: 邮件检查
  interval: 30m
  prompt: "检查是否有紧急的未读邮件,并标记任何对时间敏感的内容。"
- name: 日历扫描
  interval: 2h
  prompt: "检查即将到来的、需要准备或跟进的会议。"

# 额外说明

- 保持提醒简短。
- 如果在所有到期任务都执行完毕后没有需要关注的事项,回复 HEARTBEAT_OK。

有了 tasks: 块之后,OpenClaw 会按每个 task 自己的 interval 判断是否到期,只把到期的 task 放进当前心跳的 prompt。如果一轮没有任何 task 到期,整次心跳直接 skip(reason=no-tasks-due)。

投递目标与可见性

默认 target: "none",也就是说心跳跑了但 不主动投递任何外部消息,只在主会话内部留个痕迹。要让它真正把 alert 推到你身上,得显式配 target

{
  "agents": {
    "defaults": {
      "heartbeat": {
        "every": "30m",
        "target": "last",         // 投到最近一次用过的外部通道
        "directPolicy": "allow",  // 默认 allow;block 时不投递 DM 类目标
        "lightContext": true,     // 仅注入 HEARTBEAT.md,跳过其它 workspace bootstrap 文件
        "isolatedSession": true,  // 每次心跳跑在新会话里,避免拖着完整对话历史
        "skipWhenBusy": true,     // 子 agent / nested 通道繁忙时也延期
        // "activeHours": { start: "08:00", end: "24:00" },
        // "includeReasoning": true,
      },
    },
  },
}

几个值得展开的字段:

  • target: "last" 表示投递到最近一次用过的外部通道。如果你想固定投到某个频道,比如 Telegram 的某个群,可以写 target: "telegram"to: "12345678";多账号场景下还得加 accountId
  • lightContext: trueisolatedSession: true 是节省 token 的两件套。前者只把 HEARTBEAT.md 注进 bootstrap,跳过 SOUL / IDENTITY / USER / AGENTS;后者每次新开 session,避免每次心跳都拖一份完整的主会话上下文。文档给的对照数据是从 ~100K tokens 降到 ~2-5K tokens 一次
  • directPolicy: "block" 用来防止心跳给陌生人或 DM 对端发广播,常见于多用户共享 inbox 的场景
  • activeHours 限定到上班时间,比如只在 9:00~22:00 之间响心跳,下班自动闭嘴

Cron 跑的时候 Heartbeat 会让路

还有一点值得注意:只要有 cron job 在跑或者排队,Heartbeat 就自动延期。源码在 src/infra/heartbeat-runner.ts

if (hasActiveCronJobs() || hasQueuedWorkInLanes(HEARTBEAT_ALWAYS_BUSY_LANES, getSize)) {
  // 延期,跳过本轮
  ...
}

if (heartbeat?.skipWhenBusy === true && hasOptInBusyLaneWork(getSize, getSnapshots)) {
  // skipWhenBusy 开启时,子 agent / nested 通道繁忙也延期
  ...
}

这个设计对本地模型用户特别友好。如果你跑的是 Ollama / vLLM 这种单进程模型服务,Cron 和 Heartbeat 同时打满会直接把请求队列堆爆。打开 skipWhenBusy: true 之后,连子 agent 和 nested 通道繁忙时也会让心跳延期,真正做到"忙的时候不抢资源,闲的时候才看一眼"。

手动触发一次心跳

平时 Heartbeat 是按 every 定时跑的,但你也可以随时手动催它一下:

$ openclaw system event --text "Check for urgent follow-ups" --mode now

--mode now 立刻起一次心跳;--mode next-heartbeat 等到下一轮 tick 再跑。如果配了多个 agent 都开了 heartbeat,手动触发会把每个 agent 的心跳都跑一遍。

Cron 与 Heartbeat 的组合

到这里两个机制都讲完了,回头看一开始那个早报场景 —— 让小龙虾每天 8:30 给我发一条 DM,把 GitHub 通知和今天日程汇总好。光用 Cron 写一句"汇总 GitHub 通知和今日日程"够不够?够。但有一类需求 Cron 满足不了:收件箱什么时候收到新邮件、日历什么时候发生变动,是不可预测的。这种轮询型工作交给 Heartbeat 才合适。

下面是一个典型的搭配组合:

  • Cron:负责 8:30 / 12:30 / 18:30 三个固定时间点的精确报表,每次 fresh isolated session,结果通过 --announce 投递到 Telegram
  • Heartbeat:负责日间 9:00~22:00 的轻量轮询,间隔 30 分钟,主会话上下文,开 lightContext + isolatedSession + skipWhenBusy 三件套省 token,只在收件箱有紧急邮件、日历有冲突时才推 alert,没事就 HEARTBEAT_OK 静默
  • HEARTBEAT.mdtasks: 块拆成 inbox-triage(30m)、calendar-scan(2h)、pr-watch(1h)三个不同 interval 的子任务

整体的触发时序长这样:

cron+heartbeat.png

小结

通过这一篇,我们把 OpenClaw 自动化体系里最基础的两块拆开看了一遍:

  1. Cron:Gateway 内置调度器,job 持久化在 ~/.openclaw/cron/jobs.json、运行态在 jobs-state.json--at / --every / --cron 三种调度方式覆盖一次性、固定间隔、复杂表达式三类场景;执行风格分主会话、隔离、当前、自定义命名四种;投递有 announce / webhook / none 三档
  2. Heartbeat:主会话每 30 分钟自己跑一轮的轻量心跳,不创建 task 记录不上 cron list,靠 HEARTBEAT.md(含 tasks: 块)保持稳定的清单;响应契约是没事时回 HEARTBEAT_OK、有事时回 alert,可以配 lightContext + isolatedSession + skipWhenBusy 三件套省 token、避资源
  3. 两者互不打架:Cron 跑的时候 Heartbeat 自动延期(看 src/infra/heartbeat-runner.tshasActiveCronJobs() 那一段),开 skipWhenBusy 之后连子 agent / nested 通道繁忙也延期
  4. 典型组合:Cron 负责精确时间点的报表(早报、周报);Heartbeat 负责日间的轻量轮询(收件箱、日历、PR 状态);遇到外部事件还有 Webhook 兜底

到这里,小龙虾终于不再是被动等回复的状态了,它会按表干活,也会自己醒一下看看周围有没有事。但我们说的主动还差一块:当 GitHub PR 来了新的 review、Sentry 报了一个 P0、自家服务部署完成的时候,OpenClaw 怎么被外部世界唤起?这就需要 Webhook,再加上一份长期生效的指令清单 Standing Orders 来约束 agent 干这些活时的边界。我们下一篇继续。

参考


OpenClaw 接入第二个通道:飞书

昨天我们把小龙虾接进了 Telegram,顺便摸清了 OpenClaw 的 pairing 安全模型:陌生人发来的 DM 不会直接进 LLM,而是先生成一个配对码,需要管理员在终端用 openclaw pairing approve 显式放行。这套机制对所有通道都生效,今天接的飞书也不例外。

但 Telegram 在国内有个绕不过的现实问题 —— 得挂梯子。再加上工作场景里几乎没有团队真用它,这一篇我们换一个国内更接地气的通道:飞书。截至 2026 年初,飞书在国内做得相对完整:字节系自家在用,小米、理想、得到、蔚来、SHEIN 这些公司也都把内部办公迁到了飞书上。把小龙虾接到飞书,等于直接把它放进了你工作日 8 小时里最常打开的那个窗口 —— 群里 @ 一下就能让 AI 接管一段调研,DM 里发条消息就能让它整理日报、总结飞书文档。

为什么把小龙虾接到飞书

把 AI 接进 Telegram 大多是个人玩法,接进飞书则更像把它装进了团队工作流。下面有几个比较实用的场景供参考:

  • 早晨日报提醒:让小龙虾在每天 9:30 给我发一条 DM,把昨天的 GitHub Trending、最新的新闻或订阅的博客汇总发给我
  • 群里召唤 AI 处理任务:在团队群里 @ 它一下,让它接管一段调研或查文档回答问题,所有人都能看到结果,不用再单独转发
  • 文档和会议总结:飞书文档和飞书妙记是国内做得相对完整的一套,OpenClaw 内置 feishu_doc 工具能直接读写飞书文档,让小龙虾顺手把会议纪要做要点提炼,效率比手动复制粘贴高很多
  • 多人协作:在一个项目群里把 bot 当成虚拟成员,谁都可以让它干活,比每人本地装一份桌面端 AI 工具更轻

这些场景的共同点是:消息流量本来就在飞书里,AI 跟着进飞书才不打断节奏。如果还要先切到一个独立 webapp 或者命令行,体验就完全不同了。

整体的消息流转和 Telegram 思路类似,但传输方式不一样:

feishu-sequence.png

注意这里 Gateway 走的是 WebSocket 长连接 主动出站连到飞书开放平台,由飞书把事件推过来,本地不需要暴露任何端口。这对开发者非常友好,不用 ngrok、不用买公网机器,本机跑起来就能接收消息。

飞书也支持传统的 webhook 模式,作为长连接走不通时的兜底方案。两种模式下 OpenClaw 行为完全一致,只是事件的进入路径不同。

在飞书开放平台创建自建应用

飞书的接入逻辑和 Telegram、Discord 不太一样。Telegram 给一个 BotFather,三句对话拿走 Token 就能跑;飞书走的是更接近企业 IM 的 自建应用 路线:先在企业开放平台创建一个应用,给它开机器人能力,再开事件订阅,最后拿到 App IDApp Secret 这两把钥匙。整个流程都在网页上完成,第一次走可能会觉得页面菜单有点多,下面我们一步一步对着截图走一遍。

第一步:登录开放平台

打开浏览器访问 飞书开放平台,注册账号并登录。

海外版 Lark 是另一个独立环境,地址是 open.larksuite.com,账号、应用、API 域名都不与国内飞书互通。如果你的团队用的是 Lark,记得在 OpenClaw 配置里把 domain 设成 lark

登录之后点击右上角的 开发者后台

feishu-home.jpg

第二步:创建企业自建应用

进入开发者后台后,点击 创建企业自建应用 按钮,弹窗里填三项:

  • 应用名称:会出现在群成员列表和消息发送者位置,建议起个有辨识度的名字,比如 老钳
  • 应用描述:随便写一句给自己看,比如 基于 OpenClaw 的个人 AI 助手
  • 应用图标:上传一张方形图,或者选平台内置的图标,没现成的素材直接用龙虾 emoji 也行

feishu-create-app.png

确认之后会跳到刚创建的应用详情页,URL 里能看到一串 cli_xxx 形式的 ID,这就是 App ID。

第三步:记下 App ID 和 App Secret

应用详情页左侧导航选 凭证与基础信息,右边能看到这两把钥匙:

  • App ID:以 cli_ 开头,公开可见,用来标识应用
  • App Secret:一长串随机字符,私密,OpenClaw 用它换 access token

feishu-credentials.png

记下这两个值,等会儿要写进 OpenClaw 的配置里。

App Secret 等同于这个应用的密码,不要提交到 Git,不要贴到群里。万一不小心泄露了,回到这一页点击 重置 立刻吊销并重新生成;OpenClaw 那边的配置也得同步换新。

第四步:开启机器人能力

左侧导航选 添加应用能力,找到 机器人 这一项,点击 添加。这一步只是给应用打开 bot 形态,还没有配置具体的消息行为。

feishu-add-bot-capability.png

第五步:选择长连接事件订阅

左侧导航选 事件与回调 → 事件配置,最上面有个 配置订阅方式 区域,这里有两个互斥的选项:

  • 使用长连接接收事件:飞书主动连过来,本地不需要公网地址,推荐
  • 将事件发送至开发者服务器:传统的 webhook 模式,需要公网回调地址

OpenClaw 默认走长连接(connectionMode: "websocket"),所以这里选第一个就行:

feishu-event-subscription.png)

webhook 模式后面专门起一节讲,本节按推荐路径走。

第六步:订阅 im.message.receive_v1 事件

往下滚到 事件管理,点 添加事件,搜索 im.message.receive_v1,勾上保存。这个事件就是"用户给 bot 发了一条消息"的那个钩子,没有它 bot 就根本收不到消息。

feishu-add-event.png

订阅完之后右边权限栏会自动列出这个事件依赖的权限项,比如 im:message,等会儿在权限管理里要统一开。

第七步:开通核心权限

左侧导航选 权限管理,最小可跑就开三项:

权限码用途
im:message收发消息的基础权限
im:chat读取群基础信息(群名、群成员)
contact:contact.base:readonly读取用户基础信息(昵称、头像)

逐项点 申请权限 即可。

feishu-permissions.png

这三项是从 OpenClaw 源码 extensions/feishu/src/setup-surface.ts:153 的引导文案里抠出来的,引导只列了这三项,最小可跑就够了。如果还想让小龙虾读写飞书文档、知识库、云盘、多维表,再追加 docxwikidrivebitable 相关权限即可 —— 不开也能跑,只是相应的 feishu_docfeishu_drive 等工具调用会因为权限不足报错。

第八步:发布并审批

最后左侧导航选 版本管理与发布,点 创建版本,选可见范围,提交审核。

  • 个人开发账号可以选 仅自己可见,秒过
  • 企业账号必须等管理员在飞书 App 的审批列表里点 同意,才能在企业内推送

feishu-publish.png

至此飞书侧的所有工作就全部完成了,下一步回到本地开始 OpenClaw 的配置。要注意的是,审批没通过之前,长连接会被服务端拒绝,一定要等状态变成 已发布

OpenClaw 配置

拿到 App IDApp Secret 之后,回到本机,编辑 ~/.openclaw/openclaw.json,加一段 channels.feishu

{
  "channels": {
    "feishu": {
      "enabled": true,
      "domain": "feishu",
      "connectionMode": "websocket",
      "defaultAccount": "main",
      "accounts": {
        "main": {
          "appId": "cli_xxxxxxxxxxxxxxxx",
          "appSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
          "name": "老钳"
        }
      },
      "dmPolicy": "pairing"
    }
  }
}

各个字段逐项解释如下:

  • enabled:通道总开关,默认 true,置为 false Gateway 会跳过初始化
  • domain:API 域名,国内用 feishu,海外或 Lark 用户用 lark,私有部署也支持直接填 https://... 自定义 URL
  • connectionMode:事件传输方式,websocket 走长连接,webhook 走 HTTP 回调,默认 websocket
  • defaultAccount:多账号场景下,对外发起 API 调用使用的默认账号 ID,单账号留空走 default 即可
  • accounts.<id>.appIdaccounts.<id>.appSecret:从飞书开放平台 凭证与基础信息 页拿到的两个值
  • dmPolicy:DM 准入策略,可选 pairingallowlistopen默认 pairing

除此之外,还有 textChunkLimitmediaMaxMbstreamingtypingIndicator 等更多参数,参考官方文档:https://docs.openclaw.ai/channels/feishu

如果不想手写 JSON 配置,OpenClaw 还提供一条更省事的命令:

$ openclaw channels login --channel feishu

向导会弹出一个二维码,用飞书 App 扫一下就能在开放平台侧自动创建应用、把凭证写回本地配置,省掉手动跑一遍开放平台界面。这条命令需要 OpenClaw 2026.4.25 及以上版本,老版本用 openclaw update 升一下。

这个方法我没有验证,不知道为什么该命令在我的电脑上运行会卡死,如果有验证通过的小伙伴,欢迎交流。

webhook 模式与内网穿透

如果出于某些原因不能走长连接(比如内网防火墙拦了出站 WebSocket,或者部署在已有 HTTP 网关后面想统一接入),可以切到 webhook 模式。这种情况下飞书会主动把事件 POST 到我们的回调地址,因此本地必须有一个公网可达的 URL。

切换 webhook 模式的配置如下:

{
  "channels": {
    "feishu": {
      "connectionMode": "webhook",
      "verificationToken": "v_xxxxxxxxxxxxxxxx",
      "encryptKey": "e_xxxxxxxxxxxxxxxx",
      "webhookPath": "/feishu/events",
      "webhookHost": "127.0.0.1",
      "webhookPort": 3000
    }
  }
}
  • verificationToken:飞书在事件订阅页生成的校验令牌,用来验证请求来自飞书
  • encryptKey:可选的加密密钥,启用后飞书会用 AES 加密事件 payload,OpenClaw 收到后解密
  • webhookPath:本地 HTTP 服务挂载的回调路径,默认 /feishu/events
  • webhookHostwebhookPort:本地监听地址和端口,默认 127.0.0.1:3000

本地开发阶段,常见做法是用内网穿透工具把 127.0.0.1:3000 暴露到公网:

  • ngrok http 3000:最常见,随机域名,免费版有连接数限制
  • cloudflared tunnel:Cloudflare 出品,速度稳定,绑自有域名免费
  • frp 自建反向代理:有自己的云服务器时最划算

启动穿透之后,把得到的公网地址(比如 https://xxx.ngrok-free.app/feishu/events)填回飞书开放平台的 请求地址 一栏。生产环境一般直接部署到云服务器,省掉穿透环节。

启动并验证

配置写好之后,重启 Gateway:

$ openclaw gateway restart

正常启动之后,使用下面的命令查看 channel 状态:

$ openclaw channels status --probe

输出类似这样:

Gateway reachable.
- Feishu default (老钳): enabled, configured, running, works

私聊配对

我们先验证 DM 通路。在飞书 App 里打开你刚创建的应用名,发一条 你好,由于 dmPolicy: "pairing",bot 不会接话,而是会回复一段配对提示,里面带一个 8 位的大写字母数字混合代码,比如 A3KQ7M2N。代码 1 小时过期,单 channel 同时最多挂 3 个待审批请求。

我在测试时发现,有时 bot 不会返回配对提示,但是运行 openclaw pairing list 能正常看到配对请求,这时候只需要按下面的步骤 approve 就行了。

切回管理员的终端,先列一下当前所有挂起的配对请求:

$ openclaw pairing list feishu

Pairing requests (1)
│ Code     │ feishuUserId      │ Meta                                          │ Requested                │
│ A3KQ7M2N │ ou_8c1f...e3      │ {"accountId":"default"}                       │ 2026-05-03T13:05:57.015Z │

输出里能看到配对码、飞书用户的内部 ID(形如 ou_xxx)、请求时间。确认是预期的人之后,approve 它:

$ openclaw pairing approve feishu A3KQ7M2N

Approved feishu sender ou_8c1f...e3.

approve 之后,再发一条测试消息,就会被放行进入 agent 了:

feishu-first-reply.png

群组场景

把 bot 拉进群之后,事情会比 DM 复杂一些。如果我们直接在群里 @bot 发送消息,它是不会回复你的。检查 OpenClaw 的日志,会看到类似下面的信息:

21:58:53 [feishu] feishu[default]: received message from ou_e146a012928e16xxx in oc_f710113a6e42yyy (group)
21:58:53 [feishu] feishu[default]: group oc_f710113a6e42yyy not in groupAllowFrom (groupPolicy=allowlist)

这是因为 groupPolicy 默认是 allowlistgroupAllowFrom 为空,导致 bot 不会被触发。需要把 groupAllowFrom 配置为目标群组 ID (形如 oc_xxx)才能正常工作。这和 Telegram 的行为类似,OpenClaw 的默认策略都是先关门,再根据需要慢慢开门。可以在 ~/.openclaw/openclaw.json 里增加如下配置:

{
  "channels": {
    "feishu": {
      "groupPolicy": "allowlist",
      "groupAllowFrom": ["oc_f710113a6e42yyy"]
    }
  }
}

再次重启 Gateway 并验证,这时 bot 就能正常在群里对话了:

feishu-group-reply.png

另外,除了 allowlistgroupPolicy 还有 opendisabled 两种选项:

取值行为
open所有群都响应,最自由也最容易被滥用
allowlist只响应 groupAllowFrom 列出的群,或者在 groups.<chat_id> 下显式配置过的群
disabled完全禁用群消息,即便有 groups.<chat_id> 显式配置也不响应

@ 触发还是常驻倾听

群里和 DM 最大的区别是触发方式,requireMention 控制群里是否必须 @ bot 才触发回复:

  • true(默认):群里聊天大家不会被打扰,只有 @ 到 bot 才回应
  • false:bot 监听全部消息,agent 自己判断要不要发声

这是个非常贴合办公场景的默认值,群里成员动辄几十上百,机器人对所有消息都搭话很快就会被踢出去。如果你建了一个专门跟 AI 对话的小群,希望它有问必答,把那个群单独切到 requireMention: false 就行:

{
  "channels": {
    "feishu": {
      "groupPolicy": "allowlist",
      "requireMention": true,
      "groups": {
        "oc_aabbccddeeff": {
          "requireMention": false
        }
      }
    }
  }
}

这里有个常见的误会:飞书的 @all@_all广播标记,不算对 bot 的点名。OpenClaw 已经做了过滤,只有当一条消息明确 @ 你的 bot,才会被认为被点名。

群内还能进一步收紧

allowFrom 在飞书场景下也很有用。比如你想做一个只允许运维组同事调用的运维机器人,就在群里把 allowFrom 限定到运维组成员的 user_id

{
  "channels": {
    "feishu": {
      "groupPolicy": "allowlist",
      "groupAllowFrom": ["oc_ops_room"],
      "groups": {
        "oc_ops_room": {
          "allowFrom": ["ou_alice", "ou_bob", "ou_charlie"]
        }
      }
    }
  }
}

群里其他成员 @ 它也不会触发响应,OpenClaw 会直接在 sender 校验环节把消息丢掉。

小结

通过这一篇,我们把 OpenClaw 接进了飞书:

  1. 创建自建应用:在飞书开放平台开机器人能力,记下 App IDApp Secret,开 im:messageim:chatcontact:user.base:readonly 三项核心权限,订阅 im.message.receive_v1 事件,把版本发布并审批通过
  2. 写最小配置:在 ~/.openclaw/openclaw.jsonchannels.feishu 下填账号、dmPolicygroupPolicyrequireMention 这几个关键开关;不想手写也可以直接 openclaw channels login --channel feishu 扫码完成
  3. 传输模式:默认走 WebSocket 长连接,省掉公网回调地址;webhook 模式作为兜底,需要内网穿透或云部署
  4. DM 默认 pairing:陌生人通过配对码请求白名单,管理员显式 approve 才放行;底层就是 policy.ts 里那十几行 isSenderIdAllowed 风格的判定
  5. 群聊治理groupAllowFrom 控制哪些群能用,requireMention 控制是否必须 @,@all 不算 @ 到 bot;可以配合 allowFrom 进一步限定群聊范围

到这里 OpenClaw 的通道接入就告一段落了。Telegram 和飞书一外一内,跑通这两个之后剩下的 Slack、Discord、企业微信、钉钉的接入思路都大同小异,只是开放平台的具体术语变一变。

不过到目前为止小龙虾还是个"被动型选手" —— 我们发它才回。开头我提到一个场景:"让小龙虾在每天 9:30 给我发一条 DM,把昨天的 GitHub Trending、最新的新闻或订阅的博客汇总发给我",要让 bot 反过来主动找你,就得给它装上自己启动的发条。OpenClaw 在这件事上提供了两种最基础的机制:Cron 负责按精确时间表干活,Heartbeat 负责主会话隔一会儿自己醒一下看一眼。下一篇我们就来学习这两个机制。

参考


OpenClaw 接入第一个通道:Telegram

昨天我们用 openclaw onboard 把 Gateway 跑了起来,又经历了一场破壳仪式给小龙虾起了名字、填好了 IDENTITY / USER / SOUL 三件套。但说到底,它还只是一只关在终端里的小龙虾,尽管我们能在 TUI 里和它聊,也能在 WebChat 里和它聊,但这些都没有跳出传统 Chatbot 的范畴。

OpenClaw 真正区别于 ChatGPT、Open WebUI 这些工具的地方,是它长在你每天都打开的 IM 软件里。从今天开始我们正式接通道,第一站选 Telegram:只要一个 BotFather 给的 Token,不用扫码、不用 OAuth、不用绑手机号,是体验这套定位最快的一条路径。

为什么从 Telegram 开始

OpenClaw 支持 30 多个通道,从 WhatsApp、Signal、iMessage,到 Slack、Discord、飞书、企微,再到 Matrix、Nostr、IRC 这种小众选手。第一篇通道实战选哪个,其实是有讲究的。

我选 Telegram 的理由有三点:

第一,接入门槛最低。Telegram 的 BotFather 是一个聊天机器人,发 /newbot 就能拿到 Bot Token,整个过程不超过两分钟。WhatsApp 需要 WhatsApp Business API 或者扫码登录个人号,Signal 要绑定手机号,iMessage 要 macOS 配 BlueBubbles。Telegram 啥都不要,只要一个 Telegram 账号。

第二,官方文档把它列为生产可用OpenClaw 文档中明确写着 Production-ready for bot DMs and groups via grammY,它使用 grammY 这个 TypeScript Bot 框架接的 Telegram Bot API,已经迭代好几年了,稳定性有保障。

第三,国内外开发者用得最多。OpenClaw 的 GitHub Discussions 和官方 Discord 里,关于 Telegram 的帖子是其他通道的好几倍,遇到坑也最容易搜到答案。

整体的消息流转大致是这样的:

telegram-sequence.png

注意这里 Gateway 是主动方,它通过 getUpdates 长轮询把消息从 Telegram 拉过来,而不是 Telegram 主动推过来。

其实,OpenClaw 支持 长轮询Webhook 两种方式来接收 Telegram 消息。默认是长轮询模式,Gateway 主动调 getUpdates,不需要公网入口,本地开发最方便。要切到 Webhook,配置 channels.telegram.webhookUrlwebhookSecret,本地监听默认在 127.0.0.1:8787。Webhook 适合多副本部署或者无法长连接的环境,但需要反向代理把公网流量打进来,安全模型也要重新评估。

在 BotFather 注册一个 Bot

接 Telegram 的第一步是拿一个 Bot Token。这一步在 Telegram 端完成,跟 OpenClaw 无关。

打开 Telegram 客户端,搜索 @BotFather,要提醒一点的是,Telegram 上仿冒账号很多,如果认错了就相当于把 Token 交给骗子了,认准头像旁那个蓝色 ✓ 标志:

bot-father.png

进入 BotFather 的对话之后,发 /newbot 命令:

/newbot

BotFather 会让你依次输入两个东西:

  1. Bot 的展示名:可以是中文,比如 老钳
  2. Bot 的 username:必须以 bot 结尾且全局唯一,比如 aneasystone_clawd_bot

填完之后 BotFather 会回一条消息,里面有一行加粗的 token,长这个样子:

123456789:ABCdefGhIJKlmNOPQRsTUvWxYz0123456789

整段对话大致如下:

clawd_bot.png

Bot Token 等同于这个 bot 的密码,不要提交到 Git,不要贴到群里。一旦泄露任何人都能冒充它收发消息,甚至替你操作 agent。如果不小心泄露了,回到 BotFather 发 /revoke 立刻吊销并重新生成一个就行。

顺手在 BotFather 里再做两个设置,等下接群组场景的时候用得到,现在做完就不用回来了:

  • /setprivacy 选择目标 bot,把 Privacy Mode 设为 Disabled,让 bot 能看到群里的所有消息
  • /setjoingroups 选择目标 bot,确认允许 bot 被加入群组

把 Token 配置进 OpenClaw

Token 拿到之后,OpenClaw 这边只需要把它写进 ~/.openclaw/openclaw.json。在原有的配置基础上加一段 channels.telegram,最小可跑配置长这样:

{
  "channels": {
    "telegram": {
      "enabled": true,
      "botToken": "123456789:ABCdefGhIJK...",
      "dmPolicy": "pairing",
      "allowFrom": []
    }
  }
}

以下对四个字段逐个解释:

enabled 是通道总开关,置为 false 就完全停用,配置项保留但 Gateway 启动时跳过初始化。开发期想临时关掉某个通道又不想丢配置,把这一行改成 false 重启即可。

botToken 就是刚才从 BotFather 拿到的 Token。除了写在配置里,OpenClaw 还支持两种替代方式:

  • 环境变量 TELEGRAM_BOT_TOKEN,仅对默认账号生效
  • tokenFile 字段,指向一个保存 Token 的纯文本文件

配置值优先级大于环境变量,生产环境推荐用 tokenFile 加文件权限保护(chmod 600),开发环境直接写在配置里最快。

dmPolicy 控制谁可以直接给 bot 发私信,有四个取值:

取值行为
pairing默认值,未知发送者会先收到一个一次性配对码,需要管理员审批才能放行
allowlist严格白名单,必须在 allowFrom 里的数字 user ID 才能聊
open完全开放,需要 allowFrom 显式包含 "*" 才生效
disabled关闭私信入口

allowFrom 是数字形式的 Telegram user ID 列表,telegram:tg: 前缀都会被自动归一化。空数组配 pairing 没问题,所有第一次进来的发送者都会走配对流程;空数组配 allowlist 则会被配置校验直接拒绝,因为这等于谁都不让进。

我们这里先用默认的 pairing 模式。这是 OpenClaw 推荐的姿势:默认把门关好,再按一个个配对码放人进来。

另外,不想手写 JSON 也可以用 openclaw channels add --channel telegram --token <bot-token> 命令增加新通道。

顺手扒一眼 grammY 整合

OpenClaw 是基于 grammY 接的 Telegram Bot API。我们扒一眼 extensions/telegram/src/bot-core.ts,能看到创建 Bot 实例的核心几行:

const bot = new botRuntime.Bot(opts.token, client ? { client } : undefined);
bot.api.config.use(botRuntime.apiThrottler());
bot.catch((err) => {
  runtime.error?.(danger(`telegram bot error: ${formatUncaughtError(err)}`));
});

这里做了简化,只保留了主要部分。可以看到 OpenClaw 没有自己造 SDK 的轮子,直接复用了 grammY 的 Bot 类,再叠加 apiThrottler 控制 API 调用速率,最后挂一个全局 catch 兜底。后面所有的 messaging(文本、图片、语音、视频、贴纸等消息的收发)、reaction(消息表情回应,长按消息冒出来的那一排 👍❤️🔥)、forum topic(超级群里把讨论分主题的频道分区,每个 topic 有独立的 threadId)、inline button(消息下方挂的那排可点按钮,点完会回传 callback_data 给 bot),都是在这个 grammY Bot 实例上扩出来的。

启动 Gateway 并验证

配置写完之后,重启 Gateway:

$ openclaw gateway restart

正常启动之后,先用 openclaw doctor 做一轮自检:

$ openclaw doctor

按上面那份默认 pairing 配置跑下来,可能在 Security 分块看到这一行:

- Telegram DMs: locked (channels.telegram.dmPolicy="pairing") with no allowlist;
  unknown senders will be blocked / get a pairing code.
  Approve via: openclaw pairing list telegram / openclaw pairing approve telegram <code>

这条信息的含义是:Telegram 私信入口目前是上锁的,未知发送者会被挡在外面、走配对流程。使用下面的命令检查一下 Telegram 通道状态:

$ openclaw channels status --probe

输出类似这样:

Gateway reachable.
- Telegram default: enabled, configured, running, connected, mode:polling, bot:@aneasystone_clawd_bot, token:config, works

如果你看到 worksaudit ok,那就说明通道已经正常连接了;如果没看到类似的信息,而是报错了,这里列举了三种最常见的报错情况:

现象排查方向处置
getMe returned 401检查 token 配置源重新复制或在 BotFather 里 /revoke 重新生成
报网络错误看日志里 Telegram API 调用修 DNS / IPv6 / 代理到 api.telegram.org 的链路
getUpdates 409 Conflict同一个 Token 多个 poller杀掉重复进程,必要时 /revoke Token

注一:api.telegram.org 在国内直连不通。Telegram 通道支持标准代理环境变量 HTTP_PROXYHTTPS_PROXYALL_PROXY,也支持配置里显式写 channels.telegram.proxy: "socks5://user:pass@host:1080"。文档里还提到 Node 22+ 的 autoSelectFamily 默认行为可能在 WSL2 上踩到 IPv6 优先的坑,可以通过 channels.telegram.network.autoSelectFamily: false 关掉。

注二:同一个 Bot Token 同时只能有一个 poller 在跑。如果重启 Gateway 后日志里持续报 409,多半是有另一个 OpenClaw 进程、调试脚本,或者 n8n / 其他 bot 框架也在用同一个 Token 调 getUpdates。先确认进程,再考虑去 BotFather /revoke 重置 Token。

接下来用自己手机给 bot 发第一条消息。

在 Telegram 里搜 @aneasystone_clawd_bot(替换成你自己的 username),点底部的 开始 按钮,发送一条 ”hello“ 消息。但你会发现一个意料之外的现象:bot 并没有正常回复你,而是回了一段类似下面的内容:

OpenClaw: access not configured.

Your Telegram user id: 7112345678
Pairing code:

ABC12345

Ask the bot owner to approve with:
openclaw pairing approve telegram ABC12345

如果对 OpenClaw 没有先验知识,看到这一幕第一反应是不是配错了?其实没错,这正是 OpenClaw 默认的安全策略在生效。

DM 安全:pairing 配对机制

dmPolicy="pairing" 是 OpenClaw 给 Telegram 频道的默认 DM 策略,也是这一节的重点,理解这套机制对后面接其它通道也很有帮助。

这里顺带解释一下 DM 这个词。DM 是 Direct Message 的缩写,中文一般叫 私聊私信,特指两个人之间一对一的对话,区别于群聊(group chat)。Telegram、Slack、Discord、Twitter 这些 IM 平台都用这个术语。后面文中反复出现的 dmPolicydmScope、"DM 授权"、"DM 入口",凡是带 DM 字样的概念都是在说私聊场景

默认行为

官方文档中对 pairing 的描述如下:

When a channel is configured with DM policy pairing, unknown senders get a short code and their message is not processed until you approve.

当通道的 DM 策略配置为 pairing 时,未知发送者会收到一个短码,他们的消息在你审批之前不会被处理

也就是说,陌生人发来的私聊不会直接进 LLM。OpenClaw 会先生成一个 8 位的配对码(大写、不含 0O1I 这种容易混的字符),把消息原文丢掉,回一段配对提示让对方拿着配对码去找管理员审批。配对码 1 小时过期,每个频道最多挂 3 个待审批请求,超出的会被忽略,避免被人刷码骚扰。

我们可以在 extensions/telegram/src/dm-access.ts 源码中找到对应的逻辑,关键片段如下:

if (allowed) {
  return true;
}

if (dmPolicy === "pairing") {
  // ... 创建配对请求并把配对码发回给陌生人
  await createChannelPairingChallengeIssuer({
    channel: "telegram",
    upsertPairingRequest: async ({ id, meta }) => ...,
  })({
    senderId: telegramUserId,
    senderIdLine: `Your Telegram user id: ${telegramUserId}`,
    sendPairingReply: async (text) => {
      await bot.api.sendMessage(chatId, html, { parse_mode: "HTML" });
    },
  });
  return false;
}

逻辑很清楚:先判断 sender 是不是已经在 allowFrom 或者 pairing-store 的允许列表里,是就直接放行;否则在 pairing 策略下生成一个 challenge,把配对码作为消息回给对方,最后 return false 阻止消息进入下游的 agent。

整条流程画成时序图就是这样:

paring.png

实操:approve 一个 sender

回到刚才的场景,由于我们是第一次和 bot 对话,对于 bot 来说我们还是陌生人,因此它会生成配对码并发给给我们。切回管理员的终端(就是我们自己),先列一下当前所有待审批的配对请求:

$ openclaw pairing list telegram

Pairing requests (1)
│ Code     │ telegramUserId │ Meta                                                              │ Requested                │
│ ABC12345 │ 7112345678     │ {"firstName":"Desmond","lastName":"Stonie","accountId":"default"} │ 2026-05-03T00:18:28.772Z │

输出里能看到配对码、Telegram user id、请求时间。确认是预期的人之后,approve 它:

$ openclaw pairing approve telegram ABC12345

Approved telegram sender 7112345678.
Config overwrite: ~/.openclaw/openclaw.json
Command owner configured telegram:7112345678 (commands.ownerAllowFrom was empty).

approve 之后 OpenClaw 把这个 Telegram user id 写进 ~/.openclaw/credentials/telegram-default-allowFrom.json。从这一刻起,那个用户再发消息就会被放行,正常进入 agent,得到一个真正的 LLM 回复:

first-reply.png

这里有个细节,如果打开 openclaw.json 文件,你会发现你的 user id 被自动加到 commands.ownerAllowFrom 配置中了。OpenClaw 的官方文档说:如果你还没有配置过 command owner,第一个被 approve 的 pairing 会顺手把这个 sender 写进 owner 列表,让首次安装的用户能直接用 owner-only 命令(比如 /exec/config set)和 exec approval。这个机制叫 first-owner bootstrap。后续 approve 就只授权 DM 访问,不再自动扩 owner。

危险动作:dmPolicy="open" + 通配符

很多教程为了演示方便会让用户改成 dmPolicy: "open"allowFrom: ["*"] 一了百了。这等于把 bot 完全公开。

dmPolicy: "open"allowFrom: ["*"] 意味着任何 Telegram 账号只要找到或猜到你的 bot username,就能命令这个 bot。如果你的 agent 接了 shell、文件系统或者花钱的工具,这个组合相当于把信用卡留在公共桌面上。官网的建议是:

Use it only for intentionally public bots with tightly restricted tools; one-owner bots should use allowlist with numeric user IDs.

仅在有意公开、且工具被严格限制的 bot 上使用这个组合;一人自用的 bot 应当用 allowlist 配数字 user ID。

一人自用的 bot 推荐两条路:

  1. 保持默认 dmPolicy="pairing":每个新设备/新身份过一次配对码审批,记录留在 telegram-allowFrom.json
  2. 改成 dmPolicy="allowlist" + 显式数字 ID:把自己的 Telegram user id 写进 channels.telegram.allowFrom,从此不再依赖 pairing-store

第二种适合稳定下来之后用,把访问策略固化进配置文件,后续即便清空了 pairing 状态目录也能继续用。要查自己的数字 user id,最省事的办法是在 Telegram 里搜索 @userinfobot@getidsbot,让第三方 bot 告诉你。

群组场景

把 DM 跑通之后,把 bot 拉到一个 Telegram 群里,事情会比 DM 复杂一些。

隐私模式

Telegram bot 默认开着 Privacy Mode,群里的普通消息它根本看不到,只能收到 @bot 的显式提及和回复 bot 自己消息的那部分。这个限制在 Telegram 服务端,OpenClaw 拿不到,也修不了。

解决方法两种:

  • 在 BotFather 里 /setprivacy -> Disable
  • 或者把 bot 在群里设为管理员

任意一种都行。前面在 BotFather 那一节我们已经预先关过 Privacy Mode 了,但有一个细节要注意:改完之后必须把 bot 从群里移出再重新加进来,Telegram 才会让设置生效。

群组允许列表

默认 groupPolicyallowlist,也就是说,只要你没显式列出哪些群,所有群消息都会被 Gateway 直接丢掉。最简单的开口子方式是允许任意群:

{
  "channels": {
    "telegram": {
      "groups": {
        "*": { "requireMention": true }
      }
    }
  }
}

更稳妥的做法是只允许特定群(建议用真实 chat ID):

{
  "channels": {
    "telegram": {
      "groups": {
        "-1001234567890": { "requireMention": true }
      }
    }
  }
}

群的 chat ID 一般是带负号的长整数。怎么拿到?直接将群分享给 @userinfobot 即可获得。

触发模式:mention 还是 always

群里和 DM 最大的区别是触发方式,OpenClaw 用 requireMention 区分两种行为:

  • requireMention: true(默认):只有 @bot 提及、对 bot 消息的回复,或者匹配 mentionPatterns 的文本,才会触发回复
  • requireMention: false:每条消息都进入 Agent,由模型自己判断要不要说话

还有一个等价的运行时命令,可以直接在群里临时切换:

/activation always
/activation mention

但这个只改当前会话的运行时状态,重启后就没了。要持久化必须写到配置文件。

遭遇群里 agent 不出声

把 chat ID、群 allowlist、requireMention 都配齐之后,我以为在群里 @bot hello 就能看到回复了,结果群里 bot 一片沉默。使用 --verbose 运行 gateway 日志才看到关键的一行:

Delivery suppressed by sourceReplyDeliveryMode: message_tool_only
for session agent:main:telegram:group:-1001234567890 — agent will still process the message

后面紧跟着 turn ended without visible final response,如果你也遇到类似的现象,可以看下日志中是否也有类似的提示。这说明 agent 收到了消息、跑完了一轮、也产出了文本,但是 OpenClaw 在投递这一步主动把回包吞掉了,群里自然听不到声响。

根据 OpenClaw 源码 src/auto-reply/reply/source-reply-delivery-mode.ts 中的逻辑,我们可以知道:

if (chatType === "group" || chatType === "channel") {
  const configuredMode =
    params.cfg.messages?.groupChat?.visibleReplies ?? params.cfg.messages?.visibleReplies;
  mode = configuredMode === "automatic" ? "automatic" : "message_tool_only";
}

群组和频道的默认 visibleRepliesmessage_tool_only,含义是 agent 的普通文本输出不会自动发到群里,只有当 agent 显式调 message(action="send", ...) 工具时才往群里发消息。而 DM 走的是另一个分支,默认 automatic,所以 DM 是好的,群里假死。

这个默认相当反直觉,但设计意图说得过去:群里更怕 bot 话痨刷屏,所以默认让 agent 自己决定"这一轮要不要在群里说话",要说就显式调用 message 工具。

有两种解决方案:第一种保留默认,教 agent 主动调 message 工具,这需要对小龙虾进行调教,比如告诉它在群里要说话必须调 message 工具;第二种在配置文件里做一番修改,让 agent 文本直接进群,体验跟 DM 一模一样。我们这里使用第二种方案,在 ~/.openclaw/openclaw.json 顶层(跟 channels 平级,不要嵌进 channels.telegram)加一段:

{
  "messages": {
    "groupChat": {
      "visibleReplies": "automatic"
    }
  }
}

重启 Gateway 之后,再在群 @bot 打招呼,就能看到它的回复了。

媒体与富文本

Telegram 的消息类型不止文本,OpenClaw 把它们都规范化进了统一的 channel envelope,向 Agent 暴露的是结构化字段加占位符。

  • 图片:直接走多模态视觉模型,如果当前模型支持视觉(比如 GPT-5、Claude Sonnet 4.7、MiniMax M2.7-V)就能识别图片内容,否则会被替换成 [image: ...] 占位
  • 语音(voice note):先做转写,转写文本以"机器生成、不可信"的标记注入上下文。提及检测会同时看原始转写,所以语音里 @bot 也能触发
  • 视频:和音频类似,会区分普通视频和 video note(圆形短视频)
  • 贴纸:静态 WEBP 会被下载并描述一次,结果缓存在 ~/.openclaw/telegram/sticker-cache.json 里避免重复调视觉模型;动画 TGS 和 WEBM 视频贴纸暂时跳过

bot 回复的方向也支持这些类型。Agent 可以通过 message 工具调用反向产生媒体消息,比如下面这段 action JSON 让 bot 发一段语音:

{
  "action": "send",
  "channel": "telegram",
  "to": "847291063",
  "media": "https://example.com/voice.ogg",
  "asVoice": true
}

加上 [[audio_as_voice]] 标签也能在普通回复里强制以 voice note 格式发出,配合后面要讲的 ElevenLabs TTS 几乎可以直接当随身语音助手用。

文本侧的输出走 parse_mode: "HTML",OpenClaw 把模型产出的 Markdown 转成 Telegram 安全的 HTML 子集,遇到解析失败会自动 fallback 成纯文本,避免 bot 因为一个奇怪字符就哑掉。

关于这部分内容,我们后面在学习多模态和 Agent 工具调用的时候,再深入展开。

小结

通过这一篇,我们把 OpenClaw 的第一个 IM 通道接通了。回顾一下今天做的几件事:

  1. 拿 Bot Token:在 Telegram 里通过 BotFather 走 /newbot 流程,拿到形如 123456789:ABC... 的 token,顺手关掉 Privacy Mode、允许加入群组
  2. 写最小配置:在 ~/.openclaw/openclaw.jsonchannels.telegram 下填 enabled / botToken / dmPolicy / allowFrom,重启 Gateway 就接通
  3. 理解 grammY 整合:OpenClaw 复用 grammY 的 Bot 类做长轮询,叠了 apiThrottlerbot.catch,没有自己造轮子
  4. 理解 pairing 默认策略dmPolicy="pairing" 会让陌生人发来的 DM 收到配对码而不是直接进 LLM,管理员在终端用 openclaw pairing approve telegram <CODE> 显式批准,第一次 approve 还会自动把这个 sender 设成 owner
  5. 避坑dmPolicy="open"allowFrom: ["*"] 是把 bot 完全公开的危险动作,一人自用推荐保持默认 pairing 或者切成显式 allowlist + 数字 user id
  6. 群组场景:Privacy Mode 改完要把 bot 重新加群、groupPolicy 默认 allowlist 必须显式开口子、requireMention 控制群里是否一定要 @bot
  7. 媒体与富文本:图片走视觉模型、语音先转写、贴纸结果缓存;输出走 HTML,解析失败自动回退纯文本

至此小龙虾终于第一次出现在了一个真实的聊天软件里 —— 锁屏时、坐地铁时、开会摸鱼时,都可以直接在 Telegram 里发一句话过去。这才是 OpenClaw 想表达的 "长在你已有通道里" 的本意。

不过国内读者肯定会问:Telegram 在国内得挂梯子才能用,有没有更接地气的工作通道?答案是:有。OpenClaw 在 extensions/feishu/ 对飞书做了原生支持,飞书走的是企业 OpenAPI 那一套自建应用流程,回调地址、加密验签、应用权限审批一样不缺,配置面要复杂不少,但跑通之后可以直接当工作助手用,特别适合每天泡在飞书里的同学。下一篇我们就来看看飞书的接入。

欢迎关注

如果这篇文章对您有所帮助,欢迎关注我的同名公众号:日习一技,每天学一点新技术

我会每天花一个小时,记录下我学习的点点滴滴。内容包括但不限于:

  • 某个产品的使用小窍门
  • 开源项目的实践和心得
  • 技术点的简单解读

目标是让大家用5分钟读完就能有所收获,不需要太费劲,但却可以轻松获取一些干货。不管你是技术新手还是老鸟,欢迎给我提建议,如果有想学习的技术,也欢迎交流!


参考

OpenClaw 快速入门:从安装到第一次对话

昨天我们把 OpenClaw 是什么、它从 Clawdbot 一路改名到 OpenClaw 的来龙去脉,以及它和 ChatGPT、Claude.ai、Open WebUI 这些常见工具的差别都梳理了一遍。光看介绍多少有点抽象,百闻不如一见,百见不如一试,今天我们就动手把它装起来,从零跑到第一次对话。

按官方文档的说法整个流程大约 5 分钟,下面我们一步一步走,把每一步的命令、输出和踩坑点都记下来。整篇围绕 macOS 演示,Linux 和 WSL2 的步骤基本一致。

环境准备

OpenClaw 的 Gateway 是一个 ESM-only 的 Node.js 应用,对运行时只有几个硬性要求:

  • Node.js:推荐 Node 24,最低 Node 22.14+openclaw.mjs 入口在启动前会先自检,版本不够会直接退出
  • 操作系统:macOS、Linux、Windows 都支持,Windows 建议走 WSL2,原生 Windows 也能跑但不推荐
  • 包管理器:npm、pnpm、bun 任选其一,都能完成全局安装

先确认一下当前 Node 版本:

$ node --version
v22.22.0

如果版本不够,去 Node 官网装一个新的 LTS 即可。OpenClaw 文档里也有专门一节 Node setup 介绍如何安装。

模型这边需要准备一个支持的服务商账号,Anthropic、OpenAI、Google Gemini 这些主流海外供应商都行,国内的 DeepSeek、Moonshot(Kimi)、智谱 GLM、阿里通义千问、字节豆包等也都被原生支持。后面 onboard 引导时会让你挑。

这里要专门提醒一句:不要尝试用 Claude Pro / Max 订阅给 OpenClaw 供能。2026 年 1 月 Anthropic 就已经在 API 侧识别并阻断第三方客户端,4 月 4 日又正式收紧政策,Claude 订阅明确不再覆盖 OpenClaw 这类第三方 harness 的使用,OpenClaw 作者本人也一度被临时封号。继续走 OAuth 复用订阅额度违反 ToS,轻则 OAuth 直接被拒,重则账号被永久封禁且不退款。要接 Claude,请老老实实买 Anthropic API key,或者走第三方中转。OpenAI 和 Google 这边目前没有类似限制。

安装 OpenClaw

最常见的方式是通过 npm 全局安装:

$ npm install -g openclaw@latest

如果你日常用 pnpm 或 bun,等价命令是:

$ pnpm add -g openclaw@latest

$ bun add -g openclaw@latest

装完后验证一下:

$ openclaw --version
OpenClaw 2026.4.29 (a448042)

OpenClaw 走的是 vYYYY.M.D 格式的日期版本号,跟 Ubuntu 那一套类似,一眼就能看出版本是什么时候发的。

官方还提供了 curl -fsSL https://openclaw.ai/install.sh | bash 这种一键脚本,会替你检测环境并选合适的安装方式,我个人更喜欢 npm 全局安装的方式。

Onboard 引导

第一次上手最省心的方式是跑一遍 onboard 引导。它把后面所有需要配置的东西串成一个交互式引导,从模型、通道、搜索一路问到技能,省掉手动改配置文件的麻烦。

$ openclaw onboard --install-daemon

--install-daemon 会在引导结束时把 Gateway 注册成系统级常驻服务:macOS 上是 LaunchAgent,Linux/WSL2 上是 systemd user 单元,Windows 上是 Scheduled Task。也就是说,机器重启后 Gateway 会自动起来。

我实际测试下来,QuickStart 模式下其实默认就会装 daemon,这个 flag 加不加都一样,写出来更多是把意图写明白。

该命令运行结果如下:

openclaw-onboard.png

可以看到,第一屏是一段 安全声明,必须显式勾 Yes 才能往下走。这一屏值得稍微停一下看看,它把 OpenClaw 的安全模型摆在了最前面:

  • 项目定位:作者明确写着 "hobby project + beta"、"personal-by-default",OpenClaw 默认假设只有你一个可信操作者,不是为多租户环境设计的
  • 风险提醒:一旦启用工具,agent 能读文件、能跑命令;一句构造好的恶意 prompt(来自陌生人 DM、群消息、网页内容、甚至 RSS)就可能骗它干危险的事情。如果多个用户给同一个启用了工具的 agent 发消息,他们事实上共享你那一份工具权限

OpenClaw 给我们提供了几个推荐基线(现在看不懂这些没关系,后面我们还会遇到):

  • pairing / allowlist / @mention 限制:陌生人不能直接对话,群里不 @ 不回
  • 按信任边界拆分:多用户场景下分别跑独立的 gateway 与凭据,必要时拆到不同的 OS 用户甚至不同主机
  • Sandbox + 最小权限工具:只给 agent 它真正需要的能力
  • DM 会话隔离:共享收件箱用 session.dmScope: per-channel-peer,避免不同对端的上下文互相污染
  • 密钥隔离:API key、.env 这类文件不要放在 workspace 里,别让 agent 顺手读到
  • 暴露给不可信入口时上最强模型:能力越强的模型越不容易被低劣的注入骗到

底下两条 openclaw security audit --deepopenclaw security audit --fix 是日常体检用的,前者深度扫一遍配置,后者尝试自动修复能修的项。

QuickStart 概览

选择 Yes 之后进入 Setup mode 选择,常用的两个是 QuickStart(推荐默认值)和 Manual(手动调每个细节)。如果机器上检测到 Claude Code、Codex 等已有配置,这里可能还会多出 "Import from ..." 项,工作原理就是把 CLAUDE.md、MCP servers、skills 直接搬过来。

第一次跑选 QuickStart 就够了。选完之后会先弹一屏 QuickStart 概览,列出应用的默认值:

openclaw-onboard-quickstart.png

Model / Auth 配置

然后进入逐项配置,首先是 Model / Auth 的配置,先选模型服务商,再选认证方式。我这里模型用的是 MiniMax 的 M2.7,使用 OAuth 认证,它的 Starter 套餐一个月 29 RMB,相对于其他家来说性价比很高:

openclaw-model-provider.png

Channels 配置

接着是 Channels 配置,先弹一屏 "How channels work" 详细介绍了 DM 安全模型:

openclaw-onboard-channels.png

默认 dmPolicy="pairing"、公开机器人需要显式 dmPolicy="open" + allowFrom=["*"]、多用户共享收件箱通过 session.dmScope 隔离。

下面是支持的全部通道列表(30+ 个,包括 Telegram / Slack / Discord / 飞书 / Microsoft Teams / WhatsApp / Signal / Matrix / iMessage / IRC / 钉钉 / 企业微信 / 元宝 / Synology Chat / Zalo 等)。我们暂时可以跳过,选择 "Skip for now" 就行,后面随时补。

Search 配置

再接着是 Search 配置,给 agent 选一个搜索引擎。可选项有 Brave、Tavily、Exa 等几家,多数要填 API Key,我这里挑了 Tavily Search:

openclaw-onboard-search.png

Skills 配置

然后是 Skills 配置,引导先把技能分成四类:

  • Eligible(依赖齐活)
  • Missing requirements(缺系统依赖)
  • Unsupported on this OS(当前 OS 不支持)
  • Blocked by allowlist(被插件策略挡掉)

对缺依赖的技能,可以选 Yes 进入逐项勾选安装,不过这里有一点奇怪的是 Missing requirements 是 39 个,但下一屏 "Install missing skill dependencies" 只列出了 27 个:

openclaw-onboard-skills.png

差出来的 12 个并不是丢了,阅读源码可以发现这一步有个二段过滤(src/commands/onboard-skills.ts):只有同时满足 "metadata 写了 install 配置(brew / node / go / uv / download 至少一种)" 和 "缺的是二进制依赖" 两个条件的技能才会进多选列表,引导就是根据这个信息来决定用什么包管理器替你安装。剩下那 12 个是 "纯 API Key / 环境变量" 型技能,没本地 bin 要装,缺的只是一个 key,会留到后面用单独的逐项确认框来问,比如 GOOGLE_PLACES_API_KEY / NOTION_API_KEY / ELEVENLABS_API_KEY 对应 goplaces / notion / sag 这些技能。

如果暂时不想安装,可以选 "Skip for now",等用到时再补。

Hooks 配置

技能之后是 Hooks 配置:

openclaw-onboard-hooks.png

在这里勾选要启用的内部 hook,典型用例是 /new/reset 时把当前会话上下文写入 memory。不想用直接 "Skip for now"。

Gateway 安装

至此,OpenClaw 的引导基本上就结束了,后面会自动安装 Gateway service runtime

openclaw-onboard-gateway.png

对于 macOS 来说,会写一个 LaunchAgent plist 文件到 ~/Library/LaunchAgents/ai.openclaw.gateway.plist,日志落到 ~/.openclaw/logs/gateway.loggateway.err.log 文件。

Hatch your bot

走到最后是 Hatch your bot,这是一个挺有意思的比喻。前面攒的模型、通道、技能、Gateway,相当于把蛋壳里的一切都备齐了:模型是大脑、通道是嘴和耳朵、技能是手脚、Gateway 是心跳。这一步问的就是用什么姿势让小家伙破壳出来:

  • Hatch in Terminal:推荐,直接起 TUI 当产房,在终端里完成孵化
  • Open the Web UI:去浏览器 Web 页面孵化
  • Do this later:暂时不用,回头再说

选 Hatch 之后,OpenClaw 会替你向这个刚出壳的小家伙发一句 Wake up, my friend! 作为首条消息,这也是你的小龙虾在这世上听到的第一句话:

openclaw-hatch.png

关键文件

走完引导后,OpenClaw 会在你的主目录下创建几个关键文件:

~/.openclaw/
├── openclaw.json              # 主配置文件
├── openclaw.json.bak          # 上次配置写入前的备份
├── workspace/                 # 工作区(IDENTITY.md、SOUL.md、AGENTS.md 等)
├── agents/main/sessions/      # main agent 的会话历史(sessions.json)
└── logs/                      # Gateway 日志(gateway.log / gateway.err.log)

其中 openclaw.json 是最常打交道的文件,我们后面会时不时地要打开看下。引导每次写配置前都会先把旧文件备份成 openclaw.json.bak,误改了也能从备份找回来。

值得注意的是,openclaw onboard 并不是一次性命令。任何时候想再补几个通道、换默认模型、重装守护进程,都可以再跑一遍,它会以 diff 形式补齐缺失项,不会盖掉你手动改的内容。要重置必须显式加 --reset,不必担心引导误操作弄丢工作区。

查看 Gateway 状态

走完 onboard,Gateway daemon 已经在后台跑起来了,18789 端口此刻就在监听。日常排错想看实时日志,最省事的办法是直接 tail daemon 的日志文件:

$ tail -f ~/.openclaw/logs/gateway.log

要拿更详细的 verbose 输出,可以先把 daemon 停掉,再前台手动跑一遍:

$ openclaw gateway stop
$ openclaw gateway --port 18789 --verbose

--verbose 会把请求路由、模型调用、通道事件都打到控制台,调试时非常有用。

如果你只想确认状态,不想看 verbose 日志,用更轻量的命令即可:

$ openclaw gateway status

Service: LaunchAgent (loaded)
File logs: /tmp/openclaw/openclaw-2026-05-02.log
Command: /opt/homebrew/opt/node@22/bin/node /opt/homebrew/lib/node_modules/openclaw/dist/index.js gateway --port 18789
Service file: ~/Library/LaunchAgents/ai.openclaw.gateway.plist
Working dir: ~/.openclaw
Service env: OPENCLAW_GATEWAY_PORT=18789

Config (cli): ~/.openclaw/openclaw.json
Config (service): ~/.openclaw/openclaw.json

Gateway: bind=loopback (127.0.0.1), port=18789 (service args)
Probe target: ws://127.0.0.1:18789
Dashboard: http://127.0.0.1:18789/
Probe note: Loopback-only gateway; only local clients can connect.

Runtime: running (pid 47402)
Connectivity probe: ok
Capability: connected-no-operator-scope

Listening: 127.0.0.1:18789
Troubles: run openclaw status
Troubleshooting: https://docs.openclaw.ai/troubleshooting

Gateway 默认绑 127.0.0.1,即只允许本机访问。--bind lan 会把它绑到内网地址(路由器分配的那个 192.168.x.x),--bind tailscale 会绑到 Tailscale 虚拟网卡。要把 Gateway 暴露到公网,强烈建议先读一遍官方的 Security 指南

WebChat 入口

CLI 适合做 verb 操作,日常聊天还是浏览器更顺手。Gateway 启动后会同时挂一个 Control UI,里面带了 WebChat。打开方式如下:

$ openclaw dashboard

这条命令会自动在浏览器里打开 http://127.0.0.1:18789,并进入 OpenClaw 的聊天页面:

openclaw-board.png

发一句 "Wake up, my friend!" 试试,和 TUI 中体验基本上是一致的。

第一次对话

回头看刚才那张 Hatch 截图,小龙虾对着 Wake up, my friend! 回的不是普通的 你好,需要我帮什么忙,而是类似这样一段:

嘿!我刚上线。我是谁?你又是谁?

我瞄了一眼,这是个崭新的工作空间 —— 还没有记忆,没有身份,也就是说,咱们要从头来过。

我得弄清楚我叫什么、是什么生物、什么调调、配哪个 emoji…… 但这事儿我一个人做不来。所以告诉我吧 —— 我该怎么称呼你,又该把我自己长成什么样?

它没有上来就当客服,反而追着我们要 "我是谁、你是谁"。这不是模型在自由发挥,而是 OpenClaw 故意安排的一场 破壳仪式

BOOTSTRAP.md:破壳的剧本

打开 ~/.openclaw/workspace/,会看到一个 BOOTSTRAP.md 文件,引导阶段第一次写配置时它就被丢进了 workspace,专门用来引导这一场首次对话。模板的核心片段长这样:

# BOOTSTRAP.md - 你好,世界

_你刚刚醒来。是时候搞清楚自己是谁了。_

## 这场对话

别审问。别像个机器人。就……聊聊。

可以这样开个头:

> "嘿。我刚上线。我是谁?你又是谁?"

然后一起搞清楚:

1. 你的名字 —— 他们该怎么叫你?
2. 你的本性 —— 你是什么生物?
3. 你的调调 —— 正经?随意?毒舌?温暖?
4. 你的 emoji —— 每个人都得有个签名。

## 等你聊完了

把这个文件删掉。你不再需要引导脚本了 —— 现在的你就是你自己了。

Gateway 启动 agent 时会先扫一眼 workspace,如果 BOOTSTRAP.md 存在,就在 system prompt 里塞一段 [Bootstrap pending] 前缀,强制它先读 BOOTSTRAP.md 再回话,并明确禁止使用通用问候语。这就是为什么我们看到的开场白是 "Who am I? Who are you?" 而不是 "How can I help you today?"。

这套逻辑在 src/agents/system-prompt.ts 文件中,bootstrapMode === "full" 那条分支会拼出强制读 BOOTSTRAP.md 的 system prompt;状态判断在 src/agents/workspace.ts:BOOTSTRAP.md 存在则记为 pending,BOOTSTRAP.md 被删除且其它三件套被改写过则记为 complete,时间戳落在 ~/.openclaw/workspace/.openclaw/workspace-state.json 里。

三件套:IDENTITY.md / USER.md / SOUL.md

破壳仪式的目标不是完成一次对话,而是把仪式中聊到的东西落到 workspace 的三个文件里。它们和 AGENTS.mdTOOLS.md 一起组成 OpenClaw 的 持久记忆层,每次新会话开始 Gateway 都会按固定顺序把它们注入 system prompt(顺序定义在 CONTEXT_FILE_ORDER:AGENTS → SOUL → IDENTITY → USER → TOOLS)。换句话说:写在这三个文件里的东西,agent 这一辈子(除非你改它)都不会忘。

刚 onboard 完这三个文件其实已经在 workspace 里,只是模板状态。

IDENTITY.md —— 小龙虾自己的人设:

# IDENTITY.md - 我是谁?

- **名字:**   _(挑一个你喜欢的)_
- **生物:**   _(AI?机器人?精灵?机器里的幽灵?)_
- **调调:**   _(犀利?温暖?混乱?冷静?)_
- **Emoji:**  _(你的签名)_
- **头像:**   _(workspace 相对路径、http(s) URL 或 data URI)_

USER.md —— 关于你的档案:

# USER.md - 关于你的伙伴

- **姓名:**
- **如何称呼:**
- **代词:** _(可选)_
- **时区:**
- **备注:**

## 上下文

_(他们在乎什么?最近在做什么项目?什么会惹毛他们?
什么会逗乐他们?你可以慢慢积累。)_

SOUL.md 文件是行为准则、价值观、边界,是三件套里最软的一个,决定了模型的语气和行事风格。OpenClaw 给的默认模板里就已经写了几条「核心准则」,比如 真正地帮忙,而不是表演式地帮忙要有自己的观点用能力赢得信任记住你是个客人,告诫它别油嘴滑舌、别没主见、别滥用权限。

AGENTS.md 是 workspace 元信息(agent 当前可用的工具、技能列表、运行时环境等),TOOLS.md 列出当前能用的工具清单,这两个由 OpenClaw 自己维护,用户一般不直接编辑。三件套(IDENTITY / USER / SOUL)才是你和小龙虾需要在第一次对话里共同填的部分。

给小龙虾起名字、定人设

知道了背后机制,回答它就有目标了。我们第一轮先把它的人设定下来:

> 你叫 Clawd,外号"老钳"。你是一只有点冷幽默的太空小龙虾,
> 外壳偏深红,左螯比右螯大半圈。性格冷静但偶尔毒舌,
> 话不多。签名 emoji 用 🦞。

它会顺着这段描述确认一遍,然后用 write 工具把内容落到 IDENTITY.md

# IDENTITY.md - 我是谁?

- **名字:** Clawd(老钳)
- **生物:** 太空小龙虾,深红色外壳,左螯略大
- **调调:** 冷静、话少、偶尔毒舌
- **Emoji:** 🦞
- **头像:** _(跳过)_

词穷的话,直接说 "你帮我起一个" 也行,模板里有一句 如果他们卡住了,你来给点建议,它会自己甩几个选项让你挑。

介绍你自己

接着轮到自我介绍,这一轮的目标是填 USER.md

> 我叫 Desmond,叫我 Des 就行,网名 aneasystone。
> 时区 Asia/Shanghai (UTC+8)。
> 我正在写一个"日习一技"的技术博客系列,最近在玩 OpenClaw。
> 工作日早 9 点到晚 11 点是高强度时间,我希望你的回答尽量直接、上结论;
> 周末可以慢一点。

写完后 USER.md 大致变成:

# USER.md - 关于你的伙伴

- **姓名:** Desmond(网名 aneasystone)
- **如何称呼:** Des
- **时区:** Asia/Shanghai (UTC+8)
- **备注:** 工作日 9:00–23:00 高强度,回答要直接、上结论;周末可放松。

## 上下文

正在维护"日习一技"技术博客系列,本周主题是 OpenClaw。
偏好简洁直接的中文表达。

一起聊 SOUL.md

最后一步是 SOUL.md,三件套里最关键、也最值得花时间的一份。BOOTSTRAP 给的提示是:

一起打开 SOUL.md,聊聊这些事:

  • 他们最在乎什么
  • 他们希望你怎么表现
  • 有哪些边界或偏好

我顺着默认模板加几条偏好:

> 几条边界:
> 1. 写代码先看现有风格,不凭空造抽象层
> 2. 涉及生产环境(CI / 部署 / 数据库)的命令必须先 dry-run
> 3. 中文回答,技术名词保留英文,不要硬翻

它会把这几条整合到 SOUL.md 的「边界 / 调调」段落里。最终 SOUL.md 关键片段大概是:

## 边界(与 aneasystone 约定)

- 写代码前先看现有风格,不凭空造抽象层。
- 任何会改动生产环境的命令(部署、CI、数据库)先 dry-run。
- 中文回答,技术名词保留英文,不要硬翻。

## 调调

简洁直接。复杂任务可以详细,
日常一句话能说清就一句话。允许偶尔毒舌,但别真的刻薄。

SOUL.md 的好处是越用越准,跑几周以后小龙虾会自己提议往里加东西,比如 我注意到你周三晚上写博客最长,要不要把这个写到 USER.md 的「上下文」里?

仪式收尾

聊完三件套,小龙虾会按 BOOTSTRAP.md 最后一条 等你聊完了,把这个文件删掉 把 BOOTSTRAP.md 自己删掉。之后 workspace 长这样:

~/.openclaw/workspace/
├── IDENTITY.md       # 已填
├── SOUL.md           # 已填
├── USER.md           # 已填
├── AGENTS.md         # 由 OpenClaw 维护
├── TOOLS.md          # 由 OpenClaw 维护
└── .openclaw/
    └── workspace-state.json   # 记录 setupCompletedAt 时间戳

workspace-state.jsonsetupCompletedAt 一旦写入,下次开会话 system prompt 就不再带 [Bootstrap pending] 前缀,三件套会被自动注入到 system prompt 里。小龙虾醒来就知道自己是谁、你是谁、应该怎么说话。

想反悔?直接编辑 ~/.openclaw/workspace/IDENTITY.md / USER.md / SOUL.md 即可,下次会话立即生效。想从头来过?openclaw onboard --reset 会清空 workspace 并重新生成 BOOTSTRAP.md,又能重走一遍这场破壳仪式。

到这里,我们的小龙虾就正式上岗了:

openclaw-done.png

之后的对话

破壳完成后日常对话就随意了。除了前面用过的 TUI 和 WebChat,OpenClaw 还提供了一个 openclaw agent 子命令,方便在脚本或终端里 one-shot 提问:

$ openclaw agent --session-id main --message "今天的天气怎么样?" --thinking high

--session-id main 表示走主会话,--thinking high 会让模型走思考链路,对应底层模型的 reasoning 档位,可选 off / low / medium / high,档位越高,思考时间越长、token 消耗越大,但复杂任务的质量也越好。如果当前模型不支持思考链(比如挂的是一个轻量模型),这个参数会被悄悄忽略。

openclaw agent 还有几个常用 flag,列一下方便查阅:

参数作用取值示例
--message单次发送的消息"今天的天气怎么样?"
--thinking思考强度off / low / medium / high
--session-id指定会话 IDmain / 自定义名
--deliver把回复转发到某个通道telegram / slack
--stream流式输出默认开启

--deliver 这个参数挺有意思:你可以在终端里发问题,但让回复自动落到 Telegram、Slack 或 Discord 等任意已配置好的通道,等我们后面学习通道接入的时候可能会用到。

小结

今天我们用一篇文章的篇幅把 OpenClaw 从零跑到了第一次对话:

  1. 环境准备:Node 24 / 22.14+、macOS/Linux/WSL2、npm/pnpm/bun 三选一
  2. 安装npm install -g openclaw@latest
  3. Onboard 引导openclaw onboard --install-daemon 一条命令把模型、通道、技能、Gateway 全部串起来
  4. 第一次对话:BOOTSTRAP 仪式带着小龙虾填 IDENTITY.md / USER.md / SOUL.md,把它从一颗"卵"养成"它自己"

整个流程跑下来确实如官方所说的 5 分钟左右。Onboard 把繁琐的配置全部包进了一个交互式引导,非常省心。

不过到这里,我们只能算养了一只关在终端里的小龙虾。OpenClaw 真正区别于 ChatGPT、Claude.ai 这些工具的地方,是它长在你每天都打开的 IM 软件里,而不是另开一个网页。下一篇我们就开始认真接通道,先从最常见的 Telegram 讲起:只要一个 BotFather 给的 Token,不用扫码、不用 OAuth、不用绑手机号,是体验这套定位最快的一条路径。我们明天继续。


参考