Skip to content

feat(cua): CUA Driver 支持 — 通过 MCP tool 操作 macOS 桌面 - #616

Draft
BingZi-233 wants to merge 12 commits into
Stack-Cairn:mainfrom
BingZi-233:cua-driver
Draft

feat(cua): CUA Driver 支持 — 通过 MCP tool 操作 macOS 桌面#616
BingZi-233 wants to merge 12 commits into
Stack-Cairn:mainfrom
BingZi-233:cua-driver

Conversation

@BingZi-233

@BingZi-233 BingZi-233 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes #617

摘要

为 LiveAgent 增加 CUA (Computer Use Agent) Driver 支持,使 Agent 可通过 cua_* 工具族操作 macOS 桌面 GUI(截屏、识别窗口、点击、输入、滚动、拖拽、按键),并通过现有 MCP tool 注册协议暴露给内置 Agent 使用。

截图

测试期间产出的关键截图(来自 cua-driver 自动化测试会话,/tmp/liveagent_round7_*.png/tmp/cua_round6/):

  • 主聊天页:侧栏默认首屏,底部 Settings 按钮已加 data-testid="open-settings" + aria-label="Open settings"
  • CUA 驱动面板:设置 → 连接 → CUA 驱动,含总开关 / 平台标签(macOS / Windows / Linux)/ 白名单 / 审计上限 / 实时审计日志 / 清空按钮
  • 英文 locale 下 CUA 区域:三条 macOS 权限说明全部英文(Accessibility / Screen Recording / cliclick)
  • 审计日志:清空按钮暴露在 AX 树,audit_log_limit=0 → 审计为空;设 50 → 正常累积

注:截图来自自动化测试 sandbox 环境(headless macOS),用于回归验证,UI 在普通用户会话下应一致。

主要功能

后端 (Rust + Tauri)

  • 新增 services/cua 模块:CuaDriver trait + macOS 实现 (osascript / screencapture / cliclick)
  • CuaStoreenabled / allowed_owners / audit_log_limit 配置 + 最近 N 条审计日志,enforce() 每条命令前守门
  • CuaError 结构化错误(kind + params + message),locale-aware 渲染入口
  • 12 个 Tauri Commandcua_status / set_config / clear_audit / list_windows / focus_window / screenshot / click / double_click / type / key / scroll / drag
  • force_activate_main_window:NSApp.activate + makeKeyAndOrderFront + makeFirstResponder(WKWebView),带 respondsToSelector 守门与 objc2::exception::catch 兜底
  • WKWebView accessibilityChildren 注册 + NSAccessibilityUIElementCreatedNotification 广播
  • 新增 cua_window_ready / cua_refresh_a11y IPC 命令

前端 (agent-gui + agent-ui)

  • 9 个 MCP builtin toolcua_list_windows / focus_window / screenshot / click / double_click / type / key / scroll / drag
  • screenshot 返回多模态 ImageContent
  • 设置面板 CUA 驱动 一节(总开关 / 平台标签 / 白名单 / 审计上限 / 清空 / 实时审计列表)
  • 全部 data-testid 锚点
  • 完整 i18n(zh-CN / en-US),含 settings.cua.* / cua.errors.* / platformNotes.*

dev 环境就绪

  • 新增 scripts/dev-prepare.mjs
    • 幂等清理 macOS Problem Reporter + stale dev binary
    • --watch 模式周期 sweep 新出现的 Problem Reporter
    • 探测 ScreenCapture readiness,失败时自动重试 permissions grant + daemon 重启
    • LIVEAGENT_CUA_DRIVER_NO_OVERLAY=1 让 cua-driver 不显示 overlay
  • scripts/dev-stack.mjs desktop 分支从 crates/agent-gui 启动,避免根目录启动导致 ~icons 解析失败
  • vite.config.tscollectionsNodeResolvePath 支持跨目录 unplugin-icons

权限与安全

  • 默认禁用
  • 白名单(allowed_owners 列表)守门,非允许目标直接拒绝
  • 审计日志内存保留最近 N 条(可配),前端可查看 / 清空
  • 结构化错误,前端 locale 渲染

测试覆盖

9 轮迭代,发现 25 问题,修复 23(medium 6, high 17),2 个 low 仍 Open:

