做 UI 自动化这些年我一直在跟“怎么让电脑自己动鼠标、敲键盘”这件事较劲。最早用 AppleScript后来换 cliclick再后来用 Python 的 pyautogui折腾一圈才发现 macOS 其实自带一个叫 cua 的命令行工具专门干这事。它能模拟鼠标点击、移动、拖拽、滚动也能模拟键盘输入、组合快捷键不需要装任何依赖一条命令就能触发一个 GUI 事件。对做自动化测试、写辅助脚本、处理重复性桌面操作的开发者来说cua 是那种“早就该知道但总是被忽略”的工具。这篇博文我会把 cua 的环境、命令、参数、实战脚本和踩坑记录都梳理出来。适合三类人一是刚开始做 macOS 桌面端 UI 自动化的测试工程师二是在终端里想快速操作 UI 的开发者三是做个人效率工具、需要把重复点击自动化的进阶用户。我保证你读完能直接上手不用再去翻一堆零散的 man page。1. 认识 cua一个被很多人忽略的 macOS 原生自动化命令1.1 cua 到底是什么和 AppleScript、cliclick 有什么区别简单来说cua 是 macOS 系统自带的、通过命令行向全局事件系统投递鼠标和键盘事件的工具。它的底层走的是 Quartz Event Services也就是 CGEventPost 那一套 API和你用 AppleScript 里的“System Events”模拟点击、在 Python 里用 Quartz 发送 CGEvent 是同一层级的操作但 cua 把它封装成了一个个子命令在 shell 里直接就能调用。名字“cua”常见解释是“Click Upon Automation”不过不用纠结全称记住它是“命令行模拟鼠标键盘操作”的工具就行。它没有 GUI没有配置界面唯一的使用方式就是cua 命令 参数。和 cliclick 比cliclick 是第三方开源工具早期很多自动化脚本都在用但 cua 是系统自带的好处很明显对比项cuacliclickAppleScript安装成本无macOS 自带需要 brew install无系统自带事件层级全局 CGEvent全局 CGEventSystem Events 或 CGEvent响应速度快毫秒级快慢有额外桥接开销参数复杂度简单直接简单语法繁琐维护成本高适用场景命令行脚本、快速点击同 cua与系统 UI 深度交互如果你在终端里敲cua发现提示 command not found说明当前系统裁剪了这个工具这种情况我建议直接安装 cliclick 作为替代命令风格很接近后面我给的脚本思路同样适用。1.2 什么时候该用 cua什么时候该换别的工具cua 最擅长的是“我知道坐标我需要点一下”的场景。比如测试环境里要重复点击某个菜单项、自动填表单后点提交、遍历界面走查渲染效果、甚至给演示脚本录制一段“自动操作”。这类需求最大的特点是变化少、坐标固定、无需感知 UI 响应顶多再加个延迟等待。有几种情况我建议你别死磕 cua。一是需要读取界面元素属性和层级关系时cua 是“盲操作”它只负责点击坐标拿不到窗口内容这时应该用 AppleScript 的 System Events 或 XCTest 的 Accessibility API。二是需要跨平台cua 只能 macOS 用Windows/Linux 上你要想别的办法。三是高频稳定执行的生产级自动化还是建议走正经的 UI 测试框架因为事件投递再多也会有系统弹窗、权限变化、焦点切换这类干扰cua 更适合轻量和个人脚本级别。1.3 先确认运行环境上手前先确认几件事which cua cua help我用which cua找路径一般是在/usr/bin/cua。接着cua help会打印出所有支持的子命令不同系统版本可能略有差异以你本机输出为准。常见子命令包括click、doubleclick、rightclick、mousemove、drag、scroll、type、keypress、keydown、keyup。确认完命令后先去系统设置里给“辅助功能”权限加好你的终端应用。这一步是 cua 能正常工作的前提后面在第 4 节我会专门展开讲。2. 核心命令与参数全解析照着敲就能跑2.1 鼠标事件click、doubleclick、rightclick、mousemove、drag、scroll鼠标类命令是整个工具的基石。先看最常用的几个# 移动鼠标到坐标(300, 400)注意某些版本不会带回显 cua mousemove 300 400 # 单击指定坐标 cua click 300 400 # 双击 cua doubleclick 300 400 # 右键 cua rightclick 300 400 # 从 (200, 200) 拖拽到 (600, 400) cua drag 200 200 600 400 # 向上滚动 5 格 cua scroll -5重点说说几个参数细节。click、doubleclick、rightclick 的坐标是必填的这就是要点击的屏幕全局坐标点。mousemove 只需要目标坐标。drag 比较特殊它接收起点和终点内部实现是按下鼠标左键 - 移动到终点 - 松开左键。这段过程是连续的对拖拽文件、拖动窗口、调整分区大小这类操作非常管用我实测拖窗口标题栏也有效。scroll 参数是滚动方向负数向上滚正数向下滚。如果你在用触控板时有“自然滚动”的设置这个方向会受到影响但命令行工具大部分时候是给桌面端鼠标场景用的方向上你多试两次就能摸清规律。有一点要注意命令中的坐标是全局坐标系起点是主屏幕左上角 (0,0)。如果你有多个显示器副屏在左侧时坐标会变成负数比如副屏上的一个点在主屏左边坐标可能是 (-500, 300)这是正常现象传负数就行。有个“只用坐标找位置”的技巧我先运行一个三秒倒计时的 Python 脚本来实时打印鼠标位置手动把鼠标移到目标 UI 元素上记下坐标再用这个坐标去写 cua 命令。这个流程我放在 2.3 节细说。2.2 键盘事件type、keypress、keydown、keyup键盘类命令更不复杂但能做的事比鼠标多# 输入一段文本注意特殊字符用引号包起来 cua type hello, cua # 按一个普通键 cua keypress enter cua keypress tab cua keypress space # 组合按键 cua keypress cmdc cua keypress shiftcmd3 # 按下并按住一个修饰键执行操作后再松开 cua keydown cmd cua keypress c cua keyup cmdtype 命令会把整个字符串逐字符投递到当前焦点窗口。注意它不会自动切换输入法如果你当前是中文输入法英文文本可能被输入成拼音组合我一般会在脚本前先用cua keypress ctrlspace或固定的输入法切换快捷键强制切到英文模式再执行 type这样能避免很多乱入问题。keypress 是“按下再松开”的完整动作。单个可移植的键名包括 enter、tab、space、escape、delete、return、arrowup、arrowdown、arrowleft、arrowright、f1-f12 等。修饰键组合用连接cmd、shift、option、ctrl 都是支持的。这里我踩过一个坑组合键里修饰键的顺序有时会影响功能比如截图快捷键写成shiftcmd3和cmdshift3通常等价但一些应用会区分顺序建议按 UI 菜单里展示的顺序写。keydown 和 keyup 是“只按下不松开”和“只松开不按下”适合需要保持按住状态的场景比如按住 Shift 再点击多选文件、按住 Command 同时点选多个图标。注意用完一定要 keyup否则修饰键会一直处于按下状态后续操作全部错乱。2.3 坐标怎么获取三种定位 UI 元素的实用姿势写 cua 命令之前首先要解决“坐标从哪来”的问题。我用过三种方法效率从低到高排列第一种是肉眼观察加系统自带工具。macOS 的截屏功能会显示鼠标指针附近的坐标但我个人觉得效率一般适合偶尔用一两次。第二种是用 Python 的 pyautogui 库在 shell 里临时打印鼠标位置import pyautogui import time time.sleep(3) print(pyautogui.position())脚本运行后有 3 秒时间把鼠标移到目标元素上然后终端打印出坐标。这个方式便宜好用适合脚本化收集多个坐标。第三种是使用 Xcode 自带的 Accessibility Inspector它能查看窗口、按钮、输入框的 accessibility frame直接给出元素在屏幕上的精确位置。缺点是它主要面向开发调试工具本身有点重但对测试人员来说非常友好还能顺带理解 UI 层级。我的习惯是临时用一次 pyautogui正式调试复杂界面时用 Accessibility Inspector日常就是开一个终端跑cua mousemove x y微调坐标——移动过去后用眼睛确认鼠标位置对不对而不是直接点下去这个微调法能省下大量反复点击的确认时间。3. 三个实战场景从脚本小白到写工具3.1 场景一批量填写重复的桌面端表单我有段时间要维护一个内部数据录入工具每天要往 Web 表单里粘贴几十条 CSV 里的数据每条记录都是相同的字段顺序纯粹是重复劳动。我用 cua 写了个 shell 脚本自动完成“切到浏览器窗口 - 点击第一个输入框 - 输入值 - 点击下一个 - 输入 - 提交”。脚本的核心逻辑大概是这样#!/bin/bash input_file$1 # 激活浏览器 open -a Google Chrome # 等窗口激活 sleep 1 while IFS, read -r name value; do # 点击姓名输入框 cua click 500 300 sleep 0.3 cua type $name # 点击数值输入框 cua click 500 360 sleep 0.3 cua type $value # 点击提交按钮 cua click 600 420 sleep 2 done $input_file这段脚本运行前我先手动把窗口移到固定位置保证坐标稳定。表单每行的 y 坐标固定从 300 开始每隔 60 像素一个输入框总共两个字段所以脚本里就是两个点击坐标。提交后系统处理需要 2 秒sleep 给足时间。这个场景里最值钱的经验是不要把采集系统搞复杂先用九宫格法手动确认三到五个关键坐标然后写死进脚本。表单布局不变坐标就不会变布局变了脚本里改坐标比改逻辑快得多。3.2 场景二在 Node.js 流水线里用 cua 做端到端 GUI 冒烟测试后来我把 cua 集成进了 Node.js 的自动化流水线主要用来做桌面端应用编译后的冒烟测试。思路是用 child_process 调用 cua 命令在应用启动后自动点击首屏按钮、输入搜索词、触发导出操作然后断言应用是否产生预期文件。核心代码片段const { execFileSync } require(child_process); function click(x, y, delay 500) { execFileSync(/usr/bin/cua, [click, String(x), String(y)]); // 给 UI 渲染一点时间避免事件被吞掉 return new Promise((resolve) setTimeout(resolve, delay)); } function typeText(text, delay 300) { execFileSync(/usr/bin/cua, [type, text]); return new Promise((resolve) setTimeout(resolve, delay)); } (async () { // 启动应用 execFileSync(open, [-a, MyDesktopApp]); await click(400, 200); await typeText(测试关键词); await click(600, 450, 1500); // 检查导出文件是否存在 ... })();execFileSync 是同步调用配合 await Promise 的 sleep 可以形成最简单的“动作-等待-断言”流程。动作之间 delay 我一般是 300 到 2000 毫秒视 UI 响应速度而定。这里不建议用 Thread.sleep 式的统一大延迟比如一键等待 5 秒那样脚本会变得很慢按操作分批控制延迟才是正解。Node 的 child_process 还有一个好处可以方便地捕获 cua 命令的 stderr。如果权限被系统拦截stderr 里一般会有错误提示便于集成到 CI 日志里。3.3 场景三开发期弹窗自动关闭与流程自动遍历做桌面端应用开发时最烦的就是测试环境里每次启动都会弹几个系统提示框。我写了个遍历脚本在应用启动后自动把可能出现的弹窗按钮依次点掉#!/bin/bash for coord in 680 520 800 300 450 380; do set -- $coord cua click $1 $2 sleep 1 done原理很简单就是遍历一组已知的按钮坐标逐个点击。这类需求对精准度要求不高关键是别把脚本写死把坐标放在一个数组或外部配置文件里后续弹窗变多了改配置就行不用动逻辑。这个思路还能扩展到 UI 自动遍历启动应用后依次点击侧边栏的每个导航项每次点击后截一张图用来快速发现渲染崩溃或页面空白。配合 cua 加 screencapturecua click 200 150 sleep 1 screencapture -x screenshot_01.png cua click 200 250 sleep 1 screencapture -x screenshot_02.png这类“遍历主界面”的脚本在开发后期非常有用可以把手动点几十个页面的时间压缩到十几秒。3.4 脚本组织细节超时、失败重试与日志写多了之后我发现单纯把 cua 命令串起来还不够一套能稳定跑的自动化脚本必须考虑三个问题超时、失败重试和日志。超时最好理解。UI 操作永远比命令行操作慢不管是窗口启动、页面加载还是弹窗出现都需要时间。我的做法是每个关键操作后面跟一个等待函数而不是全脚本用一个固定 sleep。比如# 等待某个文件出现最多等 10 秒 for i in $(seq 1 20); do if [ -f $output_file ]; then break fi sleep 0.5 done失败重试也很关键。有些点击会因为弹窗抢焦点、系统卡顿等原因丢事件最稳的处理方式是“执行前先确认条件执行后再确认结果”。比如点击导出按钮后检查导出文件是否生成没生成就重试一次。这个思路比盲目加大 sleep 可靠很多。日志则是排障的救命稻草。我每个脚本里都会带一个 log 函数把当前动作、坐标、时间、执行结果记录到文件里。谁也不会记得两周前跑的那条脚本为什么突然失败但日志会告诉你。4. 权限、焦点与稳定性踩坑排查实录4.1 命令没反应八成是辅助功能权限没给cua 最大的坑不在命令本身而在系统权限。macOS 对投递全局鼠标键盘事件有严格限制执行 cua 的进程必须获得“辅助功能”权限。如果你的脚本在终端里运行那就需要去“系统设置 - 隐私与安全性 - 辅助功能”里勾选终端应用如果你通过 IDE 运行给 IDE 加权限。这里有个细节经常被忽略如果你是通过脚本间接调用 cua比如 Node 脚本调系统命令权限落在“调用它的应用”上而不是 cua 自身。我踩过一次坑终端里跑 cua 正常但用 IDE 调试 Node 脚本时一直无效排查了很久才发现是 IDE 没被加入辅助功能。权限给完之后最好重启一次终端或 IDE让配置生效。如果换了系统用户或换了机器这套权限要重新配一遍这点在 CI 环境里尤其容易踩坑。4.2 能点到但位置偏了Retina 缩放和多显示器坐标问题如果你用的是 Retina 屏幕并且设置了缩放分辨率会遇到坐标“偏移”问题。cua 底层 CGEvent 使用的坐标是屏幕的“点”坐标系而截屏工具给出的可能是物理像素。两者之间有一个 scale factor 的换算关系。比如在 2 倍缩放的屏幕上截图里坐标为 (2000, 1200) 的像素点在点坐标系里是 (1000, 600)。我的解决方式很简单截图时顺手把缩放比例记下来或者先用鼠标移到目标元素上验证坐标。更靠谱的是用 2.3 节里 pyautogui 的方式它输出的是当前软件环境下的有效坐标直接用来填 cua 命令一般是准的。多显示器场景也比较恼人。副屏在左侧时坐标为负值这本身没问题但如果系统调整过主屏和副屏的相对位置UI 元素的全局坐标会整体改变。我的经验是多屏环境里固定把目标窗口移到主屏并固定窗口位置再跑脚本能省很多事。4.3 键盘事件失效焦点、输入法、安全输入框键盘事件失效的原因通常是三类。第一类是焦点不对目标窗口虽然打开了但并没有成为当前活跃窗口。这时需要先激活应用open -a AppName sleep 1 # 或者用 osascript 激活 osascript -e tell application AppName to activate第二类是输入法干扰。当前是中文输入法时type 输入英文字符串经常变成拼音组合加上同步的候选框还会把后续按键全部吞掉。我在自动化脚本里会在所有 type 前强制切换一次输入法用cua keypress ctrlspace或者你自己系统里固定的切换快捷键。第三类是安全输入框。macOS 对密码框、某些银行控件、系统锁定界面有额外的输入保护CGEvent 投递会被拒绝。这个属于系统限制cua 也无能为力。遇到这类组件就别想着完全自动化手动输入或换用别的方案比较实际。4.4 稳定性问题慢一点给系统一点时间还有一个体会cua 模拟的事件是“异步”的它把事件投递到系统后应用侧什么时候处理完是不确定的。我在脚本里如果连着发 10 个快速点击事件不会丢失但应用界面可能来不及刷新导致下一次点击发生在一瞬间的空白状态上。对此最好的策略就是“慢一点”。每个操作后至少等待 200 到 500 毫秒关键操作比如提交表单、导出文件、等待窗口加载等待 1 到 3 秒。你可以在本地先快速跑一遍观察哪些步骤连过快导致结果不对再针对性调整。另外脚本运行期间建议停止手动操作鼠标键盘否则你会和脚本“抢控制权”事件会被混合。我把这类脚本定位为“无人值守”工具跑之前先做别的别在同一个屏幕上再动鼠标。5. 边界、规范与个人心得5.1 自动化工具的合法使用边界先说一个所有自动化工具都要注意的事只能在你有权限、有正当目的的系统和应用上使用。cua 可以用来做自己的效率脚本、测试自己负责的应用、处理合法的重复性操作但不要用它去做绕过安全检查、抢占资源、抓取他人信息、或者在未授权的软件里进行自动化操作。尤其是游戏脚本、自动抢购、点击作弊这类用途既有违服务条款也容易触发系统风控得不偿失。我写 cua 脚本的定位很清楚辅助开发、辅助测试、提升自己的工作效率。在这个边界内它真的很顺手。5.2 几点实操心得和后续扩展思路最后分享几个我个人在实际使用中积累的小经验按价值排序。第一个是配置文件思维。不要把坐标写死在脚本里用一个 json 或 yaml 存放 UI 元素的坐标和操作序列。界面一改改配置不改逻辑维护成本会低很多。第二个是“先打印后执行”的调试模式。我给脚本加过--dry-run参数只打印将要执行的 cua 命令不真正执行。这样在改坐标、调流程时很安全也方便别人 review 你的自动化脚本。第三个是结合 Accessibility 信息做动态定位。纯坐标在界面变化时会失手但 cua 本身拿不到元素信息。我的进阶方案是先用 AppleScript 或 Accessibility API 拿到元素坐标再用 cua 去点击。等于把“定位”和“操作”拆开各用各的擅长工具。如果后续想扩展我推荐研究一下 Quartz Event Services 的 C 接口也就是 CGEvent 系列函数可以用 C/C/Python 直接控制更底层的事件细节比如调整事件的时间戳、发送多指触控事件等。cua 帮你封装好了常用命令但理解底层能让你在遇到它不支持的场景时不至于抓瞎。做自动化最有成就感的地方不在于省了多少时间而在于把那些重复、无聊、容易出错的手工操作交回给机器把精力留给真正需要判断的工作。cua 虽然只是一个不起眼的命令行小工具但它在“最后一公里”里发挥的作用远比看起来大。