本文记录一个 AI Agent(ZCode + computer-use 插件)在真实 Windows 11 机器上完成的三个任务: 用记事本写一段话、用微信给联系人发消息、在网易云音乐里搜歌并播放。 三个任务难度递增:第一个五分钟内优雅完成,第三个折腾了近一个小时、搞响了作者电脑的风扇。 本文不止讲"做了什么",更讲清楚每一步用了什么手段、为什么可行、失败时背后到底发生了什么。
一、总纲:Agent 的两条"手"和一双"眼" 链接到标题
在展开案例之前,先建立整体框架。Agent 操控电脑不靠魔法,它只有两条操作通道和一种观察方式:
操作通道一:无障碍接口(语义层,最高优先级)
Windows 有一套系统级 API 叫 UI Automation(UIA),macOS 上对应 Accessibility API。它最初是给读屏软件(帮视障用户操作电脑)设计的,会把界面暴露成一棵"语义树":
- 这个元素是按钮,名字叫"确定",支持"按压"动作;
- 那个元素是文本编辑器,可编辑、当前持有焦点,可以直接读写它的值。
Agent 读这棵树,就能像查字典一样精确定位控件,然后下达语义指令(AXPress / SetValue)。这条路的好处:精确(不存在点偏)、后台可操作(不抢用户焦点)、不依赖像素和坐标。
操作通道二:视觉 + 注入输入(像素层,兜底)
当界面是"自绘"的(应用自己画像素,不向系统暴露控件),语义树就是空的。这时 Agent 只能截图,用多模态视觉能力"看"图,然后以像素坐标为目标发起点击。点击的注入方式又分两级(后面案例三会详细展开)。
一双"眼":截图观察
两条通道都依赖同一个循环:
观察(截图 / 读树)→ 行动(一次)→ 验证(新截图 / 新树)→ 下一步
三条铁律贯穿全程:
- 行动回执 ≠ 事情办成。工具返回"指令已送达"只表示发出去了,必须用新的观察确认效果。
- 绝不盲目重放。指令可能已生效,重放等于双击、双发。
- 失败信号是信息,不是噪音。拒绝、超时、不匹配,每一类错误都指向一个具体的机制问题。
二、案例一:记事本写一段话 —— 无障碍通道的完美示范 链接到标题
2.1 任务与流程 链接到标题
任务:打开记事本,写入一段话。全程约五分钟,一次成功。
实际执行序列:
- 权限检查:
request_access确认辅助功能与截屏权限就绪(只读探测,不弹窗)。 - 启动应用:
list_apps确认记事本未运行 → 调用启动器。 - 第一次命名失败:用中文名
"记事本"启动失败;改用 Windows 规范名"Notepad"成功。 - 读语义树:
get_app_state拿到完整的控件树,文本编辑器就在里面(textarea 文本编辑器 (editable))。 - 处理意外弹窗:记事本启动时尝试恢复上次会话文件
session.jsonl失败,弹出"系统找不到指定的路径。"——从树里定位"确定"按钮,AXPress 关闭。 - 语义写入:对编辑器元素执行
set_value,整段文字一次性写入,全程没有模拟击键,没有移动鼠标。 - 验证:再次读树——窗口标题变为文字开头并带
*(未保存标记),状态栏显示"143 个字符",内容确认无误。
2.2 背后的原理 链接到标题
为什么记事本这么配合? 它是传统的 Win32 应用,几十年来一直在为读屏软件做适配,UIA 树完整且规范:每个按钮有名字、编辑框声明自己是 editable。语义层的一切能力它都支持。
set_value 和"打字"的区别:打字是模拟逐个按键,受输入法、焦点、快捷键干扰;set_value 是直接设置元素的值属性(UIA Value Pattern),相当于绕过键盘直接改了数据模型,天然对中文友好、对长文本友好、不可被打断。
为什么启动器不认"记事本":应用启动器按注册名匹配,Windows 上记事本的注册名是 Notepad。教训:启动应用时用系统规范名,别用本地化显示名(macOS 上则要求逐字符复制用户提供的名字,两个平台规则相反)。
2.3 本案例的元教训 链接到标题
即使一切顺利,弹窗这种"计划外状态"也几乎必然出现(更新提示、恢复失败、首启引导)。健壮的 Agent 流程必须把"处理弹窗"当作常态步骤:从树里识别弹窗元素 → 判断是否与任务相关 → 相关则读清楚内容,无关则关闭。
三、案例二:微信发消息 —— 当语义树消失,键盘就是语义 链接到标题
3.1 任务与障碍 链接到标题
任务:打开微信,给备注为"宝"的联系人发一条消息。
第一步就遇到障碍:启动器找不到微信。排查发现这台机器装的是新版微信(Weixin 4.x,目录 C:\Program Files\Tencent\Weixin),注册名不叫 WeChat。解法:绕过启动器,用文件系统定位可执行文件直接启动(这是允许的——先确认了软件确实存在,而不是瞎猜)。
启动后停在登录确认页(当前账号"夕夕不惨")。点击"登录"按钮(这一步是 AXPress,成功),但新版微信需要手机端扫码/确认——这是只有用户能完成的物理世界动作,Agent 正确地停下来等待。
3.2 主界面的"失明":树不见了 链接到标题
用户登录后,主窗口的语义树只剩 3 个无名元素。新版微信是自绘界面,几乎不暴露控件——无障碍通道直接失效,只能走视觉通道。
视觉通道立刻撞墙。三个事实摆在一起:
- 应用级截图能拿到,画面清晰,聊天窗口和输入框都看得见;
- 但每次以截图上的坐标发起点击,都被拒绝:
click: dispatch app/window identity does not match frame …,连续三次; - 窗口每次报告的位置都不一样(漂移),而实际画面没动。
当时并不知道根因(后来在网易云案例里才彻底查明:应用上报的窗口坐标因 DPI 处理 bug 与物理位置不符,导致"截图帧 ↔ 窗口身份"的绑定校验永远失败)。但当时的应对策略是对的:这条路被安全机制封死,换路,不硬闯。
3.3 键盘救场:把微信自己的快捷键当语义接口 链接到标题
鼠标点击走不通,但键盘通道是活的(它的验证只关心"前台窗口是谁",不涉及坐标)。于是设计了一条纯键盘流水线:
Ctrl+F 唤出搜索框
→ 剪贴板写入"宝" → Ctrl+V 粘贴
→ 截图验证:搜索框出现绿色边框、内容为"宝"
→ Enter 选中第一匹配 → 截图验证:"宝"会话打开
→ 剪贴板写入消息 → Ctrl+V 粘贴 → 截图验证:文字入框、发送按钮变绿
→ Enter 发送 → 截图验证:绿色气泡出现、带时间戳
每一步都有截图证据,一步一验证。消息成功送达,对方回复"收到",第二轮对话也顺利完成。
3.4 两个关键细节 链接到标题
中文永远不打字,只粘贴。 模拟英文击键是逐字符映射,而中文输入依赖输入法状态(拼音候选、中英切换),逐键合成中文极不可靠。剪贴板 + Ctrl+V 是原子操作:要么整段进去,要么整段不进去,不受输入法影响。
前台保护机制的正确打开方式。 第二条消息粘贴时被拒绝:frontmost_pid_mismatch(前台进程是 4436,不是微信)——因为用户回到 ZCode 终端打字,焦点被终端抢走了。这个拒绝恰恰是保护:** injected 键盘输入只会送达前台窗口,如果目标不在前台还硬发,字就可能打进别的程序里**。正确应对:重新激活微信(焦点回到原窗口,窗口内的焦点状态保留),再粘贴,成功。
3.5 原理小结 链接到标题
- 自绘应用 ≠ 无障碍死刑,但语义能力归零,只能靠"应用自身的快捷键体系"充当另一种语义接口;
- 键盘通道的安全模型:验证前台身份,而不是验证坐标——这让它天然免疫坐标谎报问题(为案例三埋下伏笔);
- Agent 必须诚实标注"需要人类介入"的时刻(扫码登录),并明确告诉用户等待什么。
四、案例三:网易云音乐 —— 一场完整的调试战役 链接到标题
前两个案例的所有招数在这里全部失效,最终逼出了一套底层组合拳。这一章按时间线复盘,每个弯路都保留。
4.1 安装与首启:顺利的部分 链接到标题
安装用 winget(Windows 官方包管理器)完成:winget install -e --id NetEase.CloudMusic,自动下载官方安装包并校验哈希。比起搜索下载器 + 手点安装向导,包管理器对 Agent 是降维打击。
首启弹窗三个连发:联名活动推广、“设为默认播放器"询问、音效推广+功能提示。有趣的是网易云部分暴露了语义树(400 个元素),弹窗按钮都能 AXPress 关闭。注意一个边界:默认播放器询问点的是"关闭"而不是"确认”——Agent 不应擅自更改用户的系统设置,哪怕按钮就在手边。
放第一首歌也没费劲:首页歌单卡片的播放按钮在树里暴露良好(image play (pressable)),AXPress 一下,窗口标题立刻变成"8090后回忆中的经典老歌 - NJ彼岸",播放条出现暂停键(= 正在播放)。
4.2 困境:搜索框在语义树里"查无此人" 链接到标题
想放指定的歌《哀人》,需要搜索。然后发现:
- 搜索框不在树里。翻遍语义树的可编辑元素,只有两个"冒名顶替者":整个网页容器本身(
textarea = orpheus://...app.html)和播客页的"全部分类"下拉框。真正的搜索输入框是前端用自定义div画的,没有声明 textbox 角色,占位文字只是装饰文本——树里查无此人。(深层原因见第六章。) - 标准 UIA 只能看到 4 个元素。用 PowerShell 直接调 Windows UIA API 枚举,整个窗口只有 4 个元素——Chromium 只在检测到特定无障碍客户端时才展开完整树,CUA 的桥接能拿到 400 个,通用客户端拿不到。
- 打开系统读屏标志没用,MSAA 唤醒也没用——树只对那个专属桥接开放。
- 键盘路线失效:微信的 Ctrl+F 在这里没绑定任何功能;Tab 焦点探测落不进搜索框。
- URL 协议是玄学:
orpheus://深链确实触发了行为(窗口标题变了),但路由到完全无关的页面——协议格式无文档,猜测等于赌博,放弃。
4.3 弯路:浏览器网页版与狂转的风扇 链接到标题
退而求其次用浏览器打开网易云网页版。结果:页面极重,操作连续超时(快照 32 秒超时、截图 30 秒超时、连读取标签页状态都超时),页面陷入加载死循环吃满 CPU,用户的风扇开始狂转。
这是整场战役最该检讨的决策:第一次超时后就该止损,实际又多试了两次,每次白等 30 秒,页面还在后台烧 CPU。发现后立刻关闭标签页、恢复之前实验性打开的系统读屏标志,风扇问题随之消失。教训:当环境出现"行为无定义"的信号(连最轻量的状态查询都超时),立刻放弃该路径,不要追加投入。
4.4 终局解法:原生输入三件套 链接到标题
回到客户端。所有"体面"的路都断了,剩下的路是:绕过自动化框架的坐标通道,直接调用 Windows 原生输入 API(PowerShell + mouse_event/SendKeys)。这条路能通,靠三个缺一不可的修复:
修复一:DPI 感知声明。 第一次点击落点完全不对——PowerShell 进程默认不是 DPI-aware,SetCursorPos(1806, 257) 的坐标被系统按虚拟化规则缩放,光标去了别的地方。加一行 SetProcessDPIAware(),坐标回到物理像素语义。
修复二:前台锁定与 Alt 技巧。 点击落在搜索框上了,粘贴却没进去。逐层排查发现:合成点击不会激活后台窗口——每次跑终端命令,ZCode 自己的窗口就抢走前台,网易云沦为后台,Chromium 直接忽略后台窗口收到的点击。解法是 Windows 的经典技巧:先按一下 Alt 键再调用 SetForegroundWindow(绕过系统前台锁定),并且把"切前台 → 点击 → 粘贴 → 回车"全部放进同一个脚本原子执行,中间不给任何窗口抢前台的时间窗。
修复三:坐标从全屏基准换算。 应用自己报告的窗口位置是错的(微信同款 DPI 谎报),所以坐标换算不基于应用窗口,而基于全屏截图(显示器级别的映射,不经过坏掉的窗口坐标)。搜索框在全屏图中清晰可见,乘以固定缩放比得到物理坐标。
最终脚本骨架(一气呵成,验证输出 foreground ok: True):
| |
搜索结果页应声而出:《哀人》- 门尼(超清母带·原唱)就列在结果里。
4.5 最后一步:把失败当测量仪器 链接到标题
双击歌曲行时又点偏了一行——放出来的是《哀人i (Live)》(现场版),不是门尼原版。但这次失败送来了最宝贵的东西:一个已知的"物理坐标 → 实际落点"对应样本。结合此前搜索框的对应样本,解出真实的仿射映射(缩放 + 偏移),反推出目标行的正确物理坐标,第二发双击精准命中。
验证铁证:窗口标题变为"哀人 - 门尼",播放条显示"哀人 / 门尼"且出现暂停键(= 正在播放),结果行上叠加了"正在播放"图标。这首歌 100w+ 点赞,是抖音上爆火的原版。
4.6 为什么播放按钮能语义点击,搜索框不能 链接到标题
同一个应用、同一棵树,待遇天差地别——因为无障碍树的质量取决于开发者为每个控件声明的角色:
- 播放/暂停/上一首/下一首是读屏用户和系统媒体控制最依赖的控件,声明了名字和可按压动作,语义通道畅通;
- 搜索框是自定义绘制的头部组件,没声明 textbox 角色,占位文字只是装饰文本——房间存在,平面图上没画。图上没画的房间,语义的手就伸不进去,只能用眼睛和真实的手。
这也是无障碍工程的讽刺之处:做得好,机器和视障用户都受益;做得半吊子,最先卡住的是自动化,以及真正依赖读屏软件的人。
五、原理深潜:三个被误解的概念 链接到标题
5.1 坐标谎报是谁的锅? 链接到标题
不是 Windows 坏了,是 Windows 的 DPI 模型 + 应用声明错误的合谋。
Windows 高分屏时代引入了"每进程 DPI 感知级别":进程可以声明自己不感知(系统帮它虚拟化坐标)、系统感知、或完全感知(拿物理坐标)。这套设计为了兼容老程序而极度灵活,代价是同一系统里并存多种坐标空间,全靠每个进程如实声明来对齐。网易云是 CEF 多进程结构,不同窗口声明了不一致的级别,于是"问它窗口在哪"得到的答案与屏幕真相不符。微信同款问题。
横向对比:
- macOS 基本免疫此类问题:全局统一的"点"坐标系,Retina 渲染由窗口服务器统一处理,应用没有自行声明缩放的空间;
- Linux 两极分化:X11 下混用缩放会有类似混乱;Wayland 下出于安全隔离,应用根本不允许知道其他窗口的位置——不是坐标错,是坐标不存在,另一种难受。
5.2 “合成点击"和"原生 mouse_event"有什么区别? 链接到标题
都是注入的假输入,但出发前的功课完全不同,本质是三个信任层级:
| 层级 | 方式 | 验证 | 失败模式 | 适用 |
|---|---|---|---|---|
| 语义层 | AXPress / SetValue(对树中元素) | 元素身份即目标,无坐标 | 目标不存在则报错 | 树完整的控件(首选) |
| 帧绑定坐标 | 从截图像素取点,派发前重新核实窗口身份 | 派发瞬间重验"这个点还属于刚才那个窗口吗” | 拿不准就拒绝(fail-closed) | 视觉目标(默认视觉路径) |
| 原生注入 | mouse_event / SendInput 直入系统队列 | 无 | 屏幕变了就点错,且无人报警 | 前两者全失效时的兜底 |
关键洞察:帧绑定坐标通道的全部前提是"应用如实报告自己的位置"——网易云的坐标谎报打破了这个前提,于是每次都在出发前安检被拦。原生注入之所以能通,恰恰因为它对应用诚信没有任何要求,只要求调用方自己搞清物理真相。代价是它更危险:截图之后屏幕一变就会点错。所以它只能是兜底,且每次使用前必须有新鲜观察。
5.3 “安全机制拦截"到底拦了什么? 链接到标题
对着实战中出现的三条报错翻译:
dispatch app/window identity does not match frame—— 派发瞬间重新解析目标坐标处的窗口身份,与截图时记录的不一致(应用坐标漂移)。判断"我看到的图和要点的位置可能不是一回事”,拒绝执行,一个字节都不发(action_sent=false)。live pixel owner changed—— 截图到点击之间,目标像素的归属窗口变了(前台被抢)。frontmost_pid_mismatch—— 键盘输入只送达前台窗口,发键前核实目标是否真是前台,防的是把字打进别的程序(实战中多次触发,因为每跑一条终端命令 ZCode 终端就抢回前台)。
设计哲学是 fail-closed(拿不准就不做):注入的输入对应用来说和真人操作无法区分,应用不会报警,所以框架必须自己核实,宁可误拦、不可误点。复盘看它拦下的每一次,坐标真的都是错的——正确的应对从来不是绕过安检,而是把它的前提修好(真实前台、真实坐标)。前提一满足,中层验证自然放行。
六、方法论总结 链接到标题
把三个案例的经验压缩成可复用的清单:
通道选择优先级(从高到低,逐级降级前必须确认上级确实不可用):
- 语义元素操作(AXPress / SetValue)—— 记事本全程、网易云弹窗与播放按钮
- 应用快捷键 + 剪贴板(键盘语义)—— 微信全流程
- 帧绑定坐标点击(视觉)—— 本机被坐标谎报废掉
- 原生注入(DPI 感知 + 前台技巧 + 物理坐标)—— 网易云终局解法
通用技巧:
- 中文输入一律剪贴板粘贴,绝不逐键合成;
- 应用启动用系统规范名;找不到就用文件系统确认存在后直接启动;
- 安装软件优先 winget/包管理器;
- 系统设置类按钮(默认播放器等)默认不碰;
- URL 协议深链无文档时不要迭代猜测——行为无定义;
- 同一目标失败两次,停止升级尝试,换最强证据路径。
错误信号的正确读法:
identity mismatch / owner changed→ 应用坐标不可信,换全屏基准或原生注入;frontmost mismatch→ 有东西抢了前台,重新激活目标再继续;- 轻量操作也超时 → 环境行为异常(如页面死循环),立刻止损并检查 CPU;
- 行动回执只代表"已发出",效果必须重新观察确认。
最高级的一课:失败是测量仪器。 双击点偏一行之后,“预期落点 vs 实际落点"构成一对校准样本;两个样本解出仿射映射,失败本身变成了通往成功的坐标变换。在不可信的环境里,这比任何先验计算都可靠。
七、三案例速查表 链接到标题
| 记事本 | 微信 | 网易云音乐 | |
|---|---|---|---|
| 界面类型 | Win32 传统控件 | 自绘(Qt/自绘混合) | CEF(Chromium 内嵌网页) |
| 语义树 | 完整规范 | 几乎为空 | 400 元素但质量参差 |
| 主要通道 | 语义 SetValue | 键盘 + 剪贴板 | 语义(弹窗/播放)→ 原生注入(搜索) |
| 坐标谎报 | 无 | 有(被迫放弃视觉通道) | 有(最终绕过) |
| 关键障碍 | 首启弹窗 | 无障碍失效 + 前台抢夺 | 搜索框无角色 + 前台 + DPI |
| 打破僵局的一招 | set_value 直写 | Ctrl+F + 粘贴 + 回车 | Alt 技巧 + 原生点击 + 落点校准 |
| 耗时量级 | ~5 分钟 | ~15 分钟(含等登录) | ~1 小时 |
结语 链接到标题
这三个任务串起来,恰好是 Agent 操控 GUI 的完整光谱:从"系统处处配合"的坦途,到"系统半聋半哑"的周旋,再到"应用主动撒谎"的攻坚。技术上的答案最终都指向同一件事——在不可信的环境里建立可信的观察与验证循环:每一步都看、每一步都验、把每次失败变成测量。
而对应用开发者,这篇笔记也是一份请求:请认真写无障碍角色。你多写一个 role="textbox",读屏用户就能多打一个字,自动化 Agent 就少烧一小时 CPU、少转一分钟风扇。
本文由 ZCode Agent 基于真实操作会话整理,所有报错信息、坐标与验证截图描述均来自实际执行记录。