乐于分享
好东西不私藏

OpenClaw源码解读系列:Gateway核心

OpenClaw源码解读系列:Gateway核心

今天继续OpenClaw源码解读——Gateway核心Gateway是整个OpenClaw项目的重中之重,所以今天的篇幅也会长一些。

  • 使用的分支是main

  • commit版本是:f5160ca6becaeeb6a4dfd892fffd2130a696f766

讲解计划如下:

1. CLI 框架与进程模型

2. 配置系统

3. Gateway 核心(今日讲解)

4. 通道与路由

5. Agent 引擎

6. 自动回复管线

7. 插件系统

8. 记忆系统

9. Web 控制台

10. 原生客户端

11. 浏览器自动化

12. 运维与测试

概述

Gateway 是 OpenClaw 的神经中枢——一个运行在本地的单进程服务器,通过一个端口同时提供 HTTP 和 WebSocket 服务。所有客户端(CLI、Web 控制台、macOS 菜单栏 App、iOS/Android 客户端、通道节点)都通过 WebSocket 连接到 Gateway,以 JSON-RPC 风格的帧进行通信。HTTP 端口同时承载 Control UI 静态文件、Webhook 入口、OpenAI 兼容 API、Canvas Host 等功能。

本文按照 Gateway 从启动到运行的真实代码路径,逐层拆解其架构。

一、启动流程:从 CLI 命令到运行循环

当用户执行 `openclaw gateway run –port 18789` 时,Commander 解析参数后调用 `src/cli/gateway-cli/run.ts` 中的 `runGatewayCommand`。它做了三件事:校验配置、解析认证、启动运行循环。

真正的精华在运行循环 `runGatewayLoop`(`src/cli/gateway-cli/run-loop.ts`)。这个函数只有 128 行,但设计非常精巧:

