跳到主要内容

自定义脚本

内置脚本覆盖了常见流程。当你需要的功能它们没有提供时——换一个执行顺序、操作它们没触及的界面,或者自动化 TikTok / Instagram 以外的应用——你可以用任意语言自己写,由 TikMatrix 把手机交给你。

使用要求

授权要求

自定义脚本需要 Pro、Team 或 Business 套餐。 Starter 套餐不提供此功能。

套餐的设备数同时也是并发上限:Pro 套餐(20 台设备)最多同时驱动 20 台手机,无论走内置任务、自定义脚本,还是两者混用。

两种运行方式

独立运行

你自己运行程序,TikMatrix 只负责把设备借给你。

from tikmatrix import TikMatrix

client = TikMatrix()

for device in client.devices():
if device["busy"]:
continue
with client.device(device["serial"], label="my crawler") as d:
d.press("home")
print(d.info())

适合一次性任务、数据采集,以及任何你想用自己的调度器来跑的场景。

托管运行

把程序注册到 TikMatrix,它就变成一个普通任务,可以使用任务队列、按套餐限制并发、自动重试、任务日志和定时计划模板。TikMatrix 会在启动你的程序之前先占用设备,并把租约 ID 通过环境变量传进去。

from tikmatrix import TikMatrix

with TikMatrix.from_env() as d: # 设备已经占用好了
d.click(text="Log in")
print("done") # 这一行会出现在任务日志里

适合需要重复执行、定时执行或跨大量设备执行的场景。

该选哪一种

独立运行托管运行
谁来启动你自己TikMatrix 任务队列
设备租约你自己申请程序启动时已经持有
重试、定时、任务日志你自己实现内置提供
跨多台设备运行你自己写循环每台设备一个任务,并行下发
适合场景探索调试、采集、一次性任务任何需要反复执行的工作

你可以先用独立模式把流程调通,然后把同一个文件注册成托管脚本——需要改的只有 TikMatrix.from_env() 这一行。

快速开始

1. 安装客户端库

pip install requests

然后把 SDK 目录里的 tikmatrix.py 复制到你的脚本旁边。这个库只有单个文件,没有其它依赖。

你也可以不用它——这套 API 就是普通的 HTTP + JSON,下面有原始接口文档。

2. 编写脚本

from tikmatrix import TikMatrix

client = TikMatrix()
with client.device("192.168.1.5:5555") as d:
d.press("home")
d.adb("shell", "am", "start", "-a", "android.settings.SETTINGS")
d.wait_for(text="Settings", timeout=15)
d.screenshot("settings.png")

在 TikMatrix 已打开、手机已连接的情况下运行。如果它打印出了设备信息字典,说明整条链路已经通了。

3. 注册脚本(仅托管模式)

进入 设备 → 自定义脚本 → 添加脚本

字段含义
名称显示在脚本列表和任务日志里
命令要执行的程序命令行,例如 python C:/scripts/my_flow.py
工作目录可选。程序的启动目录
平台见下方平台模式
超时时间超过多少秒后脚本被杀掉、任务标记为失败。默认 1800
额外环境变量可选的 JSON 对象,会合并进程序的环境变量
启用不删除脚本也能停用。停用的脚本无法被下发

然后点击脚本行上的 ▶,选择设备即可,和内置脚本完全一样。

让 AI 助手替你写

AI 助手可以根据一句话描述起草一个自定义脚本并一步注册好。在任何文件写入磁盘之前,它都会先把完整代码给你看。

设备租约

一台手机同一时刻只能被一个程序驱动。占用它相当于告诉 TikMatrix 这台设备正忙,于是:

  • 任务队列不会再往同一块屏幕上派发任务;
  • 你的 JSON-RPC 调用会像内置脚本一样上报 agent 健康状态,看门狗看到的是一个忙碌的 agent,而不是一个失联的 agent。

租约同样会占用套餐中的一个设备名额。

租约会过期——默认 120 秒,最长 600 秒。Python 库会在后台线程自动续期,并在 with 代码块结束时释放,所以脚本崩溃后几秒内设备就会被释放,而不是一直占着直到你重启应用。如果你直接调用 API,就需要自己发送心跳。

设置 → 开发者 API → 设备占用会话 中可以查看所有存活的租约,也可以手动释放。

平台模式

注册脚本时需要声明它面向什么:

通用(Generic) —— 设备原样交给你。不启动任何应用、不切换账号、不检查输入法,结束后也不关闭任何东西。用它来自动化 TikTok / Instagram 之外的一切。

TikTok / Instagram —— 在你的程序启动前先打开应用并切换好账号,结束后关闭应用,和内置脚本的处理完全一致。TIKMATRIX_PACKAGE 会告诉你实际解析出的包名。用它来补上内置脚本没有覆盖的步骤。

环境变量

托管脚本会收到:

变量含义
TIKMATRIX_API_BASE服务地址,例如 http://127.0.0.1:50809
TIKMATRIX_SESSION_ID已经替你持有的租约
TIKMATRIX_SERIAL本次任务下发到的设备
TIKMATRIX_PACKAGE解析出的应用包名
TIKMATRIX_PLATFORMtiktokinstagramgeneric

