news 2026/9/13 4:30:30

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入 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/cua

macOS 桌面输入天然是"单光标、单键盘、单聚焦窗口"的同步模型,这让 GUI Agent 在驱动真实 Mac 应用时总是被迫抢占用户的前台焦点。本文基于 cua-driver 开源驱动(本仓库 libs/cua-driver)对 macOS 窗口管理内部机制的逆向工程,完整讲解 SkyLight 私有框架中的SLEventPostToPidSLPSPostEventRecordTo_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-use"(agent 在后台驱动真实 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 激活实际上是两步操作:

  1. 输入路由激活:通过内部状态翻转(SLPSPostEventRecordTo)告诉应用"你现在是输入路由的焦点应用";
  2. 窗口抬升:通过SLPSSetFrontProcessWithOptions告诉 WindowServer"请把窗口抬升并重新挂载到当前 Space"。

关键在于第一步可以不伴随第二步。yabai(macOS 平铺窗口管理器)在多年实践中必须"激活窗口而不抬升窗口",否则在多 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 字节的合成事件记录:

  • 第一个记录发给原前台 PSN,bytes[0x8A] = 0x02标记为失焦(defocus);
  • 第二个记录发给目标 PSN,bytes[0x8A] = 0x01标记为聚焦(focus),并把目标窗口 ID 以小端序写入bytes[0x3C..0x3F]

整个缓冲区构造在源码注释中有精确的字节级说明(bytes[0x04]=0xf8bytes[0x08]=0x0d等)。执行之后,目标应用进入 AppKit-active 状态,事件路由生效,但窗口仍停留在 z-stack 原位置。此时再发送 CGEvent 就能进入正确的事件循环,且用户完全无感。

值得注意的是,源码注释特别强调:刻意跳过SLPSSetFrontProcessWithOptions是保持 Chromium 用户激活门处于开启状态的关键之一(skylight.rs)。仓库中还提供了配套的 Space 查询工具集(CGSGetActiveSpaceSLSCopySpacesForWindowsSLSCopyManagedDisplayForWindow),用于判断目标窗口落在哪个 Space,确保后台交互始终不改变用户的桌面布局。

SkyLight.framework 与SLEventPostToPid

yabai 模式解决了事件路由,但CGEvent.postToPid单独仍无法到达 Chromium web 内容。真正的缺失拼图位于SkyLight.frameworkSLEventPostToPid

从调用方视角,它和CGEvent.postToPid签名相同,但事件走的是完全不同的代码路径——一个绕过IOHIDPostEvent的 auth-signed SkyLight 通道。cua-driver 源码 skylight.rs 注释给出了更精确的两层故事:

  • 投递路径SLEventPostToPidSLEventPostToPSNCGSTickleActivityMonitorSLSUpdateSystemActivityWithLocationIOHIDPostEvent。公开的CGEventPostToPid跳过了 activity-monitor tickle,这正是 Chromium/Catalyst 目标不接受那些事件作为实时输入的原因;
  • 认证(仅键盘):macOS 14+ 上 WindowServer 对发往 Chromium 类目标的合成键盘事件要求附加SLSEventAuthenticationMessage,通过 ObjC 工厂构建并调用SLEventSetAuthenticationMessage附加后再投递。

Chromium 的 renderer 过滤器接受经此通道到达的事件而拒绝其他事件。一个合理的推断是SLEventPostToPid在事件记录中盖上了某种标记,表明事件"源自 WindowServer 信任信封"(trust envelope),Chromium 的过滤器读取该位。原文档作者也明确表示这仍是推测,欢迎知情者给出精确答案。

函数本身不出现在任何 Apple 头文件中,通过 grep SkyLight 的符号导出找到。cua-driver 的实现方式(skylight.rs)是:先用dlopen加载/System/Library/PrivateFrameworks/SkyLight.framework/SkyLightRTLD_LAZY | RTLD_GLOBAL),再通过dlsym解析全部所需符号,所有句柄都用OnceLock惰性解析一次;任何符号解析失败时函数返回false,调用方自动回退到公开 APICGEvent::post_to_pidis_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 兼容左键配方完整落地为三步事件流:

  1. mouseMovedprimer:在目标坐标发送带phase=2标记的鼠标移动事件,先激活目标窗口的 cursor-tracking 状态(后台 AppKit 控件如果从未收到 move 事件,mouseDown会被静默忽略);
  2. 屏外 primer 点击:在(-1,-1)发送phase=1/2的 down/up 对,满足 Chromium 用户激活门且不命中任何 DOM 元素,之后等待约 100ms 让 primer 与真实点击构成独立手势;
  3. 目标点击对:在真实坐标发送phase=3的 down/up 对,clickState从 1 递增到 N 以支持双击合并。

每个事件都通过set_integer_field盖上一组关键字段(源码注释逐字段说明):f0=手势阶段、f1=clickState、f3=按钮编号(0=左键)、f7=3(NSEventSubtypeTouch)、f40=目标 pid(Chromium 合成事件过滤器)、f51/f91/f92=CGWindowID(窗口路由)、f58=恒定 click-group ID(手势合并),并通过CGEventSetWindowLocation盖上窗口局部坐标点。(-1,-1)这个坐标在源码中以off_screen/off_local常量直接出现,是整个配方的"签名"。

键盘输入:比预想简单,但 macOS 14+ 需要认证信封

