提交前确认 · Pre-submission checklist
问题类别 · Category
对话 / Agent 交互 · Agent chat
涉及的 Agent 框架 · Agent framework
ZCode Agent(自研)
严重程度 · Severity
影响体验 · Major
该问题可能重复执行带外部副作用的任务,例如写文件、发布内容或调用第三方工具。
复现频率 · Reproducibility
偶现 · Sometimes(取决于飞书是否重放旧事件);但去重失效条件是确定的:进程重启后必定丢失记录,或消息重放间隔超过 2 分钟后记录被清除。
问题描述 · Description
ZCode 通过飞书 WebSocket 接收消息时,已经使用 providerMessageId 做重复消息检测,但当前实现只使用进程内 Map,且 TTL 只有 2 分钟。
因此发生以下任一情况后,飞书重放同一个 message_id 时会被 ZCode 当作新输入再次执行:
- ZCode/host 进程重启,内存 Map 被清空;
- 同一消息在 2 分钟之后被再次投递。
如果用户刚执行过 /新建,旧消息重放还会被当作新任务的第一条 prompt,表现为“ZCode 突然自己重新执行已经完成的任务”。
复现步骤 · Steps to reproduce
- 在 ZCode 中配置飞书机器人(WebSocket 模式)。
- 通过飞书发送一条任务消息并等待任务完成。
- 重启 ZCode,或者等待超过 2 分钟。
- 让飞书重新投递相同的 im.message.receive_v1 事件(相同 message_id,例如 WebSocket 重连后的重放)。
- 观察 ZCode 再次调用 sendPrompt,甚至创建新 session。
期望表现 · Expected behavior
同一个飞书 message_id 在合理的幂等窗口内只能执行一次;去重状态应跨 ZCode/host 重启保留。处理失败时可以释放该 ID 以允许飞书重试。
实际表现 · Actual behavior
同一条旧消息在重启后或超过 2 分钟后再次进入 sendPrompt,导致已完成任务被重复执行。
本机日志中的脱敏时间线:
21:33:41 provider callback ... text=<消息 A>
21:33:41 provider callback duplicated ... messageId=<同一 message_id>
21:33:42 sendPrompt ... taskId=<旧 session>
21:40:48 [重启后] provider callback ... text=<消息 A>
21:40:50 sendPrompt ... taskId=<旧 session>
21:07:37 provider callback ... text=<消息 B>
22:49:39 provider callback ... text=/新建
22:52:10 provider callback ... text=<消息 B>
22:52:12 sendPrompt ... taskId=<新建 session>
本地数据库中旧 session 的输入均为 promoted,没有待处理的旧队列项;22:52 的执行来自一次新的 Feishu provider callback,并创建了新 session,因此不是本地任务队列恢复。
根因定位
ZCode 3.5.3 打包代码 out/host/index.js 中:
- 去重容器:B = new Map(),仅存在于 host 进程内存;
- TTL:hue = 2 * 6e4,即 2 分钟;
- markInboundDelivery() 只检查该 Map;
- releaseInboundDelivery() 在处理失败时删除 Map 项。
现有逻辑能挡住同一进程内的即时重复投递,但无法覆盖重启或延迟重放。
建议修复
- 以 (botId, provider, chatId/providerUserId, providerMessageId) 为 key,持久化已处理消息 ID;
- 使用至少数天的 TTL,并设置最大条数防止无限增长;
- 写入采用原子替换;
- 只有在消息处理/回复失败时才释放该 key,保留当前允许重试的语义;
- 增加“同一 message_id 在 host 重启后仍被拒绝”的回归测试。
我已在本机做了实验性补丁:将去重记录持久化到 ~/.zcode/v2/bot-inbound-dedupe.v1.json,TTL 7 天、上限 10,000 条,并保留处理失败时释放 ID 的逻辑。回归测试覆盖:
- 同一消息 ID 在两个独立 Node 进程之间仍会被拒绝;
- 处理失败释放后允许重试;
- host 已接入持久化去重实现。
测试 3/3 通过,补丁版 ZCode host 启动及重启正常。
ZCode 版本 · ZCode version
3.5.3(Build 3.5.3.3911)
设备 / 系统 / 浏览器 · Device / OS / Browser
Apple Silicon / macOS 26.6 (25G72) / arm64
提交前确认 · Pre-submission checklist
问题类别 · Category
对话 / Agent 交互 · Agent chat
涉及的 Agent 框架 · Agent framework
ZCode Agent(自研)
严重程度 · Severity
影响体验 · Major
该问题可能重复执行带外部副作用的任务,例如写文件、发布内容或调用第三方工具。
复现频率 · Reproducibility
偶现 · Sometimes(取决于飞书是否重放旧事件);但去重失效条件是确定的:进程重启后必定丢失记录,或消息重放间隔超过 2 分钟后记录被清除。
问题描述 · Description
ZCode 通过飞书 WebSocket 接收消息时,已经使用 providerMessageId 做重复消息检测,但当前实现只使用进程内 Map,且 TTL 只有 2 分钟。
因此发生以下任一情况后,飞书重放同一个 message_id 时会被 ZCode 当作新输入再次执行:
如果用户刚执行过 /新建,旧消息重放还会被当作新任务的第一条 prompt,表现为“ZCode 突然自己重新执行已经完成的任务”。
复现步骤 · Steps to reproduce
期望表现 · Expected behavior
同一个飞书 message_id 在合理的幂等窗口内只能执行一次;去重状态应跨 ZCode/host 重启保留。处理失败时可以释放该 ID 以允许飞书重试。
实际表现 · Actual behavior
同一条旧消息在重启后或超过 2 分钟后再次进入 sendPrompt,导致已完成任务被重复执行。
本机日志中的脱敏时间线:
本地数据库中旧 session 的输入均为 promoted,没有待处理的旧队列项;22:52 的执行来自一次新的 Feishu provider callback,并创建了新 session,因此不是本地任务队列恢复。
根因定位
ZCode 3.5.3 打包代码 out/host/index.js 中:
现有逻辑能挡住同一进程内的即时重复投递,但无法覆盖重启或延迟重放。
建议修复
我已在本机做了实验性补丁:将去重记录持久化到 ~/.zcode/v2/bot-inbound-dedupe.v1.json,TTL 7 天、上限 10,000 条,并保留处理失败时释放 ID 的逻辑。回归测试覆盖:
测试 3/3 通过,补丁版 ZCode host 启动及重启正常。
ZCode 版本 · ZCode version
3.5.3(Build 3.5.3.3911)
设备 / 系统 / 浏览器 · Device / OS / Browser
Apple Silicon / macOS 26.6 (25G72) / arm64