OpenClaw 设计与实现

第3章 Gateway 网关引擎

作者 杨艺韬 · 12,675 字 · 发布于 · 更新于

第3章 Gateway 网关引擎

一个系统的成熟度,不看它在正常情况下跑得多好,而看它在凌晨三点、通道断连、模型限速、配置刚改错的情况下能否自愈。

本章要点

  • 理解 Gateway 作为"编排者而非执行者"的核心角色定位
  • 剖析 Gateway 启动流程:从配置加载到子系统编排的完整链路
  • 掌握配置系统的分层覆盖机制与热重载设计
  • 理解 Daemon 模式、健康监控与事件系统的工程实现

上一章我们从万米高空俯瞰了 OpenClaw 的全景,画出了每个子系统的轮廓。从本章开始,我们降落到地面,逐一走进每一栋建筑。第一站,自然是那座最高、最显眼的大楼——Gateway 网关引擎。

3.1 Gateway 的角色

Gateway 是一个在计算机系统设计里被反复使用的概念——从 TCP/IP 的 Gateway、到 API Gateway、到微服务的 Service Gateway、再到 OpenClaw 的 AI Gateway——"Gateway"这个词的本质是"一个统一入口、背后转发到多个后端"。这种模式的价值在于:(1) 隐藏后端复杂度——客户端只需要知道 Gateway、不需要知道有多少个后端;(2) 集中管理横切关注点——auth、rate limit、logging、监控、限流都在 Gateway 做一次;(3) 便于演化——后端可以独立升级、替换、扩容、对客户端透明。OpenClaw 把这种成熟的 Gateway 模式、应用到了 AI Agent 场景——"统一入口"是通道(Telegram、Slack、HTTP API 等)、"后端"是 LLM 提供者(OpenAI、Anthropic、Gemini 等)和各种工具。这个设计选择、让 OpenClaw 天然具备了 API Gateway 类系统经过多年验证的所有优势——集中管控、统一监控、灵活扩展。

这一章我们要做的事情、远远超过"读一下 Gateway 的源码"——我们要在读源码的过程中、建立一个通用的"服务级系统"的心智模型。Gateway 类系统——无论是 OpenClaw 的 AI Gateway、Nginx 的 HTTP Gateway、Kong 的 API Gateway、Istio 的 Service Mesh Gateway——都共享一些底层模式:长期运行的守护进程、多协议多端点的服务器、热重载的配置系统、健康监控、优雅降级、生命周期管理。理解这些模式、比记住任何一个具体 Gateway 的 API 都有价值。你以后做任何一个需要"长期运行 + 多方接入 + 集中治理"的系统时、都会用到这套思维。

本章的技术内容看起来很"工程化"——启动阶段、热重载、daemon 管理、健康监控——这些没有任何"AI 含量"、都是传统服务器领域的成熟技术。但正是这些"传统能力"、让 OpenClaw 能在"AI 的新领域"扎根于"工程的老土壤"。很多 AI 项目失败、不是因为 AI 能力不行、是因为这些"传统能力"做得不够到位——启动太慢、配置难改、运维麻烦、监控缺失——这些问题让一个有 AI 能力的系统、变成了运维的噩梦。OpenClaw 的启示是——"做 AI 产品、先做好非 AI 的部分"。

你有没有想过,当你在 Telegram 里发出一条消息,到 Agent 回复你,中间到底发生了什么?

表面上看,似乎很简单:收消息、调 API、发回复。三步搞定。但仔细想想,你会发现这背后藏着惊人的隐性复杂度——消息来了,先要判断它属于哪个通道、哪个账户、哪个会话;然后加载历史上下文,但上下文可能已逼近模型窗口极限,需要先压缩;接着组装系统提示词、注册可用工具、检查安全策略;调用模型后,模型可能不给文本回复,而是要求调用工具;工具执行完毕,结果送回模型,模型可能再次要工具……如此往复,周而复始,直到任务完成。

统领这一切的,就是 Gateway。

Gateway 之于 OpenClaw,犹如内核之于操作系统——它不亲自运行应用程序,但一切应用程序的运行都仰赖于它。

Gateway 不是简单的"代理"或"转发层"。它管理着应用程序运行所需的一切:进程生命周期、资源分配、安全隔离、设备驱动(通道插件)、任务调度(Cron)。src/gateway/ 目录包含 440 个文件(其中 252 个非测试文件),在 src/ 的 51 个子目录里排第三——前两名是 src/agents/(1071/546)和 src/infra/(571/287)。但 infra/ 是横切工具集而非一个有边界的子系统,所以就「成建制的子系统」而论,Gateway 仅次于 Agent 运行时——这个体量本身,就是 Gateway 职责之重的无声证明。

关键概念:Gateway(网关) Gateway 是 OpenClaw 的核心编排进程,类似于操作系统内核的角色。它不直接执行 AI 推理或消息处理,而是协调所有子系统(通道、Agent、安全、定时任务等)的启动、运行和关闭。Gateway 作为 Daemon 常驻运行,是整个系统的"大脑"。

具体而言,Gateway 负责:

  • 进程生命周期管理:启动、关闭、重启、信号处理——确保系统 7×24 稳定运行
  • HTTP/WebSocket 服务:Control UI、REST API、OpenAI 兼容端点、实时流
  • 配置加载与热重载:读取、验证、监控配置变更——无需重启即可更新行为
  • 子系统编排:协调通道、Agent 运行时、安全系统等十几个子系统的启动顺序
  • 健康监控:通道健康检查、就绪状态管理——自动重连掉线的通道
  • 凭证管理:运行时密钥快照的激活和刷新——确保 API 密钥安全且始终可用

🔥 深度洞察:编排者的悖论

Gateway 的设计揭示了一个反直觉的工程真理:系统中最重要的组件,恰恰是什么都不做的那个。 交响乐团的指挥不演奏任何乐器,但没有指挥,一百位乐手各吹各的。军事指挥官不亲自冲锋,但没有指挥链,精锐部队不过是一群武装暴徒。Gateway 的几百个文件里,没有一行直接调用 LLM API 或解析 Telegram 消息——它的全部价值在于确保该调用 LLM 的模块在正确的时间、以正确的参数、在正确的安全约束下被触发。这就是"编排"与"执行"的根本区别:执行者的价值在于做得好,编排者的价值在于让所有执行者能同时做好。

图 3-1:Gateway 模块架构图

下图描绘了 src/gateway/ 内部的模块关系,以及它与 src/config/、src/daemon/、src/secrets/ 等外部模块的依赖关系。箭头表示调用或依赖方向。

flowchart TB
    subgraph GW["src/gateway/ — Gateway 核心"]
        IMPL["server.impl.ts<br/>startGatewayServer()"]
        HTTP["server-http.ts<br/>HTTP 路由 & 端点"]
        WS["server-ws-runtime.ts<br/>WebSocket 处理"]
        METHODS["server-methods-list.ts<br/>方法注册表"]
        CHAT["server-chat.ts<br/>Agent 事件 & 会话"]
        CHANNELS["server-channels.ts<br/>通道管理器"]
        HEALTH["channel-health-monitor.ts<br/>通道健康监控"]
        RELOAD["config-reload.ts<br/>配置热重载"]
        PLAN["config-reload-plan.ts<br/>重载计划生成"]
        STARTUP["server-startup.ts<br/>侧车启动编排"]
        BOOT["boot.ts<br/>BOOT.md 自举"]
        SENTINEL["server-restart-sentinel.ts<br/>重启哨兵"]
        CLOSE["server-close.ts<br/>优雅关闭"]
        CRON["server-cron.ts<br/>定时任务"]
        HEALTHSTATE["server/health-state.ts<br/>健康快照"]
    end
    subgraph CFG["src/config/ — 配置系统"]
        IO["io.ts — JSON5 读写"]
        VALID["validation.ts — Schema 验证"]
        ENV["env-substitution.ts<br/>环境变量替换"]
        INC["includes.ts — 文件包含"]
        PATHS["paths.ts — 路径解析"]
        TYPES["types.*.ts — 37 个类型文件"]
        LEGACY["legacy-migrate.ts<br/>遗留格式迁移"]
    end
    subgraph DMN["src/daemon/ — 守护进程"]
        SVC["service.ts<br/>GatewayService 接口"]
        SYSD["systemd.ts — Linux"]
        LNCH["launchd.ts — macOS"]
        SCHT["schtasks.ts — Windows"]
    end
    subgraph SEC["src/secrets/ — 凭证管理"]
        RTMS["runtime.ts<br/>运行时密钥快照"]
    end
    IMPL --> HTTP
    IMPL --> WS
    IMPL --> STARTUP
    IMPL --> CHANNELS
    IMPL --> RELOAD
    IMPL --> CLOSE
    IMPL --> SENTINEL
    HTTP --> METHODS
    HTTP --> CHAT
    WS --> METHODS
    RELOAD --> PLAN
    CHANNELS --> HEALTH
    HEALTH --> HEALTHSTATE
    IMPL --> IO
    IO --> ENV
    IO --> INC
    IO --> VALID
    IO --> PATHS
    IO --> LEGACY
    VALID --> TYPES
    IMPL --> RTMS
    STARTUP --> BOOT
    STARTUP --> CRON
    SVC --> SYSD
    SVC --> LNCH
    SVC --> SCHT

