11 月 3 日的 GitHub Trending 榜首换成了一个 Rust 项目:vercel-labs/agent-desktop。仓库 11 月 3 日中午还在推代码,累计 11874 星,npm 上的 agent-desktop 包上周下载量 20713。它的自我介绍只有一句话:让 AI agent 通过操作系统无障碍树去读写桌面应用。
截图加视觉模型早就能够点按钮,所以这句话本身不算新鲜。真正值得拆开看的是它把「看」和「点」拆成了两个独立且可验证的步骤,并且把每一步失败的原因写进了 JSON 契约。这决定了它能不能被无人值守的流水线直接调用,而不是只能在演示视频里跑一遍就结束。
桌面上没有 DOM,只有一棵无障碍树
网页 agent 的日子好过,因为有 DOM。一个按钮有 id、有 class、有可读文本,选择器写错会立刻报找不到元素,错误信息直接指到出错的那一行。桌面应用没有这套东西,窗口里的控件活在进程内存里,外部进程拿不到句柄之外的东西。
于是只剩两条路。一条是截图交给视觉模型判断坐标,另一条是读操作系统自己暴露的无障碍树。前者通用,代价是脆;后者覆盖面窄,但每个元素自带语义。agent-desktop 走的是后者,分别对接 macOS 的 Accessibility API、Windows 的 UI Automation 和 Linux 的 AT-SPI。
无障碍树的核心好处是定位依据不是像素。窗口换了主题、DPI 从 100% 调到 150%、系统切成深色模式,树的层级和节点名字不会跟着变,一个 ref 仍然指向同一个按钮。截图方案在这些变化下需要重新校准,校准一旦失效,整条流水线就停在等待坐标超时上,而且日志里只剩一句超时,看不出究竟是界面变了还是模型看错了。
这也是为什么它的文档反复强调 ref 属于某一次快照。快照是一次真实观测的结果,不是一份可以长期缓存的元素地图。UI 变了,旧的 ref 立即失效,工具会明确告诉你失效,而不是猜一个位置点下去。这个差别在长时间运行的批处理里非常关键,因为缓存坐标的做法会在界面更新后的某个清晨静默失败,而缓存 ref 的做法会当场报错。
一条 snapshot 命令换回一张 ref 清单
核心命令是 snapshot,最常用的形态是只列可交互元素。加上 -i 参数后,返回的 JSON 里每个可操作节点都带 role、name、ref_id 和 available_actions 四项。ref_id 形如 @s8f3k2p9:e8,前面的短串是这次快照的编号,后面是节点序号,两者绑定在一起,单写后半段会被拒绝。
信封结构固定为 version、ok、command 三个顶层字段,成功时数据落在 data 里,失败时落在 error 里。当前契约版本是 2.6。一次典型的返回里,data 会给出 app 名、窗口 id、窗口标题、snapshot_id、ref_count 和一个 complete 布尔值。
complete 这个字段在 2.2 版本才出现,意义不小。旧版遇到超大窗口会直接抛 TIMEOUT 且不给任何树,调用方只能靠加大预算硬试。新版改成返回 ok 为 true、complete 为 false、truncated 为 true,附上已经观测到的树和 nodes_observed 计数,被截断的父节点各自带 subtree_truncated 标记,读者可以沿着根一路走到每一处边界。调用方只要读 complete 就能判断手里这棵树是不是完整的,不用再从超时错误里反推。
窗口多的场景不会替你选。同一应用开了多个窗口时,快照返回 AMBIGUOUS_TARGET,候选窗口 id 放在 error.details 里,需要显式传 window-id 指定一个。对密集应用,官方建议先用 skeleton 加 compact 拿到一张骨架图,再用 root 参数从一个节点往下钻。root 下钻是替换已有快照里的 ref,不是新建快照,所以观测不完整时它仍然返回 TIMEOUT,不会给你半棵树。这条区别很容易被忽略,写循环的时候要按不同语义处理:第一次快照不完整可以加大预算重来,下钻不完整只能退回上一层重新选节点。另一种常见浪费是把骨架图当成最终结果直接拿去点,骨架里的节点往往没有 available_actions,点了会被判动作不可用,白白多一轮快照。
动作认 ref,不认坐标
拿到 ref 之后,动作命令的写法很直白:click 接一个 ref,type 接文本,set-value 直接改值。ref 必须带快照前缀,单独写 @e8 会被判 INVALID_ARGS 拒绝,这条规则防的是跨快照误用。
真正需要提前想清楚的是三种失败态。STALE_REF 表示这个 ref 在当前的 RefMap 里找不到,说明页面或窗口已经变了,恢复策略是重新快照再拿新 ref。AMBIGUOUS_TARGET 表示目标不唯一,多窗口场景常见。第三个不是错误而是结果状态:动作交付成功但没有验证,返回值里 delivery 为 delivered_unverified,同时 verification_scope 为 unavailable。文档说得很清楚,这种情况只说明动作发出去了,不证明应用层真的落库,所以不能拿它当成功依据往下跑。
默认是 headless 行为。ref 动作全部走无障碍 API,不移动鼠标指针,也不抢窗口焦点。要物理输入必须显式加 --headed。对后台跑批的任务,这一点比速度重要得多,因为抢焦点会打断用户正在做的事,而无人值守的第一原则就是不打扰。
| 维度 | 截图加视觉模型 | 无障碍树 ref |
|---|---|---|
| 定位依据 | 像素坐标与图像特征 | 节点的 role 与 name |
| 分辨率或主题变化 | 需要重新校准 | 结构不变,ref 仍有效 |
| 是否抢焦点 | 常需真实指针移动 | 默认不移动指针不抢焦点 |
| 失败信息 | 常常只给出坐标超时 | 带错误码与重试建议 |
| 覆盖范围 | 任意可见画面 | 只覆盖暴露无障碍语义的控件 |
表格最后一列那句话是这套方案的天花板。自绘画布、游戏画面、远程桌面里的内容,无障碍树里没有对应节点,ref 无从生成,这条路直接走不通。选型时先问一句:要操作的那个应用,它的控件有没有语义。
信封里的 disposition 决定该不该重试
这套工具最省事的设计是把「能不能重试」做成了机器可读字段,而不是留给人去猜。每个错误都带 disposition 对象,里面两个值:delivery 和 retry。delivery 有五种取值,unknown、not_delivered、delivery_uncertain、delivered_unverified、delivered_verified。retry 有三种,unknown、safe、unsafe。
规则是推导出来的,不是拍出来的。delivery 为 not_delivered 时 retry 一定是 safe,其余非 unknown 的取值一律 unsafe。unknown 意味着这次调用什么也没证明出来,比如一个可能已经落地的操作超时了,这种情况按不安全处理,因为重试可能造成重复提交。
所以正确的调用方代码里只该有一行判断:只有 error.disposition.retry 等于 safe 时才自动重试,其余分支读 error.code 和 message 走人工或换策略。recovery 对象只在 retry 为 safe 时出现,里面给出策略名和是否需要新快照,TIMEOUT 与 APP_UNRESPONSIVE 还会带 retry_after_ms,可以直接拿来做退避间隔。
2.4 版本又补了一层。set-value、type、clear、check、uncheck、toggle、expand、collapse 这些有状态的动作用一次交付后就校验后置条件,发现矛盾直接报错,details.kind 标为 post_action_verification。这一条针对的是最阴的一类事故:动作返回成功,界面看着变了,实际状态没改,于是后续每一步都基于错的前提往下跑,最后失败的位置离真正的根因差了十几个步骤。校验失败时工具不会替你重试,它把矛盾摆出来,因为此时再点一次很可能把错误状态放大。读这个错误时先看 details.kind,再决定是重新快照核对界面真实状态,还是回滚到上一步的业务动作。把这套约定接进流水线之后,重试逻辑就从「失败就重试三次」变成了「按错误码决定重试或停机」,后者的好处是可以事后统计哪类错误最多,而不是只留下一个笼统的失败率。
三个平台的权限账不一样
Windows 最省事,不需要任何授权,前提是目标应用和终端的完整性级别一致。管理员权限运行的应用可以被读取,但只有同样提权的 agent 才能往里输入。这个差异在混合权限环境里会表现为「看得到点不动」,不是权限没给对,而是级别不匹配,排查时要先看两个进程的完整性级别。
macOS 的门槛是辅助功能权限,截图另需屏幕录制,操作通知中心还要给 System Events 自动化权限。permissions 子命令可以只读地报告当前状态,加 request 才会发起请求,加 guide 会直接打开系统设置的对应页面。装完第一件事应该是跑一次 permissions,而不是等动作失败再逐个排查。
Linux 的前提是跑在用户图形会话里,并且装了 at-spi2-core。纯 TTY 或 SSH 会话看不到任何应用,因为没有那个会话的总线、显示和运行时环境。官方给的主目标很明确:Ubuntu 24.04 加 GNOME 46,glibc 地板 2.35。Ubuntu 22.04 的 ATK 桥接在 Action.GetActions 上会直接崩,属于已知不兼容,不是配置能修好的问题。
还有一个容易踩的坑是二进制签名。发布包没有签名,从浏览器下载会触发 SmartScreen 提示,杀毒软件可能直接隔离,全新装的 Windows 11 上 Smart App Control 会拦下来。走 npm 安装不触发这一套,所以 Windows 上优先 npm 而不是手动下载压缩包。如果确实要用压缩包,可以用 gh attestation verify 核对来源,因为发布页的校验和只能证明文件没损坏,证明不了它来自这个仓库。
接进自己的循环:从 CLI 到 FFI
安装和第一轮验证只需要几条命令。注意 npm 12 默认屏蔽安装脚本,被屏蔽时包会退回 Node 启动器,每次调用多出约 125 毫秒,用 install-scripts approve 或 --allow-scripts 放行后才是原生二进制的约 21 毫秒。这个差距在批量任务里会累积成分钟级差别。
npm install -g agent-desktop
agent-desktop version
agent-desktop permissions
agent-desktop snapshot --app Finder -i
agent-desktop click @s8f3k2p9:e8
agent-desktop snapshot --app Finder -i
PowerShell 用户要把每个 ref 用引号包住,因为裸写的 @ 开头标记会被当成变量展开运算符,命令会被解析成完全不同的东西。这是官方文档单独拎出来讲的一条,Windows 上第一次跑就撞的概率很高,而且报错信息完全不指向真正的原因。
二进制自带文档是这套工具另一个实用设计:skills get desktop 讲循环、ref、验证和重试规则,skills get platform 讲当前操作系统上的能力差异,skills get ffi 讲语言绑定的入口、内存归属和线程规则。文档跟着二进制版本走,不会出现照着网页上的旧示例写出跑不通的代码,这一点比很多只在 README 里写用法的项目要靠谱。
要在进程内调用而不是每条命令起一次子进程,可以走发布包里带的 libagent_desktop_ffi。以 Python 为例,用 ctypes 加载库,调用 ad_init 并核对返回值,再创建 adapter 句柄:
import ctypes
lib = ctypes.CDLL("./lib/libagent_desktop_ffi.dylib")
lib.ad_adapter_create.restype = ctypes.c_void_p
lib.ad_adapter_destroy.argtypes = [ctypes.c_void_p]
assert lib.ad_init(7) == 0
adapter = lib.ad_adapter_create()
lib.ad_adapter_destroy(adapter)
那段 assert 校验的是 ABI 主版本号,头文件里的 AD_ABI_VERSION_MAJOR 必须和库匹配,否则后面所有调用都是未定义行为,可能不报错但结果全错。命令总数是 60 个,其中 56 个可直接用,剩下四个 key-down、key-up、mouse-down、mouse-up 在守护进程能释放被按住的键或按钮之前拒绝运行。这个限制是有意为之,防止一次崩溃把系统留在按键卡死的状态。
它替代不了什么
第一件事是隐私。快照内容、剪贴板读取、截图、命令输出和调试产物都可能带着私人桌面内容,官方文档专门提醒不要把它们放进公开日志或上传。可选的 JEV helper 会把屏幕描述发给你配置的模型服务商,那是独立组件,不是原生 CLI 的一部分,接不接要单独判断,不能因为装了 CLI 就默认同意了这件事。
第二件事是覆盖面。它只处理有语义的控件,自绘界面和游戏画面不在射程内。浏览器里做自动化,Playwright 那种按 DOM 选择器操作的方式仍然更精确,因为 DOM 比无障碍树信息量大得多,能读属性、能拦截请求、能等到具体网络事件。两套东西不是替代关系,是按场景分工。
第三件事是节奏。这套工具把一次操作拆成观察、动作、再观察三步,每次观察都是一次真实快照。对单人手工调试的脚本,这比一次性点完所有按钮要慢;对需要留下可审计轨迹的自动化任务,这三步就是它的价值所在。慢下来的部分正是可以被验证的部分,也是事后能复盘的部分。
放在 2026 年 11 月的这个时间点看,agent-desktop 的意义不在它多快,而在于它把桌面操作从「看得见就点得到」改成了「有语义才点得到,点完能证明」。前面那半句限制了适用范围,后面那半句决定了它能不能进生产流水线。