閘道核心
職責
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)只是一個進程內的函式呼叫。
關鍵檔案
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。