3.2 启动流程剖析

OpenClaw 的启动是一个精心编排的多阶段过程。让我们逐步跟踪:

图 3-2:Gateway 启动流程时序图

下图展示了从用户执行 CLI 命令到 Gateway 完全就绪的完整启动链路,对应 src/entry.ts → src/cli/run-main.ts → src/gateway/server.impl.ts → src/gateway/server-startup.ts → src/gateway/boot.ts 的调用关系。

sequenceDiagram
    participant User as 用户 / CLI
    participant Entry as entry.ts
    participant CLI as cli/run-main.ts
    participant Server as server.impl.ts<br/>startGatewayServer()
    participant Config as config/io.ts<br/>readConfigFileSnapshot()
    participant Secrets as secrets/runtime.ts<br/>prepareSecretsRuntimeSnapshot()
    participant Sidecars as server-startup.ts<br/>startGatewaySidecars()
    participant Boot as boot.ts<br/>runBootOnce()
    User->>Entry: openclaw gateway run
    Entry->>Entry: process.title = "openclaw"
    Entry->>Entry: ensureOpenClawExecMarkerOnProcess()
    Entry->>Entry: installProcessWarningFilter()
    Entry->>Entry: normalizeEnv() / enableCompileCache()
    Entry->>CLI: 交给 CLI 路由
    CLI->>Server: startGatewayServer(port=18789)
    Server->>Config: readConfigFileSnapshot()
    Config-->>Server: ConfigFileSnapshot
    alt legacyIssues.length > 0
        Server->>Server: migrateLegacyConfig()
        Server->>Config: writeConfigFile(migrated)
        Server->>Config: readConfigFileSnapshot() [第二次]
    end
    Server->>Server: assertValidGatewayStartupConfigSnapshot()
    Server->>Server: applyPluginAutoEnable()
    Server->>Secrets: prepareSecretsRuntimeSnapshot()
    Secrets-->>Server: 解析后的密钥快照
    Server->>Server: activateSecretsRuntimeSnapshot()
    Server->>Server: initSubagentRegistry()
    Server->>Server: loadGatewayStartupPlugins()
    Server->>Server: createChannelManager()
    Server->>Server: 创建 HTTP/WebSocket 服务器
    Server->>Sidecars: startGatewaySidecars()
    Sidecars->>Sidecars: cleanStaleLockFiles()
    Sidecars->>Sidecars: startGmailWatcherWithLogs()
    Sidecars->>Sidecars: clearInternalHooks() / loadInternalHooks()
    Sidecars->>Sidecars: prewarmConfiguredPrimaryModel() / startChannels()
    Sidecars->>Sidecars: triggerInternalHook("gateway:startup")
    Sidecars->>Sidecars: startPluginServices()
    Sidecars->>Sidecars: getAcpSessionManager() [acp.enabled]
    Sidecars->>Sidecars: startGatewayMemoryBackend()
    Sidecars-->>Server: 侧车就绪
    Sidecars->>Boot: gateway:startup 钩子(内置 boot-md)→ runBootOnce()
    Boot->>Boot: loadBootFile() → buildBootPrompt()
    Server-->>User: Gateway 就绪 ✓

3.2.1 阶段 1:入口和预处理

"启动阶段"在系统架构里是一个极易被低估但极为关键的话题。一个服务从"开始"到"ready"之间的这段时间、决定了它会不会被"启动风暴"(startup storm)打垮——如果启动时间太长、在流量高峰时机器挂了重启、可能还没起来完成就又挂了、形成恶性循环。更深层的问题是——启动阶段的任何失败都是"不可商量的"——服务还没起来、自动 failover 还没激活、监控还没连接、用户还没被引导到其他实例——这个时候的失败就是真实的事故。所以成熟的系统、会把启动流程设计得极其精细——每一步都有明确的前置条件、清晰的失败策略、可以被独立验证和监控。OpenClaw 的六阶段启动流程——入口预处理、CLI 路由、配置加载、安全初始化、子系统启动、BOOT.md 执行——体现的正是这种"严谨启动"的思路。本节会逐一走过这六个阶段、让你理解每一步在做什么、为什么必须在这个顺序、如果失败会怎样。

一切从 src/entry.ts 开始。这是 Node.js 的入口文件,执行最基本的环境初始化:

// src/entry.ts:48
process.title = "openclaw";
ensureOpenClawExecMarkerOnProcess();
installProcessWarningFilter();
normalizeEnv();

关键步骤包括:

  • 设置进程标题为 "openclaw"
  • 安装进程警告过滤器(屏蔽几条已知的 Node.js 弃用警告与实验性警告)
  • 规范化环境变量
  • 启用 Node.js 编译缓存(enableCompileCache())以加速后续启动

3.2.2 阶段 2:CLI 路由

入口文件将控制权交给 CLI 路由系统(src/cli/run-main.ts),根据命令行参数决定执行路径。当用户运行 openclaw gateway run 时,最终会调用 startGatewayServer()。

3.2.3 阶段 3:配置加载与验证

startGatewayServer()(定义于 src/gateway/server.impl.ts:378)首先处理配置:

// src/gateway/server.impl.ts:378 — startGatewayServer 核心流程
export async function startGatewayServer(
  port = 18789, opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
  let configSnapshot = await readConfigFileSnapshot();
  // 1. 检查并迁移遗留配置格式
  if (configSnapshot.legacyIssues.length > 0) {
    const { config: migrated } = migrateLegacyConfig(configSnapshot.parsed);
    if (migrated) await writeConfigFile(migrated);
  }
  // 2. 重新读取并验证配置
  configSnapshot = await readConfigFileSnapshot();
  if (configSnapshot.exists) {
    assertValidGatewayStartupConfigSnapshot(configSnapshot, { includeDoctorHint: true });
  }
  // 3. 自动启用检测到的插件
  const autoEnable = applyPluginAutoEnable({ config: configSnapshot.config, env: process.env });
  // ... 后续初始化子系统
}

配置加载经历了三个子步骤:

  1. 遗留迁移:检测旧格式的配置字段,自动迁移到新格式
  2. 验证:通过 src/config/validation.ts 的 schema 验证确保配置合法
  3. 插件自动启用:根据配置(以及环境变量里的通道凭据)判断哪些通道、模型提供者等已经配好,自动启用对应插件(src/config/plugin-auto-enable.ts)

配置文件支持 JSON5 格式(通过 json5 库解析,见 src/config/io.ts),允许注释和尾逗号,大幅提升了人类可读性。配置还支持环境变量替换(src/config/env-substitution.ts)和文件包含(src/config/includes.ts)。

3.2.4 阶段 4:安全初始化

凭证系统是 Gateway 启动的关键环节:

