
关键词:消息中枢|去重|防抖|媒体下载|时间窗口|幂等性|ACP 入口
在多渠道(WhatsApp、Web、Telegram 等)接入的 OpenClaw 架构中,原始消息如潮水般涌入:用户可能连发三条“重启服务”,或上传一张截图并附带说明。若直接将这些原始数据喂给 LLM,将导致:
为此,OpenClaw 设计了 src/core/monitor-inbox.ts —— 一个智能消息流入中枢。它位于渠道插件与 AI 核心之间,负责对原始消息进行解析、清洗、聚合与标准化,确保每一条进入 LLM 的请求都是必要、完整且唯一的。
本文将详解其三大核心机制:
monitor-inbox.ts
所有渠道通过 ACP 发送 chat.rawMessage 事件至 monitor-inbox.ts,后者处理后转发 chat.processedMessage。
利用渠道提供的唯一消息 ID(如 WhatsApp 的 messageId):
// monitor-inbox.ts
const seenMessageIds = new Set<string>();
function isDuplicate(message: RawMessage): boolean {
if (message.id && seenMessageIds.has(message.id)) {
return true;
}
if (message.id) {
seenMessageIds.add(message.id);
// 5 分钟后自动清理(防止内存无限增长)
setTimeout(() => seenMessageIds.delete(message.id), 300_000);
}
return false;
}对于无 ID 渠道(如 CLI),使用 内容哈希 + 时间窗口:
const recentHashes = new Map<string, number>(); // hash → timestamp
function isFuzzyDuplicate(content: string, sessionKey: string): boolean {
const hash = md5(`${sessionKey}:${content}`);
const now = Date.now();
// 清理 2 秒前的记录
for (const [h, ts] of recentHashes.entries()) {
if (now - ts > 2000) recentHashes.delete(h);
}
if (recentHashes.has(hash)) {
return true; // 2 秒内相同内容
}
recentHashes.set(hash, now);
return false;
}无论渠道是否提供 ID,都能有效防重。
用户分多条发送:
理想行为:合并为一条“帮我查一下订单 ORD-123 的状态”。
// monitor-inbox.ts
interface DebounceBuffer {
messages: string[];
lastActivity: number;
timeoutId: NodeJS.Timeout | null;
}
const debounceBuffers = new Map<string, DebounceBuffer>(); // sessionKey → buffer
const DEBOUNCE_WINDOW = 1500; // 1.5 秒
function enqueueMessage(sessionKey: string, text: string) {
let buf = debounceBuffers.get(sessionKey);
if (!buf) {
buf = { messages: [], lastActivity: 0, timeoutId: null };
debounceBuffers.set(sessionKey, buf);
}
buf.messages.push(text);
buf.lastActivity = Date.now();
// 清除旧定时器
if (buf.timeoutId) clearTimeout(buf.timeoutId);
// 设置新定时器
buf.timeoutId = setTimeout(() => {
flushBuffer(sessionKey);
debounceBuffers.delete(sessionKey);
}, DEBOUNCE_WINDOW);
}messages.join(' '))让碎片输入,变成完整意图。
当用户发送非文本内容(如截图、语音指令),AI 需要访问这些文件才能理解。
// monitor-inbox.ts
type MediaType = 'image' | 'audio' | 'video' | 'document';
function detectMediaType(mime: string): MediaType | null {
if (mime.startsWith('image/')) return 'image';
if (mime.startsWith('audio/')) return 'audio';
// ...
return null;
}async function handleMedia(message: RawMessage) {
if (!message.mediaUrl) return;
const mediaType = detectMediaType(message.mimeType);
if (!mediaType) return;
// 生成安全路径
const ext = mime.getExtension(message.mimeType) || 'bin';
const filename = `${nanoid()}.${ext}`;
const localPath = path.join(
process.env.MEDIA_DIR || './media',
message.sessionKey,
filename
);
// 下载(带超时与重试)
await downloadWithRetry(message.mediaUrl, localPath, {
timeout: 30_000,
retries: 3
});
// 替换原始消息中的 URL 为本地路径
message.localMediaPath = localPath;
}media/,禁止遍历上级目录外部媒体 → 可控本地资产。
处理完成后,monitor-inbox.ts 发出结构化事件:
emitACPEvent('chat.processedMessage', {
sessionKey: 'wa:+1234567890',
content: '帮我查一下订单 ORD-123 的状态',
media: [
{ type: 'image', path: '/media/wa_+1234567890/abc123.jpg' }
],
sourceChannel: 'whatsapp',
timestamp: 1710234567
});此事件被 agent-core.ts 监听,作为 LLM 的唯一输入源。
seenMessageIds 和 recentHashes 使用 TTL 自动清理openclaw_inbox_messages_total{channel="whatsapp", action="deduped"} 12
openclaw_inbox_messages_total{channel="web", action="debounced"} 8
openclaw_inbox_media_downloaded_bytes_total 4567890[INFO] Merged 3 messages from wa:+1234567890 → "重启数据库并检查日志"
[DEBUG] Downloaded image to /media/wa_+1234567890/img_a1b2c3.jpg
[WARN] Duplicate message id=msg_abc123 ignoredmonitor-inbox.ts 的存在,体现了 OpenClaw 的工程哲学:在 AI 接触世界之前,先为它过滤噪音、整理秩序。去重防止混乱,防抖还原意图,媒体本地化保障安全——这一切,让用户无需“适应 AI”,而是让 AI 更好地理解人。
这不仅是预处理管道,更是人机交互的第一道礼仪。
在下一篇中,我们将探讨 OpenClaw 的部署模型演进:从单机 Docker 到 Kubernetes Operator。
下一篇预告: 第 17 篇:OpenClaw 架构中的聊天 RPC 接口 —— chat.ts 中的历史查询、发送与中止逻辑
您的 AI 助手,从此由您定义。若感兴趣可以浏览本书其他章节内容:
openclaw.mjs、config.yaml 与环境变量管理run.ts 上篇 —— 模型调度、账号轮询与上下文守护机制run.ts 下篇 —— 故障转移、重试策略与结果封装memory-search.ts 中的 RAG 配置解析与合并逻辑exec.ts 上篇 —— 安全执行 Shell 命令的三层隔离模型exec.ts 下篇 —— 用户审批、后台任务与权限提升控制process.ts —— AI 如何像开发者一样管理后台进程server-channels.ts —— 渠道插件生命周期管理器session.ts 与 Baileys 的健壮连接管理monitor-inbox.ts 如何解析、去重与防抖chat.ts 中的历史查询、发送与中止逻辑ws-log.ts 如何让 WebSocket 日志可读可用