📖 古龙精灵 API 参考手册

面向游戏脚本开发者的完整 API 参考文档。涵盖触控模拟、图像识别、OCR 文字识别、YOLO 目标检测、设备控制、网络请求等全部功能。

🎨 阅读主题(暗夜 / 暖色护眼)

本手册右上角提供「主题」按钮,可以按阅读环境切换配色。选择会自动保存在浏览器中,下次打开本页仍然生效;主题只影响本页面的显示样式,不会改动脚本代码、控制台输出或用户面板。

主题主题内容(页面配色)适合场景
🌙 暗夜(默认) 暗红/黑色背景、米金色标题与正文、深色代码块、青绿色 API 名称 夜间阅读、OLED 屏幕,与 PC 开发工具「黑色」主题一致
☀️ 暖色(护眼) 米黄纸面背景、深棕正文、暖色代码高亮、低蓝光 白天或长时间阅读文档,与 PC 开发工具「暖色(护眼)」主题一致

💡 想恢复默认外观,点击右上角主题按钮重新选择「暗夜」即可。

🚀 概览与快速入门

什么是古龙精灵?

古龙精灵是一个面向游戏脚本开发者的全栈自动化平台。开发者使用 Lua 编写脚本,通过 PC 开发工具调试,最终打包为 APK 部署到 Android 设备上运行。平台提供丰富的 API 用于触控模拟、图像识别、OCR 文字识别、YOLO 目标检测等。

脚本基本结构

每个脚本是一个 Lua 文件,必须包含 main() 函数作为入口点。脚本引擎在加载文件后会自动调用 main()。

Lua - 脚本基本结构
-- ============================================================
-- 古龙精灵脚本模板
-- ============================================================

-- 加载子模块
dofile("settings_fields.lua")   -- 设置字段定义
dofile("server_api.lua")        -- 服务器通信模块

-- 全局配置
GAMENAME = "我的游戏脚本"
ACCOUNT_ID = "user_001"

-- 主函数(脚本引擎自动调用)
function main()
    log("脚本开始运行")
    
    -- 获取用户设置
    local auto_fight = get_setting("auto_fight", true)
    
    -- 主循环
    while true do
        if auto_fight then
            tap(500, 800)       -- 点击攻击按钮
            sleep(2000)             -- 等待2秒
        end
        sleep(1000)
    end
end

main()

开发流程

  1. 在 PC 开发工具中编写 Lua 脚本
  2. 使用工具 → 抓取窗口获取坐标、颜色、图片等数据
  3. 在开发工具中连接 Android 设备进行调试
  4. 通过开发者面板上传脚本包
  5. 打包 APK 分发给用户

运行环境与权限准备

触摸与按键注入依赖下面任意一条通道;两条都没有时,tap / swipe / swipeHuman / longTap / touchDown / touchUp 会返回 false,并在运行日志里写明原因(不会再静默失败,也不会用普通进程的 input 命令假装成功)。

通道开启方式能做什么
无障碍服务 系统设置 → 无障碍 → 已安装的服务 → 打开「古龙精灵」(推荐,无需 root;部分机型重启后需要重新确认) tap / tapRandom / longTap / swipe / swipeHuman / touchDown 三件套;整条手势一次派发、最精确,会等手势真正走完才返回
root 权限 设备已 root;脚本首次触摸时在超级用户弹窗点「允许」(持久 su 通道,只弹一次) 最万能:多点触控、真正的「按住拖动」(touchDown/touchMove/touchUp)、物理按键、截图;su 通道长期驻留并已调低 oom_score_adj 抗后台回收
都没有 — 触摸类 API 返回 false,日志提示「请开启…无障碍服务或授予root权限」;不会再出现「脚本跑得好好的但屏幕没动」
常见现象对照(日志里搜关键词即可定位):
· 点击操作失败!请开启…无障碍服务 → 无障碍服务未开启或已被系统关闭
· 无障碍点击/滑动未生效:…被系统取消(onCancelled) → 同一时刻已有别的手势在跑(上一次滑动还没走完就发了下一次),两次滑动之间加 sleep / random_sleep
· 没有配对的 touchDown → touchUp 之前漏了 touchDown,或中间被触摸状态重置清掉了
· touchMoveEx … 没有可用的起点坐标,起点退化为目标点 → 漏了 touchDown(位移为 0,看起来「滑了但没动」)
· 轨迹要走曲线 / 每次都不一样 → 用 swipeHuman(swipe 是绝对直线,机器特征明显)

🎨 UI 控件参考(用户设置面板)

脚本可以在 SCRIPT_SETTINGS_FIELDS 中定义设置字段,这些字段会显示在用户控制面板的"脚本设置"选项卡中。所有控件定义使用 Lua table 格式。

控件结构层级

设置字段支持三层嵌套结构:fold(折叠面板) → tab_container(选项卡容器) → 控件字段;任意层都可以再用 hbox / row(横向)、vbox(纵向) 布局容器把多个控件排到一起。

Lua - 完整结构示例
SCRIPT_SETTINGS_FIELDS = {
    {
        key = "section_1",
        label = "战斗设置",
        type = "fold",              -- 折叠面板
        default = true,
        icon = "⚔️",
        fields = {
            {
                type = "tab_container",  -- 选项卡容器
                tabs = {
                    {
                        label = "基础",
                        icon = "❤️",
                        fields = {
                            {key = "auto_fight", label = "自动打怪", type = "bool", default = true, icon = "⚔️"},
                            -- ... 更多控件
                        }
                    },
                }
            }
        },
    },
}

1. ☑️ 布尔开关 (bool)

用于开启/关闭某个功能。必填属性:key, label, type, default。可选属性:icon, description。

属性类型必填说明
keystring✅唯一标识,脚本中用此 key 读取值
labelstring✅显示名称
type"bool"✅控件类型
defaultboolean✅默认值:true 或 false
iconstring图标(HTML实体编码)
descriptionstring提示文字
示例:基础开关 / 带副标题开关 / 默认关闭开关 / 通知开关 / 调试模式开关
-- 基础开关(默认开启)
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
{key = "auto_fight", label = "自动打怪", type = "bool", default = true, icon = "⚔️", description = "是否开启自动打怪功能"},

-- 带副标题开关
{key = "auto_pickup", label = "自动拾取", type = "bool", default = true, icon = "📦", description = "是否自动拾取掉落物品"},

-- 默认关闭开关
{key = "auto_sell", label = "自动出售装备", type = "bool", default = false, icon = "💰", description = "是否自动出售背包中的装备"},

-- 通知开关
{key = "enable_notify", label = "启用通知", type = "bool", default = true, icon = "🔔", description = "是否启用任务通知提醒"},

-- 调试模式开关
{key = "debug_mode", label = "调试模式", type = "bool", default = false, icon = "🐛", description = "开启后显示调试信息"},

2. 🔢 数字输入 (number)

用于输入数值。必填属性:key, label, type, default, min, max。可选:step, unit, icon, description。

属性类型必填说明
keystring✅唯一标识
labelstring✅显示名称
type"number"✅控件类型
defaultnumber✅默认值
minnumber✅最小值
maxnumber✅最大值
stepnumber步进值,默认1
unitstring单位(秒、%、分钟等)
iconstring图标
descriptionstring提示文字
示例:基础数字 / 百分比数字 / 时长数字 / 次数数字 / 浮点数
-- 基础数字(带单位)
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
{key = "fight_interval", label = "打怪间隔", type = "number", default = 5, min = 1, max = 60, step = 1, unit = "秒", icon = "⏱️"},

-- 百分比数字
{key = "hp_threshold", label = "回血阈值", type = "number", default = 30, min = 10, max = 90, step = 5, unit = "%", icon = "❤️"},

-- 时长数字(分钟)
{key = "max_duration", label = "最大时长", type = "number", default = 120, min = 30, max = 1440, step = 10, unit = "分钟"},

-- 次数数字
{key = "max_attempts", label = "最大尝试次数", type = "number", default = 3, min = 1, max = 99, step = 1, unit = "次"},

-- 浮点数(带小数)
{key = "move_speed", label = "移动速度", type = "number", default = 1.5, min = 0.5, max = 5.0, step = 0.1, unit = "倍"},

3. 📝 文本输入 (text)

用于输入文本。必填属性:key, label, type, default。可选:placeholder, password, multiline, icon, description。

属性类型必填说明
keystring✅唯一标识
labelstring✅显示名称
type"text"✅控件类型
defaultstring✅默认值
placeholderstring占位提示文字
passwordboolean是否为密码输入框
multilineboolean是否多行文本
iconstring图标
descriptionstring提示文字
示例:基础文本 / 密码输入 / 多行文本 / 过滤关键词
-- 基础文本(带占位符)
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
{key = "target_monster", label = "目标怪物", type = "text", default = "银月贤者", icon = "🎯", placeholder = "输入怪物名称", description = "要自动攻击的怪物名称"},

-- 密码输入
{key = "account_password", label = "账号密码", type = "text", default = "", icon = "🔑", placeholder = "输入密码", password = true, description = "登录密码"},

-- 多行文本
{key = "custom_notice", label = "自定义公告", type = "text", default = "", icon = "📋", multiline = true, placeholder = "输入公告内容..."},

-- 过滤关键词
{key = "filter_keywords", label = "过滤关键词", type = "text", default = "", icon = "🔍", placeholder = "关键词用逗号分隔"},

4. 📋 下拉选择 (select)

用于从预设选项中选择。必填属性:key, label, type, default, options。

属性类型必填说明
keystring✅唯一标识
labelstring✅显示名称
type"select"✅控件类型
defaultstring✅默认值(对应 option 的 value)
optionstable✅选项列表:{{value="x", label="显示名"}, ...}
iconstring图标
descriptionstring提示文字
示例:基础下拉 / 地图选择 / 职业选择 / 分辨率选择
-- 基础下拉选择
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
{key = "difficulty", label = "难度选择", type = "select", default = "normal", icon = "🎮",
 options = {{value = "easy", label = "简单"},{value = "normal", label = "普通"},{value = "hard", label = "困难"},{value = "hell", label = "地狱"}},
 description = "选择打怪难度等级"},

-- 地图选择
{key = "target_map", label = "目标地图", type = "select", default = "map1", icon = "🗺️",
 options = {{value = "map1", label = "新手村"},{value = "map2", label = "幽暗森林"},{value = "map3", label = "冰封雪域"},{value = "map4", label = "火焰山"}}},

-- 职业选择
{key = "class_type", label = "职业选择", type = "select", default = "warrior",
 options = {{value = "warrior", label = "战士"},{value = "mage", label = "法师"},{value = "archer", label = "弓箭手"},{value = "assassin", label = "刺客"}}},

-- 分辨率选择
{key = "resolution", label = "分辨率", type = "select", default = "720p",
 options = {{value = "720p", label = "720x1280"},{value = "1080p", label = "1080x1920"},{value = "2k", label = "1440x2560"}}},
⚠️ 重要提示:中文 key 必须使用方括号语法
如果 key 中包含中文字符(例如 "difficulty_选择采集物品"),在入口文件中必须使用方括号语法 settings["key"] 来获取值,不能使用点号语法 settings.key。
示例:包含中文的 key 用方括号读取
-- ===== 📋 select(下拉选择)=====
-- 读取用户选择的值,用于条件判断
local difficulty_选择采集物品 = settings["difficulty_选择采集物品"] or "wp_流寇"

-- 【片段】下面这段「控件定义」属于 settings_fields.lua 的字段定义(不能单独运行/检查)
-- 控件定义
{key = "difficulty_选择采集物品", label = "采集物品", type = "select", default = "normal", icon = "🎮",
 options = {{value = "wp_流寇", label = "流寇"},{value = "wp_煤矿", label = "煤矿"},{value = "wp_铁矿", label = "铁矿"},{value = "wp_伐木场", label = "伐木场"}},
 description = "选择采集物品"},

-- 【可运行代码】下面这段可以直接运行/语法检查
-- ✅ 正确:使用方括号语法
local val = settings["difficulty_选择采集物品"]
-- ❌ 错误:不能使用点号语法(key含中文时)
-- local val = settings.difficulty_选择采集物品  -- 这会报错!

5. 📦 折叠面板 (fold)

用于将一组控件折叠/展开。必填属性:key, label, type, default, fields。fields 内部可以嵌套 tab_container,tab_container 的 tabs 里才是真正的控件。

属性类型必填说明
keystring✅唯一标识
labelstring✅显示名称
type"fold"✅控件类型
defaultboolean✅是否默认展开
iconstring图标
descriptionstring提示文字
fieldstable✅内部控件列表(通常含 tab_container)
fold 完整示例
{
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
    key = "fight_settings",
    label = "战斗设置",
    type = "fold",
    default = true,
    icon = "⚔️",
    description = "战斗相关设置(可折叠)",
    fields = {
        {
            type = "tab_container",
            tabs = {
                {
                    label = "基础",
                    icon = "⚔️",
                    fields = {
                        {key = "auto_fight", label = "自动打怪", type = "bool", default = true, icon = "⚔️"},
                        {key = "fight_interval", label = "打怪间隔", type = "number", default = 5, min = 1, max = 60, step = 1, unit = "秒"},
                    }
                },
                {
                    label = "高级",
                    icon = "⚙️",
                    fields = {
                        {key = "debug_mode", label = "调试模式", type = "bool", default = false, icon = "🐛"},
                    }
                },
            }
        }
    },
},

5.1 ↔️ 横向布局容器 (hbox / row)

把多个控件放在同一行(例如两个开关并排、两个说明按钮并排)。子控件自动平分宽度,太窄时换行;行内可以是任意控件(bool / number / select / button / comment_popup 等),也可以再嵌套 hbox / vbox。子字段写在 fields 里(旧写法 children 也兼容);row 是 hbox 的别名,效果完全相同。

属性类型必填说明
type"hbox" / "row"✅横向布局容器(二者等价)
fieldstable✅同一行里的控件列表(也可写 children)
labelstring说明文字(可选,vbox 会显示在顶部)
两个开关并排 / 两个说明按钮并排
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
{
    type = "hbox",
    fields = {
        {key = "auto_a", label = "三大陆", type = "bool", default = true, icon = "🌍"},
        {key = "auto_b", label = "四大陆", type = "bool", default = false, icon = "🌎"},
    }
},
{
    type = "row",   -- 等价写法:row = hbox
    fields = {
        {type = "comment_popup", label = "使用说明", title = "使用说明(必读)", content = "脚本会自动寻路打怪,请先手动走到怪物附近", style = "info"},
        {type = "comment_popup", label = "更新日志", title = "更新日志", content = "本次更新内容", style = "success"},
    }
},

6. 📑 选项卡容器 (tab_container)

在折叠面板内部创建多个选项卡,每个选项卡包含一组控件。通常在 fold 的 fields 内部使用。

属性类型必填说明
type"tab_container"✅固定值
tabstable✅选项卡列表,每个 tab 有 label, icon, fields

7. 🎨 颜色选择 (color)

示例
-- 颜色选择器
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
{key = "theme_color", label = "主题颜色", type = "color", default = "#FF5722", icon = "🎨", description = "选择界面主题颜色"},

-- 文字颜色
{key = "text_color", label = "文字颜色", type = "color", default = "#FFFFFF", icon = "✏️"},

📋 入口文件使用示例(完整)

以下示例展示如何在脚本入口文件 main() 中读取所有类型控件的值,并据其控制脚本逻辑。覆盖:bool、number、text、select、date、time、slider、file、multi_select、tag 等全部控件类型。这是实际项目中最核心的模式。

Lua - 入口文件完整示例(全部控件读取 + 使用)
-- ============================================================
-- 第一步:加载设置字段定义文件
-- ============================================================
dofile("settings_fields.lua")

-- ============================================================
-- 第二步:main() 入口函数 - 获取并读取所有控件值
-- ============================================================
function main()
    log("==================================================")
    log("  古龙精灵 - 使用示例")

    -- ===== 获取用户设置 =====
    local settings = get_user_settings()

    -- ===== ☑️ bool(布尔开关)=====
    -- 中文key必须用方括号 settings["key"]
    local auto_fight = false
    if settings["auto_fight_自动打怪"] ~= nil then
        auto_fight = settings["auto_fight_自动打怪"]
    end
    -- 英文key可用点号语法
    local auto_pickup = settings.auto_pickup
    if auto_pickup == nil then auto_pickup = true end

    -- ===== 🔢 number(数字输入)=====
    -- 读取并在脚本中使用,单位由控件定义决定(秒、%、分钟等)
    local fight_interval = settings["fight_interval"] or 5           -- 控件单位:秒
    local hp_threshold = settings.hp_threshold or 30                      -- 控件单位:%
    local max_duration = settings.max_duration or 120                    -- 控件单位:分钟
    local move_speed = settings.move_speed or 1.5                      -- 控件单位:倍(浮点数)
    log("  📊 打怪间隔=" .. fight_interval .. "秒, 回血=" .. hp_threshold .. "%, 最长" .. max_duration .. "分钟, 移速x" .. move_speed)

    -- ===== 📝 text(文本输入)=====
    -- 读取用户输入的文本,常用于匹配目标名称、过滤关键词等
    local target_monster = settings.target_monster or "银月贤者"
    local filter_keywords = settings.filter_keywords or ""
    log("  📝 目标怪物=" .. target_monster .. ", 过滤词=" .. filter_keywords)

    -- ===== 📋 select(下拉选择)=====
    -- 读取用户选择的值,用于条件判断
    local difficulty = settings.difficulty or "normal"
    local target_map = settings.target_map or "map1"
    if difficulty == "hell" then
        log("  ⚠️ 地狱难度已开启!")
    end

    -- ===== 📅 date(日期选择)=====
    local start_date = settings.start_date or "2025-01-01"
    -- 可用于判断当天是否在设定日期之后才执行
    local y, m, d, h = get_current_time()
    local today = string.format("%d-%02d-%02d", y, m, d)
    if today > start_date then
        log("  📅 日期已到达,开始执行...")
    end

    -- ===== ⏰ time(时间选择)=====
    local start_time = settings.start_time or "08:00"
    -- 解析时间字符串用于判断当前是否在指定时间之后
    local start_hour = tonumber(start_time:sub(1, 2)) or 8
    if h >= start_hour then
        log("  ⏰ 已过" .. start_time .. ",开始执行任务")
    end

    -- ===== 🎚️ slider(滑块)=====
    local volume = settings.volume or 80
    -- 将滑块值(0-100)映射到实际逻辑(如点击速度比例)
    local click_speed = 1.0 + (volume / 100) * 2.0  -- 映射为 1.0~3.0 倍速
    log("  🎚️ 音量=" .. volume .. ", 点击倍速=" .. click_speed)

    -- ===== 📁 file(文件选择)=====
    local script_file = settings.script_file or ""
    if script_file ~= "" then
        log("  📁 用户指定脚本文件: " .. script_file)
        -- 可以 dofile(script_file) 加载用户指定的脚本
    end

    -- ===== ☑️ multi_select(多选列表)=====
    -- 多选列表返回的是 table(数组),包含所有选中的 value
    local target_maps = settings.target_maps or {}
    if #target_maps > 0 then
        log("  🗺️ 多选地图: " .. table.concat(target_maps, ", "))
        -- 遍历每个选中的地图执行逻辑
        for i, map_name in ipairs(target_maps) do
            log("    正在处理地图: " .. map_name)
            -- 跳转到对应地图、执行对应逻辑...
        end
    else
        log("  🗺️ 未选择地图,使用默认地图")
    end

    -- ===== 🏷️ tag(分组标签)=====
    -- 分组标签返回的是 table(数组),包含所有选中的标签 value
    local item_filter = settings.item_filter or {}
    local filter_equip = false
    local filter_potion = false
    for i, tag in ipairs(item_filter) do
        if tag == "equip" then filter_equip = true end
        if tag == "potion" then filter_potion = true end
    end
    log("  🏷️ 物品过滤: 装备=" .. tostring(filter_equip) .. ", 药水=" .. tostring(filter_potion))

    -- ===== 🎨 color(颜色选择)=====
    local theme_color = settings.theme_color or "#FF5722"
    log("  🎨 主题颜色: " .. theme_color)

    -- ===== 主循环:根据控件值控制脚本逻辑 =====
    while true do
        if auto_fight then
            log("⚔️ 自动打怪开始!目标: " .. target_monster .. ", 难度: " .. difficulty)
            -- 调用对应的打怪子程序
        else
            log("⚔️ 自动打怪已关闭")
            log("  [提示] 请在用户控制面板中开启自动打怪")
        end
        sleep(fight_interval * 1000)
    end
end

main()

8. 📅 其他控件

日期选择 (date)、时间选择 (time)、滑块 (slider)、文件选择 (file)、多选列表 (multi_select)、分组标签 (tag)

其他控件示例
-- 日期选择
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
{key = "start_date", label = "开始日期", type = "date", default = "2025-01-01", icon = "📅"},

-- 时间选择
{key = "start_time", label = "开始时间", type = "time", default = "08:00", icon = "⏰"},

-- 滑块
{key = "volume", label = "音量调节", type = "slider", default = 80, min = 0, max = 100, step = 1, icon = "🔊"},

-- 文件选择
{key = "script_file", label = "脚本文件", type = "file", default = "", icon = "📄", placeholder = "选择脚本文件..."},

-- 多选列表
{key = "target_maps", label = "目标地图(多选)", type = "multi_select", default = {}, icon = "🗺️",
 options = {{value = "map1", label = "新手村"},{value = "map2", label = "幽暗森林"},{value = "map3", label = "冰封雪域"}}},

-- 分组标签
{key = "item_filter", label = "物品过滤", type = "tag", default = {}, icon = "🏷️",
 options = {{value = "equip", label = "装备"},{value = "potion", label = "药水"},{value = "material", label = "材料"}}},

9. 🔘 按钮控件 (button)

按钮控件点击后 执行脚本里的函数(func 指定的函数名)。平台会自动把动作派发给脚本执行 —— Lua 侧零代码(不需要自己写轮询/分发)。

属性类型必填说明
keystring✅唯一标识(按钮不保存值,仅作占位)
labelstring✅按钮上显示的文字
type"button"✅控件类型
funcstring✅要执行的脚本函数名。必须与脚本里的函数名完全一致,且不能带 ();支持「模块表.函数名」写法(如 module_zx.一键日常)
stylestring按钮样式:primary(默认)/ success / warning / danger
iconstring图标(HTML实体编码)
confirmstring二次确认文案(如 "确定要重置今日任务吗?")。危险操作建议同时设 style = "danger"
descriptionstring按钮下方的提示文字
settings_fields.lua - 按钮控件示例
-- 基础按钮:点击后执行脚本里的 判断_打开文件()
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
{key = "btn_open_log", label = "查看日志文件", type = "button",
 func = "判断_打开文件", style = "primary", icon = "📄", description = "点击后执行脚本里的 判断_打开文件() 函数"},

-- 模块表里的函数:func 写「模块表.函数名」(支持)
{key = "btn_daily", label = "一键日常", type = "button",
 func = "module_zx.一键日常", style = "success", icon = "🎮"},

-- 危险操作:加二次确认(confirm)+ 红色样式(danger)
{key = "btn_reset_task", label = "重置今日任务", type = "button",
 func = "重置今日任务", style = "danger", icon = "⚠️",
 confirm = "确定要重置今日任务吗?", description = "需要用户确认后才会执行"},
脚本里只写业务函数即可(平台自动派发,不用写任何轮询代码)
-- 函数名与按钮 func 一致即可;中文函数名、模块函数都支持
function 判断_打开文件(params)
    local content = read_file_content("/sdcard/gulong/日志.txt")
    show_message_keep(content or "日志不存在", nil, nil, 1)
end

-- 模块表里的函数(对应 func = "module_zx.一键日常")
module_zx = module_zx or {}
function module_zx.一键日常(params)
    log("开始执行一键日常")
end

执行机制(脚本侧无需配合):

  • 点击按钮 → 面板把「函数名」发给平台;App 内面板点击走本机队列(不经过服务器、无网络延迟),用浏览器打开面板则走服务器队列;
  • 脚本正在运行:平台在脚本的让出点(sleep / 各类 API 调用)自动派发执行(同一 Lua 线程,函数内部报错不会中断脚本);
  • 脚本没在运行:平台自动启用「按钮直跑」宿主(独立 Lua 环境加载你的脚本,只加载函数、不调用 main()),首次点击约 1~2 秒准备,之后即时;
  • 服务器会校验 token、「函数是否在 settings_fields.lua 的按钮里声明过」、账号剩余时间与触发频率;start_script / stop_script / save_config / reset_config 为内置动作,不会转发给脚本。

注意事项:

  • func 必须与脚本里的函数名一致,不能带 ():写 func = "判断_打开文件",不要写 "判断_打开文件()";
  • 只有 settings_fields.lua 里声明过的按钮函数才会被执行(白名单);改了 func 必须重新推送脚本;
  • 按钮函数建议写成自包含的(能自己读配置/文件,不依赖 main() 里才赋值的全局变量);参数固定从 params 取(平台传入的参数表,通常为空表);
  • confirm 二次确认依赖 App 的 WebView 对话框(古龙精灵 / 古龙代码箱最新版已支持);用很老的 APK 时,点带 confirm 的按钮会没反应,装上最新 APK 即可。

10. 🪟 浮窗按钮(APK 悬浮窗第二行)

浮窗按钮由开发者按游戏配置,不需要写进 settings_fields.lua。配置后,用户展开 APK 悬浮窗时,会在固定按钮下方的第二行看到这些按钮;点一下即执行绑定的脚本函数。每个游戏最多 5 个。

配置步骤

  1. 打开开发者面板 →「🎮 游戏管理」;
  2. 找到目标游戏,点该游戏行的「🧩 浮窗按钮」;
  3. 点「➕ 添加按钮」,逐行填写 图标 / 名称 / 函数名(最多 5 个,整行空白视为不要该按钮);
  4. 点「💾 保存」。APK 复用动作轮询线程每 60 秒拉一次配置,保存后最多约 1 分钟生效;展开一次悬浮窗会按本机快照立即重绘。
字段必填说明
图标按钮图标(emoji),可省略,默认 ⭐;最多保留 2 个字符
名称✅悬浮窗按钮上显示的文字,最多 6 个字(超出会被截断)
函数名✅脚本里的全局函数名,必须与脚本完全一致且不能带 ();支持中文 / 字母 / 数字 / 下划线,支持「模块表.函数名」写法(如 module_zx.一键日常)
Lua - 浮窗按钮要执行的函数(必须是全局函数)
-- 【重点】必须写成全局 function(不要写 local function),否则平台找不到
function 一键回收(params)
    log("♻️ 开始一键回收")
    -- 这里写自己的回收逻辑,例如找图后点击
    local r = find_image("images/recycle.png")
    if r and r.success then
        tap(r.x, r.y)
        sleep(800)
    end
    log("✅ 一键回收完成")
end

-- 支持「模块表.函数名」:开发者面板里函数名填 module_zx.一键日常
module_zx = module_zx or {}
function module_zx.一键日常(params)
    log("📅 开始一键日常")
    -- 依次调用日常子任务...
end

-- 函数建议自包含:自己读用户设置,不依赖 main() 里才赋值的全局变量
function 切换地图(params)
    local map = get_setting("target_map", "新手村")
    log("🗺️ 切换到地图: " .. map)
    -- 这里写切图逻辑...
end

-- 可选:函数执行完想让用户看到结果,可用驻留消息窗
function 查看状态(params)
    local remain = get_setting("last_remain_text", "未知")
    show_message_keep("📊 当前状态: " .. remain, nil, nil, 2)
end
开发者面板里按下面这样填(与上面的函数一一对应):
图标 ♻️ / 名称 回收 / 函数名 一键回收
图标 📅 / 名称 日常 / 函数名 module_zx.一键日常
图标 🗺️ / 名称 换图 / 函数名 切换地图
图标 📊 / 名称 状态 / 函数名 查看状态

执行机制(脚本侧无需配合):

  • 点击 → APK 本机队列(ServerApi.queueLocalAction),与用户面板 button 控件走同一条链路,不经过服务器;
  • 脚本正在运行:平台在脚本让出点(sleep / 各类 API 调用)自动派发执行;脚本没在运行:平台自动拉起「按钮直跑」宿主(独立加载脚本、只加载函数、不调用 main()),首次点击约 1~2 秒准备,之后即时;
  • 账号剩余时间为 0 时,悬浮窗直接提示「⏰ 剩余时间不足,请先充值」,不会执行;
  • 同一函数 300ms 内只接受一次点击(防连点);函数内部报错只写运行日志,不会中断脚本主流程。

注意事项:

  • 函数名只写名字,不带括号:写 一键回收,不要写 一键回收();
  • 必须是全局函数;写成 local function 平台找不到(日志会出现「按钮函数未找到」);
  • start / stop / restart / start_script / stop_script / restart_script / save_config / reset_config / dofile / load / loadstring / require / call_python 为保留名,保存时会被服务端过滤;
  • 配置按游戏保存;同一「共享分组」的游戏共用同一份配置。删掉所有行或保存 0 个 → 悬浮窗不显示第二行(固定按钮不受影响);
  • 函数建议写成自包含的:自己读用户设置 / 文件,不依赖 main() 里才赋值的全局变量;参数固定从 params 取(平台传入的参数表,通常为空表);
  • APK 进程重启后会先读本机缓存,服务器暂时连不上也能显示上次的按钮;网络恢复后会在下一次轮询时更新。

📷 Bug截图(用户 → 开发者,同一个「🧩 浮窗按钮」弹窗里勾选)

浮窗按钮弹窗底部有一个 「☑ 📷 Bug截图」开关。勾选并保存后,该游戏的每个用户悬浮窗第二行尾部会多出一个 「📷 Bug截图」按钮;用户点一下即可把当前屏幕发给开发者,用于反馈 bug。这个按钮不占用上面 5 个自定义按钮的名额,也不需要写任何 Lua 函数。

项目说明
截图内容整屏原始位图(PNG)。PNG 的 base64 超过 8MB(约 6MB 原图)时自动改用 JPEG(质量 90)再上传
画面与提示点一下不会先在屏幕上写字:APK 会先撤掉悬浮窗上的提示(包括上一条 show_message),等它真正从画面里消失再截屏;上传完成之后才提示「✅ Bug截图已发送给开发者」。所以开发者收到的图片里不会带「正在上传截图」这类文字。同一时间只处理一张,连点不会白耗额度
文件名Bug截图_{用户账号}_{年月日_时分秒}.png,在开发者 PC 抓取窗口里就是这个标签名
次数限制悬浮窗按钮:每个用户账号每天 20 张;脚本 API server_api.bug_screenshot():每账号每天 60 张(两份额度独立计数,同一账号跨游戏共享,自然日 0 点重置)。另外每个开发者每天合计最多 100 张(按钮 + 脚本、名下所有账号合并计数;超出后该开发者名下所有账号都提示明天再试)。脚本额度还限制 2 秒最多 1 张(客户端 + 服务端各限一次);超出时 APK 提示「今天已达上限,请明天再试」或「截图过于频繁」,服务端返回 429
存储临时中转(落盘):图片保存在服务器的临时缓存目录,保留 1 小时后自动删除;内存里只有文件名/大小等元信息,所以 100 个人同时截图也不会把服务器内存撑爆。每个开发者最多缓存 100 张(超出丢最旧的),全站磁盘配额 512MB。需要长期保存请开发者在开发工具里自行保存
执行条件截图在独立线程执行:不检查剩余时间(过期账号也能反馈)、不要求脚本在运行、不走按钮队列 / 保留名过滤
开发者接收开发工具「📷 抓取窗口」工具栏点 「📷 Bug截图」 开始接收(需先登录开发者账号),每 5 秒检查一次,收到即自动开成新标签页;开了「保存截图」还会顺手存一份到保存目录

接口(APK / 开发工具自动调用,一般不需要手写):

链路:新版本 APK 把截图直接上传到打包服务器 http://mhgulong.uunat.com/(图片落盘、内存只留元信息,100 人同时截图也不占主站内存); 主站 https://guljl.com 保留同一组接口,只用于兼容还没升级的老版本 APK。 开发工具的抓取窗口会同时检查这两个来源,收到的图都会自动开成标签页。

方法与路径鉴权说明
POST /api/account/<账号>/bug-screenshot用户 token(须与该账号一致)请求 {"token": "...", "base64": "...", "source": "button|script"}(老 APK 不传 source 按 button 处理);成功返回 {success, filename, used, limit, remaining, source, dev_used, dev_limit, dev_remaining, message};超出账号每日额度 / 开发者每日总额度 / 脚本 2 秒限流 429(scope 分别为 account / developer / throttle),图片过大 413,账号未绑定开发者 403,上传并发已满 503(稍后再试)
GET /api/developer/bug-screenshots开发者 token返回 {success, screenshots:[{id, account_id, game, filename, size, time, source}], daily_limit, script_daily_limit, dev_daily_limit, dev_used, dev_remaining, keep_seconds},只给元信息(图片另取,避免一次拉几十 MB)
GET /api/developer/bug-screenshot/<id>开发者 token返回 {success, base64, filename, account_id, game, source};截图过期 / 不属于该开发者返回 404
看不到图?① 先确认开发者账号已登录(开发工具按钮没登录会提示);② 用户提交后 1 小时内要在开发工具里接收,超时会过期;③ 图片缓存在服务器磁盘上,保留 1 小时,过期自动删除;④ 悬浮窗按钮当天 20 张 / 脚本 API 当天 60 张用完需要等次日(两份额度独立),或该开发者当天按钮 + 脚本合计已到 100 张;⑤ 该开关按游戏保存,同一「共享分组」的游戏共用一份配置,切换游戏后要确认对应游戏已勾选;⑥ 老版本 APK 会传到主站、新版本传到打包服务器,开发工具会同时收,不需要手动切换。

