ios脚本开发

苹果群控发短视频脚本怎么写?从环境检查到发布的完整代码步骤

先确定用哪条链路(代理 / USB HID / 蓝牙 / OTG 对应模块不同),再按七步拆解逐段给代码——环境检查、视频进相册、节点查询与超时等待、三种定位怎么选、发布后校验。

约 20 分钟更新于

苹果群控是一套「一台电脑管多台 iPhone」的工程做法——画面集中看、任务按组发、结果统一收,绕开越狱。

发片子卡人,先看清一条视频怎么发出去

老陈做本地生活,六个短视频账号每天各发一条探店视频。卡人的地方不是剪片子,是发片子:素材提前剪好,第二天挨个账号传到相册、打开 App、找发布入口、选视频、写标题、挂话题、点发布、等传完、再确认成功,一条六分钟,六个号四十分钟,加上中途被打断、某个号卡在「上传中」要重来,实际每天接近四小时。他想把这段变成「早上点一下就好」。

写代码前先把流程拆开,标清楚每一步用什么能力:

步骤 做什么 用什么
1 视频进相册 工具栏「上传视频」或 utils 相册接口
2 打开目标 App 中控里右键运行,或 HTTP 接口 openApp
3 等界面真的就绪 节点抓取 + 按文案查询
4 进发布页 节点点击,或坐标点击
5 选相册里的视频 节点点击
6 填标题和话题 输入法接口 imeApi
7 点发布并校验 节点点击 + 二次查询 / 截图

七步里第 1、3、7 步最容易出问题。这套流程跑通之后,多账号运营 真正省时间的不是单条变快,而是「六个号都发到了」不再需要你去确认。

苹果群控执行脚本时的界面,可以按设备查看每一步的执行状态

先选链路,再搭好脚本骨架

同样的业务逻辑,在不同链路上要调用不同的事件模块:

链路 事件模块 系统要求 需要什么
代理模式 agentEvent iOS 13+ 签名 IPA
USB HID usbHidEvent iOS 17+ 一根数据线
蓝牙 BLE bleEvent iOS 17+ ESP32 开发板
OTG HID otgEvent iOS 17+ 开发板 + 转接头

USB HID 和蓝牙 BLE 都要求 iOS 17 及以上,低于 iOS 17 请用代理模式。四个模块的方法名完全一致,都叫 clickPoint、swipeToPoint、doubleClickPoint 这些,业务逻辑写一遍、换链路只改模块名。不管走哪条链路,开头都是这套骨架:

function main() {
    logd("检查自动化环境...");
    if (!autoServiceStart(3)) {
        logw("自动化服务启动失败,无法执行脚本");
        exit();
        return;
    }

    // 节点抓取参数,脚本开头设一次就够
    setFetchNodeParam({
        "labelFilter": "2",       // 只取有 label 的节点
        "visibleFilter": "2",     // 只取 visible=true 的
        "maxDepth": "20",         // 层级,越少越快,建议 1-500
        "excludedAttributes": "visible,selected,enable,accessible"
    });

    // ... 业务代码

    logd("脚本执行结束");
}

// 环境检查:官方示例里的写法,直接用
function autoServiceStart(time) {
    for (let i = 0; i < time; i++) {
        if (isServiceOk()) {
            return true;
        }
        let started = startEnv();
        logd("第" + (i + 1) + "次启动服务结果: " + started);
        if (isServiceOk()) {
            return true;
        }
    }
    return isServiceOk();
}

main();

autoServiceStart 是官方示例给出的辅助写法:循环调用 startEnv() 并检查 isServiceOk(),最多试 time 次。这段骨架是代理模式的写法,走 USB HID 链路要换成 HID 的会话启动:

let r = usbHidEvent.sessionStart(true);   // 参数是"是否尝试增强兼容模式",默认 true
if (!(r == null || r === "")) {
    logw("开启 HID 会话失败: " + r);
    return;
}
r = usbHidEvent.setScreenSize(1170, 2532);  // 坐标换算依赖这个

