
字节笔记本
2026年10月5日 · 约 16 分钟读完
Cua 让 AI 后台操作 Mac:鼠标不动、焦点不切
GUI 走过了四十多年,一直是给人设计的接口,现在它正在变成 Agent 也能直接操作的界面。开源项目 Cua(GitHub 仓库 trycua/cua,MIT 协议)把这件事推进了关键一步:它的核心组件 cua-driver 能让 ZCode、Claude Code、Codex 这类编程 Agent 在后台驱动 macOS 上的任意应用,截图、读无障碍树、点击、打字样样都行,而用户的光标不动、焦点不切、桌面 Space 不跳。有开发者把 Cua 接进 ZCode 做了演示:Agent 先在计算器里算出 7×6=42,再打开 Chrome 依次导航到微博和 X,全程演示者的鼠标纹丝不动。这篇文章拆解它是怎么做到的、为什么重要,以及怎么用起来。

一、让 Agent 操作 GUI,难在哪
让 AI 操作电脑,最直觉的思路是模拟鼠标键盘。macOS 提供了 CGEventPost,能把合成的鼠标点击丢进 HID 事件流,也就是物理鼠标走的同一条管道,WindowServer 看到点击就会移动光标。
问题也随之而来:它会抢用户的鼠标。Agent 一操作,光标就被拽走,焦点切到 Agent 操作的窗口;如果用户正在另一个 Space(桌面),macOS 还会把人拽过去。正在写代码的人突然被夺走鼠标,这种体验完全没法用。
这正是过去几年绝大多数 GUI Agent 产品止步不前的原因。Cua 团队在博客里直言,正是这个限制让他们此前一直推荐用隔离的 VM 和 GUI 容器作为 Agent 的操作空间,从不敢让用户把 computer-server 组件直接装在日常使用的桌面上。
换句话说,以前的方案是"给 Agent 一个隔离的虚拟桌面,让它在里面操作",但 Agent 碰不到用户真实的工作环境,价值大打折扣。真正的目标是:Agent 在真实的 Mac 上操作,而人完全不受打扰,就像多了一个"隐形第二光标"。
二、Cua 的解法:后台 Computer Use
Cua 的核心产物叫 cua-driver,一个开源的 macOS 驱动,让任何 Agent 都能在后台驱动 Mac 应用。官方博客的 TL;DR 只有一句话:用户光标不动、焦点不变,macOS 也不会把人拽到别的 Space。
为了做到这一点,Cua 作者逆向了 macOS 窗口管理的底层,挖出一批未公开的 Apple 私有 API。整个过程可以概括为闯过四道关卡,下面逐层拆解。

