鞠躬尽瘁,死而后已。——诸葛亮

让 agent 别再靠截图猜按钮:agent-desktop 换了个器官

先说「computer use」这件老活

让 AI 操作电脑,行业里流行了很多年的办法是:截一张图,让模型看图,猜按钮在哪,然后按坐标点下去。

猜对了,皆大欢喜。猜错了,你会在某个清晨发现它把「删除」当成了「重命名」,然后很有礼貌地告诉你任务完成了。

这个项目对这件事的措辞很客气,但意思很直白:不去像素里猜。

项目地址:https://github.com/vercel-labs/agent-desktop

它的自我介绍是:给任何 agent 提供可靠的桌面 computer use。用 Rust 写,通过操作系统的可访问性树看见任何应用真实的 UI 结构并操作它——ref 保持稳定,动作可以安全重试,而不是从像素里猜。

一句话概括:它不看脸,它摸骨。

它到底换了什么器官

可访问性树这个东西,本来是为视障用户准备的。屏幕阅读器靠它念出界面:这里有个按钮,叫「保存」,可点击;那里有个列表,第三行叫「文档」。

agent-desktop 干的事,是把这棵原本给人用的树,交给 agent 用。

于是整个交互模型变了。它不再输出一张图,而是输出一份 JSON:一份快照,里面每个 agent 能操作的元素都有一个 ref。

然后流程变成三步:观察、对 ref 动手、再观察一次确认。

这里没有像素匹配,也没有「大概在这个位置」。而循环留在你的 agent 里——它不替你思考,它只负责让你能准确地看见和准确地动手。

这个分工很关键。市面上不少 computer use 工具会顺手把你的决策循环也包进去,最后你得到一个黑箱:它说做完了,你只能信。而这个把循环留在外面,你随时可以插一句「等等,先看看那个对话框」。

ref 的三条规矩,每条都是被坑出来的

这套设计的精髓不在「有 ref」,在 ref 的纪律。

第一,ref 属于它那一份快照。 你写一个光秃秃的 @e8,会被 INVALID_ARGS 直接拒绝。必须带上快照 id,比如 @s8f3k2p9:e8。

为什么要这么麻烦?因为界面是会变的。你三分钟前看到的那一行「文档」,现在可能已经变成「下载」了。

第二,界面一变,动作就会失败。 返回 STALE_REF 或 AMBIGUOUS_TARGET,让你重新拍一张快照,用新的 ref。

这条规矩在别的地方叫「报错」,在这里叫「诚实」。多少自动化脚本死于「元素找不到就点旁边那个」这种自作聪明。

第三,多窗口会歧义。 同一个应用开着三个窗口,ref 该指向哪个?它返回 AMBIGUOUS_TARGET,并且把候选窗口 id 列在 error.details 里,让你用 --window-id 指定。

三种错误,对应三种「你不知道自己在点哪」的情形。这套 API 的性格很清楚:宁可停下来问,也不猜着往下走。

那条关于 macOS 的细节,才是真功夫

如果整份文档只让我挑一段夸,我挑这段。

在 macOS 上,CLI 调用会为每个快照命名空间和二进制构建复用一个私有的命令宿主进程。它保留原生的可访问性对象,这样一个已经被删除的行,就不会悄悄地解析到另一个同标识的存活行上。

这句话值得慢慢读。

界面上的元素是有标识的。列表里第 5 行被删掉之后,第 6 行可能顶上来,而某些系统接口会「按标识找最近的」。于是你本来想点「已删除的那一行」,结果点到了「顶上来那一行」——而且不报错。

这是一种最坏的 bug:它成功了,但成功在错的地方。

它的解法是留一个宿主进程,攥着原生对象不放,让「已删除」真的等于「不存在」。

配套还有两条:宿主在闲置 15 分钟后退出;TMPDIR 必须是当前用户所有、权限 0700,而 macOS 默认的每用户临时目录本来就满足。

一个工具愿意为「僵尸元素」写一层常驻进程,说明它被咬过。而且咬得不轻。

60 个命令,4 个主动装死

文档说它带了 60 个命令名,其中 56 个可操作。剩下的四个——key-down、key-up、mouse-down、mouse-up——在有守护进程能释放按住的键或按钮之前,拒绝运行。

我先笑一会儿。

这四个命令的共同点是:它们都是「按下不松开」的一半。按下容易,松开难;一旦 agent 按住了某个键然后崩了,你的电脑就进入一个很微妙的状态——所有输入都被修饰键劫持,你只能拔电源。

所以它干脆让这四个命令躺平,而不是让它们半残地跑起来。

对比一下行业常见做法:先把四个命令都放出来,等用户在 issue 里哭着说「我的 Mac 一直以为我在按 Command」,再打补丁。

主动装死,是一种很高级的克制。

三个平台,三种脾气

安装那一节读起来像一份各地风俗指南。

macOS 要给可访问性权限,用 agent-desktop permissions --guide 走流程。

Windows 什么都不用给——只要目标应用的完整性级别和你的终端一样。

Linux 需要在图形会话里,并且装上 at-spi2-core。

同一个功能,三个操作系统三种态度:一个要你签字画押,一个说随你便,一个先问你有没有会话。

而支持表更细,细到有点残酷:

