agent-desktop:讓 agent 操作桌面 app 不靠截圖,ref 失效時它寧可報錯也不猜
先 snapshot 一次,拿回一包 JSON,每個能操作的元素後面掛著一個 ref。agent 挑一個,下 agent-desktop click @s8f3k2p9:e8,再 snapshot 一次,確認畫面的結構變成它預期的樣子。這是 agent-desktop 的 README 在 Quick start 裡示範的流程,原文講得很直接:沒有 pixel matching,迴圈留在你自己的 agent 裡。
這套流程裡,沒有任何一步需要知道按鈕在螢幕的第幾個座標。
我想拆這個工具,是因為它把一件事做反了。靠畫面操作的做法,動作的依據是某一刻畫面上推估出來的位置;畫面只要動一下,依據就過期,而過期這件事不會有人通知你。agent-desktop 的做法是把依據換成 ref,而且讓 ref 自己會過期、過期了會大聲說。後面這一半,才是我覺得這個專案真正有東西的地方。
先講我碰過什麼:什麼都沒碰過。這篇只讀了 GitHub API、README、AGENTS.md、CONCEPTS.md、docs/install.md 和 v1.1.0 的 release notes,沒有安裝,沒有跑過任何一條指令。它會控制整個桌面,我刻意不在這種狀態下碰它。文中所有指令與輸出範例,都是文件裡的原文。
同一個動作,換掉的是依據
寫過網頁自動化的人都有這個經驗:用座標點按鈕,改版就壞;用 selector 或 DOM 節點去找,才撐得久。agent-desktop 做的是把這個道理搬到桌面 app。作業系統本來就為螢幕閱讀器維護一棵 accessibility tree,每個視窗、按鈕、輸入框都是樹上的節點;README 的平台表明寫 Linux 走 AT-SPI,macOS 和 Windows 各用系統自己的那一套。它讀這棵樹,輸出 JSON,替每個可操作的節點配一個 ref。
ref 這個設計有個細節我讀了兩遍:它屬於產生它的那一次 snapshot。文件的原話是「Refs belong to their snapshot」。這跟電影票的座位號碼是同一個邏輯,14 排 8 號只對那一場有效,換一場就是另一個位置。所以光寫 @e8 會被擋下來,回 INVALID_ARGS,必須帶上 snapshot 的 id。
AGENTS.md 把定位講得很乾脆:agent-desktop 是「a tool that AI agents invoke, not an agent」。它只負責當眼睛和手,要點哪裡、點完怎麼辦,是呼叫它的 agent 的事。這個切法我認為是對的,因為決策邏輯一旦塞進工具裡,工具就得替每個 agent 的脾氣負責。只是有一個但書,README 提到選配的 JEV helper 會把畫面描述送到你設定的模型 provider,所以說它完全不沾模型並不準確,說「預設不含迴圈」才是文件支持的版本。
大型 app 的樹可能很深,文件的建議是先 snapshot --skeleton --app Slack -i --compact 拿骨架,再用 --root 往下鑽。這一段我沒什麼可評論的,只是記下它存在:樹太大是這條路線的固有成本,工具有為它提供解法。
1 | agent-desktop snapshot --app Finder -i |
這三行是 README 的 Quick start,指令我一個字都沒改,只拿掉原文的行尾註解。第三行跟第一行一模一樣,是整個設計的重點:每個動作之後,驗證靠的是再讀一次結構,而不是相信剛剛那條指令印出來的成功訊息。
讓我意外的一節:它被設計成寧可拒絕
我預期這類工具會想辦法讓每個動作都「成功」,畢竟成功率是賣點。CONCEPTS.md 的寫法剛好相反。
預設是 headless:ref 動作走 accessibility API,不移動滑鼠指標、不搶焦點,要實體輸入得自己加 --headed。接著是這句,我認為全篇文件裡最重要的一句:如果原生 accessibility API 做不到,動作會 fail closed,回結構化的 actionability 或 policy 錯誤,而不是「silently substituting physical input」,也就是不會悄悄改用實體輸入頂替。
換成白話:工具做不到的時候,它直接告訴你做不到,不會自作主張換一條路徑去試。一個會在背後默默改用另一條路徑的工具,你根本不知道它實際做了什麼;一個報錯的工具,至少你知道該停在哪裡。
錯誤的種類也不是一句「失敗了」帶過,文件把幾種情境分得很清楚:
| 情境 | 工具的回應 |
|---|---|
ref 沒帶 snapshot id(裸 @e8) |
拒絕,回 INVALID_ARGS |
| 畫面變了,ref 對不上 | 回 STALE_REF |
| 同一個 app 開了多個視窗,目標不唯一 | 回 AMBIGUOUS_TARGET |
| 原生 accessibility API 做不到 headless 動作 | fail closed,回結構化錯誤,不改用實體輸入 |
最耐人尋味的是 CONCEPTS.md 裡區分「送達」的那段:真正重要的差別在 delivered 與 not delivered,成功或失敗反而排在後面。一個根本沒送到目標的動作,可以放心重試;一個可能已經生效的動作,重做會有重複的風險。AGENTS.md 的範例回應長這樣:
1 | {"delivery":"not_delivered","retry":"safe"} |
我之所以把這一節看得比 60 個指令名稱還重,是因為自己寫 agent 迴圈的人,最怕的就是「點了兩次」。送出一封信、按下付款、新增一筆資料,這類動作最糟的錯誤是做了兩次。工具如果只回成功或失敗,重試邏輯就得自己猜;工具把「可不可以安全重試」直接寫進回應,這個判斷就從呼叫端的猜測變成工具的承諾。
順帶一提,README 說它有 60 個指令名稱、56 個是實際可運作的指令,另外四個(key-down、key-up、mouse-down、mouse-up)在 daemon 能夠釋放被按住的鍵或按鈕之前,一律拒絕執行。這四個拒絕的理由,跟前面講的是同一種脾氣:放不回去的東西,不給。
這種脾氣不只在使用者介面上,工程規則裡也看得到。AGENTS.md 規定手寫的 .rs 單檔至多 400 行、禁止行內註解、非測試程式碼不准用 unwrap、expect、panic,release 版 CLI 超過 15 MB 則 CI 直接失敗。每個 release 還附一份 libagent_desktop_ffi,讓 Python 這類語言用 C ABI 呼叫。其中「非測試程式碼禁 unwrap」這條,我的解讀是:對一支要替 agent 動滑鼠鍵盤的程式來說,崩潰本身就是一種不明確的失敗,禁掉它跟前面禁止悄悄改道,是同一個方向的決定。我讀到的文件只有規則,沒看到理由,這是我的推測。
文件白紙黑字寫著的限制
這一節的標題我特地寫成「文件記載」,因為我沒裝過它,這裡沒有一條是我踩到的。以下全是官方文件自己寫的邊界:
- AGENTS.md 說得很明白,它看不到沒有暴露 accessibility tree 的 UI。網頁交給姊妹專案 agent-browser;那是另一個 repo,這篇不談。
- macOS 要授予 Accessibility 權限(
agent-desktop permissions --guide會引導);Windows 對同 integrity level 的 app 不需授權;Linux 需要使用者的圖形 session 與at-spi2-core。 - Linux 以 Ubuntu 24.04 加 GNOME 46 為主,Ubuntu 22.04 因為 ATK bridge 不支援而不在範圍內;Linux 的滑鼠輸入只支援 X11 且需要
--headed。 - 安裝走
npm install -g agent-desktop。README 說官方的 Homebrew 與 Scoop 套件還在送審,預設 catalog 裡找不到,也不再維護自己的 tap 或 bucket。 - Windows 的 binary 沒有簽章,SmartScreen 可能跳警告,用 npm 安裝則不會觸發。
另外有一條不是限制,是提醒:snapshot、剪貼簿、截圖、輸出和 debug 檔都可能含有私人桌面內容,別直接丟進公開 log。
接著是我的推論,不是文件的話。你授權的對象是啟動它的那個終端機,而授權一旦給了,agent 理論上碰得到的是整個桌面,密碼管理器的視窗也在樹上。官方只提醒內容別外流,並沒有談這件事,所以這句是我自己推的;我的建議是把它放在專用帳號或 VM 裡跑,這同樣是我的建議,不是文件的。我也沒有讀原始碼,只讀了文件。CONCEPTS.md 裡有一處提到某類拒絕動作的比對規則,我沒深讀,所以「這個工具完全沒有限制 agent 能碰哪些 app 的機制」這句話我不敢寫。
一週三個版本
專案很新,而且動得很快。看版本的時間軸比看星數更能說明狀態:










