👆 11. 触控操作 17 个 API
触控操作通过无障碍服务(AccessibilityService)的 dispatchGesture 实现,所有坐标会自动按 setScreenScale 设置进行缩放。坐标越界(负数或超出屏幕)会被跳过并记录日志。
click(x, y)
触控
在指定坐标执行一次点击。坐标会自动缩放,越界则跳过。
click(x: number, y: number) → nil
| 参数 | 类型 | 必填 | 说明 |
| x | number | 是 | 横坐标(像素) |
| y | number | 是 | 纵坐标(像素) |
✅ 正确
click(500, 800)
wait(1000)
click(520, 810)
❌ 错误:连续点击不加等待
click(500, 800)
click(500, 800) -- 动画未完成,第二次点击无效
原因:点击后界面需要时间响应,连续点击中间应加 wait 等待界面切换完成。
⚠️ 避坑:坐标基于真实屏幕分辨率。若脚本要兼容多分辨率设备,请先用 setScreenScale(true, 1080, 1920) 设置基准分辨率,引擎会自动缩放坐标。
long_click(x, y, duration)
触控
在指定坐标长按指定时长(毫秒)。
long_click(x: number, y: number, duration: number) → nil
| 参数 | 类型 | 必填 | 说明 |
| x | number | 是 | 横坐标 |
| y | number | 是 | 纵坐标 |
| duration | number | 是 | 长按时长(毫秒) |
✅ 正确
long_click(300, 400, 1500) -- 长按 1.5 秒
❌ 错误:duration 传秒
long_click(300, 400, 2) -- 2 毫秒,几乎无效
原因:duration 单位是毫秒不是秒。长按一般需 800–2000ms。
⚠️ 避坑:duration 过短(<200ms)会被系统识别为普通点击而非长按。
swipe(x1, y1, x2, y2, duration)
触控
从起点滑动到终点,duration 控制滑动速度(数值越大越慢)。
swipe(x1, y1, x2, y2, duration: number) → nil
| 参数 | 类型 | 必填 | 说明 |
| x1, y1 | number | 是 | 起点坐标 |
| x2, y2 | number | 是 | 终点坐标 |
| duration | number | 是 | 滑动时长(毫秒) |
✅ 正确:向上滑动列表
swipe(540, 1500, 540, 300, 500) -- 500ms 上滑
❌ 错误:duration 过短被识别为 fling
swipe(540, 1500, 540, 300, 50) -- 50ms 太快,惯性不可控
原因:duration 太小(<100ms)会触发惯性滚动,难以精确停止。需要精确定位用 300–600ms。
提示:需要快速滑动用 touchFling(duration 上限 200ms)。
input_text(text)
触控
向当前焦点输入框输入文本(依赖无障碍服务的输入法支持)。
input_text(text: string) → nil
| 参数 | 类型 | 必填 | 说明 |
| text | string | 是 | 要输入的文本 |
✅ 正确
click(200, 300) -- 先点击输入框获取焦点
wait(500)
input_text("hello world")
❌ 错误:未点击输入框直接输入
input_text("hello") -- 无焦点,输入失败
原因:必须先点击输入框使其获得焦点,再调用 input_text。
⚠️ 避坑:输入前确保输入框已聚焦;部分自绘输入框(如游戏内)不支持无障碍输入,需改用 click 模拟键盘。
press_back()
触控
模拟按下返回键。引擎会延迟 300ms 自动把本应用拉回前台,避免任务被系统暂停。
press_back() → nil
✅ 正确
press_back()
wait(1000)
❌ 错误:返回后立即操作目标应用
press_back()
click(500, 800) -- 引擎正拉回前台,点击可能落空
原因:press_back 后引擎有 300ms 拉回前台动作,应等待后再操作。
⚠️ 避坑:press_back 会触发引擎把本应用拉回前台(设计如此,防止任务被 onPause 暂停),不要在需要停留在目标应用的场景下连续调用。
press_home()
触控
模拟按下 Home 键回到桌面,随后引擎延迟 300ms 拉回本应用前台。
press_home() → nil
✅ 正确
press_home()
wait(1500)
launch_app("com.target.app")
⚠️ 避坑:与 press_back 同理,调用后引擎会拉回本应用前台。如需停留在桌面,请在调用后等待并重新启动目标应用。
touchDown(id, x, y)
触控
开始一次触摸按压(按下不抬起)。配合 touchMove/touchUp 实现自定义手势,采用累积路径模式。注意第一个参数是触点 id。
touchDown(id: number, x: number, y: number) → nil
| 参数 | 类型 | 必填 | 说明 |
| id | number | 是 | 触点 id(单点用 0) |
| x | number | 是 | 起点横坐标 |
| y | number | 是 | 起点纵坐标 |
✅ 正确:自定义滑动
touchDown(0, 300, 500)
wait(50)
touchMove(0, 600, 500)
wait(50)
touchUp(0, 600, 500)
❌ 错误:参数顺序写反
touchDown(300, 500, 0) -- 把坐标当 id,id 当坐标
原因:实际签名是 touchDown(id, x, y),id 在前。写成 (x, y, id) 会导致坐标错乱。
⚠️ 避坑:必须以 touchUp 结束,否则触摸状态不释放;调用其他触摸函数(如 click)会自动重置未完成的 touchDown 状态。
touchMove(id, x, y)
触控
移动触点到新坐标(累积路径点)。若未先 touchDown 会自动开始一次触摸。
touchMove(id: number, x: number, y: number) → nil
✅ 正确:多点累积绘制曲线路径
touchDown(0, 100, 500)
for i = 1, 10 do
wait(30)
touchMove(0, 100 + i*40, 500)
end
touchUp(0, 500, 500)
⚠️ 避坑:touchMove 只是累积路径点,真正的手势派发在 touchUp 时一次性执行。中间的 wait 不影响手势速度,手势速度由路径点数估算。
touchUp(id, [x], [y])
触控
结束触摸并派发整个累积路径手势。x/y 可省略(默认用最后位置)。
touchUp(id: number, [x: number], [y: number]) → nil
✅ 正确
touchDown(0, 300, 500)
touchMove(0, 600, 500)
touchUp(0) -- 派发手势
❌ 错误:忘记 touchUp
touchDown(0, 300, 500)
touchMove(0, 600, 500)
-- 缺少 touchUp,手势不执行,状态残留
原因:手势在 touchUp 时才派发。不调用 touchUp 会导致手势不触发,且后续触摸函数行为异常。
touchFling(x1, y1, x2, y2, [duration])
触控
快速滑动(fling)。duration 上限 200ms,超过会被截断为 200ms。
touchFling(x1, y1, x2, y2: number, [duration: number=200]) → nil
✅ 正确
touchFling(540, 1600, 540, 200, 150) -- 快速上滑
❌ 错误:期望慢速滑动
touchFling(540, 1600, 540, 200, 2000) -- 2000 被截断为 200
原因:touchFling 专为快速滑动设计,duration 上限 200ms。需要慢速滑动请用 swipe。
touchPress(x, y, [duration])
触控
在坐标按压指定时长,等价于 long_click。默认 duration 为 1000ms。
touchPress(x: number, y: number, [duration: number=1000]) → nil
✅ 正确
touchPress(400, 600) -- 默认按压 1 秒
touchPress(400, 600, 2000) -- 按压 2 秒
提示:与 long_click 功能相同,仅命名风格不同。
multiTouch(points)
触控
多点触控(缩放/旋转等)。需要 Android 10+,参数为表:每个元素是一个手指的路径表(含 {x,y} 点序列和 duration 字段)。成功返回 true。
multiTouch(points: table) → boolean
✅ 正确:双指捏合缩小
multiTouch({
{ {x=800, y=800}, {x=500, y=500}, duration=300}, -- 手指1
{ {x=200, y=200}, {x=500, y=500}, duration=300}, -- 手指2
})
❌ 错误:参数不是 table
multiTouch(800, 800, 200, 200) -- 报错,需传 table
原因:multiTouch 接收单个 table 参数,每个元素代表一个手指路径。
⚠️ 避坑:仅 Android 10(API 29)及以上支持多指手势;低版本返回 false 并记录日志。每个手指路径至少 1 个点。
gesture(path)
触控
按路径表派发单指手势。path 为 {x,y} 点序列,可含 duration 字段(默认 300ms)。成功返回 true。
gesture(path: table) → boolean
✅ 正确:绘制 L 形路径
gesture({
{x=100, y=100},
{x=100, y=500},
{x=500, y=500},
duration = 800
})
❌ 错误:路径为空
gesture({}) -- 返回 false
原因:路径至少需要 1 个点,空路径会被拒绝。
⚠️ 避坑:无障碍服务不可用时返回 false。确保本应用已开启无障碍服务权限。
touchTime(x, y, ms)
触控
在坐标按压指定毫秒数(等价于 touchDown + sleep + touchUp)。
touchTime(x: number, y: number, ms: number) → nil
✅ 正确
touchTime(300, 400, 2000) -- 按压 2 秒
❌ 错误:ms 传 0 或负数
touchTime(300, 400, 0) -- 无意义按压
原因:ms 应为正数,0 或负数按压无效。
randomTap(x, y, [range])
触控
在以 (x,y) 为中心、半径 range 的方形区域内随机点击,用于规避反作弊检测。默认 range=5。
randomTap(x: number, y: number, [range: number=5]) → nil
✅ 正确
randomTap(500, 800, 10) -- 在 (500,800)±10 范围内随机点击
❌ 错误:range 过大点偏目标
randomTap(500, 800, 200) -- 偏移过大可能点到其他控件
原因:range 是随机偏移半径,过大可能超出目标控件范围。一般取 3–15。
touchSwipe(x1, y1, x2, y2, [duration])
触控
触摸滑动,与 swipe 功能相同(命名兼容)。默认 duration=500ms。
touchSwipe(x1, y1, x2, y2: number, [duration: number=500]) → nil
✅ 正确
touchSwipe(540, 1500, 540, 300, 600)
提示:与 swipe 完全等价,区别仅在于 swipe 的 duration 为必填、touchSwipe 为可选(默认 500)。
touchLongClick(x, y, [duration])
触控
触摸长按,与 long_click 功能相同(命名兼容)。默认 duration=1000ms。
touchLongClick(x: number, y: number, [duration: number=1000]) → nil
✅ 正确
touchLongClick(400, 600, 1500)
提示:与 long_click 等价,区别在 duration 为可选参数。
⚙️ 12. 系统控制 26 个 API
系统控制涵盖等待、应用启停、按键模拟、硬件开关、Shell 命令等。部分函数需要系统级权限(root、设备管理员、WRITE_SECURE_SETTINGS 等),无权限时返回 false 并记录日志。
wait(ms)
系统
阻塞当前脚本指定毫秒数。这是脚本中最常用的节奏控制函数。
wait(ms: number) → nil
✅ 正确
click(500, 800)
wait(1500) -- 等待界面加载
click(500, 900)
❌ 错误:用秒
wait(2) -- 只等 2 毫秒,几乎无效果
原因:单位是毫秒。等 2 秒应写 wait(2000)。
⚠️ 避坑:LUA 在 C 端 TaskEngine 单线程同步执行,wait 会阻塞整个任务线程。不要用过长 wait(如 600000)卡死任务,应配合状态判断循环短 wait。
launch_app(package)
系统
启动指定包名的应用。成功返回 true,失败返回 false。
launch_app(package: string) → boolean
✅ 正确
launch_app("com.android.settings")
wait(2000)
❌ 错误:传应用名而非包名
launch_app("设置") -- 失败
原因:必须传包名(如 com.android.settings),不是应用显示名。
check_app(package)
系统
检查指定应用是否在前台运行。返回 boolean。
check_app(package: string) → boolean
✅ 正确:循环等待应用启动
launch_app("com.target.app")
local tries = 0
while not check_app("com.target.app") and tries < 20 do
wait(500)
tries = tries + 1
end
❌ 错误:启动后立即检查
launch_app("com.target.app")
if check_app("com.target.app") then ... end -- 可能还没起来
原因:应用启动需要时间,应循环等待或先 wait。
wait_until(time)
系统
阻塞脚本直到指定时间。time 为时间字符串。
wait_until(time: string) → nil
| 参数 | 类型 | 必填 | 说明 |
| time | string | 是 | 目标时间字符串 |
✅ 正确
wait_until("12:30:00") -- 等到 12:30
❌ 错误:传时间戳数字
wait_until(1752000000) -- 被当作字符串处理,行为异常
原因:参数为字符串而非时间戳整数。
closeApp(package)
系统
关闭应用后台进程(killBackgroundProcesses)。需要权限,无权限返回 false。
closeApp(package: string) → boolean
✅ 正确
closeApp("com.target.app")
⚠️ 避坑:closeApp 仅清理后台进程,不一定能关闭前台应用;强制停止用 killApp(需 root)。
killApp(package)
系统
强制停止应用(forceStopPackage 隐藏 API)。需要 root 或系统签名权限,无权限返回 false。
killApp(package: string) → boolean
✅ 正确
killApp("com.target.app")
⚠️ 避坑:forceStopPackage 是隐藏 API,需 root 权限。普通设备无权限会返回 false。
pressPower()
系统
模拟电源键(GLOBAL_ACTION_POWER_DIALOG 弹出电源选项)。返回是否成功。
pressPower() → boolean
提示:会弹出关机/重启对话框,需配合其他操作关闭对话框。
pressVolume(volume)
系统
直接设置媒体音量到指定值(0 到最大音量)。返回是否成功。
pressVolume(volume: number) → boolean
✅ 正确
pressVolume(8) -- 设置媒体音量为 8
❌ 错误:传字符串方向
pressVolume("up") -- 参数是音量数值,非方向
原因:pressVolume 接收音量整数(直接设置),不是 "up"/"down"。增减音量用 volumeUp/volumeDown。
⚠️ 避坑:音量值会自动钳制到 [0, maxVol] 范围,超过最大值不会报错但无效。
volumeUp()
系统
媒体音量增大一档。返回是否成功。
volumeUp() → boolean
volumeDown()
系统
媒体音量减小一档。返回是否成功。
volumeDown() → boolean
lockDevice()
系统
锁屏(DevicePolicyManager.lockNow)。需要设备管理员权限,无权限返回 false。
lockDevice() → boolean
⚠️ 避坑:需要设备管理员(Device Admin)权限,普通应用无权限会返回 false。
unlockDevice()
系统
解锁设备。注意:服务上下文无法直接解锁,当前实现始终返回 false。
unlockDevice() → boolean
❌ 错误:依赖 unlockDevice 解锁
unlockDevice() -- 始终返回 false,无法解锁
原因:requestDismissKeyguard 需要 Activity 上下文,服务中无法调用。当前实现恒返回 false。
⚠️ 避坑:此函数当前不可用(恒返回 false)。如需解锁,应通过 KeyguardManager 在 Activity 中实现,或用 shell("input keyevent 26") 等方式。
setAirplaneMode(enabled)
系统
开关飞行模式。需要 WRITE_SECURE_SETTINGS 权限,无权限返回 false。
setAirplaneMode(enabled: boolean) → boolean
✅ 正确
setAirplaneMode(true) -- 开启飞行模式
setAirplaneMode(false) -- 关闭
⚠️ 避坑:需 WRITE_SECURE_SETTINGS 权限(通过 adb 授权)。无权限返回 false。
setWifi(enabled)
系统
开关 WiFi。Android 10+ 需 root 或系统签名才能调用。返回是否成功。
setWifi(enabled: boolean) → boolean
⚠️ 避坑:Android 10(API 29)起,第三方应用无法直接开关 WiFi,需 root 或系统签名。低版本可用。
setBluetooth(enabled)
系统
开关蓝牙。返回是否成功。设备不支持蓝牙时返回 false。
setBluetooth(enabled: boolean) → boolean
setBrightness(level)
系统
设置屏幕亮度(0–255)。需要 WRITE_SETTINGS 权限,无权限返回 false。
setBrightness(level: number) → boolean
✅ 正确
setBrightness(128) -- 中等亮度
❌ 错误:传 0-1 比例
setBrightness(0.5) -- 被当作 0
原因:范围是 0–255 整数,不是 0–1 比例。值会自动钳制到 [0,255]。
getBrightness()
系统
获取当前屏幕亮度(0–255)。失败返回 128。
getBrightness() → number
✅ 正确
local b = getBrightness()
log("当前亮度: " .. b)
setVolume(level)
系统
设置媒体音量(0 到最大音量,通常 0–15)。返回是否成功。
setVolume(level: number) → boolean
提示:与 pressVolume 功能相同,值自动钳制到 [0, maxVol]。
vibrate(duration)
系统
震动指定毫秒。需要 VIBRATE 权限,设备不支持震动或时长无效返回 false。
vibrate(duration: number) → boolean
✅ 正确
vibrate(500) -- 震动 0.5 秒
❌ 错误:duration 为 0 或负数
vibrate(0) -- 返回 false
原因:duration 必须 > 0,否则返回 false。
resetIDLETimer(keepOn)
系统
控制屏幕常亮。keepOn=true 开启 WakeLock 保持屏幕常亮,false 释放。返回是否成功。
resetIDLETimer(keepOn: boolean) → boolean
✅ 正确
resetIDLETimer(true) -- 脚本运行期间保持常亮
-- ... 脚本逻辑 ...
resetIDLETimer(false) -- 结束时释放
⚠️ 避坑:脚本结束前务必调用 resetIDLETimer(false) 释放 WakeLock,否则屏幕一直常亮耗电。
shell(cmd)
系统
执行 Shell 命令(5 秒超时)。返回命令输出字符串,超时或失败返回 nil。
shell(cmd: string) → string | nil
✅ 正确
local out = shell("pm list packages")
if out then log(out) end
❌ 错误:执行需 root 命令但不检查返回
shell("reboot") -- 普通权限下可能 nil
原因:需 root 的命令在无 root 时返回 nil。应检查返回值。
⚠️ 避坑:命令有 5 秒超时,长时间命令(如大文件下载)会被中断返回 nil。命令输出超过限制会被截断。
amStart(package, activity)
系统
通过 Intent 启动指定 Activity。activity 为空时启动应用主入口。返回是否成功。
amStart(package: string, activity: string) → boolean
✅ 正确
amStart("com.target.app", "com.target.app.MainActivity")
amStart("com.target.app", "") -- 启动主入口
❌ 错误:activity 名写错
amStart("com.target.app", "MainActivity") -- 缺包名前缀
原因:activity 需完整类名(含包名),如 com.target.app.MainActivity。
getProp(name)
系统
读取系统属性(getprop)。返回属性值字符串,失败返回 nil。
getProp(name: string) → string | nil
✅ 正确
local ver = getProp("ro.build.version.release")
log("安卓版本: " .. ver)
setProp(name, value)
系统
设置系统属性(setprop)。需要 root 权限,无权限返回 false。
setProp(name: string, value: string) → boolean
✅ 正确
setProp("debug.layout", "true")
⚠️ 避坑:setprop 需 root 权限,普通设备返回 false。
openURL(url)
系统
通过 ACTION_VIEW 打开 URL(网页/深链)。返回是否成功。
openURL(url: string) → boolean
✅ 正确
openURL("https://www.example.com")
openURL("market://details?id=com.target.app")
❌ 错误:URL 为空
openURL("") -- 返回 false
原因:URL 为空时返回 false。
🎨 13. 颜色与图像识别 25 个 API
颜色与图像识别是自动化脚本的核心能力,包含取色、找色、找图、OCR 文字识别、AI 视觉等。坐标类函数会受 setScreenScale 缩放影响;多点找色与找图基于屏幕截图实现,频繁调用会消耗性能。
get_color(x, y)
颜色
获取屏幕指定坐标的像素颜色,返回十六进制颜色字符串。
get_color(x: number, y: number) → string "#RRGGBB"
| 参数 | 类型 | 必填 | 说明 |
x | number | 是 | 横坐标(脚本坐标系像素) |
y | number | 是 | 纵坐标(脚本坐标系像素) |
✅ 正确
local color = get_color(100, 200)
log("颜色: " .. color) -- 输出如 #FF5733
if color == "#FF0000" then
log("该位置是红色")
end
❌ 错误:坐标越界
local color = get_color(-1, 99999) -- 返回 "#000000"
原因:坐标为负数或超出屏幕尺寸时,函数直接返回 "#000000"(黑色),不会报错。应先校验坐标范围。
⚠️ 避坑:坐标会先经过 scaleManager.scaleXY 缩放到实际屏幕坐标再取色。若启用了 setScreenScale,传入的坐标应是基准分辨率坐标。返回值始终是大写 #RRGGBB 格式。
find_color(color, sim?)
颜色
在全屏范围内查找指定颜色,返回包含坐标的 table 或 nil。
find_color(color: string, sim?: number) → table {x, y} | nil
| 参数 | 类型 | 必填 | 说明 |
color | string | 是 | 目标颜色,如 "#00FF00" 或 "0x00FF00" |
sim | number | 否 | 相似度 0.0-1.0,默认 0.9 |
✅ 正确
local pos = find_color("#00FF00", 20)
if pos then
log("找到绿色: " .. pos.x .. "," .. pos.y)
click(pos.x, pos.y)
else
log("未找到绿色")
end
❌ 错误:未判空直接取字段
local pos = find_color("#00FF00")
click(pos.x, pos.y) -- pos 为 nil 时报错
原因:未找到颜色时返回 nil,对 nil 取 .x 会抛出 "attempt to index a nil value"。必须先判空。
⚠️ 避坑:返回的坐标已反向缩放回脚本坐标系。相似度参数在源码中作为 double 接收,传 20 会被当作 20.0(远大于 1),实际匹配行为由底层 executeFindColor 决定,建议传 0.0-1.0 范围的小数。
is_color(x, y, color, sim?)
颜色
判断指定坐标的颜色是否与目标颜色匹配,返回布尔值。
is_color(x: number, y: number, color: string, sim?: number) → boolean
| 参数 | 类型 | 必填 | 说明 |
x | number | 是 | 横坐标 |
y | number | 是 | 纵坐标 |
color | string | 是 | 目标颜色 "#RRGGBB" |
sim | number | 否 | 相似度 0.0-1.0,默认 0.9 |
✅ 正确
if is_color(100, 200, "#FF0000", 0.9) then
log("颜色匹配红色")
click(100, 200)
end
❌ 错误:颜色格式错误
is_color(100, 200, "red") -- 返回 false
原因:颜色必须是十六进制格式(如 "#FF0000" 或 "0xFFFF0000"),写颜色英文名无法解析。
⚠️ 避坑:异常时返回 false 而非报错,因此"返回 false"既可能是颜色不匹配,也可能是截图失败。若需区分,请检查日志。坐标会先缩放再判断。
find_image(image_name, sim?, region?)
图像
在屏幕(或指定区域)查找模板图片,返回坐标 table 或 nil。图片需预先通过 LuaImageLoader 加载。
find_image(image_name: string, sim?: number, region?: table) → table {x, y} | nil
| 参数 | 类型 | 必填 | 说明 |
image_name | string | 是 | 图片名称(非路径,需已加载) |
sim | number | 否 | 相似度 0.0-1.0,默认 0.8 |
region | table | 否 | 查找区域 {x, y, w, h},默认全屏 |
✅ 正确
local pos = find_image("login_btn.png", 0.9, {x=0, y=0, w=540, h=200})
if pos then
click(pos.x, pos.y)
end
❌ 错误:图片名空
find_image("", 0.9) -- 返回 nil
原因:图片名为空字符串时直接返回 nil。图片必须先由引擎加载到 LuaImageLoader,否则返回 nil 并记录"图片未找到"。
⚠️ 避坑:region 的 table 键是 x/y/w/h(不是 x1/y1/x2/y2)。每次调用都会截图并匹配,在大区域高相似度下较耗时,建议缩小 region 提速。返回坐标已反向缩放。
ocr(x?, y?, w?, h?)
文字
对屏幕指定矩形区域进行 OCR 文字识别,返回识别出的文本字符串。
ocr(x?: number, y?: number, w?: number, h?: number) → string
| 参数 | 类型 | 必填 | 说明 |
x | number | 否 | 区域左上角横坐标,默认 0 |
y | number | 否 | 区域左上角纵坐标,默认 0 |
w | number | 否 | 区域宽度,默认 100 |
h | number | 否 | 区域高度,默认 100 |
✅ 正确
local text = ocr(100, 200, 300, 50)
log("识别结果: " .. text)
if string.find(text, "登录") then
click(250, 225)
end
❌ 错误:区域越界不裁剪
local text = ocr(0, 0, 99999, 99999) -- 被裁剪到屏幕范围
原因:函数会自动裁剪到屏幕边界,若裁剪后区域为空则返回空字符串。不会报错但结果可能非预期。
⚠️ 避坑:OCR 结果通过内部变量 __lua_ocr_result__ 中转,识别完成后立即移除,不会污染脚本变量空间。识别失败或无文字时返回空字符串 "",不是 nil。
ai_find_element(description, sim?, lang?)
AI
通过自然语言描述在屏幕中查找 UI 元素,返回坐标 table 或 nil。基于 AI 视觉模型实现。
ai_find_element(description: string, sim?: number, lang?: string) → table {x, y} | nil
| 参数 | 类型 | 必填 | 说明 |
description | string | 是 | 元素的自然语言描述 |
sim | number | 否 | 相似度 0.0-1.0,默认 0.9 |
lang | string | 否 | 语言,默认 "auto" |
✅ 正确
local pos = ai_find_element("蓝色的登录按钮", 0.9, "auto")
if pos then
click(pos.x, pos.y)
end
❌ 错误:描述为空
ai_find_element("") -- 返回 nil
原因:描述为空字符串时直接返回 nil,不发起 AI 请求。描述应尽量具体以提升识别准确率。
⚠️ 避坑:AI 识别依赖网络请求,耗时较长且可能失败。返回 nil 既可能是没找到也可能是网络异常,需结合日志判断。坐标已反向缩放回脚本坐标系。
ai_read_text(keyword?, lang?)
AI
使用 AI 读取屏幕上的文字内容,可指定关键词过滤。返回识别到的文本字符串。
ai_read_text(keyword?: string, lang?: string) → string
| 参数 | 类型 | 必填 | 说明 |
keyword | string | 否 | 关键词过滤,默认 ""(读全部) |
lang | string | 否 | 语言,默认 "auto" |
✅ 正确
local text = ai_read_text("验证码", "auto")
log("读到: " .. text)
❌ 错误:期望返回 table
local result = ai_read_text()
click(result.x, result.y) -- result 是 string,报错
原因:ai_read_text 返回的是字符串(文字内容),不是坐标 table。若要坐标请用 ai_find_element。
⚠️ 避坑:该函数只返回文字,不返回坐标。识别失败时返回空字符串。与 ocr 的区别:本函数用 AI 模型识别,准确率更高但更慢。
ai_check(description, lang?)
AI
用 AI 判断屏幕上是否存在描述的元素或状态,返回布尔值。
ai_check(description: string, lang?: string) → boolean
| 参数 | 类型 | 必填 | 说明 |
description | string | 是 | 要检查的描述(不可为空) |
lang | string | 否 | 语言,默认 "auto" |
✅ 正确
if ai_check("弹出了更新提示框", "auto") then
click(270, 800) -- 点击"稍后"
end
❌ 错误:描述为空
ai_check("") -- 返回 false
原因:描述为空时直接返回 false,不发起 AI 请求。与 ai_read_text 不同,本函数要求 description 必填且非空。
⚠️ 避坑:异常时返回 false。因此 false 可能是"不存在"也可能是"识别失败",需结合日志区分。适合做条件判断而非精确定位。
ai_smart_click(description, sim?, cross_lang?)
AI
AI 智能点击:自动识别描述的元素并点击。无返回值(始终返回 nil)。
ai_smart_click(description: string, sim?: number, cross_lang?: boolean) → nil
| 参数 | 类型 | 必填 | 说明 |
description | string | 是 | 元素描述 |
sim | number | 否 | 相似度,默认 0.9 |
cross_lang | boolean | 否 | 是否跨语言匹配,默认 false |
✅ 正确
ai_smart_click("提交按钮", 0.9, false)
wait(1000)
❌ 错误:依赖返回值判断
if ai_smart_click("提交按钮") then -- 永远是 nil
log("点击成功")
end
原因:本函数始终返回 nil,无法通过返回值判断是否点击成功。应改用 ai_find_element 先判断再点击,或点击后用 ai_check 验证。
⚠️ 避坑:描述为空时直接返回 nil 不执行任何操作。该函数是"识别+点击"的组合,耗时较长。无法获知是否真正点到,需后续状态判断。
ai_smart_wait(description, timeout?)
AI
AI 智能等待:阻塞脚本直到屏幕上出现描述的元素或超时。返回是否找到。
ai_smart_wait(description: string, timeout?: number) → boolean
| 参数 | 类型 | 必填 | 说明 |
description | string | 是 | 等待出现的元素描述 |
timeout | number | 否 | 超时毫秒,默认 30000 |
✅ 正确
if ai_smart_wait("加载完成", 10000) then
click(500, 800)
else
log("等待超时")
end
❌ 错误:描述为空
ai_smart_wait("") -- 返回 false
原因:描述为空时直接返回 false,不进入等待逻辑。该函数会阻塞脚本线程,超时默认 30 秒。
⚠️ 避坑:该函数阻塞 LUA 线程轮询识别,超时期间无法响应其他事件。timeout 过大会长时间卡住任务,建议按场景设置合理超时。异常时返回 false。
getColorRGB(x, y)
颜色
获取指定坐标的 RGB 三通道值,返回三个数值(多返回值)。
getColorRGB(x: number, y: number) → r, g, b (三个 number)
| 参数 | 类型 | 必填 | 说明 |
x | number | 是 | 横坐标 |
y | number | 是 | 纵坐标 |
✅ 正确
local r, g, b = getColorRGB(100, 200)
log(string.format("RGB(%d, %d, %d)", r, g, b))
if r > 200 and g < 50 then
log("偏红色")
end
❌ 错误:只接收一个值
local color = getColorRGB(100, 200) -- 只拿到 r
log(color) -- 丢失 g, b
原因:该函数返回三个值,用单个变量接收只会拿到第一个(r)。必须用 local r, g, b = getColorRGB(...) 接收全部。
⚠️ 避坑:解析失败或异常时返回 0, 0, 0(黑色)。值范围 0-255。与 get_color 的区别:本函数返回三个 number 方便单独判断通道,get_color 返回 hex 字符串。
findColorInRegion(color, x1, y1, x2, y2)
颜色
在指定矩形区域内精确查找颜色,返回坐标或 -1, -1。
findColorInRegion(color: string, x1: number, y1: number, x2: number, y2: number) → x, y (number)
| 参数 | 类型 | 必填 | 说明 |
color | string | 是 | 目标颜色 "#RRGGBB" |
x1 | number | 是 | 区域左上角横坐标 |
y1 | number | 是 | 区域左上角纵坐标 |
x2 | number | 是 | 区域右下角横坐标 |
y2 | number | 是 | 区域右下角纵坐标 |
✅ 正确
local x, y = findColorInRegion("#FF0000", 0, 0, 540, 960)
if x >= 0 then
click(x, y)
else
log("区域内未找到")
end
❌ 错误:用 nil 判断
local pos = findColorInRegion("#FF0000", 0, 0, 100, 100)
if pos then -- pos 是 number 不是 table
click(pos.x, pos.y)
end
原因:本函数返回两个 number(x, y),不是 table。未找到时返回 -1, -1 而非 nil。判断应用 if x >= 0 then。
⚠️ 避坑:本函数是精确匹配(threshold=0),颜色必须完全一致。若需容差请用 findColorInRegionFuzzy。坐标已反向缩放。截图失败也返回 -1, -1。
findColorInRegionFuzzy(color, sim, x1, y1, x2, y2)
颜色
在指定区域内模糊查找颜色(带相似度容差),返回坐标或 -1, -1。
findColorInRegionFuzzy(color: string, sim: number, x1: number, y1: number, x2: number, y2: number) → x, y (number)
| 参数 | 类型 | 必填 | 说明 |
color | string | 是 | 目标颜色 |
sim | number | 是 | 相似度 0.0-1.0(1.0=精确) |
x1 | number | 是 | 区域左上角横坐标 |
y1 | number | 是 | 区域左上角纵坐标 |
x2 | number | 是 | 区域右下角横坐标 |
y2 | number | 是 | 区域右下角纵坐标 |
✅ 正确
local x, y = findColorInRegionFuzzy("#FF0000", 0.9, 0, 0, 540, 960)
if x >= 0 then
click(x, y)
end
❌ 错误:sim 传 90
findColorInRegionFuzzy("#FF0000", 90, 0, 0, 100, 100) -- sim=90 错误
原因:sim 是 0.0-1.0 的小数,传 90 会被换算成 threshold=(1-90)*255 为负数,导致匹配异常。相似度 90% 应写 0.9。
⚠️ 避坑:源码中 sim 转 threshold 的公式是 (1-sim)*255。sim=1.0 时 threshold=0(精确),sim=0.9 时 threshold=25(容差 25)。坐标已反向缩放。
findMultiColor(color, pos_and_color, x1?, y1?, x2?, y2?, sim?)
颜色
多点找色:先定位主色,再验证若干偏移点的颜色是否匹配。返回主色坐标或 -1, -1。
findMultiColor(color: string, pos_and_color: string, x1?: number, y1?: number, x2?: number, y2?: number, sim?: number) → x, y (number)
| 参数 | 类型 | 必填 | 说明 |
color | string | 是 | 主色 "#RRGGBB" |
pos_and_color | string | 是 | 偏移点串 "dx|dy|颜色,..." |
x1 | number | 否 | 区域左上角横坐标,默认 0 |
y1 | number | 否 | 区域左上角纵坐标,默认 0 |
x2 | number | 否 | 区域右下角横坐标,默认屏幕宽 |
y2 | number | 否 | 区域右下角纵坐标,默认屏幕高 |
sim | number | 否 | 相似度 0.0-1.0,默认 0.9 |
✅ 正确
local x, y = findMultiColor("#FF0000", "0|0|#FF0000,10|10|#00FF00", 0, 0, 540, 960, 0.9)
if x >= 0 then
log("多点匹配: " .. x .. "," .. y)
end
❌ 错误:偏移点格式错
findMultiColor("#FF0000", "10,10,#00FF00") -- 返回 -1,-1
原因:偏移点格式必须是 "dx|dy|颜色",用竖线 | 分隔,多个点用逗号 , 分隔。用逗号代替竖线会导致解析失败,偏移点列表为空。
⚠️ 避坑:算法采用 1/4 降采样粗找+原分辨率精验,有 5 秒超时保护。偏移点坐标是相对于主色的偏移量(可为负数)。坐标已反向缩放。偏移点解析为空时返回 -1, -1。
findMultiColorInRegionFuzzy(color, pos_and_color, x1?, y1?, x2?, y2?, sim?)
颜色
多点模糊找色。参数与行为与 findMultiColor 完全一致(内部调用同一实现)。
findMultiColorInRegionFuzzy(color: string, pos_and_color: string, x1?: number, y1?: number, x2?: number, y2?: number, sim?: number) → x, y (number)
| 参数 | 类型 | 必填 | 说明 |
color | string | 是 | 主色 |
pos_and_color | string | 是 | 偏移点串 "dx|dy|颜色,..." |
x1-sim | number | 否 | 区域与相似度,默认值同 findMultiColor |
✅ 正确
local x, y = findMultiColorInRegionFuzzy("#FF0000", "5|5|#CC0000", 0, 0, 540, 960, 0.85)
if x >= 0 then click(x, y) end
❌ 错误:以为与 findMultiColor 不同
-- 两者实际调用同一个 doFindMultiColor 实现
findMultiColorInRegionFuzzy(...) -- 与 findMultiColor 行为完全相同
原因:源码中 findMultiColorInRegionFuzzy 与 findMultiColor 调用同一个 doFindMultiColor 方法,行为完全一致,仅函数名不同以兼容不同命名习惯。
⚠️ 避坑:该函数与 findMultiColor 是等价别名(同实现),不要期望它有额外的模糊特性。选择其一即可,避免重复调用浪费性能。
isColorApprox(x, y, color, sim?)
颜色
判断指定坐标颜色是否近似匹配目标色(带相似度),是 is_color 的驼峰增强版。
isColorApprox(x: number, y: number, color: string, sim?: number) → boolean
| 参数 | 类型 | 必填 | 说明 |
x | number | 是 | 横坐标 |
y | number | 是 | 纵坐标 |
color | string | 是 | 目标颜色 |
sim | number | 否 | 相似度 0.0-1.0,默认 0.9 |
✅ 正确
if isColorApprox(100, 200, "#FF0000", 0.85) then
log("近似红色")
end
❌ 错误:与 is_color 混淆签名
isColorApprox("#FF0000", 100, 200, 0.9) -- 参数顺序错
原因:参数顺序是 (x, y, color, sim),不是 (color, x, y, sim)。顺序错误会导致颜色解析失败返回 false。
⚠️ 避坑:本函数与 is_color 内部都调用 engine.executeIsColor,行为一致,仅命名风格不同(驼峰 vs 下划线)。异常时返回 false。坐标会缩放。
findImageInRegion(img, x1?, y1?, x2?, y2?)
图像
在指定区域内查找模板图片(相似度固定 0.8),返回坐标 table 或 nil。
findImageInRegion(img: string, x1?: number, y1?: number, x2?: number, y2?: number) → table {x, y} | nil
| 参数 | 类型 | 必填 | 说明 |
img | string | 是 | 图片名称 |
x1 | number | 否 | 区域左上角横坐标,默认 0 |
y1 | number | 否 | 区域左上角纵坐标,默认 0 |
x2 | number | 否 | 区域右下角横坐标,默认屏幕宽 |
y2 | number | 否 | 区域右下角纵坐标,默认屏幕高 |
✅ 正确
local pos = findImageInRegion("btn.png", 0, 0, 540, 500)
if pos then click(pos.x, pos.y) end
❌ 错误:期望传 sim
findImageInRegion("btn.png", 0.9, 0, 0, 540) -- 0.9 被当作 x1
原因:本函数无 sim 参数(固定 0.8)。第二个参数是 x1,传 0.9 会被转成 0(toint 截断)。需自定义相似度请用 findImageInRegionFuzzy。
⚠️ 避坑:相似度固定 0.8 不可调。返回坐标 table(含 x, y 字段),已反向缩放。图片未加载时返回 nil。与 find_image 的区别:本函数用 x1/y1/x2/y2 描述区域,find_image 用 {x,y,w,h}。
findImageInRegionFuzzy(img, sim?, x1?, y1?, x2?, y2?)
图像
在指定区域内以可调相似度查找模板图片,返回坐标 table 或 nil。
findImageInRegionFuzzy(img: string, sim?: number, x1?: number, y1?: number, x2?: number, y2?: number) → table {x, y} | nil
| 参数 | 类型 | 必填 | 说明 |
img | string | 是 | 图片名称 |
sim | number | 否 | 相似度,默认 0.8 |
x1 | number | 否 | 区域左上角横坐标,默认 0 |
y1 | number | 否 | 区域左上角纵坐标,默认 0 |
x2 | number | 否 | 区域右下角横坐标,默认屏幕宽 |
y2 | number | 否 | 区域右下角纵坐标,默认屏幕高 |
✅ 正确
local pos = findImageInRegionFuzzy("btn.png", 0.95, 0, 0, 540, 500)
if pos then click(pos.x, pos.y) end
❌ 错误:参数顺序错
findImageInRegionFuzzy("btn.png", 0, 0, 540, 500, 0.95) -- 0.95 被当 y2
原因:参数顺序是 (img, sim, x1, y1, x2, y2),sim 在区域参数之前。顺序错误会导致区域异常。注意与 find_image(sim 在前)一致。
⚠️ 避坑:注意与 findColorInRegionFuzzy 的参数顺序不同:找色是 (color, sim, x1, y1, x2, y2),找图是 (img, sim, x1, y1, x2, y2),看似一致但找色的 sim 是第 2 个必填,找图的 sim 是第 2 个可选。返回 table 已反向缩放。
snapshot(path?, x1?, y1?, x2?, y2?)
图像
截取屏幕(或指定区域)并保存为 PNG 文件,返回是否成功。
snapshot(path?: string, x1?: number, y1?: number, x2?: number, y2?: number) → boolean
| 参数 | 类型 | 必填 | 说明 |
path | string | 否 | 保存路径(受限安全目录) |
x1 | number | 否 | 区域左上角横坐标,默认 0 |
y1 | number | 否 | 区域左上角纵坐标,默认 0 |
x2 | number | 否 | 区域右下角横坐标,默认 0(全屏) |
y2 | number | 否 | 区域右下角纵坐标,默认 0(全屏) |
✅ 正确
local ok = snapshot("shot.png", 0, 0, 540, 960)
if ok then log("截图已保存") end
❌ 错误:路径越界
snapshot("/sdcard/test.png") -- 返回 false
原因:路径经 FileUtils.safePath 检查,只能保存到脚本目录内。绝对路径或不安全路径返回 false。应使用相对路径如 "shot.png"。
⚠️ 避坑:当 x2>y1 且 y2>y1 时才裁剪区域,否则保存全屏。区域裁剪在原图坐标系(非脚本缩放坐标)上执行。保存格式固定 PNG。
keepScreen(flag)
图像
设置截图缓存标志(当前为空实现,仅记录日志,不影响功能)。
keepScreen(flag: boolean) → nil
| 参数 | 类型 | 必填 | 说明 |
flag | boolean | 否 | 是否缓存截图,默认 false |
✅ 正确
keepScreen(true) -- 当前仅记录日志,无实际效果
local pos = find_color("#FF0000")
❌ 错误:依赖缓存提速
keepScreen(true)
find_color("#FF0000")
find_color("#00FF00") -- 期望复用截图,实际仍各自截图
原因:当前为空实现(暂未实现缓存逻辑),每次找色/找图仍独立截图。不能依赖它提升性能。
⚠️ 避坑:源码注释明确标注"暂时空实现,后续版本补充"。调用不会报错但也不产生缓存效果。如需减少截图开销,应减少找色/找图调用次数。
getScreen()
图像
获取屏幕像素的二维 table(已缩放至最大 512x512),返回 table 或 nil。
getScreen() → table | nil
✅ 正确
local screen = getScreen()
if screen then
local pixel = screen[1][1] -- 左上角像素 ARGB int
log("像素值: " .. pixel)
end
❌ 错误:索引从 0 开始
local screen = getScreen()
local pixel = screen[0][0] -- nil,LUA 索引从 1 开始
原因:返回的 table 索引从 1 开始(screen[y][x],y,x 均从 1)。用 0 索引会得到 nil。像素值是 ARGB 整数(非 hex 字符串)。
⚠️ 避坑:为控制内存,屏幕会等比缩放至最长边 512 像素。因此 table 尺寸可能小于实际分辨率。像素值是 Bitmap.getPixel 返回的 ARGB int(如 0xFF0000FF 表示不透明蓝色)。截图失败返回 nil。
setScreenScale(flag, w?, h?)
图像
启用/禁用屏幕坐标缩放。启用后,脚本使用基准分辨率坐标,引擎自动换算到实际分辨率。
setScreenScale(flag: boolean|number, w?: number, h?: number) → nil
| 参数 | 类型 | 必填 | 说明 |
flag | boolean|number | 是 | true/非0=启用,false/0=禁用 |
w | number | 否 | 基准分辨率宽,启用时必填 |
h | number | 否 | 基准分辨率高,启用时必填 |
✅ 正确
setScreenScale(true, 540, 960) -- 基准 540x960
click(270, 480) -- 自动缩放到实际屏幕中心
setScreenScale(false) -- 关闭缩放
❌ 错误:启用时不传 w/h
setScreenScale(true) -- w=0, h=0,缩放异常
原因:启用缩放时需提供基准分辨率 w 和 h,否则 scaleManager.enable 接收 0,0 导致缩放比例异常。禁用时可不传。
⚠️ 避坑:flag 支持布尔或数字(0=false,非0=true),这是为兼容部分脚本传数字。启用后所有坐标类函数(click/get_color/find_color 等)都会自动缩放,返回坐标也会反向缩放回基准坐标系。
ocrText(x1?, y1?, x2?, y2?)
文字
对屏幕区域进行 OCR 识别(ocr 的别名,参数为两对角坐标),返回文本字符串。
ocrText(x1?: number, y1?: number, x2?: number, y2?: number) → string
| 参数 | 类型 | 必填 | 说明 |
x1 | number | 否 | 左上角横坐标,默认 0 |
y1 | number | 否 | 左上角纵坐标,默认 0 |
x2 | number | 否 | 右下角横坐标,默认 0 |
y2 | number | 否 | 右下角纵坐标,默认 0 |
✅ 正确
local text = ocrText(100, 200, 400, 250)
log("识别: " .. text)
❌ 错误:与 ocr 参数混淆
ocrText(100, 200, 300, 50) -- 把 300 当 w,50 当 h
原因:ocr 参数是 (x, y, w, h)(宽高),ocrText 参数是 (x1, y1, x2, y2)(两角点)。混用会导致区域错误。ocrText 内部计算 w=x2-x1, h=y2-y1。
⚠️ 避坑:当 x2<=x1 或 y2<=y1 时,w/h 默认用 100。即不传任何参数时识别 100x100 的左上角区域。与 ocr 功能相同,仅参数语义不同(角点 vs 宽高)。
findText(text, x1?, y1?, x2?, y2?)
文字
在指定区域 OCR 识别后查找是否包含目标文字,返回区域中心坐标或 -1, -1。
findText(text: string, x1?: number, y1?: number, x2?: number, y2?: number) → x, y (number)
| 参数 | 类型 | 必填 | 说明 |
text | string | 是 | 要查找的文字 |
x1 | number | 否 | 左上角横坐标,默认 0 |
y1 | number | 否 | 左上角纵坐标,默认 0 |
x2 | number | 否 | 右下角横坐标,默认屏幕宽 |
y2 | number | 否 | 右下角纵坐标,默认屏幕高 |
✅ 正确
local x, y = findText("登录", 0, 0, 540, 960)
if x >= 0 then
click(x, y) -- 点击区域中心
end
❌ 错误:期望精确坐标
local x, y = findText("登录")
click(x, y) -- x,y 是区域中心非文字位置
原因:返回的是区域中心坐标(x1+w/2, y1+h/2),不是文字的实际像素位置。OCR 不返回文字坐标,只能近似点击区域中心。
⚠️ 避坑:该函数用 ocrText.contains(text) 判断是否包含,是精确子串匹配(非模糊)。区域无效(w<=0 或 h<=0)时返回 -1, -1。缩小区域可提升准确度。
findTextInRegionFuzzy(text, sim?, x1?, y1?, x2?, y2?)
文字
在指定区域 OCR 后用相似度模糊匹配文字,返回区域中心坐标或 -1, -1。
findTextInRegionFuzzy(text: string, sim?: number, x1?: number, y1?: number, x2?: number, y2?: number) → x, y (number)
| 参数 | 类型 | 必填 | 说明 |
text | string | 是 | 目标文字 |
sim | number | 否 | 相似度 0.0-1.0,默认 0.8 |
x1 | number | 否 | 左上角横坐标,默认 0 |
y1 | number | 否 | 左上角纵坐标,默认 0 |
x2 | number | 否 | 右下角横坐标,默认屏幕宽 |
y2 | number | 否 | 右下角纵坐标,默认屏幕高 |
✅ 正确
local x, y = findTextInRegionFuzzy("登陆", 0.7, 0, 0, 540, 960)
if x >= 0 then click(x, y) end
❌ 错误:sim 传 70
findTextInRegionFuzzy("登陆", 70, 0, 0, 540, 960) -- sim=70
原因:sim 是 0.0-1.0 的小数,传 70 会让任何文字都匹配(因为相似度计算最大为 1.0,70 永远满足)。70% 应写 0.7。
⚠️ 避坑:相似度算法是字符匹配率(非编辑距离),OCR 结果包含目标文字时直接返回 1.0。返回区域中心坐标非文字精确位置。区域无效返回 -1, -1。
🔣 14. 数据处理 16 个 API
数据处理涵盖日志输出、任务级变量、持久化全局变量、JSON/Base64 编解码、哈希摘要、URL 编码、字符串分割等。其中 json 和 base64 是 table 命名空间,需用点号调用(如 json.encode)。
log(message)
数据
向任务日志输出一条消息(会自动加 "Log: " 前缀)。与 print 类似但走引擎日志通道。
log(message: any) → nil
| 参数 | 类型 | 必填 | 说明 |
message | any | 是 | 日志内容(会被 tojstring 转字符串) |
✅ 正确
log("开始执行任务")
log("当前积分: " .. score)
log(123) -- 数字会被转为字符串
❌ 错误:直接 log table
local t = {1, 2, 3}
log(t) -- 输出 "Log: table: 0x...",无意义
原因:table 会被 tojstring 转成 "table: 0x..." 地址,看不到内容。应先用 json.encode(t) 序列化再 log。
⚠️ 避坑:参数会经 tojstring 转换,nil 会变成 "nil",boolean 变成 "true"/"false"。日志会发送到 B 端日志区,大量 log 会影响性能,调试完应清理。
set_variable(name, value)
数据
设置任务级变量(仅在当前任务执行期间有效,任务结束即销毁)。
set_variable(name: string, value: any) → nil
| 参数 | 类型 | 必填 | 说明 |
name | string | 是 | 变量名 |
value | any | 是 | 变量值(存为字符串) |
✅ 正确
set_variable("count", 5)
set_variable("username", "test_user")
local c = get_variable("count") -- "5"
❌ 错误:期望存数字取数字
set_variable("count", 5)
local c = get_variable("count")
log(c + 1) -- c 是 "5" 字符串,算术运算报错
原因:值经 tojstring 存为字符串,取回的也是字符串。做算术前需 tonumber(c) 转换。
⚠️ 避坑:变量存储在 engine.variables(Map),是任务级非持久化的。任务停止或 C 端重启后丢失。如需跨任务保留请用 set_global。value 会被转字符串,table 无法还原。
get_variable(name)
数据
读取任务级变量的值,返回字符串或 nil。
get_variable(name: string) → string | nil
✅ 正确
local val = get_variable("count")
if val then
log("count = " .. tonumber(val))
else
log("变量不存在")
end
❌ 错误:未判空直接用
local val = get_variable("missing")
log(val:upper()) -- val 为 nil 时报错
原因:变量不存在时返回 nil,对 nil 调用字符串方法会抛出 "attempt to index a nil value"。必须先判空。
⚠️ 避坑:返回值始终是字符串(因为存储时已 tojstring)。数字 5 取回是 "5",boolean true 取回是 "true"。需要原始类型需自行转换。
set_global(name, value)
数据
设置持久化全局变量(保存到磁盘,跨任务保留,C 端重启后仍存在)。会覆盖已有值。
set_global(name: string, value: any) → nil
| 参数 | 类型 | 必填 | 说明 |
name | string | 是 | 变量名(不可为空) |
value | any | 是 | 变量值(存为字符串) |
✅ 正确
set_global("total_runs", 100)
local v = get_global("total_runs")
log("累计运行: " .. v)
❌ 错误:变量名为空
set_global("", "value") -- 返回 nil,不保存
原因:变量名为空字符串时直接返回 nil 不保存。源码中有 if (varName.isEmpty()) 检查。
⚠️ 避坑:每次调用都会触发 saveGlobalVariables 写磁盘,频繁调用(如循环内)会拖慢性能。建议批量修改后一次性保存,或用 set_variable 做临时存储。会覆盖已有值,若需"仅首次设置"请用 init_global。
init_global(name, value)
数据
初始化全局变量:仅当变量不存在时才设置。返回是否为首次设置。
init_global(name: string, value: any) → boolean
| 参数 | 类型 | 必填 | 说明 |
name | string | 是 | 变量名(不可为空) |
value | any | 是 | 初始值 |
✅ 正确
if init_global("first_run", "yes") then
log("首次运行,初始化配置")
set_global("config_version", "1.0")
else
log("非首次运行")
end
❌ 错误:期望更新值
init_global("count", 1)
init_global("count", 2) -- count 仍是 1
log(get_global("count")) -- "1"
原因:init_global 用 putIfAbsent 实现,变量已存在时不覆盖。第二次调用返回 false 且值不变。需更新请用 set_global。
⚠️ 避坑:返回 true 表示首次设置(变量原不存在),false 表示已存在被跳过。变量名为空时返回 false。首次设置会写磁盘,已存在时不写磁盘。
get_global(name)
数据
读取持久化全局变量的值,返回字符串或 nil。
get_global(name: string) → string | nil
| 参数 | 类型 | 必填 | 说明 |
name | string | 是 | 变量名(不可为空) |
✅ 正确
local v = get_global("total_runs")
if v ~= nil then
local n = tonumber(v) + 1
set_global("total_runs", n)
end
❌ 错误:变量名为空
get_global("") -- 返回 nil
原因:变量名为空时直接返回 nil 不查询。源码中有 if (varName.isEmpty()) 检查。
⚠️ 避坑:返回值是字符串或 nil(不存在时)。与 get_variable 的区别:本函数读持久化磁盘存储,get_variable 读内存任务级存储。异常时返回 nil。
json.encode(table)
数据
将 LUA table 序列化为 JSON 字符串。支持嵌套 table、数字、字符串、布尔值。
json.encode(table: table) → string | nil
| 参数 | 类型 | 必填 | 说明 |
table | table | 是 | 要序列化的 LUA table |
✅ 正确
local data = {name = "test", age = 18, scores = {90, 85, 95}}
local json_str = json.encode(data)
log(json_str) -- {"name":"test","age":18,"scores":[90,85,95]}
❌ 错误:传非 table
json.encode("hello") -- 返回 nil
json.encode(123) -- 返回 nil
原因:参数非 table 时返回 nil。源码中 if (!arg.istable()) 检查,字符串和数字都会被拒绝。
⚠️ 避坑:整数会被正确输出为数字(不带小数点),浮点数正常输出。key 会被转字符串(数字 key 也会变成字符串 key)。含循环引用的 table 会导致栈溢出。异常时返回 nil。
json.decode(str)
数据
将 JSON 字符串解析为 LUA 值(table/string/number/boolean/nil)。
json.decode(str: string) → LuaValue (table|string|number|boolean|nil)
| 参数 | 类型 | 必填 | 说明 |
str | string | 是 | JSON 字符串 |
✅ 正确
local result = json.decode('{"name":"test","age":18}')
if result then
log(result.name) -- "test"
log(result.age) -- 18
end
❌ 错误:JSON 格式非法
local result = json.decode("{invalid}")
log(result.name) -- result 为 nil 时报错
原因:JSON 格式错误时解析抛异常,返回 nil。对 nil 取 .name 报错。应先判空。空字符串也返回 nil。
⚠️ 避坑:JSON 数组会被转为 1-based 索引的 table([10,20] → table[1]=10, table[2]=20)。JSON null 转为 LUA nil。解析失败返回 nil,需判空。数字会区分整数和浮点数。
base64.encode(str)
数据
将字符串进行 Base64 编码(UTF-8,无换行)。
base64.encode(str: string) → string | nil
| 参数 | 类型 | 必填 | 说明 |
str | string | 是 | 要编码的字符串 |
✅ 正确
local encoded = base64.encode("Hello, 世界")
log(encoded) -- SGVsbG8sIOS4lueVjA==
local decoded = base64.decode(encoded)
❌ 错误:直接编码 table
base64.encode({1, 2, 3}) -- 编码 "table: 0x..."
原因:参数经 tojstring 转换,table 会变成地址字符串再编码,结果无意义。应先 json.encode 再 base64。
⚠️ 避坑:使用 Base64.NO_WRAP 模式(不含换行符)。编码中文需注意源字符串是 UTF-8。异常时返回 nil。编码不是加密,不应用于敏感数据保护。
base64.decode(str)
数据
将 Base64 字符串解码为原始字符串(UTF-8)。
base64.decode(str: string) → string | nil
| 参数 | 类型 | 必填 | 说明 |
str | string | 是 | Base64 编码字符串 |
✅ 正确
local decoded = base64.decode("SGVsbG8=")
log(decoded) -- Hello
❌ 错误:解码非法 Base64
base64.decode("!!!not-base64!!!") -- 返回乱码或 nil
原因:非法 Base64 字符串解码行为不确定(Android Base64.DEFAULT 容错性强),可能返回乱码而非 nil。应确保输入是合法 Base64。
⚠️ 避坑:使用 Base64.DEFAULT 模式解码。解码结果按 UTF-8 转字符串,若原文是二进制数据可能损坏。异常时返回 nil。建议与 base64.encode 配对使用。
md5(str)
数据
计算字符串的 MD5 哈希值,返回 32 位小写十六进制字符串。
md5(str: string) → string (32位小写hex)
| 参数 | 类型 | 必填 | 说明 |
str | string | 是 | 要哈希的字符串 |
✅ 正确
local hash = md5("hello")
log(hash) -- 5d41402abc4b2a76b9719d911017c592
if md5(input) == "5d41402abc4b2a76b9719d911017c592" then
log("匹配")
end
❌ 错误:期望大写
local hash = md5("hello")
if hash == "5D41402ABC4B2A76B9719D911017C592" then -- 永远 false
log("匹配")
end
原因:返回值始终是小写 hex。与大写比较永远不等。如需大写请 string.upper(hash) 转换。
⚠️ 避坑:输入按 UTF-8 编码后计算。MD5 已不安全,不要用于密码存储或安全场景,仅适合做校验和/去重标识。异常时返回 nil。结果固定 32 字符。
sha1(str)
数据
计算字符串的 SHA-1 哈希值,返回 40 位小写十六进制字符串。
sha1(str: string) → string (40位小写hex)
| 参数 | 类型 | 必填 | 说明 |
str | string | 是 | 要哈希的字符串 |
✅ 正确
local hash = sha1("hello")
log(hash) -- aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
❌ 错误:与 md5 返回长度混淆
local hash = sha1("hello")
if #hash == 32 then -- SHA-1 是 40 字符
log("错误判断")
end
原因:SHA-1 返回 40 字符,MD5 返回 32 字符。用 MD5 的长度判断 SHA-1 结果会出错。
⚠️ 避坑:返回小写 hex,40 字符固定长度。输入按 UTF-8 编码。SHA-1 安全性已 weakened,不建议用于新安全场景。异常时返回 nil。
sha256(str)
数据
计算字符串的 SHA-256 哈希值,返回 64 位小写十六进制字符串。安全性高于 MD5 和 SHA-1。
sha256(str: string) → string (64位小写hex)
| 参数 | 类型 | 必填 | 说明 |
str | string | 是 | 要哈希的字符串 |
✅ 正确
local hash = sha256("hello")
log(hash) -- 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c...
log(#hash) -- 64
❌ 错误:截断比较
local hash = sha256("hello")
if string.sub(hash, 1, 32) == expected then -- 只比前 32 位
log("错误:碰撞风险")
end
原因:只比较前 32 字符大幅增加碰撞概率。应比较完整 64 字符。SHA-256 的安全性依赖完整摘要。
⚠️ 避坑:返回小写 hex,64 字符固定长度。输入按 UTF-8 编码。是三个哈希函数中最安全的,适合签名校验。异常时返回 nil。
urlEncode(str)
数据
对字符串进行 URL 编码(百分号编码),用于拼接 URL 查询参数。
urlEncode(str: string) → string | nil
| 参数 | 类型 | 必填 | 说明 |
str | string | 是 | 要编码的字符串 |
✅ 正确
local q = urlEncode("hello world&foo=bar")
local url = "https://api.example.com/search?q=" .. q
httpGet(url) -- q=hello+world%26foo%3Dbar
❌ 错误:未编码直接拼
local q = "hello world&foo=bar"
httpGet("https://api.example.com/search?q=" .. q)
-- & 被当作参数分隔符,foo=bar 变成新参数
原因:含 & = 空格 等特殊字符的值未编码会破坏 URL 结构。必须 urlEncode 后再拼接。
⚠️ 避坑:使用 URLEncoder.encode(str, "UTF-8"),空格会被编码为 +(不是 %20)。异常时返回 nil。中文会被编码为 %XX 序列。
urlDecode(str)
数据
对 URL 编码的字符串进行解码,还原原始字符串。
urlDecode(str: string) → string | nil
| 参数 | 类型 | 必填 | 说明 |
str | string | 是 | URL 编码的字符串 |
✅ 正确
local decoded = urlDecode("hello+world%26foo%3Dbar")
log(decoded) -- hello world&foo=bar
❌ 错误:解码后未校验
local name = urlDecode(user_input)
httpGet("https://api.com?name=" .. name) -- name 含特殊字符破坏 URL
原因:解码后得到原始特殊字符,若再用于拼接 URL 需重新 urlEncode。解码和编码不应连续调用(等于没操作)。
⚠️ 避坑:使用 URLDecoder.decode(str, "UTF-8"),+ 会被解码为空格。非法 %XX 序列可能抛异常返回 nil。与 urlEncode 互为逆操作。
split(s, sep)
数据
按分隔符拆分字符串,返回 1-based 索引的 table。分隔符按字面量匹配(非正则)。
split(s: string, sep: string) → table (1-based 索引)
| 参数 | 类型 | 必填 | 说明 |
s | string | 是 | 要拆分的字符串 |
sep | string | 是 | 分隔符(字面量,非正则) |
✅ 正确
local parts = split("a,b,c,d", ",")
for i = 1, #parts do
log(parts[i]) -- a, b, c, d
end
log(#parts) -- 4
❌ 错误:索引从 0 开始
local parts = split("a,b,c", ",")
log(parts[0]) -- nil
log(parts[1]) -- "a"
原因:返回的 table 是 1-based 索引(源码中 table.set(i+1, ...))。parts[0] 是 nil。遍历应从 1 开始。
⚠️ 避坑:分隔符用 Pattern.quote 转义,因此 . | * 等正则元字符按字面量匹配(这点与 LUA 标准 string.split 不同)。使用 limit=-1 保留尾部空字符串。异常时返回空 table。
📁 15. 文件 IO 13 个 API
文件 IO 提供读写文件、按行处理、文件存在性/大小/删除/重命名/复制/移动、目录创建与列举等能力。所有路径均经 FileUtils.safePath 安全校验,只能访问脚本目录内文件,绝对路径或越界路径会被拒绝。
readFile(path)
文件
读取文件全部内容为字符串。文件不存在或路径不安全时返回 nil。
readFile(path: string) → string | nil
| 参数 | 类型 | 必填 | 说明 |
path | string | 是 | 相对路径(脚本目录内) |
✅ 正确
local content = readFile("config.txt")
if content then
log("内容: " .. content)
else
log("文件不存在或读取失败")
end
❌ 错误:绝对路径
readFile("/sdcard/config.txt") -- 返回 nil
原因:绝对路径经 safePath 检查会被拒绝(返回 null),导致读取失败返回 nil。应使用相对路径如 "config.txt"。
⚠️ 避坑:文件必须位于脚本目录内(由 getExternalFilesDir 决定)。返回完整字符串含换行符。大文件会占用内存,建议用 readLines 按行处理大文件。
writeFile(path, content)
文件
将字符串写入文件(覆盖模式)。返回是否成功。
writeFile(path: string, content: string) → boolean
| 参数 | 类型 | 必填 | 说明 |
path | string | 是 | 相对路径 |
content | string | 是 | 文件内容(覆盖原有) |
✅ 正确
local ok = writeFile("log.txt", "第一行内容\n第二行")
if ok then log("写入成功") end
❌ 错误:期望追加
writeFile("log.txt", "新内容") -- 覆盖旧内容
原因:writeFile 是覆盖模式,会清空原有内容。需追加请用 appendFile。
⚠️ 避坑:路径不安全时返回 false。父目录不存在时可能写入失败。content 会经 tojstring 转换,传 table 会写入 "table: 0x..."。异常时返回 false。
appendFile(path, content)
文件
将内容追加到文件末尾(文件不存在则创建)。返回是否成功。
appendFile(path: string, content: string) → boolean
| 参数 | 类型 | 必填 | 说明 |
path | string | 是 | 相对路径 |
content | string | 是 | 追加的内容 |
✅ 正确
for i = 1, 5 do
appendFile("run.log", "第" .. i .. "次运行\n")
end
❌ 错误:忘加换行
appendFile("log.txt", "第一行")
appendFile("log.txt", "第二行") -- 变成"第一行第二行"
原因:追加不会自动加换行符,需在内容末尾手动加 \n。否则所有追加内容连成一行。
⚠️ 避坑:路径经 safePath 检查。频繁追加(如循环内)会有 IO 开销,建议批量拼接后一次写入。异常时返回 false。
readLines(path)
文件
按行读取文件,返回 1-based 索引的字符串 table。失败返回 nil。
readLines(path: string) → table | nil
✅ 正确
local lines = readLines("data.csv")
if lines then
for i = 1, #lines do
log("行" .. i .. ": " .. lines[i])
end
end
❌ 错误:索引从 0
local lines = readLines("data.csv")
log(lines[0]) -- nil
原因:返回的 table 是 1-based 索引(源码 table.set(i+1, ...))。lines[0] 是 nil,应从 1 开始遍历。
⚠️ 避坑:文件不存在或路径不安全返回 nil,需判空。空文件返回空 table(非 nil)。每行不含换行符。适合处理结构化文本。
writeLines(path, lines)
文件
将字符串 table 按行写入文件(覆盖模式)。返回是否成功。
writeLines(path: string, lines: table) → boolean
| 参数 | 类型 | 必填 | 说明 |
path | string | 是 | 相对路径 |
lines | table | 是 | 字符串数组(每行一个元素) |
✅ 正确
local data = {"姓名,年龄", "张三,25", "李四,30"}
writeLines("users.csv", data)
❌ 错误:传字符串
writeLines("out.txt", "一行内容") -- 字符串非 table
原因:第二个参数应为 table。传字符串时 istable() 为 false,luaTableToStringArray 收到 null 返回空数组,写入空文件。
⚠️ 避坑:lines 非 table 时写入空文件(不报错)。table 中每个元素会被 tojstring 转换。覆盖模式,原有内容被清除。异常时返回 false。
fileExist(path)
文件
检查文件是否存在。返回布尔值。
fileExist(path: string) → boolean
✅ 正确
if fileExist("config.json") then
local content = readFile("config.json")
else
log("配置文件不存在,使用默认值")
end
❌ 错误:路径不安全不报错
fileExist("/etc/passwd") -- 返回 false,非报错
原因:路径不安全时返回 false(与文件不存在行为一致),不抛异常。无法区分"路径被拒"和"文件不存在"。
⚠️ 避坑:返回 false 既可能是文件不存在,也可能是路径被安全检查拒绝。异常时也返回 false。需区分时查看日志。
fileSize(path)
文件
获取文件大小(字节数)。文件不存在返回 -1。
fileSize(path: string) → number (字节)
✅ 正确
local size = fileSize("data.bin")
if size >= 0 then
log("文件大小: " .. size .. " 字节")
end
❌ 错误:用 nil 判断
local size = fileSize("missing.txt")
if size then -- size 是 -1 不是 nil,恒为 true
log("文件存在")
end
原因:文件不存在时返回 -1(number),不是 nil。if size then 对 -1 为 true。应判断 if size >= 0 then。
⚠️ 避坑:返回值是 number 类型。文件不存在或异常时返回 -1。路径不安全也返回 -1。目录的大小返回 0 或 -1(取决于实现)。
fileRemove(path)
文件
删除文件。返回是否成功。
fileRemove(path: string) → boolean
✅ 正确
if fileExist("temp.tmp") then
fileRemove("temp.tmp")
log("临时文件已删除")
end
❌ 错误:删除目录
fileRemove("subdir") -- 目录删除可能返回 false
原因:fileRemove 主要用于删除文件,删除非空目录可能失败。目录删除应确保为空或使用特定方法。
⚠️ 避坑:文件不存在时返回 false(不报错)。路径不安全返回 false。删除不可逆,操作前建议确认。异常时返回 false。
fileRename(oldPath, newPath)
文件
重命名/移动文件。返回是否成功。
fileRename(oldPath: string, newPath: string) → boolean
| 参数 | 类型 | 必填 | 说明 |
oldPath | string | 是 | 原文件路径 |
newPath | string | 是 | 新文件路径 |
✅ 正确
fileRename("temp.log", "archived.log")
log("重命名完成")
❌ 错误:目标已存在
fileRename("a.txt", "b.txt") -- b.txt 已存在时可能失败
原因:目标文件已存在时,重命名行为取决于底层文件系统,可能失败或覆盖。建议先检查/删除目标文件。
⚠️ 避坑:两个路径都需在安全目录内。源文件不存在返回 false。跨目录重命名等同于移动。异常时返回 false。
fileCopy(src, dst)
文件
复制文件到新路径。返回是否成功。
fileCopy(src: string, dst: string) → boolean
| 参数 | 类型 | 必填 | 说明 |
src | string | 是 | 源文件路径 |
dst | string | 是 | 目标文件路径 |
✅ 正确
fileCopy("template.lua", "task_001.lua")
log("模板已复制")
❌ 错误:源不存在
fileCopy("missing.txt", "copy.txt") -- 返回 false
原因:源文件不存在时复制失败返回 false。不会创建空文件。应先 fileExist 检查。
⚠️ 避坑:两路径都需在安全目录内。源文件不存在返回 false。目标目录不存在可能失败。异常时返回 false。
fileMove(src, dst)
文件
移动文件到新路径(源路径删除)。返回是否成功。
fileMove(src: string, dst: string) → boolean
| 参数 | 类型 | 必填 | 说明 |
src | string | 是 | 源文件路径 |
dst | string | 是 | 目标文件路径 |
✅ 正确
fileMove("inbox/data.txt", "processed/data.txt")
log("文件已移动")
❌ 错误:与 fileRename 混淆
-- fileMove 和 fileRename 功能相似,都是移动文件
fileMove("a.txt", "b.txt") -- 等同于 fileRename
原因:fileMove 与 fileRename 底层都是文件系统移动操作,功能等价。选择语义更清晰的即可。
⚠️ 避坑:移动后源文件不再存在。源文件不存在返回 false。跨目录移动需目标目录存在。异常时返回 false。
mkdir(path)
文件
创建目录。返回是否成功。
mkdir(path: string) → boolean
| 参数 | 类型 | 必填 | 说明 |
path | string | 是 | 目录路径(相对) |
✅ 正确
mkdir("logs")
writeFile("logs/today.txt", "日志内容")
❌ 错误:目录已存在
mkdir("logs") -- 已存在时可能返回 false
原因:目录已存在时,mkdir 可能返回 false(取决于 FileUtils.mkdir 实现)。创建前建议不检查,直接调用并忽略"已存在"的 false。
⚠️ 避坑:路径经 safePath 检查。多层目录(如 "a/b/c")是否递归创建取决于实现,建议逐层创建。异常时返回 false。
listFiles(path)
文件
列出目录下的文件/子目录名,返回 1-based 字符串 table。失败返回 nil。
listFiles(path: string) → table | nil
✅ 正确
local files = listFiles("scripts")
if files then
for i = 1, #files do
log(files[i])
end
end
❌ 错误:路径是文件
listFiles("data.txt") -- 返回 nil
原因:对文件(非目录)调用 listFiles 返回 nil。目录不存在或路径不安全也返回 nil。
⚠️ 避坑:返回的是文件名(非完整路径)。空目录返回空 table(非 nil)。目录不存在或路径不安全返回 nil。需判空。
🌐 16. 网络通信 7 个 API
网络通信提供 HTTP 请求、文件下载/上传、Ping 测试、网络时间获取等能力。HTTP 请求超时上限 10 秒(防止阻塞任务线程),Ping 超时 5 秒。所有网络操作同步阻塞执行。
httpGet(url)
网络
发起 HTTP GET 请求,返回响应体字符串。失败返回 nil。超时 10 秒。
httpGet(url: string) → string | nil
| 参数 | 类型 | 必填 | 说明 |
url | string | 是 | 请求 URL |
✅ 正确
local resp = httpGet("https://api.example.com/status")
if resp then
log("响应: " .. resp)
else
log("请求失败")
end
❌ 错误:未判空
local resp = httpGet("https://api.example.com/data")
log(resp:sub(1, 100)) -- resp 为 nil 时报错
原因:网络请求失败时返回 nil,对 nil 调用字符串方法会报错。必须先判空。
⚠️ 避坑:超时 10 秒,期间阻塞 LUA 线程。无网络或服务器无响应时等待 10 秒后返回 nil。不支持自定义 header,需自定义请用 httpRequest。
httpPost(url, data)
网络
发起 HTTP POST 请求,返回响应体字符串。超时 10 秒。
httpPost(url: string, data: string) → string | nil
| 参数 | 类型 | 必填 | 说明 |
url | string | 是 | 请求 URL |
data | string | 否 | 请求体(默认空字符串) |
✅ 正确
local json_str = json.encode({user = "test", pass = "123"})
local resp = httpPost("https://api.example.com/login", json_str)
if resp then log(resp) end
❌ 错误:传 table
httpPost("https://api.example.com/login", {user="test"}) -- table 被转字符串
原因:data 经 tojstring 转换,table 会变成 "table: 0x..."。应先用 json.encode 序列化为 JSON 字符串再发送。
⚠️ 避坑:data 为 nil 时发送空字符串。Content-Type 默认由 HttpUtils 决定(通常为 application/json 或 form-urlencoded)。不支持自定义 header,需自定义请用 httpRequest。
httpDownload(url, path)
网络
下载文件到脚本目录内指定路径。返回是否成功。
httpDownload(url: string, path: string) → boolean
| 参数 | 类型 | 必填 | 说明 |
url | string | 是 | 下载 URL |
path | string | 是 | 保存路径(脚本目录内) |
✅ 正确
local ok = httpDownload("https://example.com/config.json", "config.json")
if ok then
local content = readFile("config.json")
log(content)
end
❌ 错误:保存到外部路径
httpDownload("https://example.com/file.zip", "/sdcard/file.zip")
-- 返回 false,路径不安全
原因:path 经 FileUtils.safePath 检查,只能保存到脚本目录内。绝对路径被拒绝返回 false。
⚠️ 避坑:下载大文件会长时间阻塞线程。网络异常返回 false。路径不安全返回 false。建议下载后校验文件大小。
httpUpload(url, filepath, params?)
网络
以 Multipart 方式上传文件,可附带额外参数。返回是否成功。
httpUpload(url: string, filepath: string, params?: table) → boolean
| 参数 | 类型 | 必填 | 说明 |
url | string | 是 | 上传 URL |
filepath | string | 是 | 待上传文件路径(脚本目录内) |
params | table | 否 | 额外表单参数 {key=value} |
✅ 正确
local ok = httpUpload("https://api.example.com/upload", "screenshot.png", {
token = "abc123",
device = "test"
})
if ok then log("上传成功") end
❌ 错误:文件不存在
httpUpload("https://api.example.com/upload", "missing.png")
-- 返回 false,文件不存在
原因:文件不存在或路径不安全时返回 false。源码中有 file.exists() 检查。应先确认文件存在。
⚠️ 避坑:文件字段名固定为 "file"。params 是 table(非 JSON 字符串),key/value 会被 tojstring 转换。连接超时 5 秒,读取超时 10 秒。异常返回 false。
httpRequest(url, method, data?, header?)
网络
通用 HTTP 请求,支持自定义方法、请求体和 header。返回响应体字符串或 nil。
httpRequest(url: string, method: string, data?: string, header?: table) → string | nil
| 参数 | 类型 | 必填 | 说明 |
url | string | 是 | 请求 URL |
method | string | 否 | HTTP 方法,默认 "GET" |
data | string | 否 | 请求体,默认 null |
header | table | 否 | 请求头 {key=value} |
✅ 正确
local resp = httpRequest("https://api.example.com/data", "POST", '{"key":"val"}', {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer token123"
})
if resp then log(resp) end
❌ 错误:header 传字符串
httpRequest("https://api.example.com", "GET", nil, "Content-Type: json")
-- header 非table被忽略
原因:header 必须是 table(istable() 检查),传字符串会被忽略(不设置任何 header)。应传 {["Content-Type"]="json"}。
⚠️ 避坑:超时 10 秒。method 不区分大小写但建议大写。header 的 key/value 会被 tojstring 转换。data 为 nil 时不发送请求体。失败返回 nil。
ping(host)
网络
对目标主机执行一次 Ping 测试,返回延迟毫秒数。失败返回 -1。超时 5 秒。
ping(host: string) → number (毫秒) | -1
| 参数 | 类型 | 必填 | 说明 |
host | string | 是 | 主机名或 IP |
✅ 正确
local ms = ping("www.baidu.com")
if ms >= 0 then
log("延迟: " .. ms .. "ms")
else
log("Ping 失败")
end
❌ 错误:用 nil 判断
local ms = ping("unreachable.host")
if ms then -- ms 是 -1,恒 true
log("可达")
end
原因:失败时返回 -1(number),不是 nil。if ms then 对 -1 为 true。应判断 if ms >= 0 then。
⚠️ 避坑:使用系统 ping -c 1 命令,部分 Android 设备可能无 ping 二进制导致失败。返回值是 int(小数被截断)。5 秒超时后返回 -1。
getNetworkTime()
网络
通过 HTTP HEAD 请求获取网络时间,返回格式化时间字符串。失败返回空字符串。
getNetworkTime() → string "yyyy-MM-dd HH:mm:ss" | ""
✅ 正确
local timeStr = getNetworkTime()
if timeStr ~= "" then
log("网络时间: " .. timeStr)
else
log("获取失败")
end
❌ 错误:期望时间戳
local t = getNetworkTime()
local ts = tonumber(t) -- nil,t 是时间字符串
原因:返回的是格式化字符串(如 "2026-07-08 14:30:00"),不是时间戳数字。tonumber 无法解析返回 nil。需解析时间用 os.time 配合字符串分割。
⚠️ 避坑:通过请求百度 HEAD 获取 Date 响应头。连接/读取超时各 5 秒。无网络或请求失败返回空字符串 ""(不是 nil)。时区为设备默认时区。
💬 17. 界面交互 10 个 API
界面交互提供 Toast 提示、对话框、剪贴板、动态 UI、加载框等能力。dialog、alert、showUI 会用 CountDownLatch 阻塞 LUA 线程等待用户操作,超时自动放行。
toast(text)
UI
显示短暂 Toast 提示(约 2 秒自动消失)。非阻塞,立即返回。
toast(text: string) → nil
✅ 正确
toast("任务开始执行")
click(500, 800)
❌ 错误:期望阻塞等待
toast("点击确定继续")
wait(5000) -- toast 不阻塞,立即执行下一行
原因:toast 是非阻塞的,显示后立即返回。不会暂停脚本等待 Toast 消失。需阻塞等待用户请用 dialog 或 alert。
⚠️ 避坑:使用 Toast.LENGTH_SHORT(约 2 秒)。在 UI 线程显示,不影响脚本执行。连续调用会覆盖前一个 Toast。文本过长会被截断。
showToast(text)
UI
toast 的别名,功能完全一致。
showToast(text: string) → nil
❌ 错误:以为与 toast 不同
-- showToast 与 toast 实现完全相同
showToast("test") -- 等同于 toast("test")
原因:源码中 showToast 与 toast 是独立注册但实现完全一致的函数,仅为兼容不同命名习惯。选择其一即可。
⚠️ 避坑:与 toast 行为完全一致(非阻塞、SHORT 时长)。不要重复调用两者造成提示混乱。
dialog(text, timeout?)
UI
显示对话框并阻塞脚本,直到用户关闭或超时。timeout 默认 3000 毫秒。
dialog(text: string, timeout?: number) → nil
| 参数 | 类型 | 必填 | 说明 |
text | string | 是 | 对话框内容 |
timeout | number | 否 | 超时毫秒,默认 3000 |
✅ 正确
dialog("正在处理,请稍候...", 5000)
log("对话框已关闭,继续执行")
❌ 错误:timeout 用秒
dialog("提示", 3) -- 3 毫秒,几乎瞬间消失
原因:timeout 单位是毫秒。3 秒应写 3000。传 3 只显示 3 毫秒用户看不到。
⚠️ 避坑:使用 CountDownLatch 阻塞 LUA 线程,实际等待时间为 timeout + 1000 毫秒(额外 1 秒保险)。对话框无按钮,用户点击外部或返回键关闭。timeout 过大长时间卡住任务。
setClipboard(text)
UI
设置系统剪贴板内容。
setClipboard(text: string) → nil
| 参数 | 类型 | 必填 | 说明 |
text | string | 是 | 剪贴板内容 |
✅ 正确
setClipboard("https://example.com")
toast("链接已复制")
❌ 错误:期望返回值
if setClipboard("text") then -- 永远 nil
log("成功")
end
原因:本函数始终返回 nil,无法通过返回值判断是否成功。异常仅记录日志。需确认可立即 getClipboard 验证。
⚠️ 避坑:使用系统 ClipboardManager。Android 10+ 限制后台应用访问剪贴板,可能设置失败但不报错。异常时仅记录日志,返回 nil。
getClipboard()
UI
获取系统剪贴板内容。剪贴板为空或无法访问时返回空字符串。
getClipboard() → string
✅ 正确
local text = getClipboard()
if text ~= "" then
log("剪贴板: " .. text)
end
❌ 错误:用 nil 判断
local text = getClipboard()
if text then -- 空剪贴板返回 "" 不是 nil
log("有内容") -- 空字符串也为 true
end
原因:剪贴板为空时返回空字符串 ""(不是 nil)。if text then 对空字符串为 true。应判断 if text ~= "" then。
⚠️ 避坑:Android 10+ 限制后台应用读取剪贴板,可能返回空字符串。剪贴板无内容或非文本类型也返回空字符串。始终返回 string。
alert(title, msg)
UI
显示带"确定"按钮的警告框,阻塞脚本直到用户点击确定。超时 60 秒。
alert(title: string, msg: string) → nil
| 参数 | 类型 | 必填 | 说明 |
title | string | 否 | 标题,nil 时默认"提示" |
msg | string | 否 | 消息内容,nil 时默认空 |
✅ 正确
alert("确认", "是否继续执行任务?")
log("用户已确认")
❌ 错误:期望返回用户选择
local choice = alert("选择", "A或B")
if choice == "A" then -- choice 永远 nil
log("选了A")
end
原因:alert 只有一个"确定"按钮,始终返回 nil,无法获取用户选择。需多选项请用 showUI 构建自定义界面。
⚠️ 避坑:阻塞最长 60 秒(CountDownLatch.await(60, SECONDS))。title/msg 为 nil 时用默认值。用户点击确定或返回键放行。只有一个按钮,无法做选择。
showUI(json_str)
UI
根据 JSON 描述动态生成 UI(标签+输入框),阻塞等待用户操作。返回 retCode 和输入值 table。
showUI(json_str: string) → retCode: number, inputs: table
| 参数 | 类型 | 必填 | 说明 |
json_str | string | 是 | UI 描述 JSON |
✅ 正确
local ui_json = [[
{
"views": [
{"type":"label", "text":"请输入用户名"},
{"type":"edit", "id":"username", "prompt":"用户名"},
{"type":"edit", "id":"password", "prompt":"密码"}
]
}
]]
local code, inputs = showUI(ui_json)
if code == 1 then
log("用户名: " .. inputs.username)
log("密码: " .. inputs.password)
else
log("用户取消")
end
❌ 错误:只接收一个返回值
local result = showUI(ui_json)
log(result.username) -- result 是 retCode(number)
原因:函数返回两个值(retCode 和 inputs table)。单个变量只接收第一个(retCode)。必须用 local code, inputs = showUI(...) 接收两个值。
⚠️ 避坑:retCode=1 表示确定,0 表示取消。inputs 的 key 是 edit 的 id 字段。阻塞最长 60 秒。JSON 解析失败返回 0 和空 table。支持 view 类型:label(文本)、edit(输入框)。
createWindow()
UI
创建悬浮窗(当前为简化实现,仅返回 windowId,不实际创建悬浮窗)。
createWindow() → number (windowId)
✅ 正确
local id = createWindow()
log("windowId: " .. id) -- 返回递增的 id
❌ 错误:期望实际悬浮窗
local id = createWindow()
-- 期望屏幕出现悬浮窗,实际没有
原因:当前为简化实现,仅返回递增的 windowId,不创建实际悬浮窗。源码注释明确"简化实现,未实际创建悬浮窗"。
⚠️ 避坑:windowId 是自增整数(从 1 开始)。异常时返回 -1。该函数当前无实际 UI 效果,后续版本可能补充完整实现。不要依赖它创建可见 UI。
showLoading()
UI
显示加载进度框(固定文案"加载中...",不可取消)。非阻塞。
showLoading() → nil
✅ 正确
showLoading()
local result = do_something_slow()
hideLoading()
log("完成: " .. result)
❌ 错误:传文案参数
showLoading("正在处理...") -- 参数被忽略
原因:showLoading 是 ZeroArgFunction,不接收参数。文案固定为"加载中..."。传入的参数被忽略。
⚠️ 避坑:加载框不可取消(setCancelable(false)),必须配合 hideLoading 关闭。重复调用会先关闭旧框再显示新的。在 UI 线程显示,不影响脚本执行。
hideLoading()
UI
隐藏加载进度框。无返回值。
hideLoading() → nil
✅ 正确
showLoading()
-- 执行耗时操作
wait(3000)
hideLoading()
log("加载框已关闭")
❌ 错误:未配对调用
showLoading()
scriptExit() -- 加载框残留
原因:若 showLoading 后未调用 hideLoading 就退出脚本,加载框可能残留在屏幕上。应在脚本结束前确保关闭。
⚠️ 避坑:未显示加载框时调用 hideLoading 不会报错(安全)。在 UI 线程操作。加载框未显示时 progressDialog 为 null,此时调用无效果。
📱 18. 设备信息 25 个 API
设备信息 API 提供时间、设备标识、屏幕、电池、网络、运营商、语言时区、应用信息、状态栏、路径等只读查询能力。除 getAppName/getAppVersion/isAppInstalled 接收包名参数外,其余均为无参函数。多数函数在异常时返回空字符串或 0/-1 而非 nil。
get_time()
设备信息
获取当前时间,返回 yyMMddHHmmss 格式字符串(如 260708143025 表示 2026-07-08 14:30:25)。
get_time() → string
✅ 正确
local t = get_time()
log("当前时间: " .. t) -- 260708143025
❌ 错误:当作时间戳使用
local t = get_time()
wait(t) -- t 是 "260708143025" 字符串,wait 会 tonumber 得到超大毫秒数
原因:返回值是格式化时间字符串而非时间戳。需要秒级时间戳请用 getDeviceTime()。
⚠️ 避坑:格式为 2 位年份 yy 开头,tonumber 后是一个 12 位整数,不要直接用于 wait 或时间差计算。它是 getTimer 别名指向的同一函数。
getDeviceTime()
设备信息
获取设备时间戳(Unix 秒),返回 int 类型。
getDeviceTime() → int
✅ 正确
local start = getDeviceTime()
wait(3000)
local elapsed = getDeviceTime() - start
log("耗时 " .. elapsed .. " 秒")
❌ 错误:当作毫秒使用
local t = getDeviceTime()
wait(t) -- t 是秒级时间戳(约17亿),wait 单位是毫秒
原因:返回的是秒级时间戳(System.currentTimeMillis() / 1000),而 wait 接收毫秒。两者单位不一致。
⚠️ 避坑:返回值为 int 类型(32 位),2038 年后会溢出。用于时间差计算时注意单位是秒不是毫秒。异常时返回 0。
getDeviceID()
设备信息
获取设备唯一标识(ANDROID_ID),返回 string。不同应用签名下 ANDROID_ID 可能不同。
getDeviceID() → string
✅ 正确
local id = getDeviceID()
if id ~= "" then
log("设备ID: " .. id)
end
❌ 错误:假定全局唯一不可变
local id = getDeviceID()
set_global("bound_" .. id, "true") -- 恢复出厂设置后 ANDROID_ID 会改变
原因:ANDROID_ID 在恢复出厂设置后会重新生成,不能作为永久唯一标识依赖。
⚠️ 避坑:Android 8.0+ 下 ANDROID_ID 基于应用签名+用户,卸载重装同一 APK 通常不变,但换签名后会变。异常时返回空字符串。
getDeviceName()
设备信息
获取设备名称(Settings.Global.DEVICE_NAME),返回 string。
getDeviceName() → string
✅ 正确
local name = getDeviceName()
log("设备名称: " .. name) -- 如 "小米手机"
❌ 错误:假定非空
local name = getDeviceName()
local first = string.sub(name, 1, 1) -- name 为空时得到 ""
原因:部分设备 DEVICE_NAME 可能为 null(源码已处理为空字符串),string.sub("",1,1) 返回空串而非报错,但后续逻辑可能出错。
⚠️ 避坑:读取的是系统设置中的设备名(蓝牙/WiFi 名称),非型号。获取型号请用 getDeviceModel()。
getOSVer()
设备信息
获取操作系统版本号,返回 string(如 13、14)。
getOSVer() → string
✅ 正确
local ver = getOSVer()
log("系统版本: Android " .. ver)
❌ 错误:当作浮点数比较
local ver = getOSVer()
if tonumber(ver) > 10.0 then -- ver 是 "13",tonumber 得 13,可以工作但语义不清
log("高版本")
end
原因:返回的是 Build.VERSION.RELEASE 字符串(如 "13"),虽然可 tonumber 但建议先判空。
⚠️ 避坑:返回 Build.VERSION.RELEASE,预览版可能返回空字符串或字母。异常时返回空字符串。
getDeviceType()
设备信息
获取设备类型,固定返回 "android"。
getDeviceType() → string
✅ 正确
local dtype = getDeviceType()
if dtype == "android" then
log("安卓设备")
end
❌ 错误:期待其他返回值
local dtype = getDeviceType()
if dtype == "ios" then -- 永远不会为真
log("iOS")
end
原因:本系统仅运行在 Android 上,返回值恒为 "android",即使异常也返回 "android"(catch 块默认值)。
⚠️ 避坑:用于跨平台脚本兼容判断,在本环境中恒返回 "android"。
getDeviceModel()
设备信息
获取设备型号,返回 string(如 MI 13、SM-G991B)。
getDeviceModel() → string
✅ 正确
local model = getDeviceModel()
log("设备型号: " .. model)
❌ 错误:用于设备名显示
local model = getDeviceModel()
toast("运行在 " .. model) -- model 是型号代号如 "SM-G991B",不是友好名称
原因:返回 Build.MODEL(型号代号),不是用户设置的设备名。友好名称请用 getDeviceName()。
⚠️ 避坑:返回 Build.MODEL,异常时返回空字符串。同一型号设备返回值相同。
getScreenSize()
设备信息
获取屏幕尺寸,返回 table {w=宽度, h=高度}(像素)。
getScreenSize() → table {w, h}
✅ 正确
local size = getScreenSize()
log("屏幕: " .. size.w .. "x" .. size.h) -- 1080x2400
click(size.w / 2, size.h / 2) -- 点击屏幕中心
❌ 错误:当作字符串使用
local size = getScreenSize()
log("屏幕: " .. size) -- size 是 table,拼接得到 "table: 0x..."
原因:返回值是 LuaTable 不是字符串,直接 .. 拼接会得到 table: 0x... 地址。需访问 .w .h 字段。
⚠️ 避坑:异常时返回 {w=0, h=0} 而非 nil。使用前建议检查 size.w > 0。
getResolution()
设备信息
获取屏幕分辨率,返回 "WIDTHxHEIGHT" 格式字符串(如 "1080x2400")。
getResolution() → string
✅ 正确
local res = getResolution()
log("分辨率: " .. res) -- 1080x2400
❌ 错误:直接解析返回 table
local res = getResolution()
local w = res.w -- res 是字符串 "1080x2400",字符串无 .w 字段
原因:返回值是 字符串 而非 table。需要数值请用 getScreenSize(),或自行 string.match(res, "(%d+)x(%d+)") 解析。
⚠️ 避坑:与 getScreenSize() 数据源相同但返回类型不同(string vs table)。异常时返回 "0x0"。
getScreenOrientation()
设备信息
获取屏幕方向,返回 "portrait"(竖屏)或 "landscape"(横屏)。
getScreenOrientation() → string
✅ 正确
local ori = getScreenOrientation()
if ori == "landscape" then
log("横屏模式")
else
log("竖屏模式")
end
❌ 错误:当作数字判断
local ori = getScreenOrientation()
if ori == 1 then -- ori 是字符串 "portrait",永远不等于数字 1
log("竖屏")
end
原因:返回值是字符串 "portrait"/"landscape",不是数字。判断 orientation==2(内部常量)是错误的。
⚠️ 避坑:基于 Configuration.orientation,仅区分竖屏/横屏两种。异常时返回 "unknown",注意 else 分支可能匹配到 unknown。
getBatteryLevel()
设备信息
获取电池电量百分比,返回 int(0-100)。
getBatteryLevel() → int
✅ 正确
local bat = getBatteryLevel()
if bat < 20 then
toast("电量不足: " .. bat .. "%")
end
❌ 错误:未检查异常返回值
local bat = getBatteryLevel()
if bat > 0 then -- 异常时返回 -1,-1 > 0 为 false 恰好安全,但语义不清
log("电量正常")
end
原因:异常时返回 -1 不是 nil。应显式判断 bat >= 0 或 bat ~= -1。
⚠️ 避坑:异常时返回 -1。电量范围 0-100 为正常值,-1 表示读取失败。
isCharging()
设备信息
判断设备是否正在充电,返回 boolean。
isCharging() → boolean
✅ 正确
if isCharging() then
log("充电中,可执行高耗电任务")
else
log("未充电")
end
❌ 错误:与字符串比较
if isCharging() == "true" then -- boolean 与 string 比较永远为 false
log("充电中")
end
原因:返回值是 boolean true/false,不是字符串。Lua 中 true ~= "true"。
⚠️ 避坑:异常时返回 false。基于 BatteryManager 读取充电状态。
getNetworkType()
设备信息
获取网络类型,返回 "wifi"、"4g"、"3g"、"2g" 或 "none"。
getNetworkType() → string
✅ 正确
local net = getNetworkType()
if net == "wifi" then
log("WiFi 网络,可下载大文件")
elseif net == "none" then
log("无网络")
end
❌ 错误:遗漏 none 判断
local net = getNetworkType()
if net ~= "wifi" then
log("移动网络") -- net 为 "none" 时也会进入此分支
end
原因:"none" 也满足 ~= "wifi",会被误判为移动网络。应先排除 "none"。
⚠️ 避坑:异常或无网络时返回 "none"。5G 网络可能被归类为 "4g"(取决于设备上报)。
getIPAddress()
设备信息
获取设备 IP 地址,返回 string。优先返回非回环 IPv4 地址。
getIPAddress() → string
✅ 正确
local ip = getIPAddress()
if ip ~= "" then
log("本机IP: " .. ip) -- 192.168.1.100
end
❌ 错误:假定公网 IP
local ip = getIPAddress()
log("公网IP: " .. ip) -- 返回的是局域网 IP,非公网 IP
原因:遍历网络接口获取的是局域网 IP(如 192.168.x.x),不是公网 IP。获取公网 IP 需调用外部 API。
⚠️ 避坑:异常或无网络时返回空字符串。多网卡设备可能返回任一接口 IP。
getCarrier()
设备信息
获取运营商名称(TelephonyManager.getNetworkOperatorName),返回 string。
getCarrier() → string
✅ 正确
local carrier = getCarrier()
if carrier ~= "" then
log("运营商: " .. carrier) -- China Mobile
end
❌ 错误:WiFi 下期待运营商名
local carrier = getCarrier()
if carrier == "" then
log("无SIM卡") -- WiFi 连接无 SIM 时也为空,不代表设备故障
end
原因:WiFi-only 设备或无 SIM 卡时 getNetworkOperatorName 返回空,这是正常现象非错误。
⚠️ 避坑:读取的是 networkOperatorName(当前注册网络),非 SIM 卡运营商。无 SIM 或 WiFi-only 返回空字符串。
getLanguage()
设备信息
获取系统语言,返回 string(如 zh、en)。
getLanguage() → string
✅ 正确
local lang = getLanguage()
if lang == "zh" then
log("中文环境")
else
log("其他语言: " .. lang)
end
❌ 错误:期待完整语言代码
local lang = getLanguage()
if lang == "zh-CN" then -- 返回的是 "zh" 不含地区后缀
log("简体中文")
end
原因:返回 Locale.getLanguage() 仅语言代码(如 zh),不含国家/地区(如 CN)。
⚠️ 避坑:仅返回语言代码(2 字母),不含地区。需要完整 locale 请用 getTimeZone() 间接推断地区。异常返回空字符串。
getTimeZone()
设备信息
获取系统时区,返回 string(如 Asia/Shanghai)。
getTimeZone() → string
✅ 正确
local tz = getTimeZone()
log("时区: " .. tz) -- Asia/Shanghai
❌ 错误:当作时区偏移使用
local tz = getTimeZone()
local offset = tonumber(tz) -- tz 是 "Asia/Shanghai",tonumber 返回 nil
原因:返回 IANA 时区 ID 字符串(如 Asia/Shanghai),不是数字偏移(如 +8)。需要偏移需自行解析。
⚠️ 避坑:返回 TimeZone.getID(),是 IANA 时区标识符。异常返回空字符串。
getFrontApp()
设备信息
获取前台应用包名,返回 string。需要 PACKAGE_USAGE_STATS 权限,无权限返回空字符串。
getFrontApp() → string
✅ 正确
local pkg = getFrontApp()
if pkg ~= "" then
log("前台应用: " .. pkg) -- com.tencent.mm
else
log("无权限或无前台应用")
end
❌ 错误:假定总有返回值
local pkg = getFrontApp()
launch_app(pkg) -- pkg 为 "" 时 launch_app 行为未定义
原因:无 PACKAGE_USAGE_STATS 权限时返回空字符串。需先判空再使用。该权限需用户在系统设置中手动授予。
⚠️ 避坑:依赖 UsageStatsManager,需用户在「设置 → 安全 → 使用情况访问权限」中授权。无权限或异常返回空字符串。返回的是最近 10 秒内最后一个使用的应用包名。
getAppName(pkg)
设备信息
根据包名获取应用名称,返回 string。
getAppName(pkg: string) → string
✅ 正确
local name = getAppName("com.tencent.mm")
log("应用名: " .. name) -- 微信
❌ 错误:传入应用名而非包名
local name = getAppName("微信") -- "微信" 不是包名
log(name) -- 返回空字符串(找不到应用)
原因:参数必须是包名(如 com.tencent.mm),不是应用显示名。传错返回空字符串。
⚠️ 避坑:应用未安装或包名错误返回空字符串。异常也返回空字符串。
getAppVersion(pkg)
设备信息
根据包名获取应用版本名(versionName),返回 string。
getAppVersion(pkg: string) → string
✅ 正确
local ver = getAppVersion("com.tencent.mm")
log("微信版本: " .. ver) -- 8.0.37
❌ 错误:当作 versionCode 使用
local ver = getAppVersion("com.tencent.mm")
if tonumber(ver) > 100 then -- ver 是 "8.0.37",tonumber 得 8(遇小数点截断)
log("新版本")
end
原因:返回 versionName(如 8.0.37),不是 versionCode(整数)。tonumber("8.0.37") 返回 8.0 不是完整版本号。
⚠️ 避坑:返回 PackageInfo.versionName(字符串版本号),不是 versionCode(整数)。比较版本需用字符串分割或第三方比较函数。未安装返回空字符串。
isAppInstalled(pkg)
设备信息
判断应用是否已安装,返回 boolean。
isAppInstalled(pkg: string) → boolean
✅ 正确
if isAppInstalled("com.tencent.mm") then
launch_app("com.tencent.mm")
else
toast("未安装微信")
end
❌ 错误:传应用名而非包名
if isAppInstalled("微信") then -- 返回 false("微信"不是包名)
log("已安装")
end
原因:参数必须是包名(如 com.tencent.mm),传应用显示名永远返回 false。
⚠️ 避坑:异常时返回 false。注意区分「未安装」和「查询异常」都返回 false。
getStatusBarHeight()
设备信息
获取状态栏高度(像素),返回 int。用于坐标计算时避开状态栏区域。
getStatusBarHeight() → int
✅ 正确
local sbh = getStatusBarHeight()
log("状态栏高度: " .. sbh .. "px") -- 72
click(500, sbh + 100) -- 避开状态栏点击
❌ 错误:当作 dp 使用
local sbh = getStatusBarHeight()
local y = sbh * 2 -- sbh 是 px 不是 dp,乘以密度才是实际偏移
原因:返回像素值(px),不是 dp。不同密度设备 px 值不同,不能当 dp 用。
⚠️ 避坑:返回像素值。资源未找到或异常时返回 0。注意坐标缩放系统(ScreenScaleManager)可能影响实际点击坐标。
getTSPath()
设备信息
获取脚本目录绝对路径,返回 string(getExternalFilesDir()/scripts/)。
getTSPath() → string
✅ 正确
local path = getTSPath()
log("脚本目录: " .. path) -- /storage/emulated/0/Android/data/.../files/scripts
local content = readFile(path .. "/config.txt")
❌ 错误:路径拼接缺少分隔符
local path = getTSPath()
local content = readFile(path .. "config.txt") -- 缺少 "/"
原因:getTSPath() 返回的路径以 / 结尾,但直接拼接文件名时仍建议确认。实际源码返回 getAbsolutePath()(不带尾 /),需手动加 /。
⚠️ 避坑:返回 new File(getExternalFilesDir(null), "scripts").getAbsolutePath(),路径不以 / 结尾。拼接子路径需手动加 /。这是 FileIOBridge 文件操作的安全根目录。
getUserPath()
设备信息
获取用户路径,返回 string。与 getTSPath() 完全相同(都指向 scripts/ 目录)。
getUserPath() → string
✅ 正确
local path = getUserPath()
log("用户路径: " .. path) -- 与 getTSPath() 相同
❌ 错误:以为与 getTSPath 不同
if getUserPath() ~= getTSPath() then -- 永远为 false
log("路径不同")
end
原因:源码中 getUserPath 与 getTSPath 实现完全一致,返回同一个 scripts/ 目录。仅为兼容不同命名习惯。
⚠️ 避坑:与 getTSPath() 返回值完全相同,是同一目录的两种命名。选择其一即可。
getResPath()
设备信息
获取资源目录绝对路径,返回 string(getExternalFilesDir()/res/)。
getResPath() → string
✅ 正确
local resPath = getResPath()
log("资源目录: " .. resPath) -- .../files/res
local img = find_image(resPath .. "/btn.png", 0, 0, 9999, 9999)
❌ 错误:与脚本目录混淆
local path = getResPath()
writeFile(path .. "/output.txt", "data") -- res/ 目录用于存放资源,写入数据应放 scripts/
原因:getResPath() 指向 res/ 目录,用于存放图片等资源文件。脚本输出数据应写入 getTSPath()(scripts/)目录。
⚠️ 避坑:返回 res/ 目录路径,与 scripts/(getTSPath)是同级不同目录。路径不以 / 结尾,拼接需手动加。文件操作受 FileUtils.safePath 安全检查。
⚙️ 19. 脚本控制 8 个 API
脚本控制 API 提供初始化、退出、定时器、配置存储、精度设置、调试开关等能力。init 为空实现仅记录参数(兼容性保留)。setTimer 回调执行前会检查 engine.isTaskRunning(),任务已停止则不执行。setSysConfig/getSysConfig 为内存级存储(任务结束清除)。
init(origin, rotate)
脚本控制
初始化函数(空实现,仅记录参数)。为兼容驼峰命名脚本保留,不影响任何功能。支持 string 或 int 参数。
init([origin: int|string], [rotate: int|string]) → nil
| 参数 | 类型 | 必填 | 说明 |
origin | int|string | 否 | 原点坐标(兼容参数,未使用) |
rotate | int|string | 否 | 旋转角度(兼容参数,未使用) |
✅ 正确
init(0, 0) -- 兼容性调用,无实际效果
log("脚本启动")
❌ 错误:依赖 init 设置坐标原点
init(100, 200) -- origin/rotate 参数被忽略,不会改变坐标原点
click(0, 0) -- 仍然点击屏幕左上角 (0,0),不受 init 影响
原因:init 是空实现,origin 和 rotate 参数仅被记录到日志,不会改变任何运行时行为。坐标原点始终是屏幕左上角。
⚠️ 避坑:空实现函数,仅为兼容驼峰命名脚本语法。调用它不会有任何副作用,但也不会有任何效果。支持 string 参数(如 init("0", "0")),内部会尝试转换为 int。
scriptExit()
脚本控制
退出脚本,调用 engine.stopTask() 停止当前任务。接受一个参数但忽略其值。
scriptExit([arg: any]) → nil
| 参数 | 类型 | 必填 | 说明 |
arg | any | 否 | 任意参数(被忽略,仅为兼容签名) |
✅ 正确
if getBatteryLevel() < 10 then
toast("电量过低,退出脚本")
scriptExit()
end
log("这行不会执行") -- scriptExit 后任务停止
❌ 错误:期望返回值或捕获退出码
local code = scriptExit() -- 返回 nil,不是退出码
if code == 0 then -- nil == 0 为 false
log("正常退出")
end
原因:scriptExit 返回 nil,不返回退出码。调用后 engine.stopTask() 被触发,后续代码可能不会执行(取决于线程中断时机)。
⚠️ 避坑:调用后引擎标记任务停止,但当前代码可能还会执行几行(非立即中断)。不要在 scriptExit 后写关键逻辑。源码中它是 OneArgFunction(接收 1 个参数但忽略)。
setTimer(ms, func)
脚本控制
设置延时定时器,ms 毫秒后执行回调函数 func。返回 timerId(回调对象的 identityHashCode)。回调执行前检查任务是否仍在运行。
setTimer(ms: int, func: function) → int (timerId)
| 参数 | 类型 | 必填 | 说明 |
ms | int | 是 | 延迟毫秒数 |
func | function | 是 | 回调函数(无参) |
✅ 正确
setTimer(5000, function()
log("5秒后执行")
click(500, 800)
end)
log("定时器已设置")
❌ 错误:传字符串而非函数
setTimer(3000, "doSomething") -- 第二个参数应为 function
原因:第二个参数必须是 function 类型。传字符串时 callback.isnil() 为 false(字符串非 nil),但 callback.call() 会报错。实际上源码检查 callback.isnil(),字符串不是 nil 所以会通过检查,但调用时异常。
❌ 错误:负数延迟
setTimer(-100, function() log("test") end) -- 返回 -1
原因:delay < 0 时源码返回 -1 且不设置定时器。返回值 -1 表示参数错误。
⚠️ 避坑:回调在 主线程(Looper.getMainLooper)执行,不要在回调中做耗时操作。回调执行前会检查 engine.isTaskRunning(),若任务已停止则不执行。timerId 是 System.identityHashCode(callback),不可用于取消定时器(无取消 API)。异常或参数错误返回 -1。
setSysConfig(key, val)
脚本控制
设置系统配置项(内存级,ConcurrentHashMap 存储)。任务结束后清除。
setSysConfig(key: string, val: any) → nil
| 参数 | 类型 | 必填 | 说明 |
key | string | 是 | 配置键名 |
val | any | 是 | 配置值(会被 tojstring 转为字符串存储) |
✅ 正确
setSysConfig("mode", "auto")
setSysConfig("retry_count", 3)
local mode = getSysConfig("mode") -- "auto"
❌ 错误:期望存储 table
setSysConfig("data", {a=1, b=2}) -- table 被 tojstring 转为 "table: 0x..."
local d = getSysConfig("data") -- 返回 "table: 0x..." 字符串
原因:值通过 val.tojstring() 转为字符串存储。table 会被转为地址字符串而非序列化。需存储结构化数据请用 json.encode 先转换。
⚠️ 避坑:内存级存储,不持久化,任务结束后数据丢失。值统一转为 string 存储(即使传入数字)。需要持久化请用 set_global。
getSysConfig(key)
脚本控制
读取系统配置项,返回 string。不存在时返回空字符串 "" 而非 nil。
getSysConfig(key: string) → string
✅ 正确
local mode = getSysConfig("mode")
if mode == "auto" then
log("自动模式")
elseif mode == "" then
log("未设置,使用默认")
end
❌ 错误:判断 nil
local mode = getSysConfig("mode")
if mode == nil then -- 不存在时返回 "" 不是 nil
log("未设置")
end
原因:键不存在时返回空字符串 "",不是 nil。"" == nil 为 false,判断永远不会命中。
⚠️ 避坑:不存在返回空字符串。存入的数字会被转为字符串,取出后需 tonumber 转回。异常也返回空字符串。
setTouchAccuracy(val)
脚本控制
设置触摸精度(影响 randomTap 随机偏移量),范围 0-50。默认 5。
setTouchAccuracy(val: int) → nil
| 参数 | 类型 | 必填 | 说明 |
val | int | 是 | 精度值(0-50,超出范围自动钳制) |
✅ 正确
setTouchAccuracy(10) -- 设置精度为 10
randomTap(500, 800) -- 点击 (500±10, 800±10) 范围内随机点
❌ 错误:超出范围
setTouchAccuracy(100) -- 超过 50,被钳制为 50
setTouchAccuracy(-5) -- 小于 0,被钳制为 0
原因:源码中 if (accuracy < 0) accuracy = 0; if (accuracy > 50) accuracy = 50; 自动钳制。不会报错但值会被截断。
⚠️ 避坑:值越大随机偏移越大(0 = 精确点击)。仅影响 randomTap,不影响 click/touchClick。配置存储在 ScriptControlBridge 实例中(非全局)。
setFindColorSim(sim)
脚本控制
设置默认找色相似度,范围 0.0-1.0。默认 0.9。
setFindColorSim(sim: number) → nil
| 参数 | 类型 | 必填 | 说明 |
sim | number | 是 | 相似度(0.0-1.0,超出范围自动钳制) |
✅ 正确
setFindColorSim(0.95) -- 设置默认相似度为 0.95
local result = find_color("#FF0000") -- 使用设置的相似度
❌ 错误:传整数而非小数
setFindColorSim(90) -- 90 > 1.0,被钳制为 1.0
原因:参数是 0.0-1.0 的小数(如 0.9 表示 90% 相似度),不是 0-100 的整数。传 90 会被钳制为 1.0(100% 相似度)。
⚠️ 避坑:相似度范围 0.0-1.0(不是 0-100)。此设置仅作为 ScriptControlBridge 内部默认值存储,具体是否影响 find_color 等函数取决于 Bridge 是否读取该值。建议在 find_color 等函数中显式传入 sim 参数。
debug(flag)
脚本控制
开启/关闭调试模式。接受 boolean 或 int(1=开,0=关)。
debug(flag: boolean|int) → nil
| 参数 | 类型 | 必填 | 说明 |
flag | boolean|int | 是 | true/1=开启,false/0=关闭 |
✅ 正确
debug(true) -- 开启调试
debug(1) -- 开启调试(int 形式)
debug(false) -- 关闭调试
❌ 错误:传字符串
debug("on") -- 字符串 "on"
原因:源码先检查 flag.isboolean()(false),再执行 flag.toint()。tonumber("on") 在 luaj 中会抛异常,被 catch 捕获后静默忽略,调试模式不变。
⚠️ 避坑:接受 boolean 或 int 两种类型。传字符串会触发 toint() 异常(被 catch 静默忽略,配置不变)。调试模式主要影响日志详细程度,具体行为取决于引擎实现。
🔗 21. 驼峰命名别名 24 个别名
为兼容不同命名习惯,系统提供了 24 个驼峰命名(camelCase)别名,每个别名与对应的下划线命名(snake_case)原函数是同一引用(fn1 == fn2 为 true),行为完全一致。别名分为两组:7 个原保留别名(早期版本的独立实现,现已改为直接赋值)和 17 个映射别名。
mSleep(ms)
别名→ wait
wait 的驼峰命名别名,功能完全一致(延时 ms 毫秒)。
mSleep(ms: int) → nil
✅ 正确
mSleep(2000) -- 等同于 wait(2000)
log("等待2秒完成")
❌ 错误:以为 mSleep 与 wait 不同
if mSleep ~= wait then -- 永远为 false(同一引用)
log("不同函数")
end
原因:源码中 mSleep 通过 globals.set("mSleep", globals.get("wait")) 直接赋值,两者是同一函数对象的引用,mSleep == wait 为 true。
⚠️ 避坑:与 wait 完全等价(同一引用)。选择一种命名风格即可,不要在同一脚本中混用两种风格。
touchClick(x, y)
别名→ click
click 的驼峰命名别名,功能完全一致(点击指定坐标)。
touchClick(x: int, y: int) → nil
✅ 正确
touchClick(500, 800) -- 等同于 click(500, 800)
❌ 错误:混用风格
click(100, 200)
touchClick(300, 400) -- 功能相同但命名风格不一致
原因:虽然功能完全一致,但混用驼峰和下划线命名风格会降低代码可读性。建议同一脚本统一使用一种风格。
⚠️ 避坑:坐标会经过 ScreenScaleManager.scaleXY 缩放(与 click 共享同一实现)。
inputText(text)
别名→ input_text
input_text 的驼峰命名别名,功能完全一致(输入文本到当前焦点输入框)。
inputText(text: string) → nil
✅ 正确
click(500, 800) -- 点击输入框
wait(500)
inputText("hello world") -- 等同于 input_text("hello world")
⚠️ 避坑:与
input_text 完全等价。需先点击输入框获取焦点再调用。详见
input_text。
getColor(x, y)
别名→ get_color
get_color 的驼峰命名别名,功能完全一致(获取像素颜色,返回 #RRGGBB 字符串)。
getColor(x: int, y: int) → string
✅ 正确
local color = getColor(500, 800)
log("颜色: " .. color) -- #FF0000
⚠️ 避坑:与
get_color 完全等价。坐标经缩放,越界返回
#000000。详见
get_color。
findColor(color)
别名→ find_color
find_color 的驼峰命名别名,功能完全一致(全屏找色,返回 {x,y} 或 nil)。
findColor(color: string) → table {x, y} | nil
✅ 正确
local pos = findColor("#FF0000")
if pos then
click(pos.x, pos.y)
end
⚠️ 避坑:与
find_color 完全等价。返回 table 或 nil。详见
find_color。
isColor(x, y, color)
别名→ is_color
is_color 的驼峰命名别名,功能完全一致(判断指定坐标颜色是否匹配)。
isColor(x: int, y: int, color: string) → boolean
✅ 正确
if isColor(500, 800, "#FF0000") then
log("颜色匹配")
end
getTimer()
别名→ get_time
get_time 的驼峰命名别名,功能完全一致(返回 yyMMddHHmmss 格式时间字符串)。
getTimer() → string
✅ 正确
local t = getTimer()
log("时间: " .. t) -- 260708143025
❌ 错误:以为是计时器
local start = getTimer()
wait(3000)
local elapsed = getTimer() - start -- 字符串相减报错
原因:返回时间字符串(如 "260708143025"),不是数值计时器。字符串不能做减法。需时间戳请用 getDeviceTime()。
⚠️ 避坑:与
get_time 完全等价。名称
getTimer 有误导性(像是计时器),实际返回时间字符串。详见
get_time。
runApp(pkg)
别名→ launch_app
launch_app 的驼峰命名别名,功能完全一致(启动指定应用)。
runApp(pkg: string) → nil
✅ 正确
runApp("com.tencent.mm") -- 等同于 launch_app("com.tencent.mm")
pressBack()
别名→ press_back
press_back 的驼峰命名别名,功能完全一致(按下返回键)。
pressBack() → nil
✅ 正确
pressBack() -- 等同于 press_back()
pressHome()
别名→ press_home
press_home 的驼峰命名别名,功能完全一致(按下主页键)。
pressHome() → nil
✅ 正确
pressHome() -- 等同于 press_home()
checkApp(pkg)
别名→ check_app
check_app 的驼峰命名别名,功能完全一致(检查应用运行状态)。
checkApp(pkg: string) → int
✅ 正确
local status = checkApp("com.tencent.mm") -- 等同于 check_app(...)
waitUntil(cond, timeout)
别名→ wait_until
wait_until 的驼峰命名别名,功能完全一致(条件等待)。
waitUntil(cond: function, timeout: int) → boolean
✅ 正确
local ok = waitUntil(function()
return isColor(500, 800, "#FF0000")
end, 10000)
findImage(imgPath, x1, y1, x2, y2)
别名→ find_image
find_image 的驼峰命名别名,功能完全一致(区域找图)。
findImage(imgPath: string, x1: int, y1: int, x2: int, y2: int) → table | nil
✅ 正确
local pos = findImage(getResPath() .. "/btn.png", 0, 0, 1080, 2400)
if pos then
click(pos.x, pos.y)
end
⚠️ 避坑:与
find_image 完全等价。相似度固定 0.8 不可调。详见
find_image。
longClick(x, y, duration)
别名→ long_click
long_click 的驼峰命名别名,功能完全一致(长按坐标)。
longClick(x: int, y: int, duration: int) → nil
✅ 正确
longClick(500, 800, 2000) -- 等同于 long_click(500, 800, 2000)
setVariable(name, value)
别名→ set_variable
set_variable 的驼峰命名别名,功能完全一致(设置任务级变量)。
setVariable(name: string, value: any) → nil
✅ 正确
setVariable("count", 5) -- 等同于 set_variable("count", 5)
local c = getVariable("count") -- "5"
getVariable(name)
别名→ get_variable
get_variable 的驼峰命名别名,功能完全一致(读取任务级变量)。
getVariable(name: string) → string | nil
✅ 正确
local val = getVariable("count")
if val then
log("count = " .. val)
end
setGlobal(name, value)
别名→ set_global
set_global 的驼峰命名别名,功能完全一致(设置全局变量,持久化到磁盘)。
setGlobal(name: string, value: any) → nil
✅ 正确
setGlobal("total_runs", 100) -- 等同于 set_global("total_runs", 100)
⚠️ 避坑:与
set_global 完全等价。每次调用都触发磁盘持久化。详见
set_global。
initGlobal(name, value)
别名→ init_global
init_global 的驼峰命名别名,功能完全一致(初始化全局变量,仅首次设置生效)。
initGlobal(name: string, value: any) → boolean
✅ 正确
if initGlobal("first_run", "true") then
log("首次运行")
else
log("已初始化过")
end
⚠️ 避坑:与
init_global 完全等价。返回 boolean 表示是否首次设置。详见
init_global。
getGlobal(name)
别名→ get_global
get_global 的驼峰命名别名,功能完全一致(读取全局变量)。
getGlobal(name: string) → string | nil
✅ 正确
local val = getGlobal("total_runs")
if val then
log("总运行次数: " .. val)
end
aiFindElement(desc)
别名→ ai_find_element
ai_find_element 的驼峰命名别名,功能完全一致(AI 查找界面元素)。
aiFindElement(desc: string) → table | nil
✅ 正确
local elem = aiFindElement("登录按钮")
if elem then
click(elem.x, elem.y)
end
aiReadText(x1, y1, x2, y2)
别名→ ai_read_text
ai_read_text 的驼峰命名别名,功能完全一致(AI 识别区域文本)。
aiReadText(x1: int, y1: int, x2: int, y2: int) → string
✅ 正确
local text = aiReadText(100, 200, 500, 300)
log("识别到: " .. text)
aiCheck(desc)
别名→ ai_check
ai_check 的驼峰命名别名,功能完全一致(AI 检查条件是否满足)。
aiCheck(desc: string) → boolean
✅ 正确
if aiCheck("页面已加载完成") then
click(500, 800)
end
aiSmartClick(desc)
别名→ ai_smart_click
ai_smart_click 的驼峰命名别名,功能完全一致(AI 智能查找并点击元素)。
aiSmartClick(desc: string) → boolean
✅ 正确
if aiSmartClick("确认按钮") then
log("点击成功")
else
log("未找到元素")
end
aiSmartWait(desc, timeout)
别名→ ai_smart_wait
ai_smart_wait 的驼峰命名别名,功能完全一致(AI 智能等待元素出现)。
aiSmartWait(desc: string, timeout: int) → boolean
✅ 正确
if aiSmartWait("登录成功页面", 15000) then
log("登录成功")
else
log("等待超时")
end
⚠️ 别名通用避坑指南:
- 同一引用:所有 24 个别名通过
globals.set(alias, globals.get(original)) 直接赋值,别名与原函数是同一对象引用,alias == original 为 true。
- 无独立实现:别名没有独立的函数体,修改原函数行为会同步影响别名(因为是同一引用)。
- 命名风格统一:建议同一脚本统一使用驼峰或下划线命名,不要混用。项目推荐使用下划线命名(snake_case)。
- 注册顺序:别名在所有 Bridge 注册完成后才注册(
TouchSpriteCompat.register 在最后调用),若原函数未注册则别名注册失败(日志输出「原函数不存在」)。
- 已删除冲突别名:
touchLongClick/touchMove/keepScreen 三个别名已删除(Bridge 已用驼峰命名直接注册同名函数)。