🌐 公共设置(面板内置)

「公共设置」是用户面板「设置」Tab 顶部面板自带的一块配置(不属于脚本的 SCRIPT_SETTINGS_FIELDS,脚本不用声明、也读不到它的 values),目前包含两个字段:

字段类型默认值说明
开机启动脚本booleanfalse(关)开启后:模拟器/设备开机时 APK 自动启动脚本,并自动关闭控制面板。等待期可点「立即启动」跳过剩余等待
开机后等待多少秒number20开机后等待这么久再启动脚本,取值 0~600 秒(等模拟器稳定,建议 ≥20 秒)

执行流程(脚本侧无需配合):

  • 用户在面板点「💾 保存公共设置」→ 存到平台(按账号保存,与设备无关);
  • App 内保存 / 加载时会把这份配置顺手缓存到本机,开机(BOOT_COMPLETED)时直接读缓存判断,不依赖网络;
  • 开机流程:打开控制面板 → 等待 N 秒 → 启动脚本 → 关闭面板;App 起来后还会用平台的值校正一次(换设备 / 别处改了配置也能生效);
  • 等待期屏幕提示:这 N 秒里控制面板上会盖一层动态倒计时(进度环 + 剩余秒数 + 呼吸光晕,每秒脉冲一次),下方还有「立即启动」按钮:不想等时可以点它跳过剩余等待,立即发送启动脚本指令并关闭面板;到点未点击则自动走同一流程。倒计时到点切成「正在启动脚本…」再自动关闭面板 —— 用户能明确看到「还在等、还有几秒」,不会以为卡死。想先看看效果,可在「设置 → 公共设置」点「🔍 预览倒计时效果」(预览时按钮显示为「结束预览」,点击只关闭预览,不会启动脚本);
  • 开关关掉时只打开面板,不自动启动脚本(与旧版本行为一致)。

11. 💾 保存设置样式(样式1 / 样式2 预设)

用户面板「设置」Tab 底部的保存区域长什么样,由开发者按游戏配置:默认是样式1(一个「💾 保存设置」按钮,与老版本完全一样),也可以切成样式2(一行「设置N 下拉 + 💾 保存 + 📥 读取」,让用户把几套设置存起来来回切)。这项配置不需要写进 settings_fields.lua,也不需要重新推送脚本——用户在面板上刷新一次就能看到新样式。

对比项样式1(默认)样式2(预设)
用户看到的样子一个「💾 保存设置」按钮「设置N」下拉 + 「💾 保存」+「📥 读取」+ 一行提示
点一下做什么把当前控件上的值写进服务端保存:当前值存进选中的预设(本机)+ 立刻写服务端;读取:预设值回填到控件 + 立刻写服务端
多套设置不支持(服务端只保留一套)支持 1~12 套,随时切换
预设内容存哪—用户手机的浏览器 localStorage;服务端仍只保存「当前生效的那套」
适合场景设置项少、只有一种用法设置项多、用户需要在几套配置之间来回切(不同副本 / 不同角色 / 不同任务)

配置步骤(开发者面板)

  1. 打开开发者面板 →「🎮 游戏管理」;
  2. 找到目标游戏,点该游戏行的「💾 保存设置样式」;
  3. 在弹窗里选「样式1(默认)」或「样式2(预设)」;
  4. 选样式2 时填「🔢 预设个数」(1~12,默认 6),用户面板下拉会显示「设置1 ~ 设置N」;
  5. 点「💾 保存」(会写一条开发者日志 GAME_SAVE_SETTINGS_STYLE)。用户刷新 / 重新打开面板即生效,不需要重新推送脚本。

用户在样式2 下怎么用

  • 💾 保存:把当前控件上的值存进下拉选中的那一格,并立刻写服务端(脚本下一次读配置就是这套值);
  • 📥 读取:把那一格的值回填到控件,同样立刻写服务端,所以脚本会马上按这套值跑;
  • 下拉记忆:面板会记住上次用过的格子(存在用户本机),下次打开面板下拉直接停在那格;
  • ✏️ 改名:提示行右边的「✏️ 改名」可以给当前这格起个名字(最多 10 字,留空 = 恢复「设置N」);下拉里只显示名字(宽度全留给名字,手机上也不会被截断),提示行再给完整信息「设置3 · 打BOSS」,几套设置一眼就分得清;名字和预设一样只存在用户本机;
  • 提示行:下拉下面那行显示这一格有没有内容(「设置3 · 打BOSS」已有内容,点「📥 读取」套用 / 还没有内容,点「💾 保存」存下当前设置);没起名就是「设置N」;
  • 🌐 共享模板:开发者打开「允许用户提交模板」后,当前游戏的共享模板会直接并进这个「📱 本机预设」下拉(第二组,紫色 / 蓝灰)。共享模板只读:选中后「💾 保存」置灰、「✏️ 改名」失效,只有「📥 读取」可用(= 回填并立刻保存);提交入口仍是保存区下方的「📤 提交模板」;
  • 开关 / 数字 / 文本 / 下拉 / 多选(multi_select) / 标签(tag) 都能存进预设,读取时一起还原;
  • 预设存在用户本机:换手机 / 清缓存 / 换浏览器会丢,它不是平台上的云端配置。

脚本侧要不要改?

不用改。样式只影响面板上的按钮,脚本仍按老办法读「当前生效的那套设置」:

Lua - 样式2 下脚本照常读配置
-- 面板上「💾 保存」/「📥 读取」都会立刻写服务端,所以这里读到的一定是用户当前选中的那套
local auto_fight = get_setting("auto_fight", false)
local fight_interval = get_setting("fight_interval", 5)
log("自动打怪: " .. tostring(auto_fight) .. ",间隔 " .. tostring(fight_interval) .. " 秒")

平台不会把「用户正在用第几套」写进 user_settings。脚本若确实需要知道,请让用户在 settings_fields.lua 里自己选一个下拉项,或由脚本用 set_setting() 维护一个字段。

GET / POST /api/developer/game/<game_key>/save-settings-style读写保存设置样式
开发者面板「💾 保存设置样式」弹窗用的接口(需要开发者 token)。GET 读当前游戏的样式、预设个数、模板开关和已提交模板列表;POST 保存。POST 只传 save_settings_style 时,原来的预设个数会被保留,不会被重置;同理只传 allow_submit_template 不会改动样式。
参数类型说明
save_settings_stylenumberPOST 必填。1 = 老样式,2 = 预设样式;其它值一律按 1 处理
save_settings_preset_countnumber可选。预设个数 1~12,超出范围自动夹紧,非法值用默认 6
allow_submit_templatebool可选。是否允许用户提交共享设置模板;缺省 false(关闭)。关闭后用户端只是没有「📤 提交模板」入口;已经提交的模板仍会出现在下拉里、可以继续读取套用,也不会删除已存数据
JSON - POST 请求 / 返回
// 请求
{ "save_settings_style": 2, "save_settings_preset_count": 8, "allow_submit_template": true }

// 返回
{
  "success": true,
  "game": "梦幻逍遥",
  "game_key": "mhxy",
  "save_settings_style": 2,
  "save_settings_preset_count": 8,
  "allow_submit_template": true,
  "settings_template_limit": 12,
  "settings_templates": [
    { "id": "a1b2c3d4", "name": "打BOSS", "field_count": 3, "submitter": "user001", "updated_at": "2026-10-21T15:00:00" }
  ],
  "message": "已将「梦幻逍遥」的「保存设置」设为样式2(8 个预设),已允许用户提交模板"
}
注意事项
① 配置是按游戏的:同一开发者下各游戏互不影响,没配过的游戏一律是样式1;
② 切回样式1 时面板只剩「💾 保存设置」按钮,用户本机已有的预设不会丢,再切回样式2 还能读出来;
③ 把预设个数改小(例如 8 → 4)时下拉只剩「设置1 ~ 设置4」,但用户上次用的那格仍被记住,改回 8 时会回到原来那格(预设内容也还在);
④ 预设存在用户手机里(localStorage 键名 userPanel_savePresets_<账号>_<开发者>),开发者看不到也改不了;
⑤ 服务端始终只保留「当前生效的那套设置」,所以「多套并存」只在用户本机,脚本不需要也无法一次读多套;
⑥ 自定义名称也只存在用户本机(与预设同一个 localStorage),换手机 / 清缓存一起丢;名字只是给用户看的,不影响预设内容,也不会传给脚本。

共享设置模板(可选:用户提交,开发者管理)

同一个「保存设置样式」弹窗里勾选「📤 允许用户提交模板」并保存后,用户面板的保存区下方会多出「📤 提交模板」入口(样式1 / 样式2 都会显示)。用户可以把当前设置提交成模板(同名提交会覆盖自己的旧模板),开发者可以在本弹窗里查看提交人和模板内容概览、按需删除。模板按游戏存在服务端,不占用户本机的预设格子。

读取方式:样式2 下共享模板会直接并入「📱 本机预设」下拉(第二组「🌐 共享模板(只读,选中后点 📥 读取)」),不再单独放一个下拉 / 套用按钮。选中共享模板时它是只读的 ——「💾 保存」置灰、「✏️ 改名」失效,只有「📥 读取」可用:面板按 id 拉模板详情,把值回填到控件并立刻保存,脚本下一轮读配置就是这套值。下拉里 紫色 = 自己提交的(⭐),蓝灰 = 其他用户提交的(👤 · 打码账号)。样式1 没有预设下拉,共享模板只能提交、不能读取;要用共享模板请把「保存设置」改成样式2。

记忆:下拉会记住上次选中的那一项(本机格子或共享模板,存在同一个 localStorage 里),下次打开面板 / 重进「设置」页仍然选中它;共享模板被开发者删除后自动回落到本机记住的那一格。开发者关闭「允许用户提交模板」不影响已经提交的模板:它们仍然显示在下拉第二组、仍然可以读取套用(只是不再显示「📤 提交模板」入口)。

规则说明
数量上限每个游戏最多 12 个模板;满了再提交返回 409,需要开发者在弹窗里删掉旧模板
同名覆盖同一账号在同一个游戏重复提交同名模板 = 更新那一条(不额外占名额),方便用户改完设置再提交
提交人展示用户侧只显示打码账号(如 us***);开发者面板显示完整账号,方便清理。用户面板的模板下拉里,紫色 = 自己提交的(带「⭐ 我提交的」),蓝灰 = 其他用户提交的
关闭开关只收起「📤 提交模板」入口(提交接口返回 403);列表 / 详情接口照常返回,用户仍能在下拉里选中并读取已提交的模板。模板只有在开发者手动删除或删除游戏时才会消失
删除游戏删除游戏时同步清空该游戏的模板,避免重建同名游戏继承旧模板或继续占 12 个名额
GET /api/account/<account_id>/settings-templates用户拉取当前游戏的共享模板列表
用户面板用:拉当前游戏已有模板的元数据(并进「📱 本机预设」下拉的 🌐 组,不带 values,避免一次传几十 KB)。查询参数均可选:token(带了就校验)、kaifazhe_id(面板注入)、game_key(当前游戏)。每条模板带 mine:true = 当前账号提交的(用户面板据此用紫色标注,其他人是蓝灰)。列表不受「允许提交模板」开关影响:游戏里已经提交过的模板始终返回,开关只体现在 allow_submit(用户端是否显示「📤 提交模板」)。
JSON - 返回
{
  "success": true,
  "allow_submit": true,
  "limit": 12,
  "count": 1,
  "game_key": "mhxy",
  "game_name": "梦幻逍遥",
  "templates": [
    { "id": "a1b2c3d4", "name": "打BOSS", "field_count": 3, "submitter": "us***", "mine": true, "created_at": "2026-10-21T15:00:00", "updated_at": "2026-10-21T15:00:00" }
  ]
}
POST /api/account/<account_id>/settings-templates用户提交 / 覆盖自己的共享模板
body:{token?, kaifazhe_id?, game_key?, name, values}。name 最多 10 字(合并连续空白);values 只保留 JSON 基础类型 / 数组(多选、标签提交数组),服务端会限制字段数、单值长度和总大小。成功返回 updated(true = 覆盖了同账号同名模板)和模板元数据。
GET /api/account/<account_id>/settings-templates/<template_id>读取某个模板的完整 values
用户面板「📥 读取」选中共享模板时用:读取模板的完整 values 回填控件,返回 {success, template, values}(管理端 / 二次开发也可以直接用)。不要求开发者开着「允许提交模板」:开关只控制提交入口,已提交的模板随时可以读取;定位不到游戏返回 404,模板已被删除返回 404。
POST /api/developer/game/<game_key>/settings-templates/delete开发者删除某个模板
开发者面板模板列表里的「🗑️ 删除」用(需要开发者 token)。body:{"template_id": "a1b2c3d4"}。只删模板、不改开关;模板不存在返回 404。成功返回删除后的 settings_templates 列表。
说明
① 共享模板只存控件值(values),不存字段结构;读取时按当前 settings_fields.lua 回填,字段被删除 / 改名后对应值会被忽略;
② 模板内容由用户提交,开发者应在弹窗里检查后再保留;读取前请确认模板可信(读取会立刻覆盖当前设置并保存);提交人仅用于追溯,对其他用户只显示打码账号;
③ 12 个名额是每个游戏独立的,不同游戏互不影响。

⚙️ 脚本中获取用户设置参数

在脚本中,用户通过控制面板配置的设置可以通过多种方式获取。以下是完整的使用方法:

方法一:全局变量 user_settings

user_settings 是一个全局 table,包含用户在控制面板保存的所有设置。

Lua - 使用 user_settings
while true do
    if user_settings.auto_fight then
        log("自动打怪已开启")
        -- 执行打怪逻辑
    end
    sleep((user_settings.fight_interval or 5) * 1000)
end

方法二:get_setting() 函数

安全地获取单个设置项的值,如果键不存在则返回默认值。

Lua - 使用 get_setting
local auto_fight = get_setting("auto_fight", true)        -- 获取布尔值,默认 true
local fight_interval = get_setting("fight_interval", 5) -- 获取数字,默认 5
local target = get_setting("target_monster", "银月贤者") -- 获取文本
local difficulty = get_setting("difficulty", "normal") -- 获取下拉选择

if auto_fight then
    log("自动打怪已开启,间隔: " .. fight_interval .. "秒")
end

方法三:get_live_setting() 函数(读取未保存的实时值)

读取用户在「用户面板 → 脚本设置」卡片里当前控件上的值,包括刚输入、还没点「保存设置」的数字 / 文本 / 开关 / 下拉选择。仅在安卓端(古龙精灵 / 古龙代码箱)由用户面板按钮触发的脚本函数中可用:面板点击按钮时会把所有控件的当前值随动作一起提交,平台在派发函数前保存为快照。

Lua - 按钮函数读取实时值
-- settings_fields.lua:声明输入框和按钮(按钮 func 指向下面的全局函数)
{key = "boss_no", label = "Boss编号", type = "number", default = 1},
{key = "btn_run", label = "用当前输入执行", type = "button",
 func = "用实时值执行", style = "primary"}

-- 用户改了数字但没点保存,点按钮后这里拿到的就是输入框里的最新值
function 用实时值执行(params)
    local boss_no = get_live_setting("boss_no", 1)
    local auto_fight = get_live_setting("auto_fight", false)
    local target = get_live_setting("target_monster", "银月贤者")
    log("本次使用实时值: " .. boss_no .. ",auto_fight=" .. tostring(auto_fight))
end
  • 取值顺序:最近一次按钮点击的实时快照 → 已保存的 get_setting 值 → default;快照里有这个键就一定优先用实时值。
  • 不是持续同步:快照只在点击按钮时更新;用户输入后不点按钮,运行中的脚本不会自动看到新值。
  • 不会自动保存:它只读,不会把值写回服务器;要保存请让用户点「保存设置」,或在脚本里调用 set_setting。
  • 旧 APK 兼容:按钮函数也可以直接读 params.live_settings[key](面板会一并传入),不依赖新注入的全局函数;新 APK 推荐用 get_live_setting。
  • 设备端只有在用户面板按钮点击派发的脚本函数里才有实时快照;其它场景要读已保存的设置,请用 get_setting 或 server_api.get_user_settings()。

方法四:set_setting() / set_settings() 函数

在脚本中动态修改用户配置项的值,返回 true/false。新版服务器支持局部合并写入:只发改动的键、其它键保持原值,1 个请求就落库(响应带 merged=true);连到老版本服务器时自动回退成「先读 values → 合并 → 整体回写」,不会丢配置。要一次改多个键请用 set_settings(同样只有 1 个请求)。

🎯 一次改多个键想「同时跳变」就用 set_settings:客户端写设置复用同一条长连接(不再每个请求重新握手),面板还会把 250ms 内到达的多份快照合并成一次套用 —— 连着调用 set_setting 通常也会一起刷新,但要 100% 保证几个控件同一帧一起变,请用 set_settings({...})(1 个请求 = 1 份快照,几个开关一起亮/一起关)。

Lua - 使用 set_setting / set_settings
-- 写入成功后,用户面板里的对应配置会变成新值
if set_setting("auto_fight", false) then log("已关闭自动打怪") end
set_setting("fight_interval", 10)   -- 修改打怪间隔为10秒(自动合并,不会清掉其它设置)
set_setting("target_monster", "boss") -- 切换目标怪物;table 会存成 JSON 字符串

-- 一次写多个键:只有 1 个请求,面板立刻看到全部新值(推荐在按钮函数里用)
set_settings({
    -- 下面这些也可以直接用 get_live_setting 读未保存的控件值
    auto_fight = true,
    fight_interval = 10,
    boss_no = 3,
})