ID 严重级别 标题 状态
CUA-003 low 任务描述里的「连接与协作」实际只有「连接 / Connectivity」 Open(spec 命名偏差)
CUA-004 low 「启用 CUA」开关 AX/视觉一致性 Open(workaround 验证 aria-checked 正常)

测试结果:cargo check / cargo test --lib(951 通过)/ tsc --noEmit / 端到端 cua-driver 验证(MCP-bridge workaround)。

平台支持

  • macOS:完整实现(osascript + screencapture + cliclick)
  • Windows / LinuxUnsupportedDriver stub,清晰报 PlatformError::Unsupported,后续可扩展

已知限制

  1. 拖拽需 brew install cliclick(AppleScript 无原生 drag API)
  2. 窗口级截图(按 owner 截指定应用)当前全屏
  3. Info.plist 未加 NSScreenCaptureUsageDescription / NSAppleEventsUsageDescription(正式分发前需补)
  4. WKWebView AX 在某些 cua-driver sandbox 下仍 ax_window_unresolved(需真实 GUI 会话验证)

提交列表

9d2514cb fix(cua): 适配 WebUI 编译 + Biome lint 修复
88ba04a0 chore(dev): dev-prepare.mjs 幂等清理 + dev-stack.mjs CUA 环境就绪
eb4bde50 feat(cua): agent-ui 共享设置面板与 CUA Driver 一节
eaf8f8e5 feat(cua): agent-gui 前端 MCP tool 注册与 cuaService 桥接
6a7c3f19 feat(cua): Tauri 后端 CUA Driver 支持

- 新增 services/cua 模块:CuaDriver trait + macOS 实现(osascript / screencapture / cliclick)
- CuaStore 持有 enabled / allowed_owners / audit_log_limit 配置 + 最近 N 条审计日志,enforce() 在每条命令前守门
- CuaError 结构化错误(kind + params + message),Locale-aware 渲染入口
- 12 个 Tauri Command:cua_status / set_config / clear_audit / list_windows / focus_window / screenshot / click / double_click / type / key / scroll / drag
- force_activate_main_window:NSApp.activate + makeKeyAndOrderFront + makeFirstResponder(WKWebView),带 respondsToSelector 守门与 objc2::exception::catch,WKWebView accessibilityChildren 注册 + NSAccessibilityUIElementCreatedNotification 广播
- 新增 cua_window_ready / cua_refresh_a11y IPC 命令,供前端路由切换后主动重激活 a11y
- 新增 lib/cua/cuaService.ts:Tauri invoke 桥接层(desktopCuaService),统一错误归一化(normalizeCuaError)
- 新增 lib/tools/cuaTools.ts:9 个 builtin tool(cua_list_windows / focus_window / screenshot / click / double_click / type / key / scroll / drag),screenshot 返回多模态 ImageContent
- builtinRegistry 挂载 cua groupId,仅在用户设置 cua.enabled === true 时注册
- runAgentConversationTurn / useSendChatTurn 透传 getLocale,cua tool 错误按当前 locale 渲染
- i18n/config.ts 新增 settings.cua.* 文案(zh-CN / en-US)
- MacOsTitleBarSpacer 加 data-testid='open-settings' + aria-label
- App.tsx 监听 overlay 路由切换,自动调 cua_refresh_a11y
- vite.config.ts:collectionsNodeResolvePath 支持跨目录 unplugin-icons 解析
- 新增 test/cua/format-cua-error.test.mjs
- 新增 pages/settings/CuaSection.tsx:总开关 + 平台标签 + 白名单 + 审计上限 + 清空 + 实时审计列表,全部 data-testid 锚点
- 新增 lib/cua/formatCuaError.ts:按 locale 渲染 CuaError,中文 / 英文双套模板,支持结构化 params 插值
- SettingsShell 加 data-testid='settings-nav-<id>' 与 data-active;SettingsPage 注册 cua nav 项
- enUSSettings / zhCNSettings 新增 settings.cua.* / cua.errors.* / platformNotes.* 翻译
- ChatHistorySidebar:sidebar Settings Button 加 data-testid='open-settings' + aria-label='Open settings'(与 titlebar 一致)
- contracts/builtinTools + settings/index + settings/types:CuaRuntimeConfig 双向持久化(localStorage 镜像 + 后端权威双写)
- 新增 scripts/dev-prepare.mjs:
  - dismissMacProblemReporterDialogs:osascript AXCloseButton + pkill 双路径,SIGTERM->SIGKILL 升级
  - killStaleLiveagentProcesses:读 dev-stack 状态文件,豁免 in-flight PID,只杀孤儿 stale
  - --watch 模式周期 sweep(7s)新出现的 Problem Reporter
  - probeScreenCaptureReadiness:cua-driver get_desktop_state 主动探测,TCC 授权 + daemon 重启恢复
  - LIVEAGENT_CUA_DRIVER_NO_OVERLAY=1:检测到 overlay daemons 时用 serve --no-overlay 重启
