Paul Jialiang Wu agentic-portfolio English Español 한국어 日本語✉️ 免费订阅名单
← 返回作品集

AI-NATIVE 系列 · AGENT 工程

桌面应用控制:四道门,应用决定你能进哪一扇

Paul Jialiang Wu · 2026 年 9 月 23 日 · 阅读约需 13 分钟

Desktop app control: four doors into a desktop app — scriptable, accessible, vision-only, unreachable — with the measured rung for Finder, Feishu, WeChat and Slack.
macOS 应用可能暴露的四种控制方式,以及每种方式对应的开发成本。

内容简介: 这是在你编写任何桌面自动化前,帮助你回答一个关键问题的方法:这个应用的哪道门是真正开放的? 四种入口存在,应用决定你能用哪一种,而这个选择直接决定了你的自动化能否经得起下次升级。

为什么要关心这个问题: 很多团队都是基于自己假设的入口开发 agent,几个月后才发现实际情况,不得不推倒重来。检测只需几秒,而大多数人以为可脚本化的四个应用,实测其实都不行。

你可以怎么做: 你可以检测任意一个 Mac 应用,读出它所处的层级,在动手前就知道是要写十行代码还是一万行——以及你开发的工具能否在你使用电脑时后台运行。

桌面应用控制涵盖哪些内容,适合哪些人

如果你正准备让 agent 操作桌面应用——无论是聊天客户端、设计工具,还是永远不会有 API 的内部工具——这篇文章就是为你而写。内容包括:如何判断一个应用实际暴露了哪些控制方式、每种方式的成本,以及我用探针实测得出的两个关键发现。不涉及云端 API:如果你的目标应用有正式 API,请直接使用,本文讨论的全部情景都假设没有 API。

全篇只靠一张图就能说明问题,所以我更愿意把它当作工具来介绍,而不是单纯的点缀。

一图看懂:餐厅与供应商

餐厅后厨要和各类供应商打交道,但并不是每家都一样省心。

最理想的供应商会把货直接送到卸货区,并附上一份打印好的清单。你无需离开厨房,下单只需报出名字——“来四十公斤周二用的面粉”——而且明年这个名字依然有效。

次优的供应商可以电话下单。你描述所需的名字和位置:比如“第三层货架上那个,标签写着arborio”。速度慢些,货架偶尔会调整,但你用的还是名称,不是坐标。

第三种完全没有电话。你得亲自开车去市场,凭眼力找到摊位,然后现场指认。能买到,但必须本人到场,还得赶在白天,如果摊位换了位置,你还得重新找参照物。

第四类供应商则压根不开门。

这四种情形,恰好一一对应 macOS 应用实际暴露或拒绝的控制方式,映射关系非常明确:

供应商应用暴露的方式你如何定位目标升级后是否可用
卸货区 + 清单R3 AppleScript 字典(sdef)按应用自身的命名体系调用通常如此——因为这是公开发布的契约
电话下单R2 可访问性树(AX)按角色和标题定位大多数情况下——直到 UI 被重构
亲自去市场R1 像素 + 合成点击按坐标和外观定位极少——一旦界面重设计就得重写
未开放R0 无窗口界面——
Desktop app control ladder: R3 scriptable (Finder, Safari, Terminal), R2 accessible (Feishu), R1 vision plus menu (WeChat), R0 unreachable (Slack, Telegram), with what each rung costs.
同一把梯子,每一级的实际成本——以及一台 Mac 上每个应用实际处于哪一级。

梯子的价值在于,各级不可互换,而且你无法自行选择。应用在开发时就已决定了你能用哪一层,这一切都发生在你出现之前。你唯一真正的选择,是现在就查明,还是等自动化写完再发现问题。

所以我选择了实际测量,而不是凭空假设

我写了一个小型探针——每个应用独立做四项检测——直接在自己的机器上运行。这四项检测彼此独立,因为一个应用可能通过其中一项、拒绝另一项,只有组合起来才是全貌。

$ appctl survey WeChat Feishu Slack Telegram Finder Safari Terminal

  ⬜ Slack       R0_UNREACHABLE
  ⬜ Telegram    R0_UNREACHABLE
  🟨 WeChat      R1_VISION          +MENU(63)
  🟦 Feishu      R2_ACCESSIBILITY   +MENU(61)
  🟩 Finder      R3_SCRIPTABLE      +MENU(152)
  🟩 Safari      R3_SCRIPTABLE
  🟩 Terminal    R3_SCRIPTABLE      +MENU(132)

第一个结果就值得认真对待。

