在当前移动应用安全机制日益严密的背景下,传统的自动化脚本若依赖 Android 系统的无障碍服务(Accessibility Service)进行界面操作,极易被各类 App 的风控系统识别并拦截。这是因为无障碍服务具有明显的特征:它需要用户主动授权、会在系统中留下可检测的日志记录,并且其操作行为(如快速遍历控件、无真实触摸轨迹等)与人类自然交互存在显著差异。尤其在金融、社交、游戏等对安全高度敏感的应用场景中,一旦检测到无障碍服务被启用或调用异常,系统往往会直接限制功能、弹出验证甚至封禁账号。
为有效规避此类风控机制,越来越多的自动化方案转向更底层、更仿真的输入模拟技术——其中,USB HID(Human Interface Device)技术成为一种极具潜力的替代路径。USB HID 是一种标准的人机接口协议,常用于键盘、鼠标等外设。通过将自动化设备(如树莓派、单片机开发板等)配置为 USB HID 设备,并以 OTG 方式连接至目标手机,即可在操作系统内核层面模拟真实的物理输入事件。由于这些操作不经过 Android 应用层,也不依赖 Accessibility API,因此不会触发与无障碍相关的权限检查或行为分析,极大提升了隐蔽性。
更重要的是,USB HID 产生的点击、滑动、按键等信号,在系统看来与用户手动操作几乎无异,具备完整的触摸坐标、时间戳和事件序列,难以被上层应用区分。此外,该方案无需 root 权限(部分机型需开启“USB 调试”或“HID 支持”),部署相对稳定,响应延迟低。尽管其硬件成本略高于纯软件方案,且需物理连接,但在对抗高强度风控、保障长期运行稳定性方面优势显著。因此,采用 USB HID 技术实现自动化,不仅有效绕开了基于无障碍服务的检测逻辑,也为高安全环境下的智能辅助和批量操作提供了更可靠的技术路径。
在冰狐平台中可通过内置的 usb 对象与 USB HID 硬件设备(如 ESP32-S3、CH552 等)通信,实现底层输入模拟,从而完成自动化操作。该方案不依赖 Android 无障碍服务,能有效规避多数 App 的风控检测。
一、准备工作
- 硬件要求:
- 支持 USB HID 协议的主控设备(如 ESP32-S3 开发板),并已烧录兼容冰狐通信协议的固件。
- 手机设置:
- 手机需支持 OTG 功能(在“设置”中搜索 “OTG” 并开启);
- 使用 OTG 数据线将 USB HID 设备连接至手机;
- 首次连接时,系统可能弹出“允许此设备使用 USB”的提示,需点击“确定”。
- 冰狐环境:
- 已安装最新版冰狐 App 并登录账号;
- 脚本运行在具备 USB 权限的环境中(通常首次调用会自动申请)。
二、核心 API(来自官方文档)
冰狐提供以下 usb 对象方法:
| 方法 | 功能说明 |
|---|---|
usb.connect() | 连接 USB HID 设备,返回 1 表示成功 |
usb.send(data) | 发送原始字节数据(数组或字符串) |
usb.receive([timeout]) | 接收设备返回数据,默认超时 10 秒 |
usb.click(x, y) | 模拟屏幕点击(像素坐标) |
usb.swipe(x1, y1, x2, y2, [duration]) | 模拟滑动操作 |
usb.home() / usb.back() / usb.recentApps() | 模拟系统按键 |
usb.mouseMove(x, y) / usb.mouseUp() | 模拟鼠标移动与释放 |
注:所有坐标基于目标手机屏幕分辨率(如 1080×2400),建议使用全局变量
rsScreenWidth和rsScreenHeight动态适配。
三、使用步骤与完整示例脚本
步骤 1:初始化并连接设备
function main() {
// 尝试连接 USB HID 设备
var connected = usb.connect();
if (connected !== 1) {
console.error('USB 连接失败,错误码:', connected);
return;
}
console.log('USB HID 设备连接成功');
// 初始化屏幕分辨率(必须!)
usbInit(rsScreenWidth, rsScreenHeight);
// 执行自动化任务
runAutomation();
}
步骤 2:初始化 HID 设备(关键!)
function usbInit(screenWidth, screenHeight) {
// 向设备发送屏幕分辨率信息(协议规定格式)
// 命令头 0x0E + 宽度(2字节) + 高度(2字节)
usb.send([0x0E].concat(int2bytes(screenWidth, 2), int2bytes(screenHeight, 2)));
sleep(3000); // 等待设备配置完成
}
步骤 3:执行自动化操作
function runAutomation() {
// 返回桌面
usb.home();
sleep(1000);
// 点击某个应用图标(假设位于屏幕中央偏下)
usb.click(540, 2000); // 以 1080×2400 为例
sleep(2000);
// 向上滑动页面
usb.swipe(500, 1800, 500, 600, 800); // 持续 800ms
sleep(1000);
// 点击“确认”按钮
usb.click(600, 1900);
sleep(500);
// 返回上一页
usb.back();
}
四、注意事项
- 必须初始化分辨率:HID 设备需知道屏幕尺寸才能正确映射坐标,否则点击位置会偏移。
- 坐标单位为像素:务必根据实际设备分辨率调整,避免硬编码。
- 适当添加延时:USB 指令执行需要时间,关键操作后应使用
sleep()防止过快。 - 无需无障碍权限:此方案完全绕过 Accessibility,适用于强风控场景。
-
硬件兼容性:确保 HID 固件遵循冰狐定义的通信协议(命令码如
0x09=移动、0x0A=释放等)。
五、实战例子
下面给出使用usb hid实现携程自动化脚本的例子,仅供参考
function main() {
requestScreenShot()
var b = usb.connect();
console.log('connect:', b);
if (b) {
usbInit(rsScreenWidth, rsScreenHeight)
launchApp('ctrip.android.view')
var enterApp = false
var startTime = new Date().getTime();
while (new Date().getTime() - startTime < 30 * 1000) {
var ret = ocrFindView('txt:消息|txt:我的')
if (ret.length > 0) {
console.log('进入app')
enterApp = true
break;
} else {
sleep(1000)
}
}
if (!enterApp) {
console.log('进入app失败')
return;
}
var id = 0
var name = '万季酒店(扬州东关街瘦西湖店)'
var year1 = 2025, month1 = 10, day1 = 25
var year2 = 2025, month2 = 10, day2 = 26
while (true) {
var r = callMicroService('xiecheng_get_task', [rsDeviceName])
console.log('r:', r)
if (r.length > 0) {
var success = false;
id = r.id
name = r.name
year1 = r.year1
month1 = r.month1
day1 = r.day1
year2 = r.year2
month2 = r.month2
day2 = r.day2
var tIndex = 0, enter = false;
while (tIndex < 3 && !enter) {
var ret = ocrFindView('txt:旅游地图|txt:搜索')
console.log('ret', ret)
if (ret.length > 0) {
if (ret.tag == '旅游地图') {
var x = parseInt(rsScreenWidth / 2)
var y = ret.region[1] + tIndex * 10
console.log('点击x:', x, ' y:', y)
usbClick(x, y)
sleep(1000)
var ret = ocrFindView('txt:搜索')
console.log('ret:', ret)
if (ret.length > 0) {
console.log('进入了搜索页面')
enter = true
} else {
console.log('没进入搜索页面')
}
} else {
console.log('直接找到了哈')
enter = true
}
} else {
console.log('没找到旅游地图')
}
++tIndex
}
if (enter) {
// var name = '北京西站希尔顿惠庭酒店'
console.log('进入搜索页面,输入文本')
ime.inputText(name)
var ret = ocrFindView('txt:搜索')
if (ret.length > 0) {
console.log('找到搜索')
usbClick(parseInt(ret.region[0] + ret.region[2] / 2), parseInt(ret.region[1] + ret.region[3] / 2))
sleep(3000)
var r = ret.region
var nn = name.substring(0, 4)
var enter = false, index = 0;
while (!enter && index < 3) {
var ret = ocrFindView('txt*:' + nn, {debug: true, region:[0, r[1] + r[3], rsScreenWidth, rsScreenHeight / 2]})
if (ret.length > 0) {
console.log('点击酒店名')
usbClick(parseInt(ret.region[0] + ret.region[2] / 2), parseInt(ret.region[1] + 100))
var ret = ocrFindView('txt:免费停车|txt:充电柱|txt:洗衣房|txt:景点', {debug: true})
if (ret.length > 0) {
if (ret.tag == '景点') {
console.log('没有点击到')
} else {
console.log('进入了酒店页面了:' + ret.tag)
enter = true
}
} else {
console.log('进入了其他页面,退出')
break
}
}
++index;
}
if (enter) {
console.log('进入了酒店页面xxxx')
var ret = ocrFindView('txt*:月@txt*:日')
if (ret.length > 0) {
console.log('找到日期')
usbClick(parseInt(ret.region[0] + ret.region[2] / 2), parseInt(ret.region[1] + 20))
if (ocrFindView('txt*:日历').length > 0) {
console.log('进入日历')
selectDate(year1, month1, day1, 1)
if (month1 == month2) {
selectDate(year2, month2, day2, 1)
} else {
selectDate(year2, month2, day2, 2)
}
} else {
console.log('进入日历失败')
}
var count = randInt(3, 6)
while (count > 0) {
var x1 = randInt(100, 200), x2 = randInt(400, 500)
var y1 = randInt(parseInt(rsScreenHeight / 2 + 100), rsScreenHeight - 200), y2 = randInt(200, parseInt(rsScreenHeight / 2 - 100))
console.log(`x1:${x1}, y1:${y1},x2:${x2}, y2:${y2}`)
usbSwipe(x1, y1, x2, y2)
sleep(2000);
--count;
}
var r = callMicroService('xiecheng_update_status', [id, 2])
console.log('通知微服务已经完成:', r)
success = true;
} else {
console.log('没找到日期')
}
// console.log('r:', ret)
// console.log(ocr())
} else {
console.log('没进入酒店')
}
} else {
console.log('没找到搜索')
}
} else {
console.log('没进入搜索界面')
}
if (!success) {
var r = callMicroService('xiecheng_update_status', [id, -1])
console.log('通知微服务任务失败:', r)
}
var r = ocrBack2Page('txt:消息|txt:我的', {backMode: 3})
console.log('返回首页:', r)
} else {
console.log('没有获取到任务')
sleep(2000)
}
}
}
console.log('end')
}
function selectDate(year, month, day, SK) {
var d = new Date()
d.setYear(year)
d.setMonth(month - 1)
d.setDate(day)
var dd = d.getDay(); // 0位周日
console.log('dd:', dd)
var arr = [day]
var i = dd - 1, k = day - 1;
while (i >= 0) {
if (k > 0) {
arr.push(k)
} else {
break;
}
--i;
--k;
}
i = dd + 1, k = day + 1
while (i <= 6) {
if (k <= 31) {
arr.push(k)
} else {
break;
}
++i;
++k;
}
console.log('arr:', arr)
var vv = []
var sk = 0;
var ret = ocr();
for (var item of ret) {
var text = item.text
if (text.includes('年') || text.includes('月')) {
console.log('ss:', text);
++sk
}
if (sk == SK) {
// 只计算合适范围内的
var x = parseInt(text.trim())
if (null != x) {
if (arr.includes(x)) {
console.log('text:', text, ' top:', item.region[1])
vv.push(item.region[1])
}
}
} else if (sk > SK) {
break;
}
}
console.log('vv1', vv)
if (vv.length >= 3) {
// 去掉一个最大值和最小值
vv.sort()
vv.shift()
vv.pop()
console.log('vv2', vv)
}
var tt = parseInt(vv.sum() / vv.length)
var ll = parseInt((rsScreenWidth / 7) * dd)
console.log('ll:', ll, 'tt:', tt)
usbClick(ll + 30, tt + 30)
}
function usbInit(screenWidth, screenHeight) {
usb.send([0xe].concat(int2bytes(screenWidth, 2), int2bytes(screenHeight, 2)))
sleep(3000)
}
function usbClick(x, y, duration) {
if (null == duration) {
duration = 200
}
usbMouseMove(x, y);
sleep(duration);
usbMouseUp(x, y);
}
function usbMouseMove(x, y) {
usb.send([0x9].concat(int2bytes(x, 2), int2bytes(y, 2)))
}
function usbMouseUp(x, y) {
usb.send([0xa].concat(int2bytes(x, 2), int2bytes(y, 2)))
}
function usbSwipe(x1, y1, x2, y2, duration) {
if (null == duration) {
duration = 300
}
var moveCount = randInt(6, 20)
var dTime = parseInt(duration / moveCount);
var startX = 0, startY = 0, endX = 0, endY = 0, useCurve = false;
if (x1 != x2 && y1 != y2) {
if (x1 < x2 && y1 > y2) {
startX = x1;
startY = y1;
endX = x2;
endY = y2;
useCurve = true;
} else if (x1 > x2 && y1 < y2) {
startX = x2;
startY = y2;
endX = x1;
endY = y1;
useCurve = true;
}
}
if (useCurve) {
var x = endX - startX;
var y = startY - endY;
var a = (endX - startX) / ((endY - startY) * (endY - startY));
var yb = y1;
var ya = (y2 - yb) / (moveCount * moveCount);
for (var i = 1; i < moveCount; ++i) {
var fY = (ya * i * i + yb);
var y = parseInt(fY);
var x = parseInt(a * (y - startY) * (y - startY) + startX);
if (i > 1 && i < moveCount - 1) {
x += randInt(0, 5) - 2;
y += randInt(0, 5) - 2;
}
usbMouseMove(x, y)
sleep(dTime);
}
} else {
usbMouseMove(x1, y1)
var a = (x1 - x2) / (y1 - y2);
var b = x1 - a * y1;
var yb = y1;
var ya = (y2 - yb) / (moveCount * moveCount);
for (var i = 1; i < moveCount; ++i) {
var fY = (ya * i * i + yb);
var y = parseInt(fY);
var x = parseInt((a * y + b));
if (i > 1 && i < moveCount - 1) {
x += randInt(0, 5) - 2;
y += randInt(0, 5) - 2;
}
usbMouseMove(x, y)
sleep(dTime);
}
}
usbMouseMove(x2, y2)
sleep(10);
usbMouseUp(x2, y2)
}
function usbHome() {
usb.send([0x20])
}
function usbBack() {
usb.send([0xd])
}
function usbRecentApps() {
usb.send([0x8, 0x4, 0x2b])
}
function usbCopy() {
usb.send([0x08, 0x01, 0x06])
}
function usbPaste() {
usb.send([0x08, 0x01, 0x19])
}
function usbCut() {
usb.send([0x08, 0x01, 0x1B])
}
function usbSelectAll() {
usb.send([0x08, 0x01, 0x04])
}

480

被折叠的 条评论
为什么被折叠?



