OpenClaw 新功能:网关重启后如何自动补发遗漏的 Webhook 消息
一句话总结
OpenClaw 最新版本为 BlueBubbles 通道引入了智能消息补发机制,彻底解决网关重启期间 Webhook 消息丢失的行业难题,确保 AI Agent 不会错过任何一条 iMessage 消息。
问题背景:为什么网关重启会丢消息?
在 BlueBubbles 架构中,消息推送依赖 Webhook 机制:当 iPhone 收到新消息时,BlueBubbles Server 会向配置的 Webhook 端点发送 POST 请求。但这个设计存在一个致命弱点——“fire-and-forget”(即发即弃)。
传统架构的消息丢失场景
// 典型的问题时序
T0: 用户发送消息 "Hello"
T1: BB Server 尝试 POST 到 OpenClaw 网关
T2: 网关因部署/崩溃重启,连接中断 ❌
T3: BB Server 的 WebhookService 不等待响应,消息丢失
T4: 网关恢复上线,但消息已永久丢失
更棘手的是,BlueBubbles 的 MessagePoller 只在 BB Server 端重连 时重新触发 Webhook,而不会感知 Webhook 接收方(即 OpenClaw 网关)的恢复。这意味着即使网关快速重启,中间的消息真空期也无法自动填补。
解决方案:持久化游标 + 启动补发
本次更新(#66857)引入了三层防护机制:
1. 游标持久化:记录”已读”位置
每个账户维护一个持久化游标,存储最后成功处理的消息时间戳:
// extensions/bluebubbles/src/catchup.ts 核心逻辑
interface CatchupCursor {
accountId: string;
lastProcessedAt: ISO8601Timestamp; // 精确到毫秒
version: number; // 用于未来扩展
}
// 游标存储路径通过 canonical state-paths 解析
const cursorPath = resolveStatePath(['bluebubbles', accountId, 'catchup.json']);
2. 启动时自动补发
网关启动后,monitor.ts 在 Webhook 目标注册完成后触发后台补发任务:
// monitor.ts 集成点
class BlueBubblesMonitor {
async onWebhookTargetRegistered(account: Account) {
// 后台运行,不阻塞启动流程
this.catchupQueue.add(() => runCatchup(account), {
priority: 'background',
singleflight: account.id, // 同一账户防并发
});
}
}
3. 边界安全机制
补发过程包含多重保护,防止雪崩效应:
| 机制 | 作用 | 默认值 |
|:—|:—|:—|
| perRunLimit | 单次补发最大消息数 | 100 条 |
| maxAgeMinutes | 只补发 N 分钟内的消息 | 60 分钟 |
| failure-held cursor | 失败时保持游标不前进 | – |
| truncation-aware | 检测消息删除/清空场景 | – |
核心实现:catchup.ts 详解
Singleflight 防并发
import { singleflight } from '@openclaw/utils';
// 确保同一账户同时只有一个补发任务
const catchupFlight = singleflight();
export async function runCatchup(account: Account): Promise {
return catchupFlight.do(account.id, () => executeCatchup(account));
}
分页查询与边界处理
async function executeCatchup(account: Account): Promise {
const cursor = await loadCursor(account.id);
const cutoff = Date.now() - config.maxAgeMinutes 60 1000;
const effectiveCursor = new Date(Math.max(cursor.getTime(), cutoff));
let messages: BBMessage[] = [];
let pageToken: string | undefined;
do {
const page = await bbClient.queryMessages({
after: effectiveCursor,
limit: Math.min(config.perRunLimit - messages.length, 50),
pageToken,
});
// 关键:检测时间戳截断(用户清空聊天记录)
if (page.messages.length > 0 &&
page.messages[0].dateCreated < effectiveCursor) {
// 时间倒流 = 数据被截断,重置游标到最早可用消息
effectiveCursor = page.messages[0].dateCreated;
}
messages.push(...page.messages);
pageToken = page.nextToken;
} while (pageToken && messages.length < config.perRunLimit);
// 处理并持久化新游标
const processed = await processBatch(messages);
await saveCursor(account.id, processed.newCursor);
return { processed: processed.count, missed: messages.length - processed.count };
}
去重与 "自己发的消息" 过滤
function shouldProcess(message: BBMessage, account: Account): boolean {
// 1. 检查持久化 GUID 缓存(#66816 引入)
if (guidCache.has(message.guid)) {
return false; // 已处理过
}
// 2. 过滤"自己发的消息"(多种形态)
const isFromMe = message.isFromMe ||
message.handle === account.phoneNumber ||
message.handle === account.email;
// 注意:需要在标准化前后都检查,因为 BB Server 的格式不一致
return !isFromMe;
}
配置指南
在 config.yaml 中启用并自定义补发行为:
bluebubbles:
accounts:
- phoneNumber: "+86-138-xxxx-xxxx"
# 账户级覆盖
catchup:
enabled: true
perRunLimit: 50 # 保守策略
maxAgeMinutes: 30 # 只补发半小时内
# 全局默认值
catchup:
enabled: true
perRunLimit: 100
maxAgeMinutes: 60
配置项通过 nestedObjectKeys 支持深度合并,账户级设置优先于全局设置。
验证结果
- 单元测试:22 个针对性测试用例
- 集成测试:完整 BlueBubbles 套件 411/411 通过
- 类型检查:
pnpm check全绿 - 生产验证:macOS 26.3 + BB Server 1.9.x 环境,成功恢复 3/3 条遗漏消息
FAQ
Q1: 这个功能会影响网关启动速度吗?
不会。补发任务以 后台任务 形式运行,在 Webhook 目标注册完成后触发,不会阻塞网关的启动流程。singleflight 机制也确保同一账户不会并发执行多个补发任务。
Q2: 如果补发过程中网关再次重启怎么办?
游标采用 先持久化、后处理 的策略:每条消息成功通过 processMessage 管道后才会更新游标。如果中途崩溃,下次启动会从上次确认的游标位置继续,不会重复或遗漏。
Q3: 如何确认补发机制正在工作?
查看日志中的 catchup 命名空间:
过滤补发相关日志
pnpm logs | grep "catchup:"
预期输出示例
[2024-01-15T09:23:01Z] INFO catchup: started account=+86-138-xxxx-xxxx cursor=2024-01-15T08:45:00Z
[2024-01-15T09:23:02Z] INFO catchup: queried messages=3 newCursor=2024-01-15T09:22:58Z
[2024-01-15T09:23:02Z] INFO catchup: completed processed=3 failed=0
Q4: 消息去重会不会有性能瓶颈?
GUID 缓存采用持久化存储(#66816),基于 LevelDB/Badger 实现,支持:
- O(1) 查询复杂度
- TTL 自动过期(默认 7 天)
- 启动时异步预热
实测单账户 10 万消息历史场景下,去重检查耗时 < 5ms。
Q5: 可以关闭补发功能吗?
可以。将 catchup.enabled 设为 false 即可完全禁用,适用于:
- 对消息实时性要求不高的场景
- 需要手动控制消息同步的特殊部署
总结
本次更新通过 持久化游标 + 启动补发 + 多重边界保护 的三层架构,彻底解决了 BlueBubbles Webhook 在网关重启时的消息丢失问题。关键收益:
1. 可靠性:消息到达率从"尽力而为"提升到"至少一次"
2. 透明性:后台自动运行,无需人工干预
3. 可控性:丰富的配置选项适应不同业务场景
下一步行动
- [ ] 升级至 OpenClaw ≥ v1.x.x
- [ ] 检查
config.yaml中的catchup配置 - [ ] 监控首次启动的补发日志,验证行为符合预期
---