这四个聊天应用无一内置 AppleScript 字典。 WeChat、Lark/Feishu、Slack、Telegram ——sdef 均未通过检测。Finder、Safari 和 Terminal 均通过。你日常工作的那些应用都在梯子的下层;Apple 自带应用才在顶层。

这个事实直接推翻了最常见的方案。大家常用的参考实现——包括 Safari harness in CLI-Anything, 确实很优秀——以 AppleScript 为起点,是因为 Safari 提供了字典。把这个模式直接套用到聊天应用上,完全行不通。不是“效果差一点”,而是根本无法迁移。

意外的发现:63 个命令,0 个窗口

WeChat 是个典型案例,因为它对两个问题给出了互相矛盾的答案。

如果你用辅助功能 API 查询 WeChat 有多少个窗口,它会返回0。但如果问窗口服务器——也就是负责像素合成的底层——它会报告9个,最大尺寸为 1418×879。窗口确实存在,只是 WeChat 并未向辅助功能 API 暴露这些窗口。

如果只看到这里,你可能会直接放弃 WeChat——我当时也差点如此。但菜单栏其实是一个独立的界面,而 WeChat 的菜单栏完全暴露:7 个菜单共 63 个命令,包括Show Next Unread Chat, Chats, Contacts和Favorites.

不可点击,并不代表无法下达命令。一个应用可以拒绝暴露全部窗口树,但依然可以提供完整的命令接口,完全无需依赖坐标。如果把“AX 树为空”直接等同于“不可控”,就等于把唯一还敞开的那扇门也关上了。

在餐厅的比喻里:这个摊位没有电话,也不让你进仓库,但门口钉着一份菜单,你可以按编号点菜。这当然不如直接进卸货区高效,但比什么都没有要好得多,而且很稳定——因为菜单命令是按名称而不是按位置。

第二个发现:隐藏窗口拍下来只有一片空白

WeChat 处于R1,所以读取内容只能靠抓取像素。它的主窗口在托盘关闭时依然可以被枚举出来(1418×879),这让我一度以为可以:通过 id 抓取窗口、读取内容,用户完全不会被打扰。

截图操作确实完成了,得到的是94 KB 的纯白图片.

离屏窗口可以被枚举,但无法被截图;没有任何渲染像素可获取。这不是一个可以规避的 bug,而是一个会影响到调度层面的约束:

基于视觉的控制方式无法在你不操作时于后台运行。应用必须显示在屏幕上,这意味着 agent 需要和你争用显示器。想让自动化在你下午三点工作时静默读取 WeChat,这种方案根本无法实现。

所以这个探针不仅仅报告层级,还会明确指出 R1无法实现无人值守,并说明原因。这部分,我认为才是真正有价值的贡献。

层级是上限,不是主观宣称

面对这样的分类器,人们很容易想直接输出一句能力断言:“WeChat: controllable.”(中译:WeChat:可控。)但这句话的价值,完全取决于它最薄弱的支撑点。我之前写过capping a guarantee at its weakest assumption(中译:把承诺的上限限定在最弱假设上),而这种自律正好能防止这里的问题:有人把保证宣称在自己期望的层级,而不是证据实际支撑的层级。

所以这个探针也沿用了这种自律。它从不直接宣称某个层级——而是基于四项测量结果推导出层级,并将所有测量带来的上限全部应用:

🟨 WeChat  R1_VISION
   +MENU — 63 commands, no coordinates needed
   unattended: NO — needs the screen
   ⚠ capped: every window is offscreen: capture returns a blank image, so the
     app must be brought on screen before it can be read
   ⚠ capped: vision control competes with the human for the display — it cannot
     run unattended while the machine is in use

如果一个应用只是未运行,探针就会报告R0,并加上“结果不确定”的上限——因为“未运行”和“不可控”是两回事,混为一谈的话,上述证据就会把 Slack 和 Telegram 都直接排除。所有上限都通过测试来约束,而不是写在 README 里的说明,因为只有这样的规则才能真正经得起 deadline 的考验。

实际运行时遇到的问题——甚至包括 Mac 上最简单的那个应用

我最初是手动记录了探针的基准数据,基于这些数据写了分类器,19 个测试全部通过,自我感觉良好。然后我把线上后端指向同样的应用。

结果出了两个问题,其中一个还挺让人尴尬。

Finder 被判为R1_VISION.Finder——这台机器上最具脚本化能力的应用,自 1993 年以来就是 AppleScript 的标准目标。我的 bundle 查找逻辑扫描的是/Applications,而 Finder 实际位于 /System/Library/CoreServices。由于没有找到,无法读取字典,结果是本应处于顶层的应用被判定为最底层。一个连最简单案例都能误判的分类器,比没有分类器更糟糕——因为你会信任它。