export async function runGatewayLoop(params: {  start: () => Promise<Awaited<ReturnType<typeof startGatewayServer>>>;  runtime: typeof defaultRuntime;}) {  const lock = await acquireGatewayLock();  let serverAwaited<ReturnType<typeof startGatewayServer>> | null = null;  let shuttingDown = false;  let restartResolver: (() => void) | null = null;  // ... 信号处理逻辑 ...  try {    while (true) {      server = await params.start();      await new Promise<void>((resolve) => {        restartResolver = resolve;      });    }  } finally {    await lock?.release();  }}

这里有一个巧妙的无限循环 + Promise 挂起模式。每次 `params.start()` 启动一个 Gateway 服务器实例后,循环立刻用一个 `new Promise` 挂起自己。这个 Promise 的 `resolve` 函数被存到 `restartResolver`。循环不会继续——直到外部调用 `restartResolver()` 将它唤醒。

谁来唤醒?答案是信号处理器。`runGatewayLoop` 注册了三个 POSIX 信号监听:

  • SIGTERM / SIGINT:触发 `request(“stop”, …)`,关闭服务器后 `process.exit(0)` 退出

  • SIGUSR1:触发 `request(“restart”, …)`,关闭服务器后调用 `restartResolver()`,唤醒循环,重新执行 `params.start()`

这意味着 Gateway 支持进程内热重启——不需要外部进程监控器(如 pm2 或 systemd restart),发一个 `SIGUSR1` 就能让服务器在同一个进程内重新初始化。重启前还会等待进行中的 Agent 会话执行完毕(drain),避免丢消息:

if (isRestart) {  const activeTasks = getActiveTaskCount();  if (activeTasks > 0) {    gatewayLog.info(      `draining ${activeTasks} active task(s) before restart (timeout ${DRAIN_TIMEOUT_MS}ms)`,    );    const { drained } = await waitForActiveTasks(DRAIN_TIMEOUT_MS);  }}await server?.close({ reason"gateway restarting"restartExpectedMs1500 });

30 秒 drain 超时 + 5 秒关闭超时,再加一个强制退出的 `setTimeout` 兜底,确保进程不会永远挂住。

二、实例锁:防止多 Gateway 同时运行

在运行循环开始的第一行就是 `acquireGatewayLock()`。OpenClaw 用双重锁机制确保同一配置只有一个 Gateway 实例在运行。

### 文件锁

实现在 `src/infra/gateway-lock.ts`。锁路径是对配置文件路径取 SHA1 后生成的:

function resolveGatewayLockPath(env: NodeJS.ProcessEnv) {  const stateDir = resolveStateDir(env);  const configPath = resolveConfigPath(env, stateDir);  const hash = createHash("sha1").update(configPath).digest("hex").slice(08);  const lockDir = resolveGatewayLockDir();  const lockPath = path.join(lockDir, `gateway.${hash}.lock`);  return { lockPath, configPath };}

这样不同配置文件(比如不同 `–profile`)不会互相冲突,各自有独立的锁文件。

获取锁的核心逻辑用的是 `fs.open(lockPath, “wx”)`——`wx` 标志表示独占创建,如果文件已存在就抛 `EEXIST`。这是操作系统级的原子操作,不会出现竞态条件。

锁文件的内容是一个 JSON payload:

{  "pid": 12345,  "createdAt": "2026-02-07T10:00:00.000Z",  "configPath": "/Users/me/.openclaw/openclaw.json",  "startTime": 98765432}

当检测到锁文件已存在时,系统需要判断持有锁的进程是否还活着。这里有一套多层验活策略

1. `process.kill(pid, 0)` 试探——signal 0 不发送实际信号,只检查进程是否存在

2. 在 Linux 上,还会读 `/proc/<pid>/stat` 获取进程启动时间(`startTime` 字段),与锁文件中记录的对比。这是为了处理 PID 重用的问题——操作系统可能把一个已死进程的 PID 分配给新进程

3. 如果无法确认进程状态,则检查锁文件的 mtime 是否超过 30 秒阈值(`DEFAULT_STALE_MS`),超时即视为陈旧锁

如果判定持有者已死,删除锁文件并重新尝试。如果超过 5 秒(`DEFAULT_TIMEOUT_MS`)仍然无法获取锁,抛出 `GatewayLockError`。

端口锁

第二重锁是 HTTP 服务器绑定端口。在 `src/gateway/server/http-listen.ts` 中,如果 `httpServer.listen` 因为 `EADDRINUSE` 失败,同样会抛出 `GatewayLockError`。这是操作系统层面的天然互斥——同一个端口只能被一个进程占用。

两重锁互补:文件锁是主动的(能提前检测),端口锁是被动的(最后兜底)。

三、服务器初始化:`startGatewayServer`

`src/gateway/server.impl.ts` 中的 `startGatewayServer` 是整个 Gateway 的初始化入口,663 行代码(含关闭逻辑)。它是一个编排函数——自身不实现具体功能,而是按顺序调用各子系统的创建函数,把它们组装在一起。

按执行顺序,初始化分为以下几个阶段。

阶段 1:配置校验与迁移

let configSnapshot = await readConfigFileSnapshot();if (configSnapshot.legacyIssues.length > 0) {  const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed);  await writeConfigFile(migrated);}configSnapshot = await readConfigFileSnapshot();if (configSnapshot.exists && !configSnapshot.valid) {  throw new Error(`Invalid config at ${configSnapshot.path}.\n${issues}\n...`);}

先读配置快照,如果有历史遗留问题就自动迁移;迁移后再重新读取一次并校验。校验不通过直接报错退出,不会带着坏配置启动。

阶段 2:插件与通道注册

const { pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayPlugins({  cfg: cfgAtStart,  workspaceDir: defaultWorkspaceDir,  coreGatewayHandlers,  baseMethods,});const channelMethods = listChannelPlugins().flatMap((p) => p.gatewayMethods ?? []);const gatewayMethods = Array.from(new Set([...baseGatewayMethods, ...channelMethods]));

加载所有启用的插件,收集它们注册的额外 Gateway 方法。通道插件也可以注册自己的 RPC 方法,最终合并到一个去重的方法列表里。这个列表会在 WebSocket 握手时发给客户端,让客户端知道服务器支持哪些方法。

阶段 3:运行时状态创建

const { httpServer, wss, clients, broadcast, ... } = await createGatewayRuntimeState({  cfg: cfgAtStart,  bindHost,  port,  resolvedAuth,  // ... 十几个参数});

`createGatewayRuntimeState`(`src/gateway/server-runtime-state.ts`)是 Gateway 的”基础设施工厂”,它创建了:

1. Canvas Host(可选):用于 AI 画布 (A2UI) 的本地 HTTP 服务

2. HTTP 服务器:Node.js 原生 `http.createServer`(或 `https`),不使用 Express/Hono 等框架

3. WebSocket 服务器:`ws` 库的 `WebSocketServer`,`noServer: true` 模式——不自己监听端口,而是通过 HTTP 服务器的 `upgrade` 事件接入

4. 客户端注册表:`Set<GatewayWsClient>`,维护所有活跃的 WebSocket 连接

5. 广播器:`createGatewayBroadcaster` 创建的 `broadcast` 和 `broadcastToConnIds` 函数,用于向所有或特定客户端推送事件

6. Chat 运行状态:管理进行中的 Agent 对话、流式缓冲、abort 控制器等

WebSocket 使用 `noServer` 模式是一个关键设计选择。HTTP 和 WebSocket 复用同一个端口,通过 `upgrade` 事件来区分:

const wss new WebSocketServer({ noServertruemaxPayload: MAX_PAYLOAD_BYTES });for(const server of httpServers) {  attachGatewayUpgradeHandler({ httpServer: server, wss, canvasHost, clients, resolvedAuth });}

`attachGatewayUpgradeHandler`(`src/gateway/server-http.ts`)监听 HTTP 服务器的 `upgrade` 事件。如果请求路径是 Canvas WebSocket 路径,转给 Canvas Host 处理;否则交给主 WebSocket 服务器:

httpServer.on("upgrade"(req, socket, head) => {  if (canvasHost && url.pathname === CANVAS_WS_PATH) {    // Canvas WebSocket 鉴权 + 转发    canvasHost.handleUpgrade(req, socket, head);    return;  }  wss.handleUpgrade(req, socket, head, (ws) => {    wss.emit("connection", ws, req);  });});

阶段 4:连接各子系统

创建完基础设施后,`startGatewayServer` 继续初始化上层子系统:

  • NodeRegistry:追踪远程节点(如 iOS/Android 客户端)的连接、命令调用、事件订阅

  • Discovery**:mDNS/Bonjour 广播 + 广域发现,让局域网内的客户端能自动找到 Gateway

  • ChannelManager:管理消息通道(Telegram、Discord 等)的启动/停止/状态快照

  • CronService:定时任务调度

  • HeartbeatRunner:心跳发送器

  • ExecApprovalManager:工具执行审批流

  • ConfigReloader:配置文件热重载——监视文件变化,安全的改动走热更新,关键改动触发 SIGUSR1 重启

最后把所有东西组装起来,调用 `attachGatewayWsHandlers` 把 WebSocket 消息处理逻辑挂载上去,构建一个包含所有子系统引用的 `context` 对象传进去。这个 context 就是 Gateway 的”依赖注入容器”——任何 RPC handler 都可以通过它访问所需的子系统。

四、HTTP 请求路由:链式分发

`createGatewayHttpServer`(`src/gateway/server-http.ts`)创建的 HTTP 服务器没有使用任何 Web 框架,而是用一个 `handleRequest` 函数做链式分发

async function handleRequest(req: IncomingMessage, res: ServerResponse) {  if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket"return;  if (await handleHooksRequest(req, res)) return;  if (await handleToolsInvokeHttpRequest(req, res, { auth, trustedProxies })) return;  if (await handleSlackHttpRequest(req, res)) return;  if (handlePluginRequest && ...) return;  if (openResponsesEnabled && await handleOpenResponsesHttpRequest(...)) return;  if (openAiChatCompletionsEnabled && await handleOpenAiHttpRequest(...)) return;  if (canvasHost && ...) return;  if (controlUiEnabled && ...) return;  res.statusCode = 404;  res.end("Not Found");}

每个 handler 返回 `true` 表示”我处理了这个请求”,返回 `false` 表示”跳过,交给下一个”。这种责任链模式非常清晰——想增加新的 HTTP 端点,只需在链上合适的位置插入一个新 handler。

请求路径匹配的优先级从上到下依次是:Webhook 入口 → 工具调用 API → Slack HTTP → 插件路由 → OpenResponses API → OpenAI 兼容 API → Canvas Host → Control UI → 404。

值得注意的是,Channel 的 HTTP 端点(`/api/channels/…`)走的是 plugin handler 路径,并且会做 Gateway 级别的鉴权——从 `Authorization: Bearer` 头提取 token,调用 `authorizeGatewayConnect` 验证。这确保了即使插件忘记做鉴权,敏感的通道操作也不会被未授权访问。

五、WebSocket 连接生命周期

当客户端发起 WebSocket 连接后,`attachGatewayWsConnectionHandler`(`src/gateway/server/ws-connection.ts`)接管。每个连接的生命周期如下。

连接建立

服务器为每个连接生成一个唯一的 `connId`(UUID),创建一个 `GatewayWsClient` 对象,记录远程地址、User-Agent 等元信息。此时连接处于 `pending` 状态——还没通过认证。

wss.on("connection"(socket, upgradeReq) => {  const connId = randomUUID();  let handshakeState"pending" | "connected" | "failed" = "pending";  // ...});

握手超时

服务器设置一个超时计时器(可配置,默认值来自 `getHandshakeTimeoutMs()`)。如果客户端在超时窗口内没有发送 `connect` 帧完成认证,连接会被强制关闭。这防止了恶意连接占用资源。

认证握手

客户端需要发送一个 `connect` 类型的帧,包含认证凭据。消息处理逻辑在 `attachGatewayWsMessageHandler`(超过 1000 行的大文件)中,它解析 JSON 帧,验证帧格式(必须有 `type`、`id`、`method`),然后对 `connect` 方法做特殊处理——调用 `authorizeGatewayConnect` 进行认证。

消息处理

认证通过后,`handshakeState` 变为 `connected`。后续消息按帧类型路由:

  • `req` 帧(请求):调用 `handleGatewayRequest` 分发到对应的 handler

  • `res` 帧(响应):通常是节点对服务器发起的命令调用的回复

  • `event` 帧(事件):节点主动推送的事件

六、认证系统

认证逻辑集中在 `src/gateway/auth.ts`,支持三种模式。

Token 模式(默认)

服务器生成或配置一个 token,客户端在 `connect` 帧中携带相同的 token。对比使用 `safeEqualSecret`——一个**常量时间比较**函数,防止计时攻击:

if (auth.mode === "token") {  if (!safeEqualSecret(connectAuth.token, auth.token)) {    return { ok: false, reason: "token_mismatch" };  }  return { ok: true, method: "token" };}

Password 模式

类似 token,但语义是”人类记忆的密码”,同样用常量时间比较。

Tailscale 模式

当 Gateway 通过 Tailscale Serve 暴露时,Tailscale 代理会在请求头中注入 `Tailscale-User-Login`、`Tailscale-User-Name` 等身份信息。Gateway 的验证流程分三步:

1. 检查请求是否确实来自 Tailscale 代理(`isTailscaleProxyRequest`:loopback 地址 + forwarded 头)

2. 从 `X-Forwarded-For` 提取客户端 IP

3. 调用 `tailscale whois <ip>` 获取认证身份,与请求头中的 login 做规范化比较(全部转小写后对比)

这种双重验证确保了即使有人伪造 Tailscale 头,只要不是从 Tailscale 代理过来的请求,都不会通过认证。

还有一个关键概念:本地直接请求(`isLocalDirectRequest`)。如果请求来自 loopback 地址,Host 是 `localhost`/`127.0.0.1`/`::1`,且没有 forwarded 头,则被认为是本地进程发起的连接。这在某些内部通信场景中有特殊的信任逻辑。

七、RPC 分发与权限控制

通过认证后的 WebSocket 请求帧最终进入 `handleGatewayRequest`(`src/gateway/server-methods.ts`)。这个函数只做两件事:鉴权分发

方法鉴权

`authorizeGatewayMethod` 实现了一套**基于角色 + 作用域**的权限模型。每个连接有一个角色(`operator` 或 `node`)和一组作用域(scope)。

角色决定了可访问的方法集合。`node` 角色(远程设备节点)只能调用 `node.invoke.result`、`node.event`、`skills.bins` 这三个方法。`operator` 角色的权限则由 scope 细分:

  • `operator.admin`:所有方法,包括 config 修改、插件管理、会话删除

  • `operator.read`:`health`、`channels.status`、`sessions.list` 等只读方法

  • `operator.write`:`send`、`agent`、`chat.send` 等写操作

  • `operator.approvals`:工具执行审批相关

  • `operator.pairing`:设备配对相关

admin scope 是超集——拥有它就可以调用任何方法。对于不在任何显式列表中的方法,默认需要 admin scope。这遵循了**最小权限原则**。

方法注册与分发

所有核心 handler 通过展开合并注册到 `coreGatewayHandlers` 对象中:

export const coreGatewayHandlersGatewayRequestHandlers = {  ...connectHandlers,  ...healthHandlers,  ...channelsHandlers,  ...chatHandlers,  ...agentHandlers,  ...sendHandlers,  // ... 20+ 个 handler 模块};

分发逻辑非常直接——用方法名作为 key 查找 handler:

const handler = opts.extraHandlers?.[req.method] ?? coreGatewayHandlers[req.method];if (!handler) {  respond(falseundefinederrorShape(ErrorCodes.INVALID_REQUEST`unknown method: ${req.method}`));  return;}await handler({ req, params, client, respond, context });

注意这里先查 `extraHandlers`,再查 `coreGatewayHandlers`。`extraHandlers` 包含插件注册的和 Exec Approval 的 handler,这意味着插件可以覆盖核心方法。

每个 handler 模块(如 `server-methods/health.ts`、`server-methods/agent.ts`)是一个独立文件,导出一个 `{ [method: string]: HandlerFn }` 对象。这种按领域拆分的方式让 handler 代码保持在合理的规模内,每个文件专注一个功能域。

八、配置热重载

Gateway 运行过程中,用户可能通过 `openclaw config set` 或直接编辑配置文件来修改配置。`startGatewayConfigReloader`(`src/gateway/config-reload.ts`)负责处理这种情况。

它监视配置文件的变化,检测到修改后读取新快照,与当前配置做 diff。根据变化的严重程度,采取不同策略:

  • 安全变更(hooks 配置、heartbeat 间隔、cron 列表等):走热更新路径,不中断服务,直接更新内存中的配置和相关子系统

  • 关键变更(端口、绑定地址、认证方式、TLS 等):触发 SIGUSR1 重启,通过前面讲的运行循环实现进程内重启

这种分级策略在运行稳定性和配置即时生效之间取得了很好的平衡。

九、优雅关闭

`createGatewayCloseHandler`(`src/gateway/server-close.ts`)创建关闭函数时接收了所有子系统的引用。关闭时按相反顺序逐一清理:

1. 触发 `gateway_stop` 插件钩子

2. 停止配置监视器

3. 关闭浏览器控制服务

4. 关闭所有 WebSocket 客户端连接(发送带 `restartExpectedMs` 的关闭帧,让客户端知道这是重启而非永久关闭)

5. 停止所有消息通道

6. 停止 cron、heartbeat、维护定时器

7. 取消事件订阅

8. 停止 Tailscale Serve 暴露

9. 停止 mDNS/Bonjour 广播

10. 关闭 HTTP 服务器

每一步都有错误处理——单个子系统关闭失败不会阻止其他子系统的清理。

十、架构要点回顾

  1. 单端口复用:HTTP 和 WebSocket 共用一个端口,通过 `upgrade` 事件区分,简化了网络配置。

  2. 双重锁机制:文件锁(主动检测)+ 端口锁(被动兜底),确保单实例运行。

  3. 进程内热重启:SIGUSR1 信号触发,drain 进行中的任务后重建服务器,无需外部监控器。

  4. 链式 HTTP 路由:每个 handler 返回 bool 表示是否处理,简单且可扩展。

  5. 角色 + 作用域鉴权:operator/node 角色 × admin/read/write/approvals/pairing 作用域,细粒度控制方法访问。

  6. 编排式初始化:`startGatewayServer` 不实现具体逻辑,只负责按顺序创建和组装子系统,每个子系统独立可测。

  7. 配置变更分级:安全改动热更新,关键改动触发重启,保证稳定性。

掌握了这些核心机制,你就理解了 OpenClaw 所有网络通信、客户端管理和子系统协调的基础。后续的通道系统、Agent 引擎、记忆系统等,都是挂载在这个 Gateway 骨架上运行的。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » OpenClaw源码解读系列:Gateway核心

评论 抢沙发

7 + 1 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