<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>无障碍树 on AI博士 万戈</title>
        <link>https://www.yesmiracle.net/tags/%E6%97%A0%E9%9A%9C%E7%A2%8D%E6%A0%91/</link>
        <description>AI博士万戈的技术博客，聚焦 Agentic AI、AI Infra 与 Agent Security，分享 AI 基础设施与工程落地实践。</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <managingEditor>admin@yesmiracle.net (万戈)</managingEditor>
        <webMaster>admin@yesmiracle.net (万戈)</webMaster>
        <lastBuildDate>Tue, 03 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.yesmiracle.net/tags/%E6%97%A0%E9%9A%9C%E7%A2%8D%E6%A0%91/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>agent-desktop：vercel-labs 给 Agent 做的「桌面操作」CLI，不靠截图，直接读无障碍树驱动原生应用</title>
        <link>https://www.yesmiracle.net/post/20261103-agent-desktop-accessibility-tree-cli/</link>
        <pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
        <author>admin@yesmiracle.net (万戈)</author>
        <guid>https://www.yesmiracle.net/post/20261103-agent-desktop-accessibility-tree-cli/</guid>
        <description>&lt;img src="https://www.yesmiracle.net/post/20261103-agent-desktop-accessibility-tree-cli/cover.svg" alt="Featured image of post agent-desktop：vercel-labs 给 Agent 做的「桌面操作」CLI，不靠截图，直接读无障碍树驱动原生应用" /&gt;&lt;p&gt;上个月我写过 Neko，那篇的落点是「只出像素」为什么是个关键设计：把浏览器变成一条视频流，agent 拿到的和你的眼睛拿到的是同一样东西。这篇讲的工具走的是反方向，一个像素都不出。&lt;/p&gt;