// src/gateway/server.impl.ts:461(节选)
const activateRuntimeSecrets = async (config, params) =>
  await runWithSecretsActivationLock(async () => {
    const prepared = await prepareSecretsRuntimeSnapshot({ config });
    if (params.activate) {
      activateSecretsRuntimeSnapshot(prepared);
      logGatewayAuthSurfaceDiagnostics(prepared);
    }
    // ...
  });

prepareSecretsRuntimeSnapshot()(src/secrets/runtime.ts)会:

  • 解析配置和各 Agent 认证档案(auth profile)中引用的所有密钥
  • 在深拷贝的配置上填入解析结果,生成一份运行时快照
  • 有密钥解析不出来时抛错(启动阶段即失败退出)

这种"快照"模式确保了凭证变更的原子性——要么全部更新成功,要么保持上一次的已知良好状态。

3.2.5 阶段 5:子系统启动

配置和安全就绪后,Gateway 开始启动各个子系统:

// src/gateway/server.impl.ts:577-615、709-718(节选)
initSubagentRegistry();                      // 初始化 Sub-agent 注册表
const gatewayPluginConfigAtStart = applyPluginAutoEnable({ config: cfgAtStart, env: process.env }).config;
const defaultAgentId = resolveDefaultAgentId(gatewayPluginConfigAtStart);  // 解析默认 Agent ID
const defaultWorkspaceDir = resolveAgentWorkspaceDir(gatewayPluginConfigAtStart, defaultAgentId);
// 加载插件和通道
({ pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayStartupPlugins({
  cfg: gatewayPluginConfigAtStart,
  workspaceDir: defaultWorkspaceDir,
  // ...
}));
// ...
// 创建通道管理器
const channelManager = createChannelManager({
  loadConfig: () => applyPluginAutoEnable({ config: loadConfig(), env: process.env }).config,
  channelLogs,
  channelRuntimeEnvs,
  resolveChannelRuntime: getChannelRuntime,
});

随后,startGatewaySidecars()(src/gateway/server-startup.ts)启动一系列"侧车"服务:

// src/gateway/server-startup.ts:67 — 侧车启动编排(按源码顺序精简)
export async function startGatewaySidecars(params) {
  for (const sessionsDir of await resolveAgentSessionDirs(stateDir)) {         // 1. 清理会话锁文件
    await cleanStaleLockFiles({ sessionsDir, staleMs: SESSION_LOCK_STALE_MS, removeStale: true });
  }
  await startGmailWatcherWithLogs({ cfg: params.cfg, log: params.logHooks });  // 2. Gmail 监听
  clearInternalHooks();                                                        // 3. Hook 处理器
  await loadInternalHooks(params.cfg, params.defaultWorkspaceDir);
  if (!skipChannels) {                                                         // 4. 预热主模型并启动通道
    await prewarmConfiguredPrimaryModel({ cfg: params.cfg, log: params.log });
    await params.startChannels();
  }
  setTimeout(() => void triggerInternalHook(...), 250);                        // 5. gateway:startup 钩子
  pluginServices = await startPluginServices({ ... });                          // 6. 插件服务
  if (params.cfg.acp?.enabled) {                                               // 7. ACP 会话身份对账
    void getAcpSessionManager().reconcilePendingSessionIdentities({ cfg: params.cfg });
  }
  void startGatewayMemoryBackend({ cfg: params.cfg, log: params.log });        // 8. 记忆后端
}

3.2.6 阶段 6:BOOT.md 执行

侧车启动时发出的 gateway:startup 内部钩子事件,会触发内置钩子 boot-md(src/hooks/bundled/boot-md/handler.ts,内置钩子默认启用):它为每个 Agent 检查其工作区中是否存在 BOOT.md 文件(src/gateway/boot.ts)。如果存在,就将文件内容作为一次性指令发送给 Agent 执行:

// src/gateway/boot.ts:138(简化,省略日志与会话映射的保存/恢复)
export async function runBootOnce(params) {
  let result;
  try {
    result = await loadBootFile(params.workspaceDir);
  } catch (err) {
    return { status: "failed", reason: String(err) };
  }
  if (result.status === "missing" || result.status === "empty") {
    return { status: "skipped", reason: result.status };
  }
  const message = buildBootPrompt(result.content ?? "");
  // 以 agentCommand({ message, sessionKey, sessionId, deliver: false, ... }) 执行 Boot 指令...
}

这是一个优雅的"自举"机制——Agent 可以在每次启动时自动执行初始化任务,如发送上线通知、检查系统状态等。

3.3 HTTP/WebSocket 服务器

Gateway 通过 Node.js 原生的 http/https 模块和 ws 库提供网络服务(src/gateway/server-http.ts):

// src/gateway/server-http.ts:2-8(节选)
import {
  createServer as createHttpServer,
  type Server as HttpServer,
  type IncomingMessage,
  type ServerResponse,
} from "node:http";
import { createServer as createHttpsServer } from "node:https";

3.3.1 HTTP 端点

"API 设计"是系统工程里最容易显出设计者水平的地方——好的 API 像精心设计的产品说明书、每个端点都有明确的用途、参数格式统一、错误处理一致;差的 API 像老房子里的电路板、接线混乱、风格不统一、新加的端点和旧端点互相冲突。OpenClaw 的 HTTP API 设计体现了几个关键原则:(1) 路径沿用 OpenAI——/v1/ 前缀照搬 OpenAI API 的路径约定;(2) 资源导向——/v1/chat/completions 遵循 REST 风格;(3) OpenAI 兼容——直接支持 OpenAI SDK 的客户端、极大降低接入门槛;(4) 专用端点分离——管理端点和业务端点分开。这些设计细节、让 OpenClaw 的 API 不仅功能完备、而且"上手容易"——一个熟悉 OpenAI API 的开发者、几乎零成本就能接入 OpenClaw。

Gateway 暴露了以下核心 HTTP 端点:

路径 功能 实现文件
/ Control UI(Web 管理界面) control-ui.ts
/v1/chat/completions OpenAI 兼容 Chat API(默认关闭,见下方提示) openai-http.ts
/v1/responses OpenAI Responses API 兼容 openresponses-http.ts
/v1/models、/v1/embeddings OpenAI 兼容的模型列表与向量端点(上面两个端点任一打开时才启用) models-http.ts、embeddings-http.ts
/health、/healthz 存活探针(live,进程活着就 200) src/gateway/server-http.ts:131-132
/ready、/readyz 就绪探针(ready,按各通道账户的健康状况判定,有失败通道即 503) src/gateway/server-http.ts:133-134、src/gateway/server/readiness.ts
/hooks/* Webhook 端点(默认前缀 /hooks;需 hooks.enabled: true 并配置 hooks.token) hooks.ts

OpenAI 兼容 API(src/gateway/openai-http.ts)是一个特别值得注意的设计——它允许任何兼容 OpenAI API 格式的客户端直接连接 OpenClaw Gateway,将 OpenClaw 当作 OpenAI API 的代理。

⚠️ 这两个端点默认是关的。src/gateway/server-runtime-config.ts:71-75 的解析是 cfg.gateway?.http?.endpoints?.chatCompletions?.enabled ?? false 与 ...?.responses?.enabled ?? false——缺省即 false(src/config/types.gateway.ts:223-228,266-270 的 doc comment 同样写着缺省关闭)。开箱状态下 curl /v1/chat/completions 拿不到东西,必须先显式打开:

// ~/.openclaw/openclaw.json
{ "gateway": { "http": { "endpoints": {
    "chatCompletions": { "enabled": true },
    "responses": { "enabled": true },
} } } }

这是一个刻意的安全默认值:把一个无需交互即可驱动 Agent 的 HTTP 表面默认敞开,风险远大于收益。

3.3.2 WebSocket 协议

WebSocket 是 Gateway 与客户端(包括 Web UI、CLI 客户端、移动 app)之间的主要实时通信通道。WebSocket 处理逻辑的入口是 src/gateway/server-ws-runtime.ts(它只是一层薄封装,实际的连接与帧处理在 src/gateway/server/ws-connection.ts 和 src/gateway/server/ws-connection/message-handler.ts)。

协议帧只有三种(src/gateway/protocol/schema/frames.ts,按 type 字段区分):

  • 请求 { type: "req", id, method, params? }:客户端调用方法
  • 响应 { type: "res", id, ok, payload?, error? }:与请求按 id 配对
  • 事件 { type: "event", event, payload?, seq?, stateVersion? }:服务端主动推送

连接建立后,服务端先推一个 connect.challenge 事件(带 nonce),客户端的第一个请求必须是 method: "connect"(携带协议版本范围、客户端信息、认证凭据等 ConnectParams),否则握手失败;握手成功后服务端回 hello-ok,其中列出可用的方法和事件。服务端推送的事件名见 GATEWAY_EVENTS,例如:

  • agent / chat: Agent 执行事件与聊天回复的实时流
  • session.message / sessions.changed: 会话消息与会话列表变化
  • presence / tick / health / heartbeat: 在线状态、保活心跳与健康快照
  • node.pair.* / device.pair.* / exec.approval.*: 节点与设备配对、命令执行审批

图 3-3:请求处理管线

下图展示了一条消息从客户端发送到 Gateway 后的完整处理链路。HTTP 请求按顺序交给各端点处理器,命中的处理器再做认证(auth.ts,认证失败计入 auth-rate-limit.ts 的限流);WebSocket 连接先完成 connect 握手认证,之后的请求帧按方法名分发到对应处理器(server-methods.ts)。两条路径最终都可能触发 Agent 会话处理。

sequenceDiagram
    participant Client as 客户端<br/>(Web UI / CLI / 移动端)
    participant HTTP as server-http.ts<br/>HTTP 服务器
    participant Auth as auth.ts<br/>authorizeHttpGatewayConnect()
    participant RateLimit as auth-rate-limit.ts<br/>AuthRateLimiter
    participant Router as HTTP 路由
    participant WS as server-ws-runtime.ts<br/>WebSocket 服务器
    participant Methods as server-methods.ts<br/>方法处理器
    participant Chat as server-chat.ts<br/>Agent 事件处理
    participant Agent as Agent 运行时
    alt HTTP 请求
        Client->>HTTP: HTTP POST /v1/chat/completions
        HTTP->>Router: 按顺序尝试各端点处理器
        Router->>Router: /hooks/* → hooks.ts
        Router->>Router: /v1/responses → openresponses-http.ts
        Router->>Router: /v1/chat/completions → openai-http.ts
        Router->>Router: /health、/ready → 探针(server-http.ts)
        Router->>Router: / → control-ui.ts
        Router->>Auth: handleOpenAiHttpRequest() 内做认证<br/>authorizeHttpGatewayConnect()
        Auth->>RateLimit: 检查速率限制
        RateLimit-->>Auth: 通过 / 拒绝
        Auth-->>Router: GatewayAuthResult
        Router->>Agent: 转发到 Agent 执行
    end
    alt WebSocket 消息
        Client->>WS: 首帧 { type:"req", method:"connect", params }
        WS->>Auth: 握手认证
        Client->>WS: { type:"req", id, method, params }
        WS->>Methods: handleGatewayRequest() 按 method 分发
        Methods->>Agent: 创建/恢复会话 → 执行
        Agent-->>Chat: Agent 事件流(onAgentEvent)
        Chat-->>WS: createAgentEventHandler() 广播
        WS-->>Client: { type:"event", event, payload }
    end

Gateway 在 src/gateway/server-methods-list.ts 里登记对外公布的 WebSocket 方法与事件名(握手时写进 hello-ok):

// src/gateway/server-methods-list.ts:119、124(节选)
export function listGatewayMethods(): string[] { ... }  // 基础方法 + 各通道插件贡献的方法
export const GATEWAY_EVENTS = [ "connect.challenge", "agent", "chat", /* ... */ ];

