拓冰建站拓冰建站
首页 / 资讯中心 / 正文

深入 macOS 窗口内部机制:cua-driver 如何用 SkyLight 实现后台多光标 Agent

深入 macOS 窗口内部机制cua-driver 如何用 SkyLight 实现后台多光标 Agent【免费下载链接】cuaScale computer-use 2.0 with open-source drivers, cross-OS fleets, and benchmarks for training, evaluation, and data generation.项目地址: https://gitcode.com/GitHub_Trending/cua/cuamacOS 桌面输入天然是单光标、单键盘、单聚焦窗口的同步模型这让 GUI Agent 在驱动真实 Mac 应用时总是被迫抢占用户的前台焦点。本文基于 cua-driver 开源驱动本仓库 libs/cua-driver对 macOS 窗口管理内部机制的逆向工程完整讲解 SkyLight 私有框架中的SLEventPostToPid、SLPSPostEventRecordTo与_AXObserverAddNotificationAndCheckRemote三个关键 SPI以及 Chromium 用户激活门user-activation gate的破解配方。读完你将理解后台 computer-use 的完整技术栈并能在自己的 Agent 框架中接入 cua-driver 的三种采集模式与 element-indexed 点击。为什么需要后台 computer-use驱动自 2024 年以来大量 GUI Agent 产品失败的核心原因在于桌面输入的嵌入式本质一个光标、一个键盘、只能服务一个聚焦窗口。一旦 Agent 要点击某个应用就必须先激活该窗口于是用户的光标被劫持、前台被抢占、macOS 还会把用户拖到目标窗口所在的 Space——这是多年来被反复吐槽的space focus steal问题。这正是 Cua 团队此前一直推荐隔离 VM 与 GUI 容器作为 Agent 动作空间的根本原因而不是让用户把 computer-server 直接装在自己的桌面主机上。直到 Codex 与 Sky 团队推出背景 computer-useagent 在后台驱动真实 Mac 应用、不接管你的电脑的形态后台驱动才成为可能。cua-driver 的目标是让这种能力成为通用组件而非某个单一 Agent 产品的专属特性任何 harness 都可以直接接入对模型选择无任何意见。三层简单方案的失败路径第一层CGEventPost全局投递——会移动光标最省事的做法是对着按钮的屏幕坐标CGEventPost一个LeftMouseDown/LeftMouseUp。但CGEventPost把事件丢进 HID 事件流——这正是物理鼠标所在的流。WindowServer 看到一个位于(342, 198)的点击会顺带把指针更新到该坐标。副作用就是用户的光标被移动。第二层CGEvent.postToPid——Chrome 过滤合成事件CGEvent.postToPid把同样的事件直接投递给指定进程而不是全局 HID 流不会引起光标跳变对绝大多数应用都好用——除了 Chrome。Chromium 在 renderer IPC 边界过滤合成事件如果点击没有携带真实用户手势通常附加的遥测信息特定的mouseEventSubtype字节、clickState计数器、由私有 SPI 设置的窗口局部坐标戳renderer 会将其视为不可信事件并静默丢弃。点击到达外层窗口进程后凭空消失。第三层先激活目标再发 HID 事件——正是要避免的行为先激活目标窗口、发送普通 HID 事件、再失活——能工作但抬升窗口会触发 Space 跟随把用户的焦点拉回 Agent 身上。这正是所有传统计算机使用工具的做法也恰恰是想要避免的行为。yabai 的 focus-without-raise 模式macOS 的 AppKit 激活实际上是两步操作输入路由激活通过内部状态翻转SLPSPostEventRecordTo告诉应用你现在是输入路由的焦点应用窗口抬升通过SLPSSetFrontProcessWithOptions告诉 WindowServer请把窗口抬升并重新挂载到当前 Space。关键在于第一步可以不伴随第二步。yabaimacOS 平铺窗口管理器在多年实践中必须激活窗口而不抬升窗口否则在多 Space 场景下根本无法使用。它的window_manager_focus_window_without_raise函数约四十行 C 代码正是这个模式的教科书实现。cua-driver 在 skylight.rs 中完整移植了该配方activate_without_raise(target_pid, target_wid)首先用_SLPSGetFrontProcess捕获当前前台 PSN再用SLSGetWindowOwner SLSGetConnectionPSN老系统回退GetProcessForPID解析目标 PSN然后投递两个 248 字节的合成事件记录第一个记录发给原前台 PSNbytes[0x8A] 0x02标记为失焦defocus第二个记录发给目标 PSNbytes[0x8A] 0x01标记为聚焦focus并把目标窗口 ID 以小端序写入bytes[0x3C..0x3F]。整个缓冲区构造在源码注释中有精确的字节级说明bytes[0x04]0xf8、bytes[0x08]0x0d等。执行之后目标应用进入 AppKit-active 状态事件路由生效但窗口仍停留在 z-stack 原位置。此时再发送 CGEvent 就能进入正确的事件循环且用户完全无感。值得注意的是源码注释特别强调刻意跳过SLPSSetFrontProcessWithOptions是保持 Chromium 用户激活门处于开启状态的关键之一skylight.rs。仓库中还提供了配套的 Space 查询工具集CGSGetActiveSpace、SLSCopySpacesForWindows、SLSCopyManagedDisplayForWindow用于判断目标窗口落在哪个 Space确保后台交互始终不改变用户的桌面布局。SkyLight.framework 与SLEventPostToPidyabai 模式解决了事件路由但CGEvent.postToPid单独仍无法到达 Chromium web 内容。真正的缺失拼图位于SkyLight.frameworkSLEventPostToPid。从调用方视角它和CGEvent.postToPid签名相同但事件走的是完全不同的代码路径——一个绕过IOHIDPostEvent的 auth-signed SkyLight 通道。cua-driver 源码 skylight.rs 注释给出了更精确的两层故事投递路径SLEventPostToPid→SLEventPostToPSN→CGSTickleActivityMonitor→SLSUpdateSystemActivityWithLocation→IOHIDPostEvent。公开的CGEventPostToPid跳过了 activity-monitor tickle这正是 Chromium/Catalyst 目标不接受那些事件作为实时输入的原因认证仅键盘macOS 14 上 WindowServer 对发往 Chromium 类目标的合成键盘事件要求附加SLSEventAuthenticationMessage通过 ObjC 工厂构建并调用SLEventSetAuthenticationMessage附加后再投递。Chromium 的 renderer 过滤器接受经此通道到达的事件而拒绝其他事件。一个合理的推断是SLEventPostToPid在事件记录中盖上了某种标记表明事件源自 WindowServer 信任信封trust envelopeChromium 的过滤器读取该位。原文档作者也明确表示这仍是推测欢迎知情者给出精确答案。函数本身不出现在任何 Apple 头文件中通过 grep SkyLight 的符号导出找到。cua-driver 的实现方式skylight.rs是先用dlopen加载/System/Library/PrivateFrameworks/SkyLight.framework/SkyLightRTLD_LAZY | RTLD_GLOBAL再通过dlsym解析全部所需符号所有句柄都用OnceLock惰性解析一次任何符号解析失败时函数返回false调用方自动回退到公开 APICGEvent::post_to_pid。is_available()与is_focus_without_raise_available()两个探针函数让上层可以在运行时检测当前 macOS 版本的能力。Primer Click攻克 Chromium 用户激活门即便事件通过 SkyLight 信任通道到达Chromium 还有一道用户激活门user-activation gate如果 renderer 最近没有看到受信任的用户手势它会拒绝让点击激活视频播放/暂停、window.open、全屏 API 等能力。解决方案是一个诱饵点击decoy click / primer click在(-1, -1)——屏幕上没有任何窗口覆盖的像素坐标——投递一对LeftMouseDown/LeftMouseUp。Chromium 因为没有窗口认领该坐标而丢弃这次点击但用户激活门仍然向前推进。几毫秒后到达的真实点击被当作该手势的受信任延续。这是整个逆向过程中最难找到的一环用户激活门在 Chromium 面向 Web 的一侧都几乎无文档更不用说原生命中测试路径。cua-driver 的 Rust 实现 mouse.rs 中的click_at_xy_chromium函数把整个 Chromium 兼容左键配方完整落地为三步事件流mouseMovedprimer在目标坐标发送带phase2标记的鼠标移动事件先激活目标窗口的 cursor-tracking 状态后台 AppKit 控件如果从未收到 move 事件mouseDown会被静默忽略屏外 primer 点击在(-1,-1)发送phase1/2的 down/up 对满足 Chromium 用户激活门且不命中任何 DOM 元素之后等待约 100ms 让 primer 与真实点击构成独立手势目标点击对在真实坐标发送phase3的 down/up 对clickState从 1 递增到 N 以支持双击合并。每个事件都通过set_integer_field盖上一组关键字段源码注释逐字段说明f0手势阶段、f1clickState、f3按钮编号0左键、f73NSEventSubtypeTouch、f40目标 pidChromium 合成事件过滤器、f51/f91/f92CGWindowID窗口路由、f58恒定 click-group ID手势合并并通过CGEventSetWindowLocation盖上窗口局部坐标点。(-1,-1)这个坐标在源码中以off_screen/off_local常量直接出现是整个配方的签名。键盘输入比预想简单但 macOS 14 需要认证信封键盘是无聊的故事CGEvent.postToPid就足够了不需要 SkyLight。按 pid 作用域的击键只落入该应用的事件队列不会去任何别处。原因在于 macOS 没有与 Chromium renderer 鼠标过滤器对等的全局击键过滤器——应用会照单全收到达其事件循环的按键事件。不过 cua-driver 的实现比文档走得更远在 macOS 14Sonoma及以上发往 Chromium 类目标的合成键盘事件需要SLSEventAuthenticationMessage信封。源码 skylight.rs 中有完整的兼容性处理通过objc_getClasssel_registerNameobjc_msgSend调用[SLSEventAuthenticationMessage messageWithEventRecord:pid:version:]构建消息再用SLEventSetAuthenticationMessage附加到事件上。关键细节是必须用class_respondsToSelector验证选择器存在——该工厂方法只在 macOS 15Sequoia才加入macOS 14 上仅凭sel_registerName成功不能保证可用否则运行时直接抛NSInvalidArgumentException: unrecognized selector源码注释关联 issue #1503。另一个有意思的权衡出现在 keyboard.rshotkey_no_auth不带认证信封专门用于 NSMenu 键盘等效键——带信封时SLEventPostToPid会分叉到绕过IOHIDPostEvent的 direct-Mach 路径NSMenu 根本看不到那些事件不带信封时路径经过IOHIDPostEventNSApplication.sendEvent:才能分派 NSMenu 键盘等效键。此外type_text对每个字符强制set_flags(CGEventFlagNull)因为 Chrome 会检查 flags 字段推断修饰键状态否则大写字符会被视为 Shifte 并让修饰键泄漏到下一个字符keyboard.rs。Electron 应用的 AX 树保活后台化场景还有最后一个必须解决的问题Electron 应用Chrome、Slack、VS Code、Discord、Notion 等的辅助功能树在窗口被遮挡时暂停更新——Blink 的 accessibility 代码在认为没有人在看时会短路。公开的AXObserverAddNotification不会把观察者标记为remote-awareBlink 因此永远不知道有人在监听。而另一个私有变体_AXObserverAddNotificationAndCheckRemote会。一次dlsym调用即可让 AX 树在完整的 launch-snapshot-act-verify 循环中保持活跃即使目标窗口被隐藏、被其他应用挡住或位于其他 Space。作者发现它的方式颇具工程趣味对比 Accessibility Inspector 在检查被遮挡的 Electron 窗口时的行为与自己封装的差异注意到 Inspector 的路径多触碰了一个自己缺失的符号。当前仓库的 Rust 实现中该能力由dlsym惰性探测具体符号解析失败时驱动会回退到公开的 AX API 路径。三种采集模式与两种点击寻址点击、击键、AX 树都在后台工作后仍有一个路由决策留给客户端决定点什么时客户端需要基于什么来推理cua-driver 提供三种捕获模式模式返回内容适用场景特点ax简化 AX 树Markdown 大纲每个可交互节点带索引系统应用计算器、备忘录、iMessage或 AppKit/SwiftUI 应用无需屏幕捕获、无需屏幕录制权限AX 树能很好表征用户所见vision目标窗口的 PNG 截图vision-first VLM基于像素 grounding、不使用 element_index负载最小、最快但 Agent 需自己做全部空间推理对 Claude Code 等编码 Agent 仍不稳定som默认AX 树 截图通用场景树告诉 Agent 什么可点截图在标签重复或为空时消歧som是默认模式因为它能让 element-indexed 点击driver 的主要寻址方式在第一次快照上就工作并免费附带视觉确认。cua-driver 的寻址体系结合 SKILL.md 的最新约定分为两层元素寻址axclick({pid, window_id, element_index})直接触发底层 AX action可作用于隐藏/被遮挡目标完全不涉及坐标像素寻址pxclick({pid, x, y})作为 canvas、WebGL 等非 AX 表面的回退方案使用上述 SkyLight 配方。值得注意的演进SKILL.md 中标注capture_mode参数已废弃并被忽略——决策点从捕获时移到了动作时动作时根据目标选择element_tokenax或x,ypx感知始终是两者的结合。真实用例以下四个场景只有驱动行为像机器上的第二个光标而非试图替代第一个光标时才能成立开发循环 QAAgent harness 在后台跑 repro-fix-verify 循环用户继续在编辑器打字。Claude Code 通过 cua-driver 驱动目标应用、读像素、读 AX 树、改源码、重建、核对截图——用户的 Agent harness 永不失焦滚动位置不变容易被遗忘的消息轻量个人助理工作发消息、查日历、从邮件取快递单号。有趣的性质不是能发 iMessage而是发生时你正在读的屏幕纹丝不动从没在看的应用拉取视觉上下文Claude Code 读取 Figma 画布、Preview 窗口、YouTube 页面内容而不前置任何窗口。后台像素点击配方让 YouTube 全屏切换落在从未抬升的窗口上委托演示录制让 Agent 录制产品演示视频。Agent 驱动被演示的应用、记录轨迹cua-driver renderer 在导出时放大每次点击。因为点击是后台化的最终视频里唯一的鼠标就是 driver 绘制的那个。已知限制SkyLight 配方解决不了的两件事Chromium 会把合成右键强制转为左键。renderer-IPC 过滤器在非 HID-tap 路径上丢弃右键 subtype。通过 AX 的 element-indexed 右键对 AX 可寻址目标链接、按钮、工具栏项工作正常但纯 web 内容只能左键。作者认为除非内置浏览器扩展违背 drop-in driver 设计否则无解Canvas 应用Blender GHOST、Unity、游戏会整体过滤 per-pid 路由。它们的事件循环只接受来自cghidEventTap且带前置mouseMoved的事件因此需要短暂的前台激活。cua-driver 会对这类应用回退到激活后再点击——不抢占前台的承诺在这一类上被打破。如果要自动化 Blender光标会跳变。安装与接入在 macOS 上安装 cua-driver即本仓库 libs/cua-driver 中 scripts/install.sh 对应的安装脚本/bin/bash -c $(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)脚本会把CuaDriver.app放入/Applications将cua-driverCLI 软链到/usr/local/bin并安装每周自动更新器。然后在 系统设置 → 隐私与安全性 中为CuaDriver.app授予一次**辅助功能Accessibility与屏幕录制Screen Recording**权限即可使用。接入 Claude Code、Cursor 或任意 MCP 客户端——将以下配置粘贴进客户端的 MCP 配置{ mcpServers: { cua-driver: { command: /Applications/CuaDriver.app/Contents/MacOS/cua-driver, args: [mcp] } } }或直接从 shell 驱动——每个 MCP 工具都是顶层cua-driver name子命令cua-driver list_apps cua-driver launch_app {bundle_id:com.apple.calculator} cua-driver click {pid:1234,window_id:5678,element_index:14}总结cua-driver 证明了后台 computer-use不需要依赖单一厂商通过SLEventPostToPid的信任通道、SLPSPostEventRecordTo的 focus-without-raise 模式、(-1,-1)诱饵点击对 Chromium 用户激活门的破解、以及_AXObserverAddNotificationAndCheckRemote对 Electron AX 树的保活任何 Agent harness 都可以在用户继续工作的同时驱动真实的 Mac 应用。cua-driver 仍处于 v0.1 早期预览阶段以宽松许可发布其完整实现细节可继续阅读 skylight.rs、mouse.rs 与 keyboard.rs动作契约与寻址约定见 SKILL.md 和 协议定义。【免费下载链接】cuaScale computer-use 2.0 with open-source drivers, cross-OS fleets, and benchmarks for training, evaluation, and data generation.项目地址: https://gitcode.com/GitHub_Trending/cua/cua创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门