TikMatrix.from_env() 会替你读取这些变量。

独立脚本拿不到这些变量——请自己显式申请设备租约。

你在额外环境变量里填的内容会覆盖合并到上面这些之上,这也是同一个已注册脚本在不同运行之间传递参数、又不用改代码的常用做法。

Python 库参考

TikMatrix —— 连接

调用作用
TikMatrix(base_url=None, timeout=30.0)建立连接。依次回退到 TIKMATRIX_API_BASEhttp://127.0.0.1:50809
client.devices()在线设备,每项包含 serialreal_serialbusy
client.sessions()所有存活的租约,包括不属于本进程的
client.device(serial, label=..., ttl_secs=120)占用一台设备并返回 Device
TikMatrix.from_env()接管托管脚本启动时已占用的设备

Device —— 手机

调用作用
d.info()UIAutomator2 设备信息
d.window_size()(宽, 高)
d.screenshot(path=None)PNG 字节,可选写入 path
d.hierarchy()当前界面树(XML)
d.find(text=, resource_id=, description=, class_name=)匹配到的节点,每项带 boundscenter
d.exists(**criteria)是否存在匹配项
d.wait_for(timeout=10.0, interval=1.0, **criteria)阻塞直到出现,然后返回该节点
d.click(timeout=10.0, **criteria)等待元素出现后点击其中心
d.click_xy(x, y)点击坐标
d.swipe(sx, sy, ex, ey, steps=20)滑动
d.press(key)backhomerecententer ……
d.input_text(text)通过内置快速输入法向焦点输入框输入文本
d.jsonrpc(method, params=None, timeout=10)任意 UIAutomator2 方法
d.adb(*args, timeout_ms=None)执行 ADB 命令
d.release()释放租约。用 with 时会自动调用

find 是在导出的界面树上匹配的,所以选择器没命中时,可以 print(d.hierarchy()) 看看它到底搜索了什么。设备视图里的元素检查器会以可视化方式展示同一棵树,通常是找 resource-id 最快的办法。

input_text 需要 ADB

它是向内置输入法发送广播,走的是 adb shell。使用前请先开启 ADB 访问,否则会返回 403。

错误

这个库会抛出两种异常,都继承自 RuntimeError

异常出现时机
DeviceBusyErrorHTTP 409 —— 设备已被占用,或套餐没有空闲设备名额
TikMatrixError其它所有情况:套餐等级不足、租约过期、ADB 未开启、选择器始终没命中
from tikmatrix import TikMatrix, TikMatrixError, DeviceBusyError

client = TikMatrix()
try:
with client.device("192.168.1.5:5555") as d:
d.click(text="Log in", timeout=20)
except DeviceBusyError:
print("这台手机被别人占着,换一台吧")
except TikMatrixError as exc:
print("失败:", exc)

在托管脚本里,直接让异常抛出去通常才是对的:非零退出码会把任务标记为失败,堆栈也会写进任务日志。

HTTP 接口

设备类操作需要在请求头里带上 x-session-id,指向一个存活的租约。这里没有 API key:和本地 API 的其余部分一样,这些接口是不做鉴权的——能访问到这台机器本身就是访问控制。它们不返回任何 CORS 头,所以请从程序里调用(curl、Python,或任何服务端代码),而不是从浏览器页面里调用。

方法路径用途
GET/api/v1/rpc/devices列出在线设备以及是否正忙
POST/api/v1/rpc/session占用设备 → session_id
POST/api/v1/rpc/session/{id}/heartbeat续期
DELETE/api/v1/rpc/session/{id}释放
GET/api/v1/rpc/session列出存活的租约
POST/api/v1/rpc/jsonrpc调用 UIAutomator2 方法
POST/api/v1/rpc/adb执行 ADB 命令
GET/api/v1/rpc/hierarchy?serial=当前界面树(XML)
GET/api/v1/rpc/screenshot?serial=当前屏幕(PNG)

JSON 响应使用与本地 API 其余部分一致的信封结构 —— {"code": 0, "message": "success", "data": ...},失败时 code 非零。hierarchyscreenshot 直接返回原始内容。

示例

# 占用设备
curl -X POST http://127.0.0.1:50809/api/v1/rpc/session \
-H "Content-Type: application/json" \
-d '{"serial":"192.168.1.5:5555","label":"curl test","ttl_secs":120}'

# {"code":0,"message":"success","data":{"session_id":"ff3ae079-...","serial":"192.168.1.5:5555", ...}}

# 驱动设备
curl -X POST http://127.0.0.1:50809/api/v1/rpc/jsonrpc \
-H "x-session-id: ff3ae079-..." \
-H "Content-Type: application/json" \
-d '{"serial":"192.168.1.5:5555","method":"deviceInfo","params":[]}'

# 工作期间保持租约
curl -X POST http://127.0.0.1:50809/api/v1/rpc/session/ff3ae079-.../heartbeat \
-H "Content-Type: application/json" \
-d '{"ttl_secs":120}'

