网关核心
职责
startGatewayServer 是 OpenClaw 的常驻进程入口:绑定端口、加载插件、注册 RPC 方法、广播 agent 事件。所有渠道消息、ACP 请求、Web 客户端都连到这个网关,它再把请求转给 agent 主循环,把 agent 产生的事件广播回各客户端。
网关本身不做"思考"——它只负责接、转、播:接 JSON-RPC 请求,转给对应 handler(很多最终落到 agent),把 agent 事件播给订阅了该会话的 WebSocket 连接。
设计动机
为什么把网关单拎成一个常驻进程?因为 OpenClaw 要同时挂在 22 个渠道上,这些渠道大多需要长连接(WebSocket/polling)。如果每次消息都起一个短命进程,连接握手的开销和状态恢复的复杂度会压垮设计。常驻 gateway 让所有渠道连接、所有 agent session、所有插件状态都在一个进程里共享,跨层协调(比如渠道 A 的消息触发的 agent 调用工具,工具结果要投递到渠道 B)只是一个进程内的函数调用。
关键文件
openclaw.mjs— 根入口,Node 版本检查后转发到dist/entry.js。entry.ts main:79-131—main()→runMainOrRootHelp分发。run-main gateway hot path:159-225—gateway run直接动态 import。run-command .action:64-71— 解析选项后调runGatewayCommand。server.ts startGatewayServer:31-40— 薄包装,懒加载 server.impl。startGatewayServer 实现:551-580— 真正启动:清理旧插件 generation、引导网络运行时。coreGatewayHandlers:269— 核心 RPC handler 注册表。createRequestGatewayMethodRegistry:598-630— 每请求合并 core+plugin+extra handler。handleGatewayRequest:638-660— 授权并分发一个 JSON-RPC 请求。createAgentEventHandler:299-320— 把 agent 事件广播到 WebSocket 客户端。
数据流
startGatewayServer 的核心启动序列(server.impl.ts:551):
export async function startGatewayServer(
port = 18789,
opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
normalizeStateDirEnv(process.env);
// 清理上一轮重启遗留的旧插件 generation,保证两轮重启间状态干净
const installRecords = loadInstalledPluginIndexInstallRecordsSync();
const removedGenerations = await cleanupRetiredManagedNpmInstallGenerations({
activeInstallPaths: Object.values(installRecords).flatMap((record) =>
record.installPath ? [record.installPath] : [],
),
onError: (error, projectRoot) =>
log.warn(`failed to clean retained npm generation ${projectRoot}: ${String(error)}`),
});
const { bootstrapGatewayNetworkRuntime } = await import("./server-network-runtime.js");
bootstrapGatewayNetworkRuntime();注意默认端口 18789,以及启动第一件事是清理旧插件 npm generation——这是为 in-process restart 设计的:gateway 重启时要先回收上一轮留下的插件资源,避免泄漏。
请求分发(handleGatewayRequest:638)走 JSON-RPC:
/** Authorizes and dispatches one gateway JSON-RPC-style request. */
export async function handleGatewayRequest(
opts: GatewayRequestOptions & { extraHandlers?: GatewayRequestHandlers },
): Promise<void> {
const { req, respond, client, isWebchatConnect, context } = opts;
// 优先用调用方附带的 registry(它持有该方法时),否则从 live plugin registry 重建,
// 让启动快照之后注册的插件 RPC 方法仍可达 (#94127)handler registry 是每请求重建的(createRequestGatewayMethodRegistry:598):合并 core handler、当前激活插件 handler、调用方额外 handler。这种设计让插件可以热注册 RPC 方法,不必重启网关。
agent 事件广播(createAgentEventHandler:299)注入了三个原语:
export function createAgentEventHandler({
broadcast, // 广播到所有订阅该会话的连接
broadcastToConnIds, // 定向广播到指定连接
nodeSendToSession, // 跨节点投递到某 session
agentRunSeq,
chatRunState,
resolveSessionKeyForRun,
...broadcast 和 broadcastToConnIds 由 createGatewayNodeSessionRuntime 在 server.impl.ts:971 注入,贯穿整个网关生命周期。
边界与失败
- in-process restart:网关重启时先清理旧插件 npm generation(
src/gateway/server.impl.ts:560),若清理失败只打 warn 不阻断启动——保证服务能拉起比清理干净更重要。 - 插件热注册的 handler 可达性:
handleGatewayRequest在调用方快照不持方法时回退到 live registry 重建,修了 #94127——否则启动快照之后注册的插件 RPC 会"看不见"。 - 状态目录:启动先
normalizeStateDirEnv(process.env),确保$OPENCLAW_STATE_DIR解析正确,否则配置和 workspace 路径会错位。 - 网络运行时:
bootstrapGatewayNetworkRuntime()在加载网络层之前调用,顺序错了会导致 WebSocket 通道未就绪。
小结
网关只做接、转、播三件事,但每件都为"常驻 + 多渠道 + 可热插拔"做了设计:启动先清理、handler 每请求重建、广播原语注入。真正"想"的是 Agent 主循环,请求怎么授权分发看 RPC 方法表,事件怎么到客户端看 聊天广播。
对照官方资料:Gateway 文档 · README。