点击、输入、键盘三个平台都行。鼠标输入,Linux 上要 X11 加 --headed。截图,Linux 上 X11 是窗口级,Wayland 要走 portal。剪贴板,Wayland 需要 wl-clipboard。通知,Linux 只有 GNOME Shell。至于「系统界面」这个能力,macOS 还写着 Planned,Windows 已经有了,Linux 是托盘和操作中心。

一张表把所有「差不多能用」的幻觉都戳破了。这比一句「跨平台支持」值钱一百倍。

PowerShell 里的那个 @

有一个坑,只有真正在 Windows 上跑过的人才会写进文档。

在 PowerShell 里,@token 是 splatting 运算符。所以每一个 ref 都必须加引号:'@s8f3k2p9:e8'。

不加,你的命令不会报「参数错误」,它会开始展开一个变量,然后以一种你完全看不懂的方式失败。

这种细节写进文档,比十句「易于使用」都更能说明作者自己用过。

一个会自己解释自己的二进制

「给 AI agent 用」那一节的思路,我很喜欢。

二进制自带文档,而且和版本匹配:

1
2
3
agent-desktop skills get desktop      # 循环、ref、验证与重试规则
agent-desktop skills get platform # 你所在的系统有什么不同
agent-desktop <command> --help # 单个命令的参数

为什么要自带?因为 agent 读的是它手上这一份二进制的规则,而不是三年前某篇博客里的规则。

更妙的是装进 agent 的方式:一条 npx skills add,或者干脆往 AGENTS.md 里塞一段话,让它第一次用之前先读那两份 skills。

这个设计承认了一个现实:给 agent 写文档,和给人写文档不是一回事。 人读文档,agent 读的是被注入上下文的那一段。你把规则放在二进制里,它就跟版本一起走,不会脱节。

一个信封,和一句「什么时候可以重试」

所有命令都输出一个信封:ok,加上 data 或 error。错误里带 code、disposition,通常还有 suggestion。

而重试的规则只有一句:只有当 error.disposition.retry 是 safe 时,才重试。

这句话是给 agent 看的,但真正该读它的是写 agent 的人。

因为重试这件事,在 UI 自动化里是最容易变成灾难的操作。点一次「提交」失败了,重试一次,成功创建了两个订单——这种故事每个做自动化的人都听过。

把「能不能重试」做成一个显式字段,等于把「幂等」这个抽象概念,塞进了一个字符串里。

顺便说,它还能被 C 调用

每个版本还会发一份 libagent_desktop_ffi,覆盖 macOS、Linux、Windows。这样你的程序可以在进程内调用,而不是每条命令都起一次 CLI。

Python 的示例里有个细节:ad_init(6),那个 6 是 ABI 主版本号,头文件里的 AD_ABI_VERSION_MAJOR 得和库对得上。

一个 CLI 工具顺手把自己做成 C 库,通常意味着它内部的分层本来就很干净——适配层、核心层、外壳层,谁都不越界。这在 Rust 项目里不算稀奇,但愿意把 FFI 当正式产物发出来的,不多。

隐私:截图是会说话的

有一段话,每个做 agent 的人都该读一遍:

快照、剪贴板读取、截图、命令输出和调试产物,都可能包含私人桌面内容。别把它们放进公开日志和上传。

这句话的分量在于它列出了五种东西,而不是只提醒「截图小心」。剪贴板里有你刚复制的密码,命令输出里有你服务器的主机名,调试产物里有整个窗口的树。

而那个可选的 JEV helper——它会把屏幕描述发给配置好的模型提供方——文档特意强调:它和原生 CLI 是分开的。

这句「分开的」很关键。一个工具最怕的不是有联网功能,是有联网功能但没人说得清它在哪一步联网。

分发:一条已经铺好,两条还在路上

npm 一条命令装完,Bun、Yarn、pnpm 都能装同一个包,里面带 x64 和 ARM64 的原生二进制与平台 helper。

1
2
npm install -g agent-desktop
agent-desktop version

如果不想经过 JavaScript 启动器,可以直接下原生压缩包,但要把 helper 放在可执行文件旁边——又是一个「别单独把 exe 拖到桌面」的提醒。

至于 Homebrew 和 Scoop:官方包正在提交,还没进默认目录。而文档还补了一句,仓库不再维护自己的 tap 和 bucket。

先把自己的桶扔了,再去敲官方目录的门。这个操作看着冒险,其实是清醒——自建 tap 是最容易被遗忘的基础设施,放着不管,用户装到的就是三年前的版本。

最后

回到开头那个问题:agent 该怎么操作电脑。

一种答案是让它看得更清楚——更高分辨率的截图、更强的视觉模型、更准的坐标回归。

另一种答案是让它根本不需要看。

这个项目选的是后者。它把界面从「一张图」还原成「一棵有名字、有角色、有状态的树」,然后给每个节点发一个会过期的 ref,再用三种错误码把你从「猜」里拽出来。

代价是它只在能拿到可访问性树的系统上成立,代价是 Linux 上那张表里一半格子写着 X11 或者 GNOME,代价是 macOS 要你亲手去勾一个权限。

但它换来的东西很实在:点错了会报错,而不是点成了。

在一个满地都是「任务已完成」的行业里,一个会说「我不知道该点哪个窗口」的工具,反而更值得信任。