“这是保姆时代。”他给自己的开发工具打了个信任分,分数决定他能不能把AI智能体单独放着跑,还是得盯着。盯着的时候太无聊,于是他写了个扫雷。

这个扫雷不是独立游戏,是跑在Claude Code里的一个mod。从2.1.287版本起,mod默认加载。所谓mod,是一种插件,它的钩子是运行在Claude Code内部的JavaScript或TypeScript函数,而不是shell命令,并且它能自己开面板、自己画界面。

打开网易新闻 查看精彩图片

他做的这个叫Minefield。输入/mines,转录区旁边就开出一个面板,Claude干活的时候,你清你的雷区。

一块9×9的板子,10颗雷

规则很简单:一块9×9的板子,10颗雷,刚好够填满Claude工作的那几分钟。第一次翻开永远安全,并且会打开一片区域;点一个周围旗子插满的数字,会翻开它周围的格子;最好成绩跨会话保留。

细节上花了心思。橙色的脸会跟着光标走。停手六秒,它就假装在读旁边的转录内容,眼睛半闭,一行一行地看——它只是看,这个mod看不到转录的任何内容。插旗时它眨眼,赢了就戴上墨镜。他说这是实现起来最有趣的部分。

这个mod自带31个测试,通过claude plugin test运行,CI在两个固定版本的Claude Code构建上跑它们。但即便如此,有四个bug还是通过了测试套件,只在真实会话里才暴露出来:面板拿不到键盘、快捷键没反应、脸换行了、鼠标不见了。

测试里的“宿主”是你自己写的替身

在claude plugin test里,mod发出的几乎每一个引擎调用(ui.open、ui.panes、store.get)都由你在测试文件里写的on(...)桩来应答;测试套件自己只应答少数几个,比如$.ui.invalidate。所以它跑得快,也不需要凭证。

代价是:测试里的宿主表现如何,完全取决于你写的替身有多好。

第一个真实会话里的坑就出在这。/mines打开了面板,可按键还是往提示符里跑。在2.1.285的实机会话里能看到:面板只有在提示符为空时才能拿到键盘。Anthropic的文档把命令里的focus: true列为面板获取键盘的一种方式,但command.run执行期间,命令自己的文本还在提示符里,在2.1.285上这个请求被拒绝了。面板开了,键盘还在原地。

测试里,ui.open由他自己的替身应答,替身说“可以”。

修法是:先开面板,等/mines返回后再要一次键盘。register.ts依次等待250毫秒、500毫秒、1000毫秒重试,两秒内试三次,每次重试前先问$.ui.panes()面板是否还开着、是否仍然没有键盘。如果面板没了,说明是人关掉的,重试绝不能把它再拉回来。

这段重试逻辑是可测的,用mock.clock顶替$.clock。但测试查不了“被拒绝”这件事本身——那个测试里的isFocused是什么,全凭替身说了算。

快捷键为什么一个都没响

面板有两个地方可以画东西。钩子模块(register.ts)应答ui.render钩子并返回元素:盒子、文本、按钮。而Client是一个表面模块(board.ts),有自己的状态、帧计时器、指针和按键处理器——游戏想住的地方就是这里。所以控制按钮被放进了board里,紧挨着它们作用的格子,每个带一个单字母快捷键。

结果一个快捷键都没触发。一个Button的快捷键只为钩子模块画的按钮触发,从不为Client画的按钮触发。

测试也没法发现这一点。ui.press({ key: 'flag' })按的是按钮的元素键('flag',而快捷键是f),所以测试里的按键根本不走快捷键。

修法是把按钮挪进register.ts。这又带出下一个问题:按钮现在住在钩子模块里,游戏住在board里,board只能通过它的props知道按键。两次board重绘之间可能到达好几个按键,只交最新的一个会丢掉其他的。于是每个按键带一个编号,钩子模块保留最近32个。

board按顺序、只播一次每个比上次播过的更新的按键。编号比上次播过的小,说明钩子模块重新开始计数了——插件重载时就会这样——那么它列出的每个按键都是新的。没有新按键时,就是同一局游戏。