目录

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 → Telegram

Node 看不见入站渠道流量。渠道消息落在 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 会话,是架构禁止项,不是运维偏好。

文档里的不变量:

  1. 每个 host 上,打开 WhatsApp / Baileys 会话的地方只有这一处。
  2. 入站 WS 帧按 JSON Schema(TypeBox)校验;第一帧必须是 connect,否则硬关闭。
  3. 事件不重放。序列出现缺口,客户端自己 refresh。
  4. Cron 跑在 Gateway 进程内。进程不在,定时任务不在。

渠道是入站消息面,模型 provider 是出站推理面。两者都配在同一个 daemon 里,但不是同一层:Telegram 进来的文字先变成会话,再由 agent loop 按 session key 串行跑一轮;Anthropic / OpenAI / 本地模型只在这一轮里被调用。同一会话上后来的消息走 queue(steer / followup / collect / interrupt),避免两个渠道把同一条 transcript 写乱。渠道插件换了,循环还是这一套。

hello-ok.features.methods 是保守的发现列表,不是全部 RPC 的生成清单。push.testsessions.usage 这类方法可以真实存在却不出现在 discovery 里。客户端按协议版本协商(现行 4),不要把 feature 列表当成 SDK 的完整表面。

核对入口(官方 CLI,未在写这篇的环境对照跑过):

openclaw gateway status
openclaw channels status --probe
openclaw agents list --bindings
openclaw sessions --json

gateway status 同时看 supervisor 是否认为服务在跑,以及 CLI 能否真正连上 WebSocket。渠道 probe 才证明入站面活着。

协议:三种帧,三个角色

Gateway protocol 是唯一的控制面和 node 传输。现行协议版本是 4。Handshake 先发 connect.challenge,客户端第一帧必须是 connect,成功则 hello-ok(带 featuressnapshotpolicyauth)。

三种帧:

{ "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 }

sendagentchat.send 这类有副作用的方法要带 idempotencyKey。Agent 跑分两段:先 accepted ack,再流式 event:agent,最后 res:agent 终态。官方时序:

sequenceDiagram participant Client participant Gateway Client->>Gateway: req:connect Gateway-->>Client: res (ok) Note right of Gateway: or res error + close Note left of Client: payload=hello-ok<br>snapshot: presence + health Gateway-->>Client: event:presence Gateway-->>Client: event:tick Client->>Gateway: req:agent Gateway-->>Client: res:agent<br>ack {runId, status:"accepted"} Gateway-->>Client: event:agent<br>(streaming) Gateway-->>Client: res:agent<br>final {runId, status, summary}

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:当日笔记,靠搜索召回,不每轮塞进 prompt
  • USER.md / SOUL.md / AGENTS.md:人设和偏好,同样是 workspace 文件,不是 Gateway 状态库

边界:

  1. Session = 这次对话的 transcript 和路由键。受模型窗口限制,靠 compaction 压缩。Gateway 拥有。
  2. Memory = 跨会话还想留下的笔记。模型只记得写进磁盘的内容。Agent workspace 拥有。
  3. sessionKey、session label、bindings 决定消息进哪颗脑子、哪条 transcript。它们不是权限边界。同一 Gateway 上的高权限工具、browser、凭证、落盘会话仍然共享宿主信任面。

可选的 memory.search.rememberAcrossConversations 只在私聊之间做检索,不合并 transcript;默认 groupScope: "per-group" 时,群聊两边都不参与这条召回。

Node 不是 Client

Nodes 和 Clients 连同一条 WS,角色不同。

  • Client / operator:发 healthstatussendagentsessions.*,订阅 tick / presence / agent。一台一连。
  • Noderole: "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 时,agentIdmain,主会话键是 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.dmScopeopenclaw sessions --json 的 session key,不要先怪模型。

渠道插件会变,Baileys / grammY 目前仍是 WhatsApp / Telegram 的官方实现。以现行架构文档为准,不跟过期的适配器传闻。飞书、Matrix、Zalo 也是同一套 plugin 入站,不另起控制面。清单以现行 Channels 页为准。