三、技术拆解:四道关卡
关卡 1:点击不抢光标,SkyLight 登场
第一个失败尝试是 CGEventPost:事件进 HID 流,光标会跟着动,直接排除。
第二个尝试是 CGEvent.postToPid:把事件发给特定进程而不是全局流,光标不跳,对大多数应用有效,但对 Chrome 无效。Chrome 会在渲染器 IPC 边界过滤合成事件,如果点击没有真实手势附带的特定遥测数据(mouseEventSubtype 字节、clickState 计数器、私有 SPI 设置的窗口局部坐标戳),渲染器会把它当成"不可信"事件默默丢弃。
第三个尝试是先激活目标应用、发 HID 事件、再取消激活:能用,但这正是所有现有工具的做法,会 raise 窗口、拉动 Spaces、抢走焦点,恰恰是要避免的。
真正的解法是 SkyLight.framework 的 SLEventPostToPid。SkyLight 是 Apple 未公开的 C 层框架,WindowServer 用它驱动屏幕上的每个窗口。SLEventPostToPid 的签名和 CGEvent.postToPid 类似,但事件走完全不同的代码路径:一条经过授权签名的 SkyLight 通道,绕开了 IOHIDPostEvent。作者推测,SLEventPostToPid 会在事件记录里盖一个"源自 WindowServer 信任信封"的标记,Chrome 的过滤器读到这个标记就放行。这样点击直达目标进程,光标纹丝不动。
这个函数是怎么找到的?作者 grep 了 SkyLight 的符号导出表,它不出现在任何 Apple 官方头文件里。封装只用了 20 行 Swift,通过 dlopen 加 dlsym 加载 /System/Library/PrivateFrameworks/SkyLight.framework。
关卡 2:激活窗口但不置顶,抄 yabai 的作业
光能发事件还不够,还得让目标窗口进入 AppKit-active 状态(能接收事件路由),同时不能 raise(不能置顶、不能拉动 Space)。
答案来自 yabai,一个 macOS 平铺窗口管理器。它有个 window_manager_focus_window_without_raise 函数,约 40 行 C,干的正是这件事。
原理是 AppKit 激活分两步:一步告诉应用"你现在是输入路由的焦点应用"(通过 SLPSPostEventRecordTo 翻转内部状态),另一步告诉 WindowServer"把窗口置顶并 reparent 到当前 Space"(SLPSSetFrontProcessWithOptions)。你可以只做第一步、不做第二步。yabai 这么干了多年,Cua 沿用了这个模式:两次 SLPSPostEventRecordTo 调用之后,目标应用已经 AppKit-active,而窗口在 z 轴堆叠里的位置纹丝不动。
关卡 3:骗过 Chrome 的可信手势闸门
即便用 SkyLight 把点击送进去了,Chromium 还有一道闸门:user-activation gate。如果渲染器最近没见过"可信用户手势",闸门就拒绝让点击触发视频播放、window.open、全屏 API 这类高权限行为。
解法是诱饵点击:在 (-1, -1) 这个屏幕上所有窗口之外的坐标,发一对 LeftMouseDown 和 LeftMouseUp。Chromium 发现没有窗口认领那个坐标,会丢弃这次点击,但 user-activation gate 仍然向前走一格。几毫秒后真正的点击到来,被当成那个手势的可信延续。
这是整个项目里最难找的一块。作者把 Chromium content/browser/renderer_host 的源码读了个底朝天,才发现这个屏幕外的"引信"能用。
关卡 4:键盘,意外地简单
键盘的故事很无聊:CGEvent.postToPid 就够了,不需要 SkyLight。按键发给特定 pid,落进那个应用的事件队列,不会到别处。原因是 macOS 没有 Chromium 那种全局按键过滤器,应用接到什么就处理什么。

