系列目标:读完全系列,你能基于 OpenClaw 做定制开发,也能参考它的设计从零搭建类似系统。
本篇核心问题:Gateway 是什么?为什么要有它?它是怎么一步步被设计出来的?
先从你作为用户的体验说起
假设你已经装好了 OpenClaw,你的日常使用是这样的:
- 早上在手机 WhatsApp 问它:"帮我整理一下今天的会议纪要",它打开你电脑上的文件夹,生成文档,然后把链接发回给你
- 下午在电脑 Slack 频道里问它:"现在服务器状态怎么样",它 SSH 进服务器跑命令,把结果返回给你
- 同时,你打开浏览器的 Web UI,看到它正在执行的任务进度,实时滚动的日志
- 你的 iPhone 也在旁边,能随时语音唤醒它
这里面有一件微妙的事情:这四个入口(WhatsApp、Slack、Web UI、iPhone)同时在用同一个 AI 助手,而且它们看到的状态是同步的。
这就带来了一个工程问题。
问题:谁来协调这一切?
不妨想象一下,如果没有任何中枢,会发生什么:
WhatsApp 连接 → AI 进程 A
Slack 连接 → AI 进程 B
Web UI 连接 → AI 进程 C
iPhone 连接 → AI 进程 D
四个进程各自独立。在 WhatsApp 问的问题,Slack 里看不到;Web UI 看到的状态,和真实执行进度不同步;你在 Slack 说"停止",WhatsApp 那边的 AI 还在跑。
这行不通。
所有的入口必须共享同一个 AI 的同一个状态。 这意味着需要一个单一的协调中心——它连接所有的消息通道,管理唯一一个 AI 执行进程,并把状态实时同步给所有连接的客户端。
这就是 OpenClaw Gateway 存在的根本原因。
Gateway 的本质:一个控制平面
OpenClaw 的代码注释用了一个专业术语来描述 Gateway:
Gateway WebSocket control plane
控制平面(Control Plane) 是网络工程里的概念:负责"决策和协调"的那一层,而不是"传输数据"的那一层。
用更直白的话说,Gateway 干三件事:
① 消息枢纽:所有消息通道(WhatsApp、Telegram、Slack...)的消息都汇入 Gateway,Gateway 决定交给哪个 AI 会话处理,再把 AI 的回复分发出去。
② 命令中心:CLI、macOS App、Web UI、手机 App 都通过 Gateway 控制 AI——启停会话、查看状态、修改配置、触发任务。
③ 状态广播站:AI 在执行任务时,Gateway 把实时状态广播给所有连接的客户端。你在手机上问的问题,在电脑 Web UI 上也能实时看到 AI 的思考过程。
理解这三件事,Gateway 后面所有的设计决策都会变得顺理成章。
第一个设计决策:为什么用 WebSocket?
确定了"Gateway 需要实时同步状态给多个客户端"之后,接下来的问题是:用什么协议?
最常见的选项是 HTTP。但 HTTP 有一个根本性的限制:它是请求-响应模式,必须客户端先问,服务端才能答。服务端没有办法主动推送消息。
而 Gateway 有一个强烈的需求:AI 在生成回复时,要把每个字实时推给所有客户端。不是等 AI 生成完整段话再一次性发过来,而是像打字机一样,生成一个字就推一个字(这就是 LLM 的"流式输出")。
用 HTTP 实现这个需求有两种方式:
- 长轮询(Long Polling):客户端不断问"有新内容吗",服务器有了再回答。延迟高,连接开销大。
- SSE(Server-Sent Events):服务器可以主动推,但只能单向,客户端没办法同时发命令。
这两种都满足不了需求。OpenClaw 需要的是:客户端和服务端都能随时主动发消息,而且是持久连接,不用每次都重新握手。
这正是 WebSocket 的设计目标。一旦建立连接,双方可以随时互发消息,延迟极低,也没有重复握手的开销。
普通 HTTP:
客户端 →→→ 请求 →→→ 服务端
客户端 ←←← 响应 ←←← 服务端
(连接关闭,下次再来)
WebSocket:
建立一次连接后,双方随时可以发:
客户端 →→→ "执行这个命令" →→→ 服务端
服务端 ←←← "AI 正在思考..." ←←← 服务端(主动推)
服务端 ←←← "AI 说:..." ←←← 服务端(继续推)
客户端 →→→ "停止" →→→ 服务端
(连接一直保持)
这就是 Gateway 选择 WebSocket 作为主协议的原因——不是因为 WebSocket 时髦,而是业务需求决定的。
HTTP 并没有消失。Gateway 同时监听 HTTP,用于:浏览器访问 Web UI(必须 HTTP)、Slack/Webhook 等外部回调(第三方只会发 HTTP)、OpenAI 兼容接口(方便接入现有 SDK)。但这些都是辅助场景。
第二个设计决策:连接进来之后怎么认识你?
Gateway 现在用 WebSocket 对外提供服务。连接进来的客户端可能是:
- 你自己的 CLI(完全可信,可以做任何事)
- 你的 Web UI(你自己用,但最好限制只读,防止误操作)
- 你的 iPhone 节点(它能上报摄像头画面,但不应该能修改配置)
- 一个 Webhook 调用(外部触发,权限最小)
这四种客户端需要不同的权限。 怎么区分它们?
最简单的方案是:每种客户端用不同的 Token。但这样管理成本高,而且粒度太粗——你没法做到"Web UI 可以查看会话列表,但不能删除会话"。
OpenClaw 的解法是三层认证模型,每层解决不同的问题:
第一层:你是谁?(HTTP 层 Token)
建立 WebSocket 连接的那一刻,HTTP Upgrade 请求里必须带 Token:
GET /ws HTTP/1.1
Authorization: Bearer your-token-here
这一层只判断一件事:这个 Token 是不是合法的 Gateway Token。合法就允许建立连接,不合法直接断开。这是门卫,只管"能不能进门"。
第二层:你是什么角色?(连接握手 Role)
进门之后,客户端发第一条消息——connect 消息:
{
"method": "connect",
"params": {
"token": "...",
"role": "operator",
"clientId": "macos-app"
}
}
这里的 role 只有两个值:
operator:人类操作者。CLI、macOS App、Web UI 都是 operator。node:设备节点。iPhone、Android、macOS 节点模式。
两种角色能调用的方法完全隔离:
// src/gateway/role-policy.ts
export function isRoleAuthorizedForMethod(role, method) {
if (isNodeRoleMethod(method)) {
return role === "node"; // node 专属方法:只有设备节点能调用
}
return role === "operator"; // 其余方法:只有人类操作者能调用
}
iPhone(node 角色)不能调用 config.apply 修改配置——即使它拿到了合法 Token,role 不对就是不行。反过来,CLI(operator 角色)也调不了 node.invoke.result(那是设备节点上报执行结果用的)。
为什么要把 role 放在 connect 消息而不是 HTTP 层?
因为 HTTP 层只是"进门",而 role 决定"进门后能去哪个房间"。把两层分开,可以用同一个 Token 连接,但根据 role 获得不同权限——这在测试和调试时非常方便。
第三层:你能做什么?(Scope 细粒度控制)
对于 operator 角色,还有更细的 scope 控制:
// src/gateway/method-scopes.ts
const READ_SCOPE = "operator.read"; // 只读:看状态、查配置
const WRITE_SCOPE = "operator.write"; // 写操作:触发 Agent、改配置
const ADMIN_SCOPE = "operator.admin"; // 全部权限
这解决了一个实际需求:Web UI 可以对外暴露(比如给团队成员查看 AI 执行日志),但你不想让他们能触发 Agent 运行或修改配置。只要给他们的连接只分配 READ_SCOPE,就做到了权限隔离,而不需要维护多套 Token。
三层合在一起:
HTTP Token → 你能不能连进来?
Role → 你是人类操作者还是设备节点?
Scope → 在你的角色范围内,你能做哪些具体操作?
第三个设计决策:connect 为什么必须是第一条消息?
现在理解了认证的三层设计,你会自然想到一个问题:
Role 和 Scope 信息在
connect消息里,但 Token 在 HTTP 头里。为什么不把所有认证信息都放 HTTP 头里,省掉这个connect步骤?
因为 WebSocket 连接在 HTTP 升级之后,服务端就不知道这个连接的身份了——HTTP 头只在建立连接时传一次,之后的 WebSocket 帧里没有 HTTP 头。
所以必须在 WebSocket 层再做一次认证握手,connect 消息就是这个握手。
客户端 → 服务端: HTTP Upgrade(带 Bearer Token)
[第一层:能不能进门]
WebSocket 连接建立
客户端 → 服务端: { method: "connect", params: { role, scopes, clientId, ... } }
[第二层+第三层:进来之后是谁,能做什么]
服务端 → 客户端: { type: "hello-ok", gatewayMethods: [...], events: [...], ... }
[握手完成,告诉客户端这个 Gateway 支持什么]
如果 connect 之后再发一次 connect 会怎样?
// src/gateway/server-methods/connect.ts
export const connectHandlers = {
connect: ({ respond }) => {
respond(false, undefined, errorShape("connect is only valid as the first request"));
},
};
直接报错。这 12 行的文件就是一个兜底——真正的 connect 处理逻辑在更底层(ws-connection/message-handler.ts),在进入 Handler 路由之前就已经处理了。正常连接中你永远不会碰到这个兜底 Handler。
hello-ok 里有什么?
服务端返回的不只是"认证成功",还有完整的能力清单:
{
type: "hello-ok",
gatewayMethods: ["health", "agent", "sessions.list", ...], // 这个 Gateway 支持哪些 RPC 方法
events: ["agent", "presence", "tick", ...], // 会推哪些事件
healthSnapshot: { ... }, // 当前系统健康快照
presenceSnapshot: { ... }, // 当前在线状态快照
}
注意 gatewayMethods 是动态生成的:
// src/gateway/server-methods-list.ts
export function listGatewayMethods(): string[] {
const channelMethods = listChannelPlugins()
.flatMap((plugin) => plugin.gatewayMethods ?? []);
return Array.from(new Set([...BASE_METHODS, ...channelMethods]));
}
如果你安装了 MS Teams 插件,它可以注册自己的 RPC 方法,这个列表就会多出来。客户端在握手时就知道服务端支持什么,不用靠文档猜,也不用靠版本号判断兼容性。
第四个设计决策:90 个方法怎么管理?
Gateway 总共支持约 90 个 RPC 方法(health、agent、sessions.list、config.set...)。
这些方法怎么注册?OpenClaw 的解法出奇地简单:
// src/gateway/server-methods.ts
export const coreGatewayHandlers = {
...connectHandlers, // connect
...healthHandlers, // health
...agentHandlers, // agent, agent.wait
...sessionsHandlers, // sessions.list, sessions.patch, sessions.reset ...
...configHandlers, // config.get, config.set, config.apply ...
...cronHandlers, // cron.list, cron.add, cron.run ...
...skillsHandlers, // skills.status, skills.install ...
...nodeHandlers, // node.list, node.invoke ...
// ... 共约 30 个 handler 组
};
这是一个扁平的 JavaScript 对象:key 是方法名字符串,value 是处理函数。没有路由树,没有中间件链,就是一个 Map。
当一条消息进来:
// 查找 handler → 调用
const handler = extraHandlers?.[req.method] ?? coreGatewayHandlers[req.method];
if (!handler) { respond(error("unknown method")); return; }
handler({ req, respond, client, context });
为什么不用更"正规"的路由框架?
因为 90 个方法对路由树来说完全没必要——哈希表查找是 O(1),路由树反而引入了额外的解析开销和代码复杂度。
插件怎么扩展方法?
注意 extraHandlers?.[req.method] 在前面——插件注册的 Handler 优先级高于核心 Handler。插件只需要 export 一个同类型的对象,在加载时 spread 进去,就能注册新方法,甚至可以覆盖内置方法的行为。
第五个设计决策:如何让多个客户端实时同步?
Gateway 维护了一个所有已连接客户端的集合:
const clients = new Set<GatewayWsClient>();
当 AI 产生新的输出,Gateway 调用 broadcast 函数,向集合里的每个客户端发送事件:
broadcast("agent", {
phase: "streaming",
sessionKey: "agent:main:dm:alice",
text: "正在分析你的文件...",
})
所有连接的客户端——不管是 CLI、Web UI 还是 iPhone——同时收到这条消息,实时显示 AI 的输出。
一个细节:如果客户端断线重连,怎么恢复状态?
broadcast 函数有一个 stateVersion 参数:
broadcast("presence", payload, {
stateVersion: { presence: currentPresenceVersion }
})
每次状态变化,版本号 +1。客户端重连时,带上自己记住的最后版本号。如果服务端的版本更新了,就发送完整的状态快照而不是增量。
这解决了一个经典的分布式问题:客户端断线期间错过的状态变化,怎么补齐? 答案是:不补,直接发最新全量状态。简单可靠,不会出现漏更新导致的状态不一致。
把所有设计连起来看
现在可以画出 Gateway 的完整工作流程:
1. 用户在 WhatsApp 发消息
↓
2. WhatsApp 通道收到消息,通过内部事件传给 Gateway
↓
3. Gateway 路由层决定交给哪个 Agent 的哪个会话
(这部分是下一篇的主题:通道与路由系统)
↓
4. Agent 开始执行,产生流式输出
↓
5. Gateway 调用 broadcast("agent", { text: "..." })
↓
6. 所有连接的客户端同时收到:
- Web UI 实时显示进度
- iPhone App 显示通知
- CLI 打印输出
↓
7. Agent 执行完,回复通过 Gateway 发回 WhatsApp
每一个环节的设计选择都有清晰的来由:
| 问题 | 解法 | 原因 |
|---|---|---|
| 多客户端共享同一个 AI 状态 | Gateway 作为单一中枢 | 没有中枢就没法协调 |
| 需要实时双向通信 | WebSocket | HTTP 无法服务端主动推送 |
| 不同客户端需要不同权限 | 三层认证(Token/Role/Scope) | 粒度从粗到细,各层职责清晰 |
| 90 个方法的管理 | 扁平 Handler Map | 简单高效,插件轻松扩展 |
| 断线重连后状态恢复 | 版本号 + 全量快照 | 简单可靠,不怕漏更新 |
启动流程:Gateway 上电时做了什么
理解了设计之后,再来看启动流程就很自然了。Gateway 的 startGatewayServer() 函数按以下顺序初始化:
① 读取配置文件,如果是旧格式就自动迁移
② 预检所有密钥引用——有一个不存在就立刻报错退出(Fail-Fast)
③ 生成或验证 Gateway Token
④ 加载所有插件(通道插件、功能插件)
⑤ 建立所有消息通道的连接(WhatsApp、Telegram、Slack...)
⑥ 挂载 WebSocket 处理器,开始监听
⑦ 启动 Cron 任务调度、心跳监控、本地网络发现
第②步的"Fail-Fast"设计值得单独说一下:如果配置里引用了一个不存在的 API Key,很多系统的处理方式是"先跑起来,用到的时候再报错"。OpenClaw 不这样——启动时就检查,发现问题就拒绝启动,明确报错。
这对个人 AI 助手来说尤为重要:一个带着错误配置运行的助手,会出现"发消息没有回应"这种极难调试的问题。不如一开始就让它无法启动,错误信息清清楚楚。
小结
本篇从用户使用场景出发,推导了 Gateway 的每一个核心设计:
- 为什么需要 Gateway:多通道、多客户端需要一个协调中枢
- 为什么用 WebSocket:实时双向通信是刚需,HTTP 做不到
- 为什么要 connect 握手:WebSocket 层需要自己的认证机制
- 为什么三层认证:不同客户端信任级别不同,权限需要分层
- 为什么用扁平 Handler Map:简单够用,插件扩展零阻力
- 为什么版本号 + 全量快照:断线重连场景下最可靠
下一篇将进入 Gateway 内部最复杂的逻辑——通道与路由系统:
WhatsApp 来一条消息,OpenClaw 怎么知道应该交给哪个 Agent?如果你配置了多个 Agent,不同的群组、不同的联系人,怎么路由到正确的地方?
这背后是一套 8 级优先级的路由规则,设计得相当精妙。
对应代码:src/gateway/ | 关键文件:server.impl.ts、server-methods.ts、role-policy.ts、method-scopes.ts