1. 从一次连接被拒说起:OpenClaw Gateway 到底在调度什么
如果你正在读 OpenClaw 的源码,大概率会在src/gateway这个目录里卡住。我第一次跑openclaw gateway的时候,客户端连上去三秒就被踢掉,日志里只有一行challenge verify failed,当时完全不知道从哪里下手。后来把src/gateway下的文件逐个翻了一遍才明白:Gateway 不是普通的 HTTP 服务器,它是整个 OpenClaw 的控制平面,所有外部交互、内部模块协同、工具调用请求都要经过它。它同时承担了 WebSocket 长连接管理、JSON-RPC 风格的消息分发、会话持久化、通道注册、定时任务触发这几件事,任何一环出问题都会表现为“连不上”或“连上就断”。
这篇内容聚焦src/gateway目录下的源码结构,围绕 WebSocket 长连接与 JSON-RPC 消息分发两条主线,把 Gateway 的调度逻辑拆开讲清楚。目标很具体:给你一份可复制的 gateway 配置骨架,带你在本地把服务跑起来,用真实的 WebSocket 客户端完成一次connect → req → res的完整往返,并且把 AI 工具联调时用到的统一 Key/API 通道接进来。适合已经能跑起 OpenClaw 主程序、想深入理解调度层、或者正在做二次开发(自定义通道、插件、钩子)的人。读完之后你应该能自己判断:消息从客户端发出后,在 Gateway 内部经过了哪几个模块,卡在哪一步该看哪个日志。
2. 前置准备:TaoToken 统一通道与本地环境
在动 Gateway 之前,先把 AI 工具侧的调用通道准备好。OpenClaw 的 Gateway 本身只负责调度,真正执行推理和工具调用的是 Agent 层,而 Agent 层要访问模型能力时,走的就是统一的 API 通道。我这边习惯用 TaoToken 来做这件事,原因是它把 Key 管理和 API 入口统一了,联调时不用在多个配置文件里来回改 base_url 和 token。
你需要先拿到一个可用的 API Key。打开控制台创建即可:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite创建完 Key 之后,在 API Keys 页面可以查看和管理:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewriteAPI 的基础入口是https://taotoken.net/api,注意这个地址不带任何查询参数,配置里直接写它就行。如果你要对照接口字段和请求格式,接入文档在这里:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite本地环境方面,Node.js 版本建议 22 及以上,因为 Gateway 依赖的sqlite-vec在新版本 Node 上安装更顺。包管理器用 pnpm,和 OpenClaw 仓库保持一致。确认一下:
node -v # v22.x 或更高 pnpm -v # 9.x 或更高然后进入 OpenClaw 项目根目录,安装依赖:
pnpm install这一步如果卡在sqlite-vec编译上,先别急着怀疑 Gateway 代码,大概率是构建工具链的问题,后面第 5 节会专门讲。
3. 可复制配置:gateway 配置骨架与 src/gateway 关键文件
3.1 配置文件骨架
Gateway 的配置合并优先级是:环境变量 > 用户配置(~/.openclaw/config.yaml)> 默认配置。所以本地调试时,最省事的做法是只写用户配置,把要覆盖的字段填上。下面这份骨架可以直接复制,改掉注释里标出的几项就能用:
# ~/.openclaw/config.yaml gateway: # 监听端口,默认 18789 port: 18789 # 本地调试保持 127.0.0.1;需要局域网访问再改 0.0.0.0 bindHost: 127.0.0.1 # 远程连接必须配置私钥,本地连接可留空 auth: privateKey: "" # 认证失败后断开连接的等待时间(毫秒) verifyTimeout: 3000 # 会话持久化,基于 SQLite sessions: dbPath: "~/.openclaw/sessions.db" # 自动持久化间隔(分钟) flushInterval: 5 # 定时任务,标准 cron 表达式 cron: timezone: "Asia/Shanghai" jobs: - name: "daily-ping" schedule: "0 8 * * *" action: "tool.invoke" params: tool: "weather" city: "Shanghai" # AI 通道,指向统一 API 入口 ai: baseUrl: "https://taotoken.net/api" apiKey: "${TAOTOKEN_API_KEY}" model: "claude-sonnet"apiKey这里用了环境变量占位,实际运行时通过export TAOTOKEN_API_KEY=你的Key注入,避免把密钥写进文件。这一点在多人协作或者把配置提交到仓库时特别重要。
3.2 src/gateway 目录结构速览
配置写完之后,得知道这些字段分别被哪个文件读取。src/gateway下的核心文件职责如下:
| 文件 | 核心职责 | 调试关注点 |
|---|---|---|
server.ts | 创建 WS/HTTP 服务,管理 Connection 对象 | 连接状态、最后活跃时间 |
server-startup.ts | 启动流程总调度,按序初始化模块 | 配置合并、数据库初始化 |
server-channels.ts | 扫描并注册所有通道 | 通道加载失败日志 |
auth/ | 挑战-应答式身份认证 | 私钥配置、超时时间 |
sessions/ | 会话 CRUD 与 SQLite 持久化 | 过期会话清理 |
tools-invoke-http.ts | 工具调用 HTTP 接口转发 | 请求参数校验 |
server-cron.ts | 基于 node-cron 的定时任务 | 时区、表达式 |
control-ui.ts | Web 控制台,CSP 严格 | 外域资源被拦截 |
hooks.ts | 生命周期钩子管理 | 钩子执行顺序 |
server-plugins.ts | 插件扫描与初始化 | 插件加载警告 |
这里有个容易忽略的点:server.ts只负责创建服务,真正的初始化逻辑全在server-startup.ts。所以当你看到“服务起来了但通道没注册”这类现象,应该去翻server-startup.ts的调用顺序,而不是盯着server.ts。
3.3 WebSocket 与 JSON-RPC 的消息形态
Gateway 的通信核心是 WebSocket + HTTP 双协议,内部消息采用 JSON-RPC 风格。核心指令有四个:connect、req、res、event。第一帧必须是connect,并且携带challengeSig完成挑战验证,否则连接会被直接拒绝。这就是我开头踩的那个坑。
一个合法的connect帧长这样:
{ "type": "connect", "id": "c-001", "payload": { "challengeSig": "<由私钥对 challenge 签名得到>", "client": "local-debug" } }验证通过后,服务端会回一个res,之后你才能发req。req用来发起请求(比如工具调用),res是对应响应,event是服务端主动推送(比如定时任务触发、通道状态变化)。理解这四个指令的时序,是读懂src/gateway调度逻辑的关键。
4. 启动与验证:跑通一次 connect → req → res
4.1 启动 Gateway
配置就绪后,用调试模式启动,日志会更详细:
openclaw gateway --debug正常启动后,控制台会输出:
[gateway] config merged: default < user < env [gateway] database initialized at ~/.openclaw/sessions.db [gateway] channels loaded: 3 ok, 0 failed [gateway] plugins loaded: 1 ok, 0 failed [gateway] cron jobs registered: 1 Gateway started on port 18789如果卡在某一步没有继续,先看logs/gateway.log,再对照第 5 节的排查表。
4.2 用 Node 脚本完成一次往返
下面这段脚本可以直接复制运行,它完成connect → req → res的完整流程。把challengeSig换成你本地生成的签名即可(本地调试时如果auth.privateKey为空,部分版本会跳过签名校验,具体以你拉到的源码为准):
// debug-gateway.mjs import WebSocket from "ws"; const ws = new WebSocket("ws://127.0.0.1:18789"); ws.on("open", () => { console.log("[client] socket opened"); ws.send(JSON.stringify({ type: "connect", id: "c-001", payload: { challengeSig: "local-debug-sig", client: "debug-script" } })); }); ws.on("message", (raw) => { const msg = JSON.parse(raw.toString()); console.log("[client] recv:", msg.type, msg.id); if (msg.type === "res" && msg.id === "c-001") { // 认证通过,发起一次工具调用请求 ws.send(JSON.stringify({ type: "req", id: "r-001", payload: { method: "tools.invoke", params: { tool: "weather", city: "Shanghai" } } })); } if (msg.type === "res" && msg.id === "r-001") { console.log("[client] tool result:", JSON.stringify(msg.payload)); ws.close(); } }); ws.on("close", () => console.log("[client] closed")); ws.on("error", (err) => console.error("[client] error:", err.message));运行:
node debug-gateway.mjs成功时你会看到类似输出:
[client] socket opened [client] recv: res c-001 [client] recv: res r-001 [client] tool result: {"ok":true,"data":{"city":"Shanghai","temp":24}} [client] closed这一步跑通,说明 WebSocket 长连接、挑战验证、JSON-RPC 分发、工具调用转发四条链路都是通的。如果connect之后没有收到res,问题在认证;如果收到了res但req没有响应,问题在tools-invoke-http.ts的转发或 Agent 层。
4.3 接入 AI 工具联调
工具调用链路通了之后,把 AI 通道接进来。Gateway 本身不直接调模型,它把请求转发给 Agent 层,Agent 层再通过ai.baseUrl访问统一 API。联调时我一般先用模型对话页面确认 Key 和通道是通的:
https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite如果后面要做长期编码或者 Agent 类的持续任务,可以看 Coding Plan:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewriteClaude Code 相关的接入方式在:
https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite联调时的顺序建议是:先用模型对话确认通道可用,再回到 Gateway 发req,这样能把“通道问题”和“Gateway 调度问题”分开定位。
5. 本篇常见错排查
调试 Gateway 时遇到的报错,大部分集中在下面几类。我按现象、原因、处理方式整理成表,方便你直接对照。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
启动即崩溃,日志YAML parse error | 配置文件缩进用了 tab 或特殊字符未加引号 | 用 2 空格缩进,字符串加引号,过一遍 YAML 校验 |
EADDRINUSE: address already in use :::18789 | 端口被占用 | lsof -i:18789找到进程后 kill,或改gateway.port |
Cannot find module 'sqlite-vec' | 依赖未装好或 Node 版本过低 | 确认 Node ≥ 22,pnpm add sqlite-vec --force重装 |
| 连接后 3 秒被断开 | 挑战验证失败 | 检查auth.privateKey,本地调试可临时放宽校验 |
| 通道加载失败但服务正常 | 单个通道依赖缺失 | 看logs/gateway.log中channel load failed,禁用该通道即可 |
req发出后无响应 | 工具调用转发异常 | 在tools-invoke-http.ts打印请求参数,确认 Agent 层是否收到 |
| 控制台页面样式丢失 | CSP 拦截了外域资源 | 这是预期行为,改用本地资源或内联样式 |
| 定时任务不触发 | 时区或 cron 表达式错误 | 检查cron.timezone,用标准 5 段表达式 |
有一个通用经验值得单独说:Gateway 的启动流程是“串行初始化 + 容错降级”。配置和数据库属于核心步骤,失败会直接终止启动;通道和插件属于非核心步骤,失败只降级不崩溃。所以当你看到服务起来了但某个功能不可用,先去日志里找对应的failed记录,而不是怀疑整个启动流程。
另外,--debug模式会打印配置合并的每一层来源,当你分不清某个字段到底生效了没有,直接看这行日志最快:
[gateway] config merged: default < user < env它告诉你最终值来自哪一层。环境变量优先级最高,所以如果你在 shell 里 export 过同名变量,配置文件里的值会被覆盖,这一点在排查“配置改了没生效”时特别有用。
6. 继续深入:从 Gateway 到 Agent 执行引擎
把 Gateway 跑通之后,你会发现它做的事情其实是“接收、认证、分发、转发”,真正的处理逻辑在 Agent 执行引擎里。src/gateway的调度逻辑可以概括为三条线:WebSocket 负责连接生命周期,JSON-RPC 负责消息语义,server-startup.ts负责模块初始化顺序。这三条线理清楚,二次开发时你就知道该往哪里加钩子、往哪里注册通道。
如果你在联调过程中需要确认模型通道是否正常,可以回到模型对话页面做一次快速验证:
https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite需要管理多个 Key 或者查看调用记录时,控制台和 API Keys 页面是入口:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite接口字段和请求格式的细节,以接入文档为准:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite下一步建议你直接打开src/gateway/server-startup.ts,对照本文第 3 节的启动顺序表,把每个初始化步骤和日志输出对应起来。当你能看着日志说出“现在走到第几步、下一步该初始化什么”,Gateway 的调度逻辑就算真正读懂了。