- dev-stack.mjs desktop 分支:绝对路径 spawn crates/agent-gui,workdir 修复 ~icons 解析;--watch 后台 sweep;start 前先调 dev-prepare
@github-actions

Copy link
Copy Markdown
Contributor

PR governance checks failed — this PR has been converted to draft.

  • No linked issue: the PR body must contain Closes #123 / Fixes #123 / Resolves #123. This project requires an issue before a PR — see the contribution guidelines.
  • UI change without screenshots: this PR modifies frontend code. Please add before/after screenshots or a recording under "Screenshots / preview" in the PR body.

Fix the items above, then click Ready for review to re-run the checks.

@StackCairn
StackCairn marked this pull request as draft August 25, 2026 09:39
- crates/agent-gateway/web/src/pages/settings/types.ts:SectionId 加 'cua';SettingsPageProps 加 cuaService?: never(WebUI 不提供 cuaService,条件渲染跳过 CUA 段)
- agent-gui + agent-ui + gateway-webui 全量 Biome check --write:sort imports / useMemo deps / 格式统一
@coder-hhx

Copy link
Copy Markdown
Collaborator

方向本身有明确的业界对标(Claude Code / Codex 都在做桌面 CUA,且都是 macOS 先行),后端 CuaStore.enforce() 权威守门、结构化错误 + locale 渲染、isReadOnly 元数据让 plan mode 自动排除写操作,这些都做得很对。9 轮测试的投入也看得到。

不过有两个层面的问题,建议合入前对齐一下:

1. 安全门控与现有审批体系脱节

