前言
LuaTouch 是运行在 iOS 设备上的 Lua 自动化引擎。内核是一个 root 守护进程(luatouchd),通过悬浮球菜单点「开始」后,引擎加载工作目录下的 scripts/main.lua 并执行;脚本中调用的 API 由引擎映射到内置的 Objective-C/C 实现(应用控制、界面自动化、图色识别等)。
本文档覆盖 LuaTouch 的全部脚本 API,按模块划分:script / app / device / UI / 图色 …。所有 API 均为 全局函数(无模块前缀),在脚本任意位置直接调用即可,无需 require。
脚本约定
- 脚本入口固定为
scripts/main.lua,由悬浮球「开始」或 HTTP 远程启动触发;restartScript()可在脚本内重启自身 - 日志用
print()输出,写入引擎日志目录,可在 App 内或logs/目录查看 - 脚本运行在独立工作线程,涉及 UI 的操作引擎内部已自动回主线程执行,脚本层无需关心线程问题
- API 标注 已支持iOS 已支持 表示 iOS 端已实现;个别函数有平台限制的会在正文单独说明
- 脚本包(zip)内
res/、config/、libs/、scripts/四个目录的文件会被installScript()解压到工作目录对应位置
建议先通读 script 模块(脚本控制与启动模式),再按需查阅各功能模块。
用途声明:LuaTouch 仅供个人设备自动化学习研究使用。请遵守您所在地区的法律法规,勿将本工具用于任何违反法律法规或侵犯第三方权益的用途;因违规使用产生的一切责任由使用者自行承担。
下载项
LuaTouch 相关资源的下载入口(内容待补充)。
script 模块(脚本控制)
脚本自身的退出与重启控制(集合 scrcpy lua 全局变量实现:exitScript / restartScript)。
print(...) 已支持iOS 已支持
打印日志(输出到引擎/Lua 日志文件,用于调试)。
- 参数:任意多个值(字符串/数字/布尔等)
- 返回:无
print("hello", 123, true) -- 日志输出
wsLog(...) 已支持iOS 已支持
向中控推送日志({"type":"log"} 消息,中控 UI 实时可见);同时写入本地引擎日志。用法与 print 一致,多参数以 tab 分隔。需引擎 v1.10.1016+。
- 参数:任意多个值(字符串/数字/布尔等)
- 返回:
boolean—true=已推送到中控;false=中控未连接(不报错,仅写本地日志)
wsLog("开始找图", pageNum) -- 中控实时可见
local ok = wsLog("步骤1完成") -- ok=true 已推送 / false 中控未连接
sleep(ms) 已支持iOS 已支持
休眠指定毫秒数(暂停/停止脚本时立即响应,不等满时长)。
- 参数:
ms{int} 休眠毫秒数 - 返回:无
sleep(1000) -- 等 1 秒
LUA_CODE 已支持iOS 已支持
全局配置集合变量:主 App 启动引擎时把控制面板参数集合(pass 字段,key 用
Pass_ 前缀)作为 JSON 字符串传入,引擎开启脚本时写入全局 LUA_CODE。
- 类型:
string(JSON 字符串),全局只读,脚本内直接读取 - 内容:控制面板全部参数,key 为
Pass_前缀,与 NSUserDefaults 一致 - 来源:主 App 控制面板输入框(引擎模式/账号/密码/序号/设备编号/luacode)→ 重启引擎生效
- 取值:用
jsonToTable()解析成表后按 key 取字段
-- LUA_CODE 内容示例:
-- {"Pass_mode":"root","Pass_account":"test","Pass_password":"test",
-- "Pass_seq":"1","Pass_device":"cs1","Pass_luacode":""}
-- 解析成表取字段(用于账号/授权校验、设备绑定等)
local cfg = jsonToTable(LUA_CODE)
if cfg and cfg.Pass_account == "test" then
print("账号匹配: " .. cfg.Pass_account)
end
exitScript() 已支持iOS 已支持
终止当前脚本(不重启)。调用后脚本立即停止执行。
- 参数:无
- 返回:无
exitScript() -- 立即退出脚本
restartScript() 已支持iOS 已支持
终止当前脚本并重新执行同一脚本文件(从头开始)。
- 参数:无
- 返回:无
restartScript() -- 重启脚本
installScript(zipPath) 已支持iOS 已支持
解压脚本项目包到工作目录:zip 内的 res/、config/、libs/、scripts/ 四个目录
分别覆盖到工作目录对应位置(等同安装一个脚本包;zip-slip 防护)。
- 参数:
zipPath— 脚本包 zip 的完整路径(如/var/mobile/Media/com.luatouch.app/models/script.zip) - 返回:
boolean是否解压成功
local ok = installScript("/var/mobile/Media/com.luatouch.app/models/script.zip")
-- zip 内 res/ → 工作目录 res/
-- config/ → 工作目录 config/
-- libs/ → 工作目录 libs/
-- scripts/→ 工作目录 scripts/
if ok then print("install ok") end
loadEncryptedFile(path) 已支持iOS 已支持
读取并立即执行一个加密脚本文件(.luc):读文件 → 解密 → 以「文件路径」为名称加载该 chunk → 直接运行。脚本内容按全局作用域执行(不是 require 那种返回模块表的方式),调用方拿不到脚本的返回值。
path{string} 加密脚本(.luc)的完整路径- 返回:
boolean, errMsg— 执行成功返回true, nil;解密失败返回false, "decrypt failed (rc)";脚本内部运行时报错返回false, err(错误信息含脚本行号)
解密得到的明文字节在执行前会清零后释放(不在堆内存残留,防内存 dump 还原脚本)。
local ok, err = loadEncryptedFile("/var/mobile/Media/com.luatouch.app/scripts/main.luc")
if not ok then print("加载失败:", err) end
getVerifyUuid() 已支持iOS 已支持
读取客户端验证结果 uuid。引擎启动后后台线程按「上传 updateqfkjuuid → 校验 accessqfkjuuid」轮询(最多 5 轮),验证通过才写入该值。
- 参数:无
- 返回:
string验证 uuid;未验证通过时返回空字符串""(不是nil)
用法是「非空才放行业务」。注意:即使已成功过一次,引擎仍会周期性重新验证,重新验证期间该值可能短暂变空,脚本不要把它当成“一次通过就永久有效”。
-- 等验证通过再进入业务逻辑
while getVerifyUuid() == "" do
sleep(1000)
end
print("已授权:", getVerifyUuid())
executeShellCommand(cmd) iOS 不支持
执行系统 Shell 命令。iOS 未实现:函数保留(保证同一份脚本能在各平台直接调用,不会因函数不存在而报错),但内部是空实现——不执行任何命令,恒返回 nil,传入的参数被完全忽略。
cmd{string} 命令字符串(iOS 下被忽略)- 返回:
nil(恒为 nil)
local out = executeShellCommand("ls /") -- iOS 恒为 nil
getStartMode() 已支持iOS 已支持
获取当前启动模式(START_MODE,int),由各启动入口在启动时设置:悬浮球点「开始」= 0、HTTP 10010 远程启动 = 1(可扩展其他模式);restartScript 重跑不重置(保留当前值)。
- 参数:无
- 返回:
int当前 START_MODE 值(当前版本恒为0,预留字段)
local mode = getStartMode()
print("START_MODE =", mode)
Thread.newThread(callback) 已支持iOS 已支持
创建子线程并立即开始执行 callback(独立 pthread + 独立 lua_State,与主脚本
真并行)。返回 thread userdata 用于后续 stopThread() 控制。
- 参数:
callback{function} 子线程入口函数 - 返回:
threaduserdata;创建失败返回nil+ 错误消息
数据共享(globalSet / globalGet):每个线程(含主线程)的全局变量默认本线程私有 (标准 Lua 语义,线程之间互不可见)。跨线程共享走 C 层共享区,就两个公开 API:
globalSet("名", 值)—— 发布/覆盖:把值写进共享区。支持路径形式globalSet("cfg", "ip", "1.2.3.4")(= 共享区的cfg.ip,多层路径依次传分量); 值传nil= 删除该路径。表/函数都能发(表逐字段入库,函数以字节码跨线程加载,取回可直接调用)globalGet("名")/globalGet("cfg", "ip")—— 读取:返回共享区里的 深拷贝实体(表可正常 pairs / 索引 /#,每次读取是独立副本,改它不影响共享区); 不存在返回nil- 快照语义(最重要):发布是"拷一份进共享区"——发布之后本线程再改自己的变量,
共享区不会自动跟着变,要让别人看到必须重新
globalSet一次
重要约束:
- 回调不能依赖闭包 upvalue(子线程通过字节码加载回调,upvalue 不跨线程传递);
数据一律用
globalSet / globalGet传 - 子线程里先
globalGet取到本地再循环使用,要最新值时再取一次 (别在循环里反复取大表,每次都是深拷贝) - 子线程不响应主脚本的暂停/恢复;只有
stopThread()或主脚本结束/停止时强停
-- 主脚本:写全局变量,子线程直接读(无需传参)
config = { count = 0, stop = false }
-- 子线程:每 1 秒打印一次,读主脚本写的全局 config
function worker()
local i = 0
while true do
i = i + 1
print("worker tick " .. i .. " count=" .. tostring(config.count))
if config.stop then break end
sleep(1000)
end
print("worker exit")
end
local t = Thread.newThread(worker)
-- 主脚本继续干别的事...
sleep(5000)
config.stop = true -- 子线程能立即读到(C 层树形存储主/子线程互通)
thread:stopThread() 已支持iOS 已支持
强制终止该子线程(置独立停止标志,线程内 sleep 立即响应,hook 强停)。只影响该线程, 不影响主脚本与其他子线程。主脚本结束/停止时引擎会自动强停所有子线程,无需手动清理。
- 参数:无(
thread为Thread.newThread的返回值) - 返回:无
local t = Thread.newThread(worker)
sleep(3000)
t:stopThread() -- 单独停掉 worker,主脚本继续跑
app 模块
appCurrentPackage() 已支持iOS 已支持
获取当前前台应用的 Bundle Identifier。
- 参数:无
- 返回:
string前台应用的 Bundle ID;无前台应用时返回nil
-- 获取当前前台 App 的包名
local bid = appCurrentPackage()
print(bid) -- 例如 "com.apple.mobilesafari"
appLaunch(packageName) 已支持iOS 已支持
通过 Bundle Identifier 启动应用。
packageName{string} 应用 Bundle ID- 返回:
bool是否启动成功
local ok = appLaunch("com.apple.mobilesafari")
if ok then print("已启动") end
appForceStop(packageName) 已支持iOS 已支持
关闭指定应用(系统级终止进程)。
packageName{string} 应用 Bundle ID- 返回:
bool是否成功终止
appForceStop("com.example.app")
appGetList(includeSystemApps) 已支持iOS 已支持
获取已安装应用列表。
includeSystemApps{bool} 是否包含系统应用- 返回:
table应用信息数组,每个元素是下表结构
| 字段名 | 类型 | 说明 |
|---|---|---|
PackageName | string | 应用的 Bundle Identifier |
AppName | string | 应用显示名称 |
VersionName | string | 版本号(CFBundleShortVersionString) |
VersionCode | string | 构建号(CFBundleVersion) |
IsSystemApp | bool | 是否为系统应用 |
local list = appGetList(true)
for _, a in ipairs(list) do
print(a.PackageName, a.AppName, a.VersionName)
end
appGetBundlePath(packageName) 已支持iOS 已支持
获取应用安装包(.app)路径。
packageName{string} 应用 Bundle ID- 返回:
string路径;未找到返回nil
local path = appGetBundlePath("com.auto.go")
appGetDataPath(packageName) 已支持iOS 已支持
获取应用数据目录路径。
packageName{string} 应用 Bundle ID- 返回:
string数据目录路径;未找到返回nil
local data = appGetDataPath("com.auto.go")
appSelfPackage() 已支持iOS 已支持
获取当前脚本/宿主自身的包名。
- 参数:无
- 返回:
string当前 Bundle ID;获取失败返回nil
local pkg = appSelfPackage()
appGetVersion(packageName) 已支持iOS 已支持
通过 Bundle ID 获取应用版本号(CFBundleShortVersionString)。
packageName{string} 应用 Bundle ID- 返回:
string版本号;未找到返回nil
local ver = appGetVersion("com.tencent.xin")
appUninstall(packageName) 已支持iOS 已支持
卸载应用。
packageName{string} 应用 Bundle ID- 返回:
bool是否成功
appUninstall("com.example.app")
appInstall(path) 已支持iOS 已支持
后台静默安装 IPA 文件。
path{string} IPA 文件路径- 返回:
bool是否成功
appInstall("/var/mobile/Documents/app.ipa")
appClear(packageName) 已支持iOS 已支持
清除应用数据(删除数据容器内容)。
packageName{string} 应用 Bundle ID- 返回:
bool是否成功
appClear("com.example.app")
appOpenUrl(url) 已支持iOS 已支持
使用系统默认方式打开 URL。若未带 http:// 或 https:// 前缀,会自动补上 http://。
url{string} 网址或 URL Scheme- 返回:
bool是否成功
appOpenUrl("https://example.com")
appGetPackageName() 已支持iOS 已支持
获取宿主 App(LuaTouch)包名。与 appSelfPackage() 的区别:
appSelfPackage() 返回引擎宿主(com.luatouch.core),本函数返回主 App(com.luatouch.app)。
- 参数:无
- 返回:
string主 App 包名(com.luatouch.app)
print(appGetPackageName()) -- com.luatouch.app
print(appSelfPackage()) -- com.luatouch.core
device 模块
deviceGet() 已支持iOS 已支持
一次获取设备信息(本引擎便利封装)。只含「变量 + 下方函数拿不到」的信息;内存用 deviceGetTotalMem、屏幕用 deviceGetDisplayInfo。
- 参数:无
- 返回:
table,字段见下表(取不到时字段缺失)
| 字段 | 类型 | 说明 |
|---|---|---|
Model | string | 设备机型标识(如 iPhone12,1) |
Release | string | 系统版本(如 "14.6") |
Serial | string | 设备序列号 |
Build | string | 系统构建号(如 18F72) |
SystemName | string | 系统名称(如 "iOS") |
DeviceName | string | 用户设置的设备名 |
Arch | string | CPU 架构(如 arm64) |
Is64Bit | bool | 是否 64 位 |
CpuCount | int | CPU 核数 |
local d = deviceGet()
print(d.Model, d.Release, d.Serial)
print(d.Arch, d.Is64Bit, d.CpuCount)
deviceGetDisplayInfo() 已支持iOS 已支持
获取主屏幕的分辨率信息(多返回值)。
- 参数:无
- 返回:
width{int} 逻辑宽度,height{int} 逻辑高度,scale{number} 缩放因子,rotation{int} 旋转角度(0/90/180/270)
local w, h, scale, rot = deviceGetDisplayInfo()
print(w, h, scale, rot) -- 例如 390 844 2.0 0
deviceVpnStatus() 已支持iOS 已支持
获取 VPN 状态(存在 utun 网络接口即视为开启)。
- 参数:无
- 返回:
bool
if deviceVpnStatus() then
print("VPN 已开启")
end
deviceSetVPNEnabled(enabled) iOS 不支持
设置 VPN 开关状态。iOS 平台限制,恒返回 false。
enabled{bool} 是否打开- 返回:
false
deviceSetVPNEnabled(true) -- 恒 false
deviceSetWiFiEnabled(enabled) iOS 不支持
设置 WiFi 开关状态。iOS 平台限制,恒返回 false。
enabled{bool} 是否打开- 返回:
false
deviceSetWiFiEnabled(true) -- 恒 false
deviceGetBattery() 已支持iOS 已支持
获取当前电量百分比。
- 参数:无
- 返回:
int0~100;读取失败返回-1
local level = deviceGetBattery()
print("电量 " .. level .. "%")
deviceGetBatteryStatus() 已支持iOS 已支持
获取电池状态。
- 参数:无
- 返回:
int0未知,1未充电,2充电中,3已充满
local st = deviceGetBatteryStatus()
if st == 2 then print("充电中") end
deviceIsScreenOn() 已支持iOS 已支持
判断屏幕是否点亮(未锁屏即视为点亮)。
- 参数:无
- 返回:
bool
if deviceIsScreenOn() then print("屏幕亮着") end
deviceIsScreenUnlock() 已支持iOS 已支持
判断屏幕是否已解锁(未处于锁屏状态)。
- 参数:无
- 返回:
bool
if deviceIsScreenUnlock() then print("已解锁") end
deviceGetBrightness() 已支持iOS 已支持
获取当前屏幕亮度。
- 参数:无
- 返回:
int0~255
local b = deviceGetBrightness()
print("亮度 " .. b)
deviceGetTotalMem() 已支持iOS 已支持
获取设备总内存。
- 参数:无
- 返回:
int总内存,单位 KB;失败返回0
print(deviceGetTotalMem() .. " KB")
deviceGetAvailMem() 已支持iOS 已支持
获取设备当前可用内存(free + inactive,host_statistics64)。
- 参数:无
- 返回:
int单位 KB;失败返回0
print(deviceGetAvailMem() .. " KB")
deviceWakeUp() 已支持iOS 已支持
唤醒设备(点亮屏幕)。
- 参数:无
- 返回:
bool是否成功调用
deviceWakeUp()
deviceKeepScreenOn() 已支持iOS 已支持
保持屏幕常亮(关闭自动锁屏)。
- 参数:无
- 返回:
bool是否成功调用
deviceKeepScreenOn()
deviceGetIp() 已支持iOS 已支持
获取设备局域网 IPv4 地址(优先私有网段,跳过自分配地址)。
- 参数:无
- 返回:
string如"192.168.31.169";取不到返回nil
local ip = deviceGetIp()
print(ip)
deviceGetWifiMac() 已支持iOS 已支持
获取 Wi‑Fi 网卡 MAC 地址(en0,回退 en1)。
- 参数:无
- 返回:
string如"aa:bb:cc:dd:ee:ff";取不到返回nil
local mac = deviceGetWifiMac()
print(mac)
deviceReboot() 已支持iOS 已支持
重启设备(root 直接 reboot(RB_AUTOBOOT),调用后设备立即重启)。
- 参数:无
- 返回:
bool(正常不会返回)
-- 慎用:立即重启设备
deviceReboot()
getPlatform() 已支持iOS 已支持
获取运行平台标识。iOS 版恒返回 "ios"(跨平台脚本兼容用,安卓版返回 "android")。
- 参数:无
- 返回:
string平台标识,iOS 上恒为"ios"
if getPlatform() == "ios" then
print("当前运行在 iOS")
end
file 模块
deleteFile(path) 已支持iOS 已支持
删除文件或空目录(目录用 rmdir,文件用 remove)。
path{string} 文件或空目录路径- 返回:
bool是否删除成功
deleteFile("/var/mobile/Documents/tmp.txt")
moveFile(src, dst) 已支持iOS 已支持
移动或重命名文件(rename)。
src{string} 源路径dst{string} 目标路径- 返回:
bool是否成功
moveFile("/tmp/a.txt", "/var/mobile/Documents/a.txt")
unzipFile(zip, dest) 已支持iOS 已支持
解压 zip 文件到目标目录。
zip{string} zip 文件路径dest{string} 解压目标目录(不存在会自动创建)- 返回:
bool是否成功
unzipFile("/var/mobile/Documents/app.zip", "/var/mobile/Documents/extract")
readFileAsBase64(path) 已支持iOS 已支持
读取文件内容并编码为 Base64(双返回值)。
path{string} 文件路径- 返回:
stringBase64 内容;失败返回nil+错误信息
local b64, err = readFileAsBase64("/tmp/img.png")
if b64 then
print("base64 length", #b64)
end
base64Encode(str) 已支持iOS 已支持
将字符串编码为 Base64(标准字母表 A-Z a-z 0-9 + /,= 填充)。
str{string} 原始内容- 返回:
stringBase64 编码结果
local b64 = base64Encode("hello")
print(b64) -- aGVsbG8=
base64Decode(str) 已支持iOS 已支持
将 Base64 字符串解码为原始内容(自动忽略换行/空白/= 填充,容错宽松)。
str{string} Base64 内容- 返回:
string解码结果
local raw = base64Decode("aGVsbG8=")
print(raw) -- hello
http 模块
httpGet(url, timeout?) 已支持iOS 已支持
HTTP GET 请求。支持 HTTP/HTTPS,默认超时 30 秒。
url{string} 完整 URL(如http://192.168.1.1:8080/api)timeout{number} 可选,超时秒数,默认 30- 返回:
intHTTP 状态码(200/404…,负值为错误码见下表)+string响应体
local code, body = httpGet("http://example.com/api")
print(code, body)
httpPost(url, data, contentType?, timeout?) 已支持iOS 已支持
HTTP POST 请求。支持 HTTP/HTTPS,默认超时 30 秒。
url{string} 完整 URLdata{string} 请求体(POST 数据)contentType{string} 可选,Content-Type;默认application/x-www-form-urlencodedtimeout{number} 可选,超时秒数,默认 30- 返回:
int状态码 +string响应体
local code, body = httpPost("http://example.com/api",
"name=test&id=1", "application/x-www-form-urlencoded")
downloadFile(url, path, timeout?) 已支持iOS 已支持
HTTP 下载文件到本地(流式写盘,父目录自动创建)。支持 HTTP/HTTPS,默认超时 60 秒。
url{string} 完整 URLpath{string} 保存路径(绝对路径)timeout{number} 可选,超时秒数,默认 60- 返回:
boolean是否成功 +string错误信息(成功为空串)
local ok, err = downloadFile("http://example.com/file.zip",
"/var/mobile/Media/com.luatouch.app/downloads/file.zip")
tableToJson(table) 已支持iOS 已支持
Lua 表序列化为 JSON 字符串。数组(连续数字键)输出数组,否则输出对象。
table{table} 要序列化的表- 返回:
stringJSON 字符串;参数非表返回错误串
local json = tableToJson({ name = "test", id = 1 })
print(json) -- {"id":1,"name":"test"}
jsonToTable(str) 已支持iOS 已支持
JSON 字符串解析为 Lua 表。
str{string} JSON 字符串- 返回:
table解析结果;解析失败返回nil
local t = jsonToTable('{"name":"test","id":1}')
print(t.name, t.id)
HTTP 错误码 说明
| 码 | 含义 |
|---|---|
| -1 | 参数错误(url 为空 / POST 无数据) |
| -2 | URL 解析失败 |
| -4 | socket 创建失败 |
| -5 | DNS 解析失败 |
| -6 | 连接失败 |
| -7 | 发送失败 |
| -8 | 无响应(超过 timeout 秒) |
ime 模块
imeInputText(text) 已支持iOS 已支持
向当前焦点控件输入文本(支持中文、英文、数字)。
text{string} 要输入的文本(支持中文/英文/数字)- 返回:
bool是否注入成功
imeInputText("Hello, World!")
imeInputText("你好世界")
imePasteText() 已支持iOS 已支持
模拟 Command+V,把剪贴板内容粘贴到当前焦点。
- 参数:无
- 返回:
bool是否注入成功
imeSetClipText("要粘贴的内容")
imePasteText()
imeGetClipText() 已支持iOS 已支持
获取剪贴板文本内容。
- 参数:无
- 返回:
string剪贴板文本;失败返回nil
local text = imeGetClipText()
print(text)
imeSetClipText(text) 已支持iOS 已支持
设置剪贴板文本内容。
text{string} 要写入剪贴板的文本- 返回:
bool是否设置成功
imeSetClipText("要复制的内容")
touch 模块
点击、滑动与按键注入。坐标均为物理像素,与 getScreenSize() 一致(本机 1242×2208,全屏中心 621, 1104)。findColor 返回的坐标可直接用于 tap,无需换算。
tap / swipe / touchDown / touchMove 支持可选触摸参数:tip 指尖压力(0~1,默认 1)、barrel 笔身压力(0~1,默认 0)、radius 触摸半径(默认 0.04)。
这些函数都是阻塞调用:注入在引擎的串行队列上执行,函数返回时注入已经完成。
tap(x, y, tip?, barrel?, radius?) 已支持iOS 已支持
在指定位置点击一下。
x{number} 横坐标(物理像素,0~屏幕宽)y{number} 纵坐标(物理像素,0~屏幕高)tip{number} 可选,指尖压力 0~1,默认 1barrel{number} 可选,笔身压力 0~1,默认 0radius{number} 可选,触摸半径,默认 0.04- 返回:无
tap(621, 1104) -- 点击屏幕中心(1242x2208)
swipe(x1, y1, x2, y2, ms?, tip?, barrel?, radius?) 已支持iOS 已支持
从起点滑动到终点(20 步插值)。
x1, y1{number} 起点坐标x2, y2{number} 终点坐标ms{int} 可选,滑动时长毫秒,默认 300(最小 50)tip{number} 可选,指尖压力 0~1,默认 1barrel{number} 可选,笔身压力 0~1,默认 0radius{number} 可选,触摸半径,默认 0.04- 返回:无
swipe(621, 1600, 621, 400, 500) -- 从下往上滑
key(name) 已支持iOS 已支持
按命名按键(按下 → 保持约 40ms → 自动抬起)。
name{string} 键名,仅支持下列 6 个:home/home2/lock(锁屏)/volup/voldown/mute- 返回:
bool是否支持该键名(true=已注入;false=键名未知,不注入任何按键)
| 键名 | 对应 HID | 说明 |
|---|---|---|
home | page 0x0C, usage 0x40 | 回到主屏(Consumer Home/Menu) |
home2 | page 0x0C, usage 0x0223 | 回主屏的另一种实现(Consumer AC Home),部分场景比 home 更有效 |
lock | page 0x0C, usage 0x30 | 锁屏 / 电源键 |
volup | page 0x0C, usage 0xE9 | 音量 + |
voldown | page 0x0C, usage 0xEA | 音量 − |
mute | page 0x0C, usage 0xE2 | 静音开关 |
key("home") -- 回到主屏
key("volup") -- 音量+
key("lock") -- 锁屏
if not key("back") then print("key 不认识 back,需要用 keyRaw") end
keyRaw(page, usage) 已支持iOS 已支持
按 HID 原始键码(按下 → 保持约 40ms → 自动抬起),用于 key() 未内置的按键。
page{int} HID 用法页(如0x0C消费类、0x07键盘)usage{int} 用法码- 返回:无
keyRaw(0x0C, 0x40) -- Consumer Home
keyMeta(name, repeat?, meta?) 已支持iOS 已支持
带修饰键的按键(键名范围同 key();修饰键只支持 Shift)。
name{string} 键名(同key)repeat{int} 可选,重复次数,默认 1meta{int} 可选,修饰键位掩码:0x1= Shift,默认 0- 返回:无
keyMeta("home", 2) -- 按 home 两次
keyMeta("home", 1, 0x1) -- Shift + home
touchDown(idx, x, y, tip?, barrel?, radius?) 已支持iOS 已支持
多指触摸:第 idx 根手指按下。手指索引范围 0~9,按下 / 移动 / 抬起必须用同一个 idx 成对出现(自己保证配对,不做手势识别)。
idx{int} 手指索引(0~9)x, y{number} 按下位置tip{number} 可选,指尖压力 0~1,默认 1barrel{number} 可选,笔身压力 0~1,默认 0radius{number} 可选,触摸半径,默认 0.04- 返回:无
touchDown(0, 100, 300) -- 第一根手指按下
touchMove(idx, x, y, tip?, barrel?, radius?) 已支持iOS 已支持
多指触摸:移动第 idx 根手指到新位置。按规范应先 touchDown 同一 idx;idx 超出 0~9 会被忽略。
idx{int} 手指索引(0~9)x, y{number} 新位置tip{number} 可选,指尖压力 0~1,默认 1barrel{number} 可选,笔身压力 0~1,默认 0radius{number} 可选,触摸半径,默认 0.04- 返回:无
touchDown(0, 200, 400)
touchMove(0, 400, 400) -- 保持按住并拖动
touchUp(0)
touchUp(idx) 已支持iOS 已支持
多指触摸:抬起第 idx 根手指(必须与对应 touchDown 的 idx 一致,抬起位置用的是按下时记录的坐标)。若该 idx 当前并未按下,则不发送任何事件、静默返回;idx 超出 0~9 同样被忽略。
idx{int} 手指索引(0~9)- 返回:无
touchUp(0) -- 第一根手指抬起
color 模块
keepCapture() / releaseCapture() 已支持iOS 已支持
图色渲染帧冻结控制:图色函数读取的是后台持续更新的缓存帧。
keepCapture():冻结当前画面,期间所有图色函数查的是同一帧releaseCapture():恢复实时画面- 参数:无;返回:无
- 用途:画面静止时连续多次图色查找(循环找点)保证同一画面
keepCapture() -- 冻结当前帧
for i = 1, 10 do
local x, y = findMultiColors(0, 0, 0, 0, "359bed-101010",
"45|76|ffffff-101010|93|114|ffffff-101010|33|146|ffffff-101010|-9|432|359bed-101010|58|447|359bed-101010",
0, 0.9)
if x ~= -1 and y ~= -1 then print(x, y) end
end
releaseCapture() -- 恢复实时画面
⚠️ 注意:每次开启脚本(悬浮球「开始」)会自动恢复,不会因上次脚本忘记释放而永久冻结;但脚本内 keep 后建议主动 release。
颜色格式 已支持iOS 已支持
颜色统一为 6 位十六进制 RGB,格式 目标-容差(分隔符 - 或 | 均可):
目标{string} 6 位目标颜色,如ffffff容差{string} 6 位容差值(R/G/B 各一字节),如323232表示每通道允许 ±0x32
findColor(0, 0, 1242, 2208, "ffffff-323232", 0, 0, 0)
-- 多点偏移组:x|y|目标-容差
detectsMultiColors("620|389|ffffff-323232,100|200|000000-101010")
getPixel(x, y) 已支持iOS 已支持
获取指定像素点的 RGB 分量(多返回值)。
x, y{int} 屏幕像素坐标- 返回:
r, g, b(0~255);越界返回nil
local r, g, b = getPixel(100, 200)
print(r, g, b)
getRGB(x, y) 已支持iOS 已支持
获取指定像素点的打包颜色值。
x, y{int} 屏幕像素坐标- 返回:
int颜色0xAARRGGBB;失败返回0
local c = getRGB(100, 200)
print(string.format("%08X", c))
getScreenSize() 已支持iOS 已支持
获取屏幕尺寸(多返回值)。
- 参数:无
- 返回:
w, h(像素);无屏幕缓存返回nil
local w, h = getScreenSize()
findColor(x1, y1, x2, y2, color, d1?, d2?, d3?) 已支持iOS 已支持
在矩形区域内查找第一个匹配颜色的点(8 方向扫描)。
x1, y1, x2, y2{int} 搜索区域(自动归一化)color{string} 目标-容差,如ffffff-323232d1, d2, d3{int} 可选,扫描方向(0/1),决定起扫边与主轴;全 0 = 左上→右下- 返回:
x, y命中坐标;未找到返回-1, -1
local x, y = findColor(0, 0, 1242, 2208, "ff0000-101010")
if x >= 0 then
print("found at", x, y)
end
isColor(x, y, color) 已支持iOS 已支持
判定指定像素点颜色是否匹配(单点快速判定:直读渲染源目标像素,不建目标面、不整帧拷贝;keepCapture 冻结期 / 50ms 去重窗口内直接读缓存帧零成本返回)。
x, y{int} 屏幕像素坐标color{string}RRGGBB或RRGGBB-容差;纯 6 位串自动按容差000000处理- 返回:
booleantrue命中 /false不命中
if isColor(102, 86, "D33422-101010") then
print("是目标颜色")
end
findMultiColors(x1, y1, x2, y2, firstColor, offsetColors, d1?, d2?, d3?) 已支持iOS 已支持
首点 + 相对偏移多点匹配(可精确识别图标/按钮)。
x1, y1, x2, y2{int} 搜索区域firstColor{string} 首点颜色(目标-容差)offsetColors{string} 偏移点组,x|y|目标-容差逗号分隔(偏移相对首点)d1, d2, d3{int} 可选,扫描方向- 返回:
x, y首点命中坐标;未找到返回-1, -1
local x, y = findMultiColors(0, 0, 1242, 2208,
"ffffff-323232",
"620|391|ffffff-323232,100|200|000000-101010")
tableFindMultiColors(x1, y1, x2, y2, firstColor, offsetColors, d1?, d2?, d3?) 已支持iOS 已支持
与 findMultiColors 同参数,但返回区域内所有匹配点的集合(而非第一个点)。
x1, y1, x2, y2{int} 搜索区域firstColor{string} 首点颜色(目标-容差)offsetColors{string} 偏移点组,x|y|目标-容差逗号分隔(偏移相对首点)d1, d2, d3{int} 可选,扫描方向- 返回:
table所有匹配点集合{{x=,y=},...};无匹配返回空表{}
local pts = tableFindMultiColors(0, 0, 1242, 2208,
"ffffff-323232",
"620|391|ffffff-323232,100|200|000000-101010")
for i, p in ipairs(pts) do
print(i, p.x, p.y)
end
getColorCountInRegion(x1, y1, x2, y2, colorStr, sim?) 已支持iOS 已支持
统计区域内符合颜色条件的像素数量。
x1, y1{int} 区域左上角坐标x2, y2{int} 区域右下角坐标;为0时取屏幕最大宽/高colorStr{string} 颜色字符串,多颜色用|分隔,每组RRGGBB或RRGGBB-容差,如FFFFFF|CCCCCC-101010sim{number} 可选,相似度 0.1~1.0(默认 0.9);未带容差的颜色用它换算容差(1-sim)*255- 返回:
int匹配像素总数(任一颜色命中即计 1)
local count = getColorCountInRegion(0, 0, 0, 0, "FFFFFF", 0.9)
print(count)
detectsMultiColors(colors) 已支持iOS 已支持
多点同屏判定:给定一组绝对坐标点,判断是否全部匹配(常用于区域校验)。
colors{string}x|y|目标-容差逗号分隔;首点可带第 4 字段extra指定偏移搜索范围(默认 0)- 返回:
bool全部匹配返回 1,否则 0
local ok = detectsMultiColors("620|389|ffffff-323232,100|200|000000-101010")
if ok == 1 then
print("all colors match")
end
screenshot(path) 已支持iOS 已支持
截取全屏并保存为 PNG 文件。
path{string} 保存路径- 返回:
bool是否保存成功
screenshot("/var/mobile/Documents/screen.png")
findPic(x1, y1, x2, y2, files, deltaColor?, sim?, dir?) 已支持 iOS 已支持
区域找图(模板匹配):在屏幕区域内查找模板图片,返回其左上角坐标。支持透明图/异形图(模板四角同色且占比 ≥30% 时该色判为背景,匹配时只比前景像素——兼容大漠涂抹异形图用法与 PNG 真透明)、每通道偏色、多模板(| 分隔,返回最先命中的)。
x1, y1, x2, y2{int} 搜索区域;x2/y2为0时取整屏,坐标写反自动交换files{string} 模板图片路径,多图用|分隔(如a.png|b.png);相对路径以脚本工作目录res/为基准;支持 png/jpgdeltaColor{string} 可选,偏色RRGGBB(如"101010"),匹配时每通道允许 ±该值;"000000"= 无偏色sim{number} 可选,相似度 0.1~1.0(默认 0.9),1.0 = 逐像素完全一致dir{int} 可选,扫描方向:0 从左上(默认)/ 1 从右上 / 2 从左下 / 3 从右下- 返回:
x, y命中模板的左上角坐标;未找到返回-1, -1
模板图可用「图色助手 → 裁剪画笔涂抹」制作:涂抹掉不要的部分(涂抹色需一致且占比 ≥30%、四角为涂抹色,工具会自动补涂)。
-- 全屏找 btn.png,相似度 0.9,无偏色
local x, y = findPic(0, 0, 0, 0, "btn.png", "000000", 0.9, 0)
if x ~= -1 then
tap(x + 20, y + 10) -- 点模板内相对位置
end
-- 多模板:返回最先命中的那个(透明图/异形图直接可用)
x, y = findPic(0, 0, 0, 0, "btn2.jpg|btn3.png", "000000", 0.9, 0)
print(x, y)
findPicEx(x1, y1, x2, y2, files, deltaColor?, sim?, dir?) 已支持 iOS 已支持
区域找图(多图全部命中):与 findPic 同参数,但返回区域内所有模板命中结果的集合(而非第一个),结果按扫描方向排序,上限 1800 个。
- 参数与
findPic完全一致 - 返回:
table每项{index=模板序号, name=文件名, x=, y=};无匹配返回空表{}
local t = findPicEx(0, 0, 0, 0, "a.png|b.png", "000000", 0.9, 0)
for i, v in ipairs(t) do
print(i, v.name, v.x, v.y)
end
-- 无匹配时 t 为空表
getColorCountInRegion(x1, y1, x2, y2, colorStr, sim?) 已支持 iOS 已支持
统计区域内符合颜色条件的像素数量。
x1, y1{int} 区域左上角坐标x2, y2{int} 区域右下角坐标;为0时取屏幕最大宽/高colorStr{string} 颜色字符串,多颜色用|分隔,每组RRGGBB或RRGGBB-容差,如FFFFFF|CCCCCC-101010sim{number} 可选,相似度 0.1~1.0(默认 0.9);未带容差的颜色用它换算容差(1-sim)*255- 返回:
int匹配像素总数(任一颜色命中即计 1)
local count = getColorCountInRegion(0, 0, 0, 0, "FFFFFF", 0.9)
print(count)
detectsMultiColors(colors) 已支持 iOS 已支持
多点同屏判定:给定一组绝对坐标点,判断是否全部匹配(常用于区域校验)。
colors{string}x|y|目标-容差逗号分隔;首点可带第 4 字段extra指定偏移搜索范围(默认 0)- 返回:
bool全部匹配返回 1,否则 0
local ok = detectsMultiColors("620|389|ffffff-323232,100|200|000000-101010")
if ok == 1 then
print("all colors match")
end
screenshot(path) 已支持 iOS 已支持
截取全屏并保存为 PNG 文件。
path{string} 保存路径- 返回:
bool是否保存成功
screenshot("/data/local/tmp/com.luatouch.app/screen.png")
opencv 模块
图像处理套件:先用 cv_copyImageToMat 把屏幕区域作为当前图像,后续处理函数都作用在当前图像上(可链式处理),最后用 cv_saveImage / cv_matToBase64 输出或 cv_cleanup 释放。
cv_copyImageToMat(x1, y1, x2, y2) 已支持iOS 已支持
截取屏幕指定区域,作为当前图像(后续处理的基础)。
x1, y1, x2, y2{int} 屏幕区域坐标- 返回:
int0 成功;负值失败
cv_copyImageToMat(0, 0, 1242, 2208)
cv_resizeImage(scale, interp?) 已支持iOS 已支持
按倍数缩放当前图像。
scale{number} 缩放倍数,如 0.5 缩小一半、2 放大两倍interp{int} 可选,插值方式:0=最近邻 1=双线性(默认) 2=双三次 3=面积 4=兰索斯- 返回:
int0 成功;负值失败
cv_resizeImage(0.5) -- 缩小一半
cv_thresholdImage(method, p1?, p2?, p3?, p4?, p5?) 已支持iOS 已支持
对当前图像做二值化/阈值处理(先转灰度)。
method{int} 处理方式:0=固定阈值 1=自适应 2=自动计算最优阈值p1{number} 方式 0 为阈值(0~255,默认 128);方式 1 为最大像素值(默认 128)p2{number} 方式 0 为最大值(0~255,默认 255);方式 1 为邻域块大小(须为大于 1 的奇数,默认 255)p3{int} 方式 0 为阈值类型(0=二值 1=反二值 2=截断 3=归零 4=反归零);方式 1 为自适应方法(0=均值 1=高斯)p4{int} 方式 1 的阈值类型(0/1),默认 0p5{number} 方式 1 的常数偏移 C,默认 0- 返回:
int0 成功;负值失败
cv_thresholdImage(0, 128, 255, 0) -- 固定阈值二值化
cv_thresholdImage(2) -- 自动阈值
cv_colorDeviationBinary("RRGGBB-RRGGBB") 已支持iOS 已支持
按颜色范围把当前图像转为二值图:目标色 ± 偏差内的像素为白,其余为黑。
RRGGBB-RRGGBB{string} 目标色-偏差,如FFFFFF-101010表示白色 ± 0x10- 返回:
int0 成功;负值失败
cv_colorDeviationBinary("FFFFFF-101010")
cv_clahe(clipLimit?) 已支持iOS 已支持
对比度受限的自适应直方图均衡,增强图像对比度。
clipLimit{number} 可选,对比度限制,默认 2.0- 返回:
int0 成功;负值失败
cv_clahe(2.0)
cv_sharpen(strength?) 已支持iOS 已支持
图像锐化,增强边缘细节。
strength{number} 可选,锐化强度,默认 1.0- 返回:
int0 成功;负值失败
cv_sharpen(1.0)
cv_morphology(op, ksize?, kshape?) 已支持iOS 已支持
形态学操作(腐蚀/膨胀等),常用于去噪、连通区域处理。
op{int} 操作:0=腐蚀 1=膨胀 2=开运算 3=闭运算 4=梯度 5=顶帽 6=黑帽ksize{int} 可选,核大小,默认 3kshape{int} 可选,核形状:0=矩形 1=十字 2=椭圆,默认 0- 返回:
int0 成功;负值失败
cv_morphology(1, 3, 0) -- 膨胀
cv_find_image_by_path(path, method?, sim?) 已支持iOS 已支持
在当前图像中查找模板图片,返回匹配位置。
path{string} 模板图片路径method{int} 可选,匹配算法:0=平方差 1=归一化平方差 2=相关 3=归一化相关 4=相关系数 5=归一化相关系数(默认)sim{number} 可选,相似度阈值 0~1,默认 0.8- 返回:
x, y匹配位置;未找到/出错时返回x, y, errMsg(x/y 为 0,errMsg 说明原因)
local x, y = cv_find_image_by_path("/var/mobile/Documents/tpl.png")
if x > 0 then
print("找到模板", x, y)
end
cv_saveImage(path) 已支持iOS 已支持
把当前图像保存为图片文件。
path{string} 保存路径(按扩展名决定格式)- 返回:
int0 成功;负值失败
cv_saveImage("/var/mobile/Documents/out.png")
cv_matToBase64(fmt?) 已支持iOS 已支持
把当前图像编码为 base64 字符串(便于网络传输/日志)。
fmt{string} 可选,编码格式,默认png- 返回:
stringbase64 内容;失败返回nil, errMsg
local b64 = cv_matToBase64("png")
cv_rotateImage(rot) 已支持iOS 已支持
旋转当前图像。
rot{int} 旋转方向:1=顺时针 90° 2=180° 3=逆时针 90°- 返回:
int0 成功;负值失败
cv_rotateImage(1) -- 顺时针旋转 90°
cv_cleanup() 已支持iOS 已支持
释放当前图像占用的内存(结束处理链后调用)。
- 参数:无
- 返回:无
cv_cleanup()
tessocr 模块
文字识别(Tesseract):先 ocr_init 加载语言数据,再选择识别来源(文件 / 当前图像 / 像素数据),返回识别出的文字字符串。语言数据已内置在 /var/mobile/Media/com.luatouch.app/models/tessdata。
ocr_init(dataDir, language) 已支持iOS 已支持
初始化文字识别引擎,加载语言数据。
dataDir{string} 语言数据目录,如/var/mobile/Media/com.luatouch.app/models/tessdatalanguage{string} 识别语言,如eng(英文)- 返回:
bool是否初始化成功
local ok = ocr_init("/var/mobile/Media/com.luatouch.app/models/tessdata", "eng")
ocr_set_param(param, value) 已支持iOS 已支持
设置识别参数(需先 ocr_init)。底层引擎为 Tesseract 5.5.0(静态库、仅 LSTM 识别引擎),参数名沿用 Tesseract 官方变量名。
param{string} 参数名,见下方参数表,如tessedit_char_whitelist(字符白名单)value{string} 参数值,必须用字符串传入:布尔写"1"/"0"(也接受true/false/T/F/Y/N),数值写"7"、"0.7"这类字符串- 返回:
bool参数名存在即返回true;未ocr_init时返回false, "OCR not initialized"
返回值只代表“参数名存在”,不等于一定生效:
- 参数名写错、或该变量在当前 Tesseract 版本里不存在 → 返回
false - 表中标注「初始化前」的参数必须在
ocr_init之前设定,而本接口要求先ocr_init,所以这类参数会返回 true 但实际不生效 - 重新执行
ocr_init(例如换语言)会重置全部参数,需要重新设置
可直接使用的参数
| 参数名 | 类型 | 默认 | 说明 |
|---|---|---|---|
tessedit_char_whitelist | string | 空 | 只识别白名单内的字符,固定字符集场景可明显提高准确率 |
tessedit_char_blacklist | string | 空 | 禁止输出指定字符(与白名单互斥使用) |
page_seg_mode | int | 6 | 页面分割模式;本引擎的专用别名,等价于 tessedit_pageseg_mode |
tessedit_pageseg_mode | int | 6 | 同上:0=方向/脚本检测,3=全自动,6=整块文本,7=单行,8=单词,10=单字符,11=稀疏文本 |
preserve_interword_spaces | bool | 0 | 保留词间多个空格,适合列表、对齐文本 |
user_defined_dpi | int | 0 | 指定输入图像 DPI,0 表示自动判断 |
textord_tabfind_find_tables | bool | 1 | 表格检测;纯文字截图可设 0,略快 |
invert_threshold | double | 0.7 | 反色判定阈值(tessedit_do_invert 的替代参数) |
tessedit_do_invert | bool | 1 | 尝试反色图像;Tesseract 已标记废弃,6.0 版将移除 |
tessedit_write_images | bool | 0 | 保存预处理后的图像,排查识别不准时使用 |
初始化前才能设定的参数(本接口设置不会生效)
| 参数名 | 默认 | 说明 |
|---|---|---|
load_system_dawg | 1 | 加载系统词典(关闭后不再把字符纠正成词典词) |
load_freq_dawg | 1 | 加载高频词词典 |
load_punc_dawg | 1 | 加载标点词典 |
tessedit_ocr_engine_mode | 3 | 识别引擎选择;本构建仅含 LSTM 引擎 |
当前版本不存在的参数名(写了会返回 false)
tessedit_do_orientation:5.5.0 无此变量,方向检测由页面分割模式(page_seg_mode)控制tessedit_do_blur_detection:5.5.0 无此变量tessedit_use_opencl:Tesseract 5 已移除 OpenCL 支持
-- 只识别数字和字母
ocr_set_param("tessedit_char_whitelist", "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
-- 单行文本 + 保留词间空格
ocr_set_param("page_seg_mode", "7")
ocr_set_param("preserve_interword_spaces", "1")
-- 纯文字截图关掉表格检测
ocr_set_param("textord_tabfind_find_tables", "0")
-- 用返回值判断参数名是否有效
print(ocr_set_param("tessedit_do_orientation", "0")) -- false:本版本没有这个参数
ocr_from_file(path) 已支持iOS 已支持
识别指定图片文件中的文字。
path{string} 图片路径- 返回:
string识别出的文字;失败返回nil, errMsg
local text = ocr_from_file("/var/mobile/Documents/screen.png")
ocr_from_image() 已支持iOS 已支持
识别当前图像中的文字(配合 cv_copyImageToMat 截取屏幕区域)。
- 参数:无
- 返回:
string识别出的文字;失败返回nil, errMsg
cv_copyImageToMat(0, 0, 374, 811) -- 先截屏为当前图像
local text, err = ocr_from_image()
if text then print("识别:", text) end
ocr_from_pixels(data, w, h, bpp) 已支持iOS 已支持
识别原始像素数据中的文字。
data{string} 二进制像素数据(raw 字节串)w, h{int} 图像宽高bpp{int} 每像素字节数- 返回:
string识别出的文字;失败返回nil, errMsg
local text = ocr_from_pixels(rawdata, 1242, 2208, 4)
ocr_set_rectangle(left, top, w, h) 已支持iOS 已支持
限定识别区域(相对当前图像/像素数据的坐标),只识别该区域内的文字。
left, top{int} 区域左上角w, h{int} 区域宽高- 返回:
bool设置成功;失败返回false, errMsg
ocr_set_rectangle(100, 200, 200, 100)
ocr_clear() 已支持iOS 已支持
清除识别状态(区域限制、临时数据等)。
- 参数:无
- 返回:无
ocr_clear()
ocr_close() 已支持iOS 已支持
释放识别引擎资源。
- 参数:无
- 返回:无
ocr_close()
ocr 模块(点阵字库识别/找字)
点阵字库 OCR(移植自 OP 插件,大漠同语义):用字库文件里每个字的 0/1 点阵,与屏幕按颜色二值化后的点阵逐位比对。与 tessocr / ppocr 的 AI 识别不同—— 只认识字库里有的字,但速度极快(毫秒级)、结果精确可控,适合游戏内固定字体的文字 (金币数量、按钮文字、菜单项、伤害数字等)。
字库文件:用「AutoGo图色助手」的字库制作导出(OP 文本格式,每行
名字$颜色$h,w,bit$HEX),放 <数据目录>/res/ 下(与 findPic 模板同基准)。
GBK / UTF-8 编码都认(GBK 自动转 UTF-8,脚本里可直接用中文比对)。
⚠️ 字库必须与目标画面同源同尺寸:字库是在哪张截图上取的,识别就得用同一分辨率、 同一字体渲染的画面(换分辨率 / 换设备 / 换窗口大小 = 字的像素尺寸变了 → 必须重取字库, 否则简单字勉强能中、笔画复杂的字必挂)。
匹配规则:sim 0.1~1.0,默认 1.0(精确:一个像素都不许错);
sim<1 时按 有效相似度 = 0.5 + sim/2 放宽容差(如 sim=0.8 → 允许 10% 像素差异)。
建议优先用 1.0,识别不到再降(越低假阳性越多)。
区域必须完整包住文字:返回坐标是字形的左上角,区域边界把字窗口切掉一部分的
字会被直接跳过(下边界多留一个字高)。
ocrSetDict(idx, path) 已支持 iOS 已支持
载入字库文件到 idx 号槽位(0~99)。重复载入同槽 = 整槽覆盖。
idx{number} 槽位号 0~99path{string} 字库文件路径;相对路径以<数据目录>/res/为基准- 返回:
number载入的字数;失败返回-1 - 字库槽全局共用:主脚本与所有子线程看到的是同一份
local n = ocrSetDict(0, "opdict.txt") -- 实际读 <数据目录>/res/opdict.txt
print("载入 " .. n .. " 个字")
ocrUseDict(idx) 已支持 iOS 已支持
切换当前使用的字库槽位(后续 ocr/findStr 都用它)。
idx{number} 槽位号 0~99- 返回:
number该槽字数;槽空返回0
ocrSetDict(0, "dict_main.txt") -- 主字库(菜单/按钮)
ocrSetDict(1, "dict_num.txt") -- 数字字库(伤害/金币)
ocrUseDict(1) -- 切到数字字库做识别
ocrGetNowDict() 已支持 iOS 已支持
返回当前使用的槽位号(number)。
ocrGetDictCount(idx) 已支持 iOS 已支持
返回 idx 号槽的字数;该槽未载入返回 -1。
ocr(x1, y1, x2, y2, color?, sim?) 已支持 iOS 已支持
识别区域内所有字库字,按阅读顺序(|Δy|<9 视为同一行,行内按 x)拼接成字符串返回。
x1, y1, x2, y2{number} 识别区域;x2/y2 ≤ 0表示取到帧右下角color{string} 可选,颜色串(同 findColor 格式):"RRGGBB[-偏色]",'|'分隔多组;默认"FFFFFF-323232"。必须与做字库时用的颜色口径一致sim{number} 可选,相似度 0.1~1.0,默认1.0- 返回:
string拼接后的识别文本(无命中返回空串)
ocrSetDict(0, "opdict.txt")
local s = ocr(200, 310, 480, 360, "FFFFFF-323232", 1.0)
print(s) -- 例:雷电游戏中心
ocrEx(x1, y1, x2, y2, color?, sim?) 已支持 iOS 已支持
同 ocr,但返回逐字结果(含每个字的坐标)。
- 参数同
ocr - 返回:
table{{index=结果序号, name=字符, x=, y=}, ...},index从 0 起(阅读序),x/y= 字形左上角(屏幕绝对坐标)
local hits = ocrEx(0, 0, 0, 0, "FFFFFF-323232", 1.0)
for i, h in ipairs(hits) do
print(h.index, h.name, h.x, h.y)
end
findStr(x1, y1, x2, y2, strs, color?, sim?) 已支持 iOS 已支持
在区域内找字库里指定的词,返回第一个命中的词。
strs{string} 目标词,'|'分隔多词,如"确定|取消"; 词名必须与字库里的一致- 返回:3 个值 ——
idx命中的词序号(0 = 第一个词,无命中-1)、x/y该词首个命中字形的左上角(无命中 -1,-1)
local idx, x, y = findStr(0, 0, 0, 0, "确定|取消", "FFFFFF-323232", 1.0)
if idx == 0 then
print("找到 确定 @ " .. x .. "," .. y) -- 点它: tap(x + 10, y + 10)
elseif idx == 1 then
print("找到 取消 @ " .. x .. "," .. y)
end
findStrEx(x1, y1, x2, y2, strs, color?, sim?) 已支持 iOS 已支持
同 findStr,但返回全部命中。
- 参数同
findStr - 返回:
table{{index=词序号, name=词名, x=, y=}, ...}(无命中空表)
local all = findStrEx(0, 0, 0, 0, "雷|闪电|中心", "FFFFFF-323232", 1.0)
for i, h in ipairs(all) do
print("词" .. h.index .. " [" .. h.name .. "] @ " .. h.x .. "," .. h.y)
end
ppocr 模块
中文文字识别(本地模型):先 ppocr_create 创建识别器,ppocr_init 加载检测/识别模型,再识别当前图像或图片文件,返回逐行文字及位置。模型已内置在 /var/mobile/Media/com.luatouch.app/models/ch/。
ppocr_create() 已支持iOS 已支持
创建文字识别器实例。
- 参数:无
- 返回:
bool创建成功;失败返回false, errMsg
local ok = ppocr_create()
ppocr_init(det.param, det.bin, rec.param?, rec.bin?, cls.param?, cls.bin?) 已支持iOS 已支持
加载识别模型(检测模型必填,识别/分类模型可选)。
det.param, det.bin{string} 文本检测模型文件路径rec.param, rec.bin{string} 可选,文字识别模型文件路径cls.param, cls.bin{string} 可选,方向分类模型文件路径- 返回:
bool加载成功;失败返回false, errMsg
local M = "/var/mobile/Media/com.luatouch.app/models/ch/"
local ok = ppocr_init(
M .. "PP_OCRv5_mobile_det.ncnn.param", M .. "PP_OCRv5_mobile_det.ncnn.bin",
M .. "PP_OCRv5_mobile_rec.ncnn.param", M .. "PP_OCRv5_mobile_rec.ncnn.bin")
ppocr_set_param(name, value) 已支持iOS 已支持
设置识别参数。底层引擎为自研 PP-OCRv5(ncnn 推理),只支持下表列出的 8 个参数名,不沿用 PaddleOCR 官方配置文件里的字段名。
name{string} 参数名,见下方参数表value{string} 参数值,必须用字符串传入:数值写"0.45"、"960",布尔用"1"/"0"- 返回:
bool仅在未先调用ppocr_create时返回false, "PPOcr not created";其余情况一律返回true
返回值不能用来判断参数是否生效(与 ocr_set_param 不同):
- 参数名不在下表内(拼错、或用了 PaddleOCR 的字段名如
det_db_thresh、rec_img_h、num_threads)→ 静默忽略,仍然返回true - 本接口可重复调用、后设覆盖先设;参数在
ppocr_create之后即可设置,不必等ppocr_init,且ppocr_init不会重置已设参数 - 执行
ppocr_release后再ppocr_create会得到全新实例,全部参数恢复默认值 - 线程数不可调:检测固定用 ncnn 大核数、识别固定单线程
可用参数表
| 参数名 | 类型 | 默认 | 说明 |
|---|---|---|---|
target_size | int | 640 | 检测阶段输入图最长边的缩放上限(超过则等比缩小后再送模型)。调大:小字更清晰但变慢;调小:更快,小字可能漏检 |
box_thresh | float | 0.6 | 候选文本框的平均得分阈值(0~1),低于该值的框被丢弃。调低:框更多(含噪声);调高:框更少更干净 |
threshold | float | 0.3 | 检测概率图的二值化阈值。调低:文字区域更连片(易粘连);调高:更严格(易断裂) |
min_size_base | float | 3.0 | 最小框尺寸基准,实际最小边 ≈ min_size_base × 缩放比,用于过滤极小噪点框。调大:滤掉小字 |
max_candidates | int | 1000 | 单张图最多处理的候选轮廓数,超出直接截断。密集文本可调大 |
enlarge_ratio | float | 1.95 | 文本框向外扩张比例,保证文字边缘被完整裁入。过大易把相邻文字并进同一行 |
post_process_mode | int | 0 | 后处理方式:0=按比例扩张矩形(快);1=DBNet unclip(按轮廓面积/周长扩张,更贴合文字形状,稍慢) |
target_height | int | 48 | 识别阶段文字行统一缩放到的高度。调大:小字/笔画复杂字符更准但更慢;调小:更快 |
ppocr_create()
ppocr_init(M .. "PP_OCRv5_mobile_det.ncnn.param", M .. "PP_OCRv5_mobile_det.ncnn.bin",
M .. "PP_OCRv5_mobile_rec.ncnn.param", M .. "PP_OCRv5_mobile_rec.ncnn.bin")
-- 小字场景:加大检测分辨率、放宽候选框阈值
ppocr_set_param("target_size", "960")
ppocr_set_param("box_thresh", "0.4")
-- 密集文本:提高候选上限
ppocr_set_param("max_candidates", "3000")
-- 更贴合文字轮廓的后处理(略慢)
ppocr_set_param("post_process_mode", "1")
ppocr_set_param("enlarge_ratio", "1.8")
ppocr_detect_image() 已支持iOS 已支持
识别当前图像中的文字(配合 cv_copyImageToMat 截取屏幕区域),返回逐行文字及位置。
- 参数:无
- 返回:
table结果表{width, height, lines={{text=, rate=, left=, top=, right=, bottom=}, ...}};失败返回nil, errMsg width, height识别图像尺寸;lines每行文字,text文字内容,rate置信度 0~1,left/top/right/bottom位置坐标(0~1 归一化,乘屏幕宽高得像素)
cv_copyImageToMat(0, 0, 374, 811) -- 先截屏为当前图像
local res = ppocr_detect_image()
if res then
for i, line in ipairs(res.lines) do
print(i, line.text, string.format("%.2f", line.rate))
end
end
ppocr_detect_file(path) 已支持iOS 已支持
识别指定图片文件中的文字,返回格式同 ppocr_detect_image。
path{string} 图片路径- 返回:
table结果表;失败返回nil, errMsg
local res = ppocr_detect_file("/var/mobile/Documents/screen.png")
ppocr_release() 已支持iOS 已支持
释放识别器资源。
- 参数:无
- 返回:
bool是否成功释放
ppocr_release()
hud 模块(悬浮显示)
hud 提供悬浮文本显示功能,支持多实例、彩色文本、独立位置/背景/显隐。与引擎「悬浮球」是两回事:hud 是纯显示浮层(不接管触摸,绝不遮挡点击),悬浮球是单实例可拖动的控制入口。
坐标系为屏幕点(UIKit points),即 UIWindow 原生单位。注意:它与 getScreenSize() 返回的像素不是同一单位(@2x/@3x 设备 1 点 = 2/3 像素);引擎触摸(tap 等)现为物理像素,与 points 也不同,需按倍率(÷3)换算。示例中的坐标均按 points 书写。所有实例通过全局表 hud 创建。
hud 模块还包含HTML 面板系列(showUi / hideUi / isShowUi,均为全局函数):以 WKWebView 全屏加载本地 HTML 作为交互面板,可传入 JSON 配置回填表单,面板内 JS 操作结果以字符串带回 Lua。面板内置自绘输入键盘(不依赖系统键盘),随面板开关。
showUi(path [, countdown [, query [, hash [, w [, h]]]]]) 已支持iOS 已支持
显示 HTML 面板并阻塞等待用户操作。面板跟随屏幕横竖方向(横屏下面板旋转显示,宽高按所见方向自动换算;打开时实时按当前方向布局);默认尺寸为屏幕宽高的 90% 居中。返回两个值:操作结果 + JS 带回的表单数据。
path{string} 加载目标:绝对路径(/var/mobile/...)、file://绝对路径(可自带?query)、http(s)://远程 URL、或裸 HTML 字符串countdown{int} 可选,默认 0:开始按钮倒计时秒数;>0 时按钮显示「开始(N)」每秒减一,归零自动触发「开始」query{string} 可选,默认 0 / -1 / 空 = 不传:拼接在 URL?后,引擎自动 percent-encode(main.lua 直接传裸 JSON 即可)hash{string} 可选,默认 0 / -1 / 空 = 不传:拼接在 URL#后,引擎自动 percent-encode(query 的兜底通道)w{number} 可选,默认 0:面板宽(每维独立)——≤0 或不传 = 屏幕宽 × 90%;0~1 = 屏幕宽的比例(如 0.5 = 半屏宽);>1 = 像素值(与getScreenSize()同一坐标系)h{number} 可选,默认 0:面板高,规则同w(按屏幕高计算)- 返回:
result{int} 1=点「开始」/倒计时归零,0=点「关闭」,-1=叉号/其他方式关闭(含 hideUi 唤醒);data{string|nil} 面板 JS 在开始/关闭时带回的表单 JSON 字符串,无则 nil
-- 打开面板,把上次配置 JSON 作为 query 传入(裸 JSON,引擎 percent-encode 注入 ? 后)
local r, data = showUi("/var/mobile/Media/com.luatouch.app/config/ui/index.html", 0, cfgJson)
if r == 1 then
-- 用户点了「开始」,data 为表单 JSON,可直接落盘下次复用
elseif r == -1 then
-- 用户关闭了面板
end
参数传递机制:引擎把第三参 percent-encode 后拼到 URL ? 后、第四参拼到 # 后。JS 端读取:location.search 优先 → location.hash 兜底 → 去 ?/# 前缀 → decodeURIComponent → JSON.parse(search 为第一通道,hash 仅在 search 为空时兜底)。
hideUi() 已支持iOS 已支持
隐藏 HTML 面板;若有 showUi 正在阻塞,则唤醒并使其按「叉号关闭」返回 -1(无 data)。
- 返回:无
hideUi() -- 关闭面板(定时器等场景主动收起)
isShowUi() 已支持iOS 已支持
检查 HTML 面板是否正在显示。
- 返回:
booltrue 显示中 / false 未显示
if isShowUi() then
-- 面板还开着,先别重复打开
end
toast(message [, x [, y [, duration]]]) 已支持iOS 已支持
显示 Toast 提示信息(非阻塞,自动消失)。
message{string} 要显示的提示信息x{int} 可选,默认 -1:显示 X 坐标(-1 = 水平居中)y{int} 可选,默认 -1:显示 Y 坐标(-1 = 垂直居中)duration{int} 可选,默认 -1:显示时长毫秒(-1 = 2000ms)- 返回:无
toast("Hello LuaTouch", -1, -1, -1) -- 默认居中 2 秒
toast("操作完成", 100, 300, 3000) -- 指定位置 3 秒
alert(title, content [, btn1Text [, btn2Text]]) 已支持iOS 已支持
弹出系统级对话框,阻塞直到用户点击按钮。
title{string} 弹窗标题content{string} 弹窗内容btn1Text{string} 可选,默认「确定」:第一个按钮文字btn2Text{string} 可选,默认空:第二个按钮文字;传空字符串则只显示一个按钮- 返回:
int0 = 点击第一个按钮,1 = 点击第二个按钮
local r = alert("确认操作", "是否继续?", "取消", "确定")
if r == 1 then
-- 用户点了确定
end
inputAlert(title, content [, placeholder [, defaultText [, btn1Text [, btn2Text]]]]) API 存在iOS 不支持
弹出带输入框的系统级对话框,阻塞等待用户操作。
title{string} 弹窗标题content{string} 说明文字placeholder{string} 可选,默认空:输入框占位提示defaultText{string} 可选,默认空:输入框默认文本btn1Text{string} 可选,默认「取消」:取消类按钮文字btn2Text{string} 可选,默认「确定」:确认类按钮文字;为空则只显示一个按钮- 返回:
text{string} 输入内容;ok{bool} true = 点击确认按钮,false = 点击取消
local text, ok = inputAlert("输入", "请输入备注", "备注", "", "取消", "确定")
if ok then
-- 使用 text
end
setBallPosition(fx, fy) 已支持iOS 已支持
移动悬浮球并保持停靠(横竖屏自动适配)。
fx{number} 停靠边:0=左,1=右fy{number} 边缘高度百分比(0~1,支持小数点后两位)- 返回:无
setBallPosition(0, 0.1) -- 靠左边缘 10% 高
setBallPosition(1, 0.5) -- 靠右边缘中间
hud.new() 已支持iOS 已支持
创建一个新的 HUD 实例。
- 参数:无
- 返回:
userdataHUD 实例(方法均返回自身,支持链式调用)
local h = hud.new()
h:setPosition(fx, fy, width, height) 已支持iOS 已支持
设置 HUD 的位置和大小(旋转自动适配)。
fx, fy{number} 左上角百分比(0~1,支持小数点后两位)width, height{int} 宽高(屏幕点,整数)- 返回:
userdata自身(可链式)
h:setPosition(0.1, 0.8, 300, 60) -- 左上角 10%、80% 处,宽 300 高 60
h:setBackgroundColor(color) 已支持iOS 已支持
设置 HUD 背景颜色(支持透明度)。
color{string} 十六进制,#RRGGBB或#RRGGBBAA(AA 为透明度)- 返回:
userdata自身
h:setBackgroundColor("#2D2D30")
h:setBackgroundColor("#00000080") -- 半透明黑色
h:setTextSize(size) 已支持iOS 已支持
设置 HUD 字体大小。
size{int} 字体大小(推荐 30~60)- 返回:
userdata自身
h:setTextSize(45)
h:setText(items) 已支持iOS 已支持
设置 HUD 文本内容(支持多色文本)。
items{table} 文本项数组,每项含TextColor(颜色字符串)与Text(文本)- 返回:
userdata自身
h:setText({
{TextColor = "#00FF00", Text = "HP: "},
{TextColor = "#FFFFFF", Text = "100/100"},
})
h:show() 已支持iOS 已支持
显示 HUD。
- 返回:
userdata自身
h:show()
h:hide() 已支持iOS 已支持
隐藏 HUD。
- 返回:
userdata自身
h:hide()
h:isVisible() 已支持iOS 已支持
检查 HUD 是否可见。
- 返回:
booltrue 可见 / false 隐藏
if h:isVisible() then
-- HUD 当前可见
end
h:destroy() 已支持iOS 已支持
销毁 HUD 实例,释放窗口资源(也可等 Lua GC 自动回收)。
- 返回:无
h:destroy()
示例 已支持iOS 已支持
-- 创建并配置 HUD
local h = hud.new()
h:setPosition(0.1, 0.8, 300, 60) -- 屏幕下方,宽 300 高 60
:setBackgroundColor("#3D000080")
:setTextSize(45)
while true do
h:setText({
{TextColor = "#00FF00", Text = "当前时间: "},
{TextColor = "#FFFFFF", Text = os.date("%Y-%m-%d %H:%M:%S")},
})
sleep(1000)
end
draw 模块(屏幕绘制)
draw 提供屏幕叠加绘制功能:在屏幕上实时显示直线、矩形、圆形、多边形等图形标注(画在悬浮球窗口上的全屏透明覆盖层,不遮挡触摸)。常用于脚本调试时标注:找到的按钮画个框、点击位置画个圈、寻路路径画条线,肉眼直接看到。
坐标系为物理像素(与 findColor / tap / getScreenSize() 同一坐标系,绘制层内部自动换算成屏幕点)。注意:坐标基于手机实际分辨率(如 1242×2208),脚本需按自己屏幕分辨率自行换算比例。
颜色参数统一为 ARGB 整数(0xAARRGGBB),如红色 0xffff0000;thickness 传 -1 表示填充。
句柄机制:每个绘制函数返回一个句柄(自增 id),图形不会自动消失,靠 clearDraw(句柄) 按句柄单独清除,或 clearDraw() 无参全部清除。
drawLine(x0, y0, x1, y1, argb, thickness) 已支持iOS 已支持
绘制一条直线(任意角度斜线均可,两端圆头)。
x0, y0{int} 起始点坐标x1, y1{int} 终点坐标argb{int} 颜色,如0xffff0000(红色)thickness{int} 线宽(物理像素,自动换算)- 返回:{int} 图形句柄(用于
clearDraw(句柄)清除)
local id = drawLine(100, 100, 1100, 2000, 0xffff0000, 20) -- 斜线
-- clearDraw(id) 清除这条线
drawRect(x0, y0, x1, y1, argb, thickness) 已支持iOS 已支持
绘制矩形框;thickness = -1 填充。
x0, y0{int} 左上角坐标x1, y1{int} 右下角坐标argb{int} 颜色thickness{int} 线宽,-1填充- 返回:{int} 句柄
local id = drawRect(300, 400, 900, 900, 0xff00ff00, 10) -- 绿框
fillRect(x0, y0, x1, y1, argb) 已支持iOS 已支持
填充一个矩形区域。
x0, y0{int} 左上角坐标x1, y1{int} 右下角坐标argb{int} 颜色(可用0x80前缀做半透明)- 返回:{int} 句柄
local id = fillRect(700, 300, 1000, 600, 0x80ff0000) -- 半透明红填充
drawCircle(x, y, radius, argb, thickness) 已支持iOS 已支持
绘制圆形;thickness = -1 填充。
x, y{int} 圆心坐标radius{int} 半径argb{int} 颜色thickness{int} 线宽,-1填充- 返回:{int} 句柄
local id = drawCircle(621, 1104, 200, 0xff0000ff, -1) -- 屏幕中心蓝色填充圆
drawPolygon(points, argb, thickness, isclosed) 已支持iOS 已支持
绘制多边形;thickness = -1 填充。
points{table} 顶点数组{{x1,y1},{x2,y2},...}argb{int} 颜色thickness{int} 线宽,-1填充isclosed{boolean} 是否闭合- 返回:{int} 句柄
local id = drawPolygon({{200,1600},{500,1500},{450,1850}}, 0xff00ff00, 8, true)
clearDraw(id?) 已支持iOS 已支持
清除屏幕绘制图形。
id{int|nil} 可选:传句柄只清除对应图形;不传清除全部- 返回:无
clearDraw(id1) -- 只清句柄 id1 对应的图形
clearDraw() -- 全部清除
示例 已支持iOS 已支持
-- 标注:找到按钮画框、点击位置画圈(坐标=物理像素,按自己屏幕分辨率换算)
local box = drawRect(300, 400, 900, 900, 0xffff0000, 10) -- 红框标注
local dot = drawCircle(621, 1104, 60, 0xff00ff00, -1) -- 绿点标注
sleep(1000)
clearDraw(box) -- 单独清除红框,绿点保留
sleep(1000)
clearDraw() -- 全部清除
uiacc 节点
1. 根据节点的id属性匹配 (id) 已支持
说明: 根据节点的id属性匹配
函数: id(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的具体控件的id |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = id("com.luatouch.app:id/detail_tv_title")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
2. 根据id包含的部分字符串匹配 (idContains) 已支持
说明: 根据id包含的部分字符串匹配(正则 :match)
函数: idContains(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的模糊的包含id某一段字符串的内容 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = idContains("detail_tv_title")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
3. 根据id的前缀去匹配 (idStartsWith) 已支持
说明: 根据id的前缀去匹配(正则 :match('^' .. str))
函数: idStartsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的id的前缀 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = idStartsWith("com.luatouch.app")
local node = sel:findOne(10000)
if node ~= nil then
print(node:id())
end
4. 根据id的后缀去匹配 (idEndsWith) 已支持
说明: 根据id的后缀去匹配(正则 :match(str .. '$'))
函数: idEndsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的id的后缀 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = idEndsWith("detail_tv_title")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
5. 正则匹配id (idMatches) 已支持
说明: 正则匹配id
函数: idMatches(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 正则表达式字符串 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = idMatches(".*detail_tv_title$")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
6. 根据节点的text属性匹配 (text) 已支持
说明: 根据节点的text属性匹配
函数: text(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的具体控件的text |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = text("LuaTouch高级版")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
7. 根据text包含的部分字符串匹配 (textContains) 已支持
说明: 根据text包含的部分字符串匹配(正则 :match)
函数: textContains(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的模糊的包含text某一段字符串的内容 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = textContains("LuaTouch")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
8. 根据text的前缀去匹配 (textStartsWith) 已支持
说明: 根据text的前缀去匹配(正则 :match('^' .. str))
函数: textStartsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的text的前缀 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = textStartsWith("LuaTouch")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
9. 根据text的后缀去匹配 (textEndsWith) 已支持
说明: 根据text的后缀去匹配(正则 :match(str .. '$'))
函数: textEndsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的text的后缀 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = textEndsWith("高级版")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
10. 正则匹配text (textMatches) 已支持
说明: 正则匹配text
函数: textMatches(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 正则表达式字符串 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = textMatches(".*高级版$")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
11. 根据节点的desc属性匹配 (desc) 已支持
说明: 根据节点的desc属性匹配
函数: desc(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的具体控件的desc |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = desc("LuaTouch高级版")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
12. 根据desc包含的部分字符串匹配 (descContains) 已支持
说明: 根据desc包含的部分字符串匹配(正则 :match)
函数: descContains(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的模糊的包含desc某一段字符串的内容 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = descContains("LuaTouch")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
13. 根据desc的前缀去匹配 (descStartsWith) 已支持
说明: 根据desc的前缀去匹配(正则 :match('^' .. str))
函数: descStartsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的desc的前缀 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = descStartsWith("LuaTouch")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
14. 根据desc的后缀去匹配 (descEndsWith) 已支持
说明: 根据desc的后缀去匹配(正则 :match(str .. '$'))
函数: descEndsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的desc的后缀 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = descEndsWith("高级版")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
15. 正则匹配desc (descMatches) 已支持
说明: 正则匹配desc
函数: descMatches(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 正则表达式字符串 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = descMatches(".*高级版$")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
16. 根据节点的className属性匹配 (className) 已支持
说明: 根据节点的className属性匹配
函数: className(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找的具体控件的类名 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = className("AXStaticText")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
17. 根据className属性所包含的字符串模糊匹配 (classNameContains) 已支持
说明: 根据className属性所包含的字符串模糊匹配(正则 :match)
函数: classNameContains(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找className包含该字符的节点 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = classNameContains("Text")
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
18. 根据className属性前缀匹配 (classNameStartsWith) 已支持
说明: 根据className属性前缀匹配(正则 :match('^' .. str))
函数: classNameStartsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找className前缀为该字符的节点 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = classNameStartsWith("AX")
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
19. 根据className属性后缀匹配 (classNameEndsWith) 已支持
说明: 根据className属性后缀匹配(正则 :match(str .. '$'))
函数: classNameEndsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要查找className后缀为该字符的节点 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = classNameEndsWith("StaticText")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
20. 根据className属性正则匹配 (classNameMatches) 已支持
说明: 根据className属性正则匹配
函数: classNameMatches(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要匹配className的正则表达式 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = classNameMatches("^AX.*Text$")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
21. 根据packageName属性全字段匹配 (packageName) 已支持
说明: 根据packageName属性全字段匹配
函数: packageName(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 指定要匹配节点所属的包名 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = packageName("com.apple.mobilesafari"):className("AXStaticText")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
22. 匹配包含指定字符串的节点 (packageNameContains) 已支持
说明: 匹配包含指定字符串的节点(正则 :match)
函数: packageNameContains(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 包含该字符串的包名的节点将被匹配到 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = packageNameContains("apple"):className("AXStaticText")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
23. 匹配包名前缀为指定字符串的节点 (packageNameStartsWith) 已支持
说明: 匹配包名前缀为指定字符串的节点(正则 :match('^' .. str))
函数: packageNameStartsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 包名为该字符的前缀的节点将被匹配到 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = packageNameStartsWith("com.apple"):className("AXStaticText")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
24. 匹配包名后缀为指定字符串的节点 (packageNameEndsWith) 已支持
说明: 匹配包名后缀为指定字符串的节点(正则 :match(str .. '$'))
函数: packageNameEndsWith(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 包名为该字符的后缀的节点将被匹配到 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = packageNameEndsWith("mobilesafari"):className("AXStaticText")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
25. 包名正则匹配 (packageNameMatches) 已支持
说明: 包名正则匹配
函数: packageNameMatches(str)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| str | string | 是 | 根据这个正则字符串匹配所有符合规则包名的节点 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = packageNameMatches("com.apple.*"):className("AXStaticText")
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
26. 根据节点的范围匹配 (bounds) 已支持
说明: 根据节点的范围匹配(精确匹配 bounds 坐标)
函数: bounds(l, t, r, b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| l | number | 是 | 左范围 |
| t | number | 是 | 上范围 |
| r | number | 是 | 右范围 |
| b | number | 是 | 下范围 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = bounds(30, 212, 236, 250)
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
27. 匹配该范围内的节点 (boundsInside) 已支持
说明: 匹配 bounds 完全包含在指定范围内的节点
函数: boundsInside(l, t, r, b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| l | number | 是 | 左范围 |
| t | number | 是 | 上范围 |
| r | number | 是 | 右范围 |
| b | number | 是 | 下范围 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = boundsInside(30, 212, 236, 250)
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
28. 根据绘制顺序匹配 (drawingOrder) 已支持
说明: 根据绘制顺序匹配
函数: drawingOrder(level)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| level | number | 是 | 绘制顺序级别 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = drawingOrder(1)
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
29. 根据深度索引匹配 (depth) 已支持
说明: 根据深度索引匹配
函数: depth(level)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| level | number | 是 | 深度索引级别 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = depth(1)
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
30. 根据在当前父节点里面的索引顺序查找 (index) 已支持
说明: 根据在当前父节点里面的索引顺序查找
函数: index(level)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| level | number | 是 | 索引级别 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = index(1)
local node = sel:findOne(10000)
if node ~= nil then
print(node:text())
end
31. 根据是否可见匹配 (visibleToUser) 已支持
说明: 根据是否可见匹配
函数: visibleToUser(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否可见 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = visibleToUser(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
32. 根据是否选中匹配 (selected) 已支持
说明: 根据是否选中匹配
函数: selected(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否选中 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = selected(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
33. 根据是否可点击匹配 (clickable) 已支持
说明: 根据是否可点击匹配
函数: clickable(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否可点击 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = clickable(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
34. 根据是否可长按点击匹配 (longClickable) 已支持
说明: 根据是否可长按点击匹配
函数: longClickable(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否可长按点击 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = longClickable(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
35. 根据是否可用匹配 (enabled) 已支持
说明: 根据是否可用匹配
函数: enabled(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否可用 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = enabled(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
36. 根据是否是密码框匹配 (password) 已支持
说明: 根据是否是密码框匹配
函数: password(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否是密码框 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = password(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
37. 根据是否可以滚动来匹配 (scrollable) 已支持
说明: 根据是否可以滚动来匹配
函数: scrollable(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否可以滚动 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = scrollable(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
38. 根据是否被勾选来匹配 (checked) 已支持
说明: 根据是否被勾选来匹配
函数: checked(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否被勾选 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = checked(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
39. 根据是否可以被勾选来匹配 (checkable) 已支持
说明: 根据是否可以被勾选来匹配
函数: checkable(b)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| b | boolean | 是 | 是否可以被勾选 |
| 返回值类型 | 说明 |
|---|---|
| selector | 返回一个选择器对象,该对象支持级联选择 |
local sel = checkable(true)
local node = sel:findOne(10000)
if node ~= nil then
print(node:className())
end
findOne(timeoutMs) 已支持iOS 已支持
查找第一个匹配节点,未找到返回 nil。
一次调用只采集一次节点树(不是内部循环轮询):引擎会拉起一次节点采集并等待结果,拿到后在结果里做匹配。所以「等某个元素出现」需要自己写成循环(见下方示例),或用 exists()。
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| timeoutMs | int | 否 | 等待本次采集完成的上限毫秒数,默认 10000;传 0 或负数会按 3000 处理(不会变成“立即返回”) |
| 返回值类型 | 说明 |
|---|---|
| node | 匹配的节点对象;未找到 / 采集超时 / 采集失败均返回 nil |
- 采集超时(
timeoutMs到点)会终止本次采集并返回nil - 同一时刻只允许一次采集:上一次还没结束时再调用,会直接返回
nil(不会排队等待) - 采集过程中子进程要先把「屏幕中心点采样」重试到成功(最坏约 50 次 × 50ms ≈ 2.5 秒),因此
timeoutMs给得过小(如 1000)可能在产出前就被截断;建议 ≥ 3000
local node = text("LuaTouch"):findOne(5000)
if node then
print("找到", node:text(), node:bounds())
end
-- findOne 不会自己重试,需要“等元素出现”时自己轮询:
local function waitText(t, ms)
local deadline = axTimeMs() + ms
while axTimeMs() < deadline do
local n = t:findOne(3000)
if n then return n end
axSleep(300)
end
return nil
end
local btn = waitText(textContains("确定"), 10000)
find(timeoutMs) 已支持iOS 已支持
查找所有匹配节点,返回数组(顺序 = 节点树中的先后顺序);未找到返回空表 {}。
与 findOne 一样是一次采集:采集一次节点树,然后在结果里收集全部匹配节点。
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| timeoutMs | int | 否 | 等待本次采集完成的上限毫秒数,默认 10000;传 0 或负数按 3000 处理 |
| 返回值类型 | 说明 |
|---|---|
| node[] | 匹配的节点数组;未找到 / 采集超时 / 采集失败均返回空表 {} |
local ns = textContains("确定"):find(3000)
print("找到", #ns, "个")
for i, n in ipairs(ns) do
print(i, n:text(), n:bounds())
end
findOnce(timeoutMs) / exists(timeoutMs) 已支持iOS 已支持
选择器上的两个便捷方法(同样是一次采集):
| 方法 | 返回 | 说明 |
|---|---|---|
sel:findOnce(t) | node | nil | findOne 的别名,行为完全一致(默认 10000ms) |
sel:exists(t) | boolean | 是否存在匹配节点,等价于 findOne(t) ~= nil;只关心“有没有”时更直观 |
-- 等按钮出现:自己轮询(每次采集一次,间隔 300ms)
local function waitExists(sel, ms)
local deadline = axTimeMs() + ms
while axTimeMs() < deadline do
if sel:exists(3000) then return true end
axSleep(300)
end
return false
end
if waitExists(textContains("确定"), 10000) then
local n = textContains("确定"):findOnce(3000)
local l, t, r, b = n:bounds()
tap((l + r) / 2, (t + b) / 2)
end
节点方法
通过 findOne / find 拿到的节点对象是属性读取器:每个方法读取该节点对应的属性并返回,不参与匹配。属性不存在时不会报错——字符串返回 ""、数值返回 0、布尔返回 false。
| 方法 | 返回类型 | 说明 |
|---|---|---|
n:text() | string | 节点显示文本 |
n:id() | string | 节点唯一标识(accessibilityIdentifier) |
n:desc() | string | 无障碍描述 |
n:className() | string | 节点类名 |
n:packageName() | string | 所属 App Bundle ID |
n:bounds() | l,t,r,b | 节点矩形坐标(4 个返回值,物理像素) |
n:clickable() | boolean | 是否可点击 |
n:enabled() | boolean | 是否可用 |
n:focusable() | boolean | 是否可获得焦点 |
n:focused() | boolean | 当前是否已聚焦 |
n:visibleToUser() | boolean | 是否可见 |
n:selected() | boolean | 是否选中 |
n:checkable() | boolean | 是否可勾选 |
n:checked() | boolean | 是否已勾选 |
n:scrollable() | boolean | 是否可滚动 |
n:longClickable() | boolean | 是否可长按 |
n:password() | boolean | 是否是密码框 |
n:depth() | int | 节点深度 |
n:index() | int | 在父节点中的索引 |
n:drawingOrder() | int | 绘制顺序 |
local n = text("LuaTouch"):findOne(5000)
if n then
print("text =", n:text())
print("class =", n:className())
print("clickable =", n:clickable())
local l, t, r, b = n:bounds()
print("bounds =", l, t, r, b)
end
链式组合(多条件 AND)
用 : 链式串联多个 selector,节点必须同时满足所有条件才匹配。
-- 同时满足:文本包含"确定" 且 可见 且 可点击
local n = textContains("确定"):visibleToUser(true):clickable(true):findOne(5000)
-- 包名 + 类名组合
local n2 = packageName("com.apple.mobilesafari"):className("AXStaticText"):findOne(3000)
点击节点
节点对象没有 click() 方法。标准做法:取节点 bounds 的中心 → 调用引擎 tap(x, y)。bounds 与 tap 都用物理像素,直接传即可,无需换算。
local n = text("LuaTouch"):findOne(5000)
if n then
local l, t, r, b = n:bounds()
local cx, cy = (l + r) / 2, (t + b) / 2
tap(cx, cy)
end
底层 API
| 函数 | 返回 | 说明 |
|---|---|---|
axGetXml(timeout?) | string | 直接取一次节点树 XML 字符串。默认 3000ms;timeout ≤ 0 按 3000 处理。失败/超时返回空字符串 "",不是 nil。同一时刻只允许一次采集,重入时也返回 ""。findOne / find 内部就是调它,一般不用自己调 |
axSleep(ms?) | 无 | 阻塞等待指定毫秒,默认 100。属于无障碍模块的休眠,不响应脚本的暂停/停止——脚本里请优先用全局 sleep(),需要与节点采集节奏对齐时才用 axSleep |
axTimeMs() | int | 当前时间戳,单位毫秒(Unix epoch)。用于算耗时/超时截止时间 |
旧版 axGetTree / axDumpAttrs 已删除(不再整树转 Lua 表;属性改由节点对象的方法按需读取)。
-- 取原始节点树 XML(调试用)
local xml = axGetXml(3000)
if xml ~= "" then -- 注意:失败返回空字符串,不是 nil
print("XML 长度:", #xml)
else
print("采集失败/超时")
end
-- 用 axTimeMs 自己控制等待时长
local deadline = axTimeMs() + 5000
while axTimeMs() < deadline do
if textContains("确定"):exists(3000) then break end
axSleep(300)
end
注意事项
- 前台切换:节点树会随前台 App 变化,切换后建议先
findOne(5000)等新树稳定。 - 正则引擎:Contains / StartsWith / EndsWith / Matches 内部全用 Lua
string.match,支持 Lua 正则语法(非 PCRE)。 - 精确匹配:
text("A")是精确匹配;模糊匹配用textContains/textMatches。 - 性能:取树 12 个窗口 ~3000+ 节点,单次 ~50ms。建议
findOne+ 短超时。