setScreenSize 在 HID 链路上是必须的,参数要和你投屏、截图的分辨率一致,换设备或横竖屏切过都要重设,否则坐标会偏。setFetchNodeParam 的 excludedAttributes 是提速关键,设备多时累积差异明显。

五步把一条视频发出去

第一步,把视频放进相册。代理模式 / 脱机主程序(iOS 15+)用投屏工具栏的上传视频按钮,或用「选择文件夹」批量上传。HID 链路(没装代理 IPA)必须在手机上先建一条快捷指令:新建一条指令、名字叫 iOS USB 插入视频或图片到相册;到手机「设置 → 辅助功能 → 键盘与键入 → 全键盘打开 → 命令」下拉找到「快捷指令 → iOS USB 插入视频或图片到相册」;之后用 HID 快捷键发送上传。少做这一步视频根本进不了相册。相册接口:

// 首次使用先请求相册权限(会弹窗,需要人工点一次允许)
utils.requestPhotoAuthorization();

// 需要清空时(注意:有确认弹窗,需要模拟点击删除按钮)
utils.deleteAllVideos();
utils.deleteAllPhotos();

两个行为要知道:这些调用是异步的,不要依赖返回值(文档写了「防止卡住不能模拟点击,请忽略返回值」);清空操作不可恢复,且调用时会弹确认框。

第二步,打开 App 并等界面真的就绪。「打开 App」和「界面可用」是两件事,图标点开了、启动页还在转,找发布按钮必然找不到。打开动作可以中控里选中设备右键执行脚本,也可以走 HTTP 接口和外部系统对接:

POST http://127.0.0.1:8019/openapi/openApp
{"deviceId": "设备 ID", "bundleId": "目标 App 包名"}

端口 8019 是中控地址,官方给了 Python、Node.js、cURL、C# 四种示例。打开之后不要写固定等待,按节点文案去查:

const BUNDLE_ID = "com.example.shortvideo";   // ← 换成你的目标 App 包名

// 打开 App(中控里右键执行时可以省略这段,用界面操作代替)
// 查询发布入口是否出现,这一步同时起到"等加载"的作用
releaseNode();   // 先释放上一轮节点数据
lockNode();      // 锁定当前界面节点

let entry = labelMatch(".*发布.*").getOneNodeInfo(15000);  // 最多等 15 秒
if (!entry) {
    logw("15 秒内没找到发布入口,可能还在加载,也可能界面变了");
    image.captureFullScreen();   // 截图留证
    exit();
    return;
}

三个关键点:节点查询是链式写法 labelMatch("正则").getOneNodeInfo(超时毫秒),可用筛选器有 id、idMatch、label、labelMatch、name、nameMatch、type、typeMatch、value、valueMatch、xpath 以及 visible、enable;getOneNodeInfo(超时) 就是天然的元素等待,元素提前出现会立刻返回;每次查询前先 releaseNode() 再 lockNode(),不释放可能拿到上一屏的节点。

第三步,找「发布」按钮,三种定位方式怎么选:

方式 用什么 什么时候用 什么时候用不了
节点定位 labelMatch(...)、id(...) 界面元素有文字或可读属性 HID / 蓝牙 / OTG 链路不支持节点选择器
图色找图 image.findImage、模板匹配 入口是图标、没有文字 界面样式一改就要重做模板
视觉定位 工作流里的 VLM 定位步骤 界面树抓不到,需要“看得懂” 需要配大模型

节点定位优先,它按文字找,界面挪位置不影响:

let btn = labelMatch("发布").getOneNodeInfo(5000);
if (btn) {
    btn.clickCenter();     // 直接点节点中心
} else {
    logw("没找到发布按钮");
}

节点对象上可用的(官方文档列出的):id、xpath、label、name、type、value、bounds、index、depth、visible、enable,以及方法 clickCenter、clickRandom、parent、child、allChildren、siblings、previousSiblings、nextSiblings。节点找得到但点不动时用坐标兜底:

let nd = labelMatch("发布").getOneNodeInfo(5000);
if (nd) {
    // 用节点自己的坐标点,比自己量坐标稳
    clickPoint(nd.bounds.centerX(), nd.bounds.centerY());
}

