适用版本:中控 EasyClick iOS USB 10.7.0+ | 手机系统 iOS 17+ 技术栈:EasyClick iOS USB 中控 + JavaScript 脚本(也支持 Python 等语言走 HTTP 接口) 面向场景:苹果群控(一对多批量控制 iPhone)的底层链路实现 官方文档:https://ieasyclick.com/iosdocs/ | USB HID 函数手册:https://ieasyclick.com/iosdocs/funcs/usb-hid-event-api/
一、先说清楚:iOS 自动化到底难在哪
做过安卓自动化的同学应该知道,安卓那边有 AccessibilityService(无障碍服务),可以直接读取界面元素、模拟点击,开发体验相当舒服。
iOS 就没这个待遇。系统封闭,没有开放的无障碍接口给第三方调用。
所以想在 iPhone 上做自动化,不管是单机脚本还是苹果群控这种一对多的批量场景,绕来绕去只有两条技术路线:
第一条是软件注入。装一个代理 App,通过 XCTest 框架在 App 内部注入测试指令。这条路能用,但问题不少:需要签名,签完还要装描述文件、手动信任开发者;签名有期限,过期就得重来;App 或系统一升级,代理容易失效。
第二条是模拟外部输入设备。让电脑或者一块开发板冒充真实的鼠标键盘,通过系统底层的输入通道把触摸事件送进去。iOS 无法区分这是真实外设还是程序注入,所以这条路更干净。
这篇要讲的 USB HID,就是第二条路线里最省事的一种:只要一根 USB 数据线,不需要额外买开发板。
对做苹果群控的人来说,这条路最有价值的地方在于:一台中控电脑可以同时带多台 iPhone,每台都按同样的方式注入触控,脚本逻辑完全一致。设备从三台加到三十台,代码不用改。
二、USB HID 的原理拆解
HID 是 Human Interface Device 的缩写,即人机接口设备。你手上的鼠标、键盘、游戏手柄,都属于 HID 设备。
协议层面,HID 设备通过标准描述符向主机声明自己是什么类型的设备、支持哪些输入输出。这个规范是公开的,而且被所有主流操作系统原生支持,包括 iOS。
关键在于,当一个设备通过 USB 声明自己是 HID 键盘时,iOS 会无条件信任它,把它当成真实的键盘。
这就给了一条巧妙的路子。电脑端软件构造出符合 HID 规范的报文,告诉手机“我刚按下了屏幕坐标 (300, 500) 的位置”,手机的输入系统就会把它当成一次真实的触摸事件分发下去。
对系统和 App 来说,这跟用户手指点上去没有区别。
和其他两条 HID 链路的对比
同样思路,还有另外两种实现方式:
| 链路 | 硬件依赖 | 系统要求 | 适合场景 |
|---|---|---|---|
| USB HID | 一根 USB 数据线 | iOS 17+,中控 10.7.0+ | 最省事,单机快速验证 |
| 蓝牙 BLE HID | ESP32C3 开发板 | iOS 18+ 更优 | 配合无截图模式可完全绕开屏幕镜像 |
| OTG HID | ESP32S3 开发板 | 通用 | 想脱离电脑独立运行 |
三种方式的 API 能力基本一致,区别在链路和硬件依赖。选哪种后面会讲。
三、环境准备
动手之前,先确认这几件事:
- 中控版本:USB HID 功能需要 EasyClick iOS USB 中控 10.7.0 及以上。低版本没有这套 API。
- 手机系统:建议 iOS 17 以上。16 及以下的部分机型可能走不通这条路,可以用蓝牙或 OTG 替代。
- 连接状态:USB 数据线插好,手机上点“信任此电脑”,中控里确认已经启动桥接。
- 数据线:一定用原装或者 MFi 认证的线。杂牌线很多只能充电不能传数据,插上电脑毫无反应,这是新手最常踩的坑。
USB 设备授权和自动化环境这两步,官方有一份专门的说明文档,卡住的话可以直接对照:https://ieasyclick.com/iosdocs/advance/ai-agent/prerequisites
四、五个 API 快速上手
所有 USB HID 操作都挂在 usbHidEvent 这个对象下面。先看一个能跑的最小示例:
function main() {
// 1. 开启会话
let r = usbHidEvent.sessionStart(true);
if (!(r == null || r === "")) {
logw("开启会话失败: " + r);
return;
}
// 2. 设置屏幕尺寸,坐标换算依赖这个参数
r = usbHidEvent.setScreenSize(1170, 2532);
if (!(r == null || r === "")) {
logw("设置尺寸失败: " + r);
return;
}
// 3. 点击坐标 (300, 400)
r = usbHidEvent.clickPoint(300, 400);
logd("点击结果: " + (r == null || r === "" ? "成功" : r));
// 4. 输入文字,走剪贴板粘贴,中英文通用
r = usbHidEvent.inputText("Hello 自动化");
logd("输入结果: " + (r == null || r === "" ? "成功" : r));
// 5. 关闭会话
usbHidEvent.sessionStop();
}
main();
一个必须记住的约定
看上面的代码应该注意到了,每个函数都在判断 r == null || r === ""。
这是这套 API 的统一约定:返回值是 null 或者空字符串表示成功,返回其它字符串就是错误信息。
所以写判断逻辑不需要 try-catch,直接判断返回值就行。建议封装一个工具函数:
function _ok(r) {
return r == null || r === "";
}
sessionStart 的参数
sessionStart(gate) 有一个可选参数 gate,默认 true,作用是尝试增强兼容模式。低版本系统会自动忽略这个参数,所以一般不用管,保持默认即可。
会话已经存在时会复用,不会重复创建。如果需要彻底重置(比如断流、触摸失效),用 sessionRestart,它等价于先 sessionStop 再 sessionStart,比单独重开更彻底。
坐标系统
setScreenSize(w, h) 设置的是屏幕像素宽高,单位是像素。
这里有个关键点:脚本坐标、投屏画面坐标、截图像素坐标,三者是统一的。
也就是说,你在投屏界面上量出来的坐标,可以直接写进脚本。
但要注意:分辨率变化或者横竖屏切换之后,必须重新调用 setScreenSize,否则坐标会整体偏移。这是最高频的坑之一。
五、API 完整清单
下面按用途分类整理,方便速查。每个函数的完整参数说明和示例代码以官方手册为准:https://ieasyclick.com/iosdocs/funcs/usb-hid-event-api/
会话管理
| 函数 | 说明 |
|---|---|
sessionStart(gate) |
开启会话,已存在则复用 |
sessionStop() |
关闭会话,释放资源 |
sessionRestart(gate) |
强制重建会话,用于排障 |
屏幕与坐标
| 函数 | 说明 |
|---|---|
setScreenSize(w, h) |
设置屏幕像素宽高 |
触控操作
| 函数 | 说明 |
|---|---|
clickPoint(x, y) |
单击 |
doubleClickPoint(x, y) |
双击 |
press(x, y, delay) |
长按,delay 为按住毫秒数 |
swipeToPoint(x1, y1, x2, y2, duration) |
从起点滑动到终点 |
touchDown(x, y) |
按下 |
touchMove(x, y) |
移动 |
touchUp(x, y) |
抬起 |
multiTouch(points, timeout) |
按轨迹回放多点触摸 |
multiTouch 的轨迹点格式如下,action 中 0 表示按下,1 表示抬起,2 表示移动,delay 是该点的延迟毫秒数:
let trace = [
{"action": 0, "x": 100, "y": 500, "delay": 20},
{"action": 2, "x": 100, "y": 300, "delay": 30},
{"action": 1, "x": 100, "y": 300, "delay": 20}
];
usbHidEvent.multiTouch(trace, 10000);
适合做复杂手势,比如画圈、双指缩放。
文字输入
| 函数 | 行为 | 使用建议 |
|---|---|---|
inputText(text) |
统一走剪贴板粘贴 | 默认用这个,中英文都稳 |
typeText(text) |
可打印英文走键盘逐键,含中文或 emoji 自动改为粘贴 | 需要模拟真实打字时用 |
setClipboard(text) |
只写入剪贴板,不粘贴 | 配合组合键手动触发粘贴时用 |
getClipboard() |
读取剪贴板 | 有已知限制,见踩坑部分 |
按键操作
| 函数 | 说明 |
|---|---|
keyPressChar(prefix, code) |
字符按键或组合键 |
keyPress(key) |
按下单个键 |
keyUp() |
抬起全部按键 |
systemKey(key) |
系统按键,支持 home、recents、lock |
组合键的 prefix 可以是 alt、ctrl、gui、shift、r_ctrl、r_shift,不需要组合时传空字符串。
模拟粘贴的写法:
// gui 对应 iOS 上的 Command 键
usbHidEvent.keyPressChar("gui", "v");
音量控制
| 函数 | 说明 |
|---|---|
volumeUp() |
音量加 |
volumeDown() |
音量减 |
mute() |
静音 |
六、进阶:用 Python 走 HTTP 接口
如果不写 JavaScript,也可以用其它语言通过 HTTP 对接中控。所有接口都是 POST,请求体是 JSON。完整的请求参数、返回字段和 Python / Node.js / cURL / C# 四种示例见官方开放接口文档:https://ieasyclick.com/iosdocs/advance/openapi/usbhid
地址前缀是中控地址,默认 http://127.0.0.1:8019,路径与脚本函数一一对应:
| 脚本函数 | HTTP 路径 |
|---|---|
sessionStart |
/openapi/usbhidSessionStart |
sessionStop |
/openapi/usbhidSessionStop |
sessionRestart |
/openapi/usbhidSessionRestart |
setScreenSize |
/openapi/usbhidSetScreenSize |
clickPoint |
/openapi/usbhidClickPoint |
doubleClickPoint |
/openapi/usbhidDoubleClickPoint |
press |
/openapi/usbhidPress |
swipeToPoint |
/openapi/usbhidSwipeToPoint |
touchDown/Move/Up |
/openapi/usbhidTouchDown 等 |
multiTouch |
/openapi/usbhidMultiTouch |
inputText |
/openapi/usbhidInputText |
typeText |
/openapi/usbhidTypeText |
systemKey |
/openapi/usbhidSystemKey |
keyPressChar |
/openapi/usbhidKeyPressChar |
keyPress/keyUp |
/openapi/usbhidKeyPress、/openapi/usbhidKeyUp |
volumeUp/Down/mute |
/openapi/usbhidVolumeUp 等 |
setClipboard |
/openapi/usbhidSetClipboard |
getClipboard |
/openapi/usbhidGetClipboard |
返回格式与脚本不同,HTTP 接口返回的是 JSON:
{
"code": 0,
"msg": "",
"data": ""
}
code 为 0 表示成功,非 0 时 msg 是错误信息。
Python 示例
import requests
BASE = "http://127.0.0.1:8019"
DEVICE_ID = "你的设备ID" # 从设备列表接口获取
def call(path, body=None):
r = requests.post(f"{BASE}{path}", json=body or {}, timeout=30)
data = r.json()
if data.get("code") != 0:
raise RuntimeError(f"{path} 失败: {data.get('msg')}")
return data
# 开会话
call("/openapi/usbhidSessionStart", {"deviceId": DEVICE_ID, "gate": True})
# 设置屏幕尺寸
call("/openapi/usbhidSetScreenSize", {"deviceId": DEVICE_ID, "w": 1170, "h": 2532})
# 点击
call("/openapi/usbhidClickPoint", {"deviceId": DEVICE_ID, "x": 300, "y": 400})
# 输入文字
call("/openapi/usbhidInputText", {"deviceId": DEVICE_ID, "text": "Hello"})
# 关闭会话
call("/openapi/usbhidSessionStop", {"deviceId": DEVICE_ID})
这种方式适合把自动化能力接进已有的 Python 系统,比如电商后台、测试平台、运维工单。
七、实战:写一个完整的自动化流程
假设需求是:打开某个 App,等页面加载完成,点击一个按钮,输入内容并提交,然后回到桌面。
function _ok(r) {
return r == null || r === "";
}
// 随机等待,避免机械节奏
function randSleep(minSec, maxSec) {
let ms = (minSec + Math.random() * (maxSec - minSec)) * 1000;
sleep(parseInt(ms));
}
function main() {
let r = usbHidEvent.sessionStart(true);
if (!_ok(r)) {
logw("会话开启失败: " + r);
return;
}
// 屏幕尺寸要与当前实际分辨率一致
r = usbHidEvent.setScreenSize(1170, 2532);
if (!_ok(r)) {
logw("设置尺寸失败: " + r);
return;
}
// 步骤一:回到桌面,确保起点一致
usbHidEvent.systemKey("home");
randSleep(1, 2);
// 步骤二:打开目标 App(假设图标在第一屏第二个位置)
r = usbHidEvent.clickPoint(400, 780);
if (!_ok(r)) {
logw("打开 App 失败: " + r);
return;
}
randSleep(4, 7); // 等页面加载
// 步骤三:点击输入框
r = usbHidEvent.clickPoint(585, 1200);
if (!_ok(r)) {
logw("点击输入框失败: " + r);
return;
}
randSleep(1, 2);
// 步骤四:输入内容
r = usbHidEvent.inputText("这是自动化写入的内容");
if (!_ok(r)) {
logw("输入失败: " + r);
return;
}
randSleep(1, 2);
// 步骤五:点击提交按钮
r = usbHidEvent.clickPoint(585, 1600);
if (!_ok(r)) {
logw("提交失败: " + r);
return;
}
randSleep(2, 4);
// 步骤六:回到桌面
usbHidEvent.systemKey("home");
usbHidEvent.sessionStop();
logd("流程执行完成");
}
main();
几点说明:
- 每步都判断返回值:一旦某一步失败,后面的操作就是在错误的前提下继续,可能导致更糟的结果。早失败早退出,日志也更好定位。
- 等待时间要随机:固定间隔本身就是机器特征。真人操作时,看页面可能花 2 秒,也可能花 10 秒。用随机函数包一层,能显著降低被识别的概率。
- 坐标靠截图量:用投屏界面截图量,量出来的值可以直接用。别照搬别人脚本里的数字,机型不同坐标系就不同。
八、踩坑记录
这些都是我在实际使用中踩过的,按出现频率排序。
坐标整体偏移
最常见的原因是没调 setScreenSize,或者横竖屏切换之后忘了重新调。
还有一个隐蔽原因是设备型号不统一。不同机型分辨率不同,一套坐标在 iPhone 11 上是对的,换到 iPhone 12 就偏了。批量场景下建议用同型号设备。
设备频繁掉线
按这个顺序排查:数据线(换原装线)、USB 口供电(换主板直出的口)、集线器(用带独立电源的)、设备数量(单个集线器负载不超过 7 到 8 台)。
粘贴英文时多出空格
这是 iOS 的键盘设置问题。到手机的设置 → 通用 → 键盘,关闭“智能标点”,问题就消失了。
getClipboard 有已知限制
这个函数经 CoreDevice 直读,setClipboard 写入之后马上读通常没问题。
但如果读的是手机上手动长按复制的内容,在部分 iOS 版本(比如 26.x)上会超时甚至读不到,严重时会把设备的剪贴板服务卡住,需要重启手机才能恢复。
所以不要依赖它去读取人手复制的内容。 目前更稳的做法是用快捷指令绕,或者干脆改成让脚本自己写入再读。
系统半夜自动更新导致脚本失效
跑自动化的设备一定要关掉自动更新。一夜之间系统升级,第二天可能整套流程都跑不起来。这个设置是必须做的,不是可选项。
九、三种 HID 方案怎么选
最后回到选型问题。
USB HID 适合快速验证和单机场景。一根线就能跑,不需要额外硬件。缺点是必须连着电脑。
蓝牙 BLE HID 需要一块 ESP32C3 开发板,几十块钱。它的独特优势是可以配合无自动化截图模式,整个链路不走屏幕镜像。这个特性对有风控压力的场景很重要,代价是投屏帧率不高、配置更繁琐。
OTG HID 用 ESP32S3 开发板直连手机,可以脱离电脑运行,适合需要独立部署的场景。
如果是先用起来,建议从 USB HID 入门,跑通了再考虑按需升级。
后两条链路的文档在官方站上都有:蓝牙 BLE 看 https://ieasyclick.com/iosdocs/funcs/ble-event-api ,OTG HID 看 https://ieasyclick.com/iosdocs/funcs/otg-event-api 。配置步骤也各有一篇教程,搜「蓝牙 BLE 教程」「OTG 教程」就能找到。
三条路线怎么取舍,站内有一篇专门做的横向对比,看那张表比看文字快:三条 HID 路线横向对比。
十、小结
梳理一下这篇的几个要点:
USB HID 的核心思路是让电脑冒充 HID 设备,通过系统输入通道注入触摸事件,iOS 无法区分真假,所以不需要越狱也不需要签名。
这也让它成了苹果群控里最省事的一条免越狱链路:不用买蓝牙板或 OTG 板,一根数据线就能跑通,脚本还能在多台设备上复用。
API 层面就记住三件事:会话要先开,屏幕尺寸要设对,返回值 null 或空字符串表示成功。
排查问题的顺序基本固定:先看物理连接,再看参数设置,最后才怀疑代码逻辑。
参考文档
文中涉及的接口和配置,官方文档都有完整说明:
- EasyClick iOS USB 版总文档:https://ieasyclick.com/iosdocs/
- USB HID 函数手册(
usbHidEvent):https://ieasyclick.com/iosdocs/funcs/usb-hid-event-api/ - USB HID 开放接口(HTTP,含多语言示例):https://ieasyclick.com/iosdocs/advance/openapi/usbhid
- 蓝牙 BLE 函数:https://ieasyclick.com/iosdocs/funcs/ble-event-api
- OTG HID 函数:https://ieasyclick.com/iosdocs/funcs/otg-event-api
- 环境与设备授权:https://ieasyclick.com/iosdocs/advance/ai-agent/prerequisites
- 苹果群控系统(多机批量管理):https://ieasyclick.com/apple_qunkong/
站内也写过几篇相关的,视角不同,可以对着看:
- USB HID 如何重塑苹果群控格局:讲这条链路带来的格局变化,不是代码层面的
- 三条 HID 路线横向对比:USB、蓝牙、OTG 的成本与能力边界
- 蓝牙 HID 免签名方案 和 OTG HID 配置:另两条链路的落地细节
本文基于 EasyClick iOS USB 版开发文档整理,代码片段可直接在中控脚本编辑器中运行。
关于 EasyClick:手机自动化 AI 智能体平台,覆盖安卓免 root、iOS 免越狱、鸿蒙 Next 三大生态,提供脚本开发、苹果群控、本地中控投屏与云控系统。→ 了解全部产品
想要真实跑起来?
本文介绍的方案均可在 EasyClick 手机自动化平台落地。官网提供完整文档、开发工具与群控云控产品,免费体验。