-- 脚本还没登录(拿不到用户 token)时返回 false;账号取当前项目的登录账号
local ok = set_setting("boss_no", 5)
if not ok then log("写入失败:请先确认已登录用户账号") end
⚠️ 运行时支持说明(与代码实现保持一致):get_live_setting 仅在用户面板按钮点击派发的脚本函数中可用(读取点击瞬间未保存的控件值),其它场景请用 get_setting 或 server_api.get_user_settings()。user_settings / get_setting 在「📦 代码箱调试」与已安装的「古龙精灵」「古龙代码箱」里均已注入——只读用户面板保存的 values,需要用户账号 + 用户 token(未登录时 get_setting 返回默认值、user_settings.xxx 为 nil),内部带 10 秒缓存,可以放心放在循环里读取。⚠️ user_settings 是「按需读取视图」:user_settings.auto_fight 这种按键读取可用,但 pairs(user_settings) / json_stringify(user_settings) 取不到内容,需要整表时请用 server_api.get_user_settings()。set_setting 读服务器上已有的 values → 合并本次修改 → 整体回写,需要用户账号 + 用户 token(账号口径 = 脚本用户,取当前项目的登录账号)。只有开发者账号登录时返回 false(开发者 token 与用户 token 是两套签名,服务器 /api/account/* 会拒绝);修改配置也可以直接在控制面板操作。

方法五:从 settings 字典读取(节点模式)

在节点模式下,通过服务器 API 获取用户设置后使用。这是实际项目(梦幻逍遥脚本)中使用的方式:

Lua - 从服务器获取设置后使用
-- 获取设置
local settings = get_user_settings()

-- 读取配置项(中文 key 必须用方括号)
local auto_fight_自动饱食度 = false
if settings["auto_fight_自动饱食度"] ~= nil then
    auto_fight_自动饱食度 = settings["auto_fight_自动饱食度"]
end

auto_fight_自动打怪 = false
auto_fight_自动打怪 = settings["auto_fight_自动打怪"]

-- 控制主循环
if auto_fight_自动打怪 then
    log("⚔️ 自动打怪开始!")
    -- 执行打怪逻辑
else
    log("⚔️ 自动打怪已关闭")
end

-- ⚠️ 重要:包含中文的 key 必须使用方括号语法 settings["key"]
-- 不能用 settings.key 点号语法

扁平化设置字段

当设置字段包含 fold 和 tab_container 嵌套结构时,使用 flatten_settings_fields() 函数提取所有字段的默认值。该函数为递归展开,支持任意层嵌套(例如 fold 里面再套 fold / tab_container):

Lua - 扁平化与默认值
function flatten_settings_fields(fields)
    local result = {}
    if type(fields) ~= "table" then return result end
    for _, field in ipairs(fields) do
        if type(field) == "table" then
            local ftype = field.type
            if ftype == "tab_container" then
                -- 递归展开所有 tab 中的字段
                for _, tab in ipairs(field.tabs or {}) do
                    for _, f in ipairs(flatten_settings_fields(tab.fields or {})) do
                        table.insert(result, f)
                    end
                end
            elseif ftype == "fold" or ftype == "hbox" or ftype == "row" or ftype == "vbox" or ftype == "group" then
                -- 容器:fold 里面可以再套 fold / tab_container,递归往下展开
                for _, f in ipairs(flatten_settings_fields(field.fields or field.children or {})) do
                    table.insert(result, f)
                end
            else
                table.insert(result, field)
            end
        end
    end
    return result
end

-- 获取所有默认设置
function get_default_settings()
    local settings = {}
    local flat = flatten_settings_fields(SCRIPT_SETTINGS_FIELDS)
    for _, field in ipairs(flat) do
        -- 跳过 divider / comment 等没有 key 的装饰性字段
        if type(field) == "table" and field.key ~= nil then
            settings[field.key] = field.default
        end
    end
    return settings
end

🖱️ 触控方法 20个API

⚠️ 触控不生效先看这里:tap / swipe / swipeHuman / longTap / touchDown 三件套 都要依赖下面任意一条通道,两条通道都没有时会返回 false 并在运行日志写明原因(不会再「假装成功」):
· 无障碍服务(推荐,无需 root):系统设置 → 无障碍 → 已安装的服务 → 开启「古龙精灵」,支持点击 / 长按 / 滑动 / 曲线滑动
· root 权限(最万能):支持多点触控与真正的「按住拖动」(touchDown → touchMove → touchUp)、按键注入;持久 su 通道只弹一次授权
· 返回值就是真实结果:true = 手势被系统确认完成(root 通道为 su 确认执行);false = 通道不可用 / 手势被系统取消 / 超时 / 派发失败 —— 每条 false 都会在日志和浮窗里写明原因(详见「概览与快速入门 → 运行环境与权限准备」)
tap点击
点击坐标为x,y的点(推荐使用)。点击手势会等系统确认完成后才返回;无障碍服务与 root 权限都没有时返回 false(不再静默失败)。
📥 参数:
x (number): 当前屏幕横坐标 y (number): 当前屏幕纵坐标
📤 返回值:boolean: true = 手势被系统确认完成(root 通道为 su 确认执行);false = 无障碍与 root 都不可用 / 手势被系统取消 / 超时 / 派发失败 —— 原因见运行日志
示例
-- 点击坐标
tap(100, 100)

-- 配合找图使用
local result = find_image("images/button.png")
if result and result.success then
    tap(result.x, result.y)
end
longTap长点击
长点击坐标为x,y的点(按住约500ms后抬起)
📥 参数:
x (number): 当前屏幕横坐标 y (number): 当前屏幕纵坐标
📤 返回值:
boolean: true = 长按完成(root 通道为 su 确认,无障碍为手势确认完成);false = 无障碍与 root 都不可用 / 手势被取消 / 超时(原因见运行日志)
示例
longTap(100, 100)
tapRandom随机点击
模拟真人随机点击(防检测)。方式1 矩形范围:tapRandom(x, y, x1, y1);方式2 半径范围:tapRandom(x, y, r)。随机坐标复用 tap() 逻辑执行点击(自动分辨率适配)
📥 参数:
矩形范围: x, y, x1, y1 (number) / 半径范围: x, y, r (number)
📤 返回值:
boolean: true = 复用的 tap() 真正完成;false = 两条通道都不可用 / 手势被取消 / 超时(参数不足 2 个时会退化为按给定坐标点击)
示例
-- 方式1:矩形范围内随机点击
tapRandom(100, 200, 300, 400)

-- 方式2:圆形半径内随机点击
tapRandom(500, 800, 50)
swipe划动
模拟从一点滑动到另外一点(所有模式可用,推荐用于滑动操作)。手势会等真正走完再返回(默认耗时 300ms),脚本里两个滑动之间仍建议留一点间隔,避免后一个手势把前一个打断。
📥 参数:
x1 (number): 起点x / y1 (number): 起点y / x2 (number): 终点x / y2 (number): 终点y / time (number, 可选): 耗时(毫秒),默认300
📤 返回值:
boolean: true = 手势被系统确认完成(root 通道为 su 确认执行);false = 无障碍与 root 都不可用 / 被取消 / 超时 —— 不会悄悄退化成别的动作,原因见运行日志
示例
-- 从左滑到右
swipe(200, 200, 800, 200, 500)

-- 向上滑动翻页
swipe(540, 800, 540, 200, 300)
swipeHuman拟人滑动
防封更强的滑动:轨迹是三次贝塞尔曲线(不是直线),控制点沿直线两侧随机偏移,起手慢→中间快→收尾慢,并叠加轻微手指微抖,每次滑动都不一样;调用后会把这次真实走过的路径点返回给脚本。两种写法(对齐 xdvip.滑动_模拟真人SSF):① 经典 swipeHuman(x1, y1, x2, y2 [, time [, curve]]);② 间隔 swipeHuman(x1, y1, x2, y2, interval [, wave]) —— 第 5 个参数在 0~20 之间按「采样间隔(像素)」处理,大于 20 按「耗时(毫秒)」处理。通道:无障碍服务(整条曲线一次派发) > root(按采样点逐点 sendevent,完整还原缓动与微抖);两者都不可用时返回 false(不会悄悄退化成直线滑动)
📥 参数:
x1, y1 (number): 起点 / x2, y2 (number): 终点 / arg5 (number, 可选): 0~20 = 采样间隔(像素,推荐 0.1,越小点越密、越平滑也越慢);> 20 = 总耗时(毫秒),不传时按距离估算(约 0.8ms/像素)并随机 ±20% / arg6 (number, 可选): 经典写法 = 弯曲强度 0~1,默认随机 0.10~0.30,0 = 接近直线(仍有微抖);间隔写法 = 波动 0~5(默认 1,越大弧线越弯、微抖越明显);经典写法里 arg6 > 1 也按「波动」处理
📤 返回值:
table | false: 成功时返回真实走过的路径表(path.n 或 #path = 点数,path[i] = {x=, y=, t=},兼容 path[i][1] / path[i][2];另有 duration 总时长ms、interval 实际间隔像素(经典写法为 -1)、wave 实际波动(未指定为 -1)、curve 实际曲率、deviation 最大偏离px、finger、channel 实际通道);无障碍与 root 通道都不可用时返回 false
示例
-- ① 经典写法:曲线、时长、微抖全部自动随机
swipeHuman(200, 200, 800, 200)

-- ② 经典写法:指定耗时 500ms + 弯曲强度 0.3(弯得更明显)
swipeHuman(540, 800, 540, 200, 500, 0.3)

-- ③ 间隔写法(对齐 xdvip.滑动_模拟真人SSF):采样间隔 0.1px + 波动 1,并拿到路径点
local path = swipeHuman(300, 1200, 700, 600, 0.1, 1)
if path then
    log("走了 " .. path.n .. " 个点 / " .. path.duration .. "ms")
    local p = path[1]   -- {x=, y=, t=},也可以写 p[1] / p[2]
end

-- ④ 循环滑动时配合随机等待,更像真人操作
for i = 1, 3 do
    swipeHuman(540, 800, 540, 200)
    random_sleep(300, 900)
end

-- ⑤ 在误触风险大的界面可传 curve=0(接近直线,但仍有微抖)
swipeHuman(300, 900, 300, 300, 400, 0)
touchDown / touchMove / touchUp手指按下/移动/弹起
底层多点触控三件套,用于自定义手势。常规点击 / 滑动请直接用 tap / swipe / swipeHuman;需要「按住拖动」优先用 touchMoveEx。三种通道的语义不同:
① 无障碍服务:touchDown 只「登记起点」、touchMove / touchMoveEx 只「登记目标点」,真正的触点动作在 touchUp 时按记录一次性派发(有登记移动 = 从起点按 time 滑到终点;没有 = 原地点击 50ms);
② root 权限:touchDown / touchMove / touchUp 真实按下 / 移动 / 弹起手指,可多指、可长按,此时 touchMove 之间的 sleep 才会产生实际位移;
③ 都没有:touchMove / touchUp 返回 false 并在日志写明原因(不会假装成功)。
⚠️ 必须按 fingerId 配对调用:touchDown → touchMove / touchMoveEx(可多次)→ touchUp;漏掉 touchUp 会残留按下状态(脚本结束 / 停止时会自动清理)。返回值只有 touchUp 代表本次手势的最终结果。
📥 参数:
fingerId (number): 手指索引 0-4(touchDown 可省略,默认 0;touchMove/touchMoveEx/touchUp 必填)/ x, y (number): 坐标(脚本设计分辨率坐标,自动适配实际屏幕)
📤 返回值:
touchDown: 已登记 / 已按下(无障碍模式下不代表触点真的产生);touchMove: 无障碍 = 登记成功,root = 移动事件已写出,false = 该手指没按下且 root 不可用;touchUp: true = 系统确认手势完成,false = 没配对 touchDown / 被取消 / 通道不可用(原因见运行日志)
示例
-- ① 推荐:按住拖动用 touchMoveEx(无障碍 / root 都支持),一行顶替几百次 touchMove
touchDown(1, 100, 100)
sleep(50)
touchMoveEx(1, 300, 300, 800)   -- 第4个参数 = 这段滑动的耗时 800ms
touchUp(1)                     -- 无障碍:真正的滑动在这一步发出,它的返回值才是本次结果

-- ② 只在 root 通道下才需要逐步移动(每步 sleep 才有意义)
touchDown(1, 297, 327)
for i = 1, 20 do
    touchMove(1, 297, 327 - i * 15)
    sleep(16)   -- 无障碍模式下 touchMove 只是登记目标点,这些 sleep 不会产生中间触点
end
touchUp(1)

-- ③ 常规滑动更推荐 swipeHuman(自动选通道 + 拟人曲线,一行搞定)
swipeHuman(540, 800, 540, 200)
touchMoveEx模拟滑动增强版
带时间控制的滑动(增强版)
time = 本次滑动耗时(毫秒):无障碍模式在 touchUp 时按该时长发出滑动手势(需先 touchDown 再 touchUp);root 模式按约 16ms 一步插值移动(真正的按住拖动);两者都没有则返回 false 并提示开启无障碍或授权 root
⚠️ time ≤ 0 会按 300ms 处理;没有先 touchDown 时起点会退化成目标点(位移为 0,日志里会给出警告)
📥 参数:
id (number): 手指索引 / x,y (number): 目标坐标 / time (number): 滑动耗时(毫秒)
📤 返回值:
boolean: 无障碍模式 = 目标点与时长已登记(真正的结果由随后的 touchUp 给出);root 模式 = 移动事件已写出;false = 该手指没按下且 root 也不可用(本次移动未发生,原因见运行日志)
示例
-- 从 (100,100) 按住后平滑滑到 (300,300),耗时 800 毫秒
touchDown(1, 100, 100)
sleep(50)
touchMoveEx(1, 300, 300, 800) -- 第4个参数 time=800 就是这段滑动的耗时
touchUp(1) -- 无障碍模式下真正的滑动在 touchUp 时按 time 发出
keyPress / keyDown / keyUp按键
模拟物理按键操作(home/back/app_switch/power/del/enter 等)。keyPress 按一下(按下+弹起一次)。默认 auto:DEL / FORWARD_DEL 优先走古龙输入法 InputConnection.deleteSurroundingText(WebView DOM 输入框最稳),再依次尝试无障碍 ACTION_SET_TEXT → root input keyevent → 应用身份 input;其它按键 root 同步 input keyevent → 古龙输入法 → 应用身份 input。可选第 2 个参数强制通道(auto/ime/root/app/a11y),如 keyPress("del", "root") 或 keyPress("del", {channel="root"})。
keyDown / keyUp:只有古龙输入法能真正只发 DOWN / UP;root / app 只能发完整按键(keyDown 会打日志说明,keyUp 返回 false,不再假成功)。
📥 参数:
keycode (string/number): 按键名(如 "home"、"back"、"del" 退格、"forward_del" 删除、"enter"、"app_switch"…;退格键叫 del,不叫 backspace)或数字按键码(如 67 = KEYCODE_DEL)
channel (string/table, 可选): auto(默认)/ ime / root / app / a11y(a11y 仅对 del/forward_del 有效);也可写 {channel="root"}
📤 返回值:
boolean: keyPress 返回是否真的把事件送出(按键名无法识别或所有通道全失败时为 false);keyDown 在 root/app 通道下发完整按键后返回 true;keyUp 只有古龙输入法能独立弹起,否则返回 false
示例
-- 按HOME键
keyPress("home")
-- 按返回键
keyPress("back")

-- 删除键:del = 退格(删光标前一个字符);forward_del = Delete(删光标后一个字符)
keyPress("del")
keyPress("forward_del")

-- 强制走 root 通道(游戏只认硬件删除键时)
keyPress("del", "root")
keyPress("del", {channel = "root"})

-- 账号/密码框建议直接按文本删除,不依赖硬件 DEL
imeLib.lock()
imeLib.deleteChar()
imeLib.clearText()

-- 按下并弹起(只有古龙输入法能真正只发 DOWN/UP)
keyDown("home")
sleep(100)
keyUp("home")
inputText模拟输入文字
模拟输入文字(安卓端古龙精灵/古龙代码箱均支持)。底层执行系统 input text 命令,适合普通系统输入框:先点击输入框聚焦并等待输入法弹出再调用。仅适合英文/数字/常见符号;中文、空格、换行请改用 imeLib.setText。
📥 参数:
text (string): 要输入的文本内容
📤 返回值:
boolean: true 表示命令执行成功,false 表示失败(自绘UI输入框可能收不到文字)
示例
-- 常规输入(聊天框 / 账号 / 密码框)
tap(300, 800)  -- 先点击输入框唤起输入法
sleep(800)
inputText("hello123")

-- 中文/空格请用输入法通道
imeLib.setText("你好 world")
imeLib.setText / imeLib.lock / imeLib.unlock / imeLib.deleteChar / imeLib.clearText / imeLib.finishInput / imeLib.keyEvent输入法系列
输入法输入系列(推荐首选)。imeLib.lock() 锁定 App 内置「古龙输入法」后,setText 直接 commitText 到当前输入框,不弹系统键盘且支持中文(root 自动切换;无 root 需先在系统设置启用一次古龙输入法);App 重启后若古龙仍是当前输入法也会自动复用。未锁定时按优先级尝试:无障碍 ACTION_SET_TEXT → root input text → ADB_INPUT_TEXT 广播 → 应用身份 input text;前面通道成功时不会再重复发送。deleteChar 优先 deleteSurroundingText 删一个字符;clearText 一次性清空整个输入框;finishInput 回车确认;keyEvent 发送单个 DOWN/UP(只有古龙输入法能独立按下/弹起)。
示例
-- 推荐:锁定古龙输入法(不弹键盘,支持中文)
setStopCallBack(function(error)
    imeLib.unlock()
end)
imeLib.lock()
imeLib.setText("公会副本马上开始")
imeLib.finishInput()   -- 回车/发送
imeLib.unlock()        -- 手动解锁(脚本停止时也会自动解锁)

-- 账号 + 密码登录框
tap(acc_x, acc_y)
imeLib.setText("player123")
tap(pwd_x, pwd_y)
imeLib.setText("p@ss w0rd")  -- 含空格/符号也可输入

-- 删除一个字符 / 清空整个输入框
imeLib.deleteChar()
imeLib.clearText()      -- 一次清空,适合输错重输
setStopCallBack设置停止回调
注册脚本停止回调:脚本停止时自动调用(手动点「停止」、脚本自己跑完 main()、脚本报错退出都会触发,且只触发一次),常用于收尾清理:解锁古龙输入法 imeLib.unlock()、关闭驻留消息窗 close_message_keep()、恢复状态栏 execRoot() 等。回调参数为错误信息,正常停止时为空字符串。
示例
-- 推荐:把脚本的收尾清理都写进停止回调,手动停止 / 脚本跑完 / 报错退出都会执行
setStopCallBack(function(error)
    imeLib.unlock()                                        -- 解锁古龙输入法,恢复系统输入法
    close_message_keep()                                   -- 关闭 show_message_keep 驻留消息窗
    execRoot("settings put global policy_control null")    -- 恢复状态栏显示
end)

-- 最简用法:只做一件事
setStopCallBack(function(error)
    imeLib.unlock()
end)
setOnTouchListener获取用户触摸屏幕坐标
设置触摸监听,获取用户在屏幕上的触摸坐标(仅在 root / 激活模式下有效,无障碍模式与调试模式不支持)。古龙代码箱的调试模式下会直接返回 false。
示例
function onTouchEvent(x, y)
    log("触摸坐标: " .. x .. ", " .. y)
end
setOnTouchListener(onTouchEvent)
sleep(1000000)

🔍 图像识别 13个API

screenshot截图
截取当前设备屏幕:可保存到文件,同时返回截图数据(可直接当作 find_image 的模板)
📥 参数:
path (string, 可选): 保存路径。相对路径按当前项目目录 / scripts 目录解析;设备端还支持 /sdcard 等绝对路径(写不进去会自动走 root 兜底)。为空则只返回内存数据
📤 返回值:截图数据 —— 设备端 = PNG 字节(二进制安全,可直接喂 find_image);PC 本地 = 图像对象(同样可直接喂 find_image)
示例
-- 1) 截图到内存(用于找图 / 切图)
local img = screenshot()
local crop = image_crop(img, 100, 200, 300, 400)

-- 2) 截图并保存到文件(设备端与 PC 本地都支持)
screenshot("output/screen.png")

-- 3) 截图后直接找图(模板用路径方式)
if find_image("images/target.png") then
    log("找到目标")
end
find_image找图
在屏幕截图中查找模板图片的位置(支持区域内查找 / 相似度 / 偏色)
📥 参数:
template (string): 模板,三种形式 —— ① 图片路径 "images/button.png"(相对项目 / scripts 目录)或绝对路径 ② "__base64__:iVBORw0..." 数据(抓取窗口可直接生成) ③ 图片字节(screenshot() / image_crop() 的返回值)
x1, y1, x2, y2 (number, 可选): 搜索区域(脚本坐标),省略或 0 = 全屏
threshold (number, 可选): 相似度 0.0~1.0,默认 0.9(模板中「颜色达标像素」占比达到该值才算命中)
tol (number, 可选): 偏色,每通道允许的颜色偏差 0~255,默认 10(0 = 要求逐像素完全一致)
📤 返回值:table - {success, x, y, center_x, center_y, confidence, width, height}(同时保留 [1]..[5] 位置值兼容老脚本);未找到返回 nil
示例
-- 1) 路径方式(图片放在项目 images/ 下,会随项目下发到设备)
local result = find_image("images/button.png")
if result and result.success then
    tap(result.x, result.y)
    log(string.format("相似度: %.2f", result.confidence))
end

-- 2) __base64__ 内嵌数据(抓取窗口可直接生成,不依赖设备上有图片文件)
local TPL = "__base64__:iVBORw0KGgoAAAANSUhEUg..."
local r = find_image(TPL, 0, 0, 1080, 1920, 0.9, 10)
if r and r.success then tap(r.center_x, r.center_y) end

-- 3) 只在小区域内找 + 放宽相似度 / 偏色(找得慢就缩小区域,误匹配多就调高相似度)
local r2 = find_image("images/icon.png", 100, 200, 500, 800, 0.8, 20)

-- 4) 兼容写法:旧签名(模板, 阈值)仍然可用
local r3 = find_image("images/target.png", 0.8)
wait_image等待图片
等待指定图片在屏幕上出现(带超时机制;支持区域 / 相似度 / 偏色,模板形式同 find_image)
📥 参数:
template (string): 模板(路径 / __base64__ 数据 / 图片字节)
timeout (number, 可选): 超时时间(秒),默认 10
threshold (number, 可选): 相似度,默认 0.9
x1, y1, x2, y2 (number, 可选): 搜索区域(脚本坐标)
tol (number, 可选): 偏色(每通道容差),默认 10
📤 返回值:table - 同 find_image;超时返回 nil
示例
-- 等待图片出现(最多等10秒)
local result = wait_image("images/loading_done.png")
if result then
    tap(result.x, result.y)
end

-- 等 30 秒 + 限定区域 + 相似度 0.85
local r = wait_image("images/login_success.png", 30, 0.85, 0, 0, 1080, 600)
if not r then
    log("登录超时!")
end
exists判断存在
判断指定图片是否在屏幕上存在(支持区域 / 相似度 / 偏色)
📥 参数:
template (string): 模板(路径 / __base64__ 数据 / 图片字节)
threshold (number, 可选): 相似度,默认 0.9
x1, y1, x2, y2 (number, 可选): 搜索区域(脚本坐标)
tol (number, 可选): 偏色(每通道容差),默认 10
📤 返回值:boolean - 图片是否存在
示例
if exists("images/button.png") then
    log("按钮存在")
    tap(100, 200)
end

-- 限定区域 + 放宽偏色(适合图标在不同机器上有色差的场景)
if exists("images/success.png", 0.85, 0, 0, 1080, 600, 20) then
    log("成功!")
end
base64_decodebase64解码
把 base64 文本解码成图片数据(用于把模板图片内嵌到脚本里,再交给 find_image 使用)
📥 参数:
text (string): base64 文本,可带 __base64__ / __base64__: 前缀,也允许包含换行(方便分块拼接)
📤 返回值:二进制数据(设备端 = 二进制字符串,PC 本地 = 对象包装),可直接传给 find_image / exists;失败返回 nil
示例
-- 把抓取窗口生成的 __base64__ 数据存进脚本,运行时解码成图片数据
local TPL = table.concat({
    "__base64__:iVBORw0KGgoAAAANSUhEUg...",
    "...",
})
local img = base64_decode(TPL)
local r = find_image(img, 0, 0, 1080, 1920, 0.9, 10)
if r and r.success then tap(r[1], r[2]) end

-- 说明:直接把 "__base64__..." 字符串交给 find_image 也可以,效果一样
get_color获取颜色
获取屏幕指定坐标点的颜色值(返回16进制颜色字符串)
📤 返回值:string - 如 "ff0000"(红色)
示例
-- 获取坐标颜色并解析RGB
local color = get_color(500, 300)
local r, g, b = tonumber("0x"..color:sub(1,2)), tonumber("0x"..color:sub(3,4)), tonumber("0x"..color:sub(5,6))
log(r..","..g..","..b)

-- 判断颜色
if r > 200 then log("该点偏红") end
find_color找色
在屏幕上查找所有匹配指定颜色的像素点
示例
-- 查找所有红色像素
local points = find_color({255, 0, 0}, 20)
log("找到 " .. #points .. " 个红色像素点")
findMultiColor多点取色
在屏幕上查找与指定多点颜色匹配的位置(最常用的取色函数)
📥 参数:
x1,y1,x2,y2 (number): 搜索区域 / base_color (string): 基准点颜色 "RRGGBB",可带偏色 "RRGGBB-偏色"(偏色 = 每通道容差,6位hex,如 "ff0000-0a0a0a";省略则默认 ±10,带偏色后不参与分辨率容差适配) / offset_str (string): 偏移点 "dx|dy|RRGGBB",每个颜色点均可带 "-偏色"(如 "10|0|00ff00-0a0a0a") / direction (number, 可选): 方向0-4 / sim_threshold (number, 可选): 相似度0.0~1.0
示例
local result = findMultiColor(0, 0, 1080, 1920, "ff0000", "10|0|00ff00|20|0|0000ff", 0, 0.9)
if result then
    tap(result[1], result[2])
end
findMultiColorTargets多目标多点取色
一次截图查找多个多点配色,找到任意一个即返回(相比逐个调用 findMultiColor 快约3倍)
📥 参数:
x1,y1,x2,y2 (number): 搜索区域 / MultiColorTargets (table): {{base_color, offset_str}, ...}(颜色均可带 "-偏色") / direction (number, 可选): 方向0-4 / sim_threshold (number, 可选): 相似度0.0~1.0
📤 返回值:
table: 第一个匹配的 {x, y} 坐标,全不匹配返回 nil
示例
local MultiColorTargets = {
    {"845635", "-9|8|d7c189|-77|-3|7a4829"},
    {"efe3d3", "36|-18|e3c288|-10|6|efe1d3"},
}
local result = findMultiColorTargets(0, 0, 1080, 1920, MultiColorTargets, 0, 0.9)
if result then tap(result[1], result[2]) end
findMultiColorStr带文字标识的多点取色
一次截图查找多个多点配色,每个配色带文字标识;找到任意一个即返回(适合多状态识别,标识方便调试)
📥 参数:
x1,y1,x2,y2 (number): 搜索区域 / MultiColorStr (table): {{"文字", base_color, offset_str}, ...}(颜色均可带 "-偏色") / direction (number, 可选): 方向0-4 / sim_threshold (number, 可选): 相似度0.0~1.0
📤 返回值:
table: 第一个匹配的 {x, y} 坐标,全不匹配返回 nil
示例
local MultiColorStr = {
    {"系统", "e7c548", "2|15|f2a91a|-25|-3|dac04f"},
    {"返回", "ff0000", "10|0|00ff00|20|0|0000ff"},
}
local result = findMultiColorStr(0, 0, 1080, 1920, MultiColorStr, 0, 0.9)
if result then tap(result[1], result[2]) end
getMultiColorStr获取多点标记
识别范围内所有匹配的多点标记,返回所有匹配到的文字标识(用 | 分隔,同名只返回一次),适合判断界面存在哪些元素
📥 参数:
x1,y1,x2,y2 (number): 搜索区域 / MultiColorStr (table): {{"文字", base_color, offset_str}, ...}(颜色均可带 "-偏色") / direction (number, 可选): 方向0-4 / sim_threshold (number, 可选): 相似度0.0~1.0
📤 返回值:
table: {success=true/false, text="标记1|标记2|..."}
示例
local MultiColorStr = {
    {"天兵", "e7c548", "2|15|f2a91a|-25|-3|dac04f"},
    {"亢金龙", "efe3d3", "36|-18|e3c288|-10|6|efe1d3"},
}
local r = getMultiColorStr(0, 0, 1080, 1920, MultiColorStr, 0, 0.9)
if r and r.success then log("当前界面元素: " .. r.text) end
cmpColorEx / findMultiColorAll / findColor更多图色函数
cmpColorEx: 比较多点颜色(返回匹配索引)/ findMultiColorAll: 查找所有匹配位置 / findColor: 查找任意匹配颜色(颜色参数均可带 "-偏色",如 "ff0000-0a0a0a")
示例
-- 比较多点颜色
local ret = cmpColorEx("ff0000", "10|0|00ff00|20|0|0000ff", 0, 0.9)
if ret ~= -1 then log("颜色匹配成功") end

-- 查找所有匹配位置
local results = findMultiColorAll(0, 0, 1080, 1920, "ff0000", "10|0|00ff00|20|0|0000ff")
for i, pos in ipairs(results) do
    tap(pos[1], pos[2])
end
getColorNum获取区域颜色数量
统计指定区域内与目标颜色匹配的像素点个数,适合判断按钮/文字的“亮/灰”“多/少”等状态
📥 参数:
x1,y1,x2,y2 (number): 搜索区域 / color (string): 颜色 "RRGGBB",多个用 "|" 分隔,支持偏色 "RRGGBB-偏色" / sim (number): 相似度 0~1,默认0.9
📤 返回值:
number: 区域内匹配的像素点数量
示例
local number = getColorNum(10, 50, 600, 600, "e91e1e", 0.8)
print(number)

-- 判断按钮状态:区域内红色像素多 → 按钮点亮
local redNum = getColorNum(100, 200, 300, 250, "e91e1e", 0.9)
if redNum > 200 then
    tap(200, 225)
end

-- 多颜色 + 偏色
local num = getColorNum(0, 0, 500, 300, "ff0000|00ff00-202020", 0.85)

📝 OCR文字识别 4个API

使用本地ONNX模型进行文字识别,无需联网。

这 4 个 *_local API 在设备端提供:「📦 代码箱调试」与安装在手机/模拟器上的「古龙精灵」「古龙代码箱」直接可用。 另有一组名字相近的 ocr_recognize / ocr_find_text / ocr_wait_text / ocr_recognize_detail,参数、返回值和 region 含义都不同,不要混用。
ocr_recognize_localOCR本地文字识别
识别指定区域的文字
📥 参数:
lang (string): 'ch'(中英文)/ region (table): {x1, y1, x2, y2}
📤 返回值:table - {success, text, error}
示例
local text = ocr_recognize_local("ch", { 987, 332, 1121, 368 })
if text.success then
    log("识别结果: " .. text.text)
    if text.text == "开始挑战" then
        tap(1050, 350)
    end
end
ocr_find_text_localOCR本地查找文字
在指定区域查找目标文字并返回位置(推荐用于定位后点击)
📤 返回值:table - {success, found, x, y, w, h, text, error}
未找到时 {found=false, text=""};出错时(截屏失败/引擎未初始化/推理异常){success=false, error="原因"};text 始终存在,可安全做字符串拼接
示例
local pos = ocr_find_text_local("开始", nil, {987, 332, 1121, 368})
if pos and pos.found then
    -- 点击文字中心
    tap(pos.x + pos.w / 2, pos.y + pos.h / 2)
end
ocr_recognize_detail_localOCR本地文字识别详情
识别区域中所有文字块,返回每个文字块的位置和内容(安卓端 API)
📥 参数:
lang (string): 'ch'(中英文)/ region (table): {x1, y1, x2, y2}
📤 返回值:table - {success, error, results = { {text, x, y, w, h, confidence}, ... }}
⚠️ 没有 count / text 字段:数量用 #detail.results,全部文字要自己拼接;写 detail.count 会报 attempt to concatenate a nil value
⚠️ 失败时 {success=false, error="原因", results={}}(截屏失败 / OCR引擎未初始化 / 推理异常),先看 error 再排查
⚠️ x/y 已是脚本设计分辨率坐标,可直接 tap;「古龙精灵」PC 调试引擎未注册本函数(PC 用 ocr_recognize_detail,返回文字块数组、字段为 width/height)
示例
local detail = ocr_recognize_detail_local("ch", {800, 300, 1200, 500})
if type(detail) == "table" and detail.success then
    -- 没有 detail.count / detail.text:数量用 #detail.results,文字自己拼
    log("识别到 " .. #(detail.results or {}) .. " 个文字块")
    for i, item in ipairs(detail.results or {}) do
        if string.find(tostring(item.text or ""), "开始挑战", 1, true) then
            -- 中心点取整:小数进 %d 会报 number has no integer representation
            tap(math.floor(item.x + item.w / 2), math.floor(item.y + item.h / 2))
            break
        end
    end
else
    log("识别失败: " .. tostring(detail and detail.error or detail))
end
ocr_find_text_all_localOCR本地查找所有匹配文字
查找所有匹配目标文字的位置,适合多个相同文字的场景
示例
local result = ocr_find_text_all_local("确认", nil, {100, 200, 600, 400})
if result.found then
    -- 从下往上点击所有匹配项
    for i = result.count, 1, -1 do
        local item = result.results[i]
        tap(item.x + item.w / 2, item.y + item.h / 2)
        sleep(500)
    end
end

🤖 YOLO目标检测 6个API

yolo_detectYOLO目标检测
YOLO v11目标检测 - 检测屏幕上的物体
📥 参数:
conf_threshold (number, 可选): 置信度阈值,默认0.25 / iou_threshold (number, 可选): IoU阈值默认0.45
示例
local result = yolo_detect()
if result.success then
    log("检测到 " .. result.count .. " 个物体")
    for i, det in ipairs(result.detections) do
        log(string.format("%s: %.2f, (%d,%d)", det.class_name, det.confidence, det.center_x, det.center_y))
    end
    -- 点击第一个检测到的物体
    if result.count > 0 then
        tap(result.detections[1].center_x, result.detections[1].center_y)
    end
end
yolo_load_custom_model加载自定义YOLO模型
加载自定义训练的NCNN格式YOLO模型(.param + .bin)
📌 设备端未提供本 API(调用会报 attempt to call a nil value),请改用 yolo_load_model + yolo_detect。
示例
-- 加载自定义模型
local result = yolo_load_custom_model("resources/models/yolov11_npc.param")
if result.success then
    log("类别: " .. table.concat(result.class_names, ", "))
    -- 执行检测
    local det = yolo_detect(0.35)
end
yolo_detect_file / yolo_detect_and_draw / yolo_load_model / yolo_get_available_models
yolo_detect_file: 从图片文件检测 / yolo_detect_and_draw: 检测并绘制边界框 / yolo_load_model: 切换内置模型 / yolo_get_available_models: 获取模型列表
📌 运行时支持:yolo_detect_and_draw、yolo_load_model 设备端可用;yolo_detect_file、yolo_get_available_models 设备端未提供(调用会报 attempt to call a nil value)。
示例
-- 从文件检测
local result = yolo_detect_file("screenshot.png")

-- 检测并绘制
local result = yolo_detect_and_draw()

-- 加载内置模型
yolo_load_model("yolo11n")  -- Nano最快
yolo_load_model("yolo11s")  -- Small

-- 查看可用模型
local models = yolo_get_available_models()
for i, m in ipairs(models) do
    log(m.name .. ": " .. (m.exists and "✅" or "⬇️"))
end

🔧 工具函数 57个API

sleep等待
等待指定毫秒数
示例
sleep(2000)   -- 等待2秒
sleep(500)    -- 等待0.5秒
random_sleep随机等待
随机等待一段时间(模拟人类操作,避免被检测)
示例
random_sleep(1000, 3000)   -- 随机等待1~3秒
tap(500, 300)
random_sleep(300, 800)   -- 模拟人类点击间隔
tap(500, 400)
log输出日志
输出日志信息到控制台
示例
log("脚本开始执行")
log(string.format("坐标: (%d, %d)", x, y))
user_settings / get_setting / set_setting / set_settings用户设置
📌 运行时支持:user_settings / get_setting 在「📦 代码箱调试」与已安装的「古龙精灵」「古龙代码箱」里均已注入(只读、需用户账号 + 用户 token,未登录时 get_setting 返回默认值、user_settings.xxx 为 nil,内部 10 秒缓存;⚠️ user_settings 不支持 pairs() / json_stringify(),需要整表请用 server_api.get_user_settings())。set_setting / set_settings 返回 boolean:服务器支持局部合并时只发改动的键(1 个请求),老服务器自动回退成「读现有 values → 合并 → 整体回写」,不丢配置。账号口径 = 脚本用户:取当前项目登录账号;脚本还没登录(没有用户 token)时返回 false。
获取/设置用户在控制面板中配置的参数;未保存的实时控件值请用 get_live_setting(详见上方"脚本参数获取"章节)
示例
-- 全局变量
if user_settings.auto_fight then log("自动打怪已开启") end

-- 安全获取
local val = get_setting("key", defaultValue)

-- 动态修改(返回 boolean;table 等复杂值会存成 JSON 字符串)
local ok = set_setting("key", newValue)
get_live_setting读取未保存的实时设置
读取用户面板「脚本设置」卡片当前控件上的值(含未保存的输入 / 选择)。仅在由用户面板按钮触发的脚本函数中可用:面板点击按钮时把控件当前值随动作提交到 params.live_settings,平台派发函数前保存为快照。
查找顺序:最近一次按钮点击的实时快照 → 已保存的 get_setting 值 → default。⚠️ 只在点击按钮时更新,不是持续同步;不会自动保存。按钮函数也可以直接读 params.live_settings[key](兼容旧 APK)。
示例
-- 在面板按钮触发的全局函数内使用(params 由平台传入)
local boss_no = get_live_setting("boss_no", 1)
local auto_fight = get_live_setting("auto_fight", false)
local target = get_live_setting("target_monster", "银月贤者")

-- 兼容旧 APK:直接读动作参数里的快照
local live = (params and params.live_settings) or {}
local boss_no_old = live.boss_no or get_setting("boss_no", 1)
poll_user_actions面板按钮动作(可选)
面板「按钮控件」的动作,现在由平台自动派发(Java 后台拉取 + 在 sleep/API 让出点派发到脚本函数),脚本侧无需写任何轮询代码。本函数仅用于"想自己决定执行时机"的进阶场景:从同一队列取出动作(取出即消费,不会与平台钩子重复执行)。
示例
-- 1) 面板按钮声明(settings_fields.lua)
-- 【片段】第 1 步的按钮声明属于 settings_fields.lua(不能单独运行/检查)
{key = "btn_open_log", label = "查看日志", type = "button",
 func = "判断_打开文件", style = "primary", confirm = "确定查看?"}

-- 【可运行代码】下面这段可以直接运行/语法检查
-- 2) 脚本只写业务函数即可(平台会自动派发,不需要轮询)
function 判断_打开文件(params)
    show_message("已执行一键日常")
end

-- 3)【可选】自己掌控执行时机时才需要下面这段
for _, act in ipairs(poll_user_actions() or {}) do
    local f = _G[act.func]
    if type(f) == "function" then pcall(f, act.params) end
end
http_get / http_postHTTP请求
发送HTTP GET/POST请求(支持JSON和表单数据)
示例
local resp = http_get("http://api.example.com/data")
local data = json_parse(resp)

local resp = http_post("http://api.example.com/login", nil, {username = "admin", password = "123456"})
open_url打开网址
在默认浏览器中打开URL
示例
open_url("http://www.baidu.com")
write_file写入文件
写入内容到文件(目录不存在时自动创建)。相对路径以「古龙精灵」App 内置 scripts 目录为基准(示例统一使用相对路径):如 write_file("data.txt", ...) 实际写入 App 内置 scripts 目录下的 data.txt;古龙代码箱为当前工程目录、PC 调试为脚本运行目录,写法一致。无写入权限的绝对路径(如 /sdcard/Download)在设备已 root 时会自动通过 su 回退写入
📥 参数:
path (string): 文件路径(推荐相对路径,基于 App 内置 scripts 目录) / content (string): 文件内容
📤 返回值:
boolean: true 表示写入成功,false 表示失败
示例
-- 以古龙精灵为例:相对路径 = App 内置 scripts 目录
local ok = write_file("result.txt", "执行完成")
log("写入结果: " .. tostring(ok))

-- 写入JSON配置
local data = {time = os.time(), status = "ok"}
write_file("data.json", json_stringify(data))

-- ===== 绝对路径示例(雷电模拟器)=====
-- /sdcard 等价于 /storage/emulated/0
local ok2 = write_file("/sdcard/Download/gulong/data.txt", "Hello World")
log(read_file("/storage/emulated/0/Download/gulong/data.txt"))

-- 雷电共享目录(Windows 对应:文档\LDPlayer\Pictures)
write_file("/mnt/shared/Pictures/gulong_test.txt", "Hello LDPlayer")
delete_file删除文件
删除文件或目录(目录会递归删除,含子文件)。路径规则与 read_file_content / write_file_content 完全一致:绝对路径直接删除(App 沙箱无权限时,设备已 root 会自动用 su(rm -rf)回退);相对路径在「古龙精灵」= App 内置 scripts 目录、「古龙代码箱」= 当前工程目录。
⚠️ 返回值是 boolean(true = 删除成功,false = 文件/目录不存在或删除失败):把它拼进字符串必须用 tostring 包一层,否则报 attempt to concatenate string and boolean;另外「古龙代码箱」的 log 只接收第一个参数(多写的参数会被忽略),所以统一「先 tostring 拼成一个字符串再 log」在各种情况下都安全。
📥 参数:
path (string): 文件/目录路径(绝对路径、相对路径均可;相对路径 = App 内置 scripts 目录 / 代码箱当前工程目录)
📤 返回值:
boolean: true 表示删除成功(路径已不存在),false 表示文件/目录不存在或删除失败
示例
-- 相对路径(「古龙精灵」= App 内置 scripts 目录 / 「古龙代码箱」= 当前工程目录)
local del_ok = delete_file("data.txt")
-- delete_file 返回 boolean(true/false),必须 tostring;否则 string .. boolean 会报
-- "attempt to concatenate string and boolean"。也可写成:
--   log("结果: " .. (del_ok and "删除成功" or "删除失败/文件不存在"))
log("结果: " .. tostring(del_ok))

-- 用 if 判断更直观
if delete_file("old_log.txt") then
    log("旧日志已删除")
else
    log("旧日志不存在或删除失败")
end

-- ===== 绝对路径示例(雷电模拟器)=====
-- /sdcard 等价于 /storage/emulated/0;无权限时设备已 root 会自动用 su(rm -rf)回退
local del_abs = delete_file("/sdcard/Download/gulong/data.txt")
log("删除结果: " .. tostring(del_abs))

-- 删除整个目录(含子文件,递归删除)
log("目录删除: " .. tostring(delete_file("/sdcard/Download/gulong/old_logs")))

-- 雷电共享目录(Windows 对应:文档\LDPlayer\Pictures)
delete_file("/mnt/shared/Pictures/gulong_test.txt")
call_python调用Python
调用Python文件中的函数(Lua→Python跨语言调用)
示例
local result = call_python("scripts/my_module.py", "process_data", "input.txt")
get_account_info获取账号信息
获取当前「脚本用户」的信息。取值顺序:① 脚本运行期全局 ACCOUNT_ID / _G_TOKEN / SERVER_URL(脚本 login() 后就有值)→ ② 项目目录下的 account_info.txt(启动脚本时 App 会把当前登录账号自动写进去,首行账号、次行 token)→ ③ 已登录的开发者账号(只能展示,写用户设置会被拒绝)。账号ID / 服务器地址都取不到时返回 nil。⚠️ 与「设备搜索 → 连接用户」里填的账号无关(那个账号只用于「连接用户截图」)。
返回字段:account_id / token / server_url / logged_in。
示例
local account = get_account_info()
if account then
    log("账号: " .. account.account_id .. " @ " .. account.server_url)
else
    log("未登录 / 没有账号上下文")
end
get_device_info获取设备信息
获取当前设备的信息(共 12 个字段,脚本不用按运行环境写分支)。取不到的字段:文字留空字符串、数字留 0;永远返回 table(不返回 nil)。
字段:device_id / device_name / brand / model / manufacturer / os_version / sdk_int / platform(设备端恒为 android)/ resolution / screen_width / screen_height / density。
示例
local info = get_device_info()
log("设备: " .. info.device_name .. " (" .. info.brand .. " " .. info.model .. ")")
log("分辨率: " .. info.resolution .. " 密度: " .. info.density)
log("系统: Android " .. info.os_version .. " (SDK " .. info.sdk_int .. "), 运行环境: " .. info.platform)
report_message回传消息
把脚本运行中的关键信息回传到开发者节点控制中心的「📨 回传消息」标签页(按游戏分类,每个游戏只保留最近 600 条)。
📌 运行环境:PC 本机运行 /「古龙精灵」/「古龙代码箱」均已注入(需要脚本用用户账号登录,开发者 token 会被拒绝)。
📌 限制:单条最多 100 个字符(超出自动截断,按 Unicode 字符计);客户端 2 秒内只允许发送 1 条;服务端再按「每账号每分钟 30 条 / 每开发者每分钟 300 条」限频。
📌 游戏名省略时自动取脚本全局 GAMENAME;匹配不到「游戏管理」里的游戏时归入「其他」。另有 report_message_nowait(...) 在后台线程发送,不阻塞脚本。
📥 参数:
content (string):回传内容(≤100 字符)
game (string, 可选):游戏名,默认取全局 GAMENAME
📤 返回值:
table:{success=true, id, total} 或 {success=false, error}
示例
-- 推荐写法:显式传 GAMENAME(开发者面板按游戏分类)
local r = server_api.report_message("开始执行:第 " .. round .. " 轮", GAMENAME)
if not r.success then
    log("回传失败: " .. tostring(r.error))
end

-- 也可以直接调用全局函数(等价写法)
report_message("任务完成,用时 " .. seconds .. " 秒")

-- 高频循环里用 nowait:后台发送,不阻塞脚本(2 秒内重复调用会被节流)
report_message_nowait("血量 " .. hp .. "/" .. hp_max, GAMENAME)
bug_screenshot脚本Bug截图
截取当前屏幕并发送给开发者,开发者「抓取窗口」会自动收到 Bug截图_脚本_账号_时间.png。
📌 运行环境:「古龙精灵」/「古龙代码箱」已注入;PC 本机运行暂不支持。
📌 与悬浮窗「📷 Bug截图」按钮走完全相同的上传链路:先撤掉悬浮提示再截干净图,PNG 超过 8MB 自动降级 JPEG。
📌 限制:每账号每天最多 60 张(脚本额度与按钮的 20 张独立计数);客户端 2 秒内最多 1 张,服务端也会再限一次;上一张还在处理时立即返回 success=false。另有 bug_screenshot_nowait() 在后台线程截图上传、不阻塞脚本(返回 true=已排队)。
📥 参数:
无参数(token / 账号自动取当前登录上下文,账号与 token 同源校验)
📤 返回值:
table:{success=true, filename, used, limit, remaining, source='script'} 或 {success=false, error, retry_after?}
示例
-- 阻塞版:等截图 + 上传完成,返回本次额度
local r = server_api.bug_screenshot()
if r.success then
    log("截图已发送: " .. tostring(r.filename)
        .. ",今日 " .. tostring(r.used) .. "/" .. tostring(r.limit))
else
    log("截图失败: " .. tostring(r.error))
end

-- 后台版:不阻塞脚本,返回 true=已排队(false=未登录/2 秒节流/上一张未完成)
if bug_screenshot_nowait() then
    log("截图任务已提交")
end
play_sound播放提示音
播放提示音:内置提示音(ToneGenerator,即时、开销小)或本地音频文件(MediaPlayer,支持 mp3 / wav / ogg 等)。同一时间只播一个,新的 play_sound 会打断上一个。
📌 运行环境:「古龙精灵」与「古龙代码箱」均已实现;音量走 STREAM_MUSIC(与 get_volume / set_volume 默认音频流一致),系统媒体音量为 0 时同样听不到。
📌 音频文件路径:「古龙精灵」相对路径 = App 内置 scripts 目录;「古龙代码箱」相对路径 = 当前工程目录;也支持绝对路径与 assets: 前缀(App 内置 assets)。
📥 参数:
name (string|number): 要播放的声音,传 nil 或空串按 beep
 · 数字 = ToneGenerator 音调编号(0~98)
 · 字符串 = 内置提示音名:beep(tick、default)/ beep2(double)/ ok(ack、success、confirm)/ error(nack、fail)/ warn(warning)/ prompt(notice、ding)/ ring(ringtone)/ dial / busy / congestion / waiting(callwait)/ intercept / star(dtmfstar)/ hash(pound、dtmfhash)/ dtmf0~dtmf9;不认识的字符串按音频文件路径处理
volume (number, 可选): 音量百分比 0~100,默认 80(<= 0 不播放)
duration (number, 可选): 内置提示音时长(毫秒),默认 400,自动夹到 30~5000;<= 0 表示按该提示音的默认长度播一遍(对音频文件无效)
loop (boolean, 可选): 仅对音频文件有效,是否循环播放,默认 false(播完自动释放)
📤 返回值:
boolean: 已开始播放返回 true;文件不存在 / 音量 <= 0 / 播放失败返回 false
示例
-- 播放内置提示音(默认音量 80、时长 400ms)
play_sound("ok")

-- 自定义音量 + 时长(约 1 秒)
play_sound("error", 100, 1000)

-- 播放本地音频文件(相对路径:古龙精灵 = App 内置 scripts 目录,古龙代码箱 = 当前工程目录)
play_sound("alert.mp3")

-- 绝对路径 / App 内置 assets 里的音频
play_sound("/sdcard/Music/bgm.mp3", 60)
play_sound("assets:alert.mp3", 80)

-- 循环播放背景音,10 秒后 stop_sound() 停止
play_sound("/sdcard/Music/bgm.mp3", 50, 0, true)
sleep(10000)
stop_sound()
stop_sound停止提示音
立即停止正在播放的提示音 / 音频文件,并释放播放资源(ToneGenerator / MediaPlayer)。脚本停止、App 退出时也会自动释放;没有声音在播时调用同样安全,可重复调用。
📥 参数:
无参数
📤 返回值:
无(返回 nil)
示例
-- 循环播放 10 秒后停止
play_sound("/sdcard/Music/bgm.mp3", 50, 0, true)
sleep(10000)
stop_sound()

-- 脚本结束 / 停止前确保不再有声音(脚本停止时 App 也会自动释放)
stop_sound()

更多工具函数(简要列表)

函数名说明示例
get_clipboard获取剪贴板内容local text = get_clipboard()
set_clipboard设置剪贴板内容set_clipboard("文本")
get_battery_level获取电池电量百分比(取不到返回 -1)local b = get_battery_level()
get_network_type获取网络类型local n = get_network_type()
is_screen_on检查屏幕是否亮起if not is_screen_on() then ...
wake_screen唤醒设备屏幕wake_screen()
get_volume / set_volume获取/设置音量(默认媒体音量,可传 music/ring/alarm/call/system 等音频流)set_volume(15, "music")
get_notifications获取通知列表(需在「设置 > 通知 > 通知使用权」开启本应用)local n = get_notifications()
clear_notifications清除通知栏clear_notifications()
get_location获取GPS定位(需定位权限;取最近一次缓存定位,无缓存返回 nil)local loc = get_location()
get_contacts获取联系人(需通讯录权限,最多 500 条)local c = get_contacts()
get_sms_list获取短信列表(需短信权限,默认 20 条 / 上限 500)local s = get_sms_list(10)
send_sms发送短信(未授予短信权限时自动拉起短信界面预填内容)send_sms("号", "内容")
make_call拨打电话(未授予通话权限时拉起拨号盘预填号码)make_call("10086")
get_sim_info获取SIM卡信息(手机号 / IMEI / 订阅号需电话权限,未授权返回空串)local s = get_sim_info()
get_wifi_list获取WiFi列表(按信号强度排序;Android 6.0+ 需定位权限)local w = get_wifi_list()
connect_wifi连接WiFi(root 设备成功率最高;Android 10+ 未 root 受系统限制)connect_wifi("名", "密码")
get_bluetooth_devices获取已配对蓝牙设备(Android 12+ 需蓝牙权限)local b = get_bluetooth_devices()
get_file_list获取文件列表(已实现;返回 name / path / size / is_dir / is_file 的文件信息表数组)local f = get_file_list("/sdcard/")
download_file下载文件到设备(已实现;HTTP 下载,相对路径=App 内置 scripts 目录 / 代码箱当前工程目录)download_file(url, path)
upload_file上传文件到服务器(已实现;multipart/form-data,字段名 file,返回是否成功)upload_file(path, url)
delete_file删除文件/目录(已实现;目录会递归删除;返回 boolean,拼字符串必须 tostring)log("结果: " .. tostring(delete_file("data.txt")))
create_dir创建目录(已实现;等价 mkdir,可递归创建父目录)create_dir("/sdcard/app")
file_exists检查文件/目录是否存在(已实现;绝对路径直接检查,相对路径=App 内置 scripts 目录)if file_exists("/sdcard/Download/a.txt") then ...
read_file_content读取文件内容(已实现,与read_file等价;相对路径=App 内置 scripts 目录)local c = read_file_content("config.json")
write_file_content写入文件内容(已实现,与write_file等价;相对路径=App 内置 scripts 目录)write_file_content("data.txt", "内容")
get_sdcard_path获取SD卡路径get_sdcard_path()
get_internal_storage_path获取内部存储路径get_internal_storage_path()
get_app_data_path获取应用数据目录路径get_app_data_path()
get_cache_path获取应用缓存目录路径get_cache_path()
get_download_path获取下载目录路径get_download_path()
get_pictures_path获取图片目录路径get_pictures_path()
get_music_path获取音乐目录路径get_music_path()
get_movies_path获取视频目录路径get_movies_path()
get_dcim_path获取相机照片目录路径get_dcim_path()
get_documents_path获取文档目录路径get_documents_path()
get_ringtone_path获取铃声目录路径get_ringtone_path()
get_alarms_path获取闹钟音频目录路径get_alarms_path()
get_notifications_path获取通知音频目录路径get_notifications_path()
get_podcasts_path获取播客目录路径get_podcasts_path()

🔐 设备/系统 API 权限速查(古龙精灵 / 古龙代码箱):相关权限已在 AndroidManifest 中声明 —— 定位 ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION,WiFi ACCESS_WIFI_STATE / CHANGE_WIFI_STATE,通讯录 READ_CONTACTS,短信 READ_SMS / SEND_SMS,电话 CALL_PHONE / READ_PHONE_STATE / READ_PHONE_NUMBERS,蓝牙 BLUETOOTH / BLUETOOTH_ADMIN / BLUETOOTH_CONNECT。运行时权限未授予时接口不会抛异常:返回 ""、空表、false 或 nil,并写入日志,脚本可按返回值降级处理。
📌 特例说明:get_notifications 需在「设置 > 通知 > 通知使用权」手动开启本应用,否则只能读到本应用自己的通知;get_location 只读取最近一次缓存定位(无缓存返回 nil);get_wifi_list 在 Android 6.0+ 需定位权限才拿得到扫描结果;connect_wifi 在 Android 10+ 未 root 设备上受系统限制会返回 false(root 设备走系统 wifi 命令);get_bluetooth_devices 仅返回已配对设备,Android 12+ 需 BLUETOOTH_CONNECT;send_sms / make_call 无对应权限时会自动拉起短信界面 / 拨号盘预填内容(需手动点击发送或拨打)。

💡 read_file_content / write_file_content / file_exists 已在设备端(古龙精灵、古龙代码箱)实现:read 成功返回 string(内容行尾统一为 \n,CRLF 文本也会被归一化;若最后一行是空行会自动去掉,不会多出空行)、write 成功返回 true、file_exists 返回 true/false;文件不存在或失败返回 nil / false。示例统一使用相对路径,在「古龙精灵」中即以 App 内置 scripts 目录为基准(如 data.txt → App 内置 scripts 目录/data.txt),便于脚本打包发布。绝对路径同样支持;若路径无权限(如共享存储 /sdcard/Download),设备已 root 时会自动通过 su 回退读写与存在性判断。
🔎 绝对路径示例(雷电模拟器):write_file_content("/sdcard/Download/gulong/data.txt", "Hello") → read_file_content("/storage/emulated/0/Download/gulong/data.txt")(/sdcard 等价 /storage/emulated/0);判断是否存在:if file_exists("/sdcard/Download/gulong/data.txt") then ... end;雷电共享目录:/mnt/shared/Pictures(对应 Windows「文档\LDPlayer\Pictures」,电脑端可直接查看)。
🧾 文件管理 API已在安卓端(古龙精灵、古龙代码箱)实现:delete_file / create_dir(别名 mkdir)/ list_files / get_file_list / copy_file / move_file / download_file / upload_file;路径规则与 read/write 完全一致:绝对路径直接操作(App 沙箱无权限时,设备已 root 会自动用 su 回退),相对路径在「古龙精灵」= App 内置 scripts 目录、在「古龙代码箱」= 当前工程目录。
⚠️ 返回值与日志(常见坑):delete_file / create_dir / copy_file / move_file / download_file / upload_file 这些接口返回的都是 boolean(true = 成功,false = 文件/目录不存在或失败),直接拼进字符串会报 attempt to concatenate string and boolean(典型报错行:log("结果: " .. delete_file("data.txt"))),必须写 local ok = delete_file("data.txt") → log("结果: " .. tostring(ok)),或直接 log("结果: " .. (ok and "删除成功" or "删除失败/文件不存在"));delete_file 对目录会递归删除。另外「古龙代码箱」的 log 只接收第一个参数(多写的参数会被忽略),所以「先 tostring 拼成一个字符串,再 log」在古龙精灵 / 古龙代码箱 都安全。

📱 设备方法 19个API

show_message显示消息
在设备屏幕上显示提示消息(APK模式可用)。支持 top / center / bottom 预设位置,也支持按坐标显示:show_message(msg, x, y[, duration]),(x,y) 为消息框中心,按脚本设计分辨率自动缩放适配实际屏幕
📥 参数:
message (string): 消息内容 / position (string|number|table, 可选, 默认'top'):预设位置或 x 坐标 / y (number, 可选): 坐标定位的 Y / duration (number, 可选): 显示时长(毫秒),默认3000
📤 返回值:
无
示例
-- 默认顶部显示
show_message("任务开始执行")

-- 预设位置 + 时长
show_message("任务完成!", "bottom", 5000)

-- 坐标定位:(x,y) 为消息框中心(自动缩放适配分辨率)
show_message("⚠️ 血量不足,请注意!", 600, 300, 3000)

-- 坐标定位(table 写法)
show_message("检测到BOSS", {x = 640, y = 200}, 2000)
toast显示提示
在设备上显示Toast弹窗
示例
toast("操作完成")
runApp / stopApp打开/关闭应用
打开或关闭指定应用(传入包名)
示例
runApp("com.tencent.mm")
stopApp("com.tencent.mm")
clearAppData清空应用数据
清空指定应用(游戏)的全部数据,等同于系统「设置 → 应用 → 存储 → 清除数据 / 清除存储」:登录账号、SharedPreferences、数据库、files、cache 全部删除,下次打开需要重新登录(换号、清缓存、重置游戏常用)。执行时系统会先强制停止目标应用;安卓端(脚本在手机上跑)需要 root(su 已授权)或 App 是设备所有者(Android 9+),普通未 root 设备会返回 false;PC 端(古龙精灵调试运行,脚本走本机 ADB)执行 adb shell pm clear,不需要 root,只要 ADB 已连接即可。注意:这是「清数据」不是「只清 cache」,账号一定会退出;安卓端不要对自己调用(会删除脚本/工程并杀死进程,已拦截),只想重启自身请用 restartApp;不会卸载应用,也不会删除 /sdcard/Android/obb 下的资源包。
📥 参数:
package_name (string): 目标应用包名,例如 "com.tencent.tmgp.sgame"
📤 返回值:
boolean: true = 清除成功(pm clear 返回 Success);false = 失败(包名为空 / 格式不合法 / 目标不存在 / 没有 root 且不是设备所有者 / 包禁止清除 / 超时),原因同时写入脚本日志
示例
-- ============================================================
-- clearAppData(包名) -> boolean
-- 清空应用「全部数据」= 系统 设置 → 应用 → 存储 → 清除数据:
-- 登录账号 / SharedPreferences / 数据库 / files / cache 全部删除,
-- 目标应用回到「刚安装」状态,下次打开需要重新登录(换号、清缓存常用)。
-- 需要 root 或 App 是设备所有者(Android 9+);普通未 root 设备返回 false。
-- ============================================================

-- 用一个函数收口,避免「单个清 / 批量清」写两遍
local function clearAndRelaunch(pkg, relaunch)
    if pkg == nil or pkg == "" then
        log("包名为空,跳过")
        return false
    end
    -- ① 目标在跑就先关掉:个别机型「边写边清」会清不干净(pm clear 内部也会 force-stop,这里只是兜底)
    stopApp(pkg)
    sleep(300)
    -- ② 清数据:系统确认成功才返回 true
    if not clearAppData(pkg) then
        log("❌ " .. pkg .. " 清除失败:包名写错 / 没有 root 且不是设备所有者 / 目标不存在")
        return false
    end
    log("✅ " .. pkg .. " 已清除(账号已退出),下次打开需要重新登录")
    -- ③ 要重开就等系统把数据目录清干净再启动,清完立刻 runApp 有可能起不来
    if relaunch then
        sleep(1500)
        runApp(pkg)
    end
    return true
end

-- 单个游戏:清数据 → 重新打开(重开后会停在登录页)
clearAndRelaunch("com.example.game", true)

-- 批量清理(模拟器多开 / 换号常用):只清不重开,最后统一报告结果
local pkgs = {
    "com.example.game",
    "com.example.game2",
    "com.example.game3",
}
local okCount = 0
local failList = {}
for i = 1, #pkgs do
    if clearAndRelaunch(pkgs[i], false) then
        okCount = okCount + 1
    else
        failList[#failList + 1] = pkgs[i]
    end
    sleep(300)   -- 批量时给系统一点缓冲,避免连续清理互相打架
end
log(string.format("批量清理完成:成功 %d / 共 %d", okCount, #pkgs))
if #failList > 0 then
    log("失败:" .. table.concat(failList, ", "))
end

-- 防呆:包名为空 / 带空格、分号等字符会直接返回 false,不会拼进 root 命令
log(tostring(clearAppData()))                  -- false
log(tostring(clearAppData("com.foo;reboot")))  -- false
restartApp重启本应用
重启古龙精灵 / 古龙代码箱自身(APK 模式和「📦 代码箱调试」可用)。适合「改完需要冷启动才生效」的配置(截图模式 / 古龙输入法 / 包内脚本更新)后让 App 自己重启:① 先按「停止脚本」处理(触发 setStopCallBack 停止回调、解锁古龙输入法、释放按下的手指);② 约 0.1 秒后结束进程;③ 系统约 1 秒后重新拉起应用。注意:调用后不会返回(后面的代码不会执行);默认重启后回到主界面、脚本不会自动继续运行,需要重新点「启动脚本」——想省这一步就传 true,重启后会自动帮你点一次「启动脚本」(代码箱还会沿用重启前正在跑的那个项目)。
📥 参数:
continue_run (boolean, 可选): 重启后是否自动继续运行脚本。不传 / false = 旧行为(回到主界面,手动点「启动脚本」);true = 重启后自动继续运行(也接受 1 / "true" 这类写法)
📤 返回:
无(进程很快就结束,调用后拿不到返回值)
示例
-- 1) 改完需要冷启动才生效的配置,让 App 自己重启(重启后手动点「启动脚本」)
restartApp()   -- 这一行后面的代码不会执行

-- 2) 重启完自动继续跑(改完截图模式 / 输入法 / 包内脚本后用这个)
show_message_keep("配置已保存,2 秒后自动重启并继续运行...", 600, 300)
sleep(2000)
restartApp(true)   -- 新进程起来后自动帮你点一次「启动脚本」

-- 3) 放进停止回调:收尾 + 重启(并自动继续运行)一条龙
setStopCallBack(function(error)
    imeLib.unlock()        -- 解锁古龙输入法
    close_message_keep()   -- 关掉驻留消息窗
    restartApp(true)       -- 收尾完成后重启,并自动继续运行
end)
frontAppName / appIsFront / appIsRunning
获取/判断前台应用和运行状态
示例
local pkg = frontAppName()
if appIsFront("com.example.game") then
    log("游戏在前台")
end
readPasteboard / writePasteboard读写剪贴板
读取/写入剪贴板内容
示例
writePasteboard("123")
local text = readPasteboard()
rnd生成随机整数
生成指定范围的随机整数(包含端点)
示例
local num = rnd(1, 100)
local x = rnd(100, 500)   -- 随机点击位置
getDisplaySize获取屏幕分辨率
获取设备的屏幕宽度和高度
示例
local w, h = getDisplaySize()
tap(w / 2, h / 2)  -- 点击屏幕中心
setControlBarPosNew设置悬浮窗位置
设置悬浮窗位置(按比例0~1.0)
示例
setControlBarPosNew(0, 0)        -- 左上角
setControlBarPosNew(1.0, 0)      -- 右上角
setControlBarPosNew(0.5, 0.5)    -- 屏幕中心
get_current_time获取当前时间
获取当前年月日时分秒
示例
local y, m, d, h, mi, s = get_current_time()
log(string.format("%d-%02d-%02d %02d:%02d:%02d", y, m, d, h, mi, s))
get_uptime获取开机运行时间
获取系统开机运行时间(毫秒)
示例
local hours = get_uptime() / 3600000
show_message_keep驻留消息窗
显示/更新「长时间驻留」的悬浮消息窗(APK 模式可用,不自动消失,需用 close_message_keep() 关闭)。特点:① 内容会一直保留,重复调用只更新你指定的那一行,其它行内容不变;② 支持多行显示,默认写第1行,可用第 4 个参数或 table 指定行号(1~5,超过 5 按 5 处理);③ 未指定内容的行不显示(不占空间);④ 第 1 行显示在你指定的坐标位置,第 2 行在第 1 行上面,第 3 行再往上,以此类推;⑤ 整个窗口宽度按文字最长的那一行自适应;⑥ 窗口不挡触摸,脚本的点击/滑动可以直接穿过它。小提示:推荐在 setStopCallBack 停止回调里也调用一次 close_message_keep(),这样手动点「停止」时窗口也会自动关掉(见示例第 9 点)。
📥 参数:
message (string): 本行显示内容;传 nil 或 "" 表示清空该行(所有行都清空时窗口自动关闭)
x (number, 可选): 第1行的 X 坐标(脚本设计分辨率,自动缩放适配实际屏幕);与 y 同时给出时才生效
y (number, 可选): 第1行的 Y 坐标(窗口底边位置)
line (number, 可选): 行号 1~5,默认 1
提示:只传一个数字时(如 show_message_keep(msg, 3))该数字视为行号;也可用 table 写法 {x=, y=, line=}
📤 返回:
boolean: true
示例
-- 1) 创建并显示第1行(默认),坐标 (600,300) 为第1行位置
show_message_keep("状态: 运行中", 600, 300)

-- 2) 更新第1行内容(其他行不受影响)
show_message_keep("状态: 打怪中 12:30")

-- 3) 第2行(显示在第1行上面),坐标省略 → 沿用上次位置
show_message_keep("血量: 980/1200", nil, nil, 2)

-- 4) 第3行写在第2行上面
show_message_keep("金币: 12,345", nil, nil, 3)

-- 5) 只给一个数字 → 视为行号(更新第4行)
show_message_keep("经验: 15%", 4)

-- 6) table 写法:位置 + 行号一起指定
show_message_keep("BOSS 已刷新", {x = 600, y = 300, line = 5})

-- 7) 清空某一行(例如 BOSS 消失后)
show_message_keep("", nil, nil, 5)

-- 8) 用完后销毁整个驻留消息窗
close_message_keep()

-- ===== 典型循环用法(10秒刷新一次血量,全程不闪屏)=====
show_message_keep("自动挂机中", 60, 400)     -- 第1行:固定位置
while true do
    local hp = get_color(100, 100)            -- 你自己的取值逻辑
    show_message_keep("血量: " .. tostring(hp), nil, nil, 2)
    show_message_keep("金币: " .. tostring(os.time()), nil, nil, 3)
    sleep(1000)
end
-- 脚本正常结束时记得清理:close_message_keep()

-- 9) 推荐收尾:只要用了驻留窗,就顺手注册一个停止回调,
--    这样「手动点停止 / 脚本跑完 / 脚本报错退出」都会自动把窗口关掉(不会残留在屏幕上)
setStopCallBack(function(error)
    imeLib.unlock()                                        -- 解锁古龙输入法,恢复系统输入法
    close_message_keep()                                   -- 关闭驻留消息窗
    execRoot("settings put global policy_control null")    -- 恢复状态栏显示
end)
close_message_keep销毁驻留消息窗
销毁 show_message_keep 创建的驻留消息窗(APK 模式可用)。已销毁后再调用不会报错;窗口被销毁后,各行的内容也会清空,下次 show_message_keep 会重新创建窗口。小提示:脚本停止/Service 结束时也会自动清理,正常流程建议脚本末尾主动调用一次;更稳妥的写法是放进 setStopCallBack 停止回调里,这样手动点「停止」、脚本跑完、脚本报错退出都会自动关闭(见示例最后一段)。
📥 参数:
无
📤 返回:
boolean: true
示例
-- 先显示驻留面板
show_message_keep("自动挂机中", 60, 400)
show_message_keep("血量: 1200/1200", nil, nil, 2)

-- 任务结束时关闭
close_message_keep()
show_message("任务完成!", "bottom", 3000)   -- 需要短暂提示可以用 show_message

-- ===== 推荐:写进停止回调,脚本无论怎么结束都会自动关闭 =====
-- 手动点「停止」时,脚本末尾那行 close_message_keep() 根本走不到,窗口会残留在屏幕上;
-- 放进 setStopCallBack 后:手动停止 / 脚本跑完 return / 脚本报错退出,都会自动关掉
setStopCallBack(function(error)
    imeLib.unlock()                                        -- 解锁古龙输入法,恢复系统输入法
    close_message_keep()                                   -- 关闭驻留消息窗
    execRoot("settings put global policy_control null")    -- 恢复状态栏显示
end)
show_dialog弹窗提示(可暂停脚本/选择停止或继续)
弹出「阻塞式」悬浮窗并暂停脚本(APK 模式可用)。脚本执行到该函数会停下来等待用户操作:窗口左下角是「停止脚本」,右下角是「继续运行」,两个按钮都可以单独隐藏;标题、正文、按钮文字、字号、位置、超时时间都可自定义。点「继续运行」后函数返回 true、脚本继续往下跑;点「停止脚本」默认会立即停止脚本(与点击界面「停止」按钮等效),也可用 stop_action='return' 改成只返回 false、由脚本自己决定后续(方便做善后处理)。小提示:两个按钮都隐藏时,点按弹窗任意位置即「继续运行」;没有悬浮窗权限等异常情况会直接按「继续运行」放行,绝不会把脚本卡死。
📥 参数:
text (string): 正文内容(可用 \n 换行)
options (string|table, 可选): 传字符串 = 窗口标题;传 table 可精细设置:
title (string): 标题,默认「提示」
text (string): 正文(也可直接用第 1 个参数)
stop (boolean): 是否显示左下角「停止脚本」,默认 true
resume (boolean): 是否显示右下角「继续运行」,默认 true
stop_text (string): 停止按钮文字,默认「停止脚本」
resume_text (string): 继续按钮文字,默认「继续运行」
stop_action (string): 点「停止脚本」后的动作,'stop'=立即停止脚本(默认)/ 'return'=只返回 false,由脚本自行处理
timeout (number): 超时毫秒数,0=不限时(默认 0)
timeout_action (string): 超时后的动作,'resume'(默认,继续运行)/ 'stop'(停止脚本)
position (string): 'center'(默认)/ 'top' / 'bottom'
x, y (number): 坐标定位,表示窗口中心,按脚本设计分辨率自动缩放适配实际屏幕
text_size (number): 正文字号,默认 16
📤 返回:
boolean: true=继续运行(脚本继续执行);false=用户选择了「停止脚本」。注意:默认 stop_action='stop',此时脚本会被立即停止、函数不会返回到脚本里;想拿到 false 自己处理请设置 stop_action='return'
示例
-- 1) 最简用法:两个按钮都显示,脚本暂停等你选择
local ret = show_dialog("请手动完成登录,完成后点「继续运行」")
if ret then
    log("用户选择继续,脚本继续执行")
end

-- 2) 自定义标题(第 2 个参数传字符串)
show_dialog("检测到验证码,请手动输入", "人工确认")

-- 3) 只显示「继续运行」(隐藏左下角停止按钮)
local ok = show_dialog("请确认背包已清空", {stop = false})

-- 4) 只显示「停止脚本」,并自定义按钮文字
show_dialog("账号异常,脚本需要停止", {resume = false, stop_text = "退出脚本"})

-- 5) 超时自动继续(5 秒没操作就继续跑)
show_dialog("BOSS 即将刷新,是否等待?", {timeout = 5000, timeout_action = "resume"})

-- 6) 坐标定位 + 不显示任何按钮(点按弹窗任意位置继续)
show_dialog("脚本已暂停(点我继续)", {stop = false, resume = false, x = 640, y = 360})

-- 7) 典型用法:异常时询问用户,按选择决定去留
local ret = show_dialog("检测到角色死亡,\n是否继续挂机?", {
    title       = "运行提示",
    stop_text   = "停止脚本",
    resume_text = "继续挂机"
})
if ret then
    log("继续挂机")
else
    log("用户选择停止")
end

-- 8) 想自己做善后处理:用 stop_action="return" 拿到 false,再决定去留
local ret = show_dialog("检测到异常,是否继续?", {
    stop_action = "return"        -- 点「停止脚本」时只返回 false,平台不强制停止
})
if ret then
    log("继续执行")
else
    log("用户要求停止,做善后处理后自己结束脚本")
    close_message_keep()          -- 例如:清理驻留面板
    return
end

🔐 Root与Shell 3个API

execRoot执行Root命令
以 Root 权限执行命令并返回输出(需设备已 root)。优先使用持久 su 通道(不会重复弹授权)并带超时;无 root 或执行失败返回空字符串。可用于文件管理(ls/cp/mv/rm/mkdir/chmod)、系统操作(setenforce 0 / mount -o remount,rw /system;重启整台设备请用 rebootSystem(),不要在这里执行 reboot)、应用管理(pm list packages / pm disable)等。注意:高权限操作需谨慎。
📥 参数:
command (string): 要执行的命令 / timeout (number, 可选): 超时毫秒,默认10000
📤 返回值:
string: 命令输出(含 stderr),无 root 或失败返回空字符串
示例
-- 查看系统信息
local info = execRoot("cat /proc/version")
print("系统版本: " .. info)

-- 获取当前用户
print("当前用户: " .. execRoot("whoami"))

-- 查看进程列表
print(execRoot("ps -A"))

-- 隐藏状态栏(完全沉浸式,* 表示所有应用)
execRoot("settings put global policy_control immersive.full=*")
-- 恢复状态栏显示
execRoot("settings put global policy_control null")
shell执行Shell命令
执行普通 shell 命令(以当前应用 uid,无需 root)并返回输出(stdout+stderr 合并)。适合无需 root 的设置/查询命令,例如隐藏状态栏指定图标(Android 6.0+ icon_blacklist)。
📥 参数:
command (string): 要执行的命令 / timeout (number, 可选): 超时毫秒,默认10000
📤 返回值:
string: 命令输出(含 stderr),失败返回空字符串
示例
-- 查看当前已隐藏的状态栏图标
local hidden = shell("settings get secure icon_blacklist")
print("已隐藏图标: " .. hidden)

-- 隐藏蓝牙与QQ图标(无需root,Android 6.0+)
shell("settings put secure icon_blacklist bluetooth,qq")

-- 查看所有支持隐藏的图标名称
print(shell("cmd statusbar get-status-icons"))

-- 隐藏所有应用的状态栏(如需写权限请改用 execRoot)
shell("settings put global policy_control immersive.status=*")
rebootSystem重启安卓系统
重启安卓系统(模拟器 / 云手机)。
模拟器 / 虚拟机·2026-10-26 起默认不再重启系统(显式 mode="soft" 除外,详见下方 mode 四档):检测到跑在模拟器 / 虚拟机里(雷电 / MuMu / 夜神 / AVD)时,本函数改为「重启古龙自身进程」——停脚本收尾 → 预约拉起自身 → 退出进程,几秒回到主界面,安卓画面一秒都不停,adb / 投屏也不会断;continue_run=true 照旧自动继续运行,同样返回 true,且不需要任何权限。原因:软重启(sync; setprop ctl.restart zygote)客机 60~90 秒就能恢复,但宿主 LDPlayer 的渲染通道会停在重启前那一帧、不会自己恢复(实测 4 分钟以上,只有宿主窗口失焦再得焦才刷新),后台窗口受 Windows 前台锁限制唤不醒;硬重启(su -c "sync; reboot")更糟,雷电会把宿主里的 VM 一起 poweroff,得人工 ldconsole launch。
真机:需要设备已 root(su 授权)、App 是设备所有者,或 App 持有系统 REBOOT 权限三者之一;普通未 root 设备无法重启系统。mode 四档:auto(默认:模拟器 / 虚拟机重启古龙自身进程,真机整机重启)/ app(任何设备都只重启古龙自身进程,画面一秒不停,不需要 root 或任何权限)/ soft(软重启安卓框架 ctl.restart zygote,模拟器 / 云手机 / 真机都照做,需要 root;没有 root 时自动降级:模拟器 / 云手机 → app,真机 → hard)/ hard(强制整机重启;模拟器上有宿主把 VM 一起断电的风险)。调用后先走一遍正常停止流程(setStopCallBack、解锁古龙输入法、释放按下的手指、上报停止原因),再在 delay 指定的延迟后执行。真机上返回 true 只表示请求已受理(随后关机,函数之后的代码通常不执行);模拟器上返回 true 表示约 delay 毫秒后重启自身进程。
opts 表(第 5 个参数):把软重启的系统侧收益补回给「重启自身进程」;wake_screen / settle_ms / restart_surfaceflinger 在 soft / hard 档同样生效。clean_memory(重启前 sync + drop_caches 回收页缓存)/ kill_background(重启前 am kill-all 清可杀后台)/ reset_network(重启前 wifi / 数据关再开)/ wake_screen(重启后亮屏 + 解锁 + 回桌面 + 插电常亮;最多重试 3 次,用 isInteractive / isKeyguardLocked 判断是否真的亮屏解锁,治「不点一下画面就永远不进系统」;系统级重启 soft / hard 默认开、app 档默认关,显式写了按你的;需要 su,没 root 自动跳过)/ settle_ms(仅 app 档生效:重启后延迟多久再自动启动脚本,0~30000;soft / hard 由公共设置的等待秒数决定)/ restart_surfaceflinger(soft 档顺带 ctl.restart surfaceflinger,实验:缓解模拟器宿主停帧,会黑屏更久)。前三个「重启前清理」只在 app 档执行,soft / hard 本身就是整机级重启;所有选项失败都只写日志,不会卡住重启。
重启后的开机启动(两条链路,别混用):app 档(只重启自身进程)沿用原来的「自动继续运行」——停脚本 → 预约拉起自身 → 3 秒 + settle_ms 后自动启动脚本,没有倒计时(画面没中断,不需要等)。soft / hard 档(真的重启了安卓系统)不再由本函数直接拉起脚本,改走「公共设置 → 开机启动脚本」的倒计时流程:开机后控制面板自动打开 → 按公共设置里的等待秒数显示倒计时(0~600 秒,用户面板「设置 → 公共设置」可改)→ 到点启动脚本并关闭面板;公共设置没开启就只打开面板、不启动脚本(这时 continue_run 也不会绕过它)。古龙精灵会在脚本触发的系统重启后强制走这条流程,并以服务端公共设置为权威值(本机快照过期也不会漏);古龙代码箱没有公共设置链路,系统级重启仍按 continue_run 自动继续。settle_ms 只对 app 档有意义。
防重启循环:90 秒内只受理一次(受理时把受理时刻落盘,不区分开机周期):冷却期内的第二次调用直接返回 false,所以「重启 → 自动继续运行 → 立刻又执行到 rebootSystem()」不会无限循环;满 90 秒即正常放行,同一次开机里也不会被永久锁死。同机装了精灵 + 代码箱时闸门是设备级共享的(/data/local/tmp/gulong_reboot_guard,需 root)。
模拟器 / 云手机·整机重启:guest 里 reboot 只是向宿主发关机请求,宿主 poweroff 再冷启,中间黑屏 30~120 秒属正常(实测雷电 Android 14 约 90 秒),不是死机 —— 这也是模拟器上默认不走这条路的原因。确实要整机重启模拟器请用宿主命令(雷电 ldconsole quit/launch --index,MuMu MuMuManager control -v 0 reboot,夜神 NoxConsole reboot)。PC 引擎(本机调试运行)不实现 rebootSystem,调用返回 false 并在日志提示。
📥 参数:
delay_ms (number, 可选): 延迟毫秒,默认 1000,范围 0~60000 / continue_run (boolean, 可选,默认 false): app 档重启后是否自动继续运行当前脚本(3 秒 + settle_ms 后无倒计时直接启动,需系统允许 App 开机自启);soft / hard 档不看这个参数,系统重启后的开机启动统一走「公共设置 → 开机启动脚本」的倒计时流程 / mode (string, 可选): auto(默认:模拟器 / 虚拟机重启自身进程,真机整机重启)/ app(只重启古龙自身进程,画面不停,不需要权限)/ soft(软重启安卓框架,模拟器 / 云手机 / 真机都照做,需要 root;没 root 自动降级为 app 或 hard)/ hard(整机重启;模拟器上有 VM 被断电的风险)/ stop_app (string / table, 可选): 重启自身进程前顺带 force-stop 的包名,如 "com.xxx.game" 或 {"com.a", "com.b"}(有 root 才彻底)/ opts (table, 可选): clean_memory、kill_background、reset_network、wake_screen、settle_ms、restart_surfaceflinger(含义见上方说明,全部可选)
📤 返回值:
boolean: true = 已受理(app 档 delay 到点后只重启古龙自身进程、安卓画面不中断;soft / hard 档 delay 到点后停止脚本并重启系统);false = 距上次受理重启不足 90 秒(防重启循环,含同机其它古龙 App 请求的),或 soft / hard 档没有可用重启通道(未 root / 非设备所有者 / 非系统应用)(返回 false 时脚本继续运行)
示例
-- 1) 立即重启(默认 1 秒后),重启后手动重新启动脚本
local ok = rebootSystem()
if not ok then
    log("当前设备无法重启:需要 root / 设备所有者 / 系统签名")
end

-- 2) 留 3 秒收尾,重启后自动继续跑(无人值守脚本推荐)
show_message_keep("设备将在 3 秒后重启…", 600, 300)
rebootSystem(3000, true)
-- 真机上这一行通常不会执行:delay 到点后脚本会被停止并重启设备
-- 模拟器 / 虚拟机上会重启古龙自身进程,系统不重启、画面不停

-- 3) 防重启循环:90 秒内只受理一次,冷却期内的第二次一定返回 false
--    (重启后自动继续运行、脚本又跑到这里时,不会把设备再重启一遍)
local again = rebootSystem()
if not again then
    log("距上次受理重启不到 90 秒,这次不重启")
end

-- 4) 模拟器 / 虚拟机上不会重启系统:改为重启古龙自身进程(画面一秒不停),
--    顺带把游戏 force-stop,下次进游戏就回到登录 / 主界面
rebootSystem(2000, true, "auto", "com.xxx.game")

-- 5) 只想重启古龙自身进程:任何设备都画面不停,也不需要 root
rebootSystem(1000, true, "app")

-- 6) 软重启安卓框架 + 重启后亮屏解锁(需要 root)
--    没有 root 时自动降级为 app / hard,日志里会写明
rebootSystem(2000, true, "soft", nil, {
    wake_screen = true,          -- soft / hard 档默认就是 true,显式写更清楚
    settle_ms = 5000,           -- 仅 app 档生效;soft / hard 由公共设置倒计时决定
    clean_memory = true,       -- 重启前顺手回收内存缓存
    kill_background = true      -- 重启前清一次后台进程
})

-- 7) 参数多时可以用单表形式,含义和位置参数完全一样
rebootSystem{
    delay_ms = 3000,
    continue_run = true,
    mode = "app",
    stop_app = { "com.xxx.game", "com.yyy.game" },
    opts = { settle_ms = 3000 }
}

🌐 代理 IP(换 IP 防同 IP 限号) 6个API

游戏常按「同一个公网 IP 能登录几个账号」限号。这组 API 从携趣(xiequ.cn)提取短效代理, 写进模拟器的系统代理(settings put global http_proxy), 再重启目标 App,让它用新的出口 IP 连服务器。六个函数可以拆开组合,也可以只用 switch_proxy_ip 一步到位。
⚠️ 仅供学习交流,请勿用于真实游戏。

⚠️ 换完 IP 不生效先看这里(三条前置条件,缺一不可):
· 携趣账号 + 提取链接,且本机公网 IP 已加入白名单:携趣后台「API 提取」页复制完整链接; 白名单认的是本机出口 IP(curl ifconfig.me 查)。没加白名单时接口返回 DB1 之类的错误文本,脚本只会拿到 nil。
· 设备能写 settings:需要 root 或 shell 有 WRITE_SECURE_SETTINGS;否则日志报 ❌ 写入系统代理失败:期望 1.2.3.4:8888,实际读取到 ''。
· 目标 App 遵守 Android 系统代理,且换完必须重启进程:OkHttp / HttpURLConnection / WebView / UnityWebRequest 默认实现有效;自研 TCP/UDP 长连接无效(见本节末尾「局限与替代方案」)。

📱 设备端签名(代码箱调试 / 已装 APK 完全一致)

函数签名作用
set_proxy(ip, port, package)写设备系统代理,可顺带重启 App
get_proxy()读设备当前系统代理
clear_proxy(package)清除系统代理,可顺带重启 App
fetch_proxy(api_url, num)只提取,不动设备
switch_proxy_ip(api_url, package, num, verify, timeout)提取 → 校验 → 写入 → 重启
proxy_out_ip(proxy, timeout)查出口 IP

脚本统一在设备端执行(「📦 代码箱调试」与安装在手机/模拟器上的「古龙精灵」「古龙代码箱」签名完全相同), 开发工具只负责把项目发到设备运行。所以这里没有 adb 参数 —— 代理直接写脚本所在设备的系统代理。

⚠️ 提取链接必须显式传(设备端没有配置文件):fetch_proxy / switch_proxy_ip 不传 api_url 时会退到内置的 http://api.xiequ.cn/VAD/GetIp.aspx (不含 uid/vkey,通常必失败)。设备上既没有 proxy.json、也不读 GULONG_XQ_API_URL,链接只能写在脚本里、项目文件里,或从 get_setting 读(见本节末尾)。
set_proxy设置系统代理
把代理写进脚本所在设备的系统代理,支持 "ip:port"、 "ip", port、{ip=.., port=.., package=..} 三种写法。写完会自动读回校验:只有 settings get global http_proxy 读到的值与写入值完全一致才返回 true。传 package 时会 force-stop + monkey 启动目标 App —— 系统代理只在进程启动时读取,不重启的话老进程仍走旧网络。
📥 参数:
ip (string/table): "1.2.3.4:8888" 或 {ip="..", port=.., package=".."} port (number, 可选): ip 只写了地址时单独给端口 package (string, 可选): 写完要重启的 App 包名(强烈建议传) ※ 设备端系统代理只支持 HTTP,没有 protocol 参数
📤 返回值:
boolean: 写入成功 true,失败 false(原因见控制台 [代理] 日志)。 true 只代表代理写进设备成功,App 重启失败会单独打印 ⚠ 日志(代理已生效,手动打开一次 App 即可)
示例
-- ① 已有 "ip:port"(最常用)
set_proxy("1.2.3.4:8888")

-- ② IP 和端口分开
set_proxy("1.2.3.4", 8888)

-- ③ 写完顺便重启游戏
set_proxy("1.2.3.4:8888", nil, "com.xxx.game")

-- ④ table 写法:把 fetch_proxy 拿到的地址直接丢进来
local addr = fetch_proxy()
if addr then
    set_proxy({ip = addr, package = "com.xxx.game"})
end
get_proxy读取系统代理
读当前系统代理(settings get global http_proxy)。null / 空 / :0 一律视为直连并返回 nil。返回的 table 里 address 是 "ip:port",host / port 已经拆好, 可直接用于打日志或判断「是不是已经换过 IP」。
📥 参数:
(无参数)读取脚本所在设备当前的系统代理
📤 返回值:
table: {address="1.2.3.4:8888", host="1.2.3.4", port=8888};无代理(直连)返回 nil
示例
-- 看看现在走的是直连还是代理
local p = get_proxy()
if p then
    log(string.format("当前代理 %s(%s:%d)", p.address, p.host, p.port))
else
    log("当前直连")
end

-- 换号前先判断:已经是目标代理就不用重设(省时间也省代理时长)
local target = "1.2.3.4:8888"
if not p or p.address ~= target then
    set_proxy(target, nil, "com.xxx.game")
    sleep(8000)
end
clear_proxy清除系统代理
清除系统代理、恢复直连,并读回确认(读回仍有值时会补一个 http_proxy :0 兜底)。 清掉的键:http_proxy、https_proxy、global_http_proxy_host、 global_http_proxy_port、global_http_proxy_exclusion_list、 global_proxy_pac_url。传 package 时清完顺带重启该 App,让它立刻走回直连。
📥 参数:
package (string, 可选): 清完要重启的 App 包名
📤 返回值:
boolean: 清除成功(读回已为空)true,失败 false
示例
-- ① 只清代理(脚本收尾用,避免影响别的软件)
clear_proxy()

-- ② 清完并重启游戏,立刻恢复直连
clear_proxy("com.xxx.game")

-- ③ 这个号被限制:先摘代理 → 重新换一个 IP → 再登录
clear_proxy("com.xxx.game")
sleep(5000)
set_proxy("5.6.7.8:9999", nil, "com.xxx.game")
sleep(8000)
login(username, password)
fetch_proxy提取携趣代理
从携趣提取接口拉一个可用代理,只返回、不动设备。api_url 就是携趣后台「生成 API 链接」 复制出来的完整链接(已带 uid / vkey / 区域等参数);工具只把链接里的 num 改成脚本要的个数, 其它参数原样保留。api_url 必传(设备端不读环境变量 / 配置文件,不传会退到不含 uid/vkey 的 默认地址,通常必失败)。接口返回纯文本 ip:port 列表(换行 / 逗号 / 分号 / 空格分隔) 或 JSON(携趣「提取格式」选 JSON、或链接里带 plat=0,形如 [{"IP":"1.2.3.4","Port":"8888"}])都能解析。num > 1 时接口会返回多个, 本函数只返回第一个;想逐个挑选请用 switch_proxy_ip(它会挨个校验)。
📥 参数:
api_url (string, 必传): 携趣提取链接(完整链接,含 uid / vkey) num (number, 可选): 提取数量,默认 1
📤 返回值:
string: "ip:port"(提多个时返回第一个);失败返回 nil(原因见控制台 [代理] 日志)
示例
-- ① 用自己的提取链接(设备端必须显式传)
local addr = fetch_proxy(API_URL)
if addr then log("提到代理: " .. addr) end

-- ② 用自己的链接,一次提 3 个(只取第一个)
local one = fetch_proxy("http://api.xiequ.cn/VAD/GetIp.aspx?act=get&uid=xxx&vkey=xxx&num=3&spl=1", 3)

-- ③ 提取 + 写入 + 重启,自己控制流程(等价于 switch_proxy_ip)
-- API_URL = 你的携趣提取链接常量(见本节「提取链接配置」)
local addr2 = fetch_proxy(API_URL)
if addr2 then
    set_proxy(addr2, nil, "com.xxx.game")
    sleep(10000)
    login(username, password)
end
switch_proxy_ip一键换 IP
换 IP 一条龙:提取 → 校验 → 写入系统代理 → 重启 App。多开防「同 IP 限号」用这一个函数就够。 verify 默认 true 时按顺序逐个校验(经代理访问携趣查 IP 页,拿到出口 IP 才算通),死 IP 自动跳过用下一个; 写入失败/全部校验失败都返回 nil,不抛 Lua 异常,脚本可以照常继续直连。 提取结果同时支持纯文本 ip:port 列表与 JSON(携趣「提取格式」选 JSON / 链接带 plat=0)两种返回。
📥 参数:
api_url (string, 必传): 携趣提取链接(完整链接,含 uid / vkey) package (string, 可选): 写完要重启的 App 包名 num (number, 可选): 一次提几个,默认 1(短效代理容易失效,建议传 3 左右,verify 会自动跳过死 IP) verify (boolean, 可选): 是否先验证代理能出网,默认 true(false 更快,但可能拿到死 IP) timeout (number, 可选): 提取 / 校验超时秒数,默认 12 ※ 设备端签名: switch_proxy_ip(api_url, package, num, verify, timeout)
📤 返回值:
string: 真正写进设备的 "ip:port";失败返回 nil(原因见控制台 [代理] 日志)
示例
-- ① 传链接 + 重启游戏
local addr = switch_proxy_ip(API_URL, "com.xxx.game")
if addr then
    log("已换 IP: " .. addr)
    sleep(10000)            -- 等游戏起来
else
    log("换 IP 失败,本次直连登录")
end

-- ② 用自己的链接、跳过校验(快,但可能拿到死 IP)
switch_proxy_ip(API_URL, "com.xxx.game", 1, false)

-- ③ 每个账号换一次 IP
for i, account in ipairs(accounts) do
    local one = switch_proxy_ip(API_URL, "com.xxx.game", 2, true, 15)
    if not one then
        log(string.format("[%d/%d] 换 IP 失败,跳过 %s", i, #accounts, account))
    else
        log(string.format("[%d/%d] %s 走 %s", i, #accounts, account, one))
        sleep(10000)
        login(account, passwords[i])
        sleep(30000)        -- 做完任务
    end
end
proxy_out_ip查询出口 IP
查出口 IP,用来确认「换 IP 到底成没成」。默认访问 https://www.xiequ.cn/OnlyIp.aspx: 不传参数查脚本所在设备的出口 IP(即手机 / 模拟器当前网络); 传 "ip:port" 或 table(可带账密)则经该代理去查。失败返回空字符串,不抛异常。
📥 参数:
proxy (string/table, 可选): "ip:port" 或 {host=.., port=.., user=.., pass=..};不传查本机 timeout (number, 可选): 超时秒数,默认 8
📤 返回值:
string: 出口 IP(如 "121.21.193.85");失败返回 ""
示例
-- ① 查本机出口 IP(做对比基线)
log("本机出口: " .. proxy_out_ip())

-- ② 验证刚提取的代理能不能出网
local a = fetch_proxy(API_URL)
if a then
    local ip = proxy_out_ip(a)
    log(ip == "" and ("代理不可用: " .. a) or ("出口 IP: " .. ip))
end

-- ③ 换 IP 前后对比
local before = proxy_out_ip()
local addr = switch_proxy_ip(API_URL, "com.xxx.game")
log(string.format("换 IP: %s -> %s", before, proxy_out_ip(addr)))

-- ④ table 写法:带账密(携趣隧道代理)
log(proxy_out_ip({host = "1.2.3.4", port = 8888, user = "u", pass = "p"}))

⚙️ 提取链接怎么放进脚本(设备端三选一)

方式写法适用
① 脚本内常量local API_URL = "http://api.xiequ.cn/...uid=xx&vkey=xx..."最简单,但链接会随脚本一起分发
② 项目内文件local API_URL = read_file_content("proxy_api.txt")链接与代码分离,改链接不用改脚本(文件随项目一起下发到设备)
③ 服务端设置local API_URL = get_setting("xq_api_url", "")推荐:链接放在用户面板「脚本设置」里,按用户区分,不进脚本包

⚠️ 设备端没有任何「全局代理配置」:没有 proxy.json,也不读 GULONG_XQ_API_URL / GULONG_XQ_UID / GULONG_XQ_VKEY 环境变量。不传链接时只会退到内置的 http://api.xiequ.cn/VAD/GetIp.aspx(不含 uid/vkey,通常必失败),日志里会打印接口返回的错误文本。

📌 只改写 num:脚本传入的 num 会覆盖链接里的 num,其余参数(uid / vkey / type / addr / spl) 原样保留 —— 直接把携趣后台复制的链接粘进来即可,不用手工拼参数。

🧩 完整示例:每个账号换一次 IP

Lua - 换 IP 多开模板(设备端)
-- 同一份脚本在「📦 代码箱调试」与已安装 APK 上都能跑
local API_URL  = "http://api.xiequ.cn/VAD/GetIp.aspx?act=get&uid=xxx&vkey=xxx&spl=1"
local GAME_PKG = "com.xxx.game"
local accounts = {"acc1", "acc2", "acc3"}

local function login(account, password)
    log("登录 " .. account)      -- 换成自己的登录 / 起号逻辑
end

function main()
    for i, account in ipairs(accounts) do
        -- ① 提取 → 校验 → 写系统代理 → 重启游戏(返回真正生效的地址)
        local addr = switch_proxy_ip(API_URL, GAME_PKG, 2, true, nil, 15)
        if addr then
            log(string.format("[%d/%d] %s 走 %s", i, #accounts, account, addr))
            log("代理出口: " .. proxy_out_ip(addr))   -- ② 确认真的换了
        else
            log(string.format("[%d/%d] 换 IP 失败,直连继续", i, #accounts))
        end
        sleep(10000)                    -- 等游戏启动完成
        login(account, "password")
        sleep(30000)                    -- 做完任务
    end
    -- ③ 收尾:摘掉系统代理,别影响设备上的其它联网软件
    clear_proxy(GAME_PKG)
end

main()

🚫 局限与替代方案

情况表现建议
游戏用自研 TCP / UDP 长连接代理写入成功,游戏内显示的 IP 却没变系统代理天然不接管原始 socket;需 root + tun2socks / redsocks,或用携趣 PC 版「进程代理」整机转发
换 IP 后没有重启 App仍走旧 IP传 package(脚本自动 force-stop + 启动),或手动重启一次 App
代理出口是 NAT 同一 IP换了好几次,出口 IP 都一样携趣后台换线路(type)或指定地区(addr),再重新提取
系统代理是全局的设备上其它联网 App 也会走代理脚本收尾调用 clear_proxy(package)
代理需要账密写进系统代理后 App 连不上Android 静态系统代理不支持账密认证;携趣改用「本机 IP 白名单」,或用隧道代理(账密在隧道入口校验)
Android 端写本机设置失败日志:❌ 写入系统代理失败 / 需要 root给设备 root;或 adb 授予 pm grant 古龙精灵包名 android.permission.WRITE_SECURE_SETTINGS
🔎 手工确认(不写脚本也能查):
· 读当前代理:adb shell settings get global http_proxy(输出 null = 直连)
· 写 / 清代理:adb shell settings put global http_proxy 1.2.3.4:8888,清除用 adb shell settings put global http_proxy :0
· 确认代理本身可用:curl -x http://1.2.3.4:8888 https://www.xiequ.cn/OnlyIp.aspx
· 确认 App 是否重启过:adb shell pidof com.xxx.game(换 IP 前后 pid 应变化或短暂为空)
💡 排查顺序(照这个走最快):先用 proxy_out_ip("ip:port") 确认代理能用 → 再用 get_proxy() 确认地址真的写进设备 → 再看日志里有没有 ✅ 已重启: 包名 → 最后判断游戏是不是走 TCP 直连(见上表第一行)。

🧠 Memory学习(进程内存读写) 3个API

用 GetModuleHandle / MemoryRead / MemoryWrite 按「基址 + 偏移」读写安卓应用(含手游)的进程内存,用于学习逆向分析、研究数值在内存里的存放结构。三个 API 分工:GetModuleHandle 取模块基址(读写的起点)、MemoryRead 按地址读(整数 / 浮点 / 字符串 / 原始字节)、MemoryWrite 按地址写并自动回读校验。只在设备端提供(✅ 装在手机/模拟器上的「古龙精灵」「古龙代码箱」、✅「📦 代码箱调试」)。

🧭 先搞懂三件事:
① libunity.so 是什么?「模块」就是随 App 一起加载的 .so 动态库文件;libunity.so 是 Unity 引擎的运行时库(Unity 手游的核心模块,逆向时一般从它入手)。同类常见模块:libil2cpp.so(Unity 用 IL2CPP 把 C# 编成的原生代码,游戏数值多半在这里)、libmono.so(老版本 Unity 的 Mono 运行时)、libUE4.so(虚幻引擎)、libcocos2dlua.so / libcocos2djs.so / libcocos.so(Cocos2d-x)、libc.so / libm.so / libc++_shared.so(系统与 C/C++ 运行库,适合练手)。每款游戏用的引擎不同、模块名也不同:脚本里的 libunity.so 只是示例,请换成目标游戏里真实存在的 so 文件名。
② 模块名怎么查?先 execRoot("pidof 包名") 拿到进程 pid,再执行 execRoot("cat /proc/<pid>/maps | awk '{print $6}' | sort -u | grep '[.]so$'") 列出该进程加载的所有 so,取路径最后一段文件名即可。
③ 为什么必须先取基址?模块每次启动被加载到哪个地址都是随机的(ASLR),逆向工具里看到的「偏移」只有加上运行时基址才是真实地址:真实地址 = 模块基址 + 偏移;基址不要写死,脚本每次运行都要重新取。

🚀 典型流程:GetModuleHandle() 取基址 → MemoryRead() 按偏移读取(多级指针就嵌套读)→ MemoryWrite() 写入并回读校验 → 用 HEX 看内存原貌核对类型与字节序。不确定读写链路通不通,先做一次冒烟测试:MemoryRead(GetModuleHandle("libc.so"), "HEX", nil, 4) 应得到 7F 45 4C 46(ELF 魔数)。

⚠️ 使用要点(务必先看):① 需要 root:读写的是别的进程的内存,内部通过持久 su 通道 + /proc/<pid>/maps、/proc/<pid>/mem 完成;无 root 时只能读写自身 App 进程(部分机型连自身也会被系统 SELinux 拦住);
② 地址是十进制数值:安卓用户态地址 ≤ 0x7FFFFFFFFFFF(约 1.4×10¹⁴),远小于 Lua 双精度能精确表示的 2⁵³,不丢精度;
③ 偏移要自己分析:这组 API 只负责按地址读写,不做内存搜索 / 特征扫描,偏移来自静态分析(IDA/Ghidra)或动态调试;
④ 类型要写对:把 float 当 I32 读不会报错,只会得到「看起来像乱码」的数值,怀疑读错时用 HEX 看内存原貌;
⑤ 合规提醒:修改别的应用数据可能导致其崩溃或账号异常,请仅在自己拥有合法授权的应用 / 测试环境里用于学习研究,不要用于破解、绕过反作弊或修改他人数据。

🧯 读不到 / 改不动?按顺序排查:① 包名、模块名是否写对(模块名要精确到 .so 全名且区分大小写);② 模块是否已经加载(不少游戏要进到主界面才加载 libil2cpp.so,可用循环 + sleep() 重试);③ 基址 / 偏移是否属于当前游戏版本(更新后偏移会变,基址每次启动都不同);④ 地址是否真的可读(先用 HEX 读 16 字节确认那块内存存在);⑤ 写入返回 false 时看脚本日志里的原因(只读页 / 回读不一致 / 没有 root);⑥ 写进去立刻被改回来,说明该字段不是数据源头,需要顺着指针链往上游找;⑦ 自身进程也读不到:检查 su 授权与机型 SELinux 限制。

进程内存读写(安卓端需 Root)(3个)

GetModuleHandle获取模块基址
获取指定应用(游戏)进程里某个「模块」的加载基地址 —— 内存读写的起点,之后所有数据都用「基址 + 偏移」定位。
什么是模块:「模块」就是随 App 一起加载的 .so(ELF 动态库)文件,常见的有:
libunity.so —— Unity 引擎运行时(Unity 手游的核心模块,逆向时的第一入口)
libil2cpp.so —— Unity 的 IL2CPP 产物(C# 代码编译成的原生代码,游戏数值多半在这里)
libmono.so —— 老版本 Unity 的 Mono 运行时
libUE4.so —— 虚幻引擎(UE4 / UE5 手游)
libcocos2dlua.so / libcocos2djs.so / libcocos.so —— Cocos2d-x(Lua / JS / C++ 版)
libc.so / libm.so / libc++_shared.so —— 系统与 C/C++ 运行库(每台设备都有,适合练手)
libxxx.so —— 游戏自研模块,名字因游戏而异(必须写真实存在的文件名)
为什么必须先拿基址:模块每次启动被系统加载到哪个地址都是随机的(ASLR),逆向工具里看到的「偏移」只有加上运行时基址才是真实地址:真实地址 = 模块基址 + 偏移。
实现:按包名用 pidof 解析进程 pid(只传一个参数时用当前 App 自身进程)→ 读 /proc/PID/maps → 取该模块所有映射行里最小的起始地址(一个模块通常有 4~6 段映射:r--p / r-xp / rw-p 等,最小地址就是模块起点)。
匹配规则:只看「路径以 /模块名 结尾」的映射行,所以写 libunity.so 不会误命中 libunity.so.1 或别的路径下的同名文件。
需要 root(读的是别的进程);找不到进程 / 模块没加载 / 无 root 时返回 nil,原因会写进脚本日志。
📥 参数:
packageName (string): 目标应用包名,要和安装时的包名完全一致(可在「应用管理」里查看,或 execRoot("pm list packages | grep 关键字") 搜索)。示例:"com.example.game"、"com.tencent.tmgp.sgame"。
moduleName (string): 模块名,要精确到 .so 全名,例如 "libunity.so" / "libil2cpp.so" / "libc.so";不能只写 "unity",也不能写 "libunity.so.1"。
参数形式:GetModuleHandle("包名", "模块名") → 目标应用进程(需要 root)
GetModuleHandle("模块名") → 当前 App 自身进程(免 root 也可能成功)
不确定模块名时,先列出目标进程加载的 so(完整写法见示例 3):
local pid = execRoot("pidof com.example.game"):gsub("%s+$", "")
log(execRoot("cat /proc/" .. pid .. "/maps | awk '{print $6}' | sort -u | grep '[.]so$'"))
📤 返回:
number: 模块基址,十进制数值(例如 0x7F8B1C0000 会得到 140014000000);可直接参与算术:base + 0x1A2B3C。
打印建议 string.format("0x%X", base);若某端对十六进制报「number has no integer representation」,改用 string.format("%.0f", base)。
nil: 解析失败(失败原因同时写进脚本日志),常见原因:
· 包名写错 / 应用没在运行 → 找不到进程
· 模块名写错 / 模块还没加载(不少游戏要进到主界面才加载 libil2cpp.so)→ 进程里没找到模块
· 没有 root 或 su 授权被拒 → 读不到 /proc/PID/maps
示例
-- ===== 1) 最简用法:拿基址 + 判断失败 =====
local base = GetModuleHandle("com.example.game", "libunity.so")
if base == nil then
    log("取模块基址失败:请检查包名 / 模块名 / root 权限(原因见日志上一行)")
    return
end
log(string.format("libunity.so 基址 = 0x%X(十进制 %.0f)", base, base))

-- ===== 2) 拿到基址后按偏移读写(偏移来自逆向分析,下面这些都是编造的示例值) =====
local hpAddr = base + 0x1A2B3C
log(string.format("血量地址 = 0x%X", hpAddr))
log("当前血量 = " .. tostring(MemoryRead(hpAddr, "I32")))
MemoryWrite(hpAddr, 99999, "I32")
log("修改后血量 = " .. tostring(MemoryRead(hpAddr, "I32")))

-- ===== 3) 模块名不确定?先列出目标进程加载的所有 so =====
local pid = execRoot("pidof com.example.game"):gsub("%s+$", "")   -- 去掉末尾换行
if pid == "" then
    log("进程没在运行(或没有 root,列不出别的进程)")
else
    log(execRoot("cat /proc/" .. pid .. "/maps | awk '{print $6}' | sort -u | grep '[.]so$'"))
end
-- 输出示例(每行一个模块路径):
--   /data/app/~~xxxx/com.example.game-xxxx/lib/arm64/libunity.so
--   /data/app/~~xxxx/com.example.game-xxxx/lib/arm64/libil2cpp.so
-- 取路径最后一段文件名就是模块名:GetModuleHandle("com.example.game", "libil2cpp.so")

-- ===== 4) 一次取多个模块并缓存复用(推荐写法) =====
local Game = {}
Game.pkg = "com.example.game"
Game.unity = GetModuleHandle(Game.pkg, "libunity.so")
Game.il2cpp = GetModuleHandle(Game.pkg, "libil2cpp.so")
if Game.unity == nil then
    log("libunity.so 没取到:确认已 root、且游戏已经启动")
    return
end
log(string.format("unity = 0x%X,il2cpp = %s", Game.unity,
    Game.il2cpp and string.format("0x%X", Game.il2cpp) or "未加载"))
-- 基址在「本次运行」里不会变:一个脚本只取一次、放进表里反复用(别放在每帧循环里反复调用)

-- ===== 5) 只传一个参数 = 当前 App 自身进程(免 root 也可能成功) =====
local selfLibc = GetModuleHandle("libc.so")
log("自身进程 libc.so 基址 = " .. tostring(selfLibc))

-- ===== 6) 冒烟测试:不碰游戏,验证「读内存」这条链路通不通 =====
local libc = GetModuleHandle("libc.so")
if libc then
    -- ELF 文件头前 4 字节固定是 7F 45 4C 46(0x7F 后面跟 "ELF")
    log("ELF 魔数 = " .. tostring(MemoryRead(libc, "HEX", nil, 4)))   -- 期望输出 7F 45 4C 46
else
    log("连自身 libc.so 都取不到:shell 通道 / 权限有问题")
end

-- ===== 7) 偏移集中管理,脚本更好维护 =====
local OFF = {
    hp   = 0x1A2B3C,   -- 血量 (I32)
    gold = 0x1A2B40,   -- 金币 (U32)
    posX = 0x1A2B50,   -- 坐标 X (FLOAT)
    posY = 0x1A2B54,   -- 坐标 Y (FLOAT)
    name = 0x1A2B60,   -- 角色名 (STRING,最多 32 字节)
}
log("血量 = " .. tostring(MemoryRead(base + OFF.hp, "I32")))
log("金币 = " .. tostring(MemoryRead(base + OFF.gold, "U32")))
log(string.format("坐标 = (%.1f, %.1f)",
    MemoryRead(base + OFF.posX, "FLOAT"), MemoryRead(base + OFF.posY, "FLOAT")))
log("角色名 = " .. tostring(MemoryRead(base + OFF.name, "STRING", nil, 32)))

-- ===== 8) 多级指针:基址 → 对象指针 → 字段(逆向里最常见的形态) =====
-- 例:base + 0x30 里存着玩家对象地址,对象 + 0x18 里存着血量
local p1 = MemoryRead(base + 0x30, "U64")     -- 64 位进程指针是 8 字节用 U64;32 位进程用 U32
if p1 then
    local p2 = MemoryRead(p1 + 0x18, "U64")
    if p2 then
        log("玩家血量 = " .. tostring(MemoryRead(p2 + 0x10, "I32")))
    else
        log("第二级指针为空:对象还没创建(进游戏 / 开一局后再读)")
    end
end

-- ===== 9) 模块还没加载?循环重试 =====
local base2, tries = nil, 0
while base2 == nil and tries < 10 do
    base2 = GetModuleHandle("com.example.game", "libil2cpp.so")
    if base2 == nil then
        tries = tries + 1
        log("第 " .. tries .. " 次没取到,1 秒后重试…")
        sleep(1000)
    end
end
log("重试 " .. tries .. " 次后基址 = " .. tostring(base2))

-- ===== 10) 常见坑 =====
-- ① 基址只对「本次运行」有效:游戏重启 / 进程被杀后基址会变,脚本每次启动都要重新 GetModuleHandle。
-- ② 别把自己 log 出来的基址写死在脚本里,换台设备或重启就失效了。
-- ③ 包名不是应用名:用「应用管理」或 execRoot("pm list packages | grep 关键字") 确认真实包名。
-- ④ 模块名区分大小写:libUnity.so 和 libunity.so 不是同一个文件。
-- ⑤ 想要游戏没启动也能取基址是做不到的:模块必须先被加载进内存。
MemoryRead读取内存
按「地址」读取内存数据,支持整数 / 浮点 / 字符串 / 原始字节四种读法,返回 Lua 的 number 或 string。
最常用法:先 GetModuleHandle() 取模块基址,再加上逆向分析得到的偏移 → MemoryRead(基址 + 偏移, "I32")。
多级指针:把调用嵌套起来 —— 先读出「对象的地址」,再加偏移读出字段:
local obj = MemoryRead(base + 0x30, "U64") -- 第一级:读出指针
local hp = MemoryRead(obj + 0x18, "I32") -- 第二级:读出字段
小端字节序:安卓 / ARM 都是小端,同一份 4 字节 F4 01 00 00 按 U32 / I32 读就是 0x000001F4 = 500(低位字节在前);写入时本 API 也按小端编码,所以写入值和读回值能对上。
类型必须和内存里真实存放的一致:把 float 当 I32 / U32 读不会报错,只会得到「看起来像乱码」的数值(例如 1.5 的 float 按 I32 读是 1069547520)。怀疑读错时用 HEX 看内存原貌核对。
注意:这是「按已知地址读」,不做内存搜索 / 特征扫描;想找未知数值请先用内存搜索工具(Cheat Engine / GameGuardian 等)搜到地址,再用本 API 读取。
需要 root(读的是目标应用进程的内存);省略包名时读当前 App 自身进程。
📥 参数:
address (number): 内存地址(十进制数值,通常 = GetModuleHandle() 返回值 + 偏移)。可直接写十六进制字面量:MemoryRead(0x7F8B1C0000 + 0x1A2B3C, "I32")。
地址每个进程、每次启动都不同,不要写死。
type (string, 可选,默认 'U32'): 数据类型(大小写不敏感),完整别名表:
1 字节:I8(= S8 / SBYTE / INT8 / CHAR,-128~127)、U8(= BYTE / UINT8 / BOOL / BOOLEAN,0~255)
2 字节:I16(= SHORT / INT16,-32768~32767)、U16(= WORD / USHORT / UINT16,0~65535)
4 字节:I32(= INT / INT32,-2147483648~2147483647)、U32(= DWORD / UINT32,0~4294967295)
8 字节:I64(= LONG / INT64)、U64(= QWORD / ULONG / UINT64,读指针用这个)
4 字节:FLOAT(= F32 / SINGLE)、8 字节:DOUBLE(= F64)
变长:STRING(= STR / UTF8 / UTF-8 / TEXT)按 UTF-8 解码、遇到 0 结束符截断
变长:HEX(= BYTES / RAW)返回 "F4 01 00 00" 这种空格分隔的大写十六进制
常用对应:C 的 int / 血量 / 金币 → I32 / U32;C 的 float / 坐标 → FLOAT;C 的 double → DOUBLE;指针 → U32(32 位进程)/ U64(64 位进程)。
packageName (string, 可选): 目标应用包名。省略时沿用上一次解析到的目标进程(方便连续读同一个游戏),连上一次也没有则用当前 App 自身进程。
要切换 / 明确目标就显式传:MemoryRead(addr, "I32", "com.example.game")
length (number, 可选,默认 64): 仅 STRING / HEX 生效:读取字节数,最大 4096(超过按 4096 处理,传 0 或负数按默认 64)。
STRING 给 32 通常就够读角色名;HEX 建议用 16 的倍数,方便和内存工具里的一行对齐。
📤 返回:
number: 整数 / 浮点类型返回 Lua 数值。I8/U8/I16/U16/I32 原样返回;U32 不会变成负数(如 0xABCDEF12 → 2882400018)。
I64 / U64 超过 2^53(约 9×10¹⁵)会丢精度,只建议用来读指针数值或做比较(比较刚读出的两个地址是否相等没问题)。
string: STRING 返回文本(空字符串也算成功,说明首字节就是 0);HEX 返回大写十六进制串,如 "F4 01 00 00"。
nil: 读取失败(失败原因同时写进脚本日志),常见原因:
· 地址没映射 / 跨到未映射页(偏移算错、对象还没创建、对象已释放)
· 只读到 N 字节,不足 M 字节(地址在页尾,剩余字节不可读)
· 目标进程已退出 / 包名不对 / 没有 root 或 su 授权被拒
· 类型名写错(可用类型见参数表)
示例
-- 准备:先拿基址(下面所有偏移都是编造的示例值,请换成你自己分析出来的)
local base = GetModuleHandle("com.example.game", "libunity.so")
if base == nil then
    log("先解决基址问题(见 GetModuleHandle 的示例)")
    return
end

-- ===== 1) 四种基础读法 =====
log("血量(I32)   = " .. tostring(MemoryRead(base + 0x1A2B38, "I32")))
log("金币(U32)   = " .. tostring(MemoryRead(base + 0x1A2B3C, "U32")))
log("坐标X(FLOAT)= " .. tostring(MemoryRead(base + 0x1A2B40, "FLOAT")))
log("倍率(DOUBLE)= " .. tostring(MemoryRead(base + 0x1A2B48, "DOUBLE")))

-- ===== 2) 同一个地址用不同类型读,理解「类型 / 字节数」 =====
local addr = base + 0x2000
log("HEX   = " .. tostring(MemoryRead(addr, "HEX", nil, 8)))   -- 先看原始字节,例如 F4 01 00 00 00 00 00 00
log("U8    = " .. tostring(MemoryRead(addr, "U8")))            -- 只看第 1 个字节 → 244
log("U16   = " .. tostring(MemoryRead(addr, "U16")))           -- 前 2 字节 → 500
log("U32   = " .. tostring(MemoryRead(addr, "U32")))           -- 前 4 字节 → 500
log("FLOAT = " .. tostring(MemoryRead(addr, "FLOAT")))         -- 同样 4 字节按 float 解释 → 约 7e-43(一眼就是类型不对)
-- 结论:读出来的数值「离谱」,多半是类型挑错了;用 HEX 对一下就知道真实存放格式

-- ===== 3) 有符号与无符号的差别(同一块内存,两种读法) =====
-- 内存里 4 个字节是 FF FF FF FF 时:
--   I32 读 → -1
--   U32 读 → 4294967295
-- 内存里 1 个字节是 F4 时: I8 读 → -12,U8 读 → 244

-- ===== 4) 读字符串(默认 64 字节,遇到 0 结束符截断) =====
log("角色名 = " .. tostring(MemoryRead(base + 0x2100, "STRING")))            -- 默认读 64 字节
log("公会名 = " .. tostring(MemoryRead(base + 0x2140, "STRING", nil, 32)))   -- 只读 32 字节,更快也更安全
-- 中文没问题(按 UTF-8 解码);读到的比预期短,通常是原数据里本来就有 0 字节

-- ===== 5) 读原始字节,和内存工具对照 =====
log(tostring(MemoryRead(base + 0x2000, "HEX", nil, 16)))   -- 16 字节 = 内存视图的一行
log(tostring(MemoryRead(base + 0x2000, "HEX", nil, 64)))   -- 64 字节,看整体结构

-- ===== 6) 多级指针(实战里最常用) =====
local function readPtr(a) return MemoryRead(a, "U64") end   -- ARM64 指针 8 字节
local p1 = readPtr(base + 0x30)                            -- 第一级
if p1 then
    local p2 = readPtr(p1 + 0x18)                          -- 第二级
    if p2 then
        log("玩家血量 = " .. tostring(MemoryRead(p2 + 0x10, "I32")))
        log("玩家名字 = " .. tostring(MemoryRead(p2 + 0x20, "STRING", nil, 32)))
    else
        log("第二级指针为空:对象还没创建(回到游戏里再试)")
    end
end

-- ===== 7) 读连续数组:按步长循环 =====
-- 例:背包 10 个格子的物品 ID(U32,相邻元素间隔 4 字节)
local bagAddr = base + 0x3000
local ids = {}
for i = 0, 9 do
    ids[#ids + 1] = tostring(MemoryRead(bagAddr + i * 4, "U32"))
end
log("背包物品ID = " .. table.concat(ids, ", "))

-- ===== 8) 读结构体数组:元素大小 + 字段偏移 =====
-- 例:每个怪物 0x50 字节,血量在元素内偏移 4,等级在偏移 0x0C
for i = 0, 2 do
    local e = base + 0x4000 + i * 0x50
    log(string.format("怪物%d:血量=%s 等级=%s", i + 1,
        tostring(MemoryRead(e + 4, "I32")), tostring(MemoryRead(e + 0x0C, "I32"))))
end

-- ===== 9) 读坐标(3 个连续 float:x, y, z) =====
local x = MemoryRead(base + 0x1A2B50, "FLOAT")
local y = MemoryRead(base + 0x1A2B54, "FLOAT")
local z = MemoryRead(base + 0x1A2B58, "FLOAT")
log(string.format("坐标 = (%.2f, %.2f, %.2f)", x, y, z))

-- ===== 10) 读失败给默认值,避免脚本被 nil 打断 =====
local function readOr(addr, t, def)
    local v = MemoryRead(addr, t)
    if v == nil then return def end
    return v
end
log("金币(读失败就按 0 显示)= " .. tostring(readOr(base + 0x1A2B3C, "U32", 0)))
-- 注意:nil 参与算术会报错(attempt to perform arithmetic on a nil value),拿到值先判断再用

-- ===== 11) 轮询监控数值变化(1 秒一次,共 10 次) =====
local last = nil
for i = 1, 10 do
    local hp = MemoryRead(base + 0x1A2B38, "I32")
    if hp ~= last then
        log(string.format("[%d] 血量变化:%s → %s", i, tostring(last), tostring(hp)))
        last = hp
    end
    sleep(1000)
end

-- ===== 12) 省略包名 = 沿用上一次目标;显式传包名 = 固定目标 =====
local selfBase = GetModuleHandle("libc.so")          -- 只传一个参数 → 当前 App 自身进程
if selfBase then
    log("自身进程 ELF 魔数 = " .. tostring(MemoryRead(selfBase, "HEX", nil, 4)))   -- 7F 45 4C 46
end
log("指定包名读同一个地址 = " .. tostring(MemoryRead(base + 0x1A2B3C, "U32", "com.example.game")))

-- ===== 13) 读不到?按顺序排查 =====
local v = MemoryRead(base + 0x1A2B3C, "I32")
if v == nil then
    log("① 偏移是不是这个游戏版本的(更新后偏移会变)")
    log("② 目标进程还活着吗:" .. tostring(execRoot("pidof com.example.game")))
    log("③ 换成 HEX 读同一地址:" .. tostring(MemoryRead(base + 0x1A2B3C, "HEX", nil, 16)))
    log("④ 是否已 root 并授权 su;对象是否还没创建")
end
MemoryWrite写入内存
按「地址」写入新值:把值按小端编码成字节后写入目标进程内存,写完立刻回读校验,一致才返回 true。
支持整数 / 浮点 / 字符串 / 原始字节(HEX)四种写法;整数按类型的字节数截断(写 I32 就是低 4 字节),不做范围校验,所以类型一定要写对。
常见用途:修改血量、金币、坐标等「每次读取都直接来自内存」的数值。
重要提醒:
① 先读后写:先 MemoryRead 记下原值,再写,写完再读一次验证(也方便恢复)。
② 很多数值由服务器或每帧逻辑回写,改完立刻被覆盖 —— 这说明它不是数据源头,需要顺着指针链往上游找(见 GetModuleHandle / MemoryRead 示例)。
③ 写 STRING 不会自动补 0 结束符:新值比原值短时会残留旧尾巴;要精确控制请用 HEX(例如写 "31323300")。
④ 只读页 / 代码段 / 已释放的地址:dd 会失败或回读不一致,本 API 返回 false 并把原因写进日志。
需要 root(写的是别的进程内存);省略包名时写当前 App 自身进程(不建议随手改自身进程内存,容易崩溃)。
📥 参数:
address (number): 内存地址(十进制数值,通常 = GetModuleHandle() 返回值 + 偏移)。不要写死:游戏重启后基址会变。
value (number/string): 要写入的值,按 type 解释:
整数类型(I8/U8/I16/U16/I32/U32/I64/U64):数字 500、-1;也可写十六进制字符串 "0x1F4"(= 500)或 "500.0" 这种写法
浮点(FLOAT / DOUBLE):数字 1.5、8888.5,或数字字符串 "8888.5"
STRING:文本(按 UTF-8 编码,不会自动补 0 结束符)
HEX:十六进制字节串,支持 "9090" / "90 90" / "0x9090" / "90,90";也可直接写数字,按 4 字节小端写出(500 → F4 01 00 00)
type (string, 可选,默认 'U32'): 数据类型,类型表与 MemoryRead 完全相同(含所有别名;STRING 写文本、HEX 写原始字节)。
packageName (string, 可选): 目标应用包名。省略时沿用上一次解析到的目标进程,连上一次也没有则写当前 App 自身进程。
要切换 / 明确目标就显式传:MemoryWrite(addr, 99999, "I32", "com.example.game")
📤 返回:
boolean: true = 写入成功(dd 返回 0 且回读一致);false = 写入失败(原因同时写进脚本日志),常见原因:
· 十六进制串格式问题(长度不是偶数 / 含非法字符 / 为空)
· 值没法解析成整数或浮点数(比如写 "abc")
· 类型名写错
· dd 返回非 0:地址所在页只读 / 越界 / 目标进程已退出 / 没有 root 或 su 授权被拒
· 写入后回读不一致:被目标进程覆盖(游戏每帧刷新数值)
示例
-- 推荐模式:先读 → 判断 → 写 → 回读验证
local base = GetModuleHandle("com.example.game", "libunity.so")
if base == nil then
    log("先拿到基址再写(见 GetModuleHandle 示例)")
    return
end
local HP = base + 0x1A2B38

-- ===== 1) 标准写法:读 → 写 → 验证 =====
local old = MemoryRead(HP, "I32")
log("原血量 = " .. tostring(old))
if old == nil then
    log("连读都读不到,先排查地址 / 权限,别急着写")
    return
end
if MemoryWrite(HP, 99999, "I32") then
    log("写入成功,现在血量 = " .. tostring(MemoryRead(HP, "I32")))   -- 99999
else
    log("写入失败(原因见日志上一行)")
end

-- ===== 2) 各种类型怎么写 =====
MemoryWrite(base + 0x1A2B38, 1000, "I32")          -- 整数
MemoryWrite(base + 0x1A2B3C, -1, "I32")            -- 有符号负数(内存里是 FF FF FF FF)
MemoryWrite(base + 0x1A2B40, 0.5, "FLOAT")         -- 浮点
MemoryWrite(base + 0x1A2B48, 1.25, "DOUBLE")       -- 双精度
MemoryWrite(base + 0x1A2B50, "0x1F4", "U32")       -- 十六进制字符串写法:0x1F4 = 500
MemoryWrite(base + 0x1A2B54, "8888.5", "FLOAT")    -- 数字字符串也行
MemoryWrite(base + 0x1A2B60, "古龙", "STRING")      -- UTF-8 文本(不自动补 0,别比原值长)
MemoryWrite(base + 0x1A2B70, "90909090", "HEX")    -- 4 个 NOP 字节
MemoryWrite(base + 0x1A2B74, "00 00 00 00", "HEX") -- 用空格分隔的写法更直观
MemoryWrite(base + 0x1A2B78, "0x1F4", "HEX")       -- HEX 写数字:按 4 字节小端写出

-- ===== 3) 写坐标(3 个连续 float,步长 4) =====
local function writePos(addr, x, y, z)
    if not MemoryWrite(addr, x, "FLOAT") then return false end
    if not MemoryWrite(addr + 4, y, "FLOAT") then return false end
    return MemoryWrite(addr + 8, z, "FLOAT")
end
if writePos(base + 0x1A2B50, 100.5, 200.0, 0) then
    log("坐标已写入")
end

-- ===== 4) 批量写入:连续数组 =====
local bagAddr = base + 0x3000
local okCount = 0
for i = 0, 9 do
    if MemoryWrite(bagAddr + i * 4, 1000 + i, "U32") then
        okCount = okCount + 1
    else
        log("第 " .. (i + 1) .. " 个格子写入失败:" .. string.format("0x%X", bagAddr + i * 4))
    end
end
log(string.format("背包写入成功 %d/10", okCount))

-- ===== 5) 写完立刻看内存原貌,确认字节序 =====
MemoryWrite(base + 0x1A2B3C, 500, "U32")
log("500 写进去后内存 = " .. tostring(MemoryRead(base + 0x1A2B3C, "HEX", nil, 4)))   -- F4 01 00 00

-- ===== 6) 备份 → 临时修改 → 恢复(练习时推荐) =====
local backup = MemoryRead(HP, "I32")
if backup ~= nil then
    MemoryWrite(HP, 1, "I32")        -- 临时改成 1
    log("已临时修改,3 秒后恢复…")
    sleep(3000)
    MemoryWrite(HP, backup, "I32")
    log("已恢复为原值 " .. tostring(MemoryRead(HP, "I32")))
end

-- ===== 7) 封装:写失败自动重试(对象还没创建时特别有用) =====
local function writeRetry(addr, v, t, times)
    times = times or 5
    for i = 1, times do
        if MemoryWrite(addr, v, t) then return true end
        log("第 " .. i .. " 次写入失败,1 秒后重试…")
        sleep(1000)
    end
    return false
end
if not writeRetry(base + 0x1A2B38, 50000, "I32") then
    log("多次都失败:地址可能只读 / 已失效(对象被销毁、游戏版本变了)")
end

-- ===== 8) 一次性把结果写进日志,便于观察 =====
local function tryWrite(addr, v, t)
    local before = MemoryRead(addr, t)
    local ok = MemoryWrite(addr, v, t)
    local after = MemoryRead(addr, t)
    log(string.format("写 %s → %s :%s(%s → %s)", t, tostring(v), ok and "成功" or "失败",
        tostring(before), tostring(after)))
    return ok
end
tryWrite(base + 0x1A2B3C, 12345, "U32")

-- ===== 9) 写了「没用」?按这几种情况排查 =====
-- ① 值被游戏持续回写:改成循环里连续写几次验证
local loopAddr = base + 0x1A2B38
local writes = 0
for i = 1, 10 do
    if MemoryWrite(loopAddr, 9999, "I32") then writes = writes + 1 end
    sleep(200)
end
log("连续写入成功 " .. writes .. " 次;若界面数值仍不动,说明这不是生效的字段")
-- ② 类型写错:float 字段用 I32 写进去,读出来是「乱码」数值 → 用 HEX 对照原内存
-- ③ 地址在只读页(.rodata / 代码段):找存放该值的可写副本,或改指针链上游
-- ④ 游戏更新后偏移变化 / 基址变化:脚本每次运行都要重新取基址、重新核对偏移

♿ 无障碍方法 10个API

函数名说明示例
getCaptureMode获取当前截图模式(root / mediaprojection / unknown)local mode = getCaptureMode()
setFindColorDelay设置无障碍模式下找到颜色后的等待延时(毫秒),等待屏幕稳定setFindColorDelay(200)

🌳 节点操作 nodeLib(需要无障碍服务)

用无障碍树直接找节点、点节点,比「找图 / 找色」更稳、更快:一次遍历生成整棵界面快照(纯 Java 数据,300 ms 内连续查询复用),之后 findOne / findAll / findParentNode 都在快照上匹配,不反复走无障碍 IPC。快照最多 4000 个节点 / 60 层,超出自动截断,避免超大页面卡死脚本;click 之后缓存自动失效。

⚠️ 节点 API 依赖「古龙精灵」无障碍服务(系统设置 → 无障碍 → 古龙精灵)。未开启时查询返回 nil / 空表,日志会提示原因。

🚀 先跑通:一段完整脚本照着它改最快
不绑定具体游戏,把四步串起来:① 检查无障碍通不通 → ② 等界面出现 → ③ 点节点 → ④ 等新页面。建议先原样跑一遍(只要打出「当前界面共 N 个节点」就说明服务正常),再把文本 / id 换成你自己游戏里的值。
完整示例(可直接运行)
-- ① 检查无障碍是否连上:refresh() 返回当前界面节点总数,-1 = 取不到窗口
local total = nodeLib.refresh()
if total <= 0 then
    toast("请先开启「古龙精灵」无障碍服务")
    return
end
log("当前界面共 " .. total .. " 个节点")

-- ② 等按钮出现:最多 15 秒,第 3 个参数 true = 模糊匹配("开始" 也能命中「点击开始」)
local startBtn = nodeLib.waitFor({text="开始游戏", clickable=true}, 15000, true)
if startBtn == nil then
    log("15 秒没等到按钮:确认游戏在前台,或用 nodeLib.getXml() 看真实文本")
    return
end

-- ③ 点击:返回 true 只表示「动作已发给系统」,不代表页面一定跳转
if not nodeLib.click(startBtn) then
    log("点击失败:无障碍被系统回收,或节点已经消失")
    return
end

-- ④ 等新页面:用「新页面独有」的节点判断,不要去等刚才那个按钮消失
local bag = nodeLib.waitFor({text="背包"}, 10000, true)
if bag ~= nil then
    log("已进入主界面,背包入口中心点: " .. bag.cx .. "," .. bag.cy)
else
    log("点击后 10 秒没看到主界面:可能弹了公告,先跑下面「一键关弹窗」配方")
end

🧭 我该用哪个函数?

你要做的事用这个为什么
找一个节点,找不到就跳过nodeLib.findOne返回节点表或 nil,日常 90% 的查找都用它
把同类节点全找出来(列表、一堆「使用」按钮)nodeLib.findAll返回数组 + n,可 #list 取数量,找不到返回空表
等它出现(加载完、弹窗弹出、按钮亮起)nodeLib.waitFor内部每 100 ms 重抓一次,支持暂停 / 停止,比手写循环短且不会卡死
点一下 / 长按nodeLib.click先无障碍点击,再向上找可点祖先,最后坐标兜底,成功率最高
点文字所在的那一整行 / 整个卡片nodeLib.findParentNode先按文字找到节点,再返回父节点;点整行比点文本稳
已经知道节点在第几层第几个(日志里见过 path)nodeLib.findByIndex只沿路径下钻,不遍历整棵树,最快
界面刚变,马上要查最新结构nodeLib.refresh丢弃 300 ms 缓存并重建快照;同时可用返回值判断无障碍是否可用
不知道页面上有什么,要写选择器nodeLib.getXml导出整棵树的 XML,看真实 class / id / text

🎯 节点操作 vs 找图 / 找色

界面类型推荐方案原因
原生界面:登录、公告弹窗、菜单、背包列表、奖励结算节点操作不看分辨率、不怕换皮换色,文字改了也能用模糊匹配;速度是毫秒级
游戏内画面:Unity / Cocos 的按钮、怪物、地图找图 / 找色 / OCR这类界面用 SurfaceView 直接绘制,无障碍树里往往只有一个大节点,看不到里面的按钮
混合界面(原生外壳 + 游戏画布)先用节点找外壳按钮,画布内用找图两种方式可以混用,节点找到坐标后再用 tap(node.cx, node.cy) 也完全可以
选择器字段(selector)说明
class / package完整类名与包名,如 android.widget.TextView(完全相等或模糊匹配)
id控件 id。可只写斜杠后面的名字(id="btn_ok" 能匹配 com.xxx:id/btn_ok)
text / desc / hint文本、contentDescription、输入框提示。模糊匹配时支持 *(任意长度)与 ?(单个字符),全角 * ? 同样有效;没有通配符则按「包含」匹配
clickable / long_clickable / editable / scrollable / checkable / checked / selected / enabled / focused / visible / password布尔条件,例如 clickable=true 只找可点击节点
min_depth / max_depth只看指定深度的节点(根节点 depth=0)
空字符串值为 "" 的字段按「未填写」处理,不参与匹配(节点工具勾选出来的 id="" / package="" 可以直接用)
简写selector 也可以直接传字符串,等价于 {text=字符串},如 nodeLib.findOne("开始游戏")
content_desc / contentDescriptiondesc 的别名,从 XML 里直接抄属性名也能用
minDepth / maxDepthmin_depth / max_depth 的别名,两种写法都认
节点表字段(返回值)说明
text / desc / id / class / package / hint节点原始属性(缺失时为空字符串)
label给人看的名字:text > desc > id > class
path索引路径(如 "0|1|2"),可直接回传给 nodeLib.click / nodeLib.findByIndex
depth / index / children层级、在父节点中的序号、子节点数量
n(仅 findAll 返回的数组上有)数组长度,等价于 #list;一个都没找到时是 0
left / top / right / bottom / cx / cy / w / h / bounds屏幕坐标与尺寸,bounds 为 {left,top,right,bottom,cx,cy}
clickable / long_clickable / editable / scrollable / checkable / checked / selected / enabled / focused / visible / password节点状态(布尔)
nodeLib.click点击节点
点击 findOne / findAll / findByIndex 返回的节点表(也可直接传 "0|1|2" 路径)。
点击顺序(古龙增强,点到的概率更高):① 无障碍 ACTION_CLICK → ② 节点本身不可点时自动向上找最多 10 层可点击祖先 → ③ 还不行就按 bounds 中心点用坐标点击兜底(tap)。第 2 个参数传毫秒数可长按(先 ACTION_LONG_CLICK,失败再 longTap)。点不到返回 false 并写中文日志(无障碍没开 / 节点已失效 / 手势被系统取消)。
注意:返回 true 只代表「动作已经发给系统」,不代表界面一定跳转 —— 点完要用 nodeLib.waitFor 等新界面,或再查一次节点来确认结果。
📥 参数:
node (table|string): 节点表,或 "0|1|2" 索引路径 long_press_ms (number, 可选): 大于 0 表示长按毫秒数(默认 0 = 普通点击)
📤 返回值:boolean:true = 无障碍点击动作或坐标手势已生效;false = 节点为空 / 无障碍未连接 / 手势失败
示例
-- 场景 1:最普通的点击(先判空 → 点 → 看返回值)
local node = nodeLib.findOne({class="android.widget.Button", text="确定"})
if node ~= nil then
    if not nodeLib.click(node) then
        log("点击失败:无障碍没连上,或节点已经消失")
    end
end

-- 场景 2:点之前先确认「看得见 + 没被禁用」,避免点到隐藏节点
local btn = nodeLib.findOne({text="领取", clickable=true}, true)
if btn ~= nil and btn.visible and btn.enabled then
    nodeLib.click(btn)
else
    log("领取按钮不可见或被禁用,奖励可能还没到账")
end

-- 场景 3:无障碍点不动时的最后兜底 —— 用节点中心点坐标点击
local close = nodeLib.findOne({desc="关闭"}, true)
if close ~= nil and not nodeLib.click(close) then
    log("节点点击失败,改用坐标: " .. close.cx .. "," .. close.cy)
    tap(close.cx, close.cy)
end

-- 场景 4:长按(删除、拖动、查看详情)—— 第 2 个参数是毫秒数
local item = nodeLib.findOne({text="强化石"}, true)
if item ~= nil then
    nodeLib.click(item, 800)       -- 先 ACTION_LONG_CLICK,失败自动 longTap
end

-- 场景 5:已经知道路径时直接点(最快,省一次查找;结构变了会失败)
nodeLib.click("0|1|2")
nodeLib.findOne查找一个节点
按选择器找第一个满足条件的节点,找不到返回 nil。选择器字段见上方表格,可任意组合(全部满足才算命中)。
fuzzy_match 说明:true = 字符串支持 * / ? 通配符;没有通配符时按「包含」匹配(text="确定" 能匹配「点击确定」),且 id 忽略包名前缀。false(默认)= 完全相等。
字段别堆太多:所有条件是「与」关系,写多了容易一个都匹配不上。建议先用一个最能区分的关键字段(text 或 id)确认能找到,再逐步加 clickable / class 收窄。
性能:整个查询只遍历一遍界面树;最多 4000 节点 / 60 层;同一帧内的连续查询复用快照。
找不到时:先看下面「🩺 找不到 / 点不到」的排查清单,不要靠猜改文本。
📥 参数:
selector (table|string): 选择器表格;也可直接传字符串(等价 {text=字符串}) fuzzy_match (boolean, 可选): 是否模糊匹配,默认 false
📤 返回值:table:节点表(字段见上方「节点表字段」);nil:没有找到
示例
-- 写法 1:精确匹配(节点工具勾选出来的写法,id / class 一字不差)
local n1 = nodeLib.findOne({class="android.widget.TextView", text="名称: tmp.lr"})

-- 写法 2:模糊匹配 + 可点击(文本「包含」确定,且节点本身可点)
local n2 = nodeLib.findOne({text="确定", clickable=true}, true)

-- 写法 3:模糊 + 通配符(id 可省略包名,btn_* 能匹配 com.xxx:id/btn_ok)
local n3 = nodeLib.findOne({id="btn_*"}, true)

-- 写法 4:简写(等价于 {text="开始游戏"},完全相等,不带模糊)
local n4 = nodeLib.findOne("开始游戏")
if n4 ~= nil then
    log("找到: " .. n4.label .. " 中心点 " .. n4.cx .. "," .. n4.cy)
end

-- 写法 5:文字为空时,用 contentDescription(desc)找图标按钮
local close = nodeLib.findOne({desc="关闭"}, true)

-- 写法 6:用输入框提示(hint)定位输入框,比 class 更准
local edit = nodeLib.findOne({editable=true, hint="请输入"}, true)

-- 写法 7:同名节点太多?加深度区间,避开列表里重复的标题
local title = nodeLib.findOne({class="android.widget.TextView", text="背包", min_depth=2, max_depth=6})

-- 写法 8:找不到时别靠猜 —— 把可点击节点的 label 全打出来,看真实文本长什么样
if nodeLib.findOne({text="领取"}) == nil then
    local all = nodeLib.findAll({clickable=true})
    for i = 1, #all do
        log("候选 " .. i .. ": [" .. all[i].label .. "] class=" .. all[i].class)
    end
end
nodeLib.findAll查找所有满足要求的节点
按选择器查找所有匹配节点,返回数组表(从上到下 / DFS 顺序)。一个都没找到时返回空表(不是 nil),可以直接用 #list 或 list.n 取数量。
每个元素都是节点表(字段同上),节点表是纯数据快照,可以存起来、打印、写文件;真正点击时 nodeLib.click 会按 path 在实时树里重新定位,界面变了会按 text/id/class + 屏幕距离找回。
📥 参数:
selector (table|string): 选择器表格(字段同 findOne) fuzzy_match (boolean, 可选): 是否模糊匹配,默认 false
📤 返回值:table:节点数组,#list / list.n 为数量;找不到时为空表
示例
-- ① 找出所有可点击按钮,打印数量与位置
local list = nodeLib.findAll({class="android.widget.Button", clickable=true})
log("共找到 " .. list.n .. " 个按钮")
for i = 1, #list do
    local node = list[i]
    log(i .. ": " .. node.label .. " @ " .. node.cx .. "," .. node.cy)
end

-- ② 按屏幕位置排序(列表控件返回顺序不一定是视觉顺序时很有用)
table.sort(list, function(a, b)
    if a.top ~= b.top then return a.top < b.top end
    return a.left < b.left
end)

-- ③ 只要某一个:第 3 个 / 随机一个(「随机点一个商品」很常用)
if #list >= 3 then nodeLib.click(list[3]) end
if #list > 0 then nodeLib.click(list[math.random(1, #list)]) end

-- ④ 模糊匹配后逐个点击:点完界面可能重排,所以每次先 refresh 再按文本重找
local tasks = nodeLib.findAll({text="任务"}, true)
for i = 1, #tasks do
    nodeLib.refresh()
    local again = nodeLib.findOne({text=tasks[i].label}, true)
    if again ~= nil then
        nodeLib.click(again)
        sleep(300)
    end
end

-- ⑤ 没找到时是空表(不是 nil):直接用 # 判断,不会报错
local empty = nodeLib.findAll({text="绝不存在的文字"})
log("数量: " .. #empty)          -- 输出 0
nodeLib.findByIndex根据节点索引查找
按索引路径查找节点:写法是「根节点 → 每层在父节点里的序号」,用 | 分隔,根节点固定为 0。例如 "0|1|2" = 根节点 → 第 1 个子节点 → 第 2 个子节点;也可以传数组 {0,1,2}。
path 从哪来:任何节点表的 path 字段(用 findOne 找到一个节点后 log(node.path) 即可看到);去掉 path 最后一段就是父节点路径,所以可以用它向上取任意层祖先。
性能:不遍历整棵树,只沿路径逐层 getChild 下钻,复杂度 O(路径长度),已知位置时最快;界面变化导致路径失效时返回 nil 并写日志 —— 所以务必判空,并用 findOne 兜底。
📥 参数:
indexs (string|table): "0|1|2" 形式的索引路径,或 {0,1,2} 数组
📤 返回值:table:节点表;nil:路径无效 / 界面已经变化
示例
-- 字符串路径:根 → 第 1 个孩子 → 第 2 个孩子(根固定为 0)
local node = nodeLib.findByIndex("0|1|2")
if node ~= nil then
    log("找到: " .. node.label .. " 路径 " .. node.path)
    nodeLib.click(node)
end

-- 数组路径(完全等价)
local same = nodeLib.findByIndex({0, 1, 2})

-- 路径从哪来:findOne 找到后打印 node.path,下次就能直接下钻(最快)
local target = nodeLib.findOne({text="开始游戏"})
if target ~= nil then
    log("这个按钮的路径是: " .. target.path)

    -- 技巧:截掉 path 最后一段 = 父节点路径,可以一次向上取好几层
    local function parentPath(path)
        return string.match(path, "^(.*)|%d+$")
    end
    local p = parentPath(target.path)
    if p ~= nil then
        local row = nodeLib.findByIndex(p)      -- 父节点(通常就是整行)
        if row ~= nil then nodeLib.click(row) end
    end
end

-- 页面结构会变:路径失效时返回 nil,必须准备兜底方案
local fast = nodeLib.findByIndex("0|1|3|0")
if fast == nil then
    log("路径失效(页面结构变了),改用 findOne 重新找")
    fast = nodeLib.findOne({text="开始游戏"}, true)
end
if fast ~= nil then nodeLib.click(fast) end
nodeLib.findParentNode查找父节点
先按 selector 找到节点,再返回它的父节点——「找到文字 → 点它所在的那一行 / 整个卡片」最常用。selector 与 fuzzy_match 的用法和 nodeLib.findOne 完全一致。
为什么点父节点:很多界面的文字(TextView)本身不可点,真正接收点击的是外面那层 Cell / 卡片,点文本会「点了个寂寞」。
找到的节点已经是根节点、或没找到匹配节点时返回 nil;父节点表的 path 已是父节点路径,可直接交给 nodeLib.click。想一次向上取多层,用 path 逐段截断更可靠(见示例 ③)。
📥 参数:
selector (table|string): 选择器表格(字段同 findOne) fuzzy_match (boolean, 可选): 是否模糊匹配,默认 false
📤 返回值:table:父节点表;nil:没找到节点 / 节点没有父节点
示例
-- ① 点文字所在整行:文字本身通常不可点,父节点才是真正接收点击的区域
local row = nodeLib.findParentNode({text="银月贤者"}, true)
if row ~= nil then
    log("整行: " .. row.class .. " " .. row.bounds.left .. "," .. row.bounds.top)
    nodeLib.click(row)
else
    log("没有找到「银月贤者」")
end

-- ② 背包格子:按格子里的物品名,点到整个格子(格子可点,名字不可点)
local cell = nodeLib.findParentNode({text="强化石"}, true)
if cell ~= nil and cell.clickable then
    nodeLib.click(cell)
end

-- ③ 父节点也不可点?用 path 一层层往上爬(最多 3 层),谁可点就点谁
local node = nodeLib.findOne({text="每日任务"}, true)
if node ~= nil then
    local path = node.path
    for i = 1, 3 do
        path = string.match(path, "^(.*)|%d+$")   -- 去掉最后一段 = 父节点
        if path == nil then break end          -- 已经是根节点
        local up = nodeLib.findByIndex(path)
        if up == nil then break end
        log("向上第 " .. i .. " 层: " .. up.class .. " clickable=" .. tostring(up.clickable))
        if up.clickable then
            nodeLib.click(up)
            break
        end
    end
end
nodeLib.getXml获取节点XML
导出当前界面的完整节点树 XML(uiautomator dump 风格),用来分析页面结构、确认 class / id / text 该填什么。每个 <node> 带 index、text、resource-id、class、package、content-desc、clickable、long-clickable、checkable、checked、selected、enabled、focused、scrollable、editable、password、visible-to-user、bounds 属性。
什么时候用:写选择器之前、日志提示「没找到」时、怀疑页面是 WebView / 游戏画布时。怎么判断:如果 XML 里只有一两个铺满全屏的节点、没有任何文字,说明这是 SurfaceView / Unity 画布,节点方案拿不到里面的按钮,请改用找图 / 找色 / OCR。
节点超过 4000 个或深度超过 60 层会自动截断并在末尾注释说明;内容较长,建议写到文件而不是直接打印到日志。
📥 参数:
无参数
📤 返回值:string:当前界面的 XML;取不到窗口时为空字符串
示例
-- ① 导出完整 XML 到文件(内容很长,不要直接 log 整个字符串)
local xml = nodeLib.getXml()
if xml ~= "" then
    write_file("/sdcard/node_dump.xml", xml)
    log("节点 XML 已保存,共 " .. #xml .. " 字节")
else
    log("取不到节点树:请先开启无障碍服务")
end

-- ② 只想快速看一眼结构:截前 800 个字符打印
if xml ~= "" then
    log(string.sub(xml, 1, 800))
end

-- ③ 写选择器更快的办法:空选择器 = 全部节点,统计都有哪些控件类型
local all = nodeLib.findAll({})
local stat = {}
for i = 1, #all do
    local c = all[i].class
    stat[c] = (stat[c] or 0) + 1
end
for c, n in pairs(stat) do
    log(c .. " × " .. n)
end

-- ④ 相当于「界面文字大纲」:按层级缩进打印所有可见且有文字的节点
for i = 1, #all do
    local nd = all[i]
    if nd.visible and nd.text ~= "" then
        log(string.rep("  ", nd.depth) .. nd.text)
    end
end
nodeLib.waitFor等待节点出现
等待节点出现(古龙增强):每隔约 100 ms 重新抓一次界面并查找,找到立刻返回节点表,超时返回 nil。等价于「循环 findOne + sleep」,但内置了暂停 / 停止检查——用户点停止会立即退出,不会卡在等待里。
等加载、等弹窗、等按钮出现时用它,比手写循环更短、更稳。
注意:它只能等「出现」;等「消失」(loading 转圈结束、弹窗被关掉)要自己写 refresh + sleep 循环(见示例 ④)。timeout 上限 120000 ms,超时会在运行日志里写明等了多久、选择器是什么,方便定位。
📥 参数:
selector (table|string): 选择器表格(字段同 findOne) timeout_ms (number, 可选): 最长等待毫秒数,默认 5000,上限 120000 fuzzy_match (boolean, 可选): 是否模糊匹配,默认 false
📤 返回值:table:等到的节点表;nil:超时 / 取不到窗口
示例
-- ① 等「开始游戏」出现,最多等 8 秒(true = 模糊匹配)
local btn = nodeLib.waitFor({text="开始游戏", clickable=true}, 8000, true)
if btn ~= nil then
    nodeLib.click(btn)
else
    log("8 秒内没等到开始按钮,检查是否卡在登录页")
end

-- ② 开屏公告:先等公告出现(5 秒内没有就跳过),再点关闭
local notice = nodeLib.waitFor({text="公告"}, 5000, true)
if notice ~= nil then
    local close = nodeLib.findOne({desc="关闭"}, true)
    if close ~= nil then nodeLib.click(close) end
end

-- ③ 等列表加载完:等某个列表项出现,比 sleep(3000) 稳得多
local item = nodeLib.waitFor({class="android.widget.TextView", text="第 1 名", min_depth=2}, 12000, true)
log(item ~= nil and "排行榜已加载" or "12 秒还没加载出来")

-- ④ 等「消失」:waitFor 做不到,自己轮询(50 次 × 200ms ≈ 10 秒)
for i = 1, 50 do
    nodeLib.refresh()                   -- 轮询必须看最新界面
    if nodeLib.findOne({text="加载中"}, true) == nil then
        log("加载完成")
        break
    end
    sleep(200)
end
nodeLib.refresh刷新节点快照
丢弃当前缓存的节点快照并立即重新抓取一次,返回本次读到的节点数量(-1 表示取不到窗口,通常是无障碍服务没连上或当前是受保护页面 / 输入法窗口)。
为了更快,nodeLib 的查询会在 300 ms 内复用同一份快照;如果界面刚变化、又必须立刻拿到最新结构(例如切换页面后马上查找),先调用它。
什么时候不用调:nodeLib.click 成功后缓存会自动失效,紧接着的 findOne / findAll 已经是新界面;只有「界面不是脚本点的」(过场动画结束、倒计时自动跳转、玩家手动点过)才需要手动 refresh。
📥 参数:
无参数
📤 返回值:number:本次抓到的节点数量;-1 = 取不到窗口
示例
-- ① 当「无障碍是否可用」的探针:返回值 <= 0 就是取不到窗口
local total = nodeLib.refresh()
if total <= 0 then
    log("取不到节点树:无障碍没开 / 被系统回收 / 当前是受保护页面")
    return
end
log("当前界面节点数: " .. total)

-- ② 非脚本触发的页面变化(过场动画、倒计时自动跳)之后,先刷新再查
sleep(1500)
nodeLib.refresh()
local bag = nodeLib.findOne({text="背包"}, true)
if bag ~= nil then nodeLib.click(bag) end

-- ③ 反例:click 之后缓存已自动失效,这里不需要再 refresh
local ok = nodeLib.findOne({text="确定"}, true)
if ok ~= nil then
    nodeLib.click(ok)                     -- 点击成功后自动 invalidate
    local next = nodeLib.findOne({text="返回"}, true)   -- 直接查就是新页面
end

🍳 常见场景配方(可直接抄)

下面每段都是完整可运行的片段,改一下文本 / id 就能用。共同套路:先等 → 再判空 → 再操作 → 再校验,任何一步失败都写日志,绝不让脚本静默跳过。

① 一键关弹窗(公告 / 活动 / 更新提示)
-- 常见关闭文案挨个试:文字本身可点就点文字,否则点它的父节点(整行 / 整个按钮)
local CLOSE_TEXTS = {"关闭", "×", "知道了", "我知道了", "确定", "取消", "跳过", "以后再说"}

local function closePopup()
    for _, t in ipairs(CLOSE_TEXTS) do
        local n = nodeLib.findOne({text=t, clickable=true}, true)
        if n == nil then
            n = nodeLib.findParentNode({text=t}, true)
        end
        if n ~= nil and nodeLib.click(n) then
            log("已关闭弹窗: " .. t)
            sleep(300)
            return true
        end
    end
    return false
end

if closePopup() then
    log("弹窗处理完毕")
end
② 封装 waitAndClick(最常用的「等 → 点 → 判结果」)
-- 等出现 → 判可见 / 可用 → 点 → 返回结果;三处失败都写日志,方便定位
local function waitAndClick(selector, timeout, fuzzy)
    local n = nodeLib.waitFor(selector, timeout or 5000, fuzzy ~= false)
    if n == nil then
        log("等不到节点: " .. tostring(selector.text or selector.id or selector))
        return false
    end
    if not n.visible or not n.enabled then
        log("节点不可见 / 已禁用: " .. n.label)
        return false
    end
    return nodeLib.click(n)
end

-- 用法:最多等 15 秒,点「开始游戏」(模糊匹配)
if waitAndClick({text="开始游戏", clickable=true}, 15000, true) then
    log("已点击开始游戏")
end
③ 在可滚动列表里翻找目标
-- 先看当前屏能不能找到;找不到就滑动可滚动区域,划完必须 refresh 再查
local function scrollFind(text, maxTimes)
    for i = 1, maxTimes or 10 do
        local target = nodeLib.findOne({text=text}, true)
        if target ~= nil then return target end

        local list = nodeLib.findOne({scrollable=true})
        if list == nil then
            log("当前页面没有可滚动区域")
            return nil
        end

        -- 从列表下部划到上部 = 向下翻页,滑动距离取列表高度的 60%
        local x  = list.cx
        local y1 = list.top + math.floor(list.h * 0.8)
        local y2 = list.top + math.floor(list.h * 0.2)
        swipe(x, y1, x, y2, 400)
        sleep(500)
        nodeLib.refresh()
    end
    return nil
end

local item = scrollFind("限时礼包", 10)
if item ~= nil then
    nodeLib.click(item)
else
    log("翻了 10 次没找到「限时礼包」")
end
④ 输入文字(支持中文)
-- 先点输入框拿焦点,再用 imeLib 写入(比 input_text 稳,支持中文、不弹系统键盘)
local edit = nodeLib.waitFor({editable=true}, 5000, true)
if edit ~= nil then
    if not nodeLib.click(edit) then
        log("输入框点不中,改用坐标")
        tap(edit.cx, edit.cy)
    end
    sleep(300)

    imeLib.clearText()        -- 清空旧内容(无参数)
    imeLib.setText("古月")    -- 写入文字
    imeLib.finishInput()      -- 回车确认(部分游戏需要)
    log("已输入「古月」")
end

-- 首次使用 imeLib:需在系统设置里启用一次「古龙输入法」(详见「输入法」章节)
⑤ 勾选「同意协议」并校验结果
-- 先看当前状态:已勾选就不要重复点(再点一次会取消勾选)
local cb = nodeLib.findOne({checkable=true, text="同意"}, true)
if cb == nil then
    log("没找到勾选框,可能外层才是 checkable 节点")
    cb = nodeLib.findParentNode({text="同意"}, true)
end

if cb ~= nil and not cb.checked then
    nodeLib.click(cb)
    -- 校验:勾选状态可能晚 1~2 帧才更新,用 waitFor 等它变成 true
    local ok = nodeLib.waitFor({checkable=true, text="同意", checked=true}, 2000, true)
    log(ok ~= nil and "已勾选" or "勾选没生效:试试点父节点,或用 tap(cb.cx, cb.cy)")
end
⑥ 批量领取(带上限 + 每次刷新,避免死循环)
-- 每次只点「最上面那个」,点完立刻刷新;最多点 20 次,防止奖励无限弹 / 卡死
local got = 0
for i = 1, 20 do
    nodeLib.refresh()
    local list = nodeLib.findAll({text="领取", clickable=true}, true)
    if #list == 0 then break end

    -- 从上到下点,视觉上更自然,也避免总点同一个位置
    table.sort(list, function(a, b) return a.top < b.top end)
    if not nodeLib.click(list[1]) then
        log("点击失败,停止批量领取")
        break
    end
    got = got + 1
    sleep(400)
end
log("共领取 " .. got .. " 次")
⑦ 带重试的点击(网络慢 / 动画中)
-- 点一次不中很常见:每次先等 3 秒 → 点 → 失败睡 500ms 再来,最多 3 轮
local function clickWithRetry(selector, times, fuzzy)
    for i = 1, times or 3 do
        local n = nodeLib.waitFor(selector, 3000, fuzzy ~= false)
        if n ~= nil and nodeLib.click(n) then
            return true
        end
        log("第 " .. i .. " 次没点中,重试")
        sleep(500)
    end
    return false
end

if not clickWithRetry({text="领取", clickable=true}, 3, true) then
    log("领取失败,跳过这一步")
end

🩺 找不到 / 点不到?按这个顺序排查

  1. 先确认服务可用:local n = nodeLib.refresh(),返回 -1 或 0 → 无障碍没开 / 被系统回收 / 当前是受保护页面(支付、密码、输入法窗口),到系统设置里重新开启。
  2. 再确认节点真的存在:用 nodeLib.findAll({}) 打印界面文字大纲(见 getXml 示例 ④),看真实文本是「领取」还是「领取 」/「点击领取」,或者根本在 desc 里。
  3. 文本对但还是找不到:要么节点在屏幕外(先滑动,配方 ③),要么这个界面是 WebView / 游戏画布(节点树里只有一两个大节点),后者请改用找图 / 找色 / OCR。
  4. 找到了但点了没反应:换成 nodeLib.findParentNode 点父级(很多文字节点不可点),或用 tap(node.cx, node.cy) 直接坐标点击兜底。
  5. 实在查不出来:write_file("/sdcard/node_dump.xml", nodeLib.getXml()),对照 XML 里的 class / resource-id / bounds 重写选择器,比反复试文本快得多。
现象最可能的原因怎么改
refresh() 返回 -1无障碍服务没连上、被系统回收,或当前是受保护页面到系统设置里重开无障碍;脚本开头用返回值做一次判断
refresh() 正常,但 findOne 一直 nil文本在 desc / hint 里;或带空格、换行、前后缀改用 {desc="关闭"};或加模糊匹配 true,用 *关闭*
同名节点很多,总是点错findOne 只返回第一个命中的加 clickable / id / min_depth 收窄;或用 findAll 按位置取
明明在列表里,就是找不到目标在屏幕外,无障碍树只包含可见区域滑动后再查(配方 ③),每次滑完 refresh()
click 返回 true 但界面不变点到了不可点的文字节点,或上方有透明遮罩点父节点 / 点整行;再不行用 tap(cx, cy)
click 返回 false服务断开、节点已消失、手势被系统取消看运行日志里的中文原因;先 refresh() 再重找一次
XML 只有一两个铺满全屏的节点SurfaceView / Unity / Cocos 画布,无障碍树里没有内部元素节点方案无效,改用找图 / 找色 / OCR(见对应章节)
脚本越跑越慢在大循环里反复 findOne / getXml,每次都重建快照一次 findAll 拿数组再处理;getXml 只在调试时用

💡 古龙特色小结:① 快照 + 短缓存,连续查找不再反复走无障碍 IPC;② 通配符 * ?(全角也行)+ 无通配符按包含,中文文本更好找;③ id 可省略包名前缀;④ 点击失败自动向上找可点击祖先、再降级坐标点击;⑤ 节点表是纯数据,可保存、可打印、可写文件;⑥ 找不到 / 无障碍未开 / 点击降级都会在运行日志里说明原因。

🖥️ 全分辨率方法 12个API

在 720×1280 下开发,自动适配其他分辨率。末尾参数 >255 为目标分辨率:仅当设备分辨率匹配时才执行且不做缩放;≤255 为容差(部分函数为「容差 + 相似度比例」)。

函数名说明示例
setDesignResolution设置脚本设计分辨率,用于后续坐标/区域缩放适配setDesignResolution(720, 1280)
setAdaptTolerance设置分辨率缩放时的颜色适配容差setAdaptTolerance(20)
setAdaptSimScale设置分辨率缩放时的相似度比例setAdaptSimScale(0.7)
tap点击(支持指定目标分辨率)tap(716, 357, 2000, 1200)
find_color单点/多颜色找色(支持指定分辨率)find_color(100,200,300,250,"ff0000",10)
findMultiColor多点取色(支持指定分辨率)findMultiColor(0,0,1080,1920,"ff0000","10|0|00ff00",0,0.9,2000,1200)
findMultiColorTargets多目标多点取色(支持指定分辨率)findMultiColorTargets(0,0,1080,1920,MultiColorTargets,0,0.9,2000,1200)
findMultiColorStr带文字标识的多点取色(支持指定分辨率)findMultiColorStr(0,0,1080,1920,MultiColorStr,0,0.9,2000,1200)
getMultiColorStr获取多点标记(支持指定分辨率)getMultiColorStr(0,0,1080,1920,MultiColorStr,0,0.9,2000,1200)
findMultiColorAll查找所有匹配位置(支持指定分辨率)findMultiColorAll(0,0,1080,1920,"ff0000","10|0|00ff00",0,0.9,2000,1200)
cmpColorEx比较多点颜色(支持指定分辨率)cmpColorEx("ff0000","10|0|00ff00",0,0.9,2000,1200)
findColor多颜色列表找色(支持指定分辨率)findColor(100,200,300,250,"ff0000|00ff00",0,0.9,2000,1200)

📝 字符串方法 22个API

函数名说明示例
splitStr根据分隔符拆分字符串(分隔符按普通文本处理,不是正则/模式;+ . * | ( ) [ ] { } ^ $ ? \ 等特殊字符可直接用,支持多字符分隔符,保留空段)splitStr("a#b#c", "#") → {"a","b","c"}
splitStr("12+34+56", "+") → {"12","34","56"}
splitStr("a.b.c", ".") → {"a","b","c"}
string.lower转换为小写string.lower("AbC") → "abc"
string.upper转换为大写string.upper("AbC") → "ABC"
string.find查找子串位置string.find("Hello Lua", "Lua") → 7, 9
string.match模式匹配提取内容(有 () 捕获时返回括号里的内容,多个捕获按顺序返回多个值;中文按字符匹配,取中文用 .- 更稳)string.match("乙数比甲数多九", "多(.-)$") → "九"
string.match("name=古龙", "(%a+)=(.*)") → "name", "古龙"
string.gsub模式替换(返回「新字符串, 替换次数」两个值;替换中文与全角标点安全)string.gsub("a1b22", "(%d+)", "[%1]") → "a[1]b[22]", 2
string.gsub(" 古 龙 ", "%s+", "") → "古龙", 3
string.reverse反转字符串string.reverse("abc") → "cba"
string.format格式化字符串string.format("值=%d", 666)
string.char / string.byteASCII与字符互转string.char(97) → "a" / string.byte("a") → 97
string.len获取字节长度string.len("abc") → 3
string.rep重复字符串n次string.rep("ab",3) → "ababab"
gbkToUtf8GBK 编码字符串转 UTF-8local s = gbkToUtf8(gbkStr)
cleanText清理文本中的空格/换行/特殊字符(OCR 结果清洗常用)local t = cleanText(ocrText)

UTF-8 扩展方法(支持中文)

函数名说明示例
utf8.inStrUTF-8查找子串utf8.inStr(1, "中国人民", "人民")
utf8.inStrRevUTF-8反向查找utf8.inStrRev("中国人民人民", "人民")
utf8.strReverseUTF-8字符串反转utf8.strReverse("中国人民")
utf8.lengthUTF-8字符长度utf8.length("中国人民万岁")
utf8.left从左截取n个字符utf8.left("中国人民", 2)
utf8.right从右截取n个字符utf8.right("中国人民", 2)
utf8.mid中间截取utf8.mid("中国人民", 2, 2)
utf8.strCut移除指定范围字符utf8.strCut("中国人民", 2, 2)

📊 数组方法 7个API

函数名说明示例
# (数组长度)获取数组元素个数local len = #arr
table.concat连接数组元素为字符串table.concat({"a","b"}, ",") → "a,b"
table.insert插入元素table.insert(arr, "x") / table.insert(arr, 1, "x")
table.remove移除元素table.remove(arr, 2) → 移除第2个元素
table.sort数组排序table.sort(nums) / table.sort(nums, function(a,b) return a>b end)
table.unique数组去重local uniq = table.unique(arr)
table.merge合并两个数组/表local all = table.merge(arr1, arr2)

🔢 数学方法与类型转换 32个API

Lua 标准 math 库与类型转换函数。脚本在设备端执行(LuaJ 3.0.1),同一份脚本在「📦 代码箱调试」与已安装 APK 上结果一致。

⚠️ LuaJ 行为差异(务必看):① 设备端 LuaJ 只有双精度浮点,math.type / math.tointeger(math.maxinteger) / math.maxinteger 的结果和桌面 Lua 5.5 不同,脚本里不要写死类型或做精确相等判断;
② 随机数算法与其他 Lua 实现不同,math.random 的结果不能按固定数字断言,请用范围判断;
③ 浮点数只有约 7 位有效数字,比较小数请用区间(如 v > 0.13 and v < 0.15);需要固定小数位时先 math.floor(v * 100 + 0.5) / 100 再转字符串。

数学方法(math 库)(30个)

函数名说明示例
math.abs返回 x 的绝对值(去掉正负号)print(math.abs(-3)) → 3
math.ceil向上取整,返回大于等于 x 的最小整数print(math.ceil(3.1)) → 4
math.floor向下取整,返回小于等于 x 的最大整数print(math.floor(3.9)) → 3
math.fmod取余运算,返回 x 除以 y 的余数(结果符号跟随 x)。注意:Lua 的 % 运算符与 math.fmod 对负数的结果不同,% 的结果符号跟随 y。print(math.fmod(10, 3)) → 1
math.modf把数字拆成整数部分和小数部分,返回两个值(整数部分, 小数部分)。注意:小数部分是浮点数,直接打印两端精度不同,建议用范围比较。local intPart, fracPart = math.modf(3.14)
math.max返回参数中的最大值,可以传多个参数print(math.max(3, 7, 5)) → 7
math.min返回参数中的最小值,可以传多个参数print(math.min(3, 7, 5)) → 3
math.sqrt计算平方根(开平方),x 为负数时结果为 nanprint(math.sqrt(9) == 3) → true
math.pow计算 x 的 y 次方。写法与 x ^ y 等价,推荐直接使用 ^ 运算符(更简洁)print(math.pow(2, 10) == 1024) → true
math.exp返回 e 的 x 次方(e 约等于 2.718281828)。math.exp(0) 等于 1,是 math.log 的反函数print(math.exp(0) == 1) → true
math.log计算对数。只传 x 时是自然对数(以 e 为底);传第 2 个参数表示以该数为底,例如 math.log(x, 10) 是以 10 为底print(math.log(1) == 0) → true
math.log10以 10 为底的对数,等价于 math.log(x, 10)。常用于计算数字的位数:位数 = math.floor(math.log10(n)) + 1print(math.log10(1000) == 3) → true
math.deg把弧度转换为角度。游戏里坐标计算常用弧度,显示给用户时要转成角度print(math.deg(math.pi) == 180) → true
math.rad把角度转换为弧度。三角函数(sin/cos/tan)的参数都是弧度,所以要先转换print(math.rad(180) == math.pi) → true
math.sin计算正弦值,参数是弧度(角度要先 math.rad 转换)。返回 -1 到 1 之间的浮点数print(math.sin(0) == 0) → true
math.cos计算余弦值,参数是弧度(角度要先 math.rad 转换)。返回 -1 到 1 之间的浮点数print(math.cos(0) == 1) → true
math.tan计算正切值,参数是弧度(角度要先 math.rad 转换)。90 度(pi/2)附近数值会非常大print(math.tan(0) == 0) → true
math.asin反正弦函数,参数必须在 -1 到 1 之间,返回弧度值(可用 math.deg 转成角度)print(math.asin(0) == 0) → true
math.acos反余弦函数,参数必须在 -1 到 1 之间,返回弧度值(可用 math.deg 转成角度)print(math.acos(1) == 0) → true
math.atan反正切函数。只传 y 时返回弧度值(-pi/2 ~ pi/2);传两个参数 math.atan(y, x) 时可以判断象限,等价于 math.atan2(y, x)(坐标计算推荐用两参数形式)print(round2(math.atan(1)) == 0.79) → true(约等于 pi/4)
math.atan2根据 y、x 坐标求弧度值,返回值范围 -pi ~ pi,能正确判断象限(angle 类的坐标计算推荐用它)。等价于 math.atan(y, x)print(round2(math.atan2(1, 1)) == 0.79) → true(约等于 pi/4)
math.random生成随机数。不传参数返回 0 ~ 1 之间的随机小数;math.random(n) 返回 1 ~ n 的随机整数;math.random(m, n) 返回 m ~ n 的随机整数。⚠️ 随机数序列与桌面 Lua 不同,即使种子相同,得到的数字也不一样,脚本里请用范围判断,不要写死具体数字local r = math.random()
math.randomseed设置随机数种子。同一台设备上固定种子可以复现同一串随机数(脚本里常用固定种子让测试结果可重复),不设置时每次运行的结果都不同。⚠️ 只保证「同一台设备 + 同一种子可复现」,换成别的 Lua 实现产生的序列并不相同math.randomseed(12345)
math.pi圆周率 π(约 3.141592653589793)的常量,直接用即可。注意:安卓端 LuaJ 只有 7 位有效数字(约 3.1415927),不要用它做高精度比较print(math.pi > 3.14159 and math.pi < 3.1416) → true
math.huge无穷大常量(大于任何数字)。可用于初始化「最小值」或表示无限循环条件;-math.huge 表示负无穷print(math.huge > 1e308) → true
math.maxinteger整数最大值(9223372036854775807,即 2^63-1)。⚠️ 安卓端 LuaJ 的数字是双精度浮点,此值约为 9.223372036854776e18,做精确相等判断时请用本页示例的写法print(math.maxinteger > 0) → true
math.mininteger整数最小值(-9223372036854775808,即 -2^63)print(math.mininteger < 0) → true
math.type判断数字是整数(integer)还是浮点数(float),非数字返回 nil。⚠️ 设备端 LuaJ 只有双精度浮点,无法区分 3 与 3.0:math.type(3.0)、math.type(10 / 2)、math.type(math.maxinteger) 在设备端都按 integer 处理,脚本里不要依赖这个行为print(math.type(3)) → integer
math.tointeger把 x 转换成整数:数值上等于整数时返回整数,否则返回 nil(不会四舍五入)。数字字符串也能转换,常用于把界面输入的文字安全地变成数字。⚠️ 安卓端受浮点表示限制,math.tointeger(math.maxinteger) 会返回 nilprint(math.tointeger(3.0)) → 3
math.ult按「无符号整数」比较 m 是否小于 n。普通比较里 -1 小于 0,但无符号比较会把 -1 当成极大值,所以 math.ult(-1, 0) 结果是 false。只在处理内存地址、位运算等场景才需要print(math.ult(1, 2)) → true(普通比较也成立)

类型转换(tostring / tonumber)(2个)

函数名说明示例
tostring把任意值转换成字符串,常用于拼接日志或与文字比较。数字、布尔、nil 都能直接转;table/function 会转成地址字符串,没有实际意义。⚠️ 设备端 LuaJ 的浮点数只保留约 7 位有效数字(1/3 显示为 0.3333333),需要固定小数位时请先 math.floor(v * 100 + 0.5) / 100 再转字符串print(tostring(123)) → 123
tonumber把字符串转换成数字,转换失败返回 nil(不会抛错)。支持十进制、十六进制(0x1A)、科学计数法(1e3)和前后空格;第 2 个参数可指定进制(2 ~ 36),指定进制时按该进制解析(如 16 进制可写 ff)。⚠️ 第 2 个参数只在第一个参数是字符串时使用:tonumber(7, 10) 会返回 7(不会报错)print(tonumber("123")) → 123

📋 JSON方法 2个API

json_parseJSON解析
解析JSON字符串为Lua table
示例
local data = json_parse('{"name":"test","value":123}')
log("名称: " .. data.name)  -- 输出: test
json_stringifyJSON序列化
将Lua table序列化为JSON字符串
示例
local data = {name = "test", value = 123}
local json_str = json_stringify(data)
log(json_str)  -- 输出: {"name":"test","value":123}

🧵 多线程 14个API

在脚本里开多个线程并行干活:一边等待某个界面出现,一边做别的事。每个线程跑在独立 Java 线程上(真并行),互不阻塞。

⚠️ 注意事项:① 全局变量(_G)是所有线程共享的,多个线程同时改写同一个全局变量可能丢数据,请用 thread_lock() / thread_unlock() 保护临界区;② 停止是协作式的,子线程在调用 sleep / 取色 / 点击等 API 时才响应停止;③ 主脚本结束时所有子线程会自动退出;④ 最多同时 32 个子线程。
thread_start启动线程
启动一个脚本线程执行指定函数,立即返回线程 id;主脚本继续往下执行,实现真并行。可传函数或函数名,并支持传参。
📥 参数:
func (function|string): 要执行的函数或函数名
... (可选): 传给该函数的参数
📤 返回:
number: 线程 id(从 1 开始);参数非法或超过 32 个线程返回 nil
示例
-- 用法1:匿名函数(判断_等待服务器出现 放到子线程里跑)
local tid = thread_start(function()
    while true do
        if 综合调用.判断_服务器() then break end
        sleep(100)
    end
end)

-- 用法2:函数名(字符串),需与函数定义同名
-- 注意:脚本里的中文函数名会被自动转成英文标识符,字符串写法会找不到,
--       中文函数请改用函数引用:thread_start(判断_等待服务器出现)
local tid2 = thread_start("judgeServer")

-- 用法3:带参数 + 取返回值
local tid3 = thread_start(function(name, times)
    for i = 1, times do
        log(name .. " 第" .. i .. "次")
        sleep(100)
    end
    return name .. "完成"
end, "打怪", 3)

----主脚本代码写在这里

-- 主脚本继续做别的事,最后统一等待
thread_wait_all()
log(thread_result(tid3))
thread_wait等待线程
等待指定线程结束;不传参数(或传 nil)等价于等待全部子线程。
📥 参数:
tid (number, 可选): 线程 id,省略或 nil 表示等待全部
timeout (number, 可选): 超时毫秒,省略表示一直等待
📤 返回:
boolean: true=已结束 / false=超时或被打断;线程不存在返回 nil
示例
local tid = thread_start(function() sleep(1000) end)

if thread_wait(tid, 5000) then
    log("线程已结束")
else
    log("等待超时,线程仍在运行")
end

thread_wait()   -- 等待全部子线程
thread_wait_all等待全部线程
等待所有子线程结束(等价于 thread_wait(nil, timeout))。
📥 参数:
timeout (number, 可选): 超时毫秒,省略表示一直等待
📤 返回:
boolean: true=全部已结束 / false=超时
示例
thread_start(function() sleep(500) end)
thread_start(function() sleep(800) end)

if thread_wait_all(10000) then
    log("全部线程结束")
end
thread_stop停止线程
请求停止指定线程(协作式:被停止的线程在调用 sleep / 取色 / 点击等 API 时会尽快退出)。
📥 参数:
tid (number): 线程 id
📤 返回:
boolean: true=已发出停止请求 / false=线程不存在
示例
local tid = thread_start(function()
    while true do sleep(100) end
end)

sleep(500)
thread_stop(tid)          -- 请求停止
thread_wait(tid, 3000)    -- 等它退出
thread_stop_all停止全部线程
请求停止所有子线程(脚本被停止或结束时系统也会自动调用)。
📥 参数:
无
📤 返回:
number: 被请求停止的线程数量
示例
thread_start(function() while true do sleep(100) end end)
thread_start(function() while true do sleep(100) end end)

local n = thread_stop_all()
log("已停止 " .. n .. " 个线程")
thread_is_running线程是否运行中
判断指定线程是否仍在运行。
📥 参数:
tid (number): 线程 id
📤 返回:
boolean: true=运行中 / false=已结束或不存在
示例
local tid = thread_start(function() sleep(2000) end)
log(tostring(thread_is_running(tid)))  -- true

thread_wait(tid)
log(tostring(thread_is_running(tid)))  -- false
thread_count存活线程数
获取当前存活的子线程数量(主线程不计入)。
📥 参数:
无
📤 返回:
number: 存活线程数
示例
thread_start(function() sleep(1000) end)
thread_start(function() sleep(1000) end)
log("当前线程数: " .. thread_count())  -- 2

thread_wait_all()
log("当前线程数: " .. thread_count())  -- 0
thread_id当前线程ID
获取当前执行线程的 id(主线程固定为 0,子线程从 1 开始),可在同一函数里区分自己跑在主线程还是子线程。
📥 参数:
无
📤 返回:
number: 当前线程 id
示例
local tid = thread_start(function()
    log("我在子线程 " .. thread_id())  -- 例如 1
end)

log("我在主线程 " .. thread_id())      -- 0
thread_wait(tid)
thread_list存活线程列表
获取当前所有存活子线程 id 的数组,便于批量等待或批量停止。
📥 参数:
无
📤 返回:
table: 线程 id 数组
示例
thread_start(function() sleep(500) end)
thread_start(function() sleep(500) end)
sleep(100)

for i, tid in ipairs(thread_list()) do
    log("存活线程: " .. tid)
end
thread_result线程返回值
取线程函数执行后的返回值(支持多返回值;线程还在运行时返回 nil)。
📥 参数:
tid (number): 线程 id
📤 返回:
任意: 线程返回的值(可多个);未结束或不存在返回 nil
示例
local tid = thread_start(function()
    sleep(500)
    return "结果A", 123
end)

thread_wait(tid)
local a, b = thread_result(tid)
log(tostring(a) .. ", " .. tostring(b))  -- 结果A, 123
thread_error线程错误信息
取线程执行时的异常信息(正常结束或被主动停止的线程返回 nil),便于定位子线程报错。
📥 参数:
tid (number): 线程 id
📤 返回:
string: 错误信息;无错误返回 nil
示例
local tid = thread_start(function()
    error("子线程出错了")
end)

thread_wait(tid)
local err = thread_error(tid)
if err then log("线程报错: " .. err) end
thread_lock加锁
获取全局互斥锁(可重入)。多个线程同时修改同一个全局变量时用它对临界区加锁,避免并发写导致数据丢失。必须与 thread_unlock() 成对使用。
📥 参数:
无
📤 返回:
boolean: true
示例
计数 = 0

local function 累加()
    for i = 1, 1000 do
        thread_lock()        -- 进入临界区
        计数 = 计数 + 1
        thread_unlock()      -- 离开临界区
    end
end

thread_start(累加)
thread_start(累加)
thread_wait_all()
log("计数 = " .. 计数)       -- 2000(不加锁可能少几次)
thread_unlock解锁
释放 thread_lock() 获取的全局互斥锁,避免其它线程一直等待。
📥 参数:
无
📤 返回:
boolean: true=成功解锁 / false=当前线程未持有锁
示例
thread_lock()
共享变量 = 共享变量 + 1
thread_unlock()
thread_sleep线程休眠
线程休眠(毫秒)。与 sleep 效果一致,仅让当前线程等待,其它线程继续运行;语义上更明确,推荐在子线程里使用。
📥 参数:
ms (number): 休眠毫秒数
📤 返回:
无
示例
local tid = thread_start(function()
    for i = 1, 3 do
        log("子线程 tick " .. i)
        thread_sleep(300)
    end
end)

log("主线程不受影响,继续执行")
thread_wait(tid)

🪲 断点调试(PC 开发工具)

断点是 PC 开发工具内置的脚本调试功能:在编辑器左侧行号栏点一下,脚本运行到这一行就会暂停,方便你确认脚本到底走到哪里、循环了几次、某段逻辑有没有被执行。断点只保存在本机开发工具里,不会写进 .lua 脚本,也不会影响打包后的 APK。

⛔ 头号坑:断点不能设在空行上。注释行、function xxx() 声明行、else 行也不会暂停;单独的 end 行同样不要设(实测 while / for 的 end 行不产生行事件,个别位置的 end 偶尔命中,位置完全不可靠)。
原因:断点依赖 Lua 的 行事件,只有真正被执行到的语句行才会产生行事件。空行和注释行在编译后根本不存在,也不会被执行,所以红点虽然画上去了,脚本却永远走不到“这一行”。典型表现就是:断点明明在 while 循环里,却只停一次或者完全不停(其实是红点画在了循环体里的空行 / 注释行上)。
✅ 正确做法:把断点加在可执行语句上 —— 赋值、函数调用、log(...) / sleep(...)、if / while / for 判断行、return 等。
Lua - 断点位置对照(❌ 不会命中 / ✅ 会命中)
while true do
    -- ===== 任务循环体 =====        -- ❌ 注释行:断点不会命中
                                    -- ❌ 空行:断点不会命中
    计次循环 = 计次循环 + 1            -- ✅ 可执行语句:每次循环都命中
    log("捉妖-进行中" .. 计次循环)     -- ✅ 可执行语句:每次循环都命中
    sleep(100 * 10)                 -- ✅ 可执行语句:每次循环都命中
end                                 -- ❌ 循环的 end 行:断点不会命中(请设在循环体内的语句上)

适用环境(很重要)

运行方式断点是否生效说明
✅ 代码箱调试(「📦 代码箱调试」,或连接后按 F5;需要新版代码箱 APK) 生效 断点随 code 消息下发到设备并写入项目目录;设备端 LuaJ 用行钩子暂停,把「项目内相对路径 + 行号」回传,开发工具自动定位并高亮。旧版 APK 会忽略断点,请更新代码箱
❌ 打包 APK 后在手机或模拟器上独立运行 不生效 没有开发工具连接,断点只是开发工具里的本地标记,不会写进脚本
断点必须在「运行」之前设置。为了不影响正常脚本的执行速度,引擎只在运行前检测到断点时才启用行级调试;运行过程中新加的断点对当前这次运行无效,需要先「停止」再重新运行(代码箱调试改完断点后要重新点「代码箱调试」或 F5,断点会随代码下发)。

💡 断点只在「代码箱调试」会话里生效:改完断点后重新点「📦 代码箱调试」或按 F5,断点会随代码一起下发到设备。

添加 / 移除断点

操作效果
鼠标移到行号上 显示提示「第 N 行:单击后添加断点 (F9)」,并在行号左侧出现半透明红点
在空行 / 注释行上单击 红点能加上,但永远不会命中(这两类行不产生 Lua 行事件)。断点请改设在可执行语句上,否则脚本会表现为“断点不生效 / 循环里一直不停”
行号上单击鼠标左键 添加断点:变成实心红点,提示变为「第 N 行:单击后移除断点 (F9)」
再次左键单击(或按 F9、编辑器正文右键菜单「切换断点」) 移除该行断点
F9 在当前光标所在行添加 / 移除断点(只作用于当前编辑器标签页)
右键 / 中键点击行号 右键只是弹出断点菜单(切换本行 / 清除本文件 / 清除所有断点),不会直接切换断点;中键点击什么都不会发生(避免误操作)

💡 断点按文件路径自动保存(保存在开发工具的 config/breakpoints.json,与 editor_config.json 同目录)。下次启动开发工具重新打开同一个文件时,红点会自动恢复。

运行到断点后会发生什么

  1. 脚本暂停:状态栏显示「已暂停」,控制台输出 [断点] 已暂停: 文件名:行号。
  2. 自动定位:开发工具自动切回代码编辑器、滚动到命中的文件与行;行号左侧显示黄色三角箭头 ▶,整行淡黄高亮。
  3. 等待操作:此时可以点「继续」运行到下一个断点、点「单步」执行下一行、或点「停止」结束脚本。
  4. 执行时机:断点停在该行执行之前。也就是说,高亮行里的语句还没执行,日志里还看不到这一行的输出;按「单步 / 继续」之后它才会执行。
Lua - 断点调试示例(在带 ← 的行号上单击)
function main()
    local count = 0
    for i = 1, 5 do
        count = count + i          -- ← 在这一行(例如第 4 行)的行号上单击添加断点
        log("count = " .. count)
    end
    log("循环结束")
end

main()

运行后脚本每次循环走到这一行都会暂停;点「继续」进入下一次循环,重复 5 次后继续执行到「循环结束」。想观察变量值,用 log() 输出即可(暂停时该行还没执行,所以看到的是上一次循环的结果)。

单步执行

暂停后点「单步」,脚本只执行下一行就再次暂停,方便一行一行确认逻辑。单步依赖行级调试,因此运行前至少要有一个断点;没有断点时点单步会提示「未启用行级调试」,并按「继续」处理(不会卡在暂停状态)。

暂停后的按钮效果
▶ 继续继续运行,直到下一个断点命中或脚本结束
⏭ 单步只执行下一行,然后再次暂停(下一行也会高亮)
⏹ 停止立即终止脚本(暂停状态下也能停)

编辑代码时断点会自动跟随

  • 在断点上方插入整行(回车换行、粘贴多行)→ 断点行号自动 +N,仍然对着原来那条语句。
  • 删除整行 → 断点行号自动 -N;如果删掉的就是带断点的行,该断点自动移除。
  • 同一行内改字(不增删换行)→ 断点不动。
  • 重新加载文件(文件被外部修改后重新打开 / 重载)→ 该文件的断点会被清空,因为行号可能整体错位,避免停在错误的位置。

多文件 / require 子模块的断点

运行前,开发工具会把所有已打开标签页里的断点一起同步给 Lua 引擎。因此:

  • 主脚本里的断点可以直接用。
  • 被 require / dofile 加载的子文件(例如 子程序_打怪.lua),只要它在编辑器里打开过并设置了断点,也能暂停;命中时会自动切到那个子文件的标签页并高亮对应行。
  • 没有在编辑器里打开的子文件不会被同步(工具不知道它的断点),命中不了是正常现象——把该文件打开、加上断点再运行即可。
  • 断点按绝对路径匹配,项目目录里的相对路径 require("lib") 也能正确对应到 scripts/lib.lua。

性能说明(调试完记得取消断点)

启用断点后,设备端 LuaJ 会安装 Lua 行事件钩子(每执行一行回调一次),以保证每一行都能准确停下来。这会让脚本运行速度明显慢于正常模式(视脚本而定,通常慢数倍)。

正式跑脚本 / 长时间挂机前,请把所有断点取消(或关闭文件)再运行。没有断点时设备端不会安装行钩子,执行速度与以前完全一致。

断点不生效?按顺序排查

现象原因与处理
脚本直接跑完,完全没有暂停 ① 断点是在运行之后才加的 → 先停止,设置好断点再运行;
② 代码箱 APK 是旧版(不识别断点) → 更新代码箱后重新点「代码箱调试」/ F5;
③ 打包 APK 后在手机上独立运行 → 没有开发工具连接,不支持断点;
④ 代码箱改完断点后没有重新发送代码 → 断点只在发送 code 时下发。
断点在某一行,但脚本没停(循环里设了断点却一直不停) ① 断点设在了空行 / 注释行上(最常见原因) —— 这两类行不产生 Lua 行事件,红点永远不会命中;
② 该行不是可执行语句:function xxx() 声明行、else 行、while / for 的 end 行同样不会停(repeat ... until 的 until 条件行则会停);
③ 请把断点放到函数体 / 循环体内部的实际语句上(赋值、函数调用、log(...)、sleep(...)、if / while / for 判断行)。
断点行号偏了 / 停到了别的语句 文件被外部修改过或刚重载过。重新运行前确认红点位置;必要时重新加载文件(会清空旧断点)后重新添加。
暂停后不知道变量值 在断点前后用 log("变量=" .. tostring(变量)) 输出;当前版本暂停面板只高亮位置,不提供变量浏览器。
点「单步」没反应 本次运行没有启用行级调试(运行前没有任何断点),或当前不是暂停状态。先设置断点并让脚本暂停,再点单步。
新写的未保存脚本断点不生效 未保存的标签页没有 .lua 文件路径,运行时不会走 Lua 引擎。请先「Ctrl+S」保存为 .lua 文件,再设置断点并运行。

快捷键与操作一览

操作方式
添加 / 移除断点行号栏左键单击 · F9 · 行号栏右键「第 N 行:添加/移除断点」
清除断点行号栏右键「清除本文件所有断点」/「🗑 清除所有断点」· 菜单「运行 → 🗑 清除所有断点」(一次清空所有文件,含未打开文件的持久化记录)
运行脚本工具栏「▶ 运行」/ 菜单「运行」· F5
继续执行(到下一个断点)「▶ 继续」按钮 / 菜单「继续」· F8
暂停运行中的脚本「⏸ 暂停」按钮 · F6(启用行级调试时在下一行立即生效)
单步执行「↪ 单步」按钮 · F10
停止脚本「⏹ 停止」按钮 · F7(暂停状态下也可用)

🧩 扩展方法与插件开发 9个API

在脚本里调用 Java 类、加载 APK / .so 插件的扩展能力。这是设备端独有的能力(依赖设备上的 Java 虚拟机),「📦 代码箱调试」和装在设备上的 App 都可用。

⚠️ 使用要点:① java.lang / java.util / java.io 及 android.content/os/app/util/graphics/view/widget、org.json 已自动导入,其它包要先 import('包名.*');
② Lua 的 table 传给 Java 会转成 List(纯数组)或 Map(含字符串键),Java 的 List/Map 数组会转回 Lua 表;Lua 函数可以当 Java 接口(Runnable / OnClickListener 等)直接传参;
③ Java 回调可能在别的线程执行,回调里不要直接操作界面,建议用 Handler/runOnUiThread 或写全局变量;
④ 反射调用比纯 Lua 慢,避免在每帧循环里频繁调用;加载插件等于执行外来代码,只加载可信来源(完整说明见下方「扩展方法注意事项」)。
import导入Java类/包
导入 Java 类或整个包。这是设备端独有的能力(依赖设备上的 Java 虚拟机),「📦 代码箱调试」和装在设备上的 App 里都可用。导入后可以像调用普通函数一样调用 Java 类:静态方法、静态字段、构造对象、实例方法、JavaBean 属性都支持。不需要提前 import java.lang.*,java.lang / java.util / java.io 已默认自动导入,直接写 Math.sin(1.2) 也能用。
📥 参数:
className (string): 类名或包名;以 .* 结尾表示导入整个包,例如 'java.lang.*'
📤 返回:
table: 类对象(可用 () 或 .new() 构造实例,可直接取静态方法/字段);整包导入时返回 nil
示例
-- 整包导入(之后直接用包里的类名)
import('java.lang.*')
import('java.util.*')
import('android.content.Context')

print(Math.sin(1.2))                  -- 静态方法:0.932...
print(System.currentTimeMillis())     -- 静态方法(无参)
print(Integer.MAX_VALUE)             -- 静态字段

-- 导入具体类并用短名调用
local StringBuilder = import('java.lang.StringBuilder')
local sb = StringBuilder()            -- 构造对象(等价于 StringBuilder.new())
sb.append('hello').append(' world')   -- 支持链式调用
log(sb.toString())

-- 导入 Android 类
local Intent = import('android.content.Intent')
local ctx = LuaEngine.getContext()
local it = Intent(ctx, Context.ACTION_VIEW)   -- 也可以 Intent(Context.ACTION_VIEW)
log(tostring(it))

-- Lua table 会自动转成 Java 的 List / Map
local list = import('java.util.ArrayList')()
list.addAll({'a', 'b'})               -- table -> List
log(list.size())                      -- 2
local map = import('java.util.HashMap')()
map.putAll({name = 'gulong', ver = 1})-- table -> Map
log(map.get('name'))
LuaEngine.getContext获取Android上下文
获取 Android 上下文对象(Context,仅安卓端可用)。拿到后可以调用任何 Context 上的方法(getPackageName、getSystemService、startActivity 等),也可以配合 import 使用系统服务。
📥 参数:
无
📤 返回:
object: Android Context 对象(可直接点出方法/属性)
示例
import('android.content.Context')

local ctx = LuaEngine.getContext()
print(ctx.getPackageName())            -- 包名

-- JavaBean 属性写法也支持(等价于 getPackageName())
print(ctx.packageName)

-- 取系统服务:TelephonyManager / Vibrator / WifiManager ...
local tm = ctx.getSystemService(Context.TELEPHONY_SERVICE)
if tm ~= nil then
    local imei = tm.getImei(0)
    print(imei)
end

-- Application 级上下文(不持有 Service/Activity 引用)
local appCtx = LuaEngine.getApplicationContext()
LuaEngine.loadApk加载APK插件
加载一个 APK/DEX 插件(仅安卓端可用)。也支持加载 AutoJS 插件。参数写名字时按脚本目录查找(古龙精灵=App 内置 scripts 目录,古龙代码箱=当前工程目录),也可以写绝对路径,还可以把插件放进 APK 的 assets/plugins/ 里。插件 APK 里的 lib/<ABI>/*.so 会自动解压并作为类加载器的原生库搜索路径,因此插件内 System.loadLibrary("xxx") 能正常加载 C/C++ 实现。
📥 参数:
name (string): 插件文件名或绝对路径,例如 'app-release.apk' / '/sdcard/plugin.apk'
📤 返回:
table: 插件对象,含 loadClass(name) / loadSo(name) / listSo() / getClassLoader() / close(),加载失败时 loadClass 返回 nil(可用 lastError 查看原因)
示例
import('java.lang.*')

-- 1) 插件放在脚本目录,只写名字
local loader = LuaEngine.loadApk('app-release.apk')
if loader == nil then
    log('插件加载失败')
    return
end

-- 2) 加载插件里的类(类里的静态方法/构造/实例方法都能用)
local Plugin = loader.loadClass('com.lrapp.plugin.Plugin')
if Plugin ~= nil then
    print(Plugin.add(1, 2))          -- 静态方法
    local p = Plugin()               -- 构造对象
    print(p.getName())
else
    log('类不存在: ' .. tostring(loader.lastError))
end

-- 3) 插件带 C/C++ 实现时(APK 内 lib/arm64-v8a/libxxx.so)
loader.loadSo('xxx')                 -- 等价于 System.loadLibrary("xxx")
local NativeLib = loader.loadClass('com.xxx.NativeLib')
print(NativeLib.version())

-- 4) 没有 .so 时也可以看一下插件里有哪些原生库
for i, name in ipairs(loader.listSo()) do
    log(i, name)
end

loader.close()
LuaEngine.loadLib加载C/C++插件
加载纯 C/C++ 编写的插件(.so,无需 Java/dex,仅安卓端可用)。插件必须遵循古龙精灵插件协议:导出 gulong_plugin_init 并用 host->add_function 注册函数,协议头 gulong_plugin.h 见「C++插件开发指南」。参数支持 nil/布尔/数字/字符串(一一对应 Lua),返回值同样;复杂的 table/对象建议用 JSON 字符串传递。
📥 参数:
path (string): .so 文件名或绝对路径,例如 'libmyplugin.so' / '/sdcard/libmyplugin.so'
📤 返回:
table: 插件对象;lib.函数名(...) 直接调用,也可 lib:call('函数名', ...)、lib.close()、lib.functions 查看函数列表
示例
-- 场景1: 插件放脚本目录,直接按函数名调用
local lib = LuaEngine.loadLib('libmyplugin.so')
log(lib.name, lib.count)             -- 插件名 / 函数个数
print(lib.add(1, 2))                 -- 调用 C/C++ 函数
print(lib.hello('古龙'))              -- 字符串参数/返回值
print(lib:call('add', 3, 4))         -- 等价写法(函数名是保留字时用这个)

-- 场景2: 查看插件注册了哪些函数
for i, name in ipairs(lib.functions) do
    log(i, name)
end

-- 场景3: 复杂的表数据用 JSON 字符串传
local info = lib.parse('{"x":1,"y":2}')
log(info)

lib.close()                          -- 卸载插件(脚本结束时自动可省略)
LuaEngine.loadSo加载原生库
把指定 .so 载入进程(等价 System.load(绝对路径),仅安卓端可用)。用于「插件内部自带 Java 包装类」的场景:先 loadSo 加载 native 实现,再通过 import/loadClass 使用包装类调用 native 方法。注意:同一个 .so 在同一个进程里只能加载一次。
📥 参数:
path (string): .so 文件名或绝对路径
📤 返回:
boolean: 是否加载成功(失败抛错)
示例
-- JNI 标准写法(.so + Java 包装类):
-- 1) 载入原生库
LuaEngine.loadSo('libmyplugin.so')

-- 2) 载入声明了 native 方法的 Java 类
local NativeLib = import('com.myplugin.NativeLib')
print(NativeLib.add(1, 2))           -- 调用 native 静态方法
local obj = NativeLib()
print(obj.version())
LuaEngine.httpGetHTTP GET请求
发起 HTTP GET 请求并返回响应文本(UTF-8,仅安卓端可用)。用于加载插件后做网络请求、下载配置等,兼容懒人精灵的 LuaEngine.httpGet 写法。
📥 参数:
url (string): 请求地址
headers (table, 可选): 请求头键值对,例如 {['User-Agent'] = 'Mozilla/5.0'}
📤 返回:
string: 响应内容(失败抛错)
示例
local headers = {}
headers['User-Agent'] = 'Mozilla/5.0 (Linux; Android 13)'

local ret = LuaEngine.httpGet('https://www.baidu.com', headers)
log('长度: ' .. #ret)

-- 也可以 POST JSON(服务器返回 JSON 时用 json 解析)
local body = {device = 'gulong', ver = 1}
local resp = LuaEngine.httpPost('https://example.com/api', body)
LuaEngine.newInstance创建Java对象
按类名或类对象创建 Java 对象(仅安卓端可用)。等价于 import 后直接 Cls(...),用于动态类名(字符串)创建对象的场景。
📥 参数:
cls (string/table): 类名(完整类名或已导入的短名)或 import 返回的类对象
... (any, 可选): 构造参数
📤 返回:
object: 新建的 Java 对象
示例
-- 用字符串类名创建(适合类名来自配置的场景)
local clsName = 'java.util.ArrayList'
local list = LuaEngine.newInstance(clsName)
list.add('a')
log(list.size())

-- 用类对象创建
local File = import('java.io.File')
local f = LuaEngine.newInstance(File, '/sdcard/a.txt')
log(f.exists())

-- 取类对象(类不存在返回 nil,不会报错)
local C = LuaEngine.classOf('android.os.Build')
log(C.MODEL, C.VERSION.SDK_INT)      -- 静态字段
C++插件开发指南C++插件开发指南
纯 C/C++ 插件(.so)开发步骤与协议(仅安卓端)。【1】新建插件工程,把宿主提供的 gulong_plugin.h 复制进去(内容见下方示例的可下载说明:App 安装目录 app/src/main/jni/gulong_plugin.h)。【2】实现 int gulong_plugin_init(GulongPluginHost* host),在里边用 host->add_function(host->ctx, "函数名", 函数指针, "说明", 最少参数, 最多参数) 注册函数;函数签名固定为 int fn(void* ctx, int argc, const GulongValue* argv, GulongValue* result),返回 1 表示有返回值。【3】用 NDK 编译成 .so(必须 -fPIC;若用 -fvisibility=hidden,请给 gulong_plugin_init 加 GULONG_EXPORT)。【4】把 .so 放进脚本目录,Lua 里 LuaEngine.loadLib('libxxx.so') 即可调用。【5】参数只支持 nil/boolean/数字/字符串,复杂数据建议用 JSON 字符串;需要回调 Java 用 host->get_env() 取 JNIEnv。
📥 参数:
无(这是开发说明,不是函数)
📤 返回:
无
示例
// ===== myplugin.cpp =====
#include "gulong_plugin.h"
#include <string.h>
#include <stdlib.h>

static int l_add(void* ctx, int argc, const GulongValue* argv, GulongValue* result) {
    long long a = gulong_arg_int(argv, argc, 0, 0);
    long long b = gulong_arg_int(argv, argc, 1, 0);
    gulong_result_int(result, a + b);
    return 1;   // 1=有返回值, 0=返回 nil
}

static int l_hello(void* ctx, int argc, const GulongValue* argv, GulongValue* result) {
    const char* who = gulong_arg_string(argv, argc, 0, "world");
    // 返回值字符串要长期有效:这里用 static 缓冲
    static char buf[256];
    snprintf(buf, sizeof(buf), "hello, %s!", who);
    gulong_result_string(result, buf);
    return 1;
}

static int l_calc(void* ctx, int argc, const GulongValue* argv, GulongValue* result) {
    double x = gulong_arg_double(argv, argc, 0, 0);
    gulong_result_double(result, x * 3.14159);
    return 1;
}

GULONG_EXPORT int gulong_plugin_init(GulongPluginHost* host) {
    if (host->api_version != GULONG_PLUGIN_API_VERSION) return 1;
    host->add_function(host->ctx, "add",   l_add,   "两数相加", 2, 2);
    host->add_function(host->ctx, "hello", l_hello, "打招呼",   0, 1);
    host->add_function(host->ctx, "calc",  l_calc,  "乘圆周率", 1, 1);
    host->log(host->ctx, "myplugin loaded");
    return 0;   // 0=成功
}

GULONG_EXPORT const char* gulong_plugin_name(void) { return "myplugin"; }
GULONG_EXPORT const char* gulong_plugin_version(void) { return "1.0.0"; }

// ===== Android.mk(放到插件工程 jni/ 目录) =====
// LOCAL_PATH := $(call my-dir)
// include $(CLEAR_VARS)
// LOCAL_MODULE    := myplugin
// LOCAL_SRC_FILES := myplugin.cpp
// LOCAL_CPP_FEATURES := exceptions
// LOCAL_LDLIBS    := -llog
// include $(BUILD_SHARED_LIBRARY)

// ===== 或 CMakeLists.txt =====
// add_library(myplugin SHARED myplugin.cpp)
// target_link_libraries(myplugin log)

// ===== 编译(NDK r25 示例) =====
// cd 插件工程/jni && ndk-build
// 产物: ../libs/arm64-v8a/libmyplugin.so  -> 复制到脚本目录

-- ===== Lua 调用 =====
local lib = LuaEngine.loadLib('libmyplugin.so')
print(lib.add(1, 2))          -- 3
print(lib.hello('古龙精灵'))   -- hello, 古龙精灵!
print(lib.calc(2))            -- 6.28318
print(lib.name, lib.version)  -- myplugin / 1.0.0

-- 用不到的写法:内置的 lib.count 是函数个数
for i, fn in ipairs(lib.functions) do log(fn) end
扩展方法注意事项扩展方法注意事项
使用扩展方法前请先看这些限制:【运行位置】import / LuaEngine.* 是设备端独有的能力:✅「📦 代码箱调试」(开发工具把项目发到设备/模拟器里的代码箱 APK 执行)、✅ 装在手机/模拟器上的「古龙精灵」「古龙代码箱」直接运行;因为依赖设备上的 Java 虚拟机。【版本要求】设备端 App 需为包含扩展方法的新版本(古龙代码箱 ≥ 本次构建)。【默认导入】java.lang、java.util、java.io 及 android.content/os/app/util/graphics/view/widget、org.json 已自动导入,其它包请先用 import('包名.*')。【类型转换】Lua nil/布尔/数字/字符串 与 Java 自动互转;Lua table 传参时会按内容转成 List(纯数组)或 Map(含字符串键);Java 的 List/Map/数组返回值会转成 Lua 表(可用 ipairs/pairs 遍历,也能继续调用 Java 方法);Lua 函数可以当作 Java 接口(如 Runnable、OnClickListener)传给 Java 使用,实现回调。【线程】Java 回调可能在其它线程执行,回调里不要直接操作界面,建议用 Android 的 Handler/runOnUiThread 或把结果写进全局变量由主循环读取。【性能】反射调用比纯 Lua 慢,尽量避免在每帧循环里频繁调用。【安全】加载插件等于执行外来代码,请只加载可信来源的 apk/so;插件需要联网权限时同样受 App 权限限制。【ABI】插件 .so 必须包含设备对应 ABI(arm64-v8a / armeabi-v7a / x86_64 / x86);APK 插件内的 lib 目录会自动解压。【混淆】发布 APK 时不要混淆/裁剪被反射使用的类(默认已保留)。
📥 参数:
无(这是使用说明,不是函数)
📤 返回:
无
示例
-- ===== 推荐用法(简写) =====
-- 1) 默认包直接可用,不必 import
log(Math.max(1, 5))                     -- 5
log(tostring(java.util.Date()))          -- 当前时间

-- 2) 需要其它包先整包导入
import('android.hardware.*')
local SensorManager = import('android.hardware.SensorManager')
local ctx = LuaEngine.getContext()
local sm = ctx.getSystemService(Context.SENSOR_SERVICE)
log(tostring(sm))

-- 3) 回调:Lua 函数当接口用
local Thread = import('java.lang.Thread')
local t = Thread(Runnable(function()
    log('子线程执行中')
    -- 注意:不要在这里直接操作界面
end))
t.start()
t.join()

-- 4) 错误处理:用 pcall 包住扩展调用
local ok, err = pcall(function()
    return import('no.such.Class')
end)
if not ok then log('导入失败: ' .. tostring(err)) end

📄 完整脚本示例

以下是一个完整的游戏挂机脚本示例,演示了触控、找图、取色、OCR、用户设置等核心功能的使用。

Lua - 梦幻逍遥脚本(核心逻辑摘录)
-- ============================================================
-- 梦幻逍遥脚本 - 核心逻辑示例
-- 演示:触控、取色、OCR、用户设置获取
-- ============================================================

-- 加载子模块
dofile("settings_fields.lua")
dofile("server_api.lua")

local 打怪调用 = require("子程序_打怪")
local 综合调用 = require("子程序_综合调用")
local 组队调用 = require("子程序_组队")

-- 判断是否正在战斗中(取色方式)
function 判断_人物停止了()
    -- 取色判断:如果画面中某特征颜色不变,说明人物停在原处
    local colorStr = get_color(451, 585)
    local r, g, b = tonumber("0x"..colorStr:sub(1,2)), tonumber("0x"..colorStr:sub(3,4)), tonumber("0x"..colorStr:sub(5,6))
    
    sleep(900)
    
    local colorStr2 = get_color(451, 585)
    local r1, g1, b1 = tonumber("0x"..colorStr2:sub(1,2)), tonumber("0x"..colorStr2:sub(3,4)), tonumber("0x"..colorStr2:sub(5,6))
    
    -- 颜色不变 = 人物已停止
    if r == r1 and g == g1 and b == b1 then
        return true
    end
    return false
end

-- 主函数
function main()
    log("==================================================")
    log("  古龙精灵 - 梦幻逍遥")
    
    -- 获取用户设置
    local settings = get_user_settings()
    
    -- ⚠️ 中文key必须用方括号语法
    local auto_fight_自动饱食度 = settings["auto_fight_自动饱食度"]
    local auto_自动巡山 = settings["auto_自动巡山"]
    local auto_自动除妖 = settings["auto_自动除妖"]
    local auto_fight_自动打怪 = settings["auto_fight_自动打怪"]
    
    -- 主循环
    while true do
        综合调用.操作_关闭异常提示弹出()
        
        if auto_自动除妖 then
            log("⚔️ 自动除妖开始!")
            主程序调用.主程序_除妖()
        end
        
        if auto_自动巡山 then
            log("⭐ 自动巡山开始!")
            主程序调用.主程序_巡山()
        end
        
        if auto_fight_自动打怪 then
            log("⚔️ 自动打怪开始!")
            主程序调用.主程序_除妖()
        end
        
        sleep(1000)
    end
end

main()

抓取工具使用技巧

在 PC 开发工具的工具 → 抓取窗口中:

  • 右键点击画面 → 取色:自动生成 get_color(x, y) 代码
  • 左键框选区域:自动生成 findMultiColor 代码(多点取色)
  • Ctrl+左键自定义取色:手动添加取色点,生成多点取色代码
  • 右键 → ONNX查找文字:自动生成 ocr_find_text_local 代码
  • 左键框选 + OCR模式:自动生成 ocr_recognize_local 代码

代码插入方式

在开发工具的右侧 API参考面板和UI设计面板中,点击任意代码即可自动插入到编辑器并复制到剪贴板。

✅ 已复制到剪贴板
🎨 本手册支持阅读主题
点击右上角 主题 按钮,可在「🌙 暗夜(默认)」与「☀️ 暖色(护眼)」之间切换。 选择会自动保存在本机;暖色主题采用米黄纸面 + 深棕文字,适合长时间阅读。