bounds.centerX() / centerY() 是从节点直接算出来的坐标,换机型、换分辨率都不用改。用硬坐标的话,注意它和投屏、截图是同一套,分辨率变了或横竖屏切过必须先重设屏幕尺寸。

第四步,填标题和话题:

// 输入标题
if (imeApi.isOk()) {
    let r = imeApi.input("今天的探店:巷子口那家面馆");
    logd("输入结果: " + r);
}

// 话题建议走剪贴板,长文本更稳
imeApi.setClipboard("#探店 #本地生活 #今日推荐");
imeApi.paste();

// 注意:这一步不能省:收起键盘
imeApi.dismiss();

imeApi.dismiss() 必须写:输入完标题键盘会一直占着下半屏,而发布按钮往往在键盘下面,不收起键盘点击会落到键盘上、按钮根本没被点到,这个失败不报错、只表现为「一直没发出去」。imeApi.input(content) 空字符串代表输入不成功、非空代表实际输入内容,不能只判断 != null。HID 链路输入是另一套:

usbHidEvent.inputText("今天的探店");     // 走剪贴板粘贴
usbHidEvent.typeText("Today's store");   // 需要键盘打字时用,遇非英文会自动粘贴

往手机打英文如果总多出空格,去手机「设置 → 通用 → 键盘」关掉智能标点。

第五步,点发布并确认真的发出去了:

let pub = labelMatch("发布").getOneNodeInfo(3000);
if (pub) {
    pub.clickCenter();
} else {
    logw("发布按钮不见了,可能已经被点过,或者界面变了");
    exit();
    return;
}

// 等发布结果,不要用固定 sleep
releaseNode();
lockNode();
let ok = labelMatch(".*发布成功.*").getOneNodeInfo(20000);

if (!ok) {
    logw("20 秒内没看到成功提示,需要人工确认");
    image.captureFullScreen();   // 截图存证,事后好排查
} else {
    logd("发布成功");
}

校验不能省,短视频发布失败经常是静默的:不弹错误、停在草稿箱、或一直显示「上传中」然后悄悄停了,不写校验可能两天后才发现某个号少发了好几条。需要 OCR 时,工作流里可以用 ocr_screen_auto / ocr_screen_no_auto,引擎可选 paddleOcrNcnnV5(默认)、v5、v4、ocrLite,脚本里对应 image.captureFullScreen() 截图后再走 OCR 接口。

实时日志界面,可以查看每台设备每一步的执行输出

串成完整脚本、多台并跑和排错

把前面几段合成一份,直接复制到 IDEA 里改包名就能跑:

const BUNDLE_ID = "com.example.shortvideo";   // ← 改成你的目标 App
const TITLE = "今天的探店:巷子口那家面馆";
const TOPICS = "#探店 #本地生活";

function main() {
    logd("=== 短视频发布开始 ===");

    if (!autoServiceStart(3)) {
        logw("自动化服务启动失败");
        exit();
        return;
    }

    setFetchNodeParam({
        "labelFilter": "2",
        "visibleFilter": "2",
        "maxDepth": "30",
        "excludedAttributes": "visible,selected,enable,accessible"
    });

    // 1. 等发布入口出现(同时充当加载等待)
    if (!waitAndClick(".*发布.*", 15000)) {
        logw("找不到发布入口,退出");
        shotForDebug();
        exit();
        return;
    }

    // 2. 选中相册里最新的那条视频
    if (!waitAndClick("相册", 8000)) {
        logw("进不了相册");
        exit();
        return;
    }
    clickPoint(195, 640);          // ← 第一条视频的位置,按自己屏幕量
    sleep(800);
    if (!waitAndClick("下一步|确定", 8000)) {
        logw("视频没选上");
        exit();
        return;
    }

    // 3. 填标题
    if (imeApi.isOk()) {
        imeApi.input(TITLE);
    }
    imeApi.setClipboard(TOPICS);
    imeApi.paste();
    imeApi.dismiss();              // 必须收起键盘

    // 4. 发布
    if (!waitAndClick("发布", 8000)) {
        logw("发布按钮点不到");
        exit();
        return;
    }

    // 5. 校验
    releaseNode();
    lockNode();
    if (labelMatch(".*发布成功.*").getOneNodeInfo(20000)) {
        logd("=== 发布成功 ===");
    } else {
        logw("没看到成功提示,需人工确认");
        image.captureFullScreen();
    }

    releaseNode();
}

