主题
OpenClaw 源码解读(十七)自动回复引擎
一、导读
自动回复引擎(Auto-Reply Engine)是 OpenClaw 的心脏。当一条消息从 WhatsApp、Telegram、Discord、Slack 或任何渠道进入系统时,正是这个引擎决定:要不要回复?用哪个 Agent 回复?怎么回复?回复发到哪里?
它的源码位于 src/auto-reply/,约 100+ 个文件,是整个项目中最庞大、最复杂的子系统。本文沿着一条消息从"入站"到"出站"的完整生命旅程来拆解这套引擎:
入站消息
│
▼
渠道 Monitor → 防抖 → 去重 → allowFrom 检查 → @提及门控
│
▼
dispatchInboundMessage() ← 统一入口
│
▼
getReplyFromConfig() ← 引擎核心
├── 命令检测 & 解析
├── 指令处理(模型/思考级别/权限提升)
├── 内联动作处理
├── Agent Runner → AI 模型调用
└── 回复构造
│
▼
ReplyDispatcher → 回复标准化 → 渠道投递自动回复引擎/01-infographic-message-lifecycle-1775150737800.png)
二、消息上下文 —— MsgContext
templating.ts 中的 MsgContext 是整个自动回复引擎的数据载体,定义了入站消息的所有维度:
| 分类 | 典型字段 | 说明 |
|---|---|---|
| 消息体 | Body, BodyForAgent, RawBody, CommandBody | 4 种不同用途的消息体 |
| 发送者 | From, SenderId, SenderName, SenderUsername | 发送者标识 |
| 会话 | SessionKey, AccountId, ParentSessionKey | 会话与账户路由 |
| 消息ID | MessageSid, ReplyToId, RootMessageId | 消息链路追踪 |
| 引用 | ReplyToBody, ReplyToSender, ReplyToIsQuote | 引用消息上下文 |
| 转发 | ForwardedFrom, ForwardedFromType, ForwardedDate | 转发消息元数据 |
| 线程 | ThreadStarterBody, ThreadHistoryBody, ThreadLabel | 线程上下文 |
| 媒体 | MediaPath, MediaUrl, MediaType, Sticker | 多媒体附件 |
| 群聊 | ChatType, GroupSubject, GroupMembers | 群聊元数据 |
| 路由 | OriginatingChannel, OriginatingTo | 回复路由信息 |
| 权限 | CommandAuthorized, GatewayClientScopes | 命令授权状态 |
四种消息体的区别:
Body:原始消息文本BodyForAgent:Agent 提示词(可能包含信封/历史/上下文)RawBody/CommandBody:用于命令检测(去除结构化上下文后的纯文本)BodyForCommands:命令解析专用(最干净的文本)
FinalizedMsgContext 是 MsgContext 的安全化版本——CommandAuthorized 字段被强制为 boolean(默认 false),实现"默认拒绝"的安全策略。
自动回复引擎/02-infographic-msg-context-structure-1775150738528.png)
三、入站消息分发 —— dispatch.ts
dispatchInboundMessage() 是所有渠道消息进入自动回复引擎的统一入口:
typescript
async function dispatchInboundMessage(params: {
ctx: MsgContext | FinalizedMsgContext;
cfg: OpenClawConfig;
dispatcher: ReplyDispatcher;
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
replyResolver?: typeof getReplyFromConfig;
}): Promise<DispatchInboundResult> {
const finalized = finalizeInboundContext(params.ctx);
return await withReplyDispatcher({
dispatcher: params.dispatcher,
run: () => dispatchReplyFromConfig({ ... }),
});
}三个关键设计:
- Context 安全化:
finalizeInboundContext()将CommandAuthorized默认为 false - Dispatcher 生命周期保证:
withReplyDispatcher()确保无论成功还是异常,Dispatcher 都会调用markComplete()+waitForIdle()——这防止了回复丢失 - 三种分发变体:
dispatchInboundMessage:接收外部 DispatcherdispatchInboundMessageWithDispatcher:自动创建基础 DispatcherdispatchInboundMessageWithBufferedDispatcher:创建带 Typing 指示器的 Dispatcher
自动回复引擎/03-infographic-dispatch-entry-1775150739592.png)
四、入站信封格式化 —— envelope.ts
Agent 看到的不是裸消息,而是带有丰富元数据的信封格式:
[WhatsApp Alice +2min Thu 2026-03-25 14:30 CST] Hello, how are you?formatAgentEnvelope() 的信封结构:
[渠道名 发送者 +距上条消息的时间差 星期 时间戳] 消息正文安全设计——sanitizeEnvelopeHeaderPart() 对信封头部做了三重防护:
- 换行替换为空格(防止换行注入)
[]替换为()(防止括号逃逸)- 多空格折叠
时间戳支持 4 种时区模式:local(系统时区)、utc、user(用户配置的时区)、或直接指定 IANA 时区。信封还会包含星期前缀(Thu、Mon 等),因为小模型在根据日期推导星期方面出了名的不可靠。
自动回复引擎/04-infographic-envelope-format-1775150740419.png)
五、入站防抖 —— inbound-debounce.ts
当用户快速连发多条消息时,防抖器会将它们合并为一次处理:
typescript
function createInboundDebouncer<T>(params: {
debounceMs: number;
buildKey: (item: T) => string | null; // 按 session key 分组
shouldDebounce?: (item: T) => boolean;
onFlush: (items: T[]) => Promise<void>;
})关键行为:
debounceMs > 0且shouldDebounce返回 true 时,消息进入缓冲区- 每次新消息到达重置定时器(经典的"滑动窗口"防抖)
- 命令消息(
/status、/new等)不参与防抖,立即处理 - 支持 per-channel 的防抖时间覆盖
自动回复引擎/05-infographic-debounce-mechanism-1775150741308.png)
六、命令检测 —— command-detection.ts
在消息进入 AI 之前,先检测是否是控制命令:
typescript
function isControlCommandMessage(text?: string, cfg?, options?): boolean {
// 1. 检测斜杠命令(/status, /new, /model ...)
// 2. 检测中止触发词(如 stop, cancel)
return hasControlCommand(trimmed, cfg, options) || isAbortTrigger(normalized);
}hasControlCommand() 的匹配逻辑:
- 遍历所有注册命令的
textAliases - 精确匹配(
/status=/status) - 前缀匹配(
/model gpt-4匹配/model命令,前提是acceptsArgs为 true)
hasInlineCommandTokens() 是一个粗粒度的预检——通过正则 /(?:^|\s)[/!][a-z]/i 快速判断消息中是否有内联命令标记(如 hey /status),让渠道 monitor 决定是否需要计算 CommandAuthorized。
自动回复引擎/06-infographic-command-detection-1775150742053.png)
七、回复引擎核心 —— getReplyFromConfig()
这是整个自动回复系统最核心的函数(~400 行),位于 reply/get-reply.ts。它的处理流程可以分为 8 个阶段:
阶段 1:Agent & 模型解析
agentId = resolveSessionAgentId(sessionKey, config)
{ defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel(cfg, agentId)支持三层模型覆盖:
- 心跳模型覆盖(
heartbeatModelOverride) - 会话模型覆盖(
sessionEntry.modelOverride) - 渠道模型覆盖(
channelModelOverride)
阶段 2:Workspace 初始化
workspace = ensureAgentWorkspace({ dir, ensureBootstrapFiles })确保 Agent 工作目录存在,包括引导文件(如 SYSTEM.md)。
阶段 3:Media & Link 理解
applyMediaUnderstanding({ ctx, cfg, agentDir, activeModel })
applyLinkUnderstanding({ ctx, cfg })将图片、音频、视频、链接等非文本内容转换为 Agent 可理解的文本描述。
阶段 4:命令授权 & 会话初始化
resolveCommandAuthorization({ ctx, cfg, commandAuthorized })
sessionState = initSessionState({ ctx, cfg, commandAuthorized })这一步会解析会话状态(新建/恢复/重置),包括 20+ 个返回值:sessionEntry、sessionStore、sessionKey、isNewSession、resetTriggered 等。
阶段 5:指令解析
directiveResult = resolveReplyDirectives({ ... 30+ 参数 ... })指令系统是一个独立的子系统,解析消息中的内联指令(如模型选择、思考级别、权限提升等)。如果指令本身产生回复(如 /status 命令),直接返回。
阶段 6:内联动作处理
inlineActionResult = handleInlineActions({ ... })处理不需要 AI 参与的快速动作(如 /new 重置会话、/compact 压缩上下文)。
阶段 7:沙箱媒体暂存
stageSandboxMedia({ ctx, sessionCtx, cfg, sessionKey, workspaceDir })将用户发送的媒体文件复制到 Agent 沙箱的工作目录。
阶段 8:Agent 运行
return runPreparedReply({ ... 40+ 参数 ... })最终调用 Agent Runner 执行 AI 推理。
自动回复引擎/07-infographic-eight-stage-pipeline-1775150742792.png)
八、Agent Runner —— AI 调用引擎
reply/agent-runner.ts 中的 runReplyAgent() 是实际调用 AI 模型的地方(~250+ 行)。
8.1 Steer 机制
如果当前会话已有活跃的 AI 运行,且新消息到达:
typescript
if (shouldSteer && isStreaming) {
const steered = queueEmbeddedPiMessage(followupRun.run.sessionId, followupRun.prompt);
if (steered && !shouldFollowup) {
typing.cleanup();
return undefined; // 消息已注入到活跃运行中
}
}queueEmbeddedPiMessage 尝试将新消息"注入"到正在运行的 Agent turn 中——而不是排队等待。这就是"Steer"(转向)机制:让 Agent 在运行中感知到新的用户输入。
8.2 队列策略
当 Steer 不可用时,队列策略决定消息命运:
| 动作 | 含义 |
|---|---|
drop | 丢弃(心跳消息在 Agent 忙时被丢弃) |
enqueue-followup | 排入后续队列,等当前 run 结束后执行 |
| 继续执行 | 直接启动新的 Agent turn |
8.3 Memory Flush
在 Agent 运行前,检查是否需要刷新记忆:
typescript
activeSessionEntry = await runMemoryFlushIfNeeded({
cfg, followupRun, promptForEstimate,
sessionCtx, defaultModel, agentCfgContextTokens,
// ...
});当上下文窗口即将溢出时,自动将历史消息压缩/存储到长期记忆。
8.4 Fallback 机制
runAgentTurnWithFallback() 支持模型降级——当主模型调用失败时,自动切换到备用模型重试。
8.5 Block Streaming
当启用分块流式回复时,Agent 的输出会被拆分成多个小块逐步发送:
typescript
const blockReplyPipeline = createBlockReplyPipeline({
onBlockReply: opts.onBlockReply,
timeoutMs: blockReplyTimeoutMs,
coalescing: blockReplyCoalescing,
buffer: createAudioAsVoiceBuffer({ isAudioPayload }),
});分块参数包括最小/最大字符数、断行策略(paragraph/newline/sentence)、超时时间。
自动回复引擎/08-infographic-agent-runner-mechanisms-1775150743718.png)
九、回复分发器 —— ReplyDispatcher
reply/reply-dispatcher.ts 是回复投递的关键中间层。
9.1 三种回复类型
typescript
type ReplyDispatcher = {
sendToolResult: (payload) => boolean; // 工具调用结果
sendBlockReply: (payload) => boolean; // 流式分块回复
sendFinalReply: (payload) => boolean; // 最终完整回复
waitForIdle: () => Promise<void>;
markComplete: () => void;
};9.2 串行化投递
所有回复通过 Promise chain 严格串行化:
typescript
sendChain = sendChain
.then(async () => {
if (shouldDelay) {
await sleep(getHumanDelay(options.humanDelay));
}
await options.deliver(normalized, { kind });
})
.catch((err) => options.onError?.(err, { kind }))
.finally(() => {
pending -= 1;
if (pending === 0) {
unregister();
options.onIdle?.();
}
});9.3 Human Delay
一个有趣的设计——可以配置"仿人类延迟":
typescript
const DEFAULT_HUMAN_DELAY_MIN_MS = 800;
const DEFAULT_HUMAN_DELAY_MAX_MS = 2500;在分块回复之间随机插入 800ms~2500ms 的延迟,让回复看起来更像真人在打字。第一个分块不延迟(避免初始等待感)。
9.4 预留计数器
Dispatcher 使用"预留"计数器防止过早触发 idle:
创建时: pending = 1 (预留位)
每次入队: pending += 1
每次完成: pending -= 1
markComplete: 释放预留位
pending === 0: 触发 idle这保证了即使所有回复都瞬间完成,也不会在 markComplete() 之前误触发 idle。
自动回复引擎/09-infographic-reply-dispatcher-1775150744527.png)
十、回复载荷 —— ReplyPayload
typescript
type ReplyPayload = {
text?: string; // 文本内容
mediaUrl?: string; // 单个媒体 URL
mediaUrls?: string[]; // 多个媒体 URL
replyToId?: string; // 引用回复的消息 ID
replyToTag?: boolean; // 是否添加引用标记
replyToCurrent?: boolean; // 引用当前消息
audioAsVoice?: boolean; // 音频作为语音气泡发送
isError?: boolean; // 错误消息标记
isReasoning?: boolean; // 思考/推理过程标记
channelData?: Record<string, unknown>; // 渠道特定数据
};特别注意 isReasoning 字段——它标记了 AI 的"思考过程"内容。不同渠道的处理策略不同:Discord 可以用折叠块展示,WhatsApp/Web 则完全抑制。
十一、静默回复令牌
typescript
export const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
export const SILENT_REPLY_TOKEN = "NO_REPLY";这两个令牌是 Agent 与引擎之间的"暗号":
- NO_REPLY:Agent 认为不需要回复(比如消息不是对它说的,或者没有需要回答的内容)
- HEARTBEAT_OK:心跳检查回复(定时探活,不应发送给用户)
isSilentReplyText() 使用精确匹配——只有整条消息是 NO_REPLY(允许前后空白)才算静默。这防止了 #19537 问题:实质性回复恰好以 NO_REPLY 结尾被误吞。
stripSilentToken() 更宽松——支持从混合内容中剥离尾部的 NO_REPLY 令牌。
自动回复引擎/10-infographic-silent-tokens-1775150745456.png)
十二、Typing 控制器
自动回复引擎在 Agent 运行期间会模拟"正在输入"状态:
typescript
const typing = createTypingController({
onReplyStart: opts?.onReplyStart,
onCleanup: opts?.onTypingCleanup,
typingIntervalSeconds: 6, // 默认每 6 秒发一次 typing
silentToken: SILENT_REPLY_TOKEN,
});Typing 策略按消息来源分级:
| 策略 | 适用场景 | 行为 |
|---|---|---|
user_message | 用户直接消息 | 完整 typing 指示 |
system_event | 系统事件触发 | 轻量 typing |
internal_webchat | Web 控制台消息 | 轻量 typing |
heartbeat | 定时心跳 | 可完全抑制 |
auto | 自动检测 | 根据上下文决定 |
十三、指令系统
reply/directives.ts 和 directive-handling.*.ts 实现了丰富的内联指令:
13.1 模型选择指令
/model gpt-4 → 切换模型
/model list → 列出可用模型
/model status → 显示当前模型支持模型别名解析(alias → provider/model)。
13.2 思考级别指令
/think high → 设置思考级别
/think off → 关闭思考6 个思考级别:off、minimal、low、medium、high、xhigh。
13.3 权限提升指令
/elevate → 提升为 elevated 模式Elevated 模式解锁更强的工具权限,但受 allowlist 控制。
13.4 快速通道
directive-handling.fast-lane.ts 实现了"快速通道"——某些指令(如 /status、/help)不需要经过 AI,可以直接生成回复并提前返回。
自动回复引擎/11-infographic-directive-system-1775150746257.png)
十四、命令系统
reply/commands-core.ts 和 commands-*.ts 实现了 30+ 个斜杠命令:
| 命令文件 | 命令 | 功能 |
|---|---|---|
commands-session.ts | /new, /reset, /compact | 会话管理 |
commands-models.ts | /model | 模型选择 |
commands-config.ts | /set, /unset | 配置修改 |
commands-bash.ts | /bash | 执行 shell 命令 |
commands-tts.ts | /tts | 文字转语音 |
commands-subagents.ts | /agents | 子 Agent 管理 |
commands-compact.ts | /compact | 上下文压缩 |
commands-approve.ts | /approve | 工具执行审批 |
commands-system-prompt.ts | /system | 查看系统提示词 |
commands-context-report.ts | /context | 上下文使用报告 |
commands-export-session.ts | /export | 导出会话 |
commands-acp.ts | /acp | ACP 控制 |
命令授权是双层的:
CommandAuthorized—— 渠道级别的授权(由 allowFrom 决定)command-gates.ts—— 命令级别的门控(某些命令需要特定权限)
十五、回复标准化
normalize-reply.ts 在回复发送前做最后一轮处理:
- 静默回复过滤:
NO_REPLY→ 跳过 - 心跳令牌剥离:
HEARTBEAT_OK→ 跳过 - 空回复检测:无文本且无媒体 → 跳过
- Response Prefix 注入:在回复文本前添加可配置的前缀
- 模板插值:前缀支持
、等变量
Skip 原因被回调通知(onSkip),让渠道可以做差异化处理(比如 Telegram 在空回复时发送默认消息)。
十六、队列系统
reply/queue/ 实现了 per-session 的消息队列:
16.1 入队
typescript
enqueueFollowupRun(queueKey, followupRun, resolvedQueue)当 Agent 正在处理消息时,新消息进入 followup 队列。
16.2 消耗
typescript
drainQueue(queueKey, resolvedQueue)当前 turn 结束后,从队列中取出下一条消息处理。
16.3 队列模式
| 模式 | 行为 |
|---|---|
replace | 新消息替换队列中的旧消息(只保留最新) |
append | 新消息追加到队列尾部 |
drop | 队列满时丢弃新消息 |
16.4 清理
cleanup.ts 定期清理过期的队列条目,防止内存泄漏。
十七、各渠道的接入模式
所有渠道都通过相同的模式接入自动回复引擎:
渠道 Monitor (监听消息)
→ 构造 MsgContext (填充渠道特定字段)
→ 创建 ReplyDispatcher (渠道特定的投递函数)
→ 调用 dispatchInboundMessage()
→ 处理投递结果各渠道的差异主要在两处:
- MsgContext 填充:Telegram 有 sticker/topic,Discord 有 Components v2,Slack 有 thread resolution
- 回复投递:每个渠道有自己的
deliver-reply.ts,处理渠道特定的消息格式、分块限制、媒体上传
十八、心跳系统
src/infra/heartbeat-runner.ts 实现了定时触发的 Agent 回复:
Cron 定时器 → heartbeat-runner → getReplyFromConfig(isHeartbeat=true)
→ Agent 运行 → 如果回复非 NO_REPLY → 投递到渠道心跳的独特之处:
isHeartbeat: true标记,让引擎知道这是系统触发而非用户消息- 独立的模型覆盖(
heartbeatModelOverride) - 在 Agent 忙时直接 drop(不排队)
- Typing 指示器可配置抑制
十九、安全设计
| 层级 | 安全措施 |
|---|---|
| 上下文安全化 | finalizeInboundContext 默认 CommandAuthorized=false |
| 信封注入防护 | 换行/括号转义 |
| 命令授权 | 双层门控(渠道级 + 命令级) |
| 静默回复 | 精确匹配防误吞 |
| 队列溢出 | 模式化策略(replace/drop/append) |
| Dispatcher 生命周期 | 预留计数器 + finally 保证 |
| 防抖 | per-session 滑动窗口 |
二十、设计模式总结
| 模式 | 应用位置 | 效果 |
|---|---|---|
| Pipeline | getReplyFromConfig 8 阶段 | 清晰的处理流水线 |
| Strategy | 队列模式(replace/append/drop) | 可配置的队列行为 |
| Chain of Responsibility | 指令 → 内联动作 → Agent | 层层过滤,提前返回 |
| Observer | ReplyDispatcher callbacks | 投递事件通知 |
| Debounce | inbound-debounce | 防止消息风暴 |
| Steer | queueEmbeddedPiMessage | 运行中注入新消息 |
| Reservation Counter | Dispatcher pending | 防止过早 idle |
| Human Delay | 分块回复间随机延迟 | 仿人类节奏 |
| Token Protocol | NO_REPLY / HEARTBEAT_OK | Agent-引擎暗号 |
| Envelope | formatAgentEnvelope | 结构化消息上下文 |
自动回复引擎/12-infographic-design-patterns-1775150747219.png)
二十一、推荐阅读顺序
src/auto-reply/tokens.ts— 静默回复令牌src/auto-reply/types.ts— 核心类型(GetReplyOptions, ReplyPayload)src/auto-reply/templating.ts— MsgContext 消息上下文src/auto-reply/envelope.ts— 信封格式化src/auto-reply/command-detection.ts— 命令检测src/auto-reply/inbound-debounce.ts— 入站防抖src/auto-reply/dispatch.ts— 分发入口src/auto-reply/reply/reply-dispatcher.ts— 回复分发器src/auto-reply/reply/normalize-reply.ts— 回复标准化src/auto-reply/reply/inbound-context.ts— Context 安全化src/auto-reply/reply/get-reply-directives.ts— 指令解析src/auto-reply/reply/get-reply-inline-actions.ts— 内联动作src/auto-reply/reply/get-reply.ts— 引擎核心(8 阶段流水线)src/auto-reply/reply/get-reply-run.ts— 准备好的回复执行src/auto-reply/reply/agent-runner.ts— Agent Runnersrc/auto-reply/reply/agent-runner-execution.ts— 带 Fallback 的执行src/auto-reply/reply/block-streaming.ts— 分块流式回复src/auto-reply/reply/queue.ts— 消息队列
二十二、思考题
Steer 机制(运行中注入消息)和队列机制(排队等待)各有什么优缺点? 在什么情况下 Steer 可能导致 Agent 混乱?
ReplyDispatcher 的"预留计数器"设计解决了什么竞态条件? 如果没有这个预留,会发生什么?
信封格式中包含星期前缀是因为"小模型推导星期不可靠"——这个问题在大模型时代还存在吗? 这个设计决策是否值得保留?
Human Delay(仿人类延迟)功能的存在暗示了什么使用场景? 这在什么情况下是必要的,什么情况下反而有害?
getReplyFromConfig()接收 40+ 个参数——这是否违反了"函数参数不宜过多"的原则? 如果重构,你会怎么设计参数传递?