四、Electron 应用需要特殊处理
大多数现代应用(Chrome、Slack、VS Code、Discord、Notion)都是 Electron。它们有个怪癖:窗口被遮挡时,无障碍树(accessibility tree)会停止更新,因为 Blink 的无障碍代码短路了,以为没人在看。
公开的 AXObserverAddNotification 不会把观察者标记为 remote-aware,Blink 不知道有人在听。而另一个私有变体 _AXObserverAddNotificationAndCheckRemote 可以。一个 dlsym 调用,AX 树就能在整个"启动、快照、行动、验证"循环里保持活跃,哪怕目标窗口被遮挡、在别的应用后面、在别的 Space。
作者是怎么发现的?对比 Accessibility Inspector 遮挡 Electron 窗口时的行为,发现 Inspector 的调用路径多碰了一个符号。
五、Agent 怎么"看"屏幕:三种捕获模式
cua-driver 提供三种模式让 Agent 理解屏幕:
| 模式 | 返回什么 | 适合 |
|---|---|---|
ax | 简化的无障碍树(Markdown 大纲,每个可操作节点带索引) | 系统 App 与 AppKit/SwiftUI 应用,不需要屏幕录制权限 |
vision | 目标窗口的 PNG | 视觉优先的 VLM,payload 最小最快,但要自己做空间推理 |
som(默认) | AX 树加截图(set-of-mark) | 树告诉 Agent 什么可点,截图消除歧义 |
默认 som 的原因是:元素索引点击(click({pid, window_id, element_index}))是主寻址模式,直接触发底层 AX action,对隐藏或被遮挡的目标同样有效,完全不涉及坐标。像素点击(click({pid, x, y}))是 fallback,留给 canvas、WebGL 这类没有无障碍信息的表面。
六、为什么说是"绕开 Apple Events"
传统 AppleScript 自动化走 Apple Events,每次操作都可能弹权限确认,TCC(透明度、同意与控制)机制越来越严,很多操作被沙盒限制。
Cua 走的是另一条路:CGEvent(输入注入)加 Accessibility API(读 UI 树),再加 SkyLight 私有 API(后台投递)。这条路不走 AppleScript,不触发 Apple Events 权限弹窗;只用一次性的 Accessibility 授权来读 UI 树、注入输入;再靠 SkyLight 私有 API 实现后台操作不抢光标。
这是从"脚本化自动化"到"输入级自动化"的范式切换。AppleScript 是高层语义操作,比如"给某人发条消息";Cua 是低层输入操作,比如"在这个坐标点一下、打这些字"。后者更通用、更像人,也不依赖应用暴露的脚本接口。
七、能用来干什么
Cua 博客列了四个作者自己在用的场景,全部依赖同一个前提:Agent 像第二光标,而不是取代第一光标。
1. 真正的 dev-loop QA。 Agent 跑"复现、修复、验证"循环,你在编辑器里继续打字。它驱动目标应用、读像素、读 AX 树、改源码、重新构建、查看截图,你的窗口始终不失焦,滚动位置从不变。修没修好,Agent 直接告诉你。
2. 帮你发忘掉的消息。 轻量个人助理:发条 iMessage、查日历、从邮件里抠快递单号,屏幕不变,操作静默发生。
3. 从你没在看的窗口拉视觉上下文。 读 Figma 画布、读 Preview 窗口、读 YouTube 页面,都不把这些窗口带到前台。后台像素点击甚至能让全屏切换落在从未置顶的窗口上。
4. 委托录制 demo。 让 Agent 去捕获那些你不想手动录的操作演示。
八、为什么这是大事
从隔离沙盒到真实环境。 以前的 computer-use Agent 都在 VM 或容器里跑,碰不到你的真实工作环境。cua-driver 让 Agent 在你正在用的真实 Mac 上操作,它能看你正在看的东西、操作你正在用的应用,价值量级完全不同。
人机共驾的核心矛盾被解开。 Agent 自动化最大的障碍不是能力,是打扰。Agent 一动就抢鼠标,人就没法同时干活。后台 Computer Use 让 Agent 和人并行工作在同一台机器上:人打字,Agent 操作别的窗口,互不打扰。这是人机协作从理论走向实用的关键一跃。
GUI 变成 Agent 的原生接口。 以前 Agent 要操作 GUI,得"看截图猜该点哪",靠视觉模型做概率判断;现在它能读 AX 树(结构化)加注入输入(确定性),从"猜"变成"知道"。
开源让它成为基础设施。 Cua 作者在博客里明确说:后台 computer-use 驱动应该是基础设施,不该做成单个 Agent 产品的专属特性。某些闭源产品的同类能力绑定自家生态,而 Cua 选择把这套技术以 MIT 协议开源,让任何 harness(Claude Code、Codex、ZCode、你自己的)都能接入,这正是它能把 Cua 接到 ZCode 上的原因。
九、怎么用
给 ZCode 接入 Cua 走的是插件机制,具体安装方式以 ZCode 文档和 Cua 仓库为准。
开发者如果想给自己的 Agent harness 加上 computer use,直接 clone 仓库:
git clone https://github.com/trycua/cua.git仓库里有完整的 SDK、driver、benchmarks 和 notebooks。libs/cua-driver 是核心驱动,blog/inside-macos-window-internals.md 那篇 macOS 窗口机制深挖是必读材料。
上手前有三点要注意:需要授予 macOS Accessibility 权限(读 AX 树加注入输入,属高敏感权限);它用了 SkyLight 等私有 API,没有 ABI 保证,未来 macOS 更新可能破坏兼容性;项目尚处早期阶段,生产环境慎用。凡是让 Agent 操作生产环境、支付、敏感数据的场景,都应该有明确、可验证、可撤销的授权。
十、小结
一句话总结:Cua 让任何 Agent(包括 ZCode)能在后台操作你的 Mac,点击、打字、读 UI 树,而你的鼠标不动、焦点不切、Space 不跳。它靠逆向 macOS 未公开的 SkyLight 私有 API 实现,以 MIT 协议开源。
这件事更深远的意义在于:它把 GUI 从"人专用接口"变成了"人和 Agent 共用的接口"。四十年来 GUI 为人的手和眼设计,现在 Agent 也能原生地操作它,而不是笨拙地看截图猜。而 Cua 选择开源这一切,让这个能力成为基础设施而非某家公司的护城河,这在 Agent 领域是难得的姿态。
最后值得记住那句判断:GUI 四十年都是给人设计的接口,现在正变成 Agent 也能直接操作的界面。
本文基于 trycua/cua 开源仓库(MIT)及其技术博客《Inside macOS Window Internals》整理。cua-driver 使用了 macOS 私有 API,未来系统更新可能影响兼容性。