&lt;p&gt;agent-desktop 是 vercel-labs 开源的桌面操作 CLI，Rust 写的，Apache-2.0，2026 年 2 月建仓，现在 10,019 星、673 个 fork。它的主张可以压成一句话：读操作系统的无障碍树，把每个 agent 能操作的元素变成带稳定 ref 的结构化 JSON，agent 点一个 ref，再读一次界面来验证结果。整个观察（observe）和行动（act）的循环留在调用它的 agent 里，它自己不含模型、没有界面。&lt;/p&gt;
&lt;p&gt;我的判断放在前面：这条路的价值不在「省 token」这类容易被说烂的账上，而在于它把「看清界面」从概率问题换成了确定性问题。截图方案的失败模式是「看错了」和「点歪了」，而这两个失败模式在结构化 ref 上根本不存在。代价也很清楚，文章最后会说：拿不到无障碍树的界面，它什么都看不见。&lt;/p&gt;
&lt;h2 id=&#34;它明确说自己不是什么&#34;&gt;它明确说自己不是什么&lt;/h2&gt;
&lt;p&gt;这类工具容易在宣传里含糊边界，agent-desktop 反过来，在 &lt;code&gt;AGENTS.md&lt;/code&gt; 里把边界写成了排比句。原文：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;agent-desktop is a tool that AI agents invoke, not an agent. It is a Rust CLI and C library that reads native accessibility trees and returns structured JSON with ref-based element ids. The observe-act loop lives in the calling agent. It never embeds an LLM and has no GUI or interactive prompt. It does not automate web pages (use agent-browser). It cannot see UIs that expose no accessibility tree.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;四句话钉了四条界：不含模型、没有 GUI 和交互提示、不做网页自动化（把这块明确让给同 lab 的 agent-browser）、看不到没有无障碍树的界面。最后那条是这类方案天生的短板，它自己不藏。&lt;/p&gt;
&lt;h2 id=&#34;架构一个-trait-劈成四份&#34;&gt;架构：一个 trait 劈成四份&lt;/h2&gt;
&lt;p&gt;调用链是直的。CLI 用单一个 clap 枚举定义全部命令（&lt;code&gt;src/cli/mod.rs:34&lt;/code&gt;），&lt;code&gt;src/dispatch/mod.rs&lt;/code&gt; 按命令分支调用 &lt;code&gt;crates/core/src/commands/&lt;/code&gt; 里对应的模块，每个命令一个文件。core 只认一个抽象：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;trait&lt;/span&gt; PlatformAdapter: &lt;span style=&#34;color:#a6e22e&#34;&gt;ObservationOps&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; ActionOps &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; InputOps &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; SystemOps {}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这行在 &lt;code&gt;crates/core/src/adapter/mod.rs:39&lt;/code&gt;。trait 本身零个方法，靠四个子 trait 组合出来，加起来约 72 个方法，其中 &lt;code&gt;SystemOps&lt;/code&gt; 一个就占 35 个。三个平台各写一份实现，各自只在本系统上编译。&lt;/p&gt;
&lt;p&gt;代码量的分布值得看一眼，因为和文档给人的印象是反的。&lt;code&gt;src&lt;/code&gt; 的 CLI 层只有 12,797 行，core 84,803 行，FFI 15,197 行，而三个平台适配器分别是 &lt;strong&gt;Linux 123,842 行、Windows 79,045 行、macOS 43,026 行&lt;/strong&gt;。README 的平台表第一列是 macOS，快速上手用的是 Finder，文档站的平台页顺序也是 macOS 在前。代码里最大的那个是 Linux。&lt;/p&gt;
&lt;p&gt;为什么，答案在 &lt;code&gt;crates/linux&lt;/code&gt; 的子模块里：&lt;code&gt;system&lt;/code&gt; 56,324 行、&lt;code&gt;tree&lt;/code&gt; 29,042 行。Linux 这一侧要同时应付 AT-SPI 的 D-Bus 语义、三个 toolkit（GTK3、GTK4、Qt 6、Chromium）和两个显示服务器（X11、Wayland）。它在 &lt;code&gt;docs/solutions/runtime-errors/&lt;/code&gt; 下留了一篇复盘，标题本身就是结论：《一个 AT-SPI 错误名不是对象已经消失的证据》，它指出把 D-Bus 的 &lt;code&gt;UnknownObject&lt;/code&gt;、&lt;code&gt;UnknownMethod&lt;/code&gt;、&lt;code&gt;UnknownInterface&lt;/code&gt; 当成「对象已销毁」是双向都会错的，得针对四个 toolkit 逐个实测。这 15 篇故障复盘是仓库里最像工程笔记的部分，比 README 好看。&lt;/p&gt;
&lt;p&gt;架构的约束不靠自觉。&lt;code&gt;scripts/check-architecture.py&lt;/code&gt; 强制三件事：core 不得依赖任何平台或系统 crate，平台之间不得互相依赖，core 里不得出现允许清单之外的系统条件编译。&lt;/p&gt;
&lt;h2 id=&#34;ref-机制这套东西的核心&#34;&gt;ref 机制：这套东西的核心&lt;/h2&gt;
&lt;p&gt;一次 snapshot 返回的 JSON 里，每个可操作元素都带一个 &lt;code&gt;ref_id&lt;/code&gt;，形如 &lt;code&gt;@s8f3k2p9:e8&lt;/code&gt;。冒号前后是两回事。&lt;/p&gt;
&lt;p&gt;后半段 &lt;code&gt;e8&lt;/code&gt; 是这个 snapshot 内部按深度优先分配顺序自增的元素号，代码就一行 &lt;code&gt;format!(&amp;quot;@e{}&amp;quot;, self.counter)&lt;/code&gt;。前半段是 snapshot 自己的 id，生成方式是取时间戳纳秒、进程号和进程内计数器三者做哈希，再转 base36 并补上 &lt;code&gt;s&lt;/code&gt; 前缀。也就是说 ref 只在它所属的那次 snapshot 里有效，跨 snapshot 复用必然过期。&lt;/p&gt;
&lt;p&gt;这个设计的直接后果是裸 &lt;code&gt;@e8&lt;/code&gt; 会被拒绝。解析函数要求必须带 &lt;code&gt;@&amp;lt;snapshot_id&amp;gt;:&lt;/code&gt; 前缀，错误提示还专门写了建议：&lt;code&gt;A bare @eN ref is not accepted.&lt;/code&gt; 看起来是个小气的限制，实际上是这一整套能不能工作的前提：如果允许裸 ref，agent 拿着上一轮的 &lt;code&gt;e8&lt;/code&gt; 去点，你根本无法判断它指的是哪个元素。它宁可让你多写几个字符，也不接受一个有歧义的引用。&lt;/p&gt;
&lt;p&gt;那谁才配拿到 ref？规则比想象中讲究。表面看是一张 18 个角色的白名单（button、checkbox、combobox、textfield、menuitem 这些），列表在 &lt;code&gt;crates/core/src/tree/roles.rs:6-25&lt;/code&gt;。但实际判据是两条取或：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;is_ref_able_role_actions(role, actions)
  = INTERACTIVE_ROLES.contains(role) || advertises_primary_action(actions)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第二条的意思是：一个元素即使角色不在白名单里，只要它声明的动作里有不属于「通用装饰动作」的，也算可操作。而「通用装饰动作」被定义成一个只有三项的集合：&lt;code&gt;SetFocus&lt;/code&gt;、&lt;code&gt;RightClick&lt;/code&gt;、&lt;code&gt;ScrollTo&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里有一段注释解释了为什么必须把这三项剔掉，是我在这一版源码里读到的最实在的一段（&lt;code&gt;crates/core/src/refs/ref_alloc.rs:71-84&lt;/code&gt;）：Chromium 和 Electron 几乎给每一个节点都声明 &lt;code&gt;RightClick&lt;/code&gt; 和 &lt;code&gt;ScrollTo&lt;/code&gt;，如果把它们算进去，就会「给每一个惰性容器和文本片段都分配 ref，把 Web 应用上的 refmap 撑到超过大小上限」。反过来，&lt;code&gt;scrollarea&lt;/code&gt; 和 &lt;code&gt;disclosure&lt;/code&gt; 这类按角色不在白名单里、但实际可操作的节点必须能拿到 ref，哪怕它的边界框是零尺寸。&lt;/p&gt;
