返回

OpenClaw 源码解析(一):Gateway——系统的心脏

深入理解 OpenClaw 的控制中心——Gateway。从一次启动开始,揭示它如何协调 Session、Agent、Channel 等子系统,以及为什么选择单进程架构。

适用版本: OpenClaw v2026.3 阅读时间: 约 25 分钟 前置知识: TypeScript 基础、WebSocket 概念 源码位置: src/gateway/, openclaw.mjs

开篇:一条消息的旅程

想象这样一个场景:

你在 WhatsApp 上给 OpenClaw 发送了"今天天气怎么样?" 两秒后,它回复了你所在城市的实时天气。

这两秒发生了什么?消息是如何从 WhatsApp 到达 AI,又如何返回的?

答案的核心是 Gateway——OpenClaw 的心脏。它不像心脏那样泵血,而是泵消息泵状态泵控制指令

本文将回答一个核心问题:

为什么 Gateway 是 OpenClaw 的控制中心?它是如何协调各个子系统的?

Gateway 是什么?不是什么?

在深入代码之前,先建立正确的认知模型。

Gateway 不是…

不是 HTTP 服务器——虽然它有 HTTP 端点,但这只是它的一小部分功能

不是消息代理——它不负责存储和转发消息,那是 Session Manager 的工作

不是 Agent 容器——Agent 有自己独立的运行时

Gateway 是…

控制平面 (Control Plane)——它协调、调度、监控所有子系统

单一入口——所有外部请求(WebSocket、HTTP、Webhook)都通过它

状态机管理者——它维护系统状态,驱动状态转换

为什么需要控制平面?

假设没有 Gateway,OpenClaw 的架构会是怎样的:

┌─────────┐     ┌─────────┐     ┌─────────┐
│WhatsApp │────▶│ Agent   │────▶│ Session │
└─────────┘     └─────────┘     └─────────┘
     │               │               │
     ▼               ▼               ▼
┌─────────┐     ┌─────────┐     ┌─────────┐
│Telegram │────▶│ Auth    │────▶│ Config  │
└─────────┘     └─────────┘     └─────────┘

问题:

  • 每个模块都需要知道其他模块的存在(N² 复杂度)
  • 认证逻辑散落各处
  • 状态同步困难
  • 难以监控和调试

有了 Gateway:

