锁两次屏?一个竞态条件的排查实录

症状

前天给机器人加了锁屏、关机、重启、休眠这几个电源指令,用起来挺顺的——直到我发现一个奇怪的 bug:发"锁屏"之后,如果我在机器人回复"已锁"之前解锁屏幕,屏幕又会再次锁上。也就是说,每发一次锁屏,实际被锁了两次。

第一反应:又是 watcher 重放?

因为之前关机循环的阴影(详见上篇博客),我第一反应是 watcher 又在重放旧消息。但去看 session 文件里的消息记录,每条锁屏指令的 timestamp 都是不同的——说明不存在重放,每条都是独立的用户消息。

真正的问题:双通道并行

翻了一遍 watcher 的代码,根因才浮出水面。我的微信机器人消息处理有两条通道

  1. watcher(快通道):每 0.5 秒轮询 session 文件,发现关键词直接执行,不经过 AI。用来处理截图、音量这类需要秒回的指令。
  2. 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 读到消息时,看到的是"✅锁屏",就不会再执行了。

代码逻辑大概是:

  1. watcher 扫描到"锁屏"
  2. 立刻在 session JSON 里把这条消息改成"✅锁屏"
  3. 执行锁屏
  4. Claude 读到"✅锁屏",不匹配任何关键词,跳过

听上去挺合理的。但实际测试发现——还是锁两次

原因是:cc-connect 把消息传给 Claude 和我们改 JSON 文件这两件事是并发的。等 watcher 轮询到、加完 ✅、写回文件时,cc-connect 早就已经把原始消息(不带 ✅)塞给 Claude 了。标记法失败。

最终方案:砍掉就完了

既然拦不住,那就别让两条通道抢同一个东西。我把锁屏、关机、重启、休眠这四个指令从 watcher 的关键词列表里删掉了。

理由很简单:这几个指令不需要秒回。发个锁屏等 10 秒完全能接受,没必要让 watcher 0.5 秒抢跑。watcher 只保留真正需要快的——截图、音量、亮度。

改完重启,再试,正常了。一条锁屏只锁一次。

顺便踩的坑:一次截图发三张

在排查过程中反复改了 watcher 的代码,每次改完要重启才生效。但我每次只启新的,不杀旧的,导致同时跑了三个 watcher 实例。发一条"截图",三个 watcher 各截一张,用户一下收到三张截图。

教训:改后台服务,必须先杀干净旧进程,再启新的,最后验证只有一条在跑。

总结

关机循环和锁屏两次,这俩问题根因其实差不多——都是消息处理没做好隔离。修完这两个,算是把这个消息系统的并发模型彻底搞清楚了。

← 回到首页