3.4 配置系统

配置系统是 OpenClaw 灵活性的基石,实现在 src/config/ 目录中。

"配置驱动 vs 代码驱动"是软件设计里一个永恒的取舍。代码驱动的好处是灵活(可以写任何逻辑)、坏处是改动需要重新部署;配置驱动的好处是动态(可以运行时修改)、坏处是表达能力有限(只能做配置文件支持的事)。OpenClaw 选择配置驱动、是因为它面向的是"运维场景"——运营者需要频繁调整参数(切换模型、改限流、开启新通道),这些调整如果每次都要改代码重新部署、成本不可接受。JSON5 作为配置格式、又是一个深思熟虑的选择——标准 JSON 太严格(不支持注释、尾逗号、多行字符串),YAML 太灵活(缩进敏感、容易出错),JSON5 刚好在两者之间——它是"带注释、尾逗号、友好语法"的 JSON超集、对开发者和运维都很友好。这种"找一个恰到好处的中间方案"的判断力、是高级工程师的典型特征——他们不会为了"纯洁"而牺牲"实用"、也不会为了"炫技"而选择"过度工程"的方案。

3.4.1 配置文件格式

OpenClaw 使用 JSON5 作为配置文件格式(src/config/io.ts)。配置文件的查找逻辑支持多个候选路径:

// src/config/paths.ts:188
// 顺序:显式指定的配置路径(OPENCLAW_CONFIG_PATH)→ 由状态目录推出的路径 → 新默认路径
export function resolveDefaultConfigCandidates(
  env: NodeJS.ProcessEnv = process.env,
  homedir: () => string = envHomedir(env),
): string[] { ... }

图 3-4:配置系统架构图

下图展示了配置文件从磁盘到运行时的完整加载链路,包括 JSON5 解析、环境变量替换、文件包含展开、Schema 验证,以及热重载的四种模式决策流程。对应 src/config/io.ts、src/config/env-substitution.ts、src/config/includes.ts、src/config/validation.ts 和 src/gateway/config-reload.ts。

flowchart LR
    DISK["配置文件<br/>(JSON5 格式)"]
    PARSE["io.ts<br/>JSON5.parse()"]
    INC["includes.ts<br/>resolveConfigIncludes()"]
    ENVS["env-substitution.ts<br/>resolveConfigEnvVars()"]
    LEGACY["legacy-migrate.ts<br/>migrateLegacyConfig()"]
    VALID["validation.ts<br/>validateConfigObjectWithPlugins()"]
    DEFAULTS["defaults.ts<br/>applyAgentDefaults()<br/>applyModelDefaults()<br/>applySessionDefaults()"]
    RUNTIME["运行时<br/>OpenClawConfig 对象"]
    CHOK["chokidar 文件监控"]
    DIFF["config-reload.ts<br/>diffConfigPaths()"]
    PLAN["config-reload-plan.ts<br/>buildGatewayReloadPlan()"]
    DISK -->|"readConfigFileSnapshot()"| PARSE
    PARSE --> INC
    INC -->|"$include 展开"| ENVS
    ENVS -->|"${VAR} 替换"| LEGACY
    LEGACY --> VALID
    VALID --> DEFAULTS
    DEFAULTS --> RUNTIME
    CHOK -->|"文件变更事件"| DIFF
    DIFF -->|"changedPaths[]"| PLAN
    PLAN -->|"GatewayReloadPlan"| DECIDE
    subgraph DECIDE["重载模式决策"]
        direction TB
        OFF["off: 忽略"]
        RESTART["restart: 重启进程"]
        HOT["hot: 热更新"]
        HYBRID["hybrid: 按字段<br/>自动选择"]
    end

配置文件支持以下高级特性:

环境变量替换(src/config/env-substitution.ts):

{
  "channels": {
    "telegram": { "botToken": "${TELEGRAM_BOT_TOKEN}" }
  }
}

文件包含(src/config/includes.ts):

{
  "$include": "./channels.json5"
}

3.4.2 配置类型系统

配置类型定义分散在 37 个 types.*.ts 文件中(口径:ls src/config/types.*.ts,不含聚合导出的 src/config/types.ts 本身),这种分散式设计的好处是:

  • 每个子系统拥有自己的配置类型,避免巨型文件
  • 修改一个子系统的配置不会影响其他子系统的类型导出
  • 更好的编辑局部性

3.4.3 热重载