Feishu 则完全无法解析出 bundle。它的进程名是Feishu;应用 bundle 路径为Lark.app。任何基于名称推测路径的方法都无法找到。

两者的根本原因和解决方法其实相同:直接向正在运行的进程询问其实际路径,而不是根据名字去猜测。进程本身最清楚自己的位置。我却去推断系统本可以直接告诉我的信息。

而且这两个问题之所以能被发现,只因为 fixtures 是真实记录下来的,而不是凭空编造——如果我随意造一组假的数据,就会一直印证我的错误假设。顺带一提,我手动记录的数字其实也有细节误差:比如 WeChat 实际有 63 个菜单命令,我却写成了 19 个;窗口数实际过滤后是 9 个,我记成了 23 个。现在 fixtures 里都是真实输出,直接复制过来的。--json.

自己实现,全流程大约二十分钟即可完成

整个方案就是四个探针和一个纯函数。除标准库和操作系统自带组件外,无需任何额外依赖。

  1. 脚本接口。运行sdef /Applications/Foo.app。检测到字典即为第 3 层。Info.plist 里的NSAppleScriptEnabled标志无需作为判断依据——应用设置极不统一,建议仅记录,不要据此下结论。
  2. 辅助功能接口。让 System Events 查询count of windows。需要辅助功能权限;若无权限会直接报错,必须原样报告该错误,绝不能将其等同于“零窗口”——两者含义完全不同。
  3. 菜单栏接口。枚举所有菜单项,并统计其子项数量,一轮请求即可完成。每个菜单调用一次,在 Electron 应用上约三秒,整个探针流程共计二十一秒。
  4. 窗口服务器。 这是唯一能获取 AX API 拒绝暴露的窗口信息的方式。在 macOS 15+ 上,这需要一个小型 Swift 辅助程序,因为 CGWindowListCreateImage 已被废弃,标准库也无法直接获取窗口列表。

接下来进行分级:以能响应的最高控制面为准,菜单单独记录,所有未满足的前置条件都作为上限处理,而不是写成脚注。保持分类器为纯函数——无 I/O——这样核心逻辑就能在没有 Mac 的环境下直接测试。

这项测量无法告诉你的内容

如果分级过度承诺,必然会被利用,所以下面这些,是这个分级法确实无法覆盖的盲区。

它测量的是可达性,而非可用性。 Feishu 的分级处于 R2,有一棵活跃的树,顶层只有一个 AXGroup 和三个按钮——Electron 应用只有在辅助客户端请求时才会暴露真实的树。可达只是工作的起点,而非终点。

它并不涉及你是否有权限操作。 一个应用的服务条款、你所在公司的政策、以及对话中其他人的立场,这些都不是探针能衡量的。你在自己电脑上操作自己的客户端,这属于正常的辅助功能使用——这正是所有屏幕阅读器的常规做法。而如果你要大规模自动化账号,那是完全不同的问题,答案也不一样。探针只会如实告诉你门是否敞开,但你是否该进入,它无法判断。

它只是一次快照。 分级会变化。应用可能在某个小版本增加辅助支持,也可能随时撤回。测量本身之所以便宜,就是为了让你可以随时复测,而不是引用过时的数据。

还有一个必须正视的上限:分级只能告诉你成本,而不是价值。 你知道 WeChat 只能靠视觉自动化、有 63 个菜单命令,这并不能说明自动化它是否值得。分级只告诉你,如果决定做,需要付出多少成本——而这往往是大家最后才意识到的数字。

通用的部分

具体的结论只适用于 macOS 上的聊天应用,未来也可能失效,但背后的结构性思路不会变。

每当你要自动化一个你无法掌控的软件时,它可能暴露出一套分级接口,你无法选择自己处在哪一层,最经济的做法就是先 测清楚自己处于哪一层,再决定是否投入开发。这种失败模式具有普遍性,而且总是如此:有人以为有装卸码头,写了几个月代码,集成时才发现供应商根本没有电话。

如果一开始先花二十分钟探测,就能提前知道答案。


相关

作者 Paul Jialiang Wu · agentic-portfolio · 2026 年 9 月 23 日。本文所有数据均由 appctl 在一台运行 macOS 26 的 Mac 上实测,并可离线复现于已记录的 fixtures,使用 appctl --fake probe <app>。这些 app 属于我;这些门属于所有人。