你有没有遇到过这种情况:想让编码 Agent 帮你点一下桌面上的某个软件、填个表单、导个文件,但它只会写代码,碰不了图形界面?传统做法要么截屏丢给视觉模型猜坐标(又慢又容易点错),要么上重型 RPA(部署成本高)。
Vercel Labs 开源的 agent-desktop 换了一条路:它直接读取操作系统本身提供的「无障碍树」(macOS Accessibility API、Windows UI Automation、Linux AT-SPI),把桌面应用的真实结构变成一份 JSON 快照,每个可操作元素都带一个稳定的 ref。Agent 按 ref 点击、输入、滚动,再拍一张新快照验证结果——不截图、不猜像素、可安全重试。在复杂的 Electron 应用上,官方称能把 token 用量降低 78–96%。
先搞懂:为什么无障碍树比截图稳?
每个主流操作系统都会给屏幕阅读器暴露一份「应用 UI 结构树」:每个按钮、输入框、菜单项都有角色(role)、名称(name)、可做的动作(action)。agent-desktop 把这份树当作「观察与交互的事实来源」。
| 对比维度 | 截图 + 视觉模型 | 无障碍树(agent-desktop) |
|---|---|---|
| 观察成本 | 一张高清图几百到上千 token | 一份文本树小得多,可用 --skeleton/--compact 再压 |
| 稳定性 | 布局一动就坐标漂移、点错 | 元素在,ref 大概率不变,点击不漂移 |
| 失败处理 | 猜错只能重来 | 返回 STALE_REF / AMBIGUOUS_TARGET,引导重新快照 |
它不是视觉方案:agent-desktop 不「看」屏幕,它问操作系统「现在有什么、能点什么」。所以它能稳定引用元素,但也意味着它依赖应用暴露无障碍信息——极少数完全不暴露无障碍属性的老旧软件会拿不到元素。
Step 1:安装并验证
agent-desktop 是 Rust 写的单二进制,官方推荐用 npm 全局安装(也可等官方 Homebrew / Scoop 包)。装完先验证版本,确认拿到 1.0.x:
# 全局安装
npm install -g agent-desktop
# 验证安装(应显示 1.0.2 或更新)
agent-desktop --version
# 查看帮助,确认命令可用
agent-desktop --help
agent-desktop 应直接可用。本教程命令以 1.0.2 为准,旧版(0.10.0 之前)已移除 ad_get_tree / ad_find / ad_launch_app 等入口,抄老示例会直接报错。Step 2:给 Agent 授予桌面权限
无障碍能力是系统级权限,必须显式授权,否则快照拿不到元素:
# macOS:系统设置 → 隐私与安全性 → 辅助功能
# 把运行 agent-desktop 的终端 / 你的 IDE 加进允许列表
# Windows:UI Automation 默认开启,无需额外设置
# Linux:需安装并启用 AT-SPI(多数桌面环境默认开)
# 如遇权限问题,可跑权限引导,逐个勾选要授权的应用
agent-desktop
权限是安全边界,别给太宽:只把你确实需要自动化的应用加入允许列表即可。agent-desktop 默认以无头(headless)方式操作——不移动鼠标、不抢焦点;只有在你显式加 --headed 时才做物理输入,那会真的移动你的鼠标指针,操作时别去动电脑。
Step 3:给桌面应用拍一张无障碍树快照
用 snapshot 给某个应用拍快照。-i 让输出更易读,快照里每个可操作元素都带 ref_id(形如 @s8f3k2p9:e8)和 available_actions(这个元素能做什么):
# 给「访达 / Finder」拍快照(-i 人类可读)
agent-desktop snapshot --app "Finder" -i
# 对复杂应用,先用 --skeleton 看骨架,token 更省
agent-desktop snapshot --app "Slack" --skeleton
# 钻进某个区域再细看(用上一步拿到的 ref)
agent-desktop snapshot --app "Slack" --root @s8f3k2p9:e8
返回的 JSON 里,一个节点大概长这样:"role": "treeitem", "name": "Documents", "ref_id": "@s8f3k2p9:e8", "available_actions": ["Click"]。记下这个 ref_id,下一步就靠它操作。
Step 4:按 ref 操作应用(点击 / 输入 / 滚动)
拿到 ref 后,用 click / type / scroll 等操作它,操作完再拍一张快照验证结果。这就是 agent-desktop 的核心循环:
# 点击某个元素
agent-desktop click @s8f3k2p9:e8
# 在输入框里打字
agent-desktop type @s8f3k2p9:e9 "[email protected]"
# 滚动某个区域
agent-desktop scroll @s8f3k2p9:e10 --amount 200
# 操作完立刻复查,确认变成了预期状态
agent-desktop snapshot --app "Finder" -i
不要缓存 ref:ref 与快照生命周期绑定,UI 一变(哪怕只是列表刷新)旧 ref 就失效,操作必报 STALE_REF。正确做法是每次操作前重新 snapshot 取最新 ref,把「再观察」作为循环的固定一步。
Step 5:处理失败信号与重试
agent-desktop 的命令统一返回 JSON envelope(含 ok / data / error 与重试建议)。常见失败信号:
| 错误信号 | 含义 | Agent 该怎么做 |
|---|---|---|
| STALE_REF | ref 对应的快照已过期(UI 变了) | 重新 snapshot,取最新 ref 再试 |
| AMBIGUOUS_TARGET | 元素歧义,不止一个匹配 | 加 --root 缩小范围,或先用 --skeleton 定位 |
| INVALID_ARGS | ref 缺了快照 ID(只写了 @e8) | 补全为 @快照ID:元素ID 再调用 |
Step 6:把 agent-desktop 接进你的编码 Agent
agent-desktop 是 CLI,最稳的接法是把它包成一个「操作桌面」工具,让编码 Agent 在「观察—操作—再观察」循环里调用。下面是一段最小 Node 封装(调用子进程跑 CLI,把 JSON envelope 原样返回给 Agent):
// desktop-tool.mjs
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const run = promisify(execFile);
export async function desktop(action, args) {
// action: snapshot | click | type | scroll
const { stdout } = await run('agent-desktop', [action, ...args], { maxBuffer: 1e8 });
return stdout; // JSON envelope: { ok, data, error, retry }
}
// Agent 调用示例(伪代码):
// 1) desktop('snapshot', ['--app','Finder','-i'])
// 2) desktop('click', ['@s8f3k2p9:e8'])
// 3) desktop('snapshot', ['--app','Finder','-i']) // 验证结果
把这个工具注册成你 Agent 的 computer-use 能力(Claude Code 的 MCP tool、Codex 的 function、或 Cursor 的 command 都行),Agent 就能自己决定「先看看、再点哪、点完确认」。想让操作真的动鼠标,加 --headed;想压低 token,给稠密应用加 --skeleton 或 --compact。更完整的 60 个命令参考见官方文档站 agent-browser.dev/desktop。
隐私红线:快照、剪贴板读取、截图都可能包含你桌面上的私密内容(聊天、文件、密码框)。切勿把桌面快照原样写进公网日志、或上传给第三方服务。只在本地、可信的 Agent 运行环境里使用,必要时先关掉敏感窗口再跑自动化。
预期效果与常见问题
预期效果:跑完上面六步,你的编码 Agent 应该能对着一个真实桌面应用完成「打开 → 定位元素 → 点击/输入 → 验证」的闭环,而且每次操作都有结构化错误可处理。在 Finder、Slack、VS Code 这类暴露无障碍信息的应用上尤其顺滑。
| 你遇到的现象 | 大概率原因与解决 |
|---|---|
| snapshot 拿不到任何元素 | Step 2 权限没给到运行 CLI 的终端 / IDE,去系统辅助功能里补授权 |
| 点击报 STALE_REF | 用了旧 ref,重新 snapshot 取最新 ref 再点 |
| 操作会动我的鼠标 | 误加了 --headed;默认无头不会,去掉该参数即可 |
| 稠密应用 token 太高 | 先用 --skeleton 看骨架,再用 --root 钻进具体区域 |