配置热重载是 Gateway 的重要特性(src/gateway/config-reload.ts)。它使用 chokidar 文件监控库监视配置文件变更:

// src/gateway/config-reload.ts:16
export type GatewayReloadSettings = {
  mode: GatewayReloadMode;  // "off" | "restart" | "hot" | "hybrid"
  debounceMs: number;       // 默认 300ms
};

关键概念:配置热重载(Hot Reload) 热重载允许在不停机的情况下更新系统配置。OpenClaw 通过文件监控检测配置变更,然后根据变更字段的影响范围自动决定是热更新(不重启进程)还是完整重启。这确保了 7×24 运行的 Agent 系统可以在不中断用户对话的前提下更新行为。

重载模式有四种:

  • off:禁用自动重载
  • restart:检测到变更时重启整个 Gateway 进程
  • hot:热重载,不重启进程直接应用变更;需要重启才能生效的变更只记一条日志、被忽略
  • hybrid(默认):根据变更影响范围自动选择热重载或重启

实战场景:运行中切换模型

假设你的 Agent 正在 Telegram 和 Discord 上服务 30 个用户,你决定把默认模型从 claude-sonnet-4-20250514 切换到 gpt-4.1。在传统的 Agent 框架中,你需要停止服务、修改代码或环境变量、重新启动——用户会经历一段肉眼可见的"失联"。

在 OpenClaw 中,你只需要修改配置文件中的 model 字段并保存。hybrid 模式会检测到这是 agents.defaults.model 路径的变更,命中 hot 规则(只需重启心跳运行器),下一次消息处理时即使用新模型——整个过程不重启进程。但如果你修改的是 plugins 字段(比如启用一个新的通道插件),系统会识别出这需要完整重启,先等进行中的回复和 Agent 运行结束(有超时上限)再重启 Gateway。

buildGatewayReloadPlan()(src/gateway/config-reload-plan.ts)会分析配置变更的具体字段,判断哪些变更可以热更新、哪些需要重启:

// src/gateway/config-reload.ts:28(简化,省略变量声明与数组分支)
export function diffConfigPaths(prev: unknown, next: unknown, prefix = ""): string[] {
  if (prev === next) return [];
  if (isPlainObject(prev) && isPlainObject(next)) {
    const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
    const paths: string[] = [];
    for (const key of keys) {
      // 递归比较每个字段
      const childPaths = diffConfigPaths(prevValue, nextValue, childPrefix);
      if (childPaths.length > 0) paths.push(...childPaths);
    }
    return paths;
  }
  return [prefix || "<root>"];
}

源码细节:配置 Diff 的递归算法与重载计划

diffConfigPaths() 的实现值得细看:它对两个配置对象做递归深度优先遍历,收集所有值不同的叶子路径。对于数组字段(如 memory.qmd.paths),它不是逐元素比较,而是使用 Node.js 内置的 isDeepStrictEqual 做结构性比较——这意味着 ["a","b"] 和 ["a","b"] 即使是不同的数组引用也不会被报告为变更,避免了无意义的重启。

更精彩的是 buildGatewayReloadPlan()(src/gateway/config-reload-plan.ts)的前缀匹配规则引擎。它维护了一个有序的规则列表,每条规则由配置路径前缀和动作类型(restart / hot / none)组成。例如 gateway.remote 变更是 none(不影响运行时),cron 变更是 hot(触发 Cron 服务热重启),plugins 变更是 restart(必须完整重启 Gateway)。通道插件还可以动态注册自己的热重载前缀——Telegram 插件声明了 channels.telegram 前缀(extensions/telegram/src/shared.ts:129),其下的变更只需重启 Telegram 通道而非整个 Gateway。

// src/gateway/config-reload-plan.ts:137 — 前缀规则匹配
function matchRule(path: string): ReloadRule | null {
  for (const rule of listReloadRules()) {
    if (path === rule.prefix || path.startsWith(`${rule.prefix}.`))
      return rule;  // 首条匹配获胜,规则顺序即优先级
  }
  return null;  // 无匹配 → 保守策略:要求重启
}

如果某个变更路径没有匹配到任何规则,系统默认要求完整重启——这是fail-safe 设计:宁可多重启一次,也不让未知变更在 hot 模式下生效导致不一致状态。

⚠️ 注意:修改 plugins 配置字段(如启用新通道插件)会触发 Gateway 完整重启,而非热更新。系统会先等进行中的回复和 Agent 运行结束再执行重启(最多等 gateway.reload.deferralTimeoutMs,默认 5 分钟)。如果需要在不中断服务的前提下添加插件,建议在低峰时段操作。

⚠️ 常见陷阱:环境变量替换失败

配置文件中的 ${VAR_NAME} 语法要求环境变量在 Gateway 进程启动时已经设置。常见错误场景:

# ❌ 错误:在 systemd 服务中,~/.bashrc 不会被加载
# 你在 .bashrc 中 export 的变量对 systemd 服务不可见

# ✅ 正确:在 systemd unit 文件中使用 EnvironmentFile
# [Service]
# EnvironmentFile=/home/openclaw/.env

# ✅ 正确:或在 openclaw.json 中使用 $include 引用独立的密钥文件
# { "$include": "./secrets.json5" }

# ✅ 正确:或写进 ~/.openclaw/.env——加载配置时会读取它(不覆盖已存在的同名变量,见 src/infra/dotenv.ts)

引用了未设置的变量不会让启动失败,只记一条警告、相关功能不可用。所以如果你发现 Gateway 启动后通道全部失败、Provider 返回 401,首先检查环境变量是否正确传递到了运行时进程。

⚠️ 常见陷阱:$include 路径解析

$include 的路径是相对于当前配置文件所在目录解析的,而非相对于工作目录。如果主配置在 ~/.openclaw/openclaw.json,那么 "$include": "./channels.json5" 会查找 ~/.openclaw/channels.json5,而非 ~/channels.json5(见 src/config/includes.ts 里以 path.dirname(this.basePath) 为基准的 path.resolve)。路径搞错会让配置校验失败,Gateway 启动时报 Failed to read include file: …(resolved: …)。另外,被包含的文件必须位于主配置文件所在目录之内,用 ../ 跳出去会被拒绝(Include path escapes config directory)。

3.5 Daemon 模式

"Daemon"这个词来自 Unix 早期、是对"长驻后台服务"的学术化命名。从 Unix 时代的 cron、syslog、inetd、到现代的 systemd service、Docker container、Kubernetes pod——Daemon 模型是所有长驻服务的基础形态。OpenClaw 选择 Daemon 模式、不是"赶时髦"、是因为它的工作性质就要求"长期运行 + 独立于用户会话 + 自动管理生命周期"——这正是 Daemon 的定义。跨平台 Daemon 管理是 OpenClaw 的一大特色——一般的 Node.js 项目只关心"我这个 npm 包怎么跑",OpenClaw 更关心"我这个服务在 Linux/macOS/Windows 分别怎么以 Daemon 方式长期运行"。这种"把服务生命周期管理当作一等公民"的设计、体现了 OpenClaw 对"生产级运维"的深度思考。这也是为什么它的抽象 GatewayService 接口设计得这么优雅——背后是 systemd、launchd、Windows 计划任务三个完全不同的系统、被一个统一接口抽象出来、对上层透明。

什么是 Daemon? 如果你之前没接触过这个概念:Daemon(守护进程)是一种在操作系统后台持续运行的程序,不依赖任何终端窗口。你关闭了 SSH 会话或终端,Daemon 依然在运行。常见的 Daemon 包括 Web 服务器(Nginx)、数据库(MySQL)和消息队列(Redis)。OpenClaw Gateway 就是这样一个 Daemon——它在后台持续运行,维护通道连接、处理用户消息、执行定时任务,即使你没有打开任何终端窗口。

OpenClaw 设计为以 Daemon 模式长期运行。src/daemon/ 目录实现了跨平台的守护进程管理:

3.5.1 平台适配

src/daemon/service.ts 定义了统一的 GatewayService 接口,然后为每个平台提供实现:

