20天,0次点击。我的AI监督工具却每天自我检测后汇报:“Self-check OK (8/9)”。

我写了一个叫 HELM 的程序。它的工作很简单:盯着我机器上跑着的 Claude Code 智能体,在弹出权限请求时自动点击“允许”。这样即使我凌晨两点在睡觉,长时间自主运行也不会卡住等人。整款产品就一个承诺——当你的智能体需要人类时,总有一个可靠的替身站在那里。

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

7月23日,我发现它自7月3日以来一次批准都没执行过。整整20天,它一直在运行,日志里填满了看起来成功的审批记录,但它连一个按钮都没点下去。

背后是怎么一层层失效的,值得好好拆解一遍。

Claude Desktop 基于 Electron,也就是 Chromium。Chromium 的可访问性树是惰性的——它只在认为有辅助技术客户端在监听时,才为渲染内容构建这棵树。一旦它认定没人在,它就不建了。

大概在7月3日前后,它开始不再认定有监听者。这对 HELM 意味着:UIA(用户界面自动化)仍然能看到原生壳层——窗口 chrome、最小化按钮,以及展示我会话列表的侧边栏。这些全都还在。所以 HELM 照样列出每一个会话,照样画出自己的面板,看起来一切如常。唯独对话窗格,也就是“Allow”按钮真正所在的那部分窗口,完全不在树里。不是被重命名,不是被移动,而是根本不存在。

我通过导出窗口里的每一个 UIA 控件并计数才找到问题。641个节点,每一个都是 chrome 或侧边栏。内容区的数量是零。HELM 存在的意义——那个需要点击的按钮——它根本看不见。

日志里有一条记录几乎重复了3551次:approve_button=None

修复最终只用了四行 Windows API:向窗口及其渲染控件发送 WM_GETOBJECT 消息,Chromium 便会构建可访问性树。树从641个节点跃升到1191个,内容按钮从0个涨到81个。

有个细节花了我一个小时:SendMessageTimeoutW 根本行不通。我起初用它,保持进程不被挂起,树只从509个节点变成539个,纯属噪声。只有阻塞式的 SendMessageW 才会真正触发可访问性构建。于是我把这个调用扔到了守护线程里,既不阻塞主逻辑,也不对自己撒谎。

其实 HELM 早就有针对这种场景的诊断。它会打印自己能看到的按钮,这样当 Claude 新版本的文案变动时,日志里一眼就能发现。它每次都会打印出类似这样的列表:

buttons_seen=['Minimize','Maximize','Restore','Close','Menu','Collapse sidebar', 'Search','Back','Forward','Home','Code','New','Artifacts','Routines', 'Pinned','Idle Draft HOA conversation talking points', ...]

全是 chrome 和侧边栏,从来没有任何内容按钮,不管屏幕上显示的是什么。原因在于诊断只采样了前面的条目,而内容按钮从未进入过这个采样范围。结果就是 HELM 的健康信号永远指向“功能正常”。

这个故障的根源甚至比惰性树更隐蔽:一个监控工具对自身视野的盲区同样缺乏感知。我在设计 HELM 时,默认了它能看到的按钮集合就是完整的候选字段,却没想到 UIA 提供的那棵“完整”树,本身就可能是残缺的。当 Chromium 因为性能优化悄悄地把内容树剪掉,HELM 的诊断却只能在自己的可见范围内报平安,就像一位失明的哨兵,仍旧每天在日志里写下“一切正常”。

修复后的 HELM 用一条阻塞调用的守护线程主动请求构建树,并且诊断改为导出全部节点、核对内容按钮是否实际存在,而不再信任一个静态的按钮名称列表。这次事故没有造成实际损失,但它提醒我:任何依赖 UI 自动化的监督程序,都必须主动探测底层容器是否真正渲染了自己要操作的那部分界面。Chromium 今天会偷懒,明日 Electron 的某个版本也许又换了一种缩减策略。唯一可靠的防御是,不假设框架对自己诚实。