Skip to content

閘道核心

源码版本v2026.6.11

職責

startGatewayServer 是 OpenClaw 的常駐進程入口:綁定連接埠、載入外掛、註冊 RPC 方法、廣播 agent 事件。所有通道訊息、ACP 請求、Web 用戶端都連到這個閘道,它再把請求轉給 agent 主循環,把 agent 產生的事件廣播回各用戶端。

閘道本身不做「思考」——它只負責接、轉、播:接 JSON-RPC 請求,轉給對應 handler(很多最終落到 agent),把 agent 事件播給訂閱了該工作階段 (session) 的 WebSocket 連線。

設計動機

為什麼把閘道單獨拉成一個常駐進程?因為 OpenClaw 要同時掛在 22 個通道上,這些通道大多需要長連線(WebSocket/polling)。如果每次訊息都起一個短命進程,連線交握的開銷和狀態恢復的複雜度會壓垮設計。常駐 gateway 讓所有通道連線、所有 agent session、所有外掛狀態都在一個進程裡共享,跨層協調(比如通道 A 的訊息觸發的 agent 呼叫工具,工具結果要投遞到通道 B)只是一個進程內的函式呼叫。

關鍵檔案

資料流

startGatewayServer 的核心啟動序列(server.impl.ts:551):

typescript
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:

typescript
/** 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)注入了三個原語:

typescript
export function createAgentEventHandler({
  broadcast,              // 廣播到所有訂閱該工作階段的連線
  broadcastToConnIds,     // 定向廣播到指定連線
  nodeSendToSession,      // 跨節點投遞到某 session
  agentRunSeq,
  chatRunState,
  resolveSessionKeyForRun,
  ...

broadcastbroadcastToConnIdscreateGatewayNodeSessionRuntimeserver.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