&lt;p&gt;一个判据被收紧到这种程度，说明设计者是拿着真实的界面在调的，不是照着一张角色表抄的。refmap 本身有 1MB 的单文件上限，超了会报错并建议改用 &lt;code&gt;--skeleton&lt;/code&gt;、&lt;code&gt;--max-depth&lt;/code&gt; 或 &lt;code&gt;--root @ref&lt;/code&gt; 缩小范围。&lt;/p&gt;
&lt;p&gt;用法是一个渐进循环，技能文档里给的原文是这四行：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --skeleton --app &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Notes&amp;#34;&lt;/span&gt; -i --compact   &lt;span style=&#34;color:#75715e&#34;&gt;# 浅层地图&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --root @s8f3k2p9:e3 -i --compact        &lt;span style=&#34;color:#75715e&#34;&gt;# 只展开一块&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop click @s8f3k2p9:e12                              &lt;span style=&#34;color:#75715e&#34;&gt;# 点一个 ref&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --root @s8f3k2p9:e3 -i --compact        &lt;span style=&#34;color:#75715e&#34;&gt;# 重读同一块验证&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;先要骨架，再钻进需要的那一块，点，然后重读同一区域确认。元素在两次读取之间消失了，你会拿到 &lt;code&gt;STALE_REF&lt;/code&gt;，附一条结构化的恢复建议，标准动作是重新 snapshot 再重试。&lt;/p&gt;
&lt;h2 id=&#34;安全模型默认不出手四个命令直接拒绝&#34;&gt;安全模型：默认不出手，四个命令直接拒绝&lt;/h2&gt;
&lt;p&gt;这一层我认为比 ref 机制更值得抄，因为它回答了一个所有桌面 agent 都会碰到的问题：工具怎么保证自己不做出物理上有副作用的事。&lt;/p&gt;
&lt;p&gt;答案是 &lt;code&gt;InteractionPolicy&lt;/code&gt; 这个只有两个布尔值的结构：允不允许抢焦点、允不允许移动光标。它有三个预设，&lt;code&gt;headless()&lt;/code&gt; 是两个都假，&lt;code&gt;headed()&lt;/code&gt; 是两个都真，而 &lt;code&gt;Default&lt;/code&gt; 就是 &lt;code&gt;headless()&lt;/code&gt;。README 的原文是：ref 动作走无障碍 API，不移动指针、不抢焦点，全局的 &lt;code&gt;--headed&lt;/code&gt; 才允许物理输入。&lt;/p&gt;
&lt;p&gt;这个默认值不是写在文档里就算数的，它有一条测试专门逐命令断言。&lt;code&gt;crates/core/src/commands/ref_policy_tests.rs&lt;/code&gt; 里的 &lt;code&gt;default_ref_commands_use_least_permissive_supported_policy&lt;/code&gt;，会把 click、toggle、check、expand、set-value、select、type、scroll 等十来个命令在默认上下文下全部执行一遍，然后逐个请求断言它的策略等于最保守的那一档。文档会撒谎，这个不会。&lt;/p&gt;
&lt;p&gt;四个命令干脆无条件拒绝执行：&lt;code&gt;key-down&lt;/code&gt;、&lt;code&gt;key-up&lt;/code&gt;、&lt;code&gt;mouse-down&lt;/code&gt;、&lt;code&gt;mouse-up&lt;/code&gt;。理由写在 &lt;code&gt;input_hold_policy.rs&lt;/code&gt; 里，它返回的错误信息是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;hellip; is unavailable in stateless mode. Use the atomic &amp;hellip; command; held input requires a daemon-owned transaction that can guarantee release.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;翻译过来：CLI 每次调用是一个独立进程，一次调用结束，它没有任何机制保证那个被按下的键会被松开。所以它的选择是拒绝执行，而不是冒留下一个卡住的按键的风险。README 里这条被明确计数：60 个命令名，56 个可用，剩下四个等有了能保证释放的守护进程再开放。&lt;/p&gt;
&lt;p&gt;还有一处容易被忽略：为了让「不抢焦点」这条在失败路径上也成立，指针手势有一个「尚未送达」的保证。&lt;code&gt;pointer_action.rs&lt;/code&gt; 里那个 &lt;code&gt;before_first_event&lt;/code&gt; 函数的注释写得很准：它执行的是一个手势在第一个指针事件之前该做的事，到那时为止什么都还没到达目标，所以拒绝或超时都留下一个未送达的手势。失败时它会打上 &lt;code&gt;not_delivered&lt;/code&gt; 的标记，而不是含糊地报「操作失败」。&lt;/p&gt;
&lt;p&gt;数据出站这侧，它有 trace 脱敏：命中敏感键名（text、value、username、label、query、secret、token、password、title、url 等）的字段值会被替换成一个 &lt;code&gt;{&amp;quot;redacted&amp;quot;: true}&lt;/code&gt;。但它的 FAQ 里有一句我认为所有做 computer use 的人都该记住的话：脱敏不能让一张截图变得可以公开。屏幕上的内容就是用户的内容，这个性质不会因为你处理了 JSON 字段而改变。另外它明确写了，原生 CLI 不需要任何远程模型服务，仓库里只有那一个可选的 helper 会把屏幕描述发给配置好的模型 provider。&lt;/p&gt;
&lt;h2 id=&#34;工程质量400-行上限和那个会自检的脚本&#34;&gt;工程质量：400 行上限，和那个会自检的脚本&lt;/h2&gt;
&lt;p&gt;这段是我在写这篇时唯一一个先猜错、然后被打脸的地方，值得记下来。&lt;/p&gt;
&lt;p&gt;我先按文件行数排了个序，看到一批文件整齐地停在 400 行，包括平台适配器里那些明显还能拆的模块，于是判断「这里有个 400 行的软约定」。然后我用一条临时的 awk 管线数「超过 400 行的文件有几个」，得到 2。这个数字我先记下了。&lt;/p&gt;
&lt;p&gt;后来实测重数，结论是：2,741 个 Rust 文件里，&lt;strong&gt;超过 400 行的有 0 个&lt;/strong&gt;，恰好停在 400 行的有 12 个。我那个 2 是错的，原因是我的管线把 &lt;code&gt;wc -l&lt;/code&gt; 输出的 &lt;code&gt;total&lt;/code&gt; 行和几处统计口径混在了一起。规则本身则比我猜的硬得多，它明写在 &lt;code&gt;AGENTS.md:158&lt;/code&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;400 lines at most per hand-written &lt;code&gt;.rs&lt;/code&gt; file&lt;/strong&gt; (&lt;code&gt;scripts/check-rust-file-size.sh&lt;/code&gt;). Split by responsibility.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;带 &lt;code&gt;@generated&lt;/code&gt; 标记的生成文件豁免，探针脚本豁免，C 头文件由 cbindgen 生成。而且有脚本在 CI 里真的跑，跑出来是 exit 0。&lt;/p&gt;
&lt;p&gt;真正值得抄的是那个脚本本身，它有三处讲究。&lt;/p&gt;
&lt;p&gt;第一，它用 awk 数记录，不用 &lt;code&gt;wc -l&lt;/code&gt;。脚本里的注释解释了原因：&lt;code&gt;wc -l&lt;/code&gt; 数的是换行符个数，一个 401 行、末尾没有换行的文件会被少算一行，从而从行数上限里溜过去。awk 数的是记录，没换行的最后一行也是一条记录。&lt;/p&gt;
&lt;p&gt;第二，脚本在扫描之前先做一轮自检，往规则里灌入违规样本，验证它还能抓到东西。理由写得更狠：一个已经检测不到任何东西的规则，必须让这道闸门失败，而不是静默通过。&lt;/p&gt;
&lt;p&gt;第三，这个「不能静默通过」的执念有出处。脚本注释里记着一件真实事故：那条禁止行内注释的扫描器，它自己的测试套件从来没被执行过，因为 CI 里 unittest 的发现范围被限定在 &lt;code&gt;tests/e2e&lt;/code&gt; 和一个 &lt;code&gt;test_*.py&lt;/code&gt; 的模式上，那个测试文件名不匹配。结果是 7 个文件里 27 处注释违规一路进了 CI，而扫描器一直显示自己是绿的。&lt;/p&gt;
&lt;p&gt;顺着这个思路看它的其它约定就顺了：非测试代码禁止 &lt;code&gt;unwrap()&lt;/code&gt; / &lt;code&gt;expect()&lt;/code&gt; / &lt;code&gt;panic!&lt;/code&gt;（在 &lt;code&gt;main.rs&lt;/code&gt; 里用 clippy 的 &lt;code&gt;deny&lt;/code&gt; 钉住）；每个结构体最多 7 个字段，每个函数最多 5 个参数；每个文件一个领域结构；每个命令一个同名文件；禁止行内注释，理由一句话，「名字承载含义」，只允许 public 项上的 &lt;code&gt;///&lt;/code&gt;。最后这条会逼着人把命名写到能自解释的程度，而不是靠注释补。&lt;/p&gt;
&lt;p&gt;测试的体量也对得上这种纪律：&lt;code&gt;#[test]&lt;/code&gt; 出现 6,850 次，&lt;code&gt;*_tests.rs&lt;/code&gt; 文件 1,029 个，测试代码 164,643 行，占全部 Rust 代码的 44.7%，测试与非测试的行数比接近 0.81 比 1。而且命令模块禁止写 inline 测试，必须用 &lt;code&gt;#[path = &amp;quot;foo_tests.rs&amp;quot;] mod tests;&lt;/code&gt; 挂兄弟文件，这条也有测试在断言。&lt;/p&gt;
&lt;h2 id=&#34;装上试一下&#34;&gt;装上试一下&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install -g agent-desktop
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop version
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop status
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop skills get desktop
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;npm 包的 &lt;code&gt;postinstall&lt;/code&gt; 会把 Node 启动器替换成原生二进制的链接，之后每次调用约 21 毫秒；如果你的包管理器屏蔽了 install 脚本（npm 12 起默认屏蔽），启动器兜底也能用，代价是每次约 146 毫秒。想从源码构建需要 Rust 1.89+。&lt;/p&gt;
&lt;p&gt;平台要求是三行：macOS 13+（Intel 或 Apple Silicon），Windows 10 1809+ 或 Server 2019+（x64 或 ARM64），Linux 需要 Ubuntu 24.04+、glibc 2.35 以上、一个跑着 AT-SPI 总线的图形会话。macOS 上需要授予辅助功能权限。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;agent-desktop skills get desktop&lt;/code&gt; 这条值得单独说：它打印的是一份写给 agent 看的技能文档，让 agent 自己学会怎么用这套命令，而不是让你去给 agent 解释。同 lab 还有一条 &lt;code&gt;npx skills add&lt;/code&gt; 的装法。&lt;/p&gt;
&lt;p&gt;第一次跑就三行，拿 Finder 开刀：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --app Finder -i     &lt;span style=&#34;color:#75715e&#34;&gt;# 每个可操作元素带 ref&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop click @s8f3k2p9:e8           &lt;span style=&#34;color:#75715e&#34;&gt;# 用上一轮的 ref 点一下&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;agent-desktop snapshot --app Finder -i     &lt;span style=&#34;color:#75715e&#34;&gt;# 再读一次验证&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;有两个坑先讲清楚。第一，&lt;code&gt;--help&lt;/code&gt; 会列出一些当前平台根本不支持的表面，比如 Linux 上 start-menu 和 system-tray-overflow 都列在那儿但会拒绝执行，taskbar 永远拒绝。第二，它说的 headless 专指「不移动指针、不抢焦点」，不代表不需要显示环境：一个没有那个图形会话的 TTY 或 SSH 环境，看不到它里面的应用。这两个词在别的工具里含义常常相反，读文档时要按它的定义来。&lt;/p&gt;
&lt;h2 id=&#34;它做不到什么以及不该夸大什么&#34;&gt;它做不到什么，以及不该夸大什么&lt;/h2&gt;
&lt;p&gt;拿不到无障碍树的界面它看不见。网页自动化明确不归它管。Wayland 下应用指针输入会拒绝，原生窗口控制不支持。GTK4 应用会拒绝聚焦调用，得改用 focus-window。&lt;/p&gt;
&lt;p&gt;还有一条值得引用的话，写在它的 JSON 输出文档里：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ok: true means the command returned a delivered or already-satisfied result, not that the application effect was verified.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;ok: true&lt;/code&gt; 只说明这次调用送达了或者本来就已满足，不说明应用层面的效果被验证过。同页还有一句，说那个结果和验证过的元素状态都不能证明应用级别的持久化。一个工具肯在自己的文档里写清成功标志的边界，比多写十页宣传有用。&lt;/p&gt;
&lt;p&gt;锁屏状态下的行为我在源码和文档里都没找到明确声明，这里就不猜了。&lt;/p&gt;
&lt;h2 id=&#34;我的判断&#34;&gt;我的判断&lt;/h2&gt;
&lt;p&gt;把它和上个月那篇 Neko 放在一起看，桌面 agent 这条路现在有两条岔路：一条是把界面变成像素流，让模型的视觉能力去理解它；另一条是把界面变成结构化数据，让模型只做决策。Neko 那条路赢在通用性，什么界面都能看；agent-desktop 这条路赢在可靠性，能看到的界面它不会看错。&lt;/p&gt;
&lt;p&gt;两条路的胜负手不在模型能力，在于你要操作的界面有没有把无障碍树暴露出来。有，第二条路几乎全面占优，因为它连点击的坐标都不需要猜；没有，你只能回到第一条路。&lt;/p&gt;
&lt;p&gt;而我在这份源码里看到的最有意思的东西其实不在 ref 机制上，是那份把自己的失效当成 bug 来对待的工程纪律。一个 400 行的文件上限、一条禁止行内注释的规则，配上一个会先给自己灌违规样本、验证自己还有效的检查脚本，再配上一段解释「为什么这件事必须这么较真」的事故复盘。这套东西和 agent-desktop 要解决的问题没有半点关系，但它是这份代码库能被信任的原因。要抄的话，我建议先抄这一段。&lt;/p&gt;
&lt;p&gt;想先看另一条路是怎么走的，见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261024-neko-virtual-browser-teardown/&#34; &gt;《Neko 源码级拆解：把浏览器变成一条视频流，以及「只出像素」为什么是关键》&lt;/a&gt;；想让这类工具真的敢放手操作，前面还有一层审批与权限要解决，见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261008-agent-approval-permission-layer/&#34; &gt;《「在聊天框里点确认」撑不住了：Agent 的审批和权限，正在长成独立的一层》&lt;/a&gt;；想先看清浏览器 agent 这个赛道的格局，见&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20260910-browser-agent-trio-comparison/&#34; &gt;《browser-use 114k⭐ 称王！浏览器 Agent 三强争霸》&lt;/a&gt;。&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