# 归还设备
curl -X DELETE http://127.0.0.1:50809/api/v1/rpc/session/ff3ae079-...

错误码

状态码含义
403套餐低于 Pro、没有租约、租约已过期,或 ADB 访问未开启
409设备已被占用,或套餐没有空闲设备名额

用其它语言编写

这里没有任何东西是 Python 专属的。任何能发 HTTP 请求的运行时都可以——托管模式的约定只有一条:「读三个环境变量,成功时以 0 退出」。

// my_flow.js —— 注册命令:node C:/scripts/my_flow.js
const base = process.env.TIKMATRIX_API_BASE || "http://127.0.0.1:50809";
const serial = process.env.TIKMATRIX_SERIAL;
const session = process.env.TIKMATRIX_SESSION_ID;

async function jsonrpc(method, params = []) {
const res = await fetch(`${base}/api/v1/rpc/jsonrpc`, {
method: "POST",
headers: { "content-type": "application/json", "x-session-id": session },
body: JSON.stringify({ serial, method, params }),
});
const body = await res.json();
if (!res.ok || body.code !== 0) throw new Error(body.message || res.statusText);
return body.data;
}

console.log(await jsonrpc("deviceInfo"));

如果解释器不在 PATH 中,就在命令里写完整路径,例如 C:/Program Files/nodejs/node.exe C:/scripts/my_flow.js

通过 API 触发自定义脚本

已注册的脚本也可以通过任务管理 API 启动,因此一个脚本可以给自己排后续工作:

curl -X POST http://127.0.0.1:50809/api/v1/task \
-H "Content-Type: application/json" \
-d '{
"serials": ["192.168.1.5:5555"],
"script_name": "custom_script",
"script_config": {
"custom_script_id": 1,
"custom_script_platform": "generic"
}
}'

custom_script_id 就是你注册的脚本 ID。

ADB 访问

/api/v1/rpc/adb 会给脚本一个设备 shell —— 推送素材、安装 APK、修改系统设置都需要它。正因为它等同于完整的 shell 权限,而这个接口本身没有 API key,所以默认是关闭的。当你确实有脚本需要它时,再到 设置 → 开发者 API → 允许执行 ADB 命令 中打开;只做界面自动化的脚本走 /rpc/jsonrpc 即可,无需开启。

关闭状态下 /api/v1/rpc/adb 会返回 403,其余接口照常工作。脚本执行的每一条 ADB 命令都会写进日志文件。

让脚本长期可用的写法

  • 等界面,别硬睡。 d.wait_for(...) 一出现就返回;固定的 sleep 要么慢得没必要,要么在网络差的那天不够长。
  • 点之前先判断。 对隐私弹窗、「以后再说」这类提示先 d.exists(...),代价只是一次界面树导出,却能救回一整次点空的运行。
  • 把做过什么打印出来。 托管模式下 stdout 就是任务日志,而这往往是无人值守那次运行的唯一记录。
  • 保证重跑是安全的。 重试会把整个程序从头再跑一遍,所以发帖脚本应该先检查是否已经发过,而不是假设自己从零开始。
  • 一个脚本只干一件事。 并发是按设备算的,十台手机跑十个小任务,比一个脚本循环十台手机快得多。

注意事项与限制

  • 命令是直接执行的,不经过 shell,所以 &&| 会被当成参数而不是操作符。想要 shell 行为,请注册 cmd /c "..."(Windows)或 sh -c "..."(macOS)。
  • 路径含空格时要加引号:"C:/Program Files/Python/python.exe" my_script.py
  • 超过超时时间的脚本会被终止,任务标记为失败。
  • 非零退出码会把任务标记为失败;脚本写到 stdout 和 stderr 的所有内容都会进入任务日志。
  • 脚本以与 TikMatrix 相同的权限运行。只注册你自己写的或你信任的程序。

常见问题排查

API access requires Pro or higher plan(403) 这台机器上的授权是 Starter 或已失效。检查 设置 → 授权

连接 127.0.0.1:50809 被拒绝 TikMatrix 没有运行,或者运行在另一个用户下。这个服务只在应用打开时存在。

每次申请租约都返回 409 要么手机确实正忙——到 设置 → 开发者 API → 设备占用会话 里看看——要么套餐里的设备名额已经被运行中的任务占满了。

长步骤执行到一半租约过期 默认 TTL 是 120 秒,库会在后台续期,所以出现这种情况通常意味着脚本把主线程阻塞得比 TTL 还久。把 ttl_secs 调大(最多 600),或者把耗时工作挪出该线程。

d.adb(...) 返回 403 ADB 访问是关闭的。到 设置 → 开发者 API → 允许执行 ADB 命令 中打开。

选择器始终匹配不上 print(d.hierarchy()) 会显示 find 实际搜索的那棵树。文本是精确匹配的,所以多一个空格或界面是本地化文案是最常见的原因;用 resource_id 匹配比用 text 稳定得多。

任务被标记为失败,但手机看起来是好的 去看任务日志。非零退出——包括流程跑完之后才抛出的未捕获异常——同样会让任务失败,哪怕自动化本身是成功的。

下一步