// src/daemon/service.ts:65
export type GatewayService = {
  label: string;
  loadedText: string;
  notLoadedText: string;
  stage: (args: GatewayServiceStageArgs) => Promise<void>;
  install: (args: GatewayServiceInstallArgs) => Promise<void>;
  uninstall: (args: GatewayServiceManageArgs) => Promise<void>;
  stop: (args: GatewayServiceControlArgs) => Promise<void>;
  restart: (args: GatewayServiceControlArgs) => Promise<GatewayServiceRestartResult>;
  isLoaded: (args: GatewayServiceEnvArgs) => Promise<boolean>;
  readCommand: (env: GatewayServiceEnv) => Promise<GatewayServiceCommandConfig | null>;
  readRuntime: (env: GatewayServiceEnv) => Promise<GatewayServiceRuntime>;
};

平台实现:

  • Linux:src/daemon/systemd.ts — 在 ~/.config/systemd/user/ 生成用户级 unit 文件,通过 systemctl --user 管理
  • macOS:src/daemon/launchd.ts — 在 ~/Library/LaunchAgents/ 生成 plist 文件,通过 launchctl 管理
  • Windows:src/daemon/schtasks.ts — 使用 Windows 计划任务

图 3-5:Daemon 生命周期状态机

下图展示了 GatewayService 接口(src/daemon/service.ts)定义的守护进程生命周期状态转换。install() / uninstall() 管理服务注册;进程内的重启请求会先经过重启前延迟检查(src/infra/restart.ts),有活跃任务时延迟执行,重启后再由重启哨兵(server-restart-sentinel.ts)回报结果。

stateDiagram-v2
    [*] --> NotInstalled: 初始状态
    NotInstalled --> Installed: install()<br/>生成 service 文件
    Installed --> Running: start / restart()<br/>进程启动
    Running --> Running: 热重载 (hot)<br/>configReload → onHotReload()
    Running --> DeferringRestart: 重启请求 + 活跃任务<br/>setPreRestartDeferralCheck()
    DeferringRestart --> DeferringRestart: 等待任务完成<br/>getTotalQueueSize() +<br/>getTotalPendingReplies() +<br/>getActiveEmbeddedRunCount() +<br/>活跃任务数 > 0
    DeferringRestart --> Restarting: 任务完成 / 延迟超时
    Running --> Restarting: restart()<br/>无活跃任务
    Restarting --> Running: 进程重新启动<br/>scheduleRestartSentinelWake()
    Running --> Stopped: stop()
    Stopped --> Running: restart()
    Installed --> NotInstalled: uninstall()<br/>删除 service 文件
    Stopped --> NotInstalled: uninstall()

3.5.2 重启延迟与重启哨兵

一个精巧的设计是重启前的延迟检查(src/infra/restart.ts、src/gateway/server-reload-handlers.ts)。当 Gateway 需要重启(例如配置变更触发 restart 模式),它不会立刻重启,而是:

  1. 检查当前是否有进行中的任务(活跃会话、排队命令、待发送回复)
  2. 如果有,延迟重启直到任务完成
  3. 如果延迟超时,记录日志并强制重启
// src/gateway/server.impl.ts:533
setPreRestartDeferralCheck(
  () =>
    getTotalQueueSize() +
    getTotalPendingReplies() +
    getActiveEmbeddedRunCount() +
    getInspectableTaskRegistrySummary().active,
);

这确保了重启不会中断正在进行的对话或工具执行(超时强制重启的情况除外)。

"重启哨兵"(src/gateway/server-restart-sentinel.ts)管的是重启之后:通过 config.apply/config.patch、update.run 等方法或 Agent 的 gateway 工具触发重启前,会先写一个哨兵文件,记下是哪个会话、哪个通道发起的(src/infra/restart-sentinel.ts);Gateway 重新启动后 scheduleRestartSentinelWake() 读取并删除这个文件,把重启结果回报给发起的会话。

💡 最佳实践:在生产环境中,始终使用 hybrid 重载模式(默认值)。它能自动区分可热更新的配置变更(如切换模型)和需要重启的变更(如启用插件),在最小化停机和确保一致性之间取得最佳平衡。

3.6 通道健康监控

Gateway 持续监控所有已启用通道的健康状态(src/gateway/channel-health-monitor.ts)。当通道断连时,Gateway 会按指数退避策略自动重连,退避参数定义在 src/gateway/server-channels.ts:

// src/gateway/server-channels.ts:18
const CHANNEL_RESTART_POLICY: BackoffPolicy = {
  initialMs: 5_000,      // 首次重试等待 5 秒
  maxMs: 5 * 60_000,     // 最大等待 5 分钟
  factor: 2,             // 指数因子 2
  jitter: 0.1,           // 10% 抖动
};
const MAX_RESTART_ATTEMPTS = 10;

健康状态快照通过 src/gateway/server/health-state.ts 管理,支持版本号递增和缓存,供 WebSocket 的 health 方法/事件和连接握手时的状态快照使用;HTTP 的 /health 探针只报告进程存活,不走这份快照。

3.7 Gateway 启动的完整子系统编排

"启动编排"这个话题、比它看起来深邃得多。一个有 10 个子系统的服务、启动顺序有 10! = 3,628,800 种可能的排列——但只有少数几种是正确的。错误的启动顺序会导致各种微妙的 bug——子系统 A 启动时依赖 B 的能力、但 B 还没起来;子系统 C 刚启动就被外部请求命中、但 D 依赖还没建立;子系统 E 启动时占用了资源、导致 F 无法启动。systemd 的 unit 依赖管理、Spring Framework 的 bean 依赖注入走的是"声明依赖、由框架计算拓扑顺序"的路子;OpenClaw 没有这样的依赖声明机制,而是在 startGatewayServer() 里按手写的固定顺序逐步启动——依赖关系由代码顺序本身表达,好处是一眼可读、易于调试,代价是新增子系统时要自己找准插入位置。

3.7.1 子系统启动顺序

src/gateway/server.impl.ts 的 startGatewayServer() 是 Gateway 进程的真正入口点。从其 import 列表可以看出 Gateway 编排了多少子系统:

// src/gateway/server.impl.ts:2-88(import 节选,有删减)
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { registerSkillsChangeListener } from "../agents/skills/refresh.js";
import { initSubagentRegistry } from "../agents/subagent-registry.js";
import { ensureControlUiAssetsBuilt /* , ... */ } from "../infra/control-ui-assets.js";
import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js";
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
import { createPluginRuntime } from "../plugins/runtime/index.js";
import { activateSecretsRuntimeSnapshot /* , ... */ } from "../secrets/runtime.js";
import { runSetupWizard } from "../wizard/setup.js";
import { startChannelHealthMonitor } from "./channel-health-monitor.js";
import { startGatewayConfigReloader } from "./config-reload.js";

启动顺序是精心设计的——后续子系统依赖前序子系统的输出:

flowchart TD
    subgraph Phase1["阶段 1:基础设施初始化"]
        Config["1. 配置加载<br/>readConfigFileSnapshot() + migrateLegacyConfig()"]
        Secrets["2. 认证引导与凭证快照<br/>prepareGatewayStartupConfig()"]
        SubagentReg["3. 子 Agent 注册表<br/>initSubagentRegistry()"]
    end
    subgraph Phase2["阶段 2:核心服务启动"]
        Plugins["4. 插件加载<br/>loadGatewayStartupPlugins()"]
        ControlUI["5. Control UI<br/>ensureControlUiAssetsBuilt()"]
        HTTP["6. HTTP/WS 服务<br/>createGatewayRuntimeState()"]
    end
    subgraph Phase3["阶段 3:监控与自动化"]
        Skills["7. 技能监听<br/>registerSkillsChangeListener()"]
        Heartbeat["8. 心跳运行器<br/>startHeartbeatRunner()"]
        Health["9. 健康监控<br/>startChannelHealthMonitor()"]
        UpdateCheck["10. 版本检查<br/>scheduleGatewayUpdateCheck()"]
    end
    subgraph Phase4["阶段 4:通道与自举"]
        Channels["11. 侧车与通道启动<br/>startGatewaySidecars()"]
        Boot["12. BOOT.md 自举<br/>gateway:startup → boot-md 钩子"]
    end
    Config --> Secrets --> SubagentReg
    SubagentReg --> Plugins --> ControlUI --> HTTP
    HTTP --> Skills --> Heartbeat --> Health --> UpdateCheck
    UpdateCheck --> Channels --> Boot
    style Phase1 fill:#ffebee,stroke:#e74c3c
    style Phase2 fill:#e3f2fd,stroke:#2196f3
    style Phase3 fill:#e8f5e9,stroke:#4caf50
    style Phase4 fill:#fff3e0,stroke:#ff9800
    style Config fill:#c0392b,color:#fff
    style Boot fill:#1e8449,color:#fff

