vercel-labs/agent-desktop:9,569 stars 的无像素匹配桌面 Agent 工具,把可访问性树变成了 Agent 的眼睛
上周有个活儿卡住我了。
不是代码写不出来,是有一家客户的内部系统压根没有 API,也没有自动化测试入口,唯一能操作它的方式就是” 像个用户一样点”。我试了几条路:Playwright 不行,那是网页;Appium 要重写一堆 XPath,而且那个应用用了自绘控件,很多东西在无障碍树里是空的;截图喂给多模态模型倒是能看懂,但模型每次都在同一个” 保存” 按钮上犹豫 —— 不是它笨,是它只能靠像素猜,而像素本身不告诉你” 这个按钮现在是禁用的”。
最后我手写了一个 Python 脚本,用 pyobjc 调 macOS 的 AX API,勉强把流程跑通了。但那脚本非常脆:窗口一换位置就废,重启一次就得改一次,错误处理基本靠 print。
然后我看到了 vercel-labs/agent-desktop。
一、我为什么最终选了它而不是自己写
先说结论,因为它解决的就是我上面那个具体问题,而且解法出乎意料地克制。
这个项目现在是 9,569 stars、647 forks、Apache-2.0,主语言 Rust,2026 年 2 月 19 日建的仓,2026 年 10 月 28 日发 1.0.0,10 月 30 日发到 1.0.2。仓库首页写着 Vercel Labs Product 徽章,npm 包 agent-desktop 最近一个月下载量 14,843 次。
它一句话的自我定位是:
Computer use for AI agents: read and operate desktop apps through the OS accessibility tree.
注意它说的是 “read and operate”,不是 “look at”。这个区别是整个项目的立足点。
我把它和其他方案放一起对比时的判断是这样的:
| 方案 | 输入通道 | 定位问题的方式 | 适合场景 |
|---|---|---|---|
| 我自己写的 pyobjc 脚本 | AX API | 语义 ref,但没做生命周期管理 | 一次性任务 |
| OmniParser(25,602 stars) | 截图 → 结构化 | 纯视觉解析 | 应用没有无障碍树 |
| browser-use / Skyvern | DOM | 网页语义 | 浏览器内 |
| agent-desktop | AX / UIA / AT-SPI | 语义 ref + 严格重识别 | 桌面原生应用 |
OmniParser 那条路我也想过,它用视觉模型把截图转成结构化元素,好处是不依赖应用配合无障碍 API,坏处是 —— 它还是从像素来的。” 这个按钮是灰的还是亮的” 这种问题,它只能靠训练数据和模型直觉去猜;而 AX API 直接告诉你 enabled: false。
这也是我认为这个项目最值得写的地方:它没有在” 给 Agent 更好用的截图”,而是在” 把操作系统已经知道的真相结构化地交出来”。
二、它长什么样:三个部件
FAQ 里对整个项目的组成给了一张表,抄在这里当骨架:
| Component | Function |
|---|---|
| Native Rust CLI | Fast, single binary, no runtime dependencies |
| C-ABI cdylib | Load once from Python, Swift, Go, Ruby, Node, or C instead of forking |
| 60 Commands | Observation, interaction, keyboard, mouse, notifications, clipboard, window management, session lifecycle, trace read/export, and bundled skills docs |
| Snapshot & Refs | Compact snapshot IDs and deterministic element refs like @s8f3k2p9:e1 |
| Structured JSON | Machine-readable responses with error codes and recovery hints |
三个部件对应三种用法。
1. CLI—— 给 Agent 用。每个命令是独立进程,输出一个 JSON 信封。
2. C ABI 动态库 —— 给需要高频调用的程序用。libagent_desktop_ffi 支持 macOS、Linux、Windows,避免” 每条命令 fork 一次” 的开销。README 给的 Python 例子:
1 | import ctypes |
注意 ad_init(7) 那个 7——C ABI 主版本号到了 7。CHANGELOG 里 Unreleased 段落明确写了这是 breaking change:ad_resolve_element_exact 和 ad_execute_ref_action_exact_with_policy 现在总是拒绝并返回 ACTION_NOT_SUPPORTED(details.kind: exact_entry_unsupported),理由是”C 调用方无法提供 ref 现在赖以解析的 capture proof”。
3. 内置 skills 文档 —— 这个设计我觉得很聪明:
1 | agent-desktop skills get desktop # the loop, refs, verification and retry rules |
文档直接编进二进制,版本永远跟二进制一致。用 skills.sh 的方式装进 Agent 框架:
1 | npx skills add https://github.com/vercel-labs/agent-desktop --skill agent-desktop |
或者往 AGENTS.md 里塞几行:
1 | Use `agent-desktop` to see and operate desktop apps. Before the first use, run |
三、核心循环:观察、行动、再观察
这是整个项目最需要理解的部分。SKILL.md 里写得很直接:
It is a tool, not an agent. Every command prints one JSON envelope (
ok,dataorerror). The observe, act and verify loop runs in you.
也就是说,它不做决策,循环在 Agent 那一侧。它只保证每次观察给出一份确定的快照,每次行动前会重新确认目标还是不是原来那个。
README 的快速开始:
1 | agent-desktop snapshot --app Finder -i # interactive elements, each with a ref |
输出长这样:
1 | { |
这里几个细节值得停一下:
ref 是” 带快照 ID 的”。 @s8f3k2p9:e8 里 s8f3k2p9 是 snapshot_id,e8 是这次快照里的第 8 个元素。裸写 @e8 会被拒绝,返回 INVALID_ARGS。CONCEPTS.md 里的理由是:
Refs are deterministic inside one snapshot but are not stable across UI changes. Snapshot and find output qualify each ref with its snapshot ID, and every command accepts only the qualified form. A bare
@eNfails withINVALID_ARGS.
这等于强制你” 每次 UI 变了就重新观察”,而不是拿一个旧坐标硬打。
available_actions 直接告诉你这个元素能干什么。 这是语义通道的威力 —— 不是” 它长在那个位置”,而是” 它是个可点击的 treeitem”。
complete: true 是完整性标记。 这个字段很关键,下面第四节专门讲。
渐进式钻取
稠密应用(比如 Slack、Outlook)整棵树能有几千个节点,全读进来会把上下文吃光。所以有骨架模式:
1 | agent-desktop snapshot --skeleton --app Slack -i --compact # 浅层概览 |
CONCEPTS.md 叫它 Drill-down:一次从已有 ref 出发、只观察该元素子树的快照操作,而不是重读整个窗口。这里有个容易踩的坑,SKILL.md 专门警告了:
A
--rootdrill adds its refs to the same snapshot id. Refs outside the drilled subtree, and the drilled root itself, stay valid; refs that were already inside it are replaced, so an old ref there returnsSTALE_REF.
也就是说钻取不是” 新快照”,是” 往同一个快照里补货”,但被钻区域内部的旧 ref 会被作废。还有两条很实用的经验:
Never skeleton a surface (
--surface menu,sheet,alert). A surface is already focused.After an async step, use
wait(seecommands-system.md). A fixed sleep is a race.
四、最核心的设计:为什么它这么” 较真”
我读 CONCEPTS.md(353 行)和 docs/json-output.md(180 行)读了两遍。这份文档的信息密度极高,我挑几个我第一次读到时” 哦,原来是这样” 的设计讲。
4.1 Evidence Tri-State:三态证据不是二态
CONCEPTS.md:
Every property an adapter reads is
Known,Absent, orUnknown, and the three are never collapsed into two.
Absentis an answer: the provider was asked and does not have this.Unknownis the lack of one: the read failed, or what it returned cannot be trusted.
Absent(问过了,没有)和 Unknown(读失败了,不知道)必须区分,原因写在原文里:
Absentsatisfies completeness gating andUnknownmust not, so a target that never answered cannot pass for one that answered “no”. Conversely a role, state, or affordance is granted only on a positive claim, so a failed read withholds it.
然后是最狠的一句:
A convenience predicate that flattens the tri-state to
boolis therefore safe only in positive position, asking “did this say yes”, and fails open the moment it is negated, because negation silently rewrites “I could not tell” as “definitely not”.
把三态压成 bool 的便利函数,一旦被取反就会把” 我搞不清” 悄悄改写成” 肯定不是”。 这是我见过对” 不要在错误处理里偷懒” 最精确的表述之一。
4.2 Strict Ref Resolution:ref 到底怎么确认还是原来那个元素
这是我认为这个项目技术含量最高的部分。CONCEPTS.md 有一整节叫 Ref Re-identification。
macOS 上有捷径:AX API 会给元素一个 accessibility address,ref 里存着它,后来的命令直接转回去,不用搜:
On macOS the entry carries the element’s own accessibility address in
retained_object, so a later command turns the ref back into that element without searching. The address is accepted only while the element is alive and its process, role and recorded identifier still match.
其他平台没有这根救命稻草,只能靠”capture proof”:
Every other ref resolves from a capture proof. A ref is usable only when the capture saw its whole window completely and exactly one observed element matched its full predicate: role plus every recorded stable field, that is native id, stable name and description.
翻译成人话:Windows/Linux 上的 ref 要能用,必须满足两个条件 —— 当初拍快照时,这个元素在完整窗口里是唯一的;现在实时再扫一遍,还是只有一个匹配。
规则是硬编码的:
Zero matches is a stale ref, two or more is ambiguous, and a field the live read could not answer stays unknown rather than becoming a match, so incomplete evidence never resolves. Values and bounds are never identity. A ref that was not unique at capture is refused before anything is read from the application, because no search can pick one of two identical elements.
“Values and bounds are never identity”—— 值和坐标不是身份。“两个长得一样的元素,ref 宁可不给你用”。
这个取舍的代价写在 docs/json-output.md 的 2.6 migration 里:
A ref to one of two identical controls, which earlier resolved by position or geometry, now fails
AMBIGUOUS_TARGETwithdetails.kind: "identity_not_unique"before anything is read.
也就是说 2.6 是个 breaking change,以前靠位置 / 几何能蒙对的操作,现在直接拒。
四种 details.kind 值得抄下来:
details.kind |
Code | Meaning |
|---|---|---|
retained_object_stale |
STALE_REF |
macOS:元素地址已死,或进程 / 角色 / 标识符不匹配 |
saved_ref_absent |
STALE_REF |
完整证据下没有活元素匹配,元素没了 |
saved_ref_ambiguous |
AMBIGUOUS_TARGET |
出现了和当初一样的孪生元素,两个以上匹配 |
identity_not_unique |
AMBIGUOUS_TARGET |
拍快照时就没证明它唯一,或窗口没读完整 |
还有第五种 resolution_identity_unknown,带 TIMEOUT,是可重试的。
4.3 快照不完整 = ref 全部作废
这是我觉得非常值得学的一个设计:
A snapshot that needed one complete read of its window to prove its refs, and could not get it, says so with
"identity_unproven": trueindata.completestill describes the presented tree. On Windows and Linux, and on macOS for an element with no address, every ref of that snapshot failsAMBIGUOUS_TARGETwithdetails.kind: "identity_not_unique".
翻译:快照树可能是完整展示给你的,但如果这次快照没能在超时内完整读完整个窗口,它产出的所有 ref 在 Windows/Linux 上都是不可用的。
这解决了 GUI 自动化里一个经典的坑:Agent 拿到一棵被截断的树,以为看到了全部控件,其实后面三分之一没读出来,于是基于不完整信息做出错误决策。
4.4 Partial Observation:超预算不报错,报” 不完整”
docs/json-output.md 记录了 2.2 版本的这个改动:
Version 2.2 replaces the 2.1
TIMEOUTerror for a budget-exhausted snapshot with a successful, explicitly partial response, and addsdata.complete. Snapshot data always carriescomplete. A snapshot that runs out of budget now returnsok: truewith"complete": false, the tree it did observe,"truncated": true, andnodes_observed, instead of aTIMEOUTerror and no tree.
超时不是错误,是” 部分观察”。CONCEPTS.md 里 Partial Observation 条目还补了一句:
Completeness is reported on the observation as a whole and on each node whose descendants were cut, so a reader can walk from the root to the boundary.
即:既在整体上报完整度,也在每个被截断的节点上单独标 subtree_truncated: true,让读者能一路走到边界。
4.5 身份回收:窗口句柄会被复用
CONCEPTS.md Window Identity:
Window handles can be recycled: after a window is destroyed, the OS may hand its handle to a different window. A handle alone therefore names the wrong window after churn. Identity is the handle corroborated by a process-generation token (a value derived from the owning process’s creation time), so a recycled handle whose process generation no longer matches fails closed rather than resolving to the new occupant.
句柄 + 进程代际 token 双重确认,进程代际来自进程创建时间。Windows 配 HWND + creation-time token,macOS 配 window number + process start-time,Linux 配 X11 client id + boot-id 限定的进程启动时间。
还有更狠的一句:
Where the check and the act cannot be a single atomic operation, the corroboration has to be repeated immediately before each write the action performs, and the action’s own success check must itself be identity-qualified, confirming the responding resource is still the expected occupant and not merely that some resource at the expected handle responded.
检查和行动无法原子化时,每次写入前都要重新确认一次,而且成功检查本身也要带身份限定 —— 不能只确认” 那个句柄上有东西响应了”。
显示器也有同样一套(Display Identity),截图前后各查一次,中间变了就丢弃像素(Identity Sandwich)。
4.6 Secure Field:密码框的契约是双向的
CONCEPTS.md Secure Field:
The contract spans both sides: the read side withholds value and related text evidence and resolves by content-free fingerprint; the action side may write into the field but never echoes the attempted or observed value in steps, messages,
details,platform_detail, or post-state, and reportsverified: Nonewhen a re-read cannot confirm without leaking.
读侧:不给值,用无内容的指纹来定位。写侧:可以往里写,但步骤、消息、details、platform_detail、post-state 里一个字符都不许回显,连尝试写入的内容也不行。
五、动作的可靠性:delivered 和 verified 是两件事
这一整块叫 Delivery Semantics,我认为是这个项目对 Agent 生态最直接的贡献。
CONCEPTS.md:
The distinction that matters is not success versus failure but delivered versus not: an action that never reached the target can be retried freely, while one that may have landed cannot be repeated without risking a duplicate.
失败 ≠ 没送达。一个可能已经生效的动作,重试会重复提交。 这在” 帮用户点提交订单” 的场景里是致命的。
docs/json-output.md 给出了 disposition 的取值:
| 字段 | 取值 |
|---|---|
delivery |
unknown / not_delivered / delivery_uncertain / delivered_unverified / delivered_verified |
retry |
unknown / safe / unsafe |
规则是 not_delivered 一定 safe,其他非 unknown 的都 unsafe。
SKILL.md 里给 Agent 的判断准则很直白:
Retry or substitute an action only when
disposition.retryissafe. Onlynot_deliveredis safe.
Any other delivery value means the action may have landed. A repeat can click, type or submit twice, andtogglecan undo itself.
toggle 可以把自己撤销掉 —— 这个细节很实在。
错误信封的完整样子:
1 | { |
recovery 只在 disposition.retry 是 safe 时出现。docs/json-output.md 里明确写了 2.1 版本做的破坏性变更:
Version 2.1 replaces the 2.0
error.retry_commandstring with the structurederror.recoveryobject and addserror.disposition. Consumers must select a recovery strategy fromrecovery.strategyonly whendisposition.retryissafe; command strings from older envelopes must not be executed blindly. The removedretry_commandfield has no compatibility alias.
“旧的 retry_command 字符串不许盲目执行”—— 因为一个拼接出来的重试命令可能重复扣款。这是从事故里长出来的设计。
5.1 Mutation Classifier:写操作不能复用读的分类
CONCEPTS.md Mutation Classifier:
It answers whether the write was delivered, whether the affordance is genuinely absent (fall back to the next chain rung), or which structured failure and disposition apply. It is the write-side counterpart of Read Outcomes and must never reuse that cluster: the read table’s retryable transport codes are exactly the HRESULTs a write may already have delivered, so consulting it would authorize a double-dispatch.
读操作里” 可重试的传输错误码”,恰恰就是写操作” 可能已经送达” 的那些码。 拿读的标准去判断写,就会授权一次重复投递。这是两个不同的表,必须分开。
5.2 Read Outcomes:读失败的四种归宿
CONCEPTS.md 把失败的原生读取分成四类,并强调” 任何两类合并都是常见错误”:
- Settled Absence—— 最终答案:问的东西不存在(provider 没实现这个 pattern、搜索确实啥也没匹配、锚点路径已经永久失效)。不重试。
- Retryable Failure—— 关于传输而非目标的非答案(超时、fault)。值得重复。
- Unavailable Element—— 元素本身没了。
- Terminal Failure—— 没有定义恢复路径(拒绝、未分类的码)。最终性同 settled absence。
还特别加了一句:
An unrecognised failure code lands here by default, so a read nobody has classified surfaces rather than being guessed into a retry loop.
没分类的错误码默认落到” 终局失败”,宁可暴露也不要猜着重试。
六、headless 默认,以及为什么这很重要
Interaction Policy 这节定义了三种策略:
- headless(默认):只用无障碍 API,不动光标,不抢焦点,语义路径不可用时 fail closed。
- headed(
--headed显式开启):授权焦点和光标前置条件。 - focus fallback(中间态):允许焦点但不允许移动光标。CLI 的 flag 到不了,语言绑定可以直接选。
CONCEPTS.md:
Headless ref actions may still fail when the native accessibility API cannot perform the requested action; they fail closed with structured actionability or policy errors rather than silently substituting physical input. The broader headed policy must be selected explicitly with
--headed.
headless 失败时不许偷偷降级成物理输入。 这条规则的价值在于可预测性:Agent 知道失败就是失败,不会出现” 我以为我没动鼠标,结果鼠标动了”。
SKILL.md 里还有一条纪律,我觉得比前面所有规则都重要:
Never switch to headed mode to recover from a headless failure.
不许用 headed 去救 headless 的失败。 因为一旦开了物理输入这个口子,Agent 在任何一次重试时都可能顺手升级成 headed,然后点到不该点的地方。
Windows 上有个具体差异:
On Windows,
typehas no semantic headless path, because UIA offers no insert-at-selection write. Strict-headlesstypetherefore fails at policy, andset-valueis the headless text path. Headedtypesynthesizes keys physically.
Windows 上 headless 的 type 会在策略层直接被拒,因为 UIA 没有” 在选区插入文本” 的接口。要无头写文本得用 set-value。
6.1 Action Chain:动作阶梯
一个 ref 动作要走一条策略阶梯,每一级都被策略把关,每一级的送达证据都被记录:
The chain pins one execution deadline at its start (distinct from the Resolver Deadline, which budgets re-identification) and every step observes it. Expiry while a step may have partially mutated the element surfaces as a structured timeout carrying the observed state, never as a plain step failure.
两个不同的 deadline:解析用 Resolver Deadline,执行用 chain 自己的 deadline。
6.2 Actionability Battery:动手前的体检
The shared pre-dispatch set of live checks core runs against a resolved element: visible, stable, enabled, supported action, policy, editable, and receiving events, before any adapter dispatch.
七项检查。还有个细节:
The battery’s
receives_eventscheck sweeps five candidate points from the element’s bounds (center plus four quadrant points) and passes if any reaches the target, asking whether the element is reachable at all so a partially occluded control still passes.
中心加四个象限点,五个采样点任一命中就过 —— 问的是” 这个控件能碰到吗”,部分被遮挡的控件也算能操作。
6.3 Auto-Wait:默认等 5 秒
The default-on bounded poll that holds a ref action until its target becomes actionable, then fails with
TIMEOUTif the budget expires.The bound is 5000ms (
typeadds 30ms per character of its text);--timeout-ms 0restores single-shot act-immediately behavior.
type 的预算按字符加 30ms。这是个很实在的工程细节。
七、并发、租约和多 Agent
Interaction Lease:
Machine-wide exclusivity over desktop input, held by one process at a time so concurrent callers cannot interleave synthetic input into each other’s actions.
The lease covers dispatch only, never the waiting that precedes it: waiting for an element to become actionable can run long, and holding exclusivity across it would serialize every caller on the machine. Anything resolved while waiting was therefore observed without exclusivity and is re-resolved once the lease is held. That second resolution is the correctness boundary, not redundant work.
租约只覆盖” 发送” 这一刻,不覆盖” 等待”。 因为等待可能很长,锁住等待会把整台机器串行化。所以等待期间解析出的元素是” 无独占观察” 出来的,拿到租约后必须重新解析一次 —— 这一次才是正确性边界,不是冗余工作。
这一条我觉得是全项目最精妙的细节。
多 Agent 鼠标光标
CONCEPTS.md Agent Cursor Overlay 描述的场景我觉得很酷:给 harness 的多个 subagent 每个分配一个鼠标光标。
1 | session start --cursor --multi-agent |
然后每个 subagent 带 --agent-id <id>(或 AGENT_DESKTOP_AGENT_ID)。规则:
A headless action moves its cursor before it takes the interaction lease, so several agents’ cursors travel at the same time while their actions take turns.
光标同时移动,动作轮流执行。 这个区分很关键 —— 视觉上是并发,语义上还是串行。
commands-system.md 里补充了 id 的规则:1 到 64 个字母数字或 - _,按 session 作用域隔离。
光标本身是纯表现层的:
It never moves or replaces the OS pointer and never changes Interaction Policy, action delivery, or response semantics.
7.1 Release Guard:多步物理序列的保险丝
CONCEPTS.md Release Guard:
An arm-before-commit guard on multi-event physical sequences (drag, modifier chord, chunked text) that posts corrective input at the origin on abort: drag-back plus mouse-up for a drag, key-up sweep for held keys and modifiers.
Cleanup is best-effort: if the OS does not acknowledge the corrective events, the error preserves that uncertainty (
emergency_release_posted,delivered_unverified,delivered_eventscount) rather than claiming no input landed.
拖拽中途失败要” 拖回去 + 松开鼠标”,组合键要” 逐个松开”。但如果纠正事件本身没被系统确认,错误信息里要保留这个不确定性,不能撒谎说” 什么都没发生”。
八、命令清单
60 个命令名,56 个是可用的,剩下四个(key-down、key-up、mouse-down、mouse-up)会拒绝执行:
The other four (
key-down,key-up,mouse-down,mouse-up) refuse to run until a daemon can release a held key or button.
因为” 按住不放” 需要跨进程维持状态,而每次 CLI 调用都是独立进程 —— 这个限制来自架构本身,不是没实现。
分组如下:
| Group | Commands |
|---|---|
| Observe | snapshot, find, get, is, screenshot, list-surfaces |
| Act on a ref | click, double-click, triple-click, right-click, type, set-value, clear, focus, select, toggle, check, uncheck, expand, collapse, scroll, scroll-to |
| Keyboard and mouse | press, hover, drag, mouse-move, mouse-click, mouse-wheel, key-down, key-up, mouse-down, mouse-up |
| Apps and windows | launch, close-app, list-apps, list-windows, list-displays, focus-window, resize-window, move-window, minimize, maximize, restore, open-system-surface |
| Notifications | list-notifications, dismiss-notification, dismiss-all-notifications, notification-action |
| Clipboard | clipboard-get, clipboard-set, clipboard-clear |
| Wait | wait |
| Sessions | session, trace, cursor-overlay |
| System | status, permissions, version, batch, skills |
会拿到 ref 的交互角色,FAQ 里列了 18 个:
button, textfield, checkbox, link, menuitem, tab, slider, combobox, treeitem, cell, radiobutton, incrementor, menubutton, switch, colorwell, dockitem, listbox, option。
静态文本和惰性容器不给 ref。
九、平台支持:三套完全不同的底层
| macOS | Windows | Linux | |
|---|---|---|---|
| Accessibility tree | Yes | Yes | Yes (AT-SPI) |
| Click / type / keyboard | Yes | Yes | AT-SPI; X11 / GNOME keys |
| Mouse input | Yes | Yes | X11, --headed |
| Screenshot | Yes | Yes | X11 windows; Wayland portal |
| Clipboard | Yes | Yes | Yes; Wayland needs wl-clipboard |
| App & window management | Yes | Yes | Launch/list; X11 window control |
| Notifications | Yes | Yes | GNOME Shell |
Shell surfaces (open-system-surface) |
Planned | Yes | GNOME tray, Action Center |
三套 API 的词汇本身就不对齐,CONCEPTS.md Role 一节专门解释了映射的麻烦:
The platform taxonomies are not parallel: UIA’s
Tabis the container and itsTabItemis the page selector, which is the inverse of the ARIA naming core follows, and several canonical roles (switch,colorwell) have no control type at all and are reachable only through refinement or not at all.
更绝的是同一个控件在不同平台的差异:
A toggle button surfaces as
role: buttonwith statepressedon macOS, because macOS keeps the control’s role asbuttonand reads its toggle value aspressed. Windows reclassifies anyButtoncontrol type that advertises toggle support torole: switchwith statecheckedbefore states resolve, and Linux reports a toggle button aschecked, reservingpressedfor push buttons.
同一个开关按钮,macOS 上是 button + pressed,Windows 上是 switch + checked,Linux 上是 checked。 三套都是” 正确的映射”,但映射到不同的角色上。
文档还说了这不是缺陷:
The macOS/Windows divergence for the same control is a consequence of two correct role mappings meeting it, not a defect in either.
权限
- macOS 需要 Accessibility 权限,截图还要 Screen Recording,打开通知中心要 Automation 权限。
- Windows 对同权限级别的应用不用授权。但管理员运行的 app 能读,只有提权的 agent 才能给它发输入 —— 这就是 Integrity Boundary。
- Linux 目标环境是 Ubuntu 24.04+ / GNOME 46 /glibc 2.35+,需要
at-spi2-core,且必须跑在用户的图形会话里(SSH 的 TTY 里看不到应用)。Ubuntu 22.04 的 ATK bridge 会在Action.GetActions上崩溃。
Windows 的 Integrity Boundary 值得单独说,CONCEPTS.md:
Detection is a token-integrity comparison via
GetTokenInformation(TokenIntegrityLevel), never theSendInputreturn value, because the API reports success in both blocked and unblocked arms.
不能用 SendInput 的返回值判断权限边界,因为它被拦截时也返回成功。得自己去查完整性级别。文档说这在拆分权限的测试机上实测过:Medium 到 High 的 set-value 和 type 会 fail closed。
十、它怎么证明自己是对的:probe 语料和 dogfood
这一节是我在这个仓库里最欣赏的部分。
Probe Corpus:
The committed, re-runnable set of raw platform scripts a platform’s exploration work uses to observe real OS behavior before any adapter code for that platform exists.
在写任何适配器代码之前,先用脚本把真实 OS 的行为观测下来。
Capture 是一次探针运行的输出,脱敏、机器可读,存放在产生它的探针旁边。而且:
Every capture is written beside a normalized twin (regenerated on every run and git-ignored) whose run-varying values (handles, process and thread ids, timings, coordinates beyond a fixed tolerance) are canonicalized, so re-running the corpus later diffs empty unless the platform genuinely changed.
跑一遍归一化的孪生文件,diff 是空的 —— 除非平台真的变了。 这是我见过最硬核的回归测试思路:不是断言” 结果等于某串具体值”,而是断言” 重跑没有漂移”。
Findings Ledger 是探针结果的账本,每一行都有稳定 id:
Each row carries a stable id naming its area and its position in that area (
A15-7on Windows,L32-39on Linux), and that id is the project’s citation form for a measurement: a row id stays true however the code or the docs move, so shipped source may cite one the way it would cite a CVE.
行号 id 是项目的引用格式,代码和文档随便怎么改,这个 id 都不变,就像 CVE 一样。 正文里确实到处能看到 A9-3、A19-2、A4-1 这样的标注。四种判定是 CONFIRMS / CONTRADICTS / NEW-EDGE / DEFERRED,而 CONTRADICTS 的处理方式最狠:
A contradicting verdict is a defect in whatever assumed otherwise and is corrected in the same change that discovered it.
发现假设错了,就在同一个改动里把假设改掉。
还有 Mandatory Measurement Gate—— 探针自己声明” 我要产出哪些测量”,跑完检查这些测量是不是真的产出了:
Declaring nothing fails too, and for the same reason: a run that asserted nothing is indistinguishable from a clean one, so an empty declaration is reported as a gap in its own right rather than passing vacuously.
什么都没断言的运行,和干净通过的运行长得一模一样 —— 所以空声明本身要算失败。
Gate Self-Test 是给检查本身写的测试,避免” 检查规则悄悄失灵了还报绿”:
The fixture must invoke the same rule declaration the real scan invokes (a shared program text, never a copied pattern), or it proves only that the copy still agrees with itself while the shipped rule drifts away from both.
还有一条我觉得太真实了:
And it must cover the check’s ability to run at all, not merely its rule: a gate whose interpreter is absent, whose file never got its executable bit, or whose regex dialect is a silent no-op on the platform that actually runs it exits clean and is indistinguishable from a passing tree. Finding nothing is a failure, never a skip.
“什么都没找到是失败,不是跳过。” 这句话适用于一切自动化检查。
Dogfood Run 则是拿真实发布版二进制去操作仓库作者自己没写的软件:
Driving the shipped release binary against real software nobody in this repository wrote and judging it by reading its output, because correctness against real providers is not something a runner-hosted assertion can encode.
This is where a platform adapter’s correctness is established; a green build proves totality and wiring, not that a mapping is right.
绿灯的构建只能证明” 每条路径都被走了一遍”,证明不了” 映射是对的”。
Linux 上有个特别诚实的说明:
On Linux only the ledger (
probes/linux/FINDINGS.md) is committed; its probe scripts and captures are kept outside the repository.
Linux 的探针脚本和捕获不放进仓库,只有账本在里面。
还有 Dev Box 和 Hosted Runner 的区分:
A number measured only here is a fact about this machine, not about the design. Repetition removes noise but never removes environment, and a result that looked like a design win on the dev box alone has already turned out to be a dev-box artifact once.
只在开发机上测出来的数字,是这台机器的事实,不是设计的事实。
十一、跟同类项目比,差别在哪儿
我做对比时全部重新查了 GitHub API 的实时数据:
| 项目 | Stars | 许可 | 语言 | 通道 |
|---|---|---|---|---|
| vercel-labs/agent-desktop | 9,569 | Apache-2.0 | Rust | AX / UIA / AT-SPI |
| browser-use/browser-use | 119,866 | MIT | Python | 浏览器 DOM |
| vercel-labs/agent-browser | 45,281 | Apache-2.0 | Rust | 浏览器 DOM |
| trycua/cua | 31,597 | MIT | Rust | 跨 OS 沙箱 + 驱动 |
| browserbase/stagehand | 25,911 | MIT | TypeScript | 网页 DOM |
| microsoft/OmniParser | 25,602 | CC-BY-4.0 | Jupyter | 纯视觉 |
| simular-ai/Agent-S | 12,859 | Apache-2.0 | Python | 混合视觉 + 轴 |
几点观察:
1. 它不是最红的,但它是唯一做” 非浏览器桌面原生应用” 这件事的。
browser-use 119k stars 压倒性领先,但它的战场是网页。Skyvern、stagehand、agent-browser 同样是网页。agent-desktop 把浏览器那套(DOM → 语义 ref → 严格定位)搬到了桌面原生应用上 —— 这是完全不同的技术栈。
有意思的是它和 agent-browser 是同一个作者(vercel-labs),同样的 Apache-2.0、同样的 Rust、同样”CLI + C ABI + 结构化 JSON + 内置 skills” 的架构。同一个团队把同一个设计哲学用在了两个不同的树上,这本身是个强信号。
2. 和 OmniParser 的路线之争最本质。
OmniParser 走” 纯视觉” 路线:截图 → 检测 → 识别 → 结构化。优势是不要求应用配合,任何屏幕像素都能用。agent-desktop 走” 语义” 路线:直接读操作系统的无障碍树。
| OmniParser(视觉) | agent-desktop(语义) | |
|---|---|---|
| 依赖 | 只要能截图 | 应用要暴露无障碍树 |
| “按钮是否禁用” | 靠模型猜 | AX/UIA 直接给 |
| 元素身份 | 靠坐标 + 外观 | 靠 native id /name/role |
| 成本 | 每次推理 | 一次系统调用 |
| 抗 UI 变化 | 差 | 好 |
| 适用范围 | 几乎任何屏幕 | 有无障碍树的原生应用 |
它们其实是互补的。 agent-desktop 的 screenshot 命令存在,视觉调试产物(Visual Debug Artifact)也支持把快照节点和窗口截图并排画给人工检查 —— 项目自己就在用视觉手段做验证,只是不让视觉进入决策回路。
3. 和 trycua/cua 的分工。
cua 走的是” 跨 OS 沙箱 + 驱动 + 训练基准”,agent-desktop 走的是” 在真实桌面上的可靠性”。cua 更像基础设施层,agent-desktop 更像一次具体的动作原语。
4. 和 browser-use /stagehand 的对照最能说明取舍。
那俩项目里 Agent 用的是 Playwright 的语义定位(getByRole、getByText)。agent-desktop 在桌面上的对应物就是 ref—— 但比 Playwright 严格得多。Playwright 的 locator 失败会等超时重试;agent-desktop 在两个相同元素时直接给你 AMBIGUOUS_TARGET 让你换路子。这个严格是有代价的,但代价换来的是行为可预测。
十二、局限:说点不好听的
这个项目我不打算只夸。以下都是我从 README / CHANGELOG / 文档里能查到的实打实的限制。
12.1 Linux 是二等公民
Linux 支持是 1.0.0 才加的(2026-10-28),而且是” 刚加” 的状态:
- 目标环境只有 Ubuntu 24.04+ / GNOME 46,Ubuntu 22.04 直接崩(ATK bridge 在
Action.GetActions上 abort)。 - Wayland 下 app 的指针输入拒绝执行,原生窗口控制不支持。
- Wayland 剪贴板要额外装
wl-clipboard。 - Linux 的探针脚本和捕获根本没进仓库,只有账本。别人没法复现验证。
- Wayland 下连” 点两个相同控件中的一个” 都做不到 —— 文档明说只能
--headed mouse-click --xy,而 Wayland 会拒绝这个操作。
所以 Linux 用户实际上拿不到完整的 ref 语义能力。
12.2 Windows 上 type 没有无头路径
前面提过,这里再强调一次,因为它是日常最高频的操作:
On Windows,
typehas no semantic headless path, because UIA offers no insert-at-selection write.
Windows 上要么用 set-value(绕开选区语义),要么开 headed(放弃无头的所有好处)。这是一个跨平台行为差异,但差异落在最常用的动作上。
12.3 严格性带来的” 功能缺失”
2.6 版本的 breaking change 让一批以前能做的操作现在做不了:
A ref to one of two identical controls, which earlier resolved by position or geometry, now fails
AMBIGUOUS_TARGET.
表格里重复的按钮、行内重复的删除图标 —— 这些在真实应用里非常常见。SKILL.md 里承认了:
On Windows and Linux that reads the row but does not make its button actionable: a button repeated in other rows is a twin in the window and its ref is refused.
读得到,点不了。 而且 2.6 之前拍的老 refmap 全部作废:
A refmap written before 2.6 records no capture proof, so all of its refs on these platforms fail the same way. Take a new snapshot.
如果你的 Agent 上下文里缓存了旧 ref,升级后会直接全线报错。
12.4 每个 CLI 调用一个进程
Every CLI call runs in its own process. Headless calls still prohibit foreground activation and synthesized input.
README 明确说 Headless calls 也禁止前台激活和合成输入。这个设计换来了无依赖单二进制和强隔离,代价是高频调用有进程启动开销。npm 包装器的 postinstall 会把 Node 启动器换成原生二进制的链接:
If the package manager blocks install scripts (npm 12 does by default), the Node launcher runs instead and each call costs about 146 ms.
npm 12 默认屏蔽 install scripts,于是每次调用多花约 146ms。要手动批:
1 | npm install-scripts approve agent-desktop |
或者 --allow-scripts。要极致性能就用 C ABI。
12.5 权限和信任边界
- macOS 要 Accessibility + Screen Recording 权限,等于” 读你整个屏幕” 的权限级别。
- Windows 二进制没有签名:
The binary is unsigned, so SmartScreen may warn on a browser download, antivirus may quarantine it, and Smart App Control on a clean Windows 11 install blocks it.
在企业环境里这可能是直接过不去的坎。
12.6 隐私:截图和快照就是桌面内容
README 的警告很直接:
Snapshots, clipboard reads, screenshots, command output and debug artifacts can contain private desktop content. Keep them out of public logs and uploads.
FAQ 补充:
Debug HTML embeds a screenshot and accessibility data. Session traces can retain evidence artifacts even when structured fields are redacted. Redaction does not make a screenshot safe to publish.
脱敏不等于截图可以公开。 如果你要把 Agent 的运行日志贴到工单系统里,这一段得认真读。
12.7 版本口径容易看错
- Rust /npm 包版本:1.0.2(2026-10-30 发布)
- JSON 响应信封版本:2.6
这俩不是一回事。还有 C ABI 主版本到了 7。CHANGELOG 的 Unreleased 段里已经堆了好几个 breaking change,包括移除 macOS command host(每次 CLI 调用独立进程)、C ABI 的 *_exact 入口全部移除、2.6 的 ref 解析规则变化。
这是个刚发 1.0 就在做破坏性变更的项目。 用之前请认真读 CHANGELOG。
12.8 open issues 122 个
仓库当前 122 个 open issue。不是致命问题,但说明生态还很年轻。
十三、适合谁,不适合谁
适合
你在做桌面原生应用的自动化,而且应用能被读出无障碍树 —— 企业内部的桌面客户端、Electron 应用(虽然 Electron 的 web wrapper 会吃掉大量深度)、原生 macOS/Windows 工具链。
你在给 Agent 做 computer use 后端,需要一套明确的送达语义和严格的元素身份。这套 disposition / recovery / details.kind 的设计,比自己拍脑袋写 try/catch 靠谱得多。
你需要在高频路径上调用,用 C ABI 而不是 fork 进程。
你想让 Agent 自己学会用这个工具,skills get 那套内置文档 + skills.sh 安装方式做得比大多数项目都好。
你重视可复现验证 ——probe 语料、归一化捕获、findings ledger、dogfood run 这套东西,对做 GUI 自动化的团队是很好的范本。
不适合
你要做的是浏览器自动化。 用 browser-use 或 agent-browser,别用这个。
应用是无障碍树残缺的。自绘控件的游戏、部分 Electron 应用、部分 Qt 应用,树很浅或者关键控件没有 role。这种情况下视觉路线(OmniParser)反而更靠谱。
你在 Wayland 上要完整功能。 指针输入拒绝、原生窗口控制不支持、剪贴板要额外依赖。
你需要” 按住不放” 这类长时状态动作。 key-down / key-up / mouse-down / mouse-up 现在全部拒绝执行。
你在企业环境里没法给二进制签名或授权。 Windows 无签名的二进制 + macOS 的屏幕录制权限,都是审批项。
你的场景里” 点两个相同按钮中的一个” 是常规操作。 这在 Windows/Linux 上目前做不到,只能用 --headed mouse-click --xy,而 Wayland 拒绝。
折中方案
两者结合:用 agent-desktop 处理语义清晰的部分(表单、菜单、设置),用截图 / 视觉处理无障碍树残缺的部分。agent-desktop 的 screenshot 和 Visual Debug Artifact 就是往这个方向留的接口。
十四、速查
安装
1 | npm install -g agent-desktop |
或者从 GitHub Releases 下原生包。校验来源:
1 | gh attestation verify <downloaded-archive> --repo vercel-labs/agent-desktop |
从源码构建(需要 Rust 1.89+):
1 | git clone https://github.com/vercel-labs/agent-desktop && cd agent-desktop |
Linux 额外:
1 | cargo build --release -p agent-desktop-linux --bins |
并把 agent-desktop-linux-clipboard-owner 放在 CLI 旁边。
首次使用
1 | agent-desktop status # 平台、权限、支持的能力 |
典型循环
1 | # 1. 观察(稠密应用先看骨架) |
错误处理
1 | # 只在 disposition.retry == "safe" 时重试 |
常用命令分类
1 | agent-desktop find --app Safari --role button # 已知 role/name 时 |
文档地图
| 想做的事 | 看哪里 |
|---|---|
| 修安装 / 授权 / 校验下载 / 从源码构建 | docs/install.md |
| 解析 JSON 信封、错误码、退出码 | docs/json-output.md |
| 作为 AI Agent 驱动它 | agent-desktop skills get desktop |
| 搞清某个 OS 的差异 | skills/agent-desktop-{macos,windows,linux}/ |
| 查术语(refs /snapshots/sessions) | CONCEPTS.md |
| 参与开发并过 CI 关卡 | CONTRIBUTING.md |
| 读工程规则 | AGENTS.md |
| 从过去的 bug 和教训里学习 | docs/solutions/ |
| 报漏洞 | SECURITY.md |
版本参考
- npm / Rust 包:1.0.2
- JSON 信封:2.6
- C ABI 主版本:7
- 许可证:Apache-2.0
- Stars:9,569 | Forks:647 | Open issues:122
- npm 近一月下载:14,843
十五、最后
我最想从这个项目里带走的一句话,不是任何一条 API 细节,而是这个:
A ref that was not unique at capture is refused before anything is read from the application, because no search can pick one of two identical elements.
大多数 GUI 自动化工具在这里选择” 尽力猜一个”。这个项目选择” 我不猜”。
在一个充满不确定性的领域里,明确说” 我不知道” 比给一个大概率错的答案有价值得多。尤其是当调用方是 Agent 的时候 —— 一个诚实的 AMBIGUOUS_TARGET 会让 Agent 换个思路,而一个错误的成功会让 Agent 自信地往错方向走,代价可能是双重的付款或者删掉不该删的文件。
配套的 disposition.delivery / disposition.retry 体系,把” 失败了” 细分成” 没送达” 和” 可能已经送达”,这是给 Agent 的一个非常实用的抽象:前者可以放心重试,后者必须先读状态再决定。
以及那个我觉得最漂亮的工程约定:
Finding nothing is a failure, never a skip.
在任何自动化检查里,” 什么都没找到” 都不该是绿灯。
Vercel Labs 把它做成 Apache-2.0 发布,源码在 vercel-labs/agent-desktop。如果你在做桌面自动化,或者只是好奇 Agent 到底怎么” 看见” 电脑的,这套设计和文档都很值得读一遍 —— 尤其是 CONCEPTS.md,353 行,全是这种级别的思考。
参考资料:仓库 https://github.com/vercel-labs/agent-desktop | npm https://www.npmjs.com/package/agent-desktop | skills.sh https://skills.sh/vercel-labs/agent-desktop/agent-desktop | 架构与协议见 CONCEPTS.md、docs/json-output.md、docs/faq.md、docs/install.md、CHANGELOG.md