OpenClaw 架构
OpenClaw 把渠道、会话和设备收进一个 Gateway daemon。Agent 不对接 Telegram 或 WhatsApp;对接的是 Gateway 的 typed WebSocket。渠道插件只是入站面,不是第二套运行时。
一个 Gateway 对应一个信任边界。多渠道、多 agent、多 node 都叠在同一进程里;要拆的是互不信任的操作者,不是聊天软件的数量。
官方口径写在 Gateway architecture:一个长生命周期的 Gateway 拥有全部消息面;控制面客户端和 node 都连同一条 WebSocket。仓库在 openclaw/openclaw。安装、loopback、pairing、allowlist 不在这篇里,见 最佳实践。
Gateway 是枢纽
渠道适配器各自说话。WhatsApp 走 Baileys(WhatsApp Web 链接会话),Telegram 走 grammY(默认 long polling)。Slack、Discord、Signal、iMessage、WebChat 以及更多 channel plugins 也是同一模式:插件把平台事件翻译成 Gateway 内部信封,socket 和重连留在 daemon 里。
渠道 / WebChat Clients (operator) Nodes (role: node)
WhatsApp · Telegram CLI · TUI · Control UI macOS / iOS / Android
Slack · Discord · … macOS app · automations headless node host
\ | /
\ | /
v v v
┌─────────────────────────────────────────────────────┐
│ Gateway daemon ws+http 127.0.0.1:18789 │
│ 渠道连接 · 会话 SQLite · 路由/bindings · pairing │
│ typed WS RPC · cron · canvas HTTP · node.invoke │
└──────────────────────────┬──────────────────────────┘
│
v
Agent runtime(embedded)
workspace · 模型循环 · tools
MEMORY.md / memory/*.md这张图里的名字都来自现行文档,没有多造一层「Message Router」服务。FAQ 把流量写死成:
Telegram → Gateway → Agent → node.* → Node → Gateway → TelegramNode 看不见入站渠道流量。渠道消息落在 Gateway,模型在 Gateway 里跑,需要设备能力时才 node.invoke。
Daemon 管什么
Gateway 是一台机器上的常驻进程。默认一个 host 一个实例,绑定 127.0.0.1:18789。这一端口是复用的:
- WebSocket:控制面 RPC 和 node 传输
- HTTP:Control UI、hooks、
/__openclaw__/canvas/、/__openclaw__/a2ui/,以及/v1/models一类 OpenAI 兼容面
控制面仍然是 WS。HTTP 存在,并不等于渠道走 REST 轮询。Telegram 插件自己的 long polling 是渠道实现细节,出不了 Gateway 进程。
WebChat 不是另一套协议。它是走同一条 Gateway WS 的内部渠道(chat.history / chat.send),Control UI 的 HTTP 只是壳。远程时它和 CLI、node 共用同一条 SSH / Tailscale 隧道。
渠道插件可以 bundled、official、external 三种来源,但进程边界不变:Telegram 目前 bundled,WhatsApp 是 on-demand 的 official plugin,Gateway 仍然是唯一持有链接会话的地方。两个进程抢同一条 Baileys 会话,是架构禁止项,不是运维偏好。
文档里的不变量:
- 每个 host 上,打开 WhatsApp / Baileys 会话的地方只有这一处。
- 入站 WS 帧按 JSON Schema(TypeBox)校验;第一帧必须是
connect,否则硬关闭。 - 事件不重放。序列出现缺口,客户端自己 refresh。
- Cron 跑在 Gateway 进程内。进程不在,定时任务不在。
渠道是入站消息面,模型 provider 是出站推理面。两者都配在同一个 daemon 里,但不是同一层:Telegram 进来的文字先变成会话,再由 agent loop 按 session key 串行跑一轮;Anthropic / OpenAI / 本地模型只在这一轮里被调用。同一会话上后来的消息走 queue(steer / followup / collect / interrupt),避免两个渠道把同一条 transcript 写乱。渠道插件换了,循环还是这一套。
hello-ok.features.methods 是保守的发现列表,不是全部 RPC 的生成清单。push.test、sessions.usage 这类方法可以真实存在却不出现在 discovery 里。客户端按协议版本协商(现行 4),不要把 feature 列表当成 SDK 的完整表面。
核对入口(官方 CLI,未在写这篇的环境对照跑过):
openclaw gateway status
openclaw channels status --probe
openclaw agents list --bindings
openclaw sessions --jsongateway status 同时看 supervisor 是否认为服务在跑,以及 CLI 能否真正连上 WebSocket。渠道 probe 才证明入站面活着。
协议:三种帧,三个角色
Gateway protocol 是唯一的控制面和 node 传输。现行协议版本是 4。Handshake 先发 connect.challenge,客户端第一帧必须是 connect,成功则 hello-ok(带 features、snapshot、policy、auth)。
三种帧:
{ "type": "req", "id": "r1", "method": "health", "params": {} }
{ "type": "res", "id": "r1", "ok": true, "payload": { "ok": true } }
{ "type": "event", "event": "tick", "payload": { "ts": 1730000000 }, "seq": 12 }send、agent、chat.send 这类有副作用的方法要带 idempotencyKey。Agent 跑分两段:先 accepted ack,再流式 event:agent,最后 res:agent 终态。官方时序:
connect 时声明 role:
| role | 是什么 | 不是什么 |
|---|---|---|
operator |
CLI、TUI、Control UI、macOS 控制面 | 不暴露 camera / system.run |
node |
设备能力宿主:camera、screen、location、canvas、system.run |
不是第二条 Gateway,不接渠道 |
worker |
关闭协议上的云执行宿主,走 /__openclaw__/worker |
不进通用 operator RPC |
Node 还要在 connect 里声明 caps / commands / permissions。Gateway 把它们当 claim,真正放行看服务端 allowlist 和 pairing 批准。pairing 是按设备,不按用户账号。
不要自己实现一套「Agent WebSocket 服务」。捆绑的 agent runtime 跑在 Gateway 里。要写的是 operator 客户端,或给某个 agent 配 workspace 和 bindings。
Session 是路由,Memory 是磁盘上的 Markdown
Session 状态归 Gateway。UI 不本地存一份权威会话,向 Gateway 查。默认路径:
~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite默认行为:
| 来源 | 会话 |
|---|---|
| DM | 收进该 agent 的 main session(agent:<id>:main) |
| 群 / room | 每个群独立 |
| Cron | 每次运行新会话 |
| Webhook | 每个 hook 独立 |
会话键是路由坐标,不是文件名。默认 DM 收成 agent:main:main。群按渠道加 peer,Slack / Discord thread 会在后面接 :thread:,Telegram forum topic 接 :topic:。Binding 只在渠道已经放行这条消息之后才选 agent;pairing、allowlist、mention gate 是另一层,不在这篇展开。
session.scope: "global" 也不会把不同 agent 的对话揉在一起。共享键 global 仍然按路由选中的 agent 记账。
多人能私聊同一个 bot 时,session.dmScope: "main" 会把 Alice 的上下文交给 Bob。这是会话键的问题,不是 MEMORY.md 写错了。官方推荐隔离:
{
session: {
dmScope: "per-channel-peer",
},
}Memory 是 agent workspace 里的 Markdown,默认 ~/.openclaw/workspace:
MEMORY.md:长期、精炼的事实,会话开始注入memory/YYYY-MM-DD.md:当日笔记,靠搜索召回,不每轮塞进 promptUSER.md/SOUL.md/AGENTS.md:人设和偏好,同样是 workspace 文件,不是 Gateway 状态库
边界:
- Session = 这次对话的 transcript 和路由键。受模型窗口限制,靠 compaction 压缩。Gateway 拥有。
- Memory = 跨会话还想留下的笔记。模型只记得写进磁盘的内容。Agent workspace 拥有。
sessionKey、session label、bindings 决定消息进哪颗脑子、哪条 transcript。它们不是权限边界。同一 Gateway 上的高权限工具、browser、凭证、落盘会话仍然共享宿主信任面。
可选的 memory.search.rememberAcrossConversations 只在私聊之间做检索,不合并 transcript;默认 groupScope: "per-group" 时,群聊两边都不参与这条召回。
Node 不是 Client
Nodes 和 Clients 连同一条 WS,角色不同。
- Client / operator:发
health、status、send、agent、sessions.*,订阅tick/presence/agent。一台一连。 - Node:
role: "node"。macOS / iOS / Android / headless 把本机能力暴露给 Gateway。macOS 菜单栏应用本身就是一个 node,不要在同一台 Mac 上再起一个 CLI node。
远程 Gateway + 笔记本执行,是官方支持的形状:Gateway 收消息、跑模型、转发 exec host=node;node host 在那台机器上跑 system.run。没有单独的 TCP bridge。Canvas 也是这条线上的能力:daemon 在同一端口提供 /__openclaw__/canvas/,macOS node 用 canvas.* 把页面呈现在本机面板。不是另起一台静态站点。
Device pairing 管握手身份。node.pair.* 管这台设备允许暴露哪些命令。两道门。
Agent 实现什么,Gateway 拥有什么
Multi-agent 把一个 agent 定义成「一颗脑子」:workspace、agentDir、自己的 SQLite session store。Gateway 进程可以托管很多个。Binding 按 (channel, accountId, peer) 把入站消息派给其中一个。默认单 agent 时,agentId 是 main,主会话键是 agent:main:main。
| 面 | Gateway 拥有 | Agent 实现 / 配置 |
|---|---|---|
| 渠道 socket、重连、账号 login | 是 | 否 |
| 会话键、transcript、compaction | 是 | 否 |
| WS 协议、pairing、node.invoke | 是 | 否 |
| Cron / heartbeat 调度 | 是 | 否 |
| Canvas / A2UI HTTP | 是 | 否 |
AGENTS.md / SOUL.md / MEMORY.md |
否 | 是 |
| 模型选择、skills 快照、tool 调用 | 循环由 Gateway 嵌入执行 | 策略和 workspace 按 agent 配 |
| 工具硬边界(allowlist、sandbox、exec approval) | 策略存在 Gateway | 按 agent 收紧,不能比全局更松 |
「自己写一个 Agent 对接 Gateway WS」把控制面客户端和 agent runtime 混了。Operator 客户端实现 WS。Agent 是 workspace + 模型循环;循环已经嵌在 daemon 里。
一个 Gateway 里多 agent 很便宜,也是默认做法。官方 FAQ 写明:只在硬隔离或互不信任时才拆机器。
选 / 不选
选 一个 Gateway:
- 操作者是同一信任边界:个人助手,或互相信任的小团队。官方把共享 Gateway 当一等部署:会话有创建者和可指派 owner,Control UI 能看见谁在看、谁在打字。这是单信任域里的多人,不是多租户隔离。
- 要统一的是渠道和设备,不是租户。WhatsApp + Telegram + Discord 叠在同一 daemon 上,正是这个设计
- 要第二台电脑的 camera /
system.run:加 node,不要第二套 Gateway - 要第二个人设:加
agents.entries+ binding,不要第二套渠道栈
不选(拆 Gateway / 拆 OS user / 拆 host):
- 个人高权限助手、团队助手、公开入口三种风险等级
- 互为对手的用户。官方口径:a gateway is one trust domain
- 故意做的 rescue bot,或两套完全不想共享的配置
- 需要独立的
OPENCLAW_STATE_DIR、workspace、gateway.port的隔离实例,见 Multiple gateways
拆渠道、拆 sessionKey、拆 agentId,都解决不了「同一个高权限进程」。那是信任边界,不是路由。Gateway 统一渠道的办法是归一化信封、会话键和嵌入循环,不是给每个 IM 起一个 bot 进程。多一个渠道只是多一条 plugin 连接;多一个信任边界才是多一个 daemon。loopback、pairing、allowlist 怎么钉死,仍然只在最佳实践那篇。
常见失败:第一帧不是 connect
Gateway 是 WebSocket 服务器。第一帧不是 JSON connect,连接以 code 1008 关掉。常见原因:把 http://127.0.0.1:18789/ 当 WS 打开,或代理把握手剥掉。
怎么看见:
openclaw gateway status看 Probe target: 是不是 ws://…(或 wss://…),以及 Connectivity probe 是否 ok。进程活着、HTTP Control UI 能开,都不证明 RPC 握手成功。1008 是协议门,不是 Telegram token 失效;渠道 probe 失败才去查账号。
另一类架构误判:默认 DM 共用 main session,表现为「bot 把 A 的话写进 B 的对话」。先查 session.dmScope 和 openclaw sessions --json 的 session key,不要先怪模型。
渠道插件会变,Baileys / grammY 目前仍是 WhatsApp / Telegram 的官方实现。以现行架构文档为准,不跟过期的适配器传闻。飞书、Matrix、Zalo 也是同一套 plugin 入站,不另起控制面。清单以现行 Channels 页为准。