症状
前天给机器人加了锁屏、关机、重启、休眠这几个电源指令,用起来挺顺的——直到我发现一个奇怪的 bug:发"锁屏"之后,如果我在机器人回复"已锁"之前解锁屏幕,屏幕又会再次锁上。也就是说,每发一次锁屏,实际被锁了两次。
第一反应:又是 watcher 重放?
因为之前关机循环的阴影(详见上篇博客),我第一反应是 watcher 又在重放旧消息。但去看 session 文件里的消息记录,每条锁屏指令的 timestamp 都是不同的——说明不存在重放,每条都是独立的用户消息。
真正的问题:双通道并行
翻了一遍 watcher 的代码,根因才浮出水面。我的微信机器人消息处理有两条通道:
- watcher(快通道):每 0.5 秒轮询 session 文件,发现关键词直接执行,不经过 AI。用来处理截图、音量这类需要秒回的指令。
- Claude Code(慢通道):cc-connect 把消息传给 Claude,Claude 推理后通过 Bash/PowerShell 工具执行。从发消息到执行完大概要 10-15 秒。
而"锁屏"恰好同时被两条通道匹配到了。时间线是这样的:
t=0 用户发"锁屏"
t=0.5 watcher 轮询到 → 锁屏
你解锁
t=10 Claude 也处理完 → 又锁屏
(你再次解锁)
t=12 Claude 回复"已锁。"
这就是为什么会"锁两次"。不是重放,是两个消费者抢同一条消息。
第一次尝试:打标记
我的第一个想法是,让 watcher 在处理消息之前,先把 session 文件里这条消息的内容前面加上一个"✅"。这样当 Claude 读到消息时,看到的是"✅锁屏",就不会再执行了。
代码逻辑大概是:
- watcher 扫描到"锁屏"
- 立刻在 session JSON 里把这条消息改成"✅锁屏"
- 执行锁屏
- Claude 读到"✅锁屏",不匹配任何关键词,跳过
听上去挺合理的。但实际测试发现——还是锁两次。
原因是:cc-connect 把消息传给 Claude 和我们改 JSON 文件这两件事是并发的。等 watcher 轮询到、加完 ✅、写回文件时,cc-connect 早就已经把原始消息(不带 ✅)塞给 Claude 了。标记法失败。
最终方案:砍掉就完了
既然拦不住,那就别让两条通道抢同一个东西。我把锁屏、关机、重启、休眠这四个指令从 watcher 的关键词列表里删掉了。
理由很简单:这几个指令不需要秒回。发个锁屏等 10 秒完全能接受,没必要让 watcher 0.5 秒抢跑。watcher 只保留真正需要快的——截图、音量、亮度。
改完重启,再试,正常了。一条锁屏只锁一次。
顺便踩的坑:一次截图发三张
在排查过程中反复改了 watcher 的代码,每次改完要重启才生效。但我每次只启新的,不杀旧的,导致同时跑了三个 watcher 实例。发一条"截图",三个 watcher 各截一张,用户一下收到三张截图。
教训:改后台服务,必须先杀干净旧进程,再启新的,最后验证只有一条在跑。
总结
- 双通道并发的竞态条件,不是重放,是双重消费
- 用标记拦截在异步系统里不一定管用——消息已经被传走了
- 最简单的方案往往是最好的:不需要快的指令就别走快通道
- 改后台进程:先杀旧、再启新、验证数量
关机循环和锁屏两次,这俩问题根因其实差不多——都是消息处理没做好隔离。修完这两个,算是把这个消息系统的并发模型彻底搞清楚了。
← 回到首页