cua 多光标后台计算机使用演示实战:用 cua-driver 并发驱动五种 UI 框架的"国家档案系统"
【免费下载链接】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
本篇技术指南围绕仓库 samples/driver/multi-cursor 中的Multi-cursor background computer-use demo("National Records System")展开:一个前台主终端的人机操作,会被实时转发到四个后台终端,由四个独立的 cua-driver 会话(即四个不同颜色的 Agent 光标)并发执行,全程不抬升任何窗口、不移动用户鼠标。读完本文,你将掌握 cua-driver 在"无自动化集成的遗留系统"上做后台并发驱动的完整方法,理解它如何在不依赖辅助功能树(Accessibility Tree)的情况下回退到像素级注入,并学会构建、运行和扩展这样一个跨框架的演示编排器。
演示要回答的核心问题
在企业内网里,大量政府、银行、制造类业务终端是"纯手绘"的遗留应用:没有 API、没有插件接口、没有命令行,甚至在 AI 时代完全没有自动化集成通道。这个 demo 想证明的是——这类应用 cua-driver 照样能自动化。
它选择了一组刻意做旧的"政府记录终端"界面:海军蓝横幅、UNCLASSIFIED // FOR OFFICIAL USE ONLY保密条、功能键栏、绿屏记录网格、状态栏。整个演示围绕三个能力点设计:
- 一对多的并发重放:用户在中心主终端的一次操作(输入账户名 + 点击 Add Record),被同步扇出到四个角落的后台终端,每个终端由独立的 cua-driver 会话驱动;
- 零干扰:任何窗口都不会被抬到前台,用户自己的鼠标指针从未被移动;
- 有无辅助功能树都能工作:五个窗口横跨五种 UI 框架,cua-driver 的默认分发逻辑在有 a11y 树的地方自动选择 UIA-Invoke,在没有 a11y 树的地方回退到像素/指针注入。
这正是 cua-driver 后台驱动能力的浓缩验证:把"每次操作都走无障碍接口"与"不得不走像素坐标"的两类窗口,放在同一张屏幕上同时驱动。
窗口布局:2×2 四角后台 + 居中主控
每个窗口都是工作区的一半宽 × 一半高(½ work-width × ½ work-height)。四个角落窗口把任务栏安全的工作区(taskbar-safe work area)平铺成四个象限,主控窗口居中并同时压住四个角落:
┌────────────────────────┬────────────────────────┐ │ Win32 GDI (NO a11y) │ WinForms (.NET) │ │ crimson ● │ amber ● │ │ ┌────────────────────────┐ │ │ │ MASTER — Win32 controls │ ← you │ ├───────────│ (foreground, overlaps) │─────────────┤ │ WPF (XAML)│ │ Electron │ │ └────────────────────────┘ mint_lime ● │ │ aqua ● │ (Chromium) │ └────────────────────────┴────────────────────────┘运行后,在中心主控上点击SUBMIT(或先输入一个 subject 名称再提交),四个彩色光标会同时滑向四个角落终端,并发地在后台提交同一条记录。观察每个角落的绿屏记录网格不断增长、RECORDS:计数器逐条跳动,而没有任何角落窗口被带到前台。
布局由编排器源码精确实现(见 orchestrator/src/main.rs):
- 通过
SystemParametersInfoW(SPI_GETWORKAREA)取工作区矩形(失败时回退到GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN)),计算每个象限的宽高; - 四个角落窗口用
SetWindowPos以SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOZORDER放置——不激活、不改变 Z 序; - 主控窗口尺寸只有象限的一半,放在工作区正中央,恰好压住四角的中心接缝,但不会遮住位于象限中心位置的表单;
- 主控窗口通过
SetForegroundWindow置前,交给人类操作。
五种框架各测什么:一张框架能力表
| 窗口 | 框架 | 无障碍能力 | cua-driver 驱动路径 |
|---|---|---|---|
| TL | Win32 + GDI(自绘) | 无 | 像素命中测试 → PostMessage / 指针注入 |
| TR | .NET WinForms | MSAA/UIA | UIA Invoke |
| BL | .NET WPF | UIA (XAML) | UIA Invoke(通过WS_EX_NOACTIVATE避免抢占前台) |
| BR | Electron | UIA (Chromium) | UIA Invoke |
| Center | Win32 标准控件 | MSAA | (前台;由人驱动) |
每个角落节点的实现都放在 demo 目录内:
- Win32 GDI 节点(左上角,
crimson光标):见 legacy-app/src/main.rs。它完全没有注册任何可访问性接口,Account Name 字段和 Add Record 按钮都是WM_PAINT里用 GDI 画笔手绘的——这就是"没有 a11y 树"的极端情形,只能走像素路径; - WinForms 节点(右上角,
amber光标):见 dotnet/winforms/Program.cs,经典 Win32 控件 + MSAA/UIA,cua-driver 用 UIA Invoke 驱动 Account Name 文本框和 Add Record 按钮; - WPF 节点(左下角,
aqua光标):见 dotnet/wpf/Program.cs,XAML 元素树天然暴露 UIA,且窗口带WS_EX_NOACTIVATE扩展样式,确保 UIA Invoke 不会偷偷抢占前台; - Electron 节点(右下角,
mint_lime光标):见 electron/main.js 与 electron/index.html,Chromium 渲染进程暴露 UIA 树,同样走 UIA Invoke; - 中心主控:见 legacy-app/src/main.rs,Account Name 是真实 Win32 EDIT 控件、SUBMIT 是真实 BUTTON 控件,两者被埋点:每当用户提交,就把
TYPE\t<text>、CLICK\t<rx>\t<ry>写到标准输出,供编排器读取并重放。
注意一个细节:五种框架的界面布局是同一套分数化坐标。Win32 GDI 版用常量NAME/SAVE/DOODLE/GRID的客户端分数矩形(见 legacy-app/src/main.rs),WinForms 用X(a) = W*a、Y(a) = H*a的分数布局函数,Electron 用grid-template-rows/columns百分比。这让编排器可以用"客户端相对分数"(0~1)统一计算像素目标,跨框架复用同一套命中坐标。
构建:五步凑齐全部组件
在 demo 目录(samples/driver/multi-cursor)下,按顺序构建四类产物:
# from this directory cargo build # legacy-app + orchestrator (Rust) dotnet build dotnet/winforms/winforms.csproj # WinForms dotnet build dotnet/wpf/wpf.csproj # WPF npm install --prefix electron # Electron (downloads electron once)还要在仓库根工作区把驱动本身构建一次:
cargo build -p cua-driver --manifest-path ..\..\libs\cua-driver\rust\Cargo.toml各产物说明:
- Rust 侧:demo 目录本身是一个 Cargo workspace(见 Cargo.toml,
members = ["legacy-app", "orchestrator"]),一次cargo build同时产出legacy-app.exe和orchestrator.exe; - .NET 侧:两个 csproj 分别产出
winforms-legacy.exe与wpf-legacy.exe,均放在各自bin/Debug/net10.0-windows/输出目录下(编排器正是按这个路径探测可执行文件是否存在,缺失时会打印(skip)并跳过该角落); - Electron 侧:
npm install --prefix electron只安装一次 Electron 运行时(^39.8.5,见 electron/package.json),运行入口是electron .(main.js)。
说明:编排器对缺失的角落节点是容忍的。如果某个框架没构建或未安装,它会打印
[orch] (skip) ...继续运行——这非常适合先在只有 GDI + 一个 .NET 节点的环境下快速试跑。
运行:人工驱动与自播放两种模式
.\target\debug\orchestrator.exe # human-driven: click/type in the center .\target\debug\orchestrator.exe --auto # self-playing: drives a TYPE+CLICK every few seconds编排器启动后按这个顺序工作(见 orchestrator/src/main.rs):
- 以
cua-driver serve启动守护进程(daemon); - 依次拉起 legacy-app 的 GDI 角落、WinForms、WPF、Electron,以及带 stdout 管道的中心主控;
- 通过窗口标题子串(
Win32 GDI/WinForms/WPF/Electron/Master (Win32)用EnumWindows找到各窗口句柄,轮询等待最多 8 秒; - 按 2×2 象限布局放置角落,主控居中置前;
- 对每个角落调用
cua-driver call get_window_state(capture_mode:"ax")抓取辅助功能树,在树中定位] Edit(Account Name 字段)和含ADD RECORD的] Button(提交按钮)——每个动作前都会重新抓取,因为 Chromium 的树出现得晚、且随行数增加会重新编号,而 GDI 永远没有树; - 为每个会话预先启用彩色 Agent 光标:
cua-driver call set_agent_cursor_enabled {"enabled":true,"session":"<color>"}; - 为每个角落开一个独立驱动线程,每个线程通过
mpsc::channel接收来自主控的动作并转发到对应cua-driver call会话。
在人工模式下,主控的 stdout 管道被BufReader逐行读取:CLICK <rx> <ry>解析为点击动作、TYPE <text>解析为输入动作(见 orchestrator/src/main.rs),随后广播给所有角落线程。在--auto模式下,编排器自己按3 秒 → TYPE SUBJECT-001..003 → 1.8 秒 → CLICK的节奏发送动作,最后还会追加一轮画迷宫(见下文)。
环境变量覆盖(Env overrides)
默认路径写死在编排器里,但以下变量可以覆盖:
CUA_DRIVER_EXE:cua-driver 可执行文件路径,默认libs/cua-driver/rust/target/debug/cua-driver.exe(相对仓库根);LEGACY_APP_EXE:legacy-app 可执行文件路径,默认target/debug/legacy-app.exe(相对 demo 目录);WINFORMS_EXE、WPF_EXE:对应 .NET 产物路径;ELECTRON_DIR:Electron 应用目录。
若CUA_DRIVER_EXE或LEGACY_APP_EXE不存在,编排器直接报错退出(cua-driver.exe not found at ...)。
进程清理:Windows Job Object 兜底
编排器启动的每个子进程(守护进程、五个窗口)都会通过AssignProcessToJobObject挂进一个设置了JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE的 Job Object(见 orchestrator/src/main.rs)。只要关掉中心窗口或杀掉编排器进程,整个进程树会被 Windows 一次性回收,不会有孤儿进程残留。
动作分发:有树走 UIA,无树走像素
每个角落线程对两类动作采用不同的路径(见 orchestrator/src/main.rs):
输入(Type):
- 有 a11y 树:直接
cua-driver call type_text并携带element_index,cua-driver 会自动路由到 UIAValuePattern.SetValue——不抢焦点、不投 SendInput; - 无 a11y(GDI 角落):先把 Account Name 字段的分数位置(
FIELD_FRAC = (0.28, 0.145))换算成窗口本地像素,click一次聚焦,再type_text发送WM_CHAR。
点击(Click):
- 有 a11y 树:
click携带 SUBMIT 按钮的element_index,走 UIA Invoke; - 无 a11y:把主控发出的按钮相对中心(
rx/ry分数)映射到 GDI 窗口本地像素再click。
每次调用都以 JSON 传入pid、window_id(HWND 地址)和session,例如:
{"pid":1234,"window_id":-1043813,"element_index":7,"text":"SUBJECT-001","session":"amber"} {"pid":1234,"window_id":-1043813,"x":420,"y":180,"session":"crimson"}校验(Verify):点击后线程会再抓一次get_window_state,解析状态栏里的Records: N计数器,只有计数器确实递增才判定COMMITTED ✓,否则记录FAIL (no new record)。这是刻意设计的验证手段——只检查输入框里有文本很容易被骗,必须证明记录真正落进了网格。GDI 角落的计数器是手绘像素、无法从树中读出,因此它靠"网格在屏幕上真实增长"做可视化验证。
画迷宫:纯坐标拖拽 + 截图比对的自校验
--auto模式的最后一幕是画迷宫,用来证明 cua-driver 能在完全没有元素目标、没有应用配合的情况下,仅靠坐标完成画布绘制(见 orchestrator/src/main.rs):
- 迷宫是 7 段直线段组成的螺旋,坐标定义在公共的
REGION = [0.470, 0.180, 0.900, 0.295](客户端分数矩形,位于每个框架画板右上角); - 每段调用一次
cua-driver call drag,参数带"dispatch":"background":cua-driver 能 PostMessage 的地方就 PostMessage 拖拽,对会丢弃 posted mouse 的画布(Chromium/WPF)则回退到 pen 注入(pointer*事件统一覆盖 mouse/pen/touch,这正是 Electron 版用pointerdown/pointermove/pointerup而非mousedown的原因,见 electron/index.html); - 画完后通过
get_window_state(capture_mode:"vision",WGC 采集,连完全被遮挡的窗口也能截图)把结果存成 PNG; - 校验算法把"绘制的墨迹"和"参考迷宫"分别归一化到 96×96 网格,用膨胀(Chebyshev dilation 半径 2)后的 F1 分数打分,
MAZE_PASS = 0.45为及格线,低于即判定FAIL (lines don't match)——且这个分数只评价线条形状,不惩罚落点偏移和缩放。
这里用到了两个值得注意的底层能力:一是 WGC(Windows Graphics Capture)采集对遮挡窗口依然有效;二是DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS)拿到的窗口外框尺寸,用来把"原始位图像素"与"截图缩放后像素"对齐裁剪。
彩色光标:一个会话一种颜色
cua-driver 会按会话名给每个 session 分配光标颜色:调色板命名的会话(如crimson)直接选中对应颜色。每次click/type_text调用里带上"session":"<color>",动作就被路由到该会话的叠加光标,光标会滑行到目标位置。四个会话 → 四个光标同时动画。
本 demo 用到的会话/颜色映射:
| 角落 | 会话(光标颜色) |
|---|---|
| Win32 GDI | crimson |
| WinForms | amber |
| WPF | aqua |
| Electron | mint_lime |
关于"不抬升窗口"(no-z-raise)的机制细节,可进一步阅读仓库中的后台输入契约文档 the-no-foreground-contract.mdx,以及 Windows 平台 MCP 工具清单 mcp-tools-windows.mdx。WPF 角落通过WS_EX_NOACTIVATE扩展样式从窗口层面保证 UIA Invoke 不会触发前台抢占,这与 demo 的整体"无前台契约"一脉相承。
从演示到生产:可以复用的工程模式
把 orchestrator/src/main.rs 当作模板,可以提炼出几条可迁移的后台并发驱动模式:
- 会话隔离:每个目标窗口一个独立 cua-driver 会话,天然获得独立的彩色光标、独立的状态与校验上下文,互不干扰;
- 每动作重发现:
get_window_state在每次动作前重新抓取并解析element_index,规避了动态应用(如 Chromium)树编号漂移的问题——静态缓存索引在真实生产环境中几乎必坏; - 可回退的分发策略:把"有树 → UIA,无树 → 像素"的决策放在动作执行层而不是启动层,同一套编排代码可以同时服务两类窗口;
- 结果级校验:用应用状态(
Records:计数器、截图比对)而不是"调用是否成功"来判定动作是否真正生效; - 进程树治理:Job Object +
KILL_ON_JOB_CLOSE保证大规模多进程演示/测试不会留下孤儿进程。
这套模式同样适用于仓库中的其他场景:批量跑跨 OS 舰队(cross-OS fleets)评估、给 cua-bench 制造多窗口测试环境,或把同一套"人类一次操作 → 多 Agent 并发重放"的范式用于数据生成与训练轨迹采集。演示的价值正在于:它把 cua-driver 最容易被低估的"后台、并发、无 a11y 也能干"三件事,压缩进了一个肉眼可见、可运行、可自校验的最小系统。
【免费下载链接】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),仅供参考