// 等节点出现并点击,返回是否成功
function waitAndClick(labelRegex, timeout) {
    releaseNode();
    lockNode();
    let nd = labelMatch(labelRegex).getOneNodeInfo(timeout);
    if (!nd) {
        return false;
    }
    clickPoint(nd.bounds.centerX(), nd.bounds.centerY());
    return true;
}

function autoServiceStart(time) {
    for (let i = 0; i < time; i++) {
        if (isServiceOk()) return true;
        startEnv();
        if (isServiceOk()) return true;
    }
    return isServiceOk();
}

function shotForDebug() {
    image.captureFullScreen();
}

main();

waitAndClick 把「等元素 → 点它」包成一个函数,主流程只剩五行;点击优先用节点坐标,只有选视频列表那步用硬坐标(列表项通常没有可读文案);每一步失败都 exit() 加日志,短视频发布是长链路,宁可早退。

一台跑通后,六个号一起发有三种方式。中控分组批量执行最省事:把 iec 放进中控左下角「脚本目录」,右键 → 刷新 → 右键脚本 → 执行脚本,就会对当前选中的设备批量执行,设备多的话先在分组栏分好组再对分组执行——注意执行脚本需要 USB 设备授权、投屏需要 USB 投屏授权,两者不是同一个。HTTP 接口下发适合把发布流程接进自己的后台。工作流 + 定时按点触发,硬约束是同一台设备同一时间只能跑一个任务、批量提速靠设备数量而不是单台并发,并发上限可在设置里调(单任务最大设备数),改完需要重启。排期别把六个号排在同一分钟,时间分布太整齐本身就不是正常特征。

排错按「现象 → 先查什么」:

现象 先查什么
鼠标点手机没反应 设备小屏或设备列表上右键 → USB HID → 重建 USBHID 会话
改过投屏方式后画面不正常 重启手机再试
电脑往手机打英文多出空格 手机「设置 → 通用 → 键盘」关掉智能标点
界面变了、节点找不到 先截图看当前停在哪一屏,再按新文案改匹配
多台一起跑变慢 设置里调大单任务最大设备数,改后重启
想确认实际连了几台 浏览器打开 127.0.0.1:8020/devapi/devicenum
内存一直往上涨 调中控的 ioscenter.vmoptions,或访问 127.0.0.1:8020/devapi/gc 手动回收
要看是哪一步出的错 执行历史 + 实时日志,按设备逐步看

边界说清楚:脚本负责把做好的内容按时发出去(传素材、开 App、选视频、填字段、点发布、校验),这六件事它比人稳;选题、文案、封面、评论区互动留给人,这几件事的价值恰恰在于每次都不一样。别用脚本刷数据,发布自动化解决的是效率不是数量。

老陈早上做的事变成:看一眼前一天的发布结果,确认六个号都发出去了,再花时间在选题和文案上。

脚本执行需要 USB 设备授权、投屏需要 USB 投屏授权,两个都要有。这套 iOS 群控 的脚本在一台上跑通之后,想先看看多台怎么排程,可以读苹果群控短视频排程;一台电脑能带多少台的实测数据,看苹果群控带机量。

iEasyClick:手机自动化脚本与苹果群控方案站,覆盖安卓免 root、iOS 免越狱、鸿蒙 Next,提供脚本开发教程、中控投屏与批量运维方案。官网 ieasyclick.net

想要真实跑起来?

本文介绍的方案均可基于 EasyClick 能力在 iEasyClick 落地。官网提供完整文档、开发工具与自动化产品,免费体验。

访问 iEasyClick 官网 →