键盘是"无聊的故事":CGEvent.postToPid就足够了,不需要 SkyLight。按 pid 作用域的击键只落入该应用的事件队列,不会去任何别处。原因在于 macOS 没有与 Chromium renderer 鼠标过滤器对等的全局击键过滤器——应用会照单全收到达其事件循环的按键事件。

不过 cua-driver 的实现比文档走得更远:在 macOS 14(Sonoma)及以上,发往 Chromium 类目标的合成键盘事件需要SLSEventAuthenticationMessage信封。源码 skylight.rs 中有完整的兼容性处理:通过objc_getClass+sel_registerName+objc_msgSend调用+[SLSEventAuthenticationMessage messageWithEventRecord:pid:version:]构建消息,再用SLEventSetAuthenticationMessage附加到事件上。关键细节是必须用class_respondsToSelector验证选择器存在——该工厂方法只在 macOS 15(Sequoia)才加入,macOS 14 上仅凭sel_registerName成功不能保证可用,否则运行时直接抛NSInvalidArgumentException: unrecognized selector(源码注释关联 issue #1503)。

另一个有意思的权衡出现在 keyboard.rs:hotkey_no_auth(不带认证信封)专门用于 NSMenu 键盘等效键——带信封时SLEventPostToPid会分叉到绕过IOHIDPostEvent的 direct-Mach 路径,NSMenu 根本看不到那些事件;不带信封时路径经过IOHIDPostEventNSApplication.sendEvent:才能分派 NSMenu 键盘等效键。此外type_text对每个字符强制set_flags(CGEventFlagNull),因为 Chrome 会检查 flags 字段推断修饰键状态,否则大写字符会被视为 Shift+e 并让修饰键泄漏到下一个字符(keyboard.rs)。

Electron 应用的 AX 树保活

后台化场景还有最后一个必须解决的问题:Electron 应用(Chrome、Slack、VS Code、Discord、Notion 等)的辅助功能树在窗口被遮挡时暂停更新——Blink 的 accessibility 代码在认为"没有人在看"时会短路。公开的AXObserverAddNotification不会把观察者标记为"remote-aware",Blink 因此永远不知道有人在监听。

而另一个私有变体_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 的最新约定)分为两层:

  • 元素寻址(ax)click({pid, window_id, element_index})直接触发底层 AX action,可作用于隐藏/被遮挡目标,完全不涉及坐标;
  • 像素寻址(px)click({pid, x, y})作为 canvas、WebGL 等非 AX 表面的回退方案,使用上述 SkyLight 配方。

值得注意的演进:SKILL.md 中标注capture_mode参数已废弃并被忽略——决策点从"捕获时"移到了"动作时",动作时根据目标选择element_token(ax)或x,y(px),感知始终是两者的结合。

真实用例

以下四个场景只有驱动行为像"机器上的第二个光标"(而非试图替代第一个光标)时才能成立:

  1. 开发循环 QA:Agent harness 在后台跑 repro-fix-verify 循环,用户继续在编辑器打字。Claude Code 通过 cua-driver 驱动目标应用、读像素、读 AX 树、改源码、重建、核对截图——用户的 Agent harness 永不失焦,滚动位置不变;
  2. 容易被遗忘的消息:轻量个人助理工作(发消息、查日历、从邮件取快递单号)。有趣的性质不是"能发 iMessage",而是"发生时你正在读的屏幕纹丝不动";
  3. 从没在看的应用拉取视觉上下文:Claude Code 读取 Figma 画布、Preview 窗口、YouTube 页面内容而不前置任何窗口。后台像素点击配方让 YouTube 全屏切换落在从未抬升的窗口上;
  4. 委托演示录制:让 Agent 录制产品演示视频。Agent 驱动被演示的应用、记录轨迹,cua-driver renderer 在导出时放大每次点击。因为点击是后台化的,最终视频里唯一的鼠标就是 driver 绘制的那个。

已知限制:SkyLight 配方解决不了的两件事

  1. Chromium 会把合成右键强制转为左键。renderer-IPC 过滤器在非 HID-tap 路径上丢弃右键 subtype。通过 AX 的 element-indexed 右键对 AX 可寻址目标(链接、按钮、工具栏项)工作正常,但纯 web 内容只能左键。作者认为除非内置浏览器扩展(违背 drop-in driver 设计),否则无解;
  2. 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 4:30:27

Spring Boot 3集成Druid踩坑指南:四大报错与配置模板

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 4:30:03

RK3588边缘AI配置四层解耦架构:告别JSON定时炸弹

1. 为什么一个JSON文件在RK3588边缘AI项目里会成为“定时炸弹”我第一次在客户现场看到那个叫config.json的文件时&#xff0c;它正躺在RK3588板卡的/etc/ai/目录下&#xff0c;大小237KB&#xff0c;嵌套了17层对象&#xff0c;数组里套数组&#xff0c;数组里又塞对象&#x…

作者头像 李华
网站建设 2026/9/13 4:30:00

网址导航网站大全:从通用到垂直,实测推荐与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 4:29:58

从样本处理到训练落地:C++手写BP神经网络在推荐算法竞赛中的实践

简介&#xff1a;阿里移动推荐算法竞赛资源包是一份围绕移动端推荐场景的完整参考实现&#xff0c;面向推荐算法、数据挖掘方向的开发者和高校学生&#xff0c;尤其适合作为毕业设计、课程设计或实训项目的学习模板。包内整合了从数据预处理到模型训练的主干流程&#xff0c;涵…

作者头像 李华
网站建设 2026/9/13 4:29:21

Kiro架构实战:多智能体协作与AWS原生组件运维

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华