1. 从一次消息丢失说起:OpenClaw Gateway 到底在调度什么
如果你正在读 OpenClaw 的源码,大概率已经翻过src/gateway/这一层。我第一次跑通 OpenClaw 的时候,遇到一个很典型的现象:Telegram 上发出去的消息,Agent 明明回复了,但 Web UI 里看不到;反过来在 Web UI 里发消息,Telegram 那边又收不到。当时以为是 Channel 适配器写错了,后来把日志打到 Gateway 层才发现,问题出在会话分发上——两条消息被路由到了不同的 SessionKey,各自跑各自的 Lane,自然互相看不见。
这就是 OpenClaw Gateway 的核心价值:它不是简单的消息转发器,而是整个系统的控制平面(Control Plane)。用一句话概括,Gateway 负责决定一条消息该路由到哪个 Agent、如何排队、何时中断、状态如何同步;而真正收发消息、执行推理的是数据平面(Channel + Agent)。这个分离带来的好处是,Gateway 不关心消息的具体内容,只关心元信息——谁发的、从哪个通道来的、属于哪个会话。它因此可以专注做调度和管控,不被业务逻辑污染。
适合谁读这篇?如果你正在做多通道 AI Agent 接入、想理解一个生产级消息调度器怎么设计、或者单纯想给 OpenClaw 加一个新 Channel,那 Gateway 这层是绕不开的。本文会从源码结构出发,拆解消息路由、会话分发与调度链路,并给出可复制的 Gateway 配置片段和本地启动验证步骤,帮你完成一次端到端消息投递验证。全文围绕 OpenClaw Gateway 的消息调度与控制平面展开,涉及源码剖析、消息路由、会话分发、Lane Queue 等关键检索点。
先给一个全局认知:OpenClaw 的 Gateway 是四层结构——Transport 层管连接、Control 层管路由和排队、Integration 层做平台归一化、Intelligence 层挂 Agent 行为。四层之间通过明确定义的接口通信,每一层只做一件事。理解了这个分层,后面看源码就不会迷路。
2. TaoToken 前置:给 Gateway 接一个大模型后端
在动手拆 Gateway 之前,得先让 Agent 有模型可用。OpenClaw 本身不绑定模型供应商,它通过统一的 LLM 调用抽象对接后端。我实测下来,用 TaoToken 作为模型接入层比较省事,因为它同时提供 OpenAI 兼容接口和 Anthropic 兼容接口,OpenClaw 的pi-embedded-runner两种协议都能直接吃。
你需要准备三样东西:Base URL、API Key、Model ID。这三件套在后面的配置片段里会反复出现,先记牢。
Base URL 用https://taotoken.net/api,注意这个地址不带任何查询参数。API Key 到控制台创建,路径是https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=gateway_config。创建完复制出来,形如sk-开头的一串。Model ID 按你实际要用的填,比如claude-sonnet-4-20250514或gpt-4o这类,具体以模型对话页展示的为准。
如果你只是想先验证 Gateway 的调度链路能不能跑通,不想折腾真实模型,可以先用模型对话页手动发一条请求,确认 Key 和 Base URL 是通的:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=gateway_chat。这一步能排除掉 90% 的鉴权问题。
对于长期跑编码类 Agent 的场景,建议直接上 Coding Plan,额度更划算,接入方式完全一样:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=gateway_plan。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=gateway_doc,里面有各协议的完整参数说明。
这里要提醒一句:Gateway 本身不存 Key,Key 是配在 Agent 运行时的环境变量或配置文件里的。Gateway 只负责把消息路由到 Agent,Agent 拿着 Key 去调模型。所以你在排查「消息发出去了但没回复」这类问题时,要分清楚是 Gateway 路由断了,还是 Agent 调模型失败了。两者的日志位置完全不同。
3. 可复制配置:Gateway 启动参数与 Agent 后端设置
这一节给可直接复制的配置片段。OpenClaw 的 Gateway 配置分两块:一块是 Gateway 自身的监听与通道配置,一块是 Agent 运行时的模型后端配置。两块都要对,端到端才通。
先看 Gateway 的配置文件。OpenClaw 默认读~/.openclaw/gateway.toml,你也可以用--config指定路径。下面这份是我本地验证过的最小可用配置,包含 WebSocket 监听、一个 Telegram 通道、以及 Lane Queue 的并发参数:
# ~/.openclaw/gateway.toml [gateway] # WebSocket 控制平面监听地址 host = "127.0.0.1" port = 8787 # 认证挑战超时,单位毫秒 auth_timeout_ms = 30000 # 状态快照推送间隔 snapshot_interval_ms = 5000 [gateway.lanes] # 每个 SessionKey 的 Lane 最大排队长度,超出后拒绝新消息 max_queue_depth = 32 # 单条消息在 Lane 中的最长执行时间,超时后中断 task_timeout_ms = 120000 [channels.telegram] enabled = true # 从 BotFather 拿到的 token,建议用环境变量注入 bot_token = "${TELEGRAM_BOT_TOKEN}" # 允许的用户白名单,空数组表示不限制 allow_users = [] [channels.webui] enabled = true # Web UI 静态资源目录 static_dir = "./webui/dist"注意bot_token用了${TELEGRAM_BOT_TOKEN}占位,OpenClaw 启动时会从环境变量读取。这样避免把敏感信息写进配置文件。启动前先导出:
export TELEGRAM_BOT_TOKEN="你的bot token"再看 Agent 运行时的模型后端配置。OpenClaw 的 Agent 配置默认在~/.openclaw/agent.json,这里就是三件套落地的地方:
{ "agent": { "provider": "openai-compatible", "base_url": "https://taotoken.net/api", "api_key": "${TAOTOKEN_API_KEY}", "model_id": "claude-sonnet-4-20250514", "max_tokens": 4096, "temperature": 0.7 }, "memory": { "enabled": true, "short_term_turns": 20, "long_term_store": "./data/memory" }, "skills": { "dir": "./skills", "auto_load": true } }同样用环境变量注入 Key:
export TAOTOKEN_API_KEY="sk-你的key"如果你用的是 Anthropic 原生协议而不是 OpenAI 兼容协议,把provider改成anthropic,base_url保持https://taotoken.net/api不变,OpenClaw 会自动走 Anthropic 的 messages 接口。这一点在pi-embedded-runner.ts里有分支判断,源码里搜provider === 'anthropic'就能看到。
配置写完后,启动 Gateway:
openclaw gateway start --config ~/.openclaw/gateway.toml正常启动会看到类似输出:
[gateway] WebSocket server listening on 127.0.0.1:8787 [gateway] Loaded 2 channels: telegram, webui [gateway] Lane queue initialized, max_queue_depth=32 [gateway] Agent runtime ready, provider=openai-compatible如果卡在Agent runtime ready之前,多半是模型后端配置有问题,先回去检查三件套。如果卡在Loaded channels之前,那是通道配置的问题,跟模型无关。
4. 验证请求:一次端到端消息投递的完整链路
配置就绪后,我们要验证一条消息从进入到 Agent 回复的完整链路。这一步是理解 Gateway 调度器最直观的方式。我建议用 WebSocket 客户端手动发一条chat.send,观察事件流,比直接看日志清楚得多。
先装一个轻量的 WebSocket 客户端工具,比如wscat:
npm install -g wscat连接 Gateway:
wscat -c ws://127.0.0.1:8787连上后,Gateway 会立刻推一个connect.challenge事件,带一个随机 nonce:
{"event":"connect.challenge","data":{"nonce":"a3f8...","timestamp":1715040000000}}你需要用这个 nonce 做一次认证。本地开发环境如果没开严格鉴权,可以直接发一个简单的 auth 消息:
{"method":"auth","data":{"token":"local-dev-token"}}认证成功后,Gateway 回hello-ok,里面带完整状态快照:
{"event":"hello-ok","data":{"presence":{"status":"online"},"health":{"uptime":12,"channels":2},"state":{"sessions":0,"activeRuns":0}}}看到hello-ok就说明 Transport 层和 Control 层都通了。接下来发一条真实消息:
{"method":"chat.send","data":{"channelId":"webui","userId":"tester","messageText":"你好,帮我算一下 23 乘以 47"}}发送后,你会依次收到几类事件。先是agent.event,type 为text,内容是流式输出的 token:
{"event":"agent.event","data":{"sessionId":"mybot:webui:tester","type":"text","content":"23","done":false}} {"event":"agent.event","data":{"sessionId":"mybot:webui:tester","type":"text","content":" 乘以","done":false}}最后是agent.done:
{"event":"agent.done","data":{"sessionId":"mybot:webui:tester","done":true}}如果你在 Web UI 和 wscat 里同时订阅了同一个 SessionKey,两边会同时收到这些事件。这就是 Gateway「唯一事实来源」的威力——所有订阅该 Session 的客户端看到的是同一份流式输出。
验证过程中,重点观察sessionId字段。它的格式是workspace:channel:userId,本例是mybot:webui:tester。这个 Key 决定了消息进哪个 Lane。你可以再发一条channelId为telegram的消息,会看到sessionId变成mybot:telegram:tester,两条消息进了不同的 Lane,互不阻塞。
如果想验证 Lane 的串行化,可以快速连发两条消息,观察第二条的agent.event是否在第一条agent.done之后才出现。正常情况下是的,因为同一个 SessionKey 的 Lane 是 Promise 链串行的。这个行为在command-queue.ts里实现,核心就是existing.then(() => task())这一句。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
这一节对照真实报错,把 Gateway 接入和验证过程中最容易踩的坑列出来。每个报错都给出定位思路和修复方式。
报错一:401 Unauthorized,日志里出现auth failed
这个通常不是 Gateway 的问题,而是 Agent 调模型时鉴权失败。先确认TAOTOKEN_API_KEY环境变量在当前 shell 里真的存在:
echo $TAOTOKEN_API_KEY如果输出为空,说明没导出或者导出在了另一个终端。重新export后重启 Gateway。如果 Key 存在但还是 401,检查 Base URL 是不是写成了带路径的形式,比如https://taotoken.net/api/v1。OpenClaw 的 OpenAI 兼容适配器会自己拼/v1/chat/completions,你只需要给到https://taotoken.net/api就行,多写路径会导致 404 或 401。
报错二:local proxy failed或connection refused
这个报错一般出现在 Gateway 启动阶段,说明它尝试连接某个本地服务失败。常见原因是端口被占用。检查 8787 端口:
lsof -i :8787如果被占用,改gateway.toml里的port换一个,比如 8788。另一个原因是 Web UI 的static_dir路径不存在,Gateway 在挂载静态资源时会报local proxy failed。确认./webui/dist目录真实存在,或者先把channels.webui.enabled设为false排除干扰。
报错三:reading choices或cannot read property 'choices' of undefined
这个报错来自 Agent 解析模型响应时。choices是 OpenAI 兼容接口返回结构里的字段,如果模型后端返回的不是标准结构,就会读不到。排查两步:第一,确认provider和base_url匹配,OpenAI 兼容协议配openai-compatible,Anthropic 协议配anthropic,配错了响应结构对不上;第二,确认model_id是后端真实支持的模型,填了一个不存在的模型名,有些后端会返回错误结构而不是标准 choices。
报错四:OAuth 相关报错,比如oauth token expired
如果你用的是需要 OAuth 的通道(比如某些企业协作平台),token 过期会报这个。Gateway 本身不管理 OAuth 刷新,刷新逻辑在对应的 Channel Adapter 里。检查src/channels/plugins/<平台>/adapter.ts里的 refresh 逻辑,或者直接重新走一遍授权流程。本地开发阶段,建议先用 Telegram 或 Web UI 这类 token 鉴权的通道,避开 OAuth 复杂度。
报错五:消息发出去了,hello-ok也收到了,但没有任何agent.event
这种情况说明 Gateway 路由正常,但 Agent 没被触发。检查chat.send的data里channelId和userId是否都填了,缺一个就构造不出 SessionKey,消息会被丢弃。另外确认 Agent 配置里的provider不是空字符串。如果都正常,把 Gateway 日志级别调到 debug,看command-queue.ts有没有打印 enqueue 日志。没有 enqueue 日志,说明消息在 Control 层就被拦了,通常是 SessionKey 解析失败。
6. 继续深入:从 Gateway 到 Agent Loop 的下一步
把 Gateway 这层跑通之后,你对 OpenClaw 的消息调度链路应该有了实感。回顾一下核心:Transport 层用挑战-响应做认证,Control 层用workspace:channel:userId构造 SessionKey 并路由到对应 Lane,Integration 层把各平台消息归一化成 UnifiedMessage,Intelligence 层挂载 Skills、Memory 和 Heartbeat。四层各司其职,Gateway 作为控制平面串联一切。
源码阅读建议按这个顺序:先看server.ts理解启动流程,再看server-ws.ts理解连接和认证,然后sessions-resolve.ts理解 SessionKey,接着command-queue.ts理解 Lane 串行化,最后server-chat.ts把消息从接收到执行的完整链路串起来。这个顺序遵循从外到内、从简到繁的原则。
如果你在验证过程中想换模型或者对比不同后端的行为,可以直接在模型对话页手动发请求做对照:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=gateway_verify。需要新建 Key 或者查看额度,去控制台:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=gateway_keys。接入参数的完整说明在文档里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=gateway_doc_full。
下一篇会进入 Agent Loop,拆解上下文组装、工具调用协议、沙箱隔离和循环终止条件。那是 OpenClaw 真正「干活」的地方,也是和 Pi 框架深度集成的关键。Gateway 保证了消息在正确的时间、正确的上下文、正确的隔离边界内到达 Agent,而 Agent Loop 决定了 Agent 拿到消息后怎么思考和行动。两层配合起来,才是完整的 OpenClaw。