OpenClaw WhatsApp 插件:从原理到实践
OpenClaw 的 WhatsApp 插件架构解析:Baileys 协议、Plugin SDK、信道抽象、多账号管理、安全策略,以及系统通知混入和 428 重试两个踩坑记录
一、概述
OpenClaw 的 WhatsApp 插件是整个平台的信道接入层,负责把 AI Agent 的能力通过 WhatsApp 对外提供服务。用户发一条消息,Agent 回复一条消息——这背后是一套完整的消息路由、安全策略、多账号管理机制。
核心技术栈:
- Baileys(
@whiskeysockets/baileys):WhatsApp Web 协议的 Node.js 实现,负责与 WhatsApp 服务器的底层通信 - Plugin SDK:OpenClaw 的插件抽象层,把信道能力标准化为可配置、可组合的组件
- Channel Plugin:具体的信道插件实现
插件路径:源码位于 OpenClaw Git 仓库的 extensions/whatsapp/ 目录
二、架构总览
三条主线贯穿整个插件:
- 入站 — Baileys 收消息 → 规范化 → 安全检查 → 路由到 Agent
- 出站 — Agent 回复 → 文本分片 → Baileys 发送 → WhatsApp 送达
- 安全策略 — dmPolicy / allowFrom / groupPolicy / groupAllowFrom
三、最小可用配置
channels:
whatsapp:
allowFrom:
- "+8615840042341"
actions:
reactions: true
polls: true
# 1. 扫码登录
openclaw gateway login whatsapp
# 2. 重启网关
openclaw gateway restart
# 3. 测试
openclaw message send --channel whatsapp --target +8615840042341 --message "连接成功"
四、channel.ts 核心模块拆解
4.1 配置解析
每个账号对应一个 authDir(Baileys 认证状态存储路径)。webAuthExists() 检查该目录下是否存在有效会话文件。
4.2 安全模型
DM 政策(dmPolicy):
| 策略 | 行为 |
|---|---|
pairing | 默认值,需要配对(用户先发一条消息,agent 确认后才放行) |
allowlist | 只接受 allowFrom 名单里的人 |
open | 任何人可以对话(谨慎使用) |
disabled | 完全忽略 WhatsApp 私信,所有消息不处理 |
群组政策(groupPolicy):
| 策略 | 行为 |
|---|---|
open | 群成员都能发消息,mention-gating 生效 |
allowlist | 只有 groupAllowFrom / allowFrom 名单里的人能发 |
disabled | 完全屏蔽群消息 |
4.3 出站(消息发送)
超过 4000 字符自动分片发送。支持:图片、视频、音频、文档、GIF。
4.4 心跳检测
三个检查项:插件启用 → 已登录 → Web Socket 活跃。
五、消息前缀机制与系统通知混入问题
这是本次排查的核心问题。
5.1 根因:系统通知和真实消息共用出站路径
WhatsApp 插件的出站消息统一走 sendMessageWhatsApp(),无论是 Agent 回复还是系统通知,最终都被 WhatsApp 服务器标记为"这个账号发的"——服务器不区分来源。
系统通知 ──┐
Agent回复 ─┴─→ sendMessageWhatsApp() ──→ WhatsApp 服务器
都被打上相同 sender_id
5.2 流程:系统通知如何混入用户消息
- OpenClaw 内部产生系统通知
- 走
sendMessageWhatsApp()发出 - WhatsApp 服务器标记 sender = 账号所有者(不区分来源)
- 消息回落(inbound),被当作"用户发来的消息",senderLabel = "+8615840042341"
- Session 匹配找到用户
- Agent 看到消息,自动回复
5.3 关于消息前缀字段
配置中存在 messagePrefix / responsePrefix 字段(定义在 WhatsAppConfigCore 中),用于自定义消息前缀,但目前不确定是否实际用于系统通知标记——源码中未明确追踪到此字段与系统通知路由的关联。如有读者在代码中确认,欢迎指正。
5.4 解决方向
- 方案 A(推荐):在 dispatch 层识别系统通知前缀(如
[openclaw]、[Queued messages]),跳过不进入 Agent 处理流程 - 方案 B:系统通知走内部事件总线,不经过 WhatsApp 出站通道
六、428 Gateway Timeout 与重复消息
6.1 什么是 428
428 是 WhatsApp Web 协议在 Baileys 中的状态码,表示"请求超时 / 设备端未确认"。WhatsApp 服务器在一定时间内没收到设备确认时返回此状态。
6.2 Baileys 的重试机制
发送消息 → 失败(428) → Baileys 查找幂等 key(key.id) → 重发 → 用户看到两条
关键调试字段(很多人不知道):
| 字段 | 含义 | 重试时表现 |
|---|---|---|
key.id | 消息的业务 ID(幂等 key) | 重试时保持不变 |
key.stanzaId | WhatsApp 服务器分配的流水号 | 每次发送/重试都不同 |
日志中相同 key.id + 不同 stanzaId = 重试发送。
6.3 解决思路
debounceMs 解决的是入站防抖(减少对同一发送者的重复 LLM 调用),对出站重试无效。真正的解法:
- 出站消息幂等去重:用
correlationId记录本次发送请求,相同 ID 不重复发送 - 调整 Baileys 重试阈值:配置重试次数上限或超时时间
6.4 本次根因
用户发消息 → Agent 回复 → 发送成功但 WhatsApp 返回 428 → Baileys 自动重试 → 用户收到两条相同回复。
七、多账号与多 Agent 场景
场景:个人号 TangTou + 工作号神秘人
个人号 (+8615811111111) 工作号 (+8615822222222)
│ dmPolicy: pairing │ dmPolicy: allowlist
│ → TangTou Agent │ → 神秘人 Agent
│ groupPolicy: disabled
配置示例:
channels:
whatsapp:
defaultAccount: "personal"
accounts:
personal:
name: "个人号"
authDir: "~/.openclaw/whatsapp-personal"
enabled: true
dmPolicy: "pairing"
actions:
reactions: true
work:
name: "工作号"
authDir: "~/.openclaw/whatsapp-work"
enabled: true
dmPolicy: "allowlist"
allowFrom:
- "+8615822222222"
- "+8615833333333"
actions:
reactions: true
polls: true
groupPolicy: "disabled"
每个账号独立:authDir(认证状态)、安全策略、Web Socket 监听器、绑定的 Agent。
八、踩坑记录
坑 1:系统通知混入用户消息
| 项目 | 内容 |
|---|---|
| 现象 | 带有 [openclaw] / [Queued messages] 前缀的消息出现在用户消息列表,Agent 误接话 |
| 根因 | 系统通知走 sendMessageWhatsApp() 出站,WhatsApp 服务器标记 sender 为账号所有者,消息回落时被归到用户名下 |
| 影响 | Agent 无意义回复,消耗 token,误触发 cron/心跳 |
| 调试方法 | 查看消息内容是否有 [openclaw] 前缀,对比 senderLabel |
| 解决方向 | dispatch 层识别前缀跳过;或系统通知走事件总线 |
坑 2:428 重试导致重复回复
| 项目 | 内容 |
|---|---|
| 现象 | 同一 Agent 回复出现了两次 |
| 根因 | WhatsApp 428 超时 → Baileys 自动重试 → 同一条消息发送两次 |
| 影响 | 用户收到两条相同回复,体验差 |
| 调试方法 | 日志中对比 key.id(相同=同一消息)和 stanzaId(不同=重试发送) |
| 解决方向 | 出站幂等去重(correlationId);调高 Baileys 重试阈值 |
九、总结
理解 OpenClaw 的 WhatsApp 插件,关键在于抓住三条线:消息入站(Baileys → Session Router → Agent)、消息出站(Agent → Channel Plugin → Baileys)、安全策略(dmPolicy / groupPolicy / allowFrom)。掌握这三条线,就能从容应对各种配置和调试场景。
本文基于 OpenClaw WhatsApp 插件源码整理