flowchart TB
    subgraph Clients["客户端层"]
        WA[WhatsApp]
        TG[Telegram]
        SL[Slack]
        CLI[CLI]
        UI[Web UI]
    end

    subgraph Gateway["Gateway 控制平面"]
        WS[WebSocket<br/>:18789]
        HTTP[HTTP Endpoints<br/>/v1/*, /hooks/*]
        AUTH[Auth Manager]
        SM[Session Manager]
        CRON[Cron Scheduler]
    end

    subgraph Workers["工作节点层"]
        AGENT[Agent Runtime]
        CHANNEL[Channel Plugins]
        PLUGIN[Plugin Services]
    end

    subgraph Storage["存储层"]
        SESSION[(Session Store)]
        CONFIG[(Config Files)]
        SECRETS[(Secrets)]
    end

    Clients --> WS
    Clients --> HTTP

    WS --> AUTH
    HTTP --> AUTH
    AUTH --> SM
    SM --> AGENT
    SM --> CHANNEL
    CRON --> AGENT
    PLUGIN --> AGENT

    AGENT --> SESSION
    SM --> CONFIG
    AUTH --> SECRETS

收益:

  • 统一认证入口
  • 集中式监控
  • 清晰的职责边界
  • 单点控制(如优雅关闭)

Gateway 内部架构

flowchart LR
    subgraph GW["Gateway Server"]
        direction TB

        subgraph Network["网络层"]
            WSS[WebSocket Server]
            HTTPS[HTTP Server]
        end

        subgraph Core["核心层"]
            ROUTER[Request Router]
            AUTHZ[Authz Handler]
            METHOD[Method Registry]
        end

        subgraph Runtime["运行时层"]
            CLIENTS[Clients Set]
            RUNSEQ[Agent Run Seq]
            DEDUPE[Dedupe Map]
            ABORT[Abort Controllers]
        end

        subgraph Sidecars["Sidecars"]
            CHANNELS[Channel Manager]
            CRON2[Cron Service]
            GMAIL[Gmail Watcher]
            BROWSER[Browser Control]
        end
    end

    Network --> Core
    Core --> Runtime
    Runtime --> Sidecars

启动:从 openclaw gateway start 到服务就绪

让我们追踪 Gateway 的启动过程。不是为了记住每一步,而是理解它为什么这样设计

启动时序图

sequenceDiagram
    autonumber
    participant CLI as CLI
    participant Main as openclaw.mjs
    participant Config as Config Loader
    participant Secrets as Secrets Manager
    participant Auth as Auth Bootstrap
    participant Plugin as Plugin Loader
    participant GW as Gateway Server
    participant Sidecar as Sidecars

    CLI->>Main: gateway start

    Note over Main: 阶段1: 环境检查
    Main->>Main: ensureSupportedNodeVersion()
    Main->>Main: module.enableCompileCache()

    Note over Main,Config: 阶段2: 配置加载
    Main->>Config: readConfigFileSnapshot()
    Config-->>Main: ConfigSnapshot
    Main->>Config: migrateLegacyConfig()
    Main->>Config: applyPluginAutoEnable()

    Note over Main,Secrets: 阶段3: 密钥激活
    Main->>Secrets: prepareSecretsRuntimeSnapshot()
    Secrets-->>Main: SecretsSnapshot
    Main->>Secrets: activateSecretsRuntimeSnapshot()

    Note over Main,Auth: 阶段4: 认证引导
    Main->>Auth: ensureGatewayStartupAuth()
    Auth-->>Main: AuthBootstrap

    Note over Main,Plugin: 阶段5: 插件加载
    Main->>Plugin: initSubagentRegistry()
    Main->>Plugin: loadGatewayPlugins()
    Plugin-->>Main: PluginRegistry + Methods

    Note over Main,GW: 阶段6: 服务器创建
    Main->>GW: createGatewayRuntimeState()
    GW->>GW: HTTP Server (port 18789)
    GW->>GW: WebSocket Server
    GW->>GW: Canvas Host

    Note over Main,Sidecar: 阶段7: Sidecars 启动
    Main->>Sidecar: startGatewaySidecars()
    Sidecar->>Sidecar: cleanStaleLockFiles()
    Sidecar->>Sidecar: Browser Control Server
    Sidecar->>Sidecar: Gmail Watcher
    Sidecar->>Sidecar: Internal Hooks
    Sidecar->>Sidecar: Channels
    Sidecar->>Sidecar: Plugin Services
    Sidecar->>Sidecar: ACP Session Manager

    Note over Main,GW: 阶段8: 处理器绑定
    Main->>GW: attachGatewayWsHandlers()

    Main-->>CLI: Gateway ready ✓

源码解析:关键函数

入口函数签名

// src/gateway/server.impl.ts:257-280
export async function startGatewayServer(
  port = 18789,
  opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
  // port: 默认 18789
  // opts: bind, host, auth, tailscale, controlUiEnabled...
}

配置快照结构

// src/config/config-snapshot.ts
interface ConfigSnapshot {
  exists: boolean;          // 配置文件是否存在
  valid: boolean;           // 是否通过验证
  path: string;             // 配置文件路径
  parsed: OpenClawConfig;   // 解析后的配置对象
  raw: string;              // 原始 YAML 内容
  issues: ConfigIssue[];    // 验证问题列表
  legacyIssues: LegacyIssue[]; // 遗留配置问题
}

关键设计决策

决策1:为什么 Node.js 版本检查在最前面?

// openclaw.mjs:15-30
const MIN_NODE_MAJOR = 22;
const MIN_NODE_MINOR = 12;

const parseVersion = (version) => {
  const match = version.match(/^v?(\d+)\.(\d+)/);
  return { major: parseInt(match[1]), minor: parseInt(match[2]) };
};

const isSupportedNodeVersion = (version) => {
  const { major, minor } = parseVersion(version);
  return major > MIN_NODE_MAJOR ||
         (major === MIN_NODE_MAJOR && minor >= MIN_NODE_MINOR);
};

if (!isSupportedNodeVersion(process.version)) {
  console.error(`Node.js ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}+ required`);
  process.exit(1);
}

// 启用编译缓存(需要 Node.js 22.12+)
if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
  module.enableCompileCache();
}

思考过程:

❓ 为什么不延迟到需要新特性时再报错?

因为 module.enableCompileCache() 在旧版本根本不存在。延迟检查会导致用户看到难以理解的错误:

TypeError: module.enableCompileCache is not a function
    at Object.<anonymous> (openclaw.mjs:25:8)

设计启示: 前置检查失败成本最低的原则。在错误发生前就给出清晰的提示。

决策2:为什么配置快照不实时更新?

// server.impl.ts:284-330
let configSnapshot = await readConfigFileSnapshot();

// 配置迁移(如果需要)
if (configSnapshot.legacyIssues.length > 0) {
  const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed);
  if (migrated) {
    await writeConfigFile(migrated);
    log.info(`gateway: migrated legacy config entries`);
  }
}

// 重新加载验证
configSnapshot = await readConfigFileSnapshot();
if (configSnapshot.exists && !configSnapshot.valid) {
  throw new Error(`Invalid config at ${configSnapshot.path}`);
}

// 后续代码使用 configSnapshot,而非重新读取

思考过程:

❓ 如果配置实时更新,有什么问题?

sequenceDiagram
    participant ReqA as 请求 A
    participant Config as Config
    participant ReqB as 请求 B

    Note over ReqA: 读取 config.model = "gpt-4"
    ReqA->>Config: getConfig()
    Config-->>ReqA: {model: "gpt-4"}

    Note over Config: 用户修改配置
    ReqB->>Config: updateConfig({model: "claude-3"})
    Config-->>ReqB: ok

    Note over ReqA: 继续执行,模型突然变了!
    ReqA->>Config: getConfig()
    Config-->>ReqA: {model: "claude-3"} ❌ 不一致

这会导致请求内不一致。更严重的是,如果配置变更触发重连,可能在请求中途断开数据库连接。

解决方案:

// 快照隔离 + 显式重载
const configSnapshot = await readConfigFileSnapshot();

// 热更新通过专门的方法
async function reloadConfig(): Promise<void> {
  const newSnapshot = await readConfigFileSnapshot();
  // 广播事件,让子系统决定如何响应
  broadcast("config", { type: "reload", config: newSnapshot.parsed });
}

决策3:为什么密钥管理是独立阶段?

// server.impl.ts:333-397
const activateRuntimeSecrets = async (
  config: OpenClawConfig,
  params: { reason: "startup" | "reload" | "restart-check"; activate: boolean }
) => {
  try {
    // 1. 准备密钥快照
    const prepared = await prepareSecretsRuntimeSnapshot({ config });

    // 2. 激活到运行时
    if (params.activate) {
      activateSecretsRuntimeSnapshot(prepared);
      logGatewayAuthSurfaceDiagnostics(prepared);
    }

    secretsDegraded = false;
    return prepared;
  } catch (err) {
    // 3. 降级处理
    if (!secretsDegraded) {
      logSecrets.error(`[SECRETS_RELOADER_DEGRADED] ${String(err)}`);
    }
    secretsDegraded = true;

    if (params.reason === "startup") {
      throw new Error(`Startup failed: required secrets unavailable`);
    }
    throw err;
  }
};

密钥来源优先级:

flowchart LR
    ENV["环境变量<br/>OPENAI_API_KEY"] --> |优先级 1| RES
    CFG["配置文件<br/>secrets.defaults"] --> |优先级 2| RES
    RUNTIME["运行时注入<br/>Tailscale"] --> |优先级 3| RES

    RES["密钥解析结果"] --> CACHE["运行时缓存"]
    CACHE --> AGENT["Agent 调用"]

设计启示: 敏感数据需要独立的生命周期管理,支持热更新而不重启服务。

Sidecars 启动详解

Sidecars 是 Gateway 启动的辅助服务,它们的启动顺序很重要:

// src/gateway/server-startup.ts:34-191
export async function startGatewaySidecars(params: {
  cfg: OpenClawConfig;
  // ...
}): Promise<SidecarsResult> {

  // 1. 清理过期锁文件(必须在最前)
  await cleanStaleLockFiles({...});

  // 2. 浏览器控制(独立进程)
  browserControl = await startBrowserControlServerIfEnabled();

  // 3. Gmail 监听(需要网络)
  await startGmailWatcherWithLogs({ cfg, log });

  // 4. 内部钩子(无外部依赖)
  clearInternalHooks();
  await loadInternalHooks(cfg, workspaceDir);

  // 5. 渠道启动(依赖 Auth)
  if (!skipChannels) {
    await startChannels();
  }

  // 6. 网关启动钩子
  if (cfg.hooks?.internal?.enabled) {
    await triggerInternalHook({ type: "gateway:startup" });
  }

  // 7. 插件服务
  pluginServices = await startPluginServices({...});

  // 8. ACP 会话协调
  if (cfg.acp?.enabled) {
    await getAcpSessionManager().reconcilePendingSessionIdentities({ cfg });
  }

  // 9. 内存后端
  await startGatewayMemoryBackend({ cfg, log });

  return { browserControl, pluginServices };
}

优雅降级策略:

Sidecar 启动失败影响 降级行为
Browser Control 浏览器工具不可用 记录日志,继续启动
Gmail Watcher 无法自动处理邮件 记录日志,继续启动
Channels 特定渠道不可用 标记 degraded,继续启动
Plugin Services 插件功能不可用 记录日志,继续启动

WebSocket:为什么选择它作为核心协议?

Gateway 暴露的主要接口是 WebSocket (ws://127.0.0.1:18789),而非 HTTP REST API。

技术选型对比

特性 WebSocket HTTP REST gRPC SSE
双向通信 ✅ 原生支持 ❌ 需要轮询 ✅ Streaming ❌ 单向
实时性 ✅ 毫秒级 ❌ 依赖轮询 ⚠️ 需要配置 ✅ 实时推送
浏览器支持 ✅ 广泛 ✅ 广泛 ❌ gRPC-Web ✅ 广泛
实现复杂度
流式响应 ✅ 原生支持 ⚠️ Chunked ✅ Streaming ✅ 原生
连接复用 ✅ 持久连接 ❌ 每次握手 ✅ HTTP/2 ✅ 持久
心跳支持 ✅ Ping/Pong ❌ 无 ✅ 内置 ⚠️ 自定义

选择 WebSocket 的原因:

  1. Agent 流式响应——AI 的输出是逐字生成的,需要实时推送
  2. 事件广播——Session 变化、Agent 状态等需要实时通知客户端
  3. 统一协议——不需要同时维护 HTTP 和 WebSocket 两套 API

协议设计:帧格式详解

OpenClaw 定义了自己的 WebSocket 帧格式,而非使用标准 JSON-RPC:

flowchart TB
    subgraph Request["请求帧"]
        R1["kind: 'request'"]
        R2["id: 'req-xxx'"]
        R3["method: 'sessions.send'"]
        R4["params: {...}"]
    end

    subgraph Response["响应帧"]
        S1["kind: 'response'"]
        S2["id: 'req-xxx'"]
        S3["ok: true/false"]
        S4["result/error"]
    end

    subgraph Event["事件帧"]
        E1["kind: 'event'"]
        E2["event: 'agent'"]
        E3["payload: {...}"]
    end

    subgraph Stream["流式帧"]
        ST1["kind: 'event'"]
        ST2["event: 'agent'"]
        ST3["payload.type: 'text_delta'"]
        ST4["payload.text: '...'"]
    end

    Request --> |触发| Response
    Request --> |触发| Event
    Request --> |触发多个| Stream

请求帧示例

{
  "kind": "request",
  "id": "req-abc123",
  "method": "message.send",
  "params": {
    "sessionKey": "main",
    "text": "What's the weather?",
    "attachments": []
  }
}

响应帧示例

{
  "kind": "response",
  "id": "req-abc123",
  "ok": true,
  "result": {
    "sessionId": "main",
    "runId": "run-xyz789"
  }
}

事件帧示例(流式响应)

{"kind": "event", "event": "agent", "payload": {"type": "text_delta", "text": "The"}}
{"kind": "event", "event": "agent", "payload": {"type": "text_delta", "text": " weather"}}
{"kind": "event", "event": "agent", "payload": {"type": "text_delta", "text": " is sunny."}}
{"kind": "event", "event": "agent", "payload": {"type": "tool_call", "name": "weather.get"}}
{"kind": "event", "event": "agent", "payload": {"type": "done", "stopReason": "end_turn"}}

方法注册机制

// src/gateway/server-methods.ts
export const coreGatewayHandlers: GatewayMethodHandlers = {
  // 会话管理
  "sessions.list": handleSessionsList,
  "sessions.get": handleSessionsGet,
  "sessions.patch": handleSessionsPatch,
  "sessions.delete": handleSessionsDelete,

  // 消息操作
  "message.send": handleMessageSend,

  // Agent 控制
  "agent.start": handleAgentStart,
  "agent.abort": handleAgentAbort,

  // 配置管理
  "config.get": handleConfigGet,
  "config.patch": handleConfigPatch,

  // 插件管理
  "plugins.list": handlePluginsList,

  // Node 管理
  "node.list": handleNodeList,
  "node.invoke": handleNodeInvoke,

  // Cron 管理
  "cron.list": handleCronList,
  "cron.add": handleCronAdd,
};

// 插件可以扩展方法
export function loadGatewayPlugins(params: {
  cfg: OpenClawConfig;
  baseMethods: GatewayMethodHandlers;
}): {
  pluginRegistry: PluginRegistry;
  gatewayMethods: GatewayMethodHandlers;
} {
  const pluginMethods = {};

  for (const plugin of loadedPlugins) {
    if (plugin.methods) {
      Object.assign(pluginMethods, plugin.methods);
    }
  }

  return {
    pluginRegistry,
    gatewayMethods: { ...baseMethods, ...pluginMethods },
  };
}

消息处理流程

flowchart TB
    subgraph Client["客户端"]
        SEND["发送请求"]
    end

    subgraph Gateway["Gateway"]
        RECV["接收 WebSocket 消息"]
        PARSE["解析 JSON"]
        VALIDATE["Schema 验证"]
        AUTHZ["权限检查"]
        ROUTE["方法路由"]
        EXEC["执行处理器"]
        RESP["发送响应/事件"]
    end

    subgraph Handler["处理器"]
        METHOD["Method Handler"]
        AGENT["Agent 调用"]
        BROADCAST["事件广播"]
    end

    SEND --> RECV
    RECV --> PARSE
    PARSE --> VALIDATE
    VALIDATE --> |失败| ERR1["返回错误"]
    VALIDATE --> |成功| AUTHZ
    AUTHZ --> |失败| ERR2["返回 401"]
    AUTHZ --> |成功| ROUTE
    ROUTE --> |方法不存在| ERR3["返回 404"]
    ROUTE --> |找到| EXEC
    EXEC --> METHOD
    METHOD --> AGENT
    AGENT --> |流式事件| BROADCAST
    BROADCAST --> RESP
    METHOD --> |结果| RESP

实战演练:调试你的 Gateway

理论到此为止,让我们动手操作。

准备工作

确保 OpenClaw 已安装并运行:

# 检查 Gateway 是否运行
curl http://127.0.0.1:18789/health
# 预期输出: {"status":"ok"}

# 查看详细状态
openclaw status

使用 wscat 连接 Gateway

# 安装 wscat
npm install -g wscat

# 获取你的 token
openclaw config get gateway.auth.token
# 输出: your-token-here

# 连接 Gateway
wscat -c "ws://127.0.0.1:18789" -H "Authorization: Bearer your-token-here"
# 连接成功后会看到: Connected (press CTRL+C to quit)

发送一个测试请求

> {"kind":"request","id":"test-1","method":"sessions.list","params":{}}

预期响应:

< {
  "kind": "response",
  "id": "test-1",
  "ok": true,
  "result": {
    "sessions": [
      {
        "key": "main",
        "agentId": "default",
        "createdAt": 1736789123456,
        "updatedAt": 1736789456789
      }
    ]
  }
}

追踪一条消息的完整流程

现在发送一条消息,观察事件流:

> {"kind":"request","id":"test-2","method":"message.send","params":{"sessionKey":"main","text":"hi"}}

你将看到的事件序列:

< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":1,"type":"status","text":"Thinking..."}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":2,"type":"text_delta","text":"Hello"}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":3,"type":"text_delta","text":"!"}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":4,"type":"text_delta","text":" How"}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":5,"type":"text_delta","text":" can"}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":6,"type":"text_delta","text":" I"}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":7,"type":"text_delta","text":" help"}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":8,"type":"text_delta","text":" you"}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":9,"type":"text_delta","text":"?"}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":10,"type":"usage_update","input":15,"output":12}}
< {"kind":"event","event":"agent","payload":{"sessionId":"main","seq":11,"type":"done","stopReason":"end_turn"}}
< {"kind":"response","id":"test-2","ok":true,"result":{"runId":"run-xxx"}}

使用日志调试

# 启用调试日志
openclaw gateway start --log-level debug

# 或者通过环境变量
LOG_LEVEL=debug openclaw gateway start

# 只看特定模块
LOG_LEVEL=debug LOG_INCLUDE="gateway,agent" openclaw gateway start

日志示例:

[DEBUG] gateway: WebSocket connection established from 127.0.0.1
[DEBUG] gateway: Received request: {"id":"test-1","method":"sessions.list"}
[DEBUG] gateway: Method resolved: sessions.list -> handleSessionsList
[DEBUG] sessions: Listing sessions, found 1
[DEBUG] gateway: Sending response: {"id":"test-1","ok":true,...}

常见问题排查

问题1:连接被拒绝

# 检查 Gateway 是否在监听
lsof -i :18789
# 或
netstat -an | grep 18789

# 检查绑定地址配置
openclaw config get gateway.bind
# loopback = 127.0.0.1(只允许本地)
# lan = 0.0.0.0(允许局域网)
# tailnet = Tailscale 网络接口

问题2:认证失败

# 获取当前 token
openclaw config get gateway.auth.token

# 如果没有 token,生成一个
openclaw config set gateway.auth.token $(openssl rand -hex 16)

# 重启 Gateway 使配置生效
openclaw gateway restart

问题3:方法不存在

// 错误响应示例
{"kind":"response","id":"x","ok":false,"error":{"code":"METHOD_NOT_FOUND","message":"method not found: unknown.method"}}

// 解决:查看可用方法列表
> {"kind":"request","id":"list","method":"methods.list","params":{}}
< {"kind":"response","id":"list","ok":true,"result":{"methods":["sessions.list","sessions.get",...]}}

问题4:请求超时

# 检查 Agent 是否正常
openclaw agent status

# 检查模型配置
openclaw config get agents.defaults.model
openclaw config get providers.openai.apiKey

# 测试 LLM 连接
openclaw agent test --model gpt-4

常见陷阱

高级用户不仅要会解决问题,更要能预判问题。以下是 Gateway 开发和运维中常见的陷阱。

陷阱 1:Node.js 版本不兼容

症状:

$ openclaw gateway start
TypeError: module.enableCompileCache is not a function
    at Object.<anonymous> (openclaw.mjs:25:8)

原因: OpenClaw 使用了 Node.js 22.12+ 才支持的 module.enableCompileCache() API。在旧版本上,这个方法根本不存在,导致启动失败。

错误理解:

# 错误理解:以为只是性能优化问题,忽略版本要求
$ node --version
v20.10.0
# "应该没问题吧,都是 Node.js"

正确做法:

# 检查版本是否符合最低要求
$ node --version
v22.12.0  # ✅ 符合要求

# 如果版本过低,使用 nvm 升级
$ nvm install 22
$ nvm use 22
$ node --version
v22.14.0

# 或在 package.json 中指定 engines 约束
{
  "engines": {
    "node": ">=22.12.0"
  }
}

调试方法:

# 启动时添加版本检查日志
$ DEBUG=openclaw:* node --version && openclaw gateway start

# 或在代码中提前检查(OpenClaw 已内置)
# 见 openclaw.mjs:15-30

源码位置: openclaw.mjs:15-30


陷阱 2:端口被占用

症状:

$ openclaw gateway start
Error: listen EADDRINUSE: address already in use :::18789
    at Server.setupListenHandle [as _listen2] (net.js:1318:16)

原因: 18789 端口已被其他进程占用,可能是:

  • 之前启动的 Gateway 未正确关闭
  • 其他应用程序占用了该端口
  • 僵尸进程未释放端口

错误做法:

# 错误:直接 kill -9,可能导致数据丢失
$ kill -9 $(lsof -t -i:18789)

正确做法:

# 1. 查看占用端口的进程详情
$ lsof -i :18789
COMMAND   PID   USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
node     12345  user   22u  IPv6   1234567      0t0  TCP *:18789 (LISTEN)

# 2. 确认是否是 Gateway 进程
$ ps aux | grep 12345
user  12345  ... openclaw gateway start

# 3. 如果是 Gateway,使用优雅关闭
$ openclaw gateway stop
# 或发送 SIGTERM 信号
$ kill -TERM 12345

# 4. 如果是其他程序,考虑更换端口
$ openclaw gateway start --port 18790

# 或修改配置文件
$ openclaw config set gateway.port 18790

调试方法:

# 查看 Gateway 是否已在运行
$ openclaw status

# 检查端口监听状态
$ ss -tlnp | grep 18789

# 或使用 netstat
$ netstat -tlnp | grep 18789

预防措施:

# 在配置中添加端口检测
gateway:
  port: 18789
  portCheck:
    enabled: true
    killExisting: false  # 不自动 kill,只报警

陷阱 3:配置文件权限问题

症状:

$ openclaw gateway start
[ERROR] EACCES: permission denied, open '/home/user/.openclaw/openclaw.yaml'

原因: 配置文件或目录的权限设置不正确,可能是因为:

  • 使用 sudo 运行过 OpenClaw,导致文件归属权变化
  • 手动创建了配置文件但权限设置错误

错误代码:

# 错误:使用 sudo 启动,导致后续权限问题
$ sudo openclaw gateway start
# 之后普通用户无法访问配置文件

正确代码:

# 修复权限问题
$ sudo chown -R $(whoami):$(whoami) ~/.openclaw
$ chmod 700 ~/.openclaw
$ chmod 600 ~/.openclaw/openclaw.yaml

# 验证权限
$ ls -la ~/.openclaw/
drwx------  2 user user 4096 Mar 14 10:00 .
-rw-------  1 user user 1024 Mar 14 10:00 openclaw.yaml

调试方法:

# 检查配置文件路径和权限
$ openclaw config path
/home/user/.openclaw/openclaw.yaml

$ ls -la $(openclaw config path)

# 使用 strace 追踪文件访问(Linux)
$ strace -e openat openclaw gateway start 2>&1 | grep -i eacces

预防原则:

永远不要使用 sudo 运行 OpenClaw Gateway,除非你明确知道自己在做什么。


陷阱 4:WebSocket 连接认证失败

症状:

< {"kind":"response","id":"test-1","ok":false,"error":{"code":"UNAUTHORIZED","message":"invalid token"}}

原因: 认证 token 不正确或未配置。

错误代码:

// 错误:使用配置文件中的原始值而非激活后的值
const token = config.gateway.auth.token;  // 可能是加密的

正确代码:

# 1. 获取正确的 token
$ openclaw config get gateway.auth.token
your-actual-token-here

# 2. 如果没有 token,生成一个
$ openclaw config set gateway.auth.token "$(openssl rand -hex 16)"

# 3. 验证 token 配置
$ openclaw gateway auth test
Token valid: yes

调试方法:

# 启用认证调试日志
$ LOG_LEVEL=debug LOG_INCLUDE="auth,gateway" openclaw gateway start

# 查看 token 相关日志
# [DEBUG] auth: Validating token: abc123...
# [DEBUG] auth: Token validation result: valid=true

源码位置: src/gateway/auth.ts


设计启示:Gateway 模式的优劣

优势

  1. 单一入口——所有流量经过一处,便于监控、限流、审计
  2. 统一状态——系统状态集中管理,避免分散不一致
  3. 简化客户端——客户端只需连接 Gateway,不需知道其他组件
  4. 优雅关闭——可以在关闭前通知所有客户端,等待请求完成

劣势

  1. 单点故障——Gateway 挂了,整个系统不可用
  2. 扩展瓶颈——所有流量经过 Gateway,水平扩展困难
  3. 开发耦合——新功能需要修改 Gateway,不够灵活

与 Kubernetes API Server 的对比

flowchart LR
    subgraph K8s["Kubernetes"]
        KAPI["API Server<br/>(Control Plane)"]
        KETCD["etcd"]
        KSCHED["Scheduler"]
        KCTRL["Controller Manager"]

        KAPI --> KETCD
        KAPI --> KSCHED
        KAPI --> KCTRL
    end

    subgraph OpenClaw["OpenClaw"]
        OGW["Gateway<br/>(Control Plane)"]
        OSESSION["Session Store"]
        OAGENT["Agent Runtime"]
        OCHANNEL["Channels"]

        OGW --> OSESSION
        OGW --> OAGENT
        OGW --> OCHANNEL
    end
特性 OpenClaw Gateway K8s API Server
角色 控制平面 控制平面
单点问题 ⚠️ 存在 ✅ HA 模式解决
扩展方式 插件 CRD + Aggregation
认证 Token/Password 多种认证器
授权 内置 RBAC
存储后端 文件/SQLite etcd
API 风格 JSON-RPC like RESTful

OpenClaw 可以学习的:

  • 认证/授权插件化(Webhook 认证器)
  • API Aggregation 层(让扩展看起来像内置 API)
  • 更完善的 RBAC
  • etcd/分布式存储支持

未来改进方向

flowchart TB
    subgraph Current["当前架构"]
        GW1["Gateway<br/>(单实例)"]
    end

    subgraph Future["未来架构"]
        LB["Load Balancer"]
        GW2["Gateway 1"]
        GW3["Gateway 2"]
        GW4["Gateway N"]
        SHARED["Shared State<br/>(Redis/etcd)"]

        LB --> GW2
        LB --> GW3
        LB --> GW4
        GW2 <--> SHARED
        GW3 <--> SHARED
        GW4 <--> SHARED
    end

    Current --> |水平扩展| Future

小结

Gateway 是 OpenClaw 的心脏,它的核心职责是:

职责 说明 关键实现
协调 作为控制平面协调子系统 WebSocket 方法路由
调度 决定谁来处理什么 Request Router + Method Registry
监控 维护系统状态,广播事件 Event Broadcasting
安全 统一认证授权入口 Auth Manager

理解 Gateway 的关键不在于记住它有多少方法,而在于理解它为什么存在以及它如何让其他组件更简单

关键源码文件

文件 职责
src/gateway/server.impl.ts 核心服务器实现
src/gateway/server-runtime-state.ts 运行时状态管理
src/gateway/server-methods.ts 方法处理器定义
src/gateway/server-http.ts HTTP 端点处理
src/gateway/server-chat.ts 聊天消息处理
src/gateway/auth.ts 认证逻辑

在下一篇文章中,我们将深入 消息的一生——一条消息从发送到收到回复,经历了哪些站点、遇到了哪些关卡。


系列索引: OpenClaw 源码解析:目录索引

下一篇: OpenClaw 源码解析(二):消息的一生