主题
OpenClaw 源码解读(二):Gateway 网关系统
模块:
src/gateway/| 版本:2026.3.2
一、模块概览
Gateway 是 OpenClaw 的中央控制平面——一个运行在本地的 HTTP + WebSocket 混合服务器。所有消息渠道(WhatsApp/Telegram/Discord 等)、AI Agent、定时任务、Control UI 和移动端 App 都通过 Gateway 协调工作。
Gateway 的核心职责:
- WebSocket RPC 服务 — 提供 JSON-RPC 风格的双向通信协议,供 CLI、Control UI、移动 App 调用
- 多渠道消息路由 — 启动和管理所有消息渠道的连接生命周期
- 认证与安全 — Token/密码/Tailscale/可信代理 四种认证模式 + 速率限制
- 配置热重载 — 监听配置文件变化,无需重启即可应用大部分配置更新
- Agent 运行编排 — 协调 AI Agent 的启动、消息流转、事件广播
- 插件宿主 — 加载和运行第三方插件,注入自定义 Gateway 方法
- 节点注册表 — 管理连接的客户端节点(CLI/App/浏览器),跟踪能力和权限
- 定时任务调度 — 运行 Cron 任务系统
- 健康监控 — 周期性检查渠道连接状态,对外暴露健康状态
- TLS/Tailscale 暴露 — 支持 HTTPS 和 Tailscale 网络访问
涉及文件清单(按启动顺序):
| 文件 | 职责 | 行数 |
|---|---|---|
server.ts | 公共导出门面(barrel re-export) | 3 |
server.impl.ts | 核心启动编排(startGatewayServer) | ~994 |
auth.ts | 认证逻辑(4 种模式) | ~487 |
auth-rate-limit.ts | 滑动窗口速率限制器 | ~232 |
credentials.ts | 凭据解析(config + env) | ~120 |
net.ts | 网络工具(IP 检测、绑定地址) | ~180 |
boot.ts | 启动时 BOOT.md 检查 | ~203 |
client.ts | Gateway WebSocket 客户端 | ~450 |
node-registry.ts | 节点注册表(客户端管理) | ~200 |
server-runtime-state.ts | 运行时状态创建(HTTP/WS/Canvas) | ~300 |
server-runtime-config.ts | 运行时配置解析 | ~200 |
server-ws-runtime.ts | WebSocket 处理器挂载 | ~44 |
server-channels.ts | 渠道管理器(启停渠道) | ~180 |
server-chat.ts | Agent 事件处理器 | ~150 |
server-cron.ts | Cron 服务构建 | ~80 |
server-methods.ts | 核心 RPC 方法定义 | ~600 |
server-methods-list.ts | 方法名注册表 | ~70 |
server-close.ts | 优雅关闭编排 | ~120 |
server-plugins.ts | 插件加载 | ~100 |
server-startup.ts | 边车服务启动(Browser Control + 渠道) | ~80 |
config-reload.ts | 配置文件监听 + 热重载 | ~200 |
protocol/index.ts | WebSocket 协议定义(JSON Schema) | ~700+ |
events.ts | Gateway 事件类型定义 | 7 |
Gateway 网关系统/01-infographic-gateway-overview-1775150688334.png)
二、启动流程全景图
当执行 openclaw gateway run --port 18789 时,startGatewayServer() 函数编排了以下完整启动序列:
startGatewayServer(port=18789)
│
├─ 阶段1: 配置验证与迁移
│ ├─ readConfigFileSnapshot()
│ ├─ 检测旧版配置 → migrateLegacyConfig() 自动迁移
│ ├─ 二次验证配置合法性
│ └─ applyPluginAutoEnable() 自动启用新插件
│
├─ 阶段2: 密钥与认证
│ ├─ activateRuntimeSecrets() → 解析 ${{ secret.xxx }} 引用
│ ├─ ensureGatewayStartupAuth() → 确保有认证 token
│ │ └─ 如果没有 token → 自动生成并持久化
│ └─ 二次 activateRuntimeSecrets() → 最终密钥快照
│
├─ 阶段3: 核心基础设施
│ ├─ initSubagentRegistry() → 子 Agent 注册表
│ ├─ loadGatewayPlugins() → 加载插件 + 扩展方法列表
│ ├─ resolveGatewayRuntimeConfig() → 解析运行时配置
│ ├─ createGatewayAuthRateLimiters() → 创建速率限制器
│ └─ createDefaultDeps() → CLI 依赖注入容器
│
├─ 阶段4: 创建运行时状态
│ ├─ createGatewayRuntimeState()
│ │ ├─ 创建 HTTP 服务器(含 TLS)
│ │ ├─ 挂载 HTTP 路由(/health, /v1/chat/completions, Control UI 等)
│ │ ├─ 创建 WebSocket 服务器(ws.Server)
│ │ └─ 创建广播系统、Agent 运行状态、Canvas Host
│ ├─ new NodeRegistry() → 节点注册表
│ └─ createNodeSubscriptionManager() → 节点订阅管理
│
├─ 阶段5: 启动服务
│ ├─ startGatewayDiscovery() → mDNS/Bonjour 广播 + 广域发现
│ ├─ setSkillsRemoteRegistry() → 远程技能注册
│ ├─ startGatewayMaintenanceTimers() → 维护定时器
│ │ ├─ tickInterval: 30s Tick 心跳
│ │ ├─ healthInterval: 刷新健康状态
│ │ └─ dedupeCleanup: 去重缓存清理
│ ├─ onAgentEvent() → Agent 事件订阅
│ ├─ startHeartbeatRunner() → 心跳运行器
│ ├─ startChannelHealthMonitor() → 渠道健康监控
│ └─ cron.start() → Cron 调度器启动
│
├─ 阶段6: 挂载 WebSocket 处理器
│ ├─ attachGatewayWsHandlers()
│ │ └─ 注册所有 RPC 方法 + 事件广播逻辑
│ └─ logGatewayStartup() → 打印启动信息
│
├─ 阶段7: 后台服务
│ ├─ scheduleGatewayUpdateCheck() → 版本更新检查
│ ├─ startGatewayTailscaleExposure() → Tailscale 暴露
│ ├─ startGatewaySidecars() → 边车启动
│ │ ├─ Browser Control Server
│ │ ├─ startChannels() → 启动所有渠道连接
│ │ └─ 运行 gateway_start 插件钩子
│ └─ startGatewayConfigReloader() → 配置文件监听
│
└─ 返回 { close: async () => { ... } }
└─ 优雅关闭: 停止所有服务 → 断开连接 → 释放资源Gateway 网关系统/02-infographic-startup-sequence-1775150689094.png)
三、核心子系统深度解读
3.1 认证系统 (auth.ts)
Gateway 支持四种认证模式,通过 gateway.auth.mode 配置切换:
┌────────────────────────────────────────────────────────────────┐
│ 认证模式优先级链 │
│ │
│ 请求到达 → ❶ trusted-proxy 模式? │
│ │ → 检查代理 IP + 用户头 → 通过/拒绝 │
│ │ │
│ ❷ none 模式? │
│ │ → 直接放行 │
│ │ │
│ ❸ 速率限制检查 │
│ │ → 被限制?返回 429 + retryAfterMs │
│ │ │
│ ❹ Tailscale 认证(WS Control UI 专用) │
│ │ → Whois 验证 → 通过/继续 │
│ │ │
│ ❺ token 模式? │
│ │ → safeEqualSecret(提供的token, 配置的token) │
│ │ │
│ ❻ password 模式? │
│ │ → safeEqualSecret(提供的密码, 配置的密码) │
│ │ │
│ ❼ 全部未匹配 → 返回 "unauthorized" │
└────────────────────────────────────────────────────────────────┘认证模式详解
| 模式 | 配置值 | 凭据来源 | 典型场景 |
|---|---|---|---|
none | gateway.auth.mode: "none" | 无需凭据 | 仅本地开发 |
token | gateway.auth.mode: "token" | config token / env OPENCLAW_GATEWAY_TOKEN | 默认模式 |
password | gateway.auth.mode: "password" | config password | 多用户共享 |
trusted-proxy | gateway.auth.mode: "trusted-proxy" | 反向代理注入的用户头 | Nginx/Caddy 前置 |
时序恒定比较 (safeEqualSecret)
typescript
// auth.ts 中的关键调用
if (!safeEqualSecret(connectAuth.token, auth.token)) {
limiter?.recordFailure(ip, rateLimitScope);
return { ok: false, reason: "token_mismatch" };
}safeEqualSecret() 使用了 Node.js 的 crypto.timingSafeEqual(),确保无论比较的哪个位置不匹配,函数执行时间都相同。这是防止时序侧信道攻击的标准做法——攻击者无法通过响应时间推断 token 的正确前缀。
本地请求检测 (isLocalDirectRequest)
typescript
export function isLocalDirectRequest(req, trustedProxies, allowRealIpFallback): boolean {
const clientIp = resolveRequestClientIp(req, trustedProxies, allowRealIpFallback);
if (!isLoopbackAddress(clientIp)) return false;
const hasForwarded = Boolean(req.headers["x-forwarded-for"] || ...);
const remoteIsTrustedProxy = isTrustedProxyAddress(req.socket?.remoteAddress, trustedProxies);
return isLocalishHost(req.headers?.host) && (!hasForwarded || remoteIsTrustedProxy);
}判断一个请求是否来自本机:
- 客户端 IP 必须是回环地址(127.0.0.1 / ::1)
- Host 头必须是本地(localhost / 127.0.0.1)
- 如果有代理头(X-Forwarded-For),发送方必须是可信代理
Tailscale 认证
Tailscale 认证是一个三步验证过程:
❶ 从请求头读取 Tailscale-User-Login
❷ 确认请求来自 Tailscale 代理(回环地址 + X-Forwarded-* 头)
❸ 通过 Tailscale Whois API 验证用户身份
→ 比较 Whois 返回的 login 与请求头中的 login 是否一致这种双重验证防止了攻击者伪造 Tailscale 用户头。
Gateway 网关系统/03-infographic-auth-chain-1775150690240.png)
3.2 速率限制器 (auth-rate-limit.ts)
算法:滑动窗口 + 锁定期
窗口大小: 60s
┌──────────────────────────────┐
│ 失败1 失败2 ... 失败10 │
└──────────────────────────────┘
↑
超过 maxAttempts=10
│
▼
┌──────────────────────────────────────────┐
│ 锁定期: 300s (5分钟) │
│ 所有请求被拒绝,返回 retryAfterMs │
└──────────────────────────────────────────┘
↑
锁定期过后
│
▼
计数器重置,重新开始数据结构
typescript
interface RateLimitEntry {
attempts: number[]; // 失败时间戳数组(滑动窗口)
lockedUntil?: number; // 锁定到期时间
}
// 存储结构: Map<"scope:ip", RateLimitEntry>
// 示例: "shared-secret:192.168.1.5" → { attempts: [1711234567890, ...] }设计亮点
- 回环地址豁免 —
127.0.0.1和::1默认不受速率限制(exemptLoopback: true),确保本地 CLI 永远不会被锁定 - 作用域隔离 — 不同认证方式(
shared-secret/device-token/hook-auth)使用独立计数器 - 自动清理 —
setInterval每 60 秒清理过期条目,.unref()确保定时器不阻止进程退出 - 成功重置 — 认证成功后立即调用
reset(ip)清除该 IP 的所有失败记录
Gateway 网关系统/04-infographic-rate-limiter-1775150691021.png)
3.3 启动编排 (server.impl.ts)
startGatewayServer() 是整个项目中最长最复杂的函数(~760 行),它是一个过程式编排器。
配置迁移与验证(第 250-296 行)
读取配置快照 → 检测旧版字段?
│ │
├─ 有旧版字段 ├─ 无旧版字段
│ │ │
│ Nix 模式? 继续
│ ├─ 是 → 抛出错误(Nix 配置不可变)
│ └─ 否 → migrateLegacyConfig()
│ │
│ 写入迁移后的配置文件
│ │
│ 二次读取验证
│
└─ 配置无效?→ 抛出错误 + 提示运行 openclaw doctor认证引导(第 381-406 行)
typescript
const authBootstrap = await ensureGatewayStartupAuth({
cfg: cfgAtStart,
env: process.env,
authOverride: opts.auth,
tailscaleOverride: opts.tailscale,
persist: true,
});如果没有配置认证 token,ensureGatewayStartupAuth 会自动生成一个随机 token 并尝试持久化到配置文件。这确保 Gateway 启动后不会处于完全无认证的状态。
运行时状态创建(第 519-561 行)
createGatewayRuntimeState() 是 Gateway 的"基础设施工厂",它创建:
| 创建的对象 | 类型 | 用途 |
|---|---|---|
httpServer | http.Server / https.Server | HTTP 服务器(含 TLS 支持) |
wss | WebSocket.Server | WebSocket 服务器 |
clients | Set<GatewayWsClient> | 已连接的 WebSocket 客户端集合 |
broadcast | (event, payload) => void | 向所有客户端广播事件 |
canvasHost | CanvasHostServer | Canvas Host(A2UI 运行环境) |
agentRunSeq | Map<string, number> | Agent 运行序号追踪 |
dedupe | DedupeStore | 消息去重存储 |
chatRunState | ChatRunState | 聊天运行状态(中止控制器、缓冲区等) |
维护定时器(第 645-662 行)
typescript
const { tickInterval, healthInterval, dedupeCleanup } = startGatewayMaintenanceTimers({
broadcast, // 广播函数
nodeSendToAllSubscribed, // 向订阅节点发送
getPresenceVersion, // 在线状态版本
getHealthVersion, // 健康状态版本
refreshGatewayHealthSnapshot, // 刷新健康快照
dedupe, // 去重存储
chatAbortControllers, // 中止控制器
...
});三个定时器的职责:
| 定时器 | 间隔 | 职责 |
|---|---|---|
tickInterval | ~30s | 向所有客户端发送 tick 心跳,检测静默断连 |
healthInterval | ~60s | 刷新渠道健康快照,广播状态变化 |
dedupeCleanup | ~60s | 清理过期的去重条目,释放内存 |
WebSocket 处理器挂载(第 751-823 行)
typescript
attachGatewayWsHandlers({
wss, clients, port,
resolvedAuth,
gatewayMethods, // RPC 方法列表
extraHandlers: { // 扩展处理器
...pluginRegistry.gatewayHandlers,
...execApprovalHandlers,
...secretsHandlers,
},
context: { // 请求上下文(所有 RPC 方法共享)
deps, cron, cronStorePath,
execApprovalManager,
broadcast, nodeRegistry,
chatAbortControllers,
...
},
});context 对象是依赖注入容器——所有 RPC 方法通过 context 访问 Gateway 内部状态,而不是直接导入全局变量。
优雅关闭(第 945-993 行)
typescript
const close = createGatewayCloseHandler({
bonjourStop, // 停止 mDNS 广播
tailscaleCleanup, // 清理 Tailscale 暴露
canvasHost, // 关闭 Canvas Host
stopChannel, // 停止渠道
pluginServices, // 停止插件服务
cron, // 停止 Cron
heartbeatRunner, // 停止心跳
clients, // 断开所有客户端
configReloader, // 停止配置监听
browserControl, // 停止浏览器控制服务
wss, httpServer, // 关闭 HTTP/WS 服务器
...
});关闭时还会触发 gateway_stop 插件钩子,让插件有机会做清理工作。
3.4 配置热重载 (config-reload.ts)
Gateway 监听 openclaw.json 文件变化,能在不重启的情况下应用大部分配置更新。
配置文件变化检测
│
├─ 计算变更计划 (ConfigReloadPlan)
│ ├─ 哪些字段变化了?
│ ├─ 变化的字段需要热重载还是重启?
│ └─ 输出: { hotReloadable: [...], requiresRestart: [...] }
│
├─ 全部可热重载?
│ ├─ 是 → applyHotReload(plan, nextConfig)
│ │ ├─ 更新 Hooks 配置
│ │ ├─ 重建心跳运行器
│ │ ├─ 重建 Cron 服务
│ │ ├─ 启停渠道
│ │ └─ 更新渠道健康监控间隔
│ │
│ └─ 有需要重启的字段?
│ └─ requestGatewayRestart(plan, nextConfig)
│ └─ 发送 SIGUSR1 信号触发优雅重启可热重载的配置项示例:
gateway.hooks— Webhook 钩子 URLheartbeat— 心跳间隔cron— 定时任务- 渠道开关 — 启用/禁用特定渠道
需要重启的配置项示例:
gateway.auth— 认证模式变化gateway.bind— 绑定地址变化gateway.tls— TLS 证书变化
Gateway 网关系统/05-infographic-config-reload-1775150691904.png)
3.5 WebSocket 协议 (protocol/index.ts)
Gateway 使用自定义的 JSON-RPC 风格协议进行通信。
帧类型
typescript
// 请求帧: 客户端 → Gateway
type RequestFrame = {
id: string; // 请求 ID(用于匹配响应)
method: string; // RPC 方法名
params?: unknown; // 方法参数
};
// 响应帧: Gateway → 客户端
type ResponseFrame = {
id: string; // 对应请求 ID
result?: unknown; // 成功结果
error?: ErrorShape; // 错误信息
final?: boolean; // 是否是最终响应(流式场景)
};
// 事件帧: Gateway → 客户端(主动推送)
type EventFrame = {
event: string; // 事件名
seq: number; // 序列号(检测丢失)
payload?: unknown; // 事件数据
};连接握手流程
客户端 Gateway
│ │
├──── WebSocket 连接 ────────────────→│
│ │
│←──── challenge { nonce } ──────────┤ ❶ 服务端发送挑战
│ │
├──── connect { ──────→│ ❷ 客户端响应
│ auth: { token }, │
│ client: { name, version }, │
│ device: { │ 设备认证(可选)
│ id, publicKey, │
│ signature, nonce │
│ }, │
│ scopes: ["operator.admin"] │
│ } │
│ │
│←──── hello.ok { ─────┤ ❸ 认证成功
│ connId, sessionKey, │
│ version, features, │
│ canvasHost │
│ } │
│ │
│←──── events (广播) ────────────────┤ ❹ 开始接收事件
│ │
├──── request { method, params } ───→│ ❺ 发送 RPC 请求
│ │
│←──── response { result } ───────┤ ❻ 接收 RPC 响应Gateway 网关系统/06-infographic-ws-protocol-1775150692595.png)
已注册的 RPC 方法(部分列表)
协议层定义了丰富的 RPC 方法,每个方法都有 JSON Schema 验证:
| 方法分组 | 方法名 | 功能 |
|---|---|---|
| Agent | agent, agent.wait, agent.identity | Agent 对话、等待、身份 |
| Chat | chat.send, chat.abort, chat.history, chat.inject | 聊天发送/中止/历史/注入 |
| Config | config.get, config.set, config.patch, config.apply | 配置 CRUD |
| Channels | channels.status, channels.logout, channels.login | 渠道状态/登出/登录 |
| Cron | cron.list, cron.add, cron.remove, cron.run | 定时任务管理 |
| Memory | memory.search, memory.status | 记忆搜索/状态 |
| Models | models.list, models.status | 模型列表/状态 |
| Sessions | sessions.list, sessions.patch | 会话管理 |
| Agents | agents.list, agents.create, agents.update, agents.delete | 多 Agent 管理 |
| Nodes | nodes.list, nodes.invoke, nodes.subscribe | 节点管理与远程调用 |
| Exec | exec-approval.* | 命令执行审批 |
| Secrets | secrets.reload, secrets.resolve | 密钥管理 |
| Device | device-pair.*, device-token.* | 设备配对与令牌 |
3.6 Gateway 客户端 (client.ts)
GatewayClient 是一个自动重连的 WebSocket 客户端,被 CLI 和其他消费者使用来连接 Gateway。
核心状态机
┌─────────┐ start() ┌──────────┐
│ INITIAL │────────────────→│ CONNECT │
└─────────┘ └────┬─────┘
│
WebSocket open
│
▼
┌──────────┐ challenge 到达
│ WAITING │──────────────────→ sendConnect()
│ CHALLENGE│ │
└──────────┘ │
认证成功
│
▼
┌───────────┐
│ CONNECTED │
│ (活跃) │
└─────┬─────┘
│
连接断开 │ stop()
▼
┌───────────┐
│ RECONNECT │
│ (退避等待)│
└─────┬─────┘
│
backoff 到期
│
▼
回到 CONNECT安全检查
typescript
// 阻止所有明文 ws:// 到非回环地址的连接(CWE-319, CVSS 9.8)
if (!isSecureWebSocketUrl(url, { allowPrivateWs })) {
const error = new Error(
`SECURITY ERROR: Cannot connect to "${displayHost}" over plaintext ws://. ` +
"Both credentials and chat data would be exposed to network interception. ...",
);
this.opts.onConnectError?.(error);
return;
}只有以下连接被允许:
ws://127.0.0.1:*— 本地回环(安全)ws://localhost:*— 本地回环(安全)wss://*— TLS 加密(安全)ws://*+OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1— 显式破窗(不推荐)
Gateway 网关系统/07-infographic-client-state-machine-1775150693325.png)
设备认证
客户端连接时可以进行设备级认证:
typescript
const payload = buildDeviceAuthPayloadV3({
deviceId: identity.deviceId, // 设备唯一 ID
clientId: "gateway-client", // 客户端类型
role: "operator", // 角色
scopes: ["operator.admin"], // 权限范围
signedAtMs: Date.now(), // 签名时间
token: authToken, // 认证 token
nonce, // 服务端挑战随机数
platform: "darwin", // 平台
});
const signature = signDevicePayload(identity.privateKeyPem, payload);这个流程使用 公私钥签名 + 挑战-响应:
- Gateway 发送随机 nonce
- 客户端用设备私钥对
(deviceId + token + nonce + ...)签名 - Gateway 用设备公钥验证签名
- 验证通过后存储设备 token,后续连接可以跳过完整签名流程
自动重连与退避
连接断开
│
├─ backoffMs = 1000 (初始 1 秒)
│
├─ 第 1 次重连失败 → backoffMs = 2000
├─ 第 2 次重连失败 → backoffMs = 4000
├─ ...
└─ 最大退避 → backoffMs = 30000 (30 秒)断连时清理设备 token
typescript
ws.on("close", (code, reason) => {
// 仅在设备 token 认证失败时清理本地 token
if (
code === 1008 &&
reasonText.includes("device token mismatch") &&
!this.opts.token &&
!this.opts.password
) {
clearDeviceAuthToken({ deviceId, role });
clearDevicePairing(deviceId);
}
this.scheduleReconnect();
});3.7 节点注册表 (node-registry.ts)
NodeRegistry 管理所有通过 WebSocket 连接到 Gateway 的客户端节点(CLI 实例、移动 App、Control UI 等)。
typescript
class NodeRegistry {
private nodesById = new Map<string, NodeSession>(); // nodeId → 节点信息
private nodesByConn = new Map<string, string>(); // connId → nodeId
private pendingInvokes = new Map<string, PendingInvoke>(); // 待处理的远程调用
register(client, opts) { ... } // 节点连接时注册
unregister(connId) { ... } // 节点断开时注销
sendEvent(nodeId, event, payload) // 向指定节点发送事件
invoke(nodeId, command, params) // 远程调用节点上的命令
}每个连接的客户端被注册为一个 NodeSession:
typescript
type NodeSession = {
nodeId: string; // 节点 ID(设备 ID 或客户端 ID)
connId: string; // WebSocket 连接 ID
client: GatewayWsClient; // WebSocket 客户端引用
displayName?: string; // 显示名称
platform?: string; // 平台(darwin/linux/win32/ios/android)
caps: string[]; // 能力列表(如 "screen-snapshot")
commands: string[]; // 可执行命令列表
permissions?: Record<string, boolean>; // 权限
connectedAtMs: number; // 连接时间
};远程调用机制: Gateway 可以向连接的节点发送命令(如让移动 App 截屏),通过 invoke() 发起请求,等待节点响应。
3.8 Boot 机制 (boot.ts)
Gateway 启动时会检查工作目录下的 BOOT.md 文件,如果存在则让 Agent 执行其中的指令。
Gateway 启动
│
├─ loadBootFile(workspaceDir) → 读取 BOOT.md
│ ├─ 文件不存在 → { status: "missing" } → 跳过
│ ├─ 文件为空 → { status: "empty" } → 跳过
│ └─ 文件有内容 → { status: "ok", content: "..." }
│
├─ snapshotMainSessionMapping() → 快照当前会话映射
│
├─ buildBootPrompt() → 构建 Boot 提示词
│ "You are running a boot check. Follow BOOT.md instructions exactly."
│ "BOOT.md: [文件内容]"
│ "If nothing needs attention, reply with ONLY: __SILENT__"
│
├─ agentCommand({ message, deliver: false }) → Agent 执行
│
└─ restoreMainSessionMapping() → 恢复会话映射(不污染主会话)设计洞察:
- Boot 运行使用独立的 session ID(
boot-2026-03-25_12-00-00-abc12345),不会污染用户的主会话历史 - 运行前快照会话映射,运行后恢复——即使 Agent 修改了会话数据,也会被回滚
deliver: false确保 Boot 产生的消息不会发送到消息渠道
四、关键设计模式
4.1 过程式编排器(Procedural Orchestrator)
startGatewayServer() 采用了一种"大函数编排"模式,而不是将每个步骤封装到单独的类中。这种设计的优劣:
优势:
- 启动流程一目了然,从上到下就是执行顺序
- 所有依赖关系通过局部变量的作用域自然表达
- 闭包可以直接访问启动阶段创建的所有对象
劣势:
- 函数体非常长(~760 行)
- 难以对启动流程的某个阶段做单元测试
缓解措施: 每个逻辑子步骤都被提取到独立模块(server-runtime-state.ts、server-channels.ts 等),编排器只负责调用顺序和依赖传递。
4.2 广播/订阅模式
Gateway 的事件分发使用了一个简单但高效的广播系统:
typescript
const broadcast = (event: string, payload: unknown, opts?) => {
for (const client of clients) {
if (opts?.dropIfSlow && client.bufferedAmount > threshold) continue;
client.send(JSON.stringify({ event, seq: nextSeq++, payload }));
}
};dropIfSlow 机制: 当某个客户端的发送缓冲区积压严重时,跳过该客户端的事件推送,避免慢客户端拖慢整个 Gateway。
4.3 请求上下文注入
所有 RPC 方法通过 GatewayRequestContext 共享同一个上下文对象:
typescript
type GatewayRequestContext = {
deps: CliDeps;
cron: CronService;
nodeRegistry: NodeRegistry;
broadcast: BroadcastFn;
execApprovalManager: ExecApprovalManager;
// ... 共 30+ 个字段
};这种"宽接口"设计让每个 RPC 方法都能访问 Gateway 的全部内部状态。虽然违反了接口最小化原则,但在单进程服务器中是务实的选择——避免了为每个方法定义独立的依赖接口。
4.4 最小测试 Gateway 模式
typescript
const minimalTestGateway =
process.env.VITEST === "1" &&
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY === "1";测试中可以启动一个"最小 Gateway"——跳过 mDNS 发现、渠道连接、插件加载、Cron 启动等重量级操作,只保留 HTTP/WS 核心和认证系统。这大大加快了测试速度。
五、数据流图
5.1 CLI 发送消息到 WhatsApp 的完整链路
用户: openclaw message send --target +155 --message "Hi"
│
├─ CLI 连接 Gateway WebSocket
│ └─ GatewayClient.start() → 握手 → 认证
│
├─ CLI 发送 RPC 请求
│ { method: "chat.send", params: { message: "Hi", target: "+155" } }
│
├─ Gateway 处理 chat.send
│ ├─ resolveSessionKeyForRun() → 确定会话 Key
│ ├─ agentCommand({ message: "Hi", deliver: true })
│ │ ├─ Agent 处理消息 → 生成回复
│ │ └─ 发布 AgentEvent(streaming 事件流)
│ │
│ ├─ createAgentEventHandler 捕获事件
│ │ ├─ broadcast("chat.delta", { ... }) → 推送给所有 WS 客户端
│ │ └─ nodeSendToSession() → 推送给订阅该会话的节点
│ │
│ └─ Agent 完成 → deliver 到 WhatsApp 渠道
│ └─ sendMessageWhatsApp({ to: "+155", text: "Agent 回复" })
│
└─ CLI 收到 chat.complete 事件 → 显示结果Gateway 网关系统/08-infographic-message-flow-1775150694246.png)
5.2 配置热重载流程
用户修改 ~/.openclaw/openclaw.json
│
├─ fs.watch 检测到变化
│
├─ readConfigFileSnapshot() → 读取新配置
│
├─ 计算 ConfigReloadPlan
│ { changed: ["heartbeat.interval", "cron.jobs[0].schedule"] }
│
├─ 全部可热重载 → onHotReload(plan, nextConfig)
│ ├─ activateRuntimeSecrets(nextConfig) → 更新密钥快照
│ ├─ applyHotReload()
│ │ ├─ heartbeatRunner.updateConfig(nextConfig)
│ │ ├─ cron.stop() → buildGatewayCronService(nextConfig) → cron.start()
│ │ └─ 更新 channelHealthMonitor 间隔
│ └─ 如果热重载失败 → 回滚密钥快照
│
└─ 有需要重启的字段 → onRestart(plan, nextConfig)
└─ SIGUSR1 → Gateway 优雅重启六、安全架构
6.1 防护层次
| 层次 | 防护手段 | 位置 |
|---|---|---|
| 传输层 | 强制 WSS / 回环地址限制 | client.ts:121 |
| 认证层 | Token / 密码 / Tailscale / 可信代理 | auth.ts |
| 速率限制 | 滑动窗口 + 锁定期 | auth-rate-limit.ts |
| 时序攻击 | crypto.timingSafeEqual | security/secret-equal.js |
| 设备认证 | 公私钥签名 + 挑战响应 | client.ts:274-298 |
| 代理验证 | 可信代理 IP 白名单 + 必要头检查 | auth.ts:321-358 |
| Tailscale | Whois 双重验证 | auth.ts:183-214 |
6.2 SSRF 防护
Gateway 的 HTTP 端点和 WebSocket 连接都考虑了 SSRF 防护:
- 代理头(
X-Forwarded-For)仅在发送方是可信代理时才被采信 isLocalDirectRequest()综合判断请求是否真正来自本机X-Real-IP头默认不信任,需要allowRealIpFallback显式开启
Gateway 网关系统/09-infographic-security-layers-1775150694988.png)
七、调试入口推荐
| 断点位置 | 观察目标 |
|---|---|
server.impl.ts:232 (startGatewayServer 入口) | 整体启动流程 |
server.impl.ts:250 (readConfigFileSnapshot) | 配置加载 |
server.impl.ts:382 (ensureGatewayStartupAuth) | 认证引导 |
server.impl.ts:537 (createGatewayRuntimeState) | HTTP/WS 创建 |
auth.ts:364 (authorizeGatewayConnect) | 认证判断逻辑 |
auth-rate-limit.ts:141 (check) | 速率限制判断 |
client.ts:107 (start) | 客户端连接流程 |
client.ts:234 (sendConnect) | 客户端认证握手 |
node-registry.ts:43 (register) | 节点注册 |
boot.ts:138 (runBootOnce) | Boot 机制 |
config-reload.ts → onHotReload | 配置热重载 |
调试命令:
bash
# 启动 Gateway(无渠道连接,适合调试核心逻辑)
OPENCLAW_SKIP_CHANNELS=1 pnpm gateway:dev
# 运行 Gateway 认证测试
pnpm vitest run --config vitest.gateway.config.ts src/gateway/auth.test.ts
# 运行 Gateway 启动测试
pnpm vitest run --config vitest.gateway.config.ts src/gateway/boot.test.ts
# 运行速率限制测试
pnpm vitest run --config vitest.gateway.config.ts src/gateway/auth-rate-limit.test.ts
# 运行客户端测试
pnpm vitest run --config vitest.gateway.config.ts src/gateway/client*.test.ts八、源码文件依赖图
server.ts (公共门面)
└── server.impl.ts (核心编排)
├── auth.ts ← 认证逻辑
│ ├── auth-rate-limit.ts ← 速率限制
│ ├── credentials.ts ← 凭据解析
│ └── net.ts ← 网络工具(IP/回环检测)
│
├── server-runtime-state.ts ← HTTP/WS/Canvas 创建
│ └── server/ws-connection.ts ← WS 连接处理
│
├── server-runtime-config.ts ← 运行时配置解析
├── server-channels.ts ← 渠道管理器
├── server-chat.ts ← Agent 事件处理
├── server-cron.ts ← Cron 服务构建
├── server-methods.ts ← 核心 RPC 方法
├── server-methods-list.ts ← 方法名注册
├── server-plugins.ts ← 插件加载
├── server-startup.ts ← 边车服务启动
├── server-close.ts ← 优雅关闭
├── server-ws-runtime.ts ← WS 处理器挂载
├── server-maintenance.ts ← 维护定时器
├── server-discovery-runtime.ts ← mDNS/广域发现
├── server-tailscale.ts ← Tailscale 暴露
├── config-reload.ts ← 配置热重载
├── server-reload-handlers.ts ← 热重载执行
├── startup-auth.ts ← 启动认证引导
├── node-registry.ts ← 节点注册表
├── exec-approval-manager.ts ← 命令审批管理
│
├── protocol/index.ts ← WebSocket 协议定义
│ └── (JSON Schema + 类型定义 + 验证器)
│
├── boot.ts ← BOOT.md 启动检查
│
└── events.ts ← 事件类型定义
client.ts (独立模块 — Gateway 客户端)
├── device-auth.ts ← 设备认证
├── net.ts ← 安全 URL 检查
└── protocol/index.ts ← 协议复用九、学到的设计思想
9.1 "分层安全"原则
Gateway 的安全不依赖于单一措施,而是多层叠加:
- 传输层阻止明文连接
- 认证层验证身份
- 速率限制防暴力破解
- 设备签名防重放攻击
- 代理验证防 IP 伪造
每一层都假设其他层可能被绕过。
9.2 "过程式编排 + 模块化子步骤"
startGatewayServer() 虽然很长,但每个子步骤都是独立可测试的模块(server-channels.ts、server-cron.ts 等)。编排器本身不包含业务逻辑,只负责调用顺序和依赖传递。
9.3 "最小 Gateway"测试模式
通过 minimalTestGateway 标志,测试可以启动一个轻量级 Gateway,只包含核心 HTTP/WS 功能。这个设计在测试速度和测试覆盖率之间找到了平衡。
9.4 "快照-执行-恢复"模式
Boot 机制中的 snapshotMainSessionMapping → agentCommand → restoreMainSessionMapping 三步走,确保了 Boot 运行不会产生副作用。这种"快照-执行-恢复"模式在需要隔离副作用的场景中非常有用。
9.5 热重载的"回滚安全"
配置热重载如果失败,会自动回滚密钥快照到上一个已知正确的版本。这种"乐观更新 + 悲观回滚"策略确保了 Gateway 在配置变更失败时不会处于不一致状态。