图 3-6:Gateway 启动子系统编排顺序

3.7.2 BOOT.md 自举机制

src/gateway/boot.ts 实现了 OpenClaw 独有的自举机制——Gateway 启动后自动执行工作区中 BOOT.md 文件的指令:

// src/gateway/boot.ts:35
const BOOT_FILENAME = "BOOT.md";

export type BootRunResult =
  | { status: "skipped"; reason: "missing" | "empty" }
  | { status: "ran" }
  | { status: "failed"; reason: string };

function buildBootPrompt(content: string) {
  return [
    "You are running a boot check. Follow BOOT.md instructions exactly.",
    "",
    "BOOT.md:",
    content,
    "",
    "If BOOT.md asks you to send a message, use the message tool (action=send with channel + target).",
    "Use the `target` field (not `to`) for message tool destinations.",
    `After sending with the message tool, reply with ONLY: ${SILENT_REPLY_TOKEN}.`,
    `If nothing needs attention, reply with ONLY: ${SILENT_REPLY_TOKEN}.`,
  ].join("\n");
}

BOOT.md 的典型用途:

  • 启动时发送通知消息("Gateway 已重启")
  • 检查关键服务状态
  • 执行数据迁移
  • 恢复被中断的任务

自举会话使用独立的 session ID——generateBootSessionId() 把 ISO 时间戳里的 : . 换成 -、T 换成 _、去掉结尾 Z,再拼上 8 位 UUID 前缀,形如 boot-2026-03-31_02-15-08-431-a1b2c3d4;它借用主会话键运行,但事先快照主会话键的映射、跑完后恢复,不会干扰用户的主会话。

3.7.3 配置迁移与向后兼容

migrateLegacyConfig() 自动将旧版配置格式迁移到新版,确保 OpenClaw 版本升级时用户不需要手动修改配置文件。这是 Gateway 在启动阶段就处理的关键任务——在任何子系统使用配置之前完成迁移。

3.7.4 重启预延迟检查

scheduleGatewaySigusr1Restart() 在真正发出 SIGUSR1 之前,会先调用 setPreRestartDeferralCheck 注册的回调。回调返回的是待处理项的数量(0 表示可以安全重启),而不是布尔值:

// src/infra/restart.ts:102
/** The callback should return the number of pending items (0 = safe to restart). */
export function setPreRestartDeferralCheck(fn: () => number): void {
  preRestartCheck = fn;
}

如果有活跃的 Agent 运行、待发送的回复、排队的命令或进行中的任务,重启会被推迟,直到所有活跃任务完成或等待超时(gateway.reload.deferralTimeoutMs,默认 5 分钟)。这确保了一般情况下用户不会因为 Gateway 重启而丢失正在进行的对话。

3.7.5 凭证运行时快照

src/secrets/runtime.ts 中的凭证管理是 Gateway 安全架构的关键组件:

// src/gateway/server.impl.ts:467-469(调用处节选;prepare 是异步的,activate 是同步的)
const prepared = await prepareSecretsRuntimeSnapshot({ config });
if (params.activate) {
  activateSecretsRuntimeSnapshot(prepared);
}

凭证系统采用快照模式——系统解析配置和认证档案中的密钥引用后,生成一份(深拷贝的)运行时快照并整体激活,运行中的子系统读的都是当前激活的这一份。配置重载时,系统重新解析、生成新快照再整体切换。

这种设计确保了:

  1. 原子性:密钥更新不会导致部分子系统用旧密钥、部分用新密钥
  2. 一致性:新快照只有在全部密钥解析成功后才会被激活
  3. 降级保护:新快照解析失败时不切换,继续沿用上一个已知良好的快照(并发出 SECRETS_RELOADER_DEGRADED 事件)

3.8 Gateway 的事件系统

3.8.1 Agent 事件总线

src/infra/agent-events.ts 提供了全局的 Agent 事件总线,Gateway 通过 onAgentEvent 订阅所有 Agent 生命周期事件:

// src/gateway/server.impl.ts:935-949(节选)
agentUnsub = onAgentEvent(
  createAgentEventHandler({   // src/gateway/server-chat.ts
    broadcast,                // 推给所有 WebSocket 客户端
    broadcastToConnIds,       // 只推给指定连接
    nodeSendToSession,        // 推给订阅该会话的设备节点
    // ... agentRunSeq、chatRunState、toolEventRecipients 等
  }),
);

事件总线采用同步分发机制——事件处理器在事件发生时立即执行,确保状态更新的及时性。对于耗时的事件处理(如通道消息发送),处理器内部使用异步操作,避免阻塞事件总线。

3.8.2 会话生命周期事件

src/sessions/session-lifecycle-events.ts 提供了会话级别的事件:

// src/gateway/server.impl.ts:1065(节选)
onSessionLifecycleEvent((event) => {
  // event: { sessionKey, reason, parentSessionKey?, label?, displayName? }
  // pin 版本中的发出点是子 Agent 的派生(reason: "create")与状态变化(reason: "subagent-status")
  // Gateway 把它转成 "sessions.changed" 事件,只推给订阅了会话事件的连接
});

3.8.3 转录更新事件

src/sessions/transcript-events.ts 在对话转录发生变化时触发事件,Gateway 以 session.message 事件把新消息实时推送给订阅了该会话的 WebSocket 连接(如 Control UI)。

3.9 Gateway 的 Auth 系统

3.9.1 认证模式策略

认证模式的取值定义在 src/config/types.gateway.ts 的 GatewayAuthMode,src/gateway/auth-mode-policy.ts 负责在配置歧义时强制显式声明(assertExplicitGatewayAuthModeWhenBothConfigured()——同时配了 gateway.auth.token 和 gateway.auth.password 却没写 mode 时直接拒绝启动):

// src/config/types.gateway.ts:123
export type GatewayAuthMode = "none" | "token" | "password" | "trusted-proxy";
  • none:不做认证(仅适用于纯回环部署)
  • token:需要预共享的 API token
  • password:密码认证
  • trusted-proxy:认证交给前置的可信反向代理,Gateway 只信任 gateway.trustedProxies 列表里的来源

3.9.2 速率限制

src/gateway/auth-rate-limit.ts 实现了针对认证失败的内存滑动窗口限流:按{作用域, 客户端 IP}统计失败次数,默认 1 分钟内失败 10 次即锁定 5 分钟,回环地址默认豁免(本机 CLI 不会被锁在外面),用来防范暴力猜测凭据:

// src/gateway/auth-rate-limit.ts:95
export function createAuthRateLimiter(config?: RateLimitConfig): AuthRateLimiter;
// 限流分作用域计数:AUTH_RATE_LIMIT_SCOPE_DEFAULT / _SHARED_SECRET /
// _DEVICE_TOKEN / _HOOK_AUTH

3.10 与业界对比:Gateway 架构的独特性

3.10.1 为什么其他框架没有 Gateway?

大多数 Agent 框架(LangChain、CrewAI、AutoGen)是库——它们被导入到用户的应用代码中使用。用户需要自己搭建 Web 服务、管理进程生命周期、实现通道集成。

OpenClaw 选择了截然不同的路径——它是一个运行时系统,自带完整的应用服务器。这个设计决策带来了深远的影响:

优势:

  • 开箱即用的多通道支持(无需集成工作)
  • 统一的生命周期管理(daemon、热重载、优雅重启)
  • 内置的安全纵深(Auth、速率限制、凭证管理)
  • 完整的可观测性(事件总线、健康监控、日志子系统)

