系列目标读完全系列你能基于 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 的代码注释用了一个专业术语来描述 GatewayGateway WebSocket control plane控制平面Control Plane是网络工程里的概念负责决策和协调的那一层而不是传输数据的那一层。用更直白的话说Gateway 干三件事① 消息枢纽所有消息通道WhatsApp、Telegram、Slack…的消息都汇入 GatewayGateway 决定交给哪个 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客户端不断问有新内容吗服务器有了再回答。延迟高连接开销大。SSEServer-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 请求里必须带 TokenGET /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.tsexportfunctionisRoleAuthorizedForMethod(role,method){if(isNodeRoleMethod(method)){returnrolenode;// node 专属方法只有设备节点能调用}returnroleoperator;// 其余方法只有人类操作者能调用}iPhonenode 角色不能调用config.apply修改配置——即使它拿到了合法 Tokenrole 不对就是不行。反过来CLIoperator 角色也调不了node.invoke.result那是设备节点上报执行结果用的。为什么要把 role 放在 connect 消息而不是 HTTP 层因为 HTTP 层只是进门而 role 决定进门后能去哪个房间。把两层分开可以用同一个 Token 连接但根据 role 获得不同权限——这在测试和调试时非常方便。第三层你能做什么Scope 细粒度控制对于 operator 角色还有更细的 scope 控制// src/gateway/method-scopes.tsconstREAD_SCOPEoperator.read;// 只读看状态、查配置constWRITE_SCOPEoperator.write;// 写操作触发 Agent、改配置constADMIN_SCOPEoperator.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.tsexportconstconnectHandlers{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.tsexportfunctionlistGatewayMethods():string[]{constchannelMethodslistChannelPlugins().flatMap((plugin)plugin.gatewayMethods??[]);returnArray.from(newSet([...BASE_METHODS,...channelMethods]));}如果你安装了 MS Teams 插件它可以注册自己的 RPC 方法这个列表就会多出来。客户端在握手时就知道服务端支持什么不用靠文档猜也不用靠版本号判断兼容性。第四个设计决策90 个方法怎么管理Gateway 总共支持约 90 个 RPC 方法health、agent、sessions.list、config.set…。这些方法怎么注册OpenClaw 的解法出奇地简单// src/gateway/server-methods.tsexportconstcoreGatewayHandlers{...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 → 调用consthandlerextraHandlers?.[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 维护了一个所有已连接客户端的集合constclientsnewSetGatewayWsClient();当 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 作为单一中枢没有中枢就没法协调需要实时双向通信WebSocketHTTP 无法服务端主动推送不同客户端需要不同权限三层认证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