CUA 可以操作任意桌面应用、发任意组合键,风险面不小于 Bash,但目前的门控存在几个缺口:

  • 无逐次审批cua_click / cua_type / cua_drag 这些写操作没有 seed group:cua 的默认策略,按 resolveToolPolicycrates/agent-gui/src/lib/tools/toolPolicy.ts)的兜底逻辑默认 allow——总开关一开,所有操作免审直行。对比业界:Claude Code 是逐会话、逐应用交互式授权;Codex 把审批路由放在调用链正中间(app-server 作为 approval router)。建议至少 seed group:cua 默认 askcua_screenshot 虽是只读,但全屏截图含隐私内容,是否同样 ask 值得讨论)。
  • 无 sandboxOffline 处理。离线沙箱的语义是 agent 不出网,但 CUA 能操作任意联网应用——点开浏览器、邮件客户端就能收发数据,等于从桌面层绕开了离线限制。建议 sandboxOffline 下 cua_* 一律 deny。
  • 截图不排除宿主自身窗口。全屏截图(已知限制 feat(chat): add skill references with $ mentions #2)会截到 LiveAgent 自己的聊天界面——模型看到自己的对话内容,形成 prompt 注入回路。Claude Code 把自家终端窗口排除出截图,明确就是防这个。建议至少把 LiveAgent 自身窗口从截图中排除(或遮罩)。
  • 白名单是静态预配置allowed_owners 在设置页预先配置,而非会话内按应用弹窗确认(Claude Code 即使最大信任模式仍逐会话授权)。这条可以作为 follow-up,但建议先在文档里记为已知取舍。

前三条改动成本都不大,风险收益比很高。

2. 建议评估:直接接 cua.ai 的 cua-driver,而非自研 shell-out 驱动

一个架构层面的观察:这个 PR 的端到端测试全程跑在 cua.ai 的 cua-driver 上(dev-prepare.mjsforce_activate_main_window、WKWebView AX 注册这些基本都是为它服务的),说明它已经被验证可用、可集成(stdio/MCP)。但最终发给用户的,是能力弱一档的自研 shell-out 驱动(osascript + screencapture + cliclick):

自研 driver.rs cua-driver(MIT)
光标 cliclick 抢真实光标,agent 工作时用户被打断 按窗口寻址后台驾驶,不打断用户
平台 macOS only,Win/Linux 空 stub macOS / Windows / Linux 现成
非 AX 表面(Chromium、Figma 等) 支持
外部依赖 需用户 brew install cliclick 单二进制
后续维护 Windows/Linux/权限坑自己填 上游社区

建议考虑两段式:先把 cua-driver 作为推荐 MCP 预设接进来(走现有 MCP 基建,零新协议,安全上还能直接复用 server:<id> 级工具策略,等于顺手解决第 1 点的一部分),原生自研留到收集完交互反馈、有明确理由时再做。如果有必须自研的考量(比如不愿引入外部二进制),建议补一份 docs/design/ 文档把这个取舍写清楚,也符合仓库"先设计文档后代码"的纪律。

按 [cua.ai 官方文档](https://cua.ai/docs/how-to-guides/driver/install) 实现:

后端 (Rust):
- services/cua/installer.rs:CUA Driver 检测/安装/更新/daemon 启动逻辑
  - 平台分发:macOS/Linux 走 /bin/bash -c 'curl -fsSL https://cua.ai/driver/install.sh | bash -s --';Windows 走 PowerShell irm ... | iex + cua-driver autostart kick
  - Linux 额外提示 libxi6 + at-spi2-core apt 依赖
  - macOS daemon 启动:open -n -g -a CuaDriver --args serve
  - 30 分钟超时 + 进度事件流(cua_install_progress: starting / downloading / installing / startingDaemon / completed | failed)
- services/cua/error.rs:6 个 installer_* 错误变体(network / curl / signature / permission / unsupported / timeout)
- commands/cua.rs:5 个新 Tauri Command(cua_driver_detect / install / update / start_daemon / install_preview)
- lib.rs:注册到 invoke handler

前端 (agent-ui / agent-gui):
- pages/settings/CuaInstaller.ts:CUA Installer 类型 + 服务接口
- pages/settings/CuaInstallerPanel.tsx:610 行 UI——status badge / 检测/安装/重启/检查更新/应用更新按钮 / 进度条 / 折叠 log / macOS 权限卡片(含 x-apple.systempreferences deep link)/ Linux apt 依赖提示
- pages/settings/CuaSection.tsx:挂载 installer 面板
- lib/cua/cuaService.ts:5 个新 Tauri invoke 桥接方法 + 进度订阅
- App.tsx:visibilitychange / focus 事件调 cua_refresh_a11y
- i18n:agent-gui/config.ts + enUSSettings / zhCNSettings 新增 ~30 个 settings.cua.installer.* + cua.errors.installer.* 文案

data-testid 锚点:cua-driver-status / install-button / update-button / restart-daemon-button / install-progress / install-log 等 17 个

测试:cargo test 957 pass(4 新 installer 测试)/ pnpm test 2530 pass(6 新 cuaService 契约测试)
…034)

问题:useSettingsOverlay 用 requestAnimationFrame 链推进 overlay 状态机(entering → open / leaving → closed)。WebKit 在 document.visibilityState === 'hidden' 时暂停 rAF 派发,且不发出 transitionend 事件,导致:

- open 路径:overlay 永远停在 opacity-0 translate-y-6(用户看不见设置)
- close 路径:panel 永远卡在 leaving 状态保留在 DOM 中(每次开关累积)

修复:open 与 close 都加 350ms setTimeout 兜底 + visibilitychange 监听(hidden→visible 时强制 promote)。对称的 promoteEntering / promoteLeaving 收敛状态推进,clearEnterFallback / clearLeaveFallback 互不干扰。open/close 互相切换时清掉对方的 pending timer。

测试:jsdom 模拟 visibilityState 端到端 16/16 assertion 通过(含 hidden close、visible close 保留动画、re-open 时 cancel pending 三场景)
CUA-033/034 的 useSettingsOverlay 修复只覆盖外层 overlay 容器(React-driven opacity/translate 类)。但 .settings-section-enter / .settings-section-title-enter 的 CSS keyframe 动画(settingsSectionIn)在 document.visibilityState === 'hidden' 时由 WebKit 暂停,opacity/transform 卡在 from 状态(opacity:0, translateY(14px) scale(0.985))。结果是 Settings overlay 已 open 但右侧内容空白。

修复:SettingsShell 加 useIsDocumentHidden() 监听 visibilitychange;hidden 时挂 data-anim-suspended='true' + inline style (animation:none; opacity:1; transform:none);visible 时撤回,CSS 动画按原节奏播放。common-settings.css 加匹配 CSS 规则作 defense-in-depth。

测试:typecheck + cargo check 通过;2530 测试通过;cua-driver 截图验证 hidden 状态下右侧主区域内容完整渲染。
根因:Tauri WKWebView 切换 activeSection 时,sidebar 子树的合成图层存在 stale frame 缓存。DOM class、data-active、右侧内容都正确更新,但视觉渲染卡在前一个活动项(bg-accent + font-medium 不掉)。inline style 写入也无法刷新截图。

修复:sidebar nav-item 的 React key 从 definition.id 改为 `${definition.id}-${active ? 'active' : 'idle'}`。activeSection 改变时旧 active 与新 active 按钮都被 React 视为不同元素,触发 unmount + remount,WebView 必须为全新 DOM 节点从零计算样式,绕开 stale 合成图层。

验证:tsc PASS × 2;cua-driver foreground delivery 4 次切换(系统设置→CUA→供应商→系统工具→Remote)均同步转移。
Tauri/Wry WKWebView 对 SVG 元素的 CSS transform/rotate transition 有持续 bug:rotate-180 class 挂到 ChevronDown SVG 上时 Web Animations API 报告 playState=running / currentTime=0 / startTime=null,动画时钟不推进;aria-expanded 已切但视觉不旋转。

修复:把 transition-transform + logExpanded 条件 rotate-180 从 SVG 节点移到外层包装 <span>,SVG 节点只保留 h-3 w-3 尺寸。绕开 SVG transform transition 的 WebView bug,aria-expanded 与 data-testid 不变。

验证:tsc 通过;Vite HMR bundle 已 serve 修复后代码;截图确认展开态 chevron 呈 ∧ 朝上。
…死(CUA-039)

CUA-038 把 chevron 从 SVG 移到父级 span(Tailwind rotate-180 + transition-transform)。但 Tailwind v4 的 .rotate-180 编译成 'rotate: 180deg' 独立 CSS 属性,与 .transition-transform 级联顺序不可靠,expand 路径 animation 卡在 currentTime=0。

修复:chevron span 改用 inline style.transform + transition(浏览器原生处理 transition,行为跨 Tailwind 版本稳定)。CDP 实测 expand 与 collapse 双向 currentTime 从 0 推进到 ~33ms,computed transform 正确收敛到 0deg / 180deg。

测试:新增 test/cua/cua-installer-panel-chevron.test.mjs(React+jsdom 4/4 通过);2534 全量测试通过;pnpm typecheck + build 通过。
CUA-038/039 修复都用 CSS transition 管线(Tailwind rotate-180 / inline style.transform)。CUA-039 在 setState 后异步再 click 时仍卡 currentTime=0、computed transform 停 matrix(1,0,0,1,0,0),存在 inline style 序列化顺序(transform first vs transition first)的时序依赖。

修复:chevron span 脱离 CSS transition,改用 Web Animations API(element.animate)。新 useEffect([logExpanded]) 取消残留 animation,按当前 logExpanded 方向以 keyframes [{rotate(0deg)}, {rotate(180deg)}] / [{rotate(180deg)}, {rotate(0deg)}] + duration 150ms ease forwards 驱动。WebKit/WKWebView 唯一不依赖 CSS 级联顺序的旋转触发方式。

测试:新增 5 个 jsdom 测试(Element.prototype.animate stub);2535 全量通过;typecheck + build 通过。WebView 真实点击验证需先解 CUA-021。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 增加 CUA Driver 支持 (Computer Use Agent)

2 participants