OpenClaw 源码架构全解剖
OpenClaw 是一个跑在你自己设备上的「个人 AI 助手」:你在 WhatsApp / Telegram / Discord 等 20+ 个聊天软件里给它发消息,它在你家里的一台机器上思考、执行工具、再把回复发回来。本文基于 2026-07-06 的 main 分支源码(commit 83f0527),逐层拆解一条消息从聊天软件到 agent 再回到聊天软件的全过程。所有结论都标注 文件:行号,可直接对照源码阅读。读过《Claude Code 源码架构全解剖》的读者会发现两者的 agent 内核惊人地相似,而「外壳」完全不同——本文会不时对比两者。
00全景架构:一个 Gateway,众星拱月
先建立整体心智模型。Claude Code 是「终端里的一个进程」,OpenClaw 是「一台常驻服务器 + 一群客户端」。
OpenClaw 的核心是一个叫 Gateway(网关)的常驻进程:它监听一个 WebSocket 端口(默认 18789),所有东西——聊天渠道、网页控制台、终端 UI、手机 App、定时任务——都围着它转。你可以把它理解成「你私人的 AI 后端服务器」。产品口号也直说了:「The Gateway is just the control plane — the product is the assistant」(Gateway 只是控制平面,产品是助手本身,README.md)。
npm bin 启动器(纯 JS、零依赖、带快路径)→ TS 入口(respawn 自我重启机制)→ Commander 命令树(60+ 个子命令)
常驻守护进程:HTTP + WebSocket 服务器、自研 req/res/event 三帧协议(v4)、鉴权、配置热重载、渠道生命周期管理。launchd / systemd / schtasks 三平台可装成系统服务
每个聊天平台一个插件:收消息 → 落盘 spool(可靠性)→ 授权闸门(配对/白名单)→ 规范化成统一格式。回程负责格式转换与分块
路由(这条消息归哪个 agent、哪个会话)、会话级并发控制(steer / 排队 / 打断)、回复分块流水线
真正的 agentic while 循环:调模型 → 执行工具 → 回填结果 → 再调模型。源自 Mario Zechner 的 pi-mono,已 fork 进 monorepo 改名 @openclaw/agent-core
按「API 协议族」注册的流式适配器(Anthropic/OpenAI/Google/Mistral 官方 SDK)+ 认证 profile 轮换 + 五级失败恢复链
插件(能注册 30+ 种能力)、Skills(SKILL.md)、Hooks、MCP、定时任务(cron)、节点设备(手机/Mac 外设)
0.1 和 Claude Code 的三个根本差异
| 维度 | Claude Code | OpenClaw |
|---|---|---|
| 进程模型 | 用户敲 claude 起一个进程,退出即结束 | Gateway 常驻 24/7,装成系统服务(launchd/systemd),客户端来来去去 |
| 入口面 | 一个终端(Ink 渲染的 REPL) | 20+ 聊天渠道 + WebUI + TUI + 手机 App + HTTP API,全部收敛到同一个 WS 协议 |
| 会话粒度 | 一个目录一个会话 | 按「agent × 渠道 × 聊天对象」路由出的 session key,一个 Gateway 可养多个 agent、并行服务多个群聊 |
相同点也很本质:两者的心脏都是「一个 while 循环 + 一张工具表」。OpenClaw 的 packages/agent-core/src/agent-loop.ts 与 Claude Code 的 query.ts 在结构上几乎是同构的(第 5 章详述)。
0.2 仓库地图
| 目录 | 内容 |
|---|---|
src/ | 核心:gateway、channels、auto-reply、agents、routing、config、plugins、cron、tui……(5328 个非测试 TS 文件) |
packages/ | 21 个 workspace 包:agent-core(主循环)、ai(provider 流式运行时)、gateway-protocol(WS 帧 schema)、plugin-sdk 等 |
extensions/ | 141 个 bundled 插件:渠道(telegram/whatsapp/discord/imessage…)、模型 provider(anthropic/openai/ollama…)、能力(browser/canvas/memory-core…) |
ui/ | Control UI:Vite + Lit 的单页应用,由 Gateway 直接托管 |
docs/ · skills/ | 文档站源码;内置 skills |
三个最重要的文件:如果只看三个文件,看 packages/agent-core/src/agent-loop.ts(主循环)、src/gateway/server.impl.ts(Gateway 怎么启动和接客)、src/agents/agent-tools.ts(工具怎么装配和被策略过滤)。
血统说明:OpenClaw 的 agent 内核源自 Mario Zechner 的 pi-mono(pi-ai / pi-agent-core),THIRD_PARTY_NOTICES.md 官方声明「Portions of OpenClaw were adapted from Pi / pi-mono」。历史上是外部 npm 依赖(CHANGELOG 可见从 @mariozechner/pi-ai 0.45.7 一路升级到 0.58.0),现已 vendor 进 monorepo 改名 @openclaw/agent-core + @openclaw/ai;只有终端 UI 库 @earendil-works/pi-tui 仍是外部依赖。代码里还留有化石:exec 工具至今读 PI_BASH_YIELD_MS 环境变量兜底(src/agents/bash-tools.exec.ts:1308)。
01一条消息的一生 ★
全文主线。以 Telegram 为例:从你在手机上按下发送,到回复出现在聊天窗口,共 14 站。每一站在后面章节都有放大详解。
Telegram 服务器 → 先落盘,再确认
Gateway 里的 Telegram 插件通过长轮询(getUpdates)或 webhook 收到 update。它做的第一件事不是处理,而是把原始 update 写进磁盘 spool(缓冲池):轮询模式下 spool 落盘成功才推进 offset;webhook 模式下 spool 写成功才回 200,失败就让 Telegram 重投。这保证进程崩溃也不丢消息(durable-before-ack,「先持久化再确认」)。
extensions/telegram/src/telegram-ingress-spool.ts:17 · polling-session.ts:610
spool 回放进 grammY
处理循环从 spool 认领一条 update(带 30 分钟租约,卡死可被别的进程接管),回放进 grammY(Telegram Bot 框架)的 bot.handleUpdate。处理成功打「墓碑」标记而不是删除——Telegram 可能重发同一条 update,删了会导致副作用重跑。失败则按退避策略重试,绝不吞错。
polling-session.ts:610-648 · bot-processing-outcome.ts
授权闸门:你是谁,凭什么跟我说话
过滤掉 bot 自己的回声后,进入单一授权闸门(跑在一切缓存副作用之前):群消息查 groupPolicy(默认 allowlist 白名单);私聊查 dmPolicy(默认 pairing 配对——陌生人会收到一个 8 位配对码,等 bot 主人在终端批准,见第 7 章)。白名单用数字 sender ID 而不是 username(username 可以改,不可靠)。
bot-handlers.runtime.ts:3267 · src/security/dm-policy-shared.ts:170-231
入站缓冲:把碎片拼回一句话
三套缓冲同时工作:debounce(你连发三条短消息会在时间窗内合并成一条)、相册攒批(多张图按 media_group_id 等齐再处理)、长文分片拼接(Telegram 把 >4096 字符的长文拆成多条,按连续 message_id 拼回)。群里没 @ 到 bot 时甚至不下载媒体,省流量。
bot-handlers.runtime.ts:611-744(debounce)· :1005-1119(相册)· :1121-1197(分片)
回复链 + 群聊历史 → prompt 上下文
从本地 message-cache 拉出这条消息的 reply-to 祖先链和群聊近期历史(Bot API 拉不到任意历史,上下文只来自 OpenClaw 亲眼观察过的消息),组装成结构化的「Conversation context」,附在给模型的 prompt 里。
bot-handlers.runtime.ts:1456-1598 · :1274-1407
规范化 + mention 判定 + typing 提示
Telegram 特有字段被转换成内部统一格式 MsgContext(Body/From/ChatType/SessionKey…)。群里如果要求 @ 才响应(requireMention)而你没 @,消息在这里被丢弃(但仍记入历史当旁观上下文)。通过则立刻发 typing... 提示。
extensions/telegram/src/bot-message-context.ts · bot-message.ts:183-190
路由:哪个 agent、哪个会话
按配置里的 bindings[] 走 8 级优先匹配(精确聊天对象 > 话题继承父群 > 通配 > Discord 服务器+角色 > … > 默认 agent),选出 agentId;再生成 session key,比如群聊是 agent:main:telegram:group:-1001234:topic:42,而所有私聊默认折叠进同一个主会话 agent:main:main(第 4 章详表)。
src/routing/resolve-route.ts:611-818 · session-key.ts:222-274
Channel Turn 内核:五阶段准入
所有渠道共用同一个「回合内核」:ingest(原始→规范输入)→ classify(这事件能开一个 agent 回合吗)→ preflight(准入裁决:dispatch / 只旁观 / 已处理 / 丢弃)→ resolveTurn → dispatch。中间有 bot 对 bot 死循环熔断;先把入站消息记进会话,再派发。
src/channels/turn/kernel.ts:676-838 · :523-641
会话级并发控制:同一会话一次只跑一个 run
如果这个会话的 agent 正在干活,新消息按配置四选一:steer(注入进行中的回合,「顺便帮我把标题改了」这种补话);排队(等当前 run 结束后逐条 drain);interrupt(打断重来);drop(心跳类直接丢)。这是多渠道产品必须有、而单终端的 Claude Code 不需要的一层。
src/auto-reply/reply/agent-runner.ts:1263-1349 · get-reply-run.ts:1232-1320
跑 agent:进入 while 循环
runEmbeddedAgent 启动嵌入式 agent run:组装 system prompt(工具清单 + AGENTS.md/SOUL.md 等工作区文件 + 记忆)、解析模型与认证 profile、然后进入 agent-loop.ts 的双层 while 循环:调模型 → 有 toolCall 就执行工具 → 结果回填 → 再调模型,直到模型不再要工具(第 5、6 章)。
src/agents/embedded-agent-runner/run.ts · packages/agent-core/src/agent-loop.ts:305-344
流式回流:block 而不是 token
模型的流式输出经三级订阅链冒泡回渠道层,但 OpenClaw 有一条铁律:对外不发 token 级增量(聊天软件不是终端,逐 token 编辑消息会刷爆 API 限速)。文本被切成语义 block,经保序流水线(promise 链 + 单块超时熔断 + 与 final 去重)交给渠道发送。
src/agents/embedded-agent-subscribe.ts:162 · src/auto-reply/reply/block-reply-pipeline.ts:106-363
工具执行(若模型要求)
工具默认并行执行(任一工具声明 sequential 则整批串行)。exec(shell)工具跑 10 秒没完自动转后台;危险命令触发两阶段审批——你会在聊天窗口里收到一条「Reply with: /approve abc123」的审批卡(第 6、7 章)。
agent-loop.ts:548-597 · src/agents/bash-tools.exec.ts
Telegram 侧发送:draft 流 + 分块 + 三级降级
流式预览用「sendMessage + editMessageText 就地编辑」让一条消息慢慢长大(progress 模式则是一个进度窗口,完成后塌缩成一行摘要)。最终回复按 4000 字符分块(代码围栏内不断行),Markdown 转 Telegram HTML,失败三级降级:富实体 → HTML → 纯文本。reply 引用挂第一块、按钮挂最后一块。
bot-message-dispatch.ts:1005-1125 · send.ts:705-954 · src/auto-reply/chunk.ts
收尾:reaction 收敛 + 转录镜像 + spool 墓碑
状态 reaction 从 👀(思考中)收敛为 ✅/❌;bot 自己的回复也记入 message-cache(下次别人 reply 它时有上下文);assistant 回复镜像写回会话转录(JSONL);最后给 spool 里那条 update 打上墓碑。这条消息的一生结束。
bot-message-dispatch.ts:818-855 · send.ts:902 · polling-session.ts:640
为什么比 Claude Code 的 12 站多了「前置可靠性」?终端里丢一次输入,用户重敲就行;聊天渠道是异步的,用户发完就锁屏了。所以 OpenClaw 在消息进 agent 之前垫了 spool 落盘、去重、授权、缓冲合并四层「消息队列基础设施」——这是全仓库工程含量最高的部分之一。
02启动流程:从 openclaw 命令到 Gateway 就绪
入口链:openclaw.mjs(纯 JS 启动器)→ dist/entry.js → runCli → Commander → startGatewayServer。中间夹着两类「自我重启」。
2.1 三级入口设计
npm 的 bin 指向 openclaw.mjs——一个刻意零依赖的纯 JS 启动器。它先卡 Node 版本门槛(≥22.19,推荐 24,不满足直接退出并提示 nvm,openclaw.mjs:11-15,46-59),然后跑一组快路径:--version 不加载任何 TS 运行时、直接拼 package.json + git HEAD 输出(openclaw.mjs:63-65);裸 --help 优先输出构建期预计算好的帮助文本(openclaw.mjs:725-752)——但如果配置文件里出现 plugins 或 $include 字样就放弃快路径,因为插件可能注册新命令(openclaw.mjs:547-563)。都不命中才动态 import("./dist/entry.js"):
if (!waitingForCompileCacheRespawn) {
if (!isHelpFastPathDisabled() && (await tryOutputBareRootHelp())) {
// OK:预计算帮助,零运行时加载
} else if (!isHelpFastPathDisabled() && tryOutputPrecomputedCommandHelp()) {
} else {
await installProcessWarningFilter();
if (await tryImport("./dist/entry.js")) {
} else if (await tryImport("./dist/entry.mjs")) {
} else { throw new Error(await buildMissingEntryErrorMessage()); }
}
}
src/entry.ts:51-62 有个防御性守卫 isMainModule:entry.js 可能被打包工具当共享依赖 import,若非主模块则跳过全部入口副作用——否则会二次 runCli,起重复 Gateway、撞锁撞端口。
2.2 两类自我 respawn(重生)
「respawn」= 进程发现自己需要不同的 Node 启动参数,于是带着新参数重新 spawn 一个自己、把信号转发过去。OpenClaw 有两类:
- compile-cache respawn(src/entry.compile-cache.ts:148-180):Node 的模块编译缓存能显著加速冷启动,但在两种场景必须禁用并 respawn——源码 checkout(存在
.git或src/entry.ts)、Windows + Node 24.0-24.14(该组合在 ESM 加载期可能死锁,entry.compile-cache.ts:15-17)。打包安装则启用版本化缓存目录(版本号 + 安装标记做 key,防升级后用旧字节码,entry.compile-cache.ts:100-124)。 - CLI flag respawn(src/entry.respawn.ts:75-159):Windows 注入
--stack-size=8192;探测到额外 TLS CA 时注入NODE_EXTRA_CA_CERTS;压掉 ExperimentalWarning。逃生口OPENCLAW_NO_RESPAWN。
启动器对 respawn 出的子进程做信号监督:转发 SIGTERM/SIGINT 等,三级宽限各 1 秒(转发 → SIGTERM → SIGKILL → 父进程强退,openclaw.mjs:110-213)。
2.3 runCli:裸命令的智能分派
src/cli/run-main.ts:841-1357 是 CLI 主流程。最有意思的是裸 openclaw(不带任何子命令)的分派逻辑(run-main.ts:297-329):
- 配置文件不存在(或只有壳)→ 进 onboarding 向导(引导你配 Gateway、渠道、模型);
- 配置有效 → WS 探测本机/远端 Gateway 是否活着:活着就起 TUI 连上去,不活就 TUI local 模式;
- 配置无效 → 起 Crestodian——一个专门的「诊断修复助手」agent,帮你把配置修好。
命令树规模:23 个核心命令(onboard / doctor / message / sessions / tasks…,src/cli/program/core-command-descriptors.ts:10-117)+ 39 个子 CLI(gateway / daemon / nodes / skills / plugins / security…,src/cli/program/subcli-descriptors.ts:10-194),全部懒注册;openclaw gateway run 还有专用快路径——只注册这一条命令的裁剪版 Commander,跳过整棵命令树构建(run-main.ts:147-242)。
2.4 Gateway 启动 12 步
startGatewayServer(port = 18789)(src/gateway/server.impl.ts:524)按 startupTrace 逐步推进,关键顺序:
环境规范化
state 目录 env 归一,把端口写进 OPENCLAW_GATEWAY_PORT 让 browser/canvas 等派生端口一致。
server.impl.ts:528-553
config.snapshot + auth
加载配置快照(可复用 CLI 预读、避免重复解析);解析鉴权——mode=token 但没配 token 时生成随机 token 并警告(只存内存,不落盘)。
server.impl.ts:577-632
plugins.bootstrap
构建插件 registry 与查表;channel 插件可带自己的 gateway 方法;未配置的 channel 插件延后加载。
server.impl.ts:674-721
runtime.config:bind 与 TLS
定 bindHost(默认 loopback 回环)、Control UI 开关、TLS。非回环绑定 + 无鉴权 = 直接拒绝启动(第 7 章)。
server.impl.ts:740-843
channelManager + runtime.state
创建渠道管理器(Telegram/Discord 等插件运行时);创建 HTTP server、WebSocketServer、客户端集合、广播器——注意此时只创建、还没 listen。
server.impl.ts:852-919
ws-attach → http.listen
把带鉴权、限流、握手超时的 WS 连接处理器挂上,然后才真正绑端口。顺序很讲究:先能处理,再开门。
server.impl.ts:1553-1591
post-attach sidecars → ready
启动「边车」集:插件运行时、渠道连接、tailscale serve/funnel、update check。就绪前新 WS 连接会被以专用 close code 拒绝并提示重试。
server.impl.ts:1628-1734 · ws-connection.ts:436
配置热重载 + 兜底维护
watch 配置文件 + 订阅进程内写事件,diff 后按 reload plan 增量应用(渠道启停/cron 重建/插件 reload),必要时判定整体重启;启动快照晋升为 last-known-good 备份。250ms 后起维护定时器(健康检查、去重清理、media TTL)。
server.impl.ts:1736-1839
2.5 配置系统:一个 JSON5 文件 + 三板斧
配置文件默认 ~/.openclaw/openclaw.json(state 目录可用 OPENCLAW_STATE_DIR 覆盖,src/config/paths.ts:24-25,150)。加载管线(src/config/io.ts:1982-2067):JSON5 解析(允许注释和尾逗号)→ $include 指令(配置拆多文件)→ ${ENV} 环境变量替换(缺失的 env var 降级为警告而非致命——Gateway 允许带残缺 provider key 降级启动)→ 插件感知的 schema 校验。写入带冲突检测与重试;解析失败的快照标 invalid,Gateway 会进降级 / Crestodian 修复路径,且随时可从 last-known-good 恢复(src/config/config.ts:18-24)。
对比 Claude Code 的「五层 settings 合并」:OpenClaw 是单文件 + include模型——因为它的配置对象是「一台服务器」而不是「用户×项目×企业」的矩阵。
2.6 装成系统服务(daemon)
openclaw daemon install 把 Gateway 装成开机自启服务,三平台适配器统一聚合在 src/daemon/service.ts:8-53:macOS launchd(label ai.openclaw.gateway,plist 走 launchctl bootstrap 装载,src/daemon/launchd.ts:307-346)、Linux systemd 用户单元(~/.config/systemd/user/openclaw-gateway.service,src/daemon/systemd.ts:67)、Windows 计划任务(schtasks,任务名「OpenClaw Gateway」,src/daemon/constants.ts:4-17)。运行中的 Gateway 还支持 SIGUSR1 触发自我重启,带 restart-handoff 文件把重启前后的耗时追踪串起来(server.impl.ts:562-569,641)。
03Gateway 控制平面:自研 WS 协议
所有客户端——WebUI、TUI、手机 App、节点设备——说的都是同一门语言:跑在 WebSocket 上的 req / res / event 三帧协议(v4)。
3.1 帧模型:三种帧走天下
协议 schema 独立成包 packages/gateway-protocol/(用 TypeBox 定义,TypeBox 是一个「写 TS 即得 JSON Schema」的校验库)。当前协议版本 4(version.ts:2-6)。整个协议只有三种帧:
/** 客户端请求帧;method 决定用哪个 payload 校验器 */
export const RequestFrameSchema = Type.Object(
{ type: Type.Literal("req"), id: NonEmptyString,
method: NonEmptyString, params: Type.Optional(Type.Unknown()) },
{ additionalProperties: false },
);
/** 服务端响应帧,靠 id 与请求配对 */
export const ResponseFrameSchema = Type.Object(
{ type: Type.Literal("res"), id: NonEmptyString, ok: Type.Boolean(),
payload: Type.Optional(Type.Unknown()), error: Type.Optional(ErrorShapeSchema) },
{ additionalProperties: false },
);
/** 服务端事件帧(推送),带可选 seq 与状态版本号 */
export const EventFrameSchema = Type.Object(
{ type: Type.Literal("event"), event: NonEmptyString,
payload: Type.Optional(Type.Unknown()),
seq: Type.Optional(Type.Integer({ minimum: 0 })),
stateVersion: Type.Optional(StateVersionSchema) },
{ additionalProperties: false },
);
方法名如 chat.send、agents.list、sessions.*、config.*、nodes.invoke,每个域一个实现文件(src/gateway/server-methods/)。这跟 Claude Code 的「SDK 控制协议走 stdin/stdout」是同一个思想的网络化版本。
3.2 握手:挑战 → connect → hello-ok
连接建立后服务端先发 connect.challenge 事件,带一个随机 nonce(一次性随机数,防重放攻击)。
src/gateway/server/ws-connection.ts:387-392
客户端第一帧必须是 req connect,携带:协议版本区间(协商用)、客户端身份(id/version/platform/mode)、角色与 scopes(node 设备走 role:"node")、设备身份签名(Ed25519 公钥 + 对 challenge nonce 的签名)、以及 auth(token / password / deviceToken / bootstrapToken…)。
message-handler.ts:720-728 · frames.ts:30-82
成功回 hello-ok:协商后的协议号、服务端信息、能力发现(methods[] / events[] 清单)、初始状态快照、可签发 deviceToken 供后续免密重连、payload 大小策略、插件的 Control UI tab 注入。
frames.ts:85-155 · message-handler.ts:2103
3.3 防护细节(值得抄的部分)
- 预认证预算:未完成握手的连接占用一个有限预算池,握手完成才释放;握手还有超时定时器(ws-connection.ts:258-340)。
- 预认证帧限长:认证前的帧大小上限远小于认证后(ws-connection.ts:26-29)——没验明正身之前,别想发大包。
- 失败限速只烧「错误凭据」:给了 token 但错了才计入限速;没给凭据不计(防止把正常探测也锁死,src/gateway/auth.ts:399-408)。回环地址默认豁免。
- 慢消费者熔断:socket 积压超
MAX_BUFFERED_BYTES直接 close(1008, "slow consumer")(ws-connection.ts:362-379);认证后每 25 秒 ping 心跳。 - 握手处理器懒加载:处理器模块加载期间入队最多 16 帧,超限断连——连「模块还在 import」这种窗口期都设了上限(ws-connection.ts:53,184-218)。
同一个 HTTP server 上还挂着:Control UI 静态资源、OpenAI 兼容的 chat-completions HTTP 端点(让任何支持 OpenAI API 的工具把 OpenClaw 当模型用)、回环 MCP server、插件 HTTP 路由(src/gateway/ 下 control-ui.ts / openai-compatible-http*.ts / mcp-http.ts;逐端点实现本文未核实)。
04多渠道接入架构
「渠道」= 一个聊天平台的接入插件。141 个 bundled 扩展里有 20+ 个渠道,它们只做传输(transport-only),所有共性逻辑都在核心层。
4.1 分层:插件只管收发,内核管准入
extensions/telegram/ 传输层:grammY 收发、spool 落盘、格式转换、分块(transport-only)
↓ ingest
src/channels/turn/ 回合内核:ingest → classify → preflight → resolveTurn → dispatch
↓ dispatch
src/auto-reply/ 回复编排:命令、hook、typing、会话并发控制、block 流水线
↓ prompt
src/agents/ agent 运行时(第 5 章)
回合内核 runChannelTurn(src/channels/turn/kernel.ts:676-724)是所有渠道的共同必经之路:
export async function runChannelTurn<TRaw, TDispatchResult>(params) {
const input = await params.adapter.ingest(params.raw); // 原始 → 规范化输入
if (!input) { return { admission: { kind: "drop", reason: "ingest-null" }, dispatched: false }; }
const eventClass = (await params.adapter.classify?.(input)) ?? DEFAULT_EVENT_CLASS;
if (!eventClass.canStartAgentTurn) { return { admission: { kind: "handled" } }; }
const preflight = normalizePreflight(await params.adapter.preflight?.(input, eventClass));
// admission ∈ dispatch | observeOnly | handled | drop;drop 时仍可记入群历史
const resolved = await params.adapter.resolveTurn(input, eventClass, preflight);
// 之后 runPreparedChannelTurnCore:先 recordInboundSession,再 runDispatch()
}
注意 observeOnly(只旁观)这个裁决:群里没 @ 你的消息不触发回复,但会被记进群历史——下次有人 @ 你时,agent 能看到之前的讨论。这是「群聊 AI」和「单聊 AI」的关键差异。
4.2 路由:bindings 8 级匹配 + Session Key
一个 Gateway 可以养多个 agent(比如「工作 agent」绑 Slack、「家庭 agent」绑 WhatsApp)。谁接谁的消息由配置 bindings[] 决定,按 8 级优先匹配(src/routing/resolve-route.ts:736-817):精确聊天对象 → 话题继承父群 → 通配(group:*)→ Discord 服务器+角色 → Discord 服务器 → Slack team → 账号 → 渠道 → 默认 agent。命中即产出 {agentId, sessionKey},带 LRU 路由缓存(上限 4000 key)。
Session key 决定「哪些消息共享同一段记忆」,真实格式(src/routing/session-key.ts:222-274):
| 场景 | Session Key 格式 |
|---|---|
| agent 主会话 | agent:main:main |
| 私聊(默认 dmScope=main) | 折叠进主会话——你在 Telegram 和 WhatsApp 私聊它是同一段记忆 |
| 私聊(per-channel-peer) | agent:main:telegram:direct:<peerId>(按渠道×联系人拆开) |
| 群聊 | agent:main:telegram:group:-1001234567890 |
| 群话题(forum topic) | agent:main:telegram:group:<chatId>:topic:<threadId> |
| 跨渠道同一个人 | identityLinks 可把多渠道 peerId 映射到统一身份,折叠成一个会话(session-key.ts:276-320) |
默认值有安全含义:dmScope 默认 main 意味着所有获准私聊的人共享 agent 主会话——A 说的话 B 可能看到。所以 openclaw security audit 有一条专门的 finding:dm.scope_main_multiuser(多人共用主会话告警,第 7 章)。
4.3 会话级并发:steer / followup / interrupt / drop
同一 session 同时只跑一个 agent run(src/auto-reply/reply/get-reply-run.ts:1232-1320)。run 进行中来了新消息,按队列模式四选一,其中 steer(转向)最有意思——把新消息注入进行中的回合,模型在下一轮工具调用间隙就能看到:
if (effectiveShouldSteer && isActive) {
const steerSessionId =
(sessionKey ? replyRunRegistry.resolveSessionId(sessionKey) : undefined) ??
followupRun.run.sessionId;
const steerOutcome = await queueEmbeddedAgentMessageWithOutcomeAsync(
steerSessionId, followupRun.prompt,
{ steeringMode: "all", ... },
);
if (steerOutcome.queued) {
await touchActiveSessionEntry();
typing.cleanup();
return undefined; // 本轮不再起新 run
}
排队(followup)模式则把整个 run 描述入 session 队列、按 message-id 去重,当前 run 清场后逐条 drain(agent-runner.ts:1316-1349)。渠道层还有第二重串行化:Telegram 按 chat/topic 生成 lane key,spool 按 lane 串行 drain,只读命令(/status)可绕行(extensions/telegram/src/sequential-key.ts:150)。
4.4 出站:流式 draft、分块、降级
- 流式三模式:
partial/block——「sendMessage + editMessageText 就地编辑」让一条预览消息慢慢长大(首条有 30 字符起送门槛,改善推送通知体验);progress——单个进度窗口渲染 🧠 思考 / 💬 评论 / 工具行,final 送达后塌缩成一行摘要;off——只发最终回复(bot-message-dispatch.ts:1005-1125)。 - 保序流水线:block 经 promise 链保证出站顺序,单块发送超时则整体熔断丢弃后续块——宁可少发,不可乱序;final 与已流式内容重复时被去重压掉(block-reply-pipeline.ts:158-224,340-360)。
- 分块与格式:Telegram 上限 4000 字符(留余量 <4096),代码围栏内不断行;Markdown → Telegram HTML,三级降级:富实体 400 → HTML;HTML 解析 400 → 纯文本(send.ts:815-826,954 · src/auto-reply/chunk.ts:1-33)。多块时 reply 引用挂第一块、inline 键盘挂最后一块。
- 限速:bot-token 级共享节流器 + 尊重 Telegram 的
retry_after。
渠道插件的「同构降级」纪律:流式和非流式两条发送漏斗必须共享同一批错误谓词与降级链(extensions/telegram/CLAUDE.md Send funnel parity)——否则会出现「流式能发、final 报错」的分裂行为。这类不变量直接写在渠道目录的 CLAUDE.md 里,是 AI 辅助开发时代「把守则放在代码旁边」的实践。
05Agentic 主循环:@openclaw/agent-core
整个产品的心脏。和 Claude Code 的 query.ts 一样,是一个「调模型 → 执行工具 → 回填 → 再调」的 while 循环——只是这里是双层的。
5.1 九层调用栈:从渠道消息到 while 循环
渠道 dispatch
└─ runEmbeddedAgent() [src/agents/embedded-agent-runner/run.ts, 4294 行] 命令排队/模型与 auth 解析/failover/compaction 编排
└─ runEmbeddedAttemptWithBackend() [run/attempt.ts, 5916 行] 单次 provider 尝试的全部编排
└─ createAgentSession() [sessions/sdk.ts:406] new Agent(...) + new AgentSession(...)
└─ session.prompt() [sessions/agent-session.ts:1145] 命令展开/steer 或 followUp 分流
└─ agent.prompt() [packages/agent-core/src/agent.ts:374]
└─ runAgentLoop() [packages/agent-core/src/agent-loop.ts:172] ★真·主循环
分工与 Claude Code 的 QueryEngine / queryLoop 二分如出一辙:外层管「会话生命周期」(转录落盘、锁、failover、压缩),内层 agent-loop.ts 只管「干活」。内核包 packages/agent-core 依赖极简——只有 @openclaw/ai 和 typebox(packages/agent-core/package.json:106-109),可以脱离 OpenClaw 单独复用。
5.2 双层 while:内层跑工具,外层接后续
// Outer loop: continues when queued follow-up messages arrive after agent would stop
while (true) {
let hasMoreToolCalls = true;
// Inner loop: process tool calls and steering messages
while (hasMoreToolCalls || pendingMessages.length > 0) {
if (await stopIfAborted()) { return; }
...
// 注入 pending 消息(steering 补话在下一次模型响应前生效)
if (pendingMessages.length > 0) {
for (const message of pendingMessages) {
await emit({ type: "message_start", message });
await emit({ type: "message_end", message });
currentContext.messages.push(message);
}
}
// 流式拿一条 assistant 消息
const message = await streamAssistantResponse(
currentContext, config, signal, emit, streamFn, runtime,
);
// 之后:过滤 toolCall 块 → executeToolCalls → 结果 push 回 context → turn_end
}
// 内层停了;followUp 队列非空 → 回填 pendingMessages,continue 外层
}
内层循环条件 hasMoreToolCalls 就是 Claude Code 的 needsFollowUp——模型不再要工具即停,没有任何计划器。外层多出的一圈是为聊天场景准备的:agent 刚要收工时用户又发来一条消息(followUp 队列),不需要重新起 run,直接续循环。
两条消息队列的语义(agent.ts:155-190):steering(转向)每个 turn 末尾 drain,打断式补话;followUp(后续)只在 agent 本该停下时 drain。abort 时会补一条 stopReason:"aborted" 的 assistant 消息再收尾(agent-loop.ts:281-303)——保证转录永远不会停在裸 toolCall 上,否则续跑和压缩都会坏档。
5.3 模型调用:streamFn 依赖注入
内核不直接认识任何厂商——它只调一个被注入的 streamFn:
async function streamAssistantResponse(context, config, signal, emit, streamFn, runtime) {
let messages = context.messages;
if (config.transformContext) { // 压缩/剪枝的挂点(第 9 章)
messages = await config.transformContext(messages, signal);
}
const llmMessages = await config.convertToLlm(messages);
const llmContext: Context = { systemPrompt: context.systemPrompt, messages: llmMessages, tools: context.tools };
const streamFunction = resolveAgentCoreStreamFn(runtime, streamFn);
// 每次调用都重解析 API key —— 应对会过期的 OAuth token
const resolvedApiKey =
(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
const response = await streamFunction(config.model, llmContext, { ...config, apiKey: resolvedApiKey, signal });
// for-await provider 事件:text_delta / thinking_* / toolcall_* / done / error
嵌入层在 src/agents/sessions/sdk.ts:406-432 注入真实实现:streamFn 内部先向模型注册表要凭据和 header,再调 @openclaw/ai 的 streamSimple。这个依赖注入设计让 agent-core 可以被单测、被 CLI 复用、甚至换整个模型层。
5.4 System Prompt 组装:一个「人格」是怎么拼出来的
真正的渲染器是 src/agents/system-prompt.ts:680 的 buildAgentSystemPrompt(1425 行)。按稳定前缀顺序输出这些节:身份行("You are a personal assistant running inside OpenClaw.")→ ## Tooling 工具清单 → 子 agent 委派指引 → ## Tool Call Style(含 /approve 规则)→ ## Safety → ## Skills → ## Memory → 时间与授权发送者 → Project Context(工作区文件注入) → ## Heartbeats。
工作区文件是 OpenClaw 的「人格文件系统」,注入顺序硬编码(system-prompt.ts:69-81):
const CONTEXT_FILE_ORDER = new Map<string, number>([
["agents.md", 10], // 操作守则(≈ Claude Code 的 CLAUDE.md)
["soul.md", 20], // 人格/语气 —— OpenClaw 特色
["identity.md", 30], // 我是谁
["user.md", 40], // 用户是谁
["tools.md", 50], // 工具使用指导(不改变可用性)
["bootstrap.md", 60], // 首次开机仪式,setup 完成后不再注入
["memory.md", 70], // 长期记忆精华
]);
const DYNAMIC_CONTEXT_FILE_BASENAMES = new Set(["heartbeat.md"]);
缓存策略与 Claude Code 的 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 同思路:稳定前缀按内容 sha256 做 LRU 缓存(上限 64 条,system-prompt.ts:121-141),动态内容(heartbeat、当前时间)放缓存边界之下,保 provider 端 prompt cache 命中。文件按 dev:ino:size:mtime 身份缓存、变了才重读,单文件上限 2MB(src/agents/workspace.ts:62-107)。子 agent 只注入 AGENTS+TOOLS(workspace.ts:1126);还有个动人的细节——workspace 目录 24 小时内有存活证明却突然消失时,抛 WorkspaceVanishedError 拒绝重新播种 BOOTSTRAP.md:防止把一个老 agent 当新生儿重新「洗脑」(workspace.ts:217-234)。
5.5 流式事件的三级订阅链
Agent.subscribe()(agent.ts:278-283):内核事件流agent_start / turn_start / message_update / tool_execution_* / turn_end / agent_end。AgentSession订阅 Agent(sessions/agent-session.ts:439),翻译成会话事件并驱动转录落盘。- 嵌入层
subscribeEmbeddedAgentSession(src/agents/embedded-agent-subscribe.ts:1313)挂上一束回调(onBlockReply / onPartialReply / onReasoningStream / onToolResult…),内部做<think>标签剥离、markdown 围栏安全断句、block 分块,然后进第 4 章的渠道流水线。
06工具系统
与 Claude Code 的差异一句话:schema 从 Zod 换成 TypeBox、权限从「弹窗问用户」换成「多层策略过滤 + 聊天内审批」,其余惊人一致。
6.1 AgentTool 接口(TypeBox schema)
export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = unknown>
extends Tool<TParameters> {
label: string; // UI 显示名
hideFromChannelProgress?: boolean; // 不在聊天进度窗里刷存在感
prepareArguments?: (args: unknown) => Static<TParameters>;
/** 执行。失败要 throw,而不是把错误编码进 content */
execute: (
toolCallId: string,
params: Static<TParameters>,
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<TDetails>, // 进度回调 → tool_execution_update 事件
) => Promise<AgentToolResult<TDetails>>;
executionMode?: ToolExecutionMode; // "sequential" | "parallel" 单工具覆写
}
参数校验失败不炸循环——转成 errorKind:"argument-validation" 的错误 tool result 回给模型自我修正(agent-loop.ts:922-932),和 Claude Code 的 <tool_use_error> 反馈闭环同一思想。执行默认并行,任一工具声明 sequential 则整批串行(agent-loop.ts:548-597)——对比 Claude Code 的「只读并发、写串行」二分法,这里把并发决定权下放给了单个工具。
6.2 装配管线:六路来源 + 十一层策略
createOpenClawCodingTools(src/agents/agent-tools.ts:384-1195)文件头注释即总纲:「Assembles core, shell, channel, OpenClaw, plugin, and Tool Search tools, then applies sandbox, profile, provider, sender, group, and sub-agent policy.」六路工具来源合流后过 applyToolPolicyPipeline:
const subagentFiltered = applyToolPolicyPipeline({
tools: toolsForModelProvider,
steps: [
...buildDefaultToolPolicyPipelineSteps({
profilePolicy, profile,
globalPolicy, globalProviderPolicy,
agentPolicy, agentProviderPolicy,
groupPolicy, senderPolicy, agentId, ...
}),
{ policy: sandboxToolPolicy, label: "sandbox tools.allow", ... },
{ policy: subagentPolicy, label: "subagent tools.allow", ... },
{ policy: inheritedToolPolicy, label: "inherited tools", ... },
],
});
层级依次是:profile → provider profile → 全局 → 全局×provider → agent → agent×provider → 群 → 发送者 → 沙箱 → 子 agent → 父会话继承。「群」和「发送者」两层是聊天产品特有的:同一个 agent 在公开群里可以只留只读工具,对 owner 私聊才放开 exec。被 deny 的工具在模型看到之前就被剔除。收尾还有三道包裹:schema 兼容清洗(OpenAI 拒绝 root union、Gemini 要剥约束关键字而 Anthropic 要保留,agent-tools.ts:1139-1148)→ before/after_tool_call 插件钩子 → abort 信号注入。
6.3 内置工具速查表
| 工具 | 文件(src/agents/ 下) | 职责 |
|---|---|---|
| read / write / edit | sessions/tools/*.ts | 文件读写编辑;sandbox 模式换成沙箱版并包工作区根守卫 |
| grep / find / ls | sessions/tools/*.ts | 工作区搜索/列目录 |
| exec / process | bash-tools.exec.ts / bash-tools.process.ts | shell 执行 + 后台会话管理(见 6.4) |
| apply_patch | apply-patch.ts | 补丁式多文件编辑(按模型白名单开启) |
| message | tools/message-tool.ts | 主动/跨渠道发消息——agent 能「给你发微信」的原因 |
| browser | (extensions/browser 插件提供) | 浏览器控制 |
| canvas / nodes | (canvas 插件)/ tools/nodes-tool.ts | 给配对设备推可视化画布;调用手机摄像头、定位、通知(第 11 章) |
| cron | tools/cron-tool.ts | agent 给自己定闹钟(第 12 章) |
| sessions_spawn / sessions_yield / subagents | tools/sessions-spawn-tool.ts 等 | 派生子 agent 并等待其完成(见 6.5) |
| sessions_list / sessions_history / sessions_send | tools/sessions-*-tool.ts | 跨会话查询与发送 |
| web_search / web_fetch | tools/web-tools.ts | 搜索/抓取(provider 有原生搜索时可被抑制) |
| image / image_generate / video_generate / music_generate / tts / pdf | tools/*.ts | 多媒体理解与生成(按配置和凭据可用性 gating) |
| gateway | tools/gateway-tool.ts | agent 自管 Gateway:查/改配置、重启——「它能给自己做手术」 |
| get_goal / update_plan / heartbeat_response | tools/*.ts | 目标管理、计划板、心跳回执 |
| tool_search / tool_describe / tool_call | tool-search.ts | 工具目录太大时的延迟加载三件套(同 Claude Code 的 ToolSearch 思路,agent-loop.ts:843-864 的 resolveDeferredTool 是底座) |
| crestodian | tools/crestodian-tool.ts | ring-zero 安装修复工具,仅 Crestodian 运行器注入——普通 agent 连通配符也拿不到(openclaw-tools.ts:139-142) |
6.4 exec:默认 10 秒转后台 + 聊天内审批
exec 的参数 schema 就体现了它比 Claude Code 的 Bash 工具多出的维度(src/agents/bash-tools.schemas.ts:13-38):
export const execSchema = Type.Object({
command: Type.String({ description: "Shell command to execute" }),
yieldMs: Type.Optional(Type.Number({ description: "Milliseconds to wait before backgrounding (default 10000)" })),
background: Type.Optional(Type.Boolean({ description: "Run in background immediately" })),
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, kills process on expiry)" })),
pty: Type.Optional(Type.Boolean({ ... })), // 伪终端(交互式程序)
elevated: Type.Optional(Type.Boolean({ description: "Run on the host with elevated permissions (if allowed)" })),
host: optionalStringEnum(EXEC_TOOL_HOST_VALUES, { description: "Exec host/target (auto|sandbox|gateway|node)." }),
});
- yield 后台化:默认 10 秒(钳制 10ms–120s)没跑完就「让出」转后台,进程进注册表,由
process工具 poll / log / send-keys / kill 继续管理;硬超时默认 1800 秒杀进程。工具调用被 abort 不杀已后台化的会话,timeout 到点仍会杀(bash-tools.exec.ts:1304-1317,1996)。对比 Claude Code 的「超时原地转后台」,同一招。 - host 多宿主:命令可以跑在沙箱容器(sandbox)、Gateway 宿主机(gateway)、甚至另一台配对设备(node,第 11 章)。
- 审批:安全策略是
security(deny / allowlist / full)×ask(off / 未命中问 / 总是问)双轴合并取更严(exec.ts:1682-1705)。触发审批时走两阶段 gateway 审批注册(bash-tools.exec-approval-request.ts:1-50),用户在聊天窗口收到审批卡或直接回/approve <id>——system prompt 明确教模型「如果 exec 返回 approval-pending,把 Reply with: 里的命令原样发给用户」,并禁止模型自己去 shell 里执行 /approve(system-prompt.ts:264-266,1107)。还有可选的模型自动审查员(一个小模型先替你审一遍命令,exec.ts:90)。 - 纵深防御:safeBins 白名单需配合 hardened profile 才生效;宿主机环境变量里的危险项被剥离;elevated 提权需要配置双开关(enabled + allowed)且提权不等于免审批(exec.ts:1335-1379)。
6.5 子 agent:sessions_spawn
Claude Code 的子 agent 是「递归调用 query()」;OpenClaw 的子 agent 是「spawn 一个新会话」(src/agents/tools/sessions-spawn-tool.ts),几个讲究的设计:
- 默认隔离:子会话默认不带父转录,
context:"fork"才继承——system prompt 里明确教「omit context unless transcript needed」(system-prompt.ts:1060)。 - 不许自选投递目标:spawn 参数显式禁止 target/channel/threadId——子 agent 不能决定「把结果发到哪个群」,投递上下文只能由父继承注入(sessions-spawn-tool.ts:54-63)。这是防「子 agent 被注入后乱发消息」的契约级防线。
- 工具面继承:父会话的最终实效 allowlist 以引用传入,子会话继承的是「父被过滤后真实拥有的」工具面而不是配置原文(agent-tools.ts:883-889)。
- 完成回报是推送式:子 agent 完成作为事件回注父会话;父 agent 被教导用
sessions_yield让出等待,而不是轮询subagents list(system-prompt.ts:1084)。 - runtime 可以是外人:
runtime:"acp"时任务经 ACP(Agent Client Protocol)发给 Claude Code、Gemini CLI 等外部编码 agent 执行(sessions-spawn-tool.ts:69-76)——OpenClaw 可以雇佣 Claude Code 打工。
07权限与安全模型
Claude Code 防的是「模型乱动我的电脑」;OpenClaw 还要防「互联网上的陌生人指挥我的模型动我的电脑」。所以它的安全面多了一整层:入站身份。
7.1 Pairing:陌生人的配对仪式
私聊策略 dmPolicy 四个取值:pairing | allowlist | open | disabled,默认 pairing(src/config/zod-schema.providers-core.ts:270)。陌生人首次私聊 bot 的完整流程:
入站判定:sender 不在白名单、dmPolicy=pairing → 裁决 pairing(src/security/dm-policy-shared.ts:215-231)。消息不进 agent。
生成 8 位配对码:字母表剔除易混淆的 0/O/1/I,crypto.randomInt 生成;pending 请求 1 小时过期、每账号最多 3 个——洪水攻击下静默不建新请求,既不刷屏也不撑爆存储(src/pairing/pairing-store.ts:33-37,688-696)。
回给陌生人一条提示:「OpenClaw: access not configured. Pairing code: XXXX. Ask the bot owner to approve with: openclaw pairing approve telegram XXXX」(pairing-messages.ts:14-27)。
Bot 主人在自己的终端(不是聊天窗口!)跑 approve 命令 → 配对码命中 → sender id 写入 allowFrom 存储,之后畅通(pairing-store.ts:714-769)。批准动作必须发生在聊天渠道之外,这样「聊天消息里的提示注入」永远无法自我批准。
细节:dmPolicy=open 也不是裸奔——配了 allowFrom 且不含 * 时仍按名单过滤;通配符 * 写入 pairing 存储时会被归一为空,防止把「全开」持久化进磁盘(dm-policy-shared.ts:88-106 · pairing-store.ts:288-301)。群命令授权刻意不继承 DM 配对批准(dm-policy-shared.ts:310-314)。群策略 groupPolicy 非法或缺省一律归一为 allowlist(dm-policy-shared.ts:171-174)——所有默认值都朝关闭的方向倒。
7.2 Gateway 鉴权:拒绝启动比警告更狠
if (!isLoopbackHost(bindHost) && !hasSharedSecret && authMode !== "trusted-proxy") {
throw new Error(
`refusing to bind gateway to ${bindHost}:${params.port} without auth ` +
`(set gateway.auth.token/password, or set OPENCLAW_GATEWAY_TOKEN/...)`,
);
}
要把 Gateway 绑到非回环地址(局域网/公网)而没配鉴权?直接拒绝启动,不是打个警告了事。且判定用的是「解析后的真实 secret 非空」而不是 mode 字段——配了 mode=token 但 token 是空串照样拦(server-runtime-config.ts:130-133)。其他硬规则:tailscale funnel(暴露公网)强制 password 模式;缺 token 时自动生成的随机 token 只存内存不落盘——持久化凭据必须走显式的 doctor --fix / configure(src/gateway/startup-auth.ts:226-243);token/password 比较走常量时间函数 safeEqualSecret 防时序侧信道(src/gateway/auth.ts:405,427)。
7.3 exec 审批与沙箱(执行面)
第 6.4 节讲了机制,这里补策略框架:security(deny / allowlist / full)与 ask(何时问人)两轴独立配置、多来源合并时取更严(bash-tools.exec.ts:1682-1705);审批请求两阶段注册防 /approve 竞态,超时走 askFallback(默认 deny)。沙箱方面:配置 sandbox mode 支持把非主会话(群聊、子 agent)的执行整体放进 Docker 容器(agent-tools 装配时给 exec 传入容器 buildExecSpec,agent-tools.ts:845-860),主人私聊的主会话则直跑宿主机——「对自己人方便,对世界谨慎」。安全审计对危险的 bind mount 和 network 模式有专项 finding(见下)。
7.4 openclaw security audit:给自己做体检
这是 Claude Code 没有对应物的子系统(src/security/audit.ts,1548 行):扫描整个部署面,输出结构化 finding(checkId 按「域.子面.问题」命名,severity 三级)。抽几个有代表性的检查项:
| checkId | 在查什么 |
|---|---|
gateway.bind_no_auth | 非回环绑定无鉴权(critical) |
gateway.token_too_short | token 短于 24 字符 |
channels.*.dm.open | 某渠道私聊策略全开 |
dm.scope_main_multiuser | 多个获准用户共享主会话(互相看得到上下文) |
security.exposure.open_channels_with_exec | 「开放渠道 × exec 工具」的爆炸半径组合项 |
fs.credentials_dir.* / fs.synced_dir | 凭据目录权限;state 目录在 Dropbox/iCloud 同步盘里 |
plugins.installs_missing_integrity | 插件供应链:安装记录缺完整性校验 / npm 版本未钉死 |
models.small_params | 小参数模型带工具 = 提示注入风险更高,也算安全问题 |
--deep 追加真连 Gateway 探活、已装插件/skill 代码扫描、Docker 容器核对;--fix 只做窄而可逆的修复(groupPolicy open→allowlist、目录 700 / 文件 600 权限收紧——Windows 上用 icacls ACL 而非 chmod),刻意不自动修 bind/auth 类问题(src/security/fix.ts:394-478,118-192)。
7.5 信任模型:一句话划清边界
SECURITY.md:5 开宗明义:「OpenClaw is local-first agent infrastructure for trusted operators」——一个 Gateway 只服务一个受信任的操作者;纯 prompt injection(未穿越 auth/policy/sandbox 边界)通常不算安全漏洞;多个敌对用户想共享一个 Gateway 互相隔离?不支持,请拆 Gateway 或拆 OS 用户。「能改 ~/.openclaw 的人即 trusted operator」(docs/gateway/security/index.md:23)。看懂这句话,上面所有设计的取舍就都通了。
08模型 / Provider 层
Claude Code 只对一家 API 做到极致;OpenClaw 要对几十家 provider 做到通用——所以它的模型层是一套「按协议族注册的适配器 + 五级失败恢复链」。
8.1 五层结构
| 层 | 位置 | 职责 |
|---|---|---|
| ① 协议类型 | packages/llm-core/src/types.ts | Api / Model / Context / 事件流等纯类型契约 |
| ② 流式运行时 | packages/ai/src/(@openclaw/ai) | 各 API 协议族的 stream 适配器 + ApiRegistry(pi-ai 的后继) |
| ③ 核心 facade | src/llm/ | 进程级默认 registry 装配与 re-export |
| ④ 目录/选择/认证/failover | src/agents/ + src/model-catalog/ | 模型目录合并、别名解析、auth profile 轮换、fallback 链 |
| ⑤ Provider 插件 | extensions/anthropic · openai · ollama · clawrouter… | 每家厂商的认证方式、模型声明、动态发现 |
关键设计:registry 的 key 是 API 协议族而不是厂商(packages/llm-core/src/types.ts:6-18)——内置九族:openai-completions / openai-responses / anthropic-messages / google-generative-ai / google-vertex / bedrock-converse-stream / mistral-conversations…。任何 provider 的模型只要声明 api:"openai-completions" 就复用同一个适配器,这就是「接一个新厂商 = 写一段配置」的原因。底层全用官方 SDK:@anthropic-ai/sdk / openai / @google/genai / @mistralai/mistralai(packages/ai/package.json:70-77)。
export function createApiRegistry() {
const providers = new Map<string, RegisteredApiProviderEntry>();
function registerApiProvider(provider, sourceId?) {
providers.set(provider.api, {
provider: {
api: provider.api,
stream: wrapStream(provider.api, provider.stream), // model.api 不匹配直接 throw,防路由错协议
streamSimple: wrapStreamSimple(provider.api, provider.streamSimple),
},
sourceId, // 支持按插件批量注销
});
}
...
模型 id 统一 provider/model 格式(如 anthropic/claude-opus-4-8)。目录行来自 5 类来源,按权威度合并:用户 config(0,最强)> 插件 manifest(1)> 缓存/运行时刷新(2)> ClawHub 未装插件的 preview(3,最弱)(src/model-catalog/authority.ts:9-15)。
8.2 认证 profile:多账号轮换与冷却
凭据存 ~/.openclaw/agents/<agentId>/agent/auth-profiles.json,三种类型:api_key / token / oauth(OpenClaw 负责刷新)(src/agents/auth-profiles/types.ts:75)。同一 provider 可配多个 profile,轮换规则(auth-profiles/order.ts:244-388):
- 无显式顺序时按「类型偏好(oauth > token > api_key)+ 同类型内 lastUsed 最旧先用」轮转(round-robin);注释明确刻意忽略 lastGood——否则一个健康 profile 会饿死其他所有 profile(order.ts:379-381,439)。
- 失败冷却分三层:瞬态阶梯 30s → 1min → 封顶 5min(auth-profiles/usage.ts:427-436);billing / 永久认证错误走指数长退避(billing 5h 起步 24h 封顶);rate_limit / timeout 只冷却触发的那个模型,同 profile 其他模型照用,活跃窗口内第二个模型也挂才扩大到全 profile(usage.ts:683-703)。
- 冷却过期时顺带清零错误计数,防止陈旧计数把下一次小失败直接升级成长冷却(order.ts:255-258)。openrouter 这类聚合网关整体绕过本地冷却——它们自己管限流(usage-state.ts:11-14)。
8.3 失败恢复五级链
① SDK 内建重试 同一请求、同凭据(maxRetries 透传)
② operation-retry 传输层瞬态小重试:2 次、250ms 起指数退避;create 默认不重试防双写 [src/provider-runtime/operation-retry.ts:31-53]
③ auth profile 轮换 同 provider 换账号 [embedded-agent-runner/run.ts:1454-1505]
④ model fallback 换模型/换厂商 [src/agents/model-fallback.ts:1397-1933]
⑤ session suspension 全链耗尽 → 挂起会话 lane,等冷却过期 [model-fallback.ts:1551-1607]
模型 fallback 链 = 请求的 primary → agents.defaults.model.fallbacks[] 按序 → 配置默认兜底(model-fallback.ts:1066-1095)。哪些错误不触发 fallback 值得细看:用户取消 / 重启类 abort;本地协调错误(防止把本地故障误算成 N 个模型全挂);上下文溢出——应由压缩处理,换个更小窗口的模型只会更糟(model-fallback.ts:1761-1786)。rate_limit 引发的 profile 轮换超上限时直接升级成模型级 fallback(run.ts:1694-1720)。
还有一个精巧机制:provider 全员冷却时的恢复探测(probe)——每 provider 30 秒节流探一次;无 fallback 链的单 provider 每个节流窗都探(否则要等 provider 报的 reset 时间,可能是几天后);每次 fallback run 每 provider 只允许一次瞬态探测(model-fallback.ts:1097-1213,1642-1679)。全链耗尽时抛 FallbackSummaryError:聚合每次尝试的错误、算出全链最早冷却到期时间,给用户「retry in 30s」级别的提示(model-fallback.ts:144-168)。
8.4 流式与 provider 插件
统一抽象 AssistantMessageEventStream:适配器同步返回流对象,内部异步推事件、.result() 聚合成最终消息(packages/ai/src/stream.ts:24-55)。Anthropic 适配器用官方 SDK 的 .asResponse() 拿原始 SSE 自己解析,流没走到 message_stop 就断线会显式报错;逐事件累计 usage 并按模型价目计费——「Fallback-served turns bill at the serving model's rates」(服务端 fallback 边界会切换计价模型,packages/ai/src/providers/anthropic.ts:470-525)。
provider 插件三例:anthropic——API key / setup-token / 复用本机 Claude CLI 凭据三条认证路(装了 Claude Code 的机器可以零配置借用它的登录态,extensions/anthropic/provider-discovery.ts:10-37);openai——API key + ChatGPT OAuth 全套 device-code 流程;clawrouter——不是本地路由算法,而是指向 clawrouter.openclaw.ai 的服务端代理 provider:一把 key 决定可见模型集与预算,目录从服务端拉、60 秒缓存(extensions/clawrouter/provider-catalog.ts:12-63)。
09会话、压缩与记忆
Claude Code 的记忆活在一个终端会话里;OpenClaw 的 agent 要「活很多年」——所以它有一整套持久身份:转录、压缩、日记、检索索引。
9.1 会话存储:JSONL 转录
目录布局(src/config/sessions/paths.ts:11-38):转录在 ~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl,注册表在同目录 sessions.json(session key → sessionId 映射)。每个 JSONL 文件首行是 header:
export function createSessionTranscriptHeader(params = {}) {
return {
type: "session",
version: CURRENT_SESSION_VERSION,
id: params.sessionId ?? randomUUID(),
timestamp: new Date().toISOString(),
cwd: params.cwd ?? process.cwd(),
};
}
防御性细节:sessionId 白名单正则 ^[a-z0-9][a-z0-9._-]{0,127}$;所有 session 文件路径必须 realpath 收敛在 sessions 目录内,越界直接抛错(防路径穿越,paths.ts:63-74,180-242);序列化遇到 JSON.stringify → undefined 立即 fail-fast,防静默丢转录行(transcript-jsonl.ts:16-29)。转录在逻辑上是树/DAG:改写历史走「分支再追加」(branch-and-reappend)而不是原地改。
9.2 压缩(compaction):触发、分块、防死循环
- 触发阈值 = 模型上下文窗口 − reserveTokens(预留),预留下限默认 20,000 token(src/agents/agent-settings.ts:9)。小窗口模型有钳制保护:floor 被钳到「窗口 − 最小 prompt 预算」,否则 16K 模型会每条 prompt 都判溢出 → 无限压缩循环(agent-compaction-constants.ts:1-12)。
- 分块摘要(src/agents/compaction-planning.ts):目标块 = 窗口 40%(下限 15%),token 估算带 1.2 安全系数;切块绝不拆开 tool_use / tool_result 配对,切完还跑配对修复丢弃孤儿——防 Anthropic 的「unexpected tool_use_id」400(:90-195,378-386);单条超窗口 50% 的巨型消息不进摘要、换占位符
[Large <role> (~NK tokens) omitted from summary](:273-315)。两处 SECURITY 注释:工具结果的 details 与运行时上下文条目绝不进 LLM 摘要。 - 压缩即轮转:压缩生成新 sessionId、换新 JSONL 文件,旧文件按 checkpoint 保留(compaction-session-file.ts:11-36)——历史永远可考古。
- 时机:prompt 发出前有预检(按字符/token 估算压力,embedded-agent-runner/run/preemptive-compaction.ts);provider 真报溢出后有补救压缩 + 重试,配防死循环守卫。压缩归属三选一互斥(runtime 自动 / OpenClaw safeguard / context-engine 插件全权),防止双压(agent-settings.ts:174-210)。
9.3 cache-ttl 剪枝:压缩之外的轻量层
对应 Claude Code 的 microcompact,但触发条件更聪明——围绕 provider 的 prompt cache 生命周期设计,默认启用:
export const DEFAULT_CONTEXT_PRUNING_SETTINGS = {
mode: "cache-ttl",
ttlMs: 5 * 60 * 1000, // provider prompt cache 视为过期的 TTL
keepLastAssistants: 3, // 最近 3 个 assistant 回合免剪
softTrimRatio: 0.3, // 上下文压力 30% 起软剪(留头尾)
hardClearRatio: 0.5, // 50% 起硬清(换占位符)
minPrunableToolChars: 50_000,
softTrim: { maxChars: 4_000, headChars: 1_500, tailChars: 1_500 },
hardClear: { enabled: true, placeholder: "[Old tool result content cleared]" },
};
原理:prompt cache 还热的时候改历史 = 打破缓存、得不偿失;cache 过期(5 分钟没交互)后,旧工具结果就只剩 token 成本了——这时软剪或硬清才净赚。图片按 8000 字符估价,剪掉换 [image removed during context pruning]。
9.4 记忆三层:MEMORY.md + 日记 + SQLite 索引
MEMORY.md
工作区根的长期精华,注入 system prompt。必须是真文件——拒绝 symlink(src/memory/root-memory-files.ts:41-56)。
memory/YYYY-MM-DD.md 日记
日常追加写。/new 重置后自动带入最近 2 天日记(每文件截 1200 字符)作启动上下文(src/config/types.agent-defaults.ts:91-106)。
SQLite 混合检索
bundled 插件 memory-core 提供 memory_search / memory_get 工具;索引 = FTS5 全文 + sqlite-vec 向量,混合打分 vectorWeight×向量分 + textWeight×文本分,叠加 MMR 去冗余与时间衰减(extensions/memory-core/src/memory/hybrid.ts:32-39 · packages/memory-host-sdk/src/host/memory-schema.ts:7-13)。
Memory flush(压缩前抢救记忆)是点睛之笔:会话逼近压缩阈值(距离 < 4000 token)或转录超 2MB 时,先注入一个额外回合,让模型把「值得长存的内容」append 到当天日记,然后才压缩——压缩丢掉的细节至少精华已经落盘。flush prompt 强制三条安全约束:只写日记文件、只追加不覆盖、MEMORY.md / SOUL.md 等启动文件在 flush 期间一律只读(extensions/memory-core/src/flush-plan.ts:12-27,76-84)。memory-core 还有一套 cron 化的「dreaming」(做梦)整理管线,把日记消化进长期记忆(light/REM/deep 三档;实现细节本文未核实)。
9.5 context-engine:可插拔的上下文管理生命周期
OpenClaw 把「上下文管理」整体抽象成一个可被插件接管的接口(src/context-engine/types.ts:303-456):bootstrap(初始化)/ ingest(逐条吸收)/ assemble(按 token 预算组装上模型的上下文)/ compact / maintain。引擎声明 ownsCompaction:true 时 runtime 的自动压缩被关闭;引擎想改写转录必须通过 rewriteTranscriptEntries() 请求「分支再追加」,磁盘 DAG 的更新权始终在 runtime 手里。注册中心还带隔离机制:引擎连续出错会被 quarantine(隔离),防止一个坏插件把所有会话拖死(src/context-engine/registry.ts)。bundled 的 active-memory 插件就是一个 context-engine 实现(行为细节未核实)。
10UI 层:Control UI(Web)与 TUI(终端)
Claude Code 的 UI 是产品本体;OpenClaw 的 UI 只是 Gateway 的又一批 WebSocket 客户端——「UI 无特权」是这里的关键词。
10.1 Control UI:Vite + Lit 的单页应用
技术栈是 Lit(Web Components)+ Vite,不是 React/Vue(ui/package.json:12-27);由 Gateway 进程直接托管在 http://host:18789/。它连 Gateway 用的就是第 3 章那套 WS 协议——没有任何私有后门 API。功能面 30 个路由:chat(WebChat)、agents、channels、config、sessions、usage、logs、skills(含 skill 编写工坊)、cron、nodes、debug…(ui/src/app-route-paths.ts:4-31)。内嵌终端用的是 ghostty-web(Ghostty 终端的 WASM 版)。
浏览器的身份认证颇为讲究:token 之外还有 Ed25519 设备身份——安全上下文(HTTPS/localhost)下生成持久密钥对,对「deviceId + clientId + role + scopes + 时间戳 + token + 服务端 challenge nonce」整体签名(ui/src/api/gateway.ts:447-479)。新浏览器首连会被 1008 拒绝、等 openclaw devices approve 批准(回环直连自动批);握手成功后 Gateway 签发 deviceToken 存 localStorage 供免密重连(ui/src/api/gateway.ts:820-827 · docs/web/control-ui.md:38-70)。
WebChat 的两条数据路径值得注意:session JSONL 是持久权威转录;Gateway 推的实时事件只是投递投影(非权威)。历史拉取有界化(长文截断 + 占位符,可按 messageId 补拉全文);chat.send 带幂等键防双发(docs/web/webchat.md:24-47)。
10.2 TUI:pi-tui + 双后端
终端 UI 用 @earendil-works/pi-tui(pi-mono 家族里唯一还以外部依赖存在的成员——组件式终端 UI 库,非 Ink,src/tui/tui.ts:6-15)。架构上是 TuiBackend 接口的双实现(src/tui/tui-backend.ts):默认走 Gateway 后端——就是又一个 WS 客户端,调 chat.send / chat.history;tui --local 则走嵌入式后端,进程内直接跑 agent 运行时,完全不需要 Gateway。对比 Claude Code 那个 5000 行的 REPL:OpenClaw 的 TUI 主循环只有 1707 行,因为渲染、流式组装、审批交互大都复用 Gateway 侧的既有语义。
10.3 Canvas / A2UI:给设备推「活的界面」
Canvas 是个 bundled 插件(extensions/canvas/),给已配对的节点设备(手机/Mac)提供实时可视化画布。agent 拿到一个 canvas 工具:present / navigate / eval / snapshot(截图给自己看渲染结果)/ A2UI 推送。A2UI 是 Google 的声明式 agent-to-UI 协议——agent 推 JSONL 组件流,设备端渲染(extensions/canvas/index.ts:14-46)。这是「agent 的输出不止是文字」方向上最激进的尝试。
11节点(Nodes)体系:把手机变成 agent 的外设
node = 连到 Gateway 的伴生设备(iPhone / Android / Mac / 无头 CLI),给 agent 暴露摄像头、屏幕、定位、通知等命令面。官方定性:「Nodes are peripherals, not gateways」——节点是外设,不是网关。
11.1 连接与双层配对
node 复用同一个 WS 协议连 Gateway,connect 帧差异只在 mode: NODE、role:"node",外加能力声明(src/node-host/runner.ts:280-296):
const client = new GatewayClient({
url, token, password,
clientName: GATEWAY_CLIENT_NAMES.NODE_HOST,
mode: GATEWAY_CLIENT_MODES.NODE,
role: "node",
scopes: [],
caps: ["system", ...pluginNodeHost.caps],
commands: [
...NODE_SYSTEM_RUN_COMMANDS, // system.run 系列
...NODE_EXEC_APPROVALS_COMMANDS,
...pluginNodeHost.commands, // 插件注册的节点命令(canvas.* 等)
],
deviceIdentity: loadOrCreateDeviceIdentity(), // Ed25519 设备身份
});
配对是双层的:设备配对(device pairing)门禁 WS 握手本身;节点配对(node.pair.*)跟踪该节点被批准的命令面。审批要求随声明的命令自动升级:不带命令 → 普通配对权限即可批;带非 exec 命令 → 需要写权限;带 system.run(能在设备上跑 shell)→ 需要管理员权限(docs/nodes/index.md:22-38)。
11.2 system.run 的防篡改设计
agent 要在你手机/Mac 上跑命令时,审批在设备侧强制执行,且有 TOCTOU(检查时与使用时不一致)防护:审批前先生成规范化的执行计划(systemRunPlan),批准后 Gateway 转发的是存储的 plan 而不是调用方事后可改的 command/cwd;直接执行的脚本绑定具体文件、执行前文件变了即拒绝(docs/nodes/index.md:50-53 · 实现 src/node-host/invoke-system-run-plan.ts)。能力清单:camera(拍照入上下文)、canvas(A2UI 渲染)、screen、location、notifications、语音唤醒等(docs/nodes/ 各页;iOS/Android App 本体不在本仓库,端上实现未核实)。
12扩展生态:插件、Skills、Hooks、MCP、Cron
Claude Code 的扩展收敛为「注入文本」或「注册工具」两种本质;OpenClaw 的插件 API 宽得多——它是在扩展一台服务器,而不是一个会话。
12.1 插件系统:manifest + 同步 register
插件从三类根目录发现:安装包内 extensions/(141 个 bundled)、全局 ~/.openclaw/extensions/、工作区 <workspace>/.openclaw/extensions/(src/plugins/roots.ts:18-28),还兼容 Claude / Codex / Cursor 的插件 bundle 布局。每个插件必须带 openclaw.plugin.json manifest——不执行插件代码即可读它做配置校验(含严格 JSON Schema 的 configSchema、能力归属快照 contracts)。安装来源 npm / git / ClawHub(官方注册表),装前有安全扫描(src/plugins/install-security-scan.ts)。
生命周期最有意思的是注册窗口设计:
function runPluginRegisterSync(register, api): void {
const guarded = createGuardedPluginRegistrationApi(api);
try {
const result = register(guarded.api);
if (isPromiseLike(result)) {
void Promise.resolve(result).catch(() => {});
throw new Error("plugin register must be synchronous");
}
} finally {
guarded.close(); // 注册窗口关闭,此后 api 上大部分方法失效
}
}
register(api) 必须同步返回;api 是个 Proxy 包装的「守卫对象」,注册窗口一关就失效——插件不能在运行期偷偷往注册表里塞东西。注册面超过 30 种:registerTool / registerChannel / registerProvider / registerGatewayMethod / registerCli / registerHttpRoute / registerContextEngine / registerAgentHarness / registerCompactionProvider / registerMemoryCapability…(src/plugins/api-builder.ts:20-95)。TS 源码插件经 jiti 即时编译加载;SDK import 被 alias 重写到宿主内的 plugin-sdk。
12.2 Hooks:两套体系
- 插件 hooks(进程内,typed):
api.on("before_tool_call", handler, {priority, timeoutMs}),可阻断/改写/要求审批工具调用;事件覆盖before_agent_start / before_prompt_build / before_model_resolve / before_tool_call / before_agent_reply / before_agent_finalize等,按 priority 降序串行(docs/plugins/hooks.md:10-63 · src/plugins/hooks.ts)。其中 before_agent_finalize 能要求 agent「重写最终回复」(带副作用检测与最大重写次数,run/attempt.ts:3465-3575)——这是比 Claude Code 的 Stop hook 更内嵌的干预点。 - 目录式 hooks(HOOK.md):操作员安装的脚本目录(HOOK.md + handler 模块),响应
/new、agent:bootstrap、gateway:startup等事件;bundled 自带 5 个(boot-md、session-memory、compaction-notifier…)。加载 workspace hook 代码进 Gateway 进程时会打「trusted local code」警告(src/hooks/loader.ts:49-60)。
12.3 Skills:与 Claude Code 同一物种
Skill = 目录 + SKILL.md(frontmatter 带 OpenClaw 专有 metadata:always / requires 依赖声明 / install 规格 / os),三源优先级 workspace > managed(~/.openclaw/skills)> bundled(src/skills/loading/frontmatter.ts:191-203)。合格 skills 被格式化进 system prompt(路径做 ~ 压缩省 token);skill 自带的可执行文件走 exec-approvals 信任链。分发走 ClawHub:搜索/下载/sha256 校验/风险确认,装进 workspace 并写来源溯源文件(src/skills/lifecycle/clawhub.ts:1-40)。Control UI 还有个 skill 编写工坊页面,配套 skill_workshop 工具让 agent 自己写 skill(沙箱内禁用)。
12.4 MCP:先当 server,后当 client
与 Claude Code 相反的侧重:OpenClaw 作为 MCP server 更成熟——src/mcp/ 提供 stdio MCP server,把插件注册的工具暴露给外部 MCP 客户端(典型场景:ACP 会话里的 Claude Code 反过来调 OpenClaw 的 memory_recall)。作为 MCP client 则走 agent bundle 配置:外部 MCP server 在运行时物化成 mcp__server__tool 命名的工具(src/agents/agent-bundle-mcp-tools.ts;完整配置 schema 与传输种类未核实)。
12.5 Cron:agent 的生物钟
src/cron/ 是定时任务子系统,四种 schedule:at(一次性)/ every / cron(标准表达式,croner 解析 + LRU 缓存)/ on-exit(Gateway 退出时触发);三种 payload:systemEvent(往主会话注入系统事件)/ agentTurn(独立 agent 回合)/ command(src/cron/types.ts:8-29,238-298)。落库 SQLite;调度是单 setTimeout 指向最近到期的 job,重启补跑、整点错峰防雷群。隔离 job 的执行是一条完整编排:独立 session key → 模型预检 → skills 快照 → 执行 → 按 CronDeliveryPlan 把结果投递到指定渠道 → 会话收割(reaper 在 finally 里保证执行)。目录下有 8+ 个以 issue 号命名的回归测试——看得出这是踩坑大户。心跳(heartbeat)机制让 agent 定期「醒来」读 HEARTBEAT.md 自查——这是「always-on 助手」和「按需 CLI」的分水岭。
13文件索引速查
按「我想看 X」检索,路径相对仓库根。
| 我想看… | 去这里 |
|---|---|
| 进程入口 / 快路径 / respawn | openclaw.mjs:773 · src/entry.ts:51 · entry.respawn.ts:75 · entry.compile-cache.ts:148 |
| CLI 主流程 / 裸命令分派 | src/cli/run-main.ts:841,297 · program/core-command-descriptors.ts:10 |
| Gateway 启动 / 关闭 | src/gateway/server.impl.ts:524,1845 · server.ts:1-36(懒加载壳) |
| WS 协议帧 / 握手 / 鉴权 | packages/gateway-protocol/src/schema/frames.ts:30,169 · src/gateway/server/ws-connection.ts:387 · auth.ts:533 |
| 配置加载 / 热重载 | src/config/io.ts:1982 · paths.ts:24,277 · server.impl.ts:1736 |
| daemon(launchd/systemd/schtasks) | src/daemon/service.ts:8 · constants.ts:4 · launchd.ts:307 · systemd.ts:67 |
| Telegram 入站 / spool / 授权 | extensions/telegram/src/bot-handlers.runtime.ts:3267,3510 · telegram-ingress-spool.ts:17 · polling-session.ts:610 |
| 渠道回合内核 | src/channels/turn/kernel.ts:676,523 |
| 路由 / session key | src/routing/resolve-route.ts:611,736 · session-key.ts:222 |
| 会话并发(steer/queue) | src/auto-reply/reply/agent-runner.ts:1263,1316 · get-reply-run.ts:1232 |
| block 回复流水线 / 分块 | src/auto-reply/reply/block-reply-pipeline.ts:106 · chunk.ts:1 · extensions/telegram/src/send.ts:705 |
| ★ agent 主循环 | packages/agent-core/src/agent-loop.ts:172,305,446 · agent.ts:374 |
| 嵌入式 runner / 单次尝试 | src/agents/embedded-agent-runner/run.ts · run/attempt.ts · sessions/sdk.ts:406 |
| System prompt / 工作区文件 | src/agents/system-prompt.ts:680,69 · workspace.ts:32 · bootstrap-files.ts:292 |
| 工具装配 / 策略过滤 | src/agents/agent-tools.ts:384,1088 · openclaw-tools.ts:425 |
| exec / 审批 / process | src/agents/bash-tools.exec.ts:1304 · bash-tools.schemas.ts:13 · bash-tools.exec-approval-request.ts:1 |
| 子 agent spawn | src/agents/tools/sessions-spawn-tool.ts · subagent-spawn.ts |
| pairing / DM 策略 | src/pairing/pairing-store.ts:33,614,714 · src/security/dm-policy-shared.ts:170 |
| 安全审计 / 修复 | src/security/audit.ts · fix.ts:394 · src/gateway/server-runtime-config.ts:148 |
| Provider registry / 流式 | packages/ai/src/api-registry.ts:76 · stream.ts:24 · providers/anthropic.ts:490 |
| auth profile 轮换 / 冷却 | src/agents/auth-profiles/order.ts:244,439 · usage.ts:427 |
| 模型 fallback 链 | src/agents/model-fallback.ts:982,1397,1252 |
| 会话转录 / 路径安全 | src/config/sessions/paths.ts:11,180 · transcript-header.ts:12 · transcript-jsonl.ts:16 |
| 压缩 / 剪枝 | src/agents/compaction-planning.ts:90 · agent-settings.ts:9,174 · agent-hooks/context-pruning/settings.ts:52 |
| 记忆(flush / 检索) | extensions/memory-core/src/flush-plan.ts:12 · memory/hybrid.ts:32 · src/memory/root-memory-files.ts:41 |
| context-engine 接口 | src/context-engine/types.ts:303 · registry.ts |
| Control UI 客户端 / 设备签名 | ui/src/api/gateway.ts:493,447 · app-route-paths.ts:4 |
| TUI | src/tui/tui.ts:6 · tui-backend.ts · gateway-chat.ts |
| 节点 host / system.run 防篡改 | src/node-host/runner.ts:280 · invoke-system-run-plan.ts |
| 插件加载 / 注册面 | src/plugins/loader.ts:630,560 · api-builder.ts:20 · roots.ts:18 · manifest.ts |
| Skills / ClawHub | src/skills/loading/frontmatter.ts:191 · workspace.ts:36 · lifecycle/clawhub.ts:1 |
| MCP server / client | src/mcp/plugin-tools-serve.ts:1 · src/agents/agent-bundle-mcp-tools.ts |
| Cron | src/cron/types.ts:8,238 · schedule.ts:1 · store.ts:39 · isolated-agent/run.ts |
写在最后:如果说 Claude Code 展示了「一个 agent 循环可以多精致」,OpenClaw 展示的是「把同一个循环放进真实世界要垫多少层」——消息可靠性、入站身份、会话并发、多模型容灾、常驻记忆、设备联邦。两份解剖对照着读,你就同时拥有了 agent 产品的「内核视角」和「系统视角」。