OpenClaw Gateway 是整个 OpenClaw 系统的“控制中枢”和“通信枢纽”,负责统一管理所有聊天渠道(如 Telegram、WhatsApp、飞书等)的连接、消息路由、会话状态维护以及安全认证,是实现本地化 AI 助手运行的核心组件。Gateway 是什么?为什么要有它?它是怎么一步步被设计出来的?

支持多种即时通讯平台(IM),将不同平台的消息格式统一处理,确保跨渠道通信无缝衔接。
根据用户身份、聊天来源自动识别会话上下文,并将请求精准路由到对应的 AI Agent,保持对话连贯性。
协调 AI 模型推理、工具调用(如浏览器操作、文件读写、定时任务)及多 Agent 协同工作,驱动实际任务执行。
提供访问令牌、密码认证、设备配对等功能,保障本地服务不被未授权访问;默认仅绑定本地回环地址(127.0.0.1),增强安全性。
对外提供 OpenAI 兼容 API 和响应式接口,支持 CLI、Web 控制台、移动端等多种客户端接入。
假设你已经装好了 OpenClaw,你的日常使用是这样的:
这里面有一件微妙的事情:这四个入口(WhatsApp、Slack、Web UI、iPhone)同时在用同一个 AI 助手,而且它们看到的状态是同步的。
这就带来了一个工程问题。
不妨想象一下,如果没有任何中枢,会发生什么:
WhatsApp 连接 → AI 进程 ASlack 连接 → AI 进程 BWeb UI 连接 → AI 进程 CiPhone 连接 → AI 进程 D
四个进程各自独立。在 WhatsApp 问的问题,Slack 里看不到;Web UI 看到的状态,和真实执行进度不同步;你在 Slack 说"停止",WhatsApp 那边的 AI 还在跑。
这行不通。
所有的入口必须共享同一个 AI 的同一个状态。 这意味着需要一个单一的协调中心——它连接所有的消息通道,管理唯一一个 AI 执行进程,并把状态实时同步给所有连接的客户端。
这就是 OpenClaw 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 后面所有的设计决策都会变得顺理成章。
确定了"Gateway 需要实时同步状态给多个客户端"之后,接下来的问题是:用什么协议?
最常见的选项是 HTTP。但 HTTP 有一个根本性的限制:它是请求-响应模式,必须客户端先问,服务端才能答。服务端没有办法主动推送消息。
而 Gateway 有一个强烈的需求:AI 在生成回复时,要把每个字实时推给所有客户端。不是等 AI 生成完整段话再一次性发过来,而是像打字机一样,生成一个字就推一个字(这就是 LLM 的"流式输出")。
用 HTTP 实现这个需求有两种方式:
这两种都满足不了需求。OpenClaw 需要的是:客户端和服务端都能随时主动发消息,而且是持久连接,不用每次都重新握手。
这正是 WebSocket 的设计目标。一旦建立连接,双方可以随时互发消息,延迟极低,也没有重复握手的开销。
普通 HTTP: 客户端 →→→ 请求 →→→ 服务端 客户端 ←←← 响应 ←←← 服务端 (连接关闭,下次再来)WebSocket: 建立一次连接后,双方随时可以发: 客户端 →→→ "执行这个命令" →→→ 服务端 服务端 ←←← "AI 正在思考..." ←←← 服务端(主动推) 服务端 ←←← "AI 说:..." ←←← 服务端(继续推) 客户端 →→→ "停止" →→→ 服务端 (连接一直保持)
这就是 Gateway 选择 WebSocket 作为主协议的原因——不是因为 WebSocket 时髦,而是业务需求决定的。
HTTP 并没有消失。Gateway 同时监听 HTTP,用于:浏览器访问 Web UI(必须 HTTP)、Slack/Webhook 等外部回调(第三方只会发 HTTP)、OpenAI 兼容接口(方便接入现有 SDK)。但这些都是辅助场景。
Gateway 现在用 WebSocket 对外提供服务。连接进来的客户端可能是:
这四种客户端需要不同的权限。 怎么区分它们?
最简单的方案是:每种客户端用不同的 Token。但这样管理成本高,而且粒度太粗——你没法做到"Web UI 可以查看会话列表,但不能删除会话"。
OpenClaw 的解法是三层认证模型,每层解决不同的问题:
建立 WebSocket 连接的那一刻,HTTP Upgrade 请求里必须带 Token:
GET /ws HTTP/1.1Authorization: Bearer your-token-here
这一层只判断一件事:这个 Token 是不是合法的 Gateway Token。合法就允许建立连接,不合法直接断开。这是门卫,只管"能不能进门"。
进门之后,客户端发第一条消息——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.tsexport 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 获得不同权限——这在测试和调试时非常方便。
对于 operator 角色,还有更细的 scope 控制:
// src/gateway/method-scopes.tsconst 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 → 在你的角色范围内,你能做哪些具体操作?
现在理解了认证的三层设计,你会自然想到一个问题:
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.tsexport 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.tsexport function listGatewayMethods(): string[] { const channelMethods = listChannelPlugins() .flatMap((plugin) => plugin.gatewayMethods ?? []); return Array.from(new Set([...BASE_METHODS, ...channelMethods]));}如果你安装了 MS Teams 插件,它可以注册自己的 RPC 方法,这个列表就会多出来。客户端在握手时就知道服务端支持什么,不用靠文档猜,也不用靠版本号判断兼容性。
Gateway 总共支持约 90 个 RPC 方法(health、agent、sessions.list、config.set…)。
这些方法怎么注册?OpenClaw 的解法出奇地简单:
// src/gateway/server-methods.tsexport 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 的 startGatewayServer() 函数按以下顺序初始化:
① 读取配置文件,如果是旧格式就自动迁移② 预检所有密钥引用——有一个不存在就立刻报错退出(Fail-Fast)③ 生成或验证 Gateway Token④ 加载所有插件(通道插件、功能插件)⑤ 建立所有消息通道的连接(WhatsApp、Telegram、Slack...)⑥ 挂载 WebSocket 处理器,开始监听⑦ 启动 Cron 任务调度、心跳监控、本地网络发现
第②步的"Fail-Fast"设计值得单独说一下:如果配置里引用了一个不存在的 API Key,很多系统的处理方式是"先跑起来,用到的时候再报错"。OpenClaw 不这样——启动时就检查,发现问题就拒绝启动,明确报错。
这对个人 AI 助手来说尤为重要:一个带着错误配置运行的助手,会出现"发消息没有回应"这种极难调试的问题。不如一开始就让它无法启动,错误信息清清楚楚。
本篇从用户使用场景出发,推导了 Gateway 的每一个核心设计:
WhatsApp 来一条消息,OpenClaw 怎么知道应该交给哪个 Agent?如果你配置了多个 Agent,不同的群组、不同的联系人,怎么路由到正确的地方?
这背后是一套 8 级优先级的路由规则,设计得相当精妙。
对应代码:src/gateway/ | 关键文件:server.impl.ts、server-methods.ts、role-policy.ts、method-scopes.ts