六个账号,每天四小时
老陈做本地生活,手上六个短视频账号,每天各发一条探店视频。这个规模的短视频矩阵,卡人的地方从来不是剪片子,是发片子。
流程是这样的:素材提前剪好放电脑里,第二天早上挨个账号操作——传到手机相册、打开 App、找到发布入口、选视频、写标题、挂话题、点发布、等它传完、再确认有没有成功。一条视频六分钟,六个账号下来四十分钟上下。加上中途被打断、某个号卡在“上传中”要重来,实际每天耗掉的时间接近四小时。
他想把这段变成“早上点一下就好”。
这篇文章就是把这条链路写成脚本的完整过程。每一步都给代码,并且注明官方文档里的函数签名和返回值约定——因为写这种脚本最耗时间的不是逻辑,是搞清楚每个函数到底返回什么。
先说一个前提:文中所有函数名都来自 EC iOS 版官方文档,逐条核对过。写 iPhone自动化 脚本最耗时间的其实不是逻辑,是搞清楚每个函数到底返回什么。但目标 App 的包名、界面结构、坐标需要你换成自己的,这部分我没法替你定。
下面这套 iOS自动化脚本 的写法,抖音、快手、视频号、TikTok 这类短视频 App 都适用,差别只在包名和界面元素上——你把这两处对着自己的目标 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+ | 开发板 + 转接头 |
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 次。这个模式比自己写一个 sleep(5000) 再赌它能起来可靠得多。
注意这段骨架是代理模式的写法。 走 USB HID 链路的话,开头要换成 HID 自己的会话启动:
let r = usbHidEvent.sessionStart(true); // 参数是"是否尝试增强兼容模式",默认 true
if (!(r == null || r === "")) {
logw("开启 HID 会话失败: " + r);
return;
}
r = usbHidEvent.setScreenSize(1170, 2532); // 坐标换算依赖这个
setScreenSize 这一步在 HID 链路上是必须的,它的参数要和你投屏、截图的分辨率一致;换了设备或者横竖屏切过,都要重新设一次,否则所有坐标都会偏。蓝牙和 OTG 链路同理,用的是各自模块的会话接口。
setFetchNodeParam 的 excludedAttributes 是提速的关键。文档原话是「可以增加抓取速度」——设备多的时候,少抓几个用不上的属性,累积下来差异很明显。
第一步:把视频放进相册
这是第一个坑,因为两条链路的做法完全不同。
代理模式 / 脱机主程序(iOS 15+)
用投屏右侧工具栏的上传视频按钮,或者在小屏幕上右键 → 图片视频。可以用「选择文件夹」批量上传,一次把当天要发的片子都推进去。
HID 链路(没装代理 IPA)
官方文档给了完整步骤,必须在手机上先建一条快捷指令:
- 在「快捷指令」App 里新建一条指令,名字叫 iOS USB插入视频或图片到相册
- 到手机「设置 → 辅助功能 → 键盘与键入 → 全键盘打开 → 命令」,下拉找到「快捷指令 → iOS USB插入视频或图片到相册」
- 之后用 HID 快捷键发送上传
少做这一步,HID 链路上视频根本进不了相册——很多人卡在这里以为是脚本问题。
相册相关的脚本接口
// 首次使用先请求相册权限(会弹窗,需要人工点一次允许)
utils.requestPhotoAuthorization();
// 需要清空时(注意:有确认弹窗,需要模拟点击删除按钮)
utils.deleteAllVideos();
utils.deleteAllPhotos();
两个必须知道的行为:
这些调用是异步的,不要依赖返回值。 文档明确写了「防止卡住不能模拟点击,请忽略返回值」。
清空操作不可恢复。 而且调用时会出现确认弹窗,需要你模拟点击删除按钮——这也是为什么它不能直接无条件用。试跑前一定确认设备和路径。
第二步:打开 App,并等到界面真的就绪
“打开 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(超时) 里的超时就是你天然的元素等待。 传 15000 就是“最多等 15 秒”,比 sleep(15000) 好得多——元素提前出现它会立刻返回。
每次查询前先 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 这个封包。 整套流程里“等元素 → 点它”出现了五次,包成一个函数之后主流程只剩五行,改超时、改点击方式都只改一处。
点击优先用节点坐标 (nd.bounds.centerX()),只有选中视频列表那一步用了硬坐标——因为列表项通常没有可读文案。那一步的坐标记得按自己屏幕量。
每一步失败都 exit() 加日志。 短视频发布是长链路,中间任何一步静默失败,后面都会以奇怪的方式报错。宁可早退。
多台设备怎么跑
脚本在一台上跑通了,下一步是六个号一起发。矩阵号运营 最费力的一直是这件事——一条一条点,点到第四五个就开始糊弄。三种方式按投入排序:
方式一:中控分组批量执行(最省事)
把编译好的 iec 文件放进中控左下角「脚本目录」,右键 → 刷新 → 右键脚本 → 执行脚本,就会对当前选中的设备批量执行。设备多的话,先在左侧分组栏把手机分好组,再对分组执行。
这里有个容易搞混的地方:执行脚本需要的是 USB 设备授权,投屏需要的是 USB 投屏授权,两者不是同一个授权。买错授权是这个环节最常见的坑。
方式二:HTTP 接口下发 前面提过的 POST /openapi/openApp,配合其他动作接口,可以让外部系统驱动多台设备。适合把发布流程接到自己的后台或项目管理工具里。
方式三:工作流 + 定时 把脚本逻辑做成工作流,配合定时规则按点触发。要注意一条硬约束:同一台设备同一时间只能跑一个任务,多台才能并行。所以批量提速靠的是设备数量,不是单台并发。
另外并发上限可以在设置里调(单任务最大设备数),改完需要重启。
排期上有个建议:六个号不要排在同一分钟。手机端发内容,时间分布太整齐本身就不是正常特征。错开几分钟,或者按分组错峰。
排错清单
按“现象 → 先查什么”整理,都是文档里给过的:
| 现象 | 先查什么 |
|---|---|
| 鼠标点手机没反应 | 设备小屏或设备列表上右键 → 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 落地。官网提供完整文档、开发工具与自动化产品,免费体验。