代价:

  • 更重的安装和部署(不是一个 pip install 就能用的库)
  • 更高的学习曲线(需要理解 Gateway 的配置和行为)
  • 更少的自定义灵活性(框架规定了应用结构)

3.10.2 Gateway 模式的适用场景

Gateway 模式最适合以下场景:

  1. 需要 7×24 运行:Agent 需要持续在线,随时响应来自多个通道的消息
  2. 多通道集成:同一个 Agent 需要同时在 Telegram、Discord、Slack 等平台提供服务
  3. 团队协作:多人共享同一个 Agent 系统,需要统一的认证和访问控制
  4. 生产环境运维:需要健康监控、自动重启、优雅升级等运维能力

对于一次性的脚本任务或嵌入到现有应用中的场景,库模式(如 LangChain)可能更合适。

3.10.3 Gateway 架构的设计哲学

Gateway 的设计体现了三个核心哲学:

编排优于执行:Gateway 不执行具体的 AI 推理或消息处理,它编排所有子系统的协同工作。这类似于操作系统内核——内核不运行应用程序,但它管理应用程序的执行环境。

配置优于代码:用户通过 JSON5 配置文件定义系统行为,而非编写代码。这降低了使用门槛,同时通过配置验证减少了出错概率。

安全内建于架构:安全不是插件或可选组件,而是 Gateway 启动序列的核心步骤。凭证快照、Auth 策略、速率限制都在第一时间被初始化。

3.11 小结:编排者的智慧

如果你只从本章记住一个概念,让它是这个:编排优于执行。

Gateway 本身不执行 AI 对话,不处理具体的通道协议,不管理特定模型的 API。就像交响乐团的指挥——他不演奏任何一件乐器,但没有他,乐团就只是一群各自为政的乐手。Gateway 的价值不在于它做了什么,而在于它让其他所有子系统能够协调一致地工作。

"编排优于执行"体现在控制平面与数据平面分离的设计上——Kubernetes 的 kube-apiserver 和 kubelet、Istio 的 istiod 和 Envoy、Kafka 的 Controller 和 Broker,都是同类分工的典型。OpenClaw 的 Gateway 把这种分工落在 AI 网关场景:它决定"怎么做"(降级、限流、重连、凭证切换),具体的 Provider 调用和通道协议解析交给下游子系统。

本章剖析了 Gateway 的五个核心面向——启动、服务、配置、守护、自愈:

  1. 多阶段启动:入口 → 配置 → 安全 → 子系统 → 自举,环环相扣,每一步都有明确的前置条件和失败兜底
  2. 双协议服务:HTTP 和 WebSocket 并驾齐驱,REST API 与 OpenAI 兼容端点比肩而立
  3. 配置即代码:JSON5 + 环境变量 + 文件包含 + 四种热重载模式,配置的灵活度接近编程语言
  4. 跨平台 Daemon:一个 GatewayService 接口统一了 systemd、launchd 和 schtasks 三个完全不同的世界
  5. 自愈能力:通道断线自动重连,需要重启时等活跃任务结束再重启,凭证解析失败时沿用上一个已知良好的快照——系统会自己照顾自己

最好的基础设施,是你感觉不到它存在的基础设施。 当 Gateway 正常运转时,你只会注意到 Agent 在流畅地工作——就像你不会注意到空气,直到它消失。

图 3-7:Gateway 与其他 Agent 框架对比

为了帮助读者理解 OpenClaw Gateway 的设计定位,下表将其与常见的 Agent 基础设施框架进行对比:

特性 OpenClaw Gateway LangServe Dify AutoGen Runtime
运行模式 长驻 Daemon(systemd/launchd/schtasks) 按请求的 Web 服务 容器化 Web 应用 进程内运行时
配置格式 JSON5(支持注释、尾逗号、环境变量替换、文件包含) Python 代码 Web UI / YAML Python 代码
热重载 四种模式(off / restart / hot / hybrid),字段级 diff 自动决策 无(需重启) 部分热更新 无
多通道支持 内置 Telegram、Discord、Slack、Signal、WhatsApp、iMessage + 插件扩展 无(HTTP only) 有限通道支持 无
OpenAI API 兼容 原生 /v1/chat/completions 和 /v1/responses 端点 自定义端点 部分兼容 无 HTTP 接口
跨平台 Daemon 统一 GatewayService 接口适配三大平台 需自行配置 Docker 依赖 无
优雅重启 重启前检查活跃任务,延迟重启直到任务完成或超时 无 滚动更新 不适用
通道健康监控 指数退避自动重连(5s→5min,最多 10 次),陈旧事件检测 无 基本健康检查 无
凭证管理 运行时快照整体切换,解析失败沿用上一个已知良好快照 环境变量 数据库存储 环境变量
自举机制 BOOT.md — 启动时自动执行初始化指令 无 无 无

Gateway 是编排者,但编排者需要乐手。下一章,我们走进乐池,深入 Provider 抽象层——看 OpenClaw 如何在几十个模型提供商之间切换,尽量不让模型故障打扰到用户。

🛠️ 试一试:启动一个最简 Gateway

读完本章后,你可以在 5 分钟内启动一个真实的 Gateway 实例:

# 1. 安装 OpenClaw(如果尚未安装)
npm install -g openclaw

# 2. 初始化配置与工作区(生成 ~/.openclaw/openclaw.json,并写入 gateway.mode=local;
#    未设 gateway.mode=local 时 gateway run 会拒绝启动)
openclaw setup

# 3. 设置 API Key 与网关 token 环境变量(API Key 也可以写进 openclaw.json 的 env 段)
#    默认模型是 anthropic/claude-opus-4-6(src/agents/defaults.ts);换别家要另设 agents.defaults.model
export ANTHROPIC_API_KEY="sk-ant-..."  # ← 替换为你的真实 API Key
#    默认认证模式是 token;不设的话启动时会自动生成一个
export OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 24)"

# 4. 前台启动 Gateway(想装成系统服务,用 openclaw gateway install——安装后即启动,
#    之后用 gateway start/stop/restart 管理;第 1 章推荐的 openclaw onboard --install-daemon 一步到位)
openclaw gateway run

# 5. 测试:向 Gateway 发送一条消息(默认端口 18789)
#    前提:OpenAI 兼容端点默认关闭,先在配置里打开
#    gateway.http.endpoints.chatCompletions.enabled = true,然后重启网关
curl http://localhost:18789/v1/chat/completions \
  -H "Authorization: Bearer $OPENCLAW_GATEWAY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model": "openclaw", "messages": [{"role": "user", "content": "Hello!"}]}'
# model 字段在 OpenClaw 里是 *agent target*(openclaw / openclaw/default /
# openclaw/<agentId>),不是 provider 的模型 id

💡 观察要点:启动时注意控制台日志中的多阶段启动顺序——配置加载 → 安全初始化 → 子系统编排 → 服务就绪。这正是本章 3.7 节剖析的启动序列在真实运行中的体现。


思考题

  1. 概念理解:Gateway 的多阶段启动序列为什么要求每一步都有明确的前置条件?如果允许并行启动所有子系统,可能会导致什么问题?
  2. 实践应用:如果你需要将 OpenClaw Gateway 部署到一个资源受限的嵌入式设备(如树莓派),你会对启动流程和配置系统做哪些裁剪?
  3. 开放讨论:Gateway 采用了"编排优于执行"的设计哲学。在你的工作经验中,是否遇到过"编排层太厚"反而成为瓶颈的情况?如何判断编排层的合理边界?

水平扩展的边界:OpenClaw 当前是单进程常驻,没有内置的多实例协同能力。小规模部署下足够,面向多租户或多实例场景,恐怕需要外接 Redis、etcd 之类做状态同步——这是作者的推测,pin 版本里没有相关实现。单实例优先的设计,换来的是启动快、调试简单、故障域清晰;拆成多服务会引入网络开销、部署复杂度和分布式一致性问题。单体还是分布式没有绝对答案,只有对当前规模最合适的答案。

📚 推荐阅读