【源码解析】OpenClaw 多渠道 AI 助手网关的架构设计与核心原理
本文基于 OpenClaw 开源仓库源码(4.5版本)进行深度分析,全面覆盖路由、会话隔离、安全边界、AI 推理执行引擎、上下文管理、ACP 协议、Cron 任务等核心模块,重点模块附有源码注解与 Mermaid 架构图,帮助读者系统性理解这套多渠道 AI 助手网关的设计原理。
适读人群:有一定 TypeScript / Node.js 经验,希望深入理解 AI 助手网关架构、消息路由系统、Agent 上下文管理机制的工程师。不需要 OpenClaw 使用经验,但了解基本的 LLM API 调用会有帮助。
文章结构:
| 章节 | 内容 | 推荐人群 |
|---|---|---|
| §1 项目概述 | 规模与定位 | 所有读者 |
| §2 模块总览 | 架构分层图 + 模块速览表 | 快速了解全貌 |
| §3.1 Channel 渠道机制 | SessionKey、Binding 路由、安全防线 | 想理解消息如何进入系统 |
| §3.2 消息处理全链路 | 端到端时序、信任边界、懒加载 | 想理解一条消息的完整生命周期 |
| §3.3 AI 推理执行引擎 | System Prompt 构建、队列仲裁、三阶段上下文管理 | 想深入 Agent 执行机制 |
| §3.4 ACP 子 Agent 编排 | 会话状态机、Control-Plane | 多 Agent 编排场景 |
| §3.5-3.6 Cron + Tasks | 定时任务调度、任务生命周期注册表 | 自动化任务场景 |
| §3.7-3.9 Hooks / MCP / Secrets | 事件钩子、MCP 双向集成、凭证管理 | 插件扩展开发者 |
| §3.10-3.12 多模态 / 其他模块 / Plugin-SDK | 媒体理解、生态全景 | 想扩展或集成 OpenClaw |
| §4 总结 | 架构亮点、设计哲学、产品护城河分析 | 所有读者,推荐最后阅读 |
一、项目概述
OpenClaw 是一个多渠道 AI 助手网关系统。可以把它理解为一个"AI 消息中枢":从 Telegram、Discord、Slack、WhatsApp、iMessage、Signal、Matrix、Line、Zalo 等任意聊天平台接收消息,统一路由给 AI 模型(Claude、GPT、Gemini、DeepSeek 等),再把回复投递回原渠道。
核心价值:一套 AI Agent,接入所有渠道,支持定时任务、子 Agent 编排、多模态理解。
项目规模:
src/核心模块:约 70 个子目录,数千个 TypeScript 源文件extensions/插件生态:约 100 个 bundled 插件(AI Provider + 渠道 + 工具插件)apps/原生客户端:iOS / Android / macOS
二、模块总览
2.1 整体架构图
2.2 模块分类速览
接入层
| 模块 | 目录 | 职责 |
|---|---|---|
| Gateway | src/gateway/ |
HTTP/WebSocket 服务,所有流量统一入口 |
| Pairing | src/pairing/ |
移动端设备 QR 码配对认证 |
| TUI | src/tui/ |
纯终端交互 UI,富文本渲染 |
| ACP Server | src/acp/ |
Agent Control Protocol 服务端,接受外部 Agent 调用 |
渠道与路由层
| 模块 | 目录 | 职责 |
|---|---|---|
| Channels | src/channels/ |
渠道插件类型合同(Adapter 定义),不含具体实现 |
| Routing | src/routing/ |
SessionKey 计算,8 级 Binding 规则匹配 |
| Security | src/security/ |
DM Policy、allowlist、外部内容安全策略 |
业务逻辑层
| 模块 | 目录 | 职责 |
|---|---|---|
| Auto-reply | src/auto-reply/ |
消息安全检查→上下文装配→队列→AI 回复全链路 |
| Cron | src/cron/ |
定时任务引擎(cron 表达式 + every + at 三种模式) |
| Tasks | src/tasks/ |
任务注册表,跨 runtime 的任务生命周期追踪 |
| Hooks | src/hooks/ |
5 类事件可插拔钩子(command/session/agent/gateway/message) |
| Flows | src/flows/ |
Channel 配置引导流、Model 选择引导流 |
| ACP Control-Plane | src/acp/control-plane/ |
子 Agent 会话生命周期管理与编排 |
AI 能力层
| 模块 | 目录 | 职责 |
|---|---|---|
| Agents | src/agents/ |
AI 推理循环、工具调用、上下文管理、模型路由 |
| MCP | src/mcp/ |
Model Context Protocol,暴露渠道工具给 AI 客户端 |
| Media Understanding | src/media-understanding/ |
图像描述、音频转录、视频描述 |
| TTS | src/tts/ |
文本转语音(ElevenLabs、Deepgram 等) |
| Image Generation | src/image-generation/ |
AI 生图(fal、OpenAI DALL-E 等) |
| Video Generation | src/video-generation/ |
AI 生视频 |
| Canvas Host | src/canvas-host/ |
交互式 Canvas 渲染(A2UI 协议) |
| Memory Host SDK | src/memory-host-sdk/ |
外部记忆后端接入(LanceDB 等) |
| Realtime Voice | src/realtime-voice/ |
实时语音对话流 |
| Context Engine | src/context-engine/ |
上下文感知与动态注入 |
基础设施层
| 模块 | 目录 | 职责 |
|---|---|---|
| Config/Sessions | src/config/ src/sessions/ |
配置加载、会话持久化(JSONL Transcript) |
| Secrets | src/secrets/ |
SecretRef 凭证管理(env/file/exec 三种来源) |
| Plugin-SDK | src/plugin-sdk/ |
给第三方插件暴露的稳定公开 API |
| Plugins | src/plugins/ |
Plugin 发现、加载、注册表 |
| Infra | src/infra/ |
网络、TLS、出站投递 |
| Daemon | src/daemon/ |
后台守护进程管理 |
| Logging | src/logging/ |
结构化日志体系 |
| i18n | src/i18n/ |
国际化支持 |
插件生态(extensions/,约 100 个)
| 类别 | 代表插件 |
|---|---|
| AI Provider | anthropic、openai、google、xai、deepseek、qwen、minimax、mistral、moonshot、ollama、groq、openrouter、amazon-bedrock、github-copilot 等 |
| 消息渠道 | telegram、discord、slack、whatsapp、signal、matrix、line、zalo、irc、feishu、msteams、googlechat、imessage、bluebubbles 等 |
| 搜索工具 | exa、brave、tavily、duckduckgo、perplexity、searxng、xai(X-Search) |
| 媒体理解 | zai、groq、deepgram、elevenlabs |
| 图像生成 | fal、openai |
| 记忆后端 | memory-core、memory-lancedb |
| 编程 Agent | browser、kilocode、opencode、opencode-go、kimi-coding |
| 其他扩展 | voice-call、speech-core、diagnostics-otel、thread-ownership 等 |
三、核心模块详解
3.1 Channel 渠道机制
3.1.1 ChannelPlugin:组合式适配器设计
每个渠道(Telegram、Discord 等)以 Plugin 形式接入。核心类型 ChannelPlugin(src/channels/plugins/types.plugin.ts)是一个由多个可选适配器组成的配置对象:
// src/channels/plugins/types.plugin.ts
export type ChannelPlugin<ResolvedAccount = any> = {
id: ChannelId;
meta: ChannelMeta; // 展示信息(名称、文档、描述)
capabilities: ChannelCapabilities; // 能力声明(是否支持线程/投票/编辑等)
config: ChannelConfigAdapter; // 必选:账号配置管理
security?: ChannelSecurityAdapter; // 安全检查(allowFrom、DM 策略)
outbound?: ChannelOutboundAdapter; // 发送消息
messaging?: ChannelMessagingAdapter; // 会话 Key 解析、目标路由
threading?: ChannelThreadingAdapter; // 线程/回复语义
agentPrompt?: ChannelAgentPromptAdapter; // 注入到 System Prompt 的渠道提示
directory?: ChannelDirectoryAdapter; // 用户/群组目录查找
actions?: ChannelMessageActionAdapter; // 消息工具 Action(发送/编辑/删除等)
lifecycle?: ChannelLifecycleAdapter; // 启动/停止生命周期
};
这是典型的组合优于继承模式(类比 Java 中的 Strategy + Facade 组合):每个 Adapter 是一组可选函数,渠道插件按需实现,核心层通过接口调用。
3.1.2 SessionKey:会话的"坐标系"
每条入站消息都会被计算出一个 SessionKey,它唯一标识一段对话,是会话隔离的基础。
SessionKey 格式(src/routing/session-key.ts):
agent:<agentId>:<channel>:<peerKind>:<peerId>
agent:<agentId>:<channel>:<accountId>:direct:<peerId> ← 多账号 + 按用户隔离
agent:<agentId>:main ← 默认 DM,所有私信共享
关键:dmScope 控制 DM 会话的隔离粒度
// src/routing/session-key.ts
export function buildAgentPeerSessionKey(params) {
if (peerKind === "direct") {
switch (dmScope) {
case "per-account-channel-peer":
return `agent:${agentId}:${channel}:${accountId}:direct:${peerId}`;
// 每个渠道账号下的每个用户,独立会话
case "per-channel-peer":
return `agent:${agentId}:${channel}:direct:${peerId}`;
// 跨账号,但每个渠道下每个用户独立会话
case "per-peer":
return `agent:${agentId}:direct:${peerId}`;
// 跨渠道、跨账号,同一用户共享会话
default: // "main"
return `agent:${agentId}:main`;
// 所有 DM 共享同一个 Agent 会话(默认)
}
}
// 群组/频道:每个群组独立
return `agent:${agentId}:${channel}:${peerKind}:${peerId}`;
}
实际 SessionKey 示例:
| 场景 | SessionKey |
|---|---|
| 默认 DM(所有私信共享 Agent 会话) | agent:main:main |
| Telegram 私信按用户隔离 | agent:main:telegram:direct:123456 |
| Discord 频道 | agent:main:discord:channel:C456789 |
| Telegram 群组 | agent:main:telegram:group:-100123456 |
| Slack 线程 | agent:main:slack:channel:C123:thread:T456 |
| 多账号隔离的 DM | agent:main:telegram:acc1:direct:123456 |
线程也有独立的 SessionKey,格式为 <baseSessionKey>:thread:<threadId>,并保存 parentSessionKey 用于继承父级上下文。
3.1.3 路由规则(Binding):8 级精细匹配
SessionKey 决定"这条消息属于哪个会话",Binding(绑定规则) 决定"这个会话交给哪个 Agent 处理"。
用户在配置文件中声明 Binding 规则,每条规则有 match 条件和目标 agentId:
bindings:
- match:
channel: discord
peer: { kind: channel, id: "123456789" } # 匹配特定频道
agentId: support-agent
- match:
channel: discord
guildId: "987654321"
roles: ["mod-role-id"] # 匹配特定服务器 + 角色
agentId: mod-agent
- match:
channel: telegram
accountId: bot2 # 匹配特定账号下所有消息
agentId: telegram-bot2-agent
路由匹配优先级(从高到低,src/routing/resolve-route.ts):
代码实现为按优先级遍历 tier 数组,在预建倒排索引中查找匹配,第一个命中即为结果:
// src/routing/resolve-route.ts(简化)
const tiers = [
{ matchedBy: "binding.peer", candidates: bindingsIndex.byPeer.get(peerKey) },
{ matchedBy: "binding.peer.parent", candidates: bindingsIndex.byPeer.get(parentPeerKey) },
{ matchedBy: "binding.peer.wildcard", candidates: bindingsIndex.byPeerWildcard },
{ matchedBy: "binding.guild+roles", candidates: bindingsIndex.byGuildWithRoles.get(guildId) },
{ matchedBy: "binding.guild", candidates: bindingsIndex.byGuild.get(guildId) },
{ matchedBy: "binding.team", candidates: bindingsIndex.byTeam.get(teamId) },
{ matchedBy: "binding.account", candidates: bindingsIndex.byAccount },
{ matchedBy: "binding.channel", candidates: bindingsIndex.byChannel },
];
for (const tier of tiers) {
const matched = tier.candidates?.find(
(c) => tier.predicate(c) && matchesBindingScope(c.match, scope)
);
if (matched) return choose(matched.binding.agentId, tier.matchedBy);
}
return choose(resolveDefaultAgentId(cfg), "default");
路由结果通过 WeakMap<OpenClawConfig, Map<cacheKey, ResolvedAgentRoute>> 缓存,最多缓存 4000 条,超限整体清除。
3.1.4 入口安全措施
第一道防线:DM 策略(dmPolicy)
// src/channels/plugins/types.core.ts
export type ChannelSecurityDmPolicy = {
policy: "pairing" | "allowlist" | string;
allowFrom?: Array<string | number> | null;
};
两种核心策略:
pairing模式:用户必须先完成 Pairing 握手(类似扫码授权),Pairing Store 条目与静态allowFrom合并allowlist模式:只有allowFrom列表中的用户 ID 才能对话,Pairing Store 条目被明确忽略(防止通过 Pairing 绕过严格白名单)
// src/channels/allow-from.ts
function mergeDmAllowFromSources({ allowFrom, storeAllowFrom, dmPolicy }) {
// allowlist 模式下绝不合并 Pairing Store,防绕过
const storeEntries = dmPolicy === "allowlist" ? [] : (storeAllowFrom ?? []);
return [...(allowFrom ?? []), ...storeEntries];
}
第二道防线:消息去重(Inbound Dedupe)
防止同一条消息被处理两次(网络重试、多路由并发等场景):
// src/auto-reply/reply/inbound-dedupe.ts
export function buildInboundDedupeKey(ctx: MsgContext): string | null {
// Key = provider | accountId | agentScope | peerId | threadId | messageId
return [provider, accountId, sessionScope, peerId, threadId, messageId]
.filter(Boolean).join("|");
}
去重缓存使用 TTL + maxSize 双重限制,并以 Symbol.for() 挂载到全局,确保即使从不同模块路径进入也命中同一个缓存实例。
3.2 消息处理全链路(Auto-reply)
3.2.1 端到端处理时序
3.2.2 MsgContext:消息的标准化"信封"
每条消息进入系统后,被标准化为 MsgContext,是贯穿整个处理链路的核心数据结构:
export type MsgContext = {
// 消息正文(三种视图)
Body?: string; // 原始正文,用于 UI 显示
BodyForAgent?: string; // 给 AI 的版本(注入了时间戳等)
CommandBody?: string; // 用于命令检测的纯净文本(去掉@提及等)
// 路由标识
From?: string; // "telegram:user:123"
SessionKey?: string; // 已计算好的 SessionKey
AccountId?: string; // 多账号标识
// 消息元数据(来自平台,不可信)
MessageSid?: string; // 消息 ID(用于去重)
ReplyToId?: string; // 被回复消息 ID
InboundHistory?: Array<{ sender; body; timestamp }>; // 群聊近期消息
// 发送者信息(不可信)
SenderName?: string;
SenderId?: string;
// 安全与权限
CommandAuthorized?: boolean;
UntrustedContext?: string[]; // 需要隔离的不可信内容块
};
3.2.3 信任边界:System(可信)vs User(不可信)双层注入
这是 OpenClaw 中最关键的安全设计。给 AI 的输入被明确分为两个层次:
System Prompt 层(可信):由系统生成,攻击者无法控制
// src/auto-reply/reply/inbound-meta.ts
export function buildInboundMetaSystemPrompt(ctx: TemplateContext): string {
// 注意:发件人名字、群组名等用户可控字符串绝不放这里
// 原因1:防止 Prompt Injection 攻击
// 原因2:这些字段变动频繁,会破坏 prompt cache 前缀稳定性
const payload = {
schema: "openclaw.inbound_meta.v1",
chat_id: ctx.OriginatingTo, // 系统路由信息,可信
account_id: ctx.AccountId, // 系统账号 ID,可信
channel: channelValue, // 系统判断的渠道,可信
chat_type: chatType, // 系统判断的会话类型,可信
response_format: resolveInboundFormattingHints(ctx),
};
return `## Inbound Context (trusted metadata)\n...\`\`\`json\n${JSON.stringify(payload)}\n\`\`\``;
}
User 消息层(不可信):用户可控内容,明确标注来源
// src/auto-reply/reply/inbound-meta.ts
export function buildInboundUserContextPrefix(ctx: TemplateContext): string {
blocks.push("Conversation info (untrusted metadata):\n```json\n" + JSON.stringify({
sender: ctx.SenderName, // 用户可控,放 User 层
group_subject: ctx.GroupSubject, // 用户可控
}) + "\n```");
if (ctx.ReplyToBody) {
blocks.push("Replied message (untrusted, for context):\n...");
}
if (ctx.InboundHistory?.length) {
blocks.push("Chat history since last reply (untrusted, for context):\n...");
}
}
这套设计核心目标是防御 Prompt Injection:攻击者无法通过在消息里写 ## Inbound Context 来欺骗 AI,因为这些字段只在 System 层出现,用户输入只出现在 User 层的 untrusted 块中。
同时这也是 Prompt Cache 优化:把每条消息都不同的变量(时间戳、发件人名等)排除在 System Prompt 之外,使同一 Agent 在同一渠道的所有对话的 System Prompt 前缀保持字节级不变,最大化 Claude 等模型的 Prompt Cache 命中率,直接降低 API 成本。
3.2.4 懒加载边界设计
// src/auto-reply/reply/get-reply-run.ts
let agentRunnerRuntimePromise: Promise<typeof import("./agent-runner.runtime.js")> | null = null;
function loadAgentRunnerRuntime() {
// ??= 保证只初始化一次(单例 Promise 模式)
agentRunnerRuntimePromise =
agentRunnerRuntimePromise ?? import("./agent-runner.runtime.js");
return agentRunnerRuntimePromise;
}
const { runReplyAgent } = await loadAgentRunnerRuntime();
最重的 AI 推理模块延迟到真正有消息需要处理时才加载,降低 Gateway 冷启动时间,同时让测试可以精确 mock 这个边界。
3.2.5 Session 历史持久化
Session 历史以两层结构持久化在磁盘:
索引文件(sessions.json),Key 就是 SessionKey:
{
"agent:main:main": {
"sessionId": "uuid-abc123",
"sessionFile": "uuid-abc123.jsonl",
"updatedAt": 1710000000000,
"totalTokens": 12345,
"model": "claude-sonnet-4-6"
}
}
Transcript 文件(<sessionId>.jsonl),完整对话历史,每行一条:
{"type":"session","id":"uuid-abc123","version":1}
{"type":"message","id":"msg-1","message":{"role":"user","content":[{"type":"text","text":"你好"}]}}
{"type":"message","id":"msg-2","message":{"role":"assistant","content":[{"type":"text","text":"你好!"}],"usage":{"input":50,"output":10}}}
Session 有完善的老化淘汰机制(pruneAfter、maxEntries、maxDiskBytes),旧文件重命名为 .jsonl.deleted.<timestamp> 归档而非直接删除。
3.2.6 Context Window 管理
随着对话变长,Token 使用量接近模型 Context Window 上限时,系统会主动触发上下文压缩,防止超限报错。这套压缩机制分为三个阶段(Preflight Compaction → Memory Flush → in-turn Auto Compaction),在 AI 推理执行引擎层实现——详见 §3.3.5 三阶段上下文管理。
3.2.7 群聊消息历史的内存索引
群聊场景下,系统维护内存中的群组消息历史 Map,当机器人被 @ 触发时,把"自上次回复以来的群聊消息"作为上下文注入:
// src/auto-reply/reply/history.ts
export const DEFAULT_GROUP_HISTORY_LIMIT = 50; // 最多 50 条
export const MAX_HISTORY_KEYS = 1000; // 最多 1000 个群组的历史
3.3 AI 推理执行引擎:上下文构建与执行调度
src/auto-reply/reply/ 目录解决的核心问题是:一条消息是如何变成一次 AI 推理调用,并安全地把回复送回去的。
这个执行引擎由两层组成:
| 层次 | 文件 | 角色 |
|---|---|---|
| 装配层 | get-reply-run.ts |
懒加载、前置安全检查、FollowupRun 构建 |
| 执行层 | agent-runner.ts |
队列仲裁、上下文管理、AI Turn 驱动、回复投递 |
3.3.1 System Prompt 的 8 层拼接顺序
attempt.ts 负责组装每次 AI 调用的 System Prompt。拼接顺序设计有严格的缓存稳定性约束:越靠前的内容越稳定(字节级不变),越靠后的内容越动态:
| 层次 | 来源 | 稳定性 |
|---|---|---|
| 1. 核心指令 | agentPrompt / 工作区 AGENTS.md |
★★★★★ 最稳定 |
| 2. 渠道上下文 | ChannelAgentPromptAdapter 注入 |
★★★★☆ |
| 3. 工作区文件 | workspaceDir 下的上下文文件 |
★★★★☆ |
| 4. Context Engine 注入 | 动态感知的环境上下文 | ★★★☆☆ |
| 5. Memory Hook 注入 | 外部记忆后端检索结果 | ★★★☆☆ |
| 6. Session Hook 注入 | 会话级 Hook(session-memory 等) | ★★☆☆☆ |
| 7. Extra System Prompt | 压缩后刷新内容(Post-compaction refresh) | ★★☆☆☆ |
| 8. 渠道元数据 | 当前时间戳、发送者信息等 | ★☆☆☆☆ 最动态 |
设计哲学:稳定内容放前面,可以复用 Provider 的 Prompt Cache(Anthropic/OpenAI 都支持前缀缓存);动态内容放后面,避免破坏缓存前缀,显著降低 API 费用。
3.3.2 FollowupRun:可序列化的运行快照
在执行引擎中流转的核心数据结构是 FollowupRun(src/auto-reply/reply/queue/types.ts)。它是一个可序列化的运行快照,把执行一次 AI 对话所需的全部参数打包在一起,可以安全地入队、出队、重写:
// src/auto-reply/reply/queue/types.ts
export type FollowupRun = {
prompt: string; // 用户原始消息
messageId?: string; // 渠道消息 ID(用于去重)
summaryLine?: string; // 队列丢弃时的摘要行
enqueuedAt: number; // 入队时间戳(毫秒)
// 回复路由:把回复送回哪个渠道/账号/目标
originatingChannel?: OriginatingChannelType;
originatingTo?: string;
originatingAccountId?: string;
originatingThreadId?: string | number;
run: {
agentId: string; // Agent 标识
sessionId: string; // 会话 ID(对应 JSONL Transcript 文件)
sessionFile: string; // Transcript 绝对路径
workspaceDir: string; // 工作区目录(Agent 工具的工作目录)
config: OpenClawConfig; // 整个运行时配置快照
skillsSnapshot?: SkillSnapshot; // 技能快照(保证运行期间技能不会变化)
provider: string; // AI Provider(anthropic / openai / ...)
model: string; // 模型名称
thinkLevel?: ThinkLevel; // 思考深度(low / medium / high)
timeoutMs: number; // 超时(毫秒)
// ...其他执行参数
};
};
FollowupRun 是纯数据对象(无方法),可以跨队列传递,也可以在运行中被安全地重写(例如 Session 轮转后更新 sessionId)。
3.3.3 装配层:get-reply-run.ts
getReplyRun() 是进入执行引擎的大门。它的职责是"把一条入站消息准备好,变成可执行的 FollowupRun"。
关键细节:prepareSessionManagerForRun 会在 AI 推理开始之前就创建好 Transcript 文件(touch 操作),保证后续追加写入时文件一定存在,避免并发竞争。
3.3.4 执行层:队列仲裁策略
进入 runReplyAgent() 后,第一个决策点是队列仲裁:当前有没有正在运行的 AI Turn?如果有,这条新消息应该怎么处理?
这个逻辑封装在 resolveActiveRunQueueAction()(src/auto-reply/reply/queue-policy.ts):
// src/auto-reply/reply/queue-policy.ts
export type ActiveRunQueueAction = "run-now" | "enqueue-followup" | "drop";
export function resolveActiveRunQueueAction(params: {
isActive: boolean; // 当前是否有 AI Turn 正在运行
isHeartbeat: boolean; // 是否是心跳消息
shouldFollowup: boolean;// 是否是后续跟进消息
queueMode: QueueSettings["mode"]; // 队列策略
}): ActiveRunQueueAction {
if (!params.isActive) return "run-now"; // 空闲:立即执行
if (params.isHeartbeat) return "drop"; // 心跳+忙碌:直接丢弃
if (params.shouldFollowup || params.queueMode === "steer") return "enqueue-followup";
return "run-now";
}
五种队列模式(QueueMode)在"有并发"时的行为:
| 模式 | 并发时行为 | 适用场景 |
|---|---|---|
interrupt |
中断当前 Turn,立即执行新消息 | 高优先级打断场景 |
steer |
入队,并把新消息注入当前 Turn 的 Steering 通道 | 流式期间修正方向 |
followup |
入队,等当前 Turn 结束后顺序执行 | 多轮跟进消息 |
collect |
入队,等待并合并多条消息一起执行 | 防抖合并(去重+聚合) |
steer-backlog |
Steer + 维护积压队列 | 高频输入场景 |
3.3.5 三阶段上下文管理
仲裁通过后,在真正发起 AI 调用之前,执行层会依次执行三个上下文管理阶段,确保模型始终在有效的 Context Window 内运行:
阶段一:Preflight Compaction(运行前压缩)
在 Turn 开始前检查 Token 使用量。如果 totalTokens >= contextWindowTokens - reserveTokensFloor - softThresholdTokens,则触发 compactEmbeddedPiSession(),调用内嵌 Pi Agent 对 Transcript 进行摘要压缩。压缩后会将 [Post-compaction context refresh] 注入到下一次请求的 extraSystemPrompt,防止 Agent "遗忘"自己的工作流程。
阶段二:Memory Flush(记忆写入刷新)
这是比 Preflight Compaction 更重量级的操作:专门调用一次 AI Agent,让它把当前对话的重要信息写入磁盘记忆文件(memoryFlushWritePath),再压缩 Transcript。这样就算后续会话重置,关键信息也已持久化。每个 compaction 周期只触发一次(通过 compactionCount 与 memoryFlushCompactionCount 对比判断是否重复):
// src/auto-reply/reply/memory-flush.ts
export function hasAlreadyFlushedForCurrentCompaction(
entry: Pick<SessionEntry, "compactionCount" | "memoryFlushCompactionCount">,
): boolean {
const compactionCount = entry.compactionCount ?? 0;
const lastFlushAt = entry.memoryFlushCompactionCount;
// 幂等保护:本轮压缩已经 Flush 过则跳过
return typeof lastFlushAt === "number" && lastFlushAt === compactionCount;
}
阶段三:in-turn Auto Compaction
Turn 执行完成后,如果 AI 模型在流式输出过程中自动触发了压缩(autoCompactionCount > 0),执行层会同步更新 SessionEntry 的 sessionId,并把新的 sessionId 广播给等待队列中的所有 FollowupRun(refreshQueuedFollowupSession),确保积压消息不会继续使用过期的 Transcript 文件。
// agent-runner.ts:自动压缩后刷新队列中已入队消息的会话信息
refreshQueuedFollowupSession({
key: queueKey,
previousSessionId,
nextSessionId: refreshedSessionEntry.sessionId,
nextSessionFile: refreshedSessionEntry.sessionFile,
});
3.3.6 流式输出与回复投递
AI Turn 执行时,reply-dispatcher.ts 负责异步投递回复。它维护了一个引用计数的发送链(sendChain),确保:
- 工具调用结果(
sendToolResult)、Block 回复(sendBlockReply)、最终回复(sendFinalReply)按序不交叉投递 - 调用
markComplete()后,等所有 pending 发送任务完成才触发onIdle - 外部超时(
BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000ms)兜底,防止渠道发送卡死
3.3.7 设计总结
执行引擎的核心范式是**「快照驱动 + 不可变参数传递 + 幂等去重 + 三阶段上下文管理」**:
FollowupRun是完整的执行快照,运行中不修改原始入参,只通过refreshQueuedFollowupSession在必要时重写队列数据- 每个决策点(队列仲裁、压缩触发、Memory Flush)都有幂等保护,防止同一周期内重复执行
- 上下文窗口管理是主动的:不等模型报错,在 Turn 开始前主动检测、压缩,并在压缩后注入刷新提示,确保 Agent 不会「失忆」
3.4 ACP:子 Agent 控制协议
3.4.1 ACP 是什么
ACP(Agent Control Protocol) 是 OpenClaw 的子 Agent 编排层,基于 @agentclientprotocol/sdk,实现两大场景:
- 外部 AI 接入:Claude Desktop、Cursor 等 AI 客户端通过 ACP 直接操作 OpenClaw 管理的渠道(发消息、读历史等)
- 内部 Agent 编排:OpenClaw 内部 Agent 通过 ACP 派生子 Agent 会话,实现并行任务分发
3.4.2 ACP 会话状态机
3.4.3 Control-Plane:会话管理器核心操作
// src/acp/control-plane/manager.core.ts 核心接口
initializeSession(input: AcpInitializeSessionInput)
// 创建或恢复一个 ACP 子 Agent 会话
// 向 Tasks 注册表记录一个 "running" Task Run
runTurn(input: AcpRunTurnInput)
// 向子 Agent 发送一轮对话
// 通过 onEvent 回调流式接收事件(支持 AbortController 取消)
closeSession(input: AcpCloseSessionInput)
// 关闭会话,清理运行时缓存
// 从 Tasks 注册表标记任务完成
Session 类型:
export type AcpSession = {
sessionId: SessionId;
sessionKey: string; // 与主 SessionKey 体系对齐
cwd: string; // 工作目录
abortController: AbortController | null; // 支持中途取消
activeRunId: string | null;
};
运行时缓存(RuntimeCache) 管理所有活跃 ACP 会话,空闲 TTL 到期后自动驱逐,防止资源泄漏。可观测性快照提供 activeSessions、turns.active、turns.queueDepth、errorsByCode 等指标。
3.4.4 Provenance Mode(溯源模式)
export type AcpProvenanceMode = "off" | "meta" | "meta+receipt";
off:不记录任何溯源信息meta:在 ACP 响应中附带元数据(哪个 Agent 生成的)meta+receipt:元数据 + 回执(已投递确认)
3.5 Cron:定时任务引擎
3.5.1 Cron 概述
OpenClaw 内置了一个完整的定时任务调度系统,让 AI Agent 可以按计划自动执行任务,并把结果投递到指定渠道。这不是简单的 setInterval,而是支持持久化、容错重试、跨 Session 执行的完整调度引擎。
3.5.2 三种调度模式
// src/cron/types.ts
export type CronSchedule =
| { kind: "at"; at: string } // 一次性:指定时间点执行
| { kind: "every"; everyMs: number; anchorMs?: number } // 间隔执行
| {
kind: "cron";
expr: string; // 标准 cron 表达式
tz?: string; // 时区
staggerMs?: number; // 随机抖动,防止惊群效应
};
3.5.3 任务 Payload:两种执行类型
export type CronPayload =
| { kind: "systemEvent"; text: string } // 系统事件,不调用 AI
| {
kind: "agentTurn";
message: string; // 发给 AI 的提示词
model?: string; // 可覆盖 Agent 默认模型
fallbacks?: string[]; // 自定义 fallback 模型链
thinking?: string; // 思维深度(low/medium/high)
timeoutSeconds?: number;
toolsAllow?: string[]; // 工具白名单
lightContext?: boolean; // 轻量 bootstrap 上下文
};
3.5.4 投递系统(Delivery)
任务执行完毕后,结果可以通过多种方式投递:
export type CronDelivery = {
mode: "none" | "announce" | "webhook"; // 不投递/发渠道消息/HTTP 回调
channel?: CronMessageChannel; // 目标渠道(如 telegram)
to?: string; // 目标地址(如群组 ID)
threadId?: string | number; // 线程/话题 ID
accountId?: string; // 多账号场景指定账号
bestEffort?: boolean; // 投递失败是否容忍
failureDestination?: CronFailureDestination; // 失败通知单独地址
};
3.5.5 Cron 执行生命周期
3.5.6 四种 Session 目标
type CronSessionTarget =
| "main" // 使用 Agent 的主 Session
| "isolated" // 每次执行创建独立 Session(无历史污染)
| "current" // 复用当前活跃 Session
| `session:${string}`; // 指定 Session Key
isolated 模式最为常用:每次 Cron 执行都在全新的 Session 中运行,避免历史对话污染定时任务的执行结果。
3.6 Tasks:任务生命周期注册表
3.6.1 Tasks 的作用
src/tasks/ 是一个跨 Runtime 的任务状态追踪系统。无论任务是通过 Cron、ACP、CLI Runner 还是 Sub-Agent 发起的,都统一在 Tasks 注册表中记录和追踪。
3.6.2 任务状态机
// src/tasks/task-registry.types.ts
export type TaskRuntime = "subagent" | "acp" | "cli" | "cron";
export type TaskStatus =
| "queued" // 已入队,等待执行
| "running" // 正在执行
| "succeeded" // 执行成功
| "failed" // 执行失败
| "timed_out" // 超时
| "cancelled" // 被用户取消
| "lost"; // 执行器丢失(进程重启等)
3.6.3 任务记录(TaskRecord)结构
export type TaskRecord = {
taskId: string;
runtime: TaskRuntime; // 执行器类型
requesterSessionKey: string; // 发起者的 SessionKey
ownerKey: string; // 任务归属 Key
scopeKind: "session" | "system";// 会话级或系统级任务
childSessionKey?: string; // 子 Agent 的 SessionKey
parentFlowId?: string; // 所属 Flow(任务流)
parentTaskId?: string; // 父任务 ID(支持嵌套)
label?: string; // 人类可读标签
task: string; // 任务描述
status: TaskStatus;
deliveryStatus: TaskDeliveryStatus;
notifyPolicy: "done_only" | "state_changes" | "silent";
progressSummary?: string; // 进度摘要(用于 UI 显示)
terminalSummary?: string; // 最终结果摘要
};
Tasks 还支持 TaskFlow(任务流):多个 Task 组成一个有依赖关系的 Flow,统一管理生命周期,实现复杂的 Agent 工作流编排。
3.7 Hooks:可插拔事件钩子
3.7.1 5 类事件类型
// src/hooks/internal-hooks.ts
export type InternalHookEventType =
| "command" // 命令处理前/后
| "session" // Session 创建/更新
| "agent" // Agent bootstrap(工作空间初始化)
| "gateway" // Gateway 启动
| "message"; // 消息接收/发送
3.7.2 内置 Bundled Hooks
| Hook | 位置 | 功能 |
|---|---|---|
boot-md |
src/hooks/bundled/boot-md/ |
Gateway 启动时注入 Markdown 文件到 Agent 上下文 |
command-logger |
src/hooks/bundled/command-logger/ |
记录所有命令调用日志 |
session-memory |
src/hooks/bundled/session-memory/ |
Session 级别记忆注入(跨 Session 保留关键信息) |
bootstrap-extra-files |
src/hooks/bundled/bootstrap-extra-files/ |
启动时注入额外文件到 Agent 工作空间 |
3.7.3 高级用法:Gmail Watcher Hook
src/hooks/gmail-watcher.ts 展示了 Hook 系统的高级用法:监听 Gmail 收件箱,当有新邮件时触发 message Hook 事件,由 AI Agent 处理邮件内容并自动回复或归档。
3.8 MCP:Model Context Protocol 集成
3.8.1 作为 MCP 服务端
src/mcp/ 实现了 MCP 服务端,把 OpenClaw 管理的渠道能力暴露为 MCP 工具,供支持 MCP 的 AI 客户端(Claude Desktop、Cursor 等)直接调用:
// src/mcp/channel-server.ts
export async function createOpenClawChannelMcpServer(opts) {
const server = new McpServer({ name: "openclaw", version: VERSION });
const bridge = new OpenClawChannelBridge(cfg, opts);
bridge.setServer(server);
// 注册渠道工具:send_message、list_channels、get_history 等
registerChannelMcpTools(server, bridge);
return { server, bridge, start, close };
}
3.8.2 作为 MCP 客户端
OpenClaw 的 Agents 模块也支持作为 MCP 客户端连接到外部 MCP 服务器,通过三种传输方式:
| 传输方式 | 文件 | 适用场景 |
|---|---|---|
| stdio | src/agents/mcp-stdio.ts |
本地子进程 MCP 服务 |
| SSE | src/agents/mcp-sse.ts |
HTTP Server-Sent Events |
| HTTP | src/agents/mcp-http.ts |
HTTP Streamable MCP |
3.9 Secrets:统一凭证管理
3.9.1 SecretRef 体系
src/secrets/ 实现了统一的密钥引用体系,API Key 等敏感信息不直接存储在配置文件中,而是通过 SecretRef 描述来源:
// 三种密钥来源
type SecretRefSource = "env" | "file" | "exec";
// 示例:从环境变量读取 OpenAI API Key
{ source: "env", provider: "openai", id: "OPENAI_API_KEY" }
// 示例:从文件读取(支持 JSON Pointer 路径)
{ source: "file", provider: "anthropic", id: "/credentials/api_key" }
// 示例:执行外部命令获取(集成 1Password、Vault 等)
{ source: "exec", provider: "custom", id: "op read op://vault/item/field" }
运行时通过 src/secrets/runtime.ts 动态解析 SecretRef,支持凭证矩阵(多组 API Key 轮换)和 OAuth Token 自动刷新。
3.10 Media Understanding:多模态理解
3.10.1 三类多模态能力
src/media-understanding/ 统一管理三类多模态理解:
| 能力 | 类型 | 支持 Provider |
|---|---|---|
| 音频转录 | audio.transcription |
Deepgram、ElevenLabs、Groq (Whisper) |
| 视频描述 | video.description |
Zai、自定义 Provider |
| 图像描述 | image.description |
OpenAI、Anthropic、Google Vision |
3.10.2 统一 Provider 接口
export type MediaUnderstandingProvider = {
id: string;
capabilities?: MediaUnderstandingCapability[];
transcribeAudio?: (req: AudioTranscriptionRequest) => Promise<AudioTranscriptionResult>;
describeVideo?: (req: VideoDescriptionRequest) => Promise<VideoDescriptionResult>;
describeImage?: (req: ImageDescriptionRequest) => Promise<ImageDescriptionResult>;
};
媒体文件被转录/描述后,文本内容作为上下文注入到 AI 对话中,实现"发一张图片/音频/视频,AI 能理解并回答"的多模态交互。
3.11 其他模块简述
3.11.1 TUI 终端界面(src/tui/)
实现了功能完整的终端聊天 UI:
- 富文本 Markdown 渲染(代码高亮、表格、列表)
- 实时流式输出(token 逐字显示)
- Tool Call 可视化(执行中/执行结果展示)
- Session 切换与管理
- OSC8 超链接支持(终端可点击链接)
组件基于 @inkjs 构建,采用类 React 的声明式组件模型。
3.11.2 Daemon 守护进程(src/daemon/)
- macOS 通过 launchd 管理 Gateway 进程
entry.respawn.ts实现监控重启逻辑,进程崩溃后自动拉起- 支持优雅停机(SIGTERM 信号处理,等待活跃请求完成)
3.11.3 TTS 语音合成(src/tts/)
统一的 TTS 抽象层,支持 ElevenLabs、Deepgram 等 Provider,配合 voice-call 插件实现语音通话 AI 助手。
3.11.4 Image/Video Generation(src/image-generation/ src/video-generation/)
AI 生图/生视频的核心抽象,Provider 实现在 extensions/fal/、extensions/image-generation-core/ 等插件中。
3.11.5 Canvas Host(src/canvas-host/)
实现 A2UI 协议的交互式 Canvas 渲染,允许 AI Agent 渲染结构化 UI 组件到客户端(图表、表单、交互按钮等)。
3.11.6 Memory Host SDK(src/memory-host-sdk/)
外部向量数据库记忆后端接入层,extensions/memory-lancedb/ 实现了基于 LanceDB 的向量记忆,支持语义搜索历史对话。
3.11.7 Pairing 配对(src/pairing/)
移动端设备(iOS/Android)通过 QR 码扫描与 Gateway 建立 WebSocket 长连接的配对认证流程。
3.11.8 Flows 引导流(src/flows/)
Channel 配置向导流、Model 选择流等引导界面逻辑,用于首次设置时的交互式配置。
3.12 Plugin-SDK 与 Extensions 生态
3.12.1 三类插件合同
src/plugin-sdk/ 是第三方插件的唯一公开合同,定义了三种插件类型:
| 插件类型 | 入口文件 | 说明 |
|---|---|---|
| Channel Plugin | channel-contract.ts |
接入新的消息渠道 |
| Provider Plugin | provider-entry.ts |
接入新的 AI 模型 Provider |
| General Plugin | plugin-entry.ts |
通用扩展(工具、Hook、能力扩展等) |
3.12.2 插件命名规范
每个 bundled 插件遵守严格的命名约定,确保多处 ID 对齐:
// extensions/<id>/openclaw.plugin.json
{
"id": "telegram", // 插件 ID
"openclaw.install.npmSpec": "@openclaw/telegram", // npm 包名
"openclaw.channel.id": "telegram" // 渠道 ID(Channel 插件必须)
}
3.12.3 Extensions 生态全景
四、总结:技术与产品的双重视角
4.1 架构设计亮点速览
读完全部源码,有 13 个设计决策值得重点标记——它们不是孤立的技巧,而是 OpenClaw 能够在"100+ 插件、15+ 消息渠道、30+ AI Provider"规模下依然稳定运行的结构性原因:
| 设计点 | 实现方式 | 核心价值 |
|---|---|---|
| 渠道扩展性 | ChannelPlugin 组合式适配器 | 新渠道无需修改核心代码,按需实现 Adapter |
| 会话隔离 | 多粒度 dmScope(4 种),SessionKey 体系 | 精确控制 DM 隔离策略,支持多账号场景 |
| 路由精细化 | 8 级 Binding 规则 + 倒排索引缓存 | 多 Agent 场景灵活分发,缓存加速 |
| 安全防护 | dmPolicy + allowFrom + 信任边界分层 | 防未授权访问 + Prompt Injection 双重防护 |
| Prompt Cache 稳定性 | System Prompt 8 层拼接,动态字段后置 | 降低 API 成本,字节级前缀稳定 |
| 子 Agent 编排 | ACP + Tasks 注册表 | 跨 Runtime 统一任务追踪,支持嵌套 Agent |
| 定时任务 | Cron 三种调度模式 + Delivery 投递 | AI 定时执行并推送结果到任意渠道 |
| 凭证安全 | SecretRef 三种来源 + 运行时解析 | 密钥不落配置文件,支持外部密钥管理器 |
| 多模态能力 | Media Understanding + TTS + 生成 | 图像/音频/视频统一接入,多 Provider 可选 |
| 插件生态 | Plugin-SDK + ~100 个 bundled 插件 | 开放生态,第三方插件零侵入接入 |
| 懒加载 | 动态 import 边界 + 单例 Promise | 降低冷启动延迟,测试可精确 mock |
| 历史持久化 | JSONL Transcript + 生命周期维护 | 可靠对话历史,支持 Memory Flush 压缩 |
| Agent Harness | FollowupRun 快照 + 三阶段上下文管理 + 队列仲裁 | 并发安全、Context 自动管理、记忆持久化 |
4.2 设计哲学:边界即架构
OpenClaw 最值得深究的,是它对边界的执着——这不是代码风格偏好,而是整个系统能否扩展的根本。
| 边界类型 | 体现 | 为什么重要 |
|---|---|---|
| 模块边界 | Plugin-SDK 是唯一公开合同,核心从不调用插件内部实现 | 100+ 插件不会随便"污染"核心逻辑,插件可独立迭代 |
| 信任边界 | System Prompt 只放可信内容,用户消息严格标注 untrusted |
Prompt Injection 攻击面大幅缩小 |
| 类型边界 | 大量使用 discriminated union 和 closed error code | 消灭 magic string 分支,IDE 可全量静态检查 |
| 运行时边界 | 懒加载 + *.runtime.ts 命名约定 |
测试可精确隔离重量级模块,冷启动性能有保障 |
四类边界各自解决了一种"扩散风险":
- 模块边界 阻止插件实现细节渗入核心——新增或修改一个插件,不需要理解核心代码,也不会意外破坏其他插件
- 信任边界 阻止用户输入污染系统指令——攻击面被收窄到明确标注的
untrusted区域,而不是整个 Prompt - 类型边界 阻止错误的控制流在运行时才暴露——编译期就能发现,改一处代码影响的范围由 TypeScript 全量告知
- 运行时边界 阻止重量级模块在不需要时被加载——测试只引入必要的部分,冷启动不因某个插件膨胀而变慢
叠加起来,任何局部改动的影响范围都被明确限定:加一个新渠道插件不需要回归全部 30+ Provider,修一个 Provider Bug 不需要担心破坏 Cron 或 Hooks。这正是为什么一个个人开源项目,在没有大型工程团队支撑的情况下,能稳定维护 100+ 插件、15+ 渠道的庞大生态。
4.3 产品护城河:被低估的形态优势
理解了技术设计之后,有一个更重要的问题值得正视:OpenClaw 各个技术模块单独看,并不存在特别高的壁垒。
- LLM 调用本质是 SDK 封装;消息路由是成熟的规则引擎思路;JSONL 持久化是标准日志模式;Plugin Registry 模式在企业中间件领域早已普及。
如果只看技术实现,同类开源项目都有覆盖。OpenClaw 的真正护城河,建立在三个产品层面的选择上。
① 消除上下文切换:AI-in-your-inbox
传统 AI 助手: 用户 → 打开新 App → 输入问题 → 等待回复
OpenClaw: 用户 → 照常发 Telegram/iMessage → AI 自动回复,就在对话里
这个差异触达了用户行为最深层的惰性——人不愿意切换上下文。不需要记住新入口,不需要学习新 UI,AI 就活在你每天已经打开的聊天窗口里。这是任何纯技术优化都无法替代的产品决策。
② 渠道覆盖的积累型壁垒
OpenClaw 接入了 15+ 消息渠道:Telegram、Discord、Slack、Signal、iMessage、WhatsApp Web,以及插件扩展的 Matrix、Zalo、Voice Call 等。
渠道覆盖不是线性叠加,而是网络效应型的护城河:
- 每新增一个渠道,所有已有用户立刻获得新接入点,无需迁移数据
- 每个渠道适配背后都是深度的工程投入(消息格式差异、图片上传 API、Thread 模型、速率限制……)——复制一个容易,复制 15 个需要数年积累
- 对企业场景,内部 Slack Bot + 外部 Telegram Bot 共用同一套 AI 配置和记忆,这是横向平台才能做到的
③ 管道化架构 + 私有部署:数据主权与粘性双锁定
OpenClaw 的架构是管道优先的:AI 模型本身是可替换的组件,而消息管道——接收、路由、执行、回复、持久化——才是稳定的核心资产。
这带来两个粘性来源:
数据粘性:用户的 Transcript 历史、Memory 文件、技能配置都沉淀在自己的 OpenClaw 实例里。换一个 AI 助手产品,这些积累全部清零。
主权粘性:OpenClaw 天然支持完全私有化部署。gateway.mode = local 让整个 Gateway 跑在本地机器;Session JSONL 和 Memory 文件落在 ~/.openclaw/,从不经过任何第三方服务器;API Key 通过 SecretRef 体系以 env(环境变量)、file(本地文件)或 exec(调用外部密钥管理器如 1Password/Vault)的方式读取,密钥从不明文写入配置文件。官方甚至专门提供了树莓派部署文档——35 美元的设备 + 自己的 API Key,就能搭建一个 24 小时在线、数据完全自有的 AI 助手,成本远低于任何 SaaS 订阅。
4.4 结语
OpenClaw 给出了一个有趣的答案:AI 时代最有价值的不一定是"最聪明的 AI",而是"让 AI 最容易被用上的基础设施"。
从源码层面看,它的边界设计、懒加载策略、三阶段上下文管理、幂等保护,全都在服务同一个产品目标:让 AI 在用户已有的消息流里稳定、低成本、无感地存在。技术架构的每一个决策,都是在降低"使用 AI"的摩擦系数。
从产品层面看,它选择了一个独特的竞争位置——不做"更强的模型",不做"更好看的聊天框",而是做人与 AI 之间的连接层:把用户已有的沟通习惯(发消息)直接变成与 AI 交互的方式,同时让用户的数据和记忆沉淀在自己手里。
当大模型能力趋于同质、AI 产品竞争白热化,OpenClaw 的这种定位反而越来越清晰:它不是在和 ChatGPT 竞争谁更聪明,而是在占据一个 LLM 厂商天然无法直接复制的位置——每个人消息流的入口。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)