19 KiB
API 接口文档
platform-tools Web 后台提供 JSON API,所有接口需登录后访问(Flask-Login session 认证)。
Base URL:http://localhost:18050
认证方式:Cookie Session(先 POST /login 获取 session cookie,后续请求带上)
通用响应格式:
{"ok": true, "data": "..."}
{"ok": false, "error": "错误信息"}
权限模型(v2 起):
- 所有接口需登录;查看类 GET 接口(状态/列表/截图)所有登录用户可用
- 写操作按权限位授权(管理员拥有全部权限):
权限位 中文 覆盖接口 tasks任务管理 任务/自定义动作/分组的增删改、启停、立即执行 devices设备控制 停止设备、释放占用、清除异常、前台扫描、元素抓取 apks应用管理 APK 上传、安装、删除 logs日志查看 GET /api/logs - 用户管理接口仅管理员可用(普通用户即使被授予业务权限也无法访问)
- 无权限访问返回
403 {"ok": false, "error": "无权限执行此操作..."} - 当前用户权限查询:
GET /api/me
1. 认证
POST /login
用户登录。
请求(form-data):
| 参数 | 类型 | 说明 |
|---|---|---|
| username | string | 用户名 |
| password | string | 密码 |
响应:成功重定向到 /,失败返回登录页(含 error 信息)。
GET /logout
登出,重定向到登录页。
2. 页面路由
GET /
单页应用首页(需登录)。响应头设置 Cache-Control: no-store 防止缓存。
GET /login
登录页面(GET)。
3. 设备状态
GET /api/status
获取设备池 + Worker 综合状态(带 5 秒缓存)。
响应:
{
"ok": true,
"server_time": 1700000000.0,
"fg_scanning": false,
"fg_last_scan": 1700000000.0,
"devices": [
{
"serial": "192.168.1.100:5555",
"model": "Pixel 6",
"device_name": "测试机1",
"present": true,
"ready": true,
"stf_occupied": false,
"owner": "",
"worker_status": "idle",
"foreground_app": "空闲",
"progress": {"done": 5, "total": 80, "unit": "视频", "action_counts": {"like": 3}},
"current_action": "观看视频 6",
"last_error": "",
"running_job": "",
"task_job": "",
"attempt": 0
}
]
}
worker_status 取值:idle / connecting / running / done / error / failed / released
(权限:设备控制)
手动触发前台 App 扫描(后台异步执行,不打扰设备)。
响应:
{"ok": true, "msg": "扫描已启动"}
{"ok": false, "error": "已有扫描在进行中"}
GET /api/devices
返回所有在线设备 serial 列表(供分组表单勾选用)。
响应:
{"ok": true, "devices": ["192.168.1.100:5555", "192.168.1.101:5555"]}
GET /api/device/screenshot
获取设备当前画面截图(PNG)。
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| serial | string | 设备 serial |
| t | int | 时间戳(避免缓存,前端自动加) |
响应:成功返回 image/png,失败返回 JSON 错误。
用
adb exec-out screencap -p,只读操作,任务运行中调用安全。
4. 任务类型
GET /api/task_types
返回所有已注册任务类型。
响应:
{
"ok": true,
"task_types": [
{
"task_type": "douyin_nurture",
"name": "抖音养号",
"description": "自动观看抖音视频...",
"default_params": {"watch_count": 80, "...": "..."}
},
{
"task_type": "generic_steps",
"name": "通用步骤",
"description": "通过步骤编辑器编排...",
"default_params": {"steps": [...]}
}
]
}
GET /api/actions
返回指定任务类型支持的专属操作。
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| task_type | string | 任务类型 |
响应:
{
"ok": true,
"actions": [
{"action_type": "like", "name": "点赞", "description": "...", "default_params": {"rate": 0.3}}
]
}
5. 任务计划 CRUD
GET /api/jobs
列出所有任务计划。
响应:
{
"ok": true,
"jobs": [{"id": "abc123", "name": "抖音养号", "task_type": "douyin_nurture",
"next_run": "2026-08-12 09:00", "...": "..."}],
"task_types": [...]
}
next_run:下次真正执行时间(已按运行窗口跳过窗口外触发点,格式 YYYY-MM-DD HH:MM);手动任务/已停用为 null。
(权限:任务管理)
创建任务计划。
请求(JSON):
{
"name": "抖音养号",
"task_type": "douyin_nurture",
"target": {"mode": "all"},
"params": {"watch_count": 50, "actions": {"like": {"params": {"rate": 0.5}}}},
"schedule": {"mode": "cron", "cron": "0 */2 * * *"},
"retry": {"max_attempts": 3, "delay": 60},
"enabled": true
}
响应:
{"ok": true, "msg": "任务已创建", "job": {"id": "abc123", "...": "..."}}
schedule 字段格式:
| 字段 | 说明 |
|---|---|
mode |
once 手动 / cron 定时启动 / cron_stop 定时启动+停止 |
cron |
标准 5 段 cron:分 时 日 月 周(周 0/7=周日);如 0 */2 * * * 每 2 小时整点 |
stop_cron |
(cron_stop 必填)到点停止本任务 worker |
window |
可选,运行窗口 {"start": "21:00", "end": "09:00"}(每天重复,支持跨午夜)。窗口外定时触发和手动执行(POST /api/jobs/:id/run)均不启动,手动执行返回错误提示 |
(权限:任务管理)
更新任务计划。只需传要更新的字段。
请求(JSON):
{"params": {"watch_count": 100}}
响应:
{"ok": true, "msg": "任务已更新", "job": {"...": "..."}}
(权限:任务管理)
删除任务计划。
响应:
{"ok": true, "msg": "任务已删除"}
(权限:任务管理)
立即执行任务(异步,不阻塞)。
响应:
{"ok": true, "msg": "任务 抖音养号 已触发"}
(权限:任务管理)
启用/停用任务。
请求(JSON):
{"enabled": false}
响应:
{"ok": true, "msg": "任务已停用"}
6. 设备分组
GET /api/groups
列出所有分组。
响应:
{
"ok": true,
"groups": [
{"name": "A组", "serials": ["192.168.1.100:5555"], "description": "测试组"}
]
}
(权限:任务管理)
创建分组。
请求(JSON):
{"name": "A组", "serials": ["192.168.1.100:5555"], "description": "测试组"}
(权限:任务管理)
更新分组。
请求(JSON):
{"serials": ["192.168.1.100:5555", "192.168.1.101:5555"], "description": "更新描述"}
(权限:任务管理)
删除分组。
7. 运行控制
(权限:设备控制)
停止单台设备的 worker(并阻止后续重试)。
请求(JSON):
{"serial": "192.168.1.100:5555"}
响应:
{"ok": true, "msg": "已发送停止信号给 192.168.1.100:5555"}
(权限:设备控制)
停止所有运行中的 worker。
响应:
{"ok": true, "stopped": ["192.168.1.100:5555", "192.168.1.101:5555"]}
(权限:设备控制)
释放当前账户占用的所有 STF 设备(清理用)。
响应:
{"ok": true, "released": ["192.168.1.100:5555"]}
8. 用户管理
(仅管理员)
列出所有用户。
响应:
{"ok": true, "users": [{"id": 1, "username": "admin", "is_admin": true}]}
(仅管理员)
创建用户。
请求(JSON):
{"username": "user1", "password": "pass123", "is_admin": false}
(仅管理员)
更新用户(修改密码/管理员权限)。
请求(JSON):
{"password": "newpass", "is_admin": true}
(仅管理员)
删除用户(不能删除 admin 和当前登录用户)。
9. 日志
(权限:日志查看)
查看日志文件内容。
参数:
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
| file | string | core.log |
日志文件名 |
| lines | int | 300 | 返回最后 N 行 |
可选文件:core.log / task.log / web.log / action.log
响应:
{
"ok": true,
"content": "2026-08-08 10:00:00 [INFO] [core.worker] ...",
"file": "core.log",
"files": {"core": "core.log", "task": "task.log", "web": "web.log", "action": "action.log"}
}
10. 自定义动作
GET /api/custom_actions
列出所有自定义动作(步骤打包)。
(权限:任务管理)
创建自定义动作。
请求(JSON):
{"name": "登录流程", "icon": "📦", "steps": [{"type": "click", "...": "..."}]}
(权限:任务管理)
更新自定义动作。
(权限:任务管理)
删除自定义动作。
11. 元素抓取(uiauto2)
GET /api/uiauto/status
探测 uiauto2 本地服务是否运行。
响应:
{"ok": true, "running": true}
(权限:设备控制)
获取 uiauto2 已连接的设备列表。
响应:
{"ok": true, "devices": [{"serial": "192.168.1.100:5555", "model": "Pixel 6"}]}
(权限:设备控制)
通过 uiauto2 获取设备截图(JPEG)。
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| serial | string | 设备 serial |
响应:成功返回 image/jpeg,失败返回 JSON 错误。
(权限:设备控制)
获取设备 UI 元素树。
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| serial | string | 设备 serial |
响应:
{
"ok": true,
"elements": [
{
"depth": 0,
"name": "android.widget.FrameLayout",
"resource_id": "",
"text": "",
"description": "",
"class": "android.widget.FrameLayout",
"bounds": "[0,0][1080,2400]",
"suggested": {"type": "xpath", "value": "//*"}
}
]
}
12. 应用管理(APK)
GET /api/apks
列出所有已上传的 APK。
响应:
{
"ok": true,
"apks": [
{
"id": "abc123",
"display_name": "抖音",
"package_name": "com.ss.android.ugc.aweme",
"version_name": "25.0.0",
"version_code": 2500,
"size": 104857600,
"upload_time": "2026-08-08 10:00:00"
}
]
}
(权限:应用管理)
上传 APK 文件(自动解析包名/版本/应用名)。
请求(multipart/form-data):
| 字段 | 类型 | 说明 |
|---|---|---|
| file | file | APK 文件 |
响应:
{"ok": true, "apk": {"...": "..."}, "msg": "上传成功: 抖音"}
(权限:应用管理)
删除 APK 文件和记录。
(权限:应用管理)
批量安装 APK 到指定设备。
请求(JSON):
{"apk_id": "abc123", "serials": ["192.168.1.100:5555", "192.168.1.101:5555"]}
响应:
{"ok": true, "msg": "开始安装 抖音 到 2 台设备"}
GET /api/apks/install/devices
可安装设备列表:STF 在线设备池 + 本机 adb 设备(含 USB 有线连接,serial 无冒号标记 usb)。
安装弹窗用此列表,USB 设备安装时跳过 adb connect 直接安装。
响应:
{"ok": true, "devices": [
{"serial": "100.100.10.11:5555", "model": "22120RN86C", "source": "stf"},
{"serial": "ZY322ABCDEF", "model": "", "source": "usb"}
]}
GET /api/apks/install/status
获取安装任务实时状态。
响应:
{
"ok": true,
"status": {
"apk_name": "抖音",
"finished": false,
"total": 2,
"success": 1,
"failed": 0,
"skipped": 0,
"installing": 1,
"pending": 0,
"items": {
"192.168.1.100:5555": {"name": "Pixel 6", "status": "success", "msg": "安装成功(已验证)"},
"192.168.1.101:5555": {"name": "Pixel 7", "status": "installing", "msg": "正在安装..."}
}
}
}
13. 维护 / 工具(仅管理员)
以下接口都仅管理员可用(普通用户即使有业务权限也访问不了,返回 403)。 维护页现仅含 STF 服务管理;adb 终端、Tailscale 管理、应用管理、STF 设备管理等 已集中到"工具"页(页内子分栏),API 不变。
POST /api/stf/restart
一键重启 STF Docker 容器。通过 SSH 到部署机执行 docker restart,随后轮询 STF API 确认恢复。
- 目标与容器名:
STF_SSH_TARGET(默认[email protected])、STF_DOCKER_CONTAINER(默认stf)环境变量配置 - 前置条件:本机可免密 SSH 到部署机
- 有任务在运行时返回 409 拒绝执行(STF 重启会重置设备占用状态)
- 失败返回 502(SSH 不可达/容器名错误);执行超时返回 408
响应:
{"ok": true, "msg": "STF 容器已重启", "detail": "stf: Up 3 seconds", "api_alive": true}
GET /api/adb/devices
维护终端设备列表:本地 adb 已连接(含 offline)+ STF 在线设备池(标记 stf)。
供终端设备选择器使用——选中后前端自动附加 -s <serial>。
响应:
{"ok": true, "devices": [
{"serial": "100.100.10.11:5555", "state": "device"},
{"serial": "100.100.10.13:5555", "state": "offline"},
{"serial": "100.100.10.12:5555", "state": "stf"}
]}
POST /api/adb/cmd
adb 远程终端:用平台 adb 二进制执行任意 adb 命令(20s 超时)。
请求(JSON):
{"cmd": "adb -s 100.100.10.11:5555 shell ls /sdcard"}
(开头的 adb 前缀可省略)
安全红线:包含 kill-server / disconnect 的命令直接拒绝(400)——会断开 STF provider 共享的 adb transport,导致全部设备被 STF 误判离线。
响应:
{"ok": true, "stdout": "...", "stderr": "", "code": 0}
14. 步骤测试(仅管理员可触发,需"设备控制"权限)
POST /api/steps/test
在指定设备上单步试执行(步骤编辑器"测试此步骤"按钮),验证选择器是否命中。 只读连接(adb connect + u2),不占用/释放 STF,与运行中任务互不干扰。
请求(JSON):
{
"serial": "100.100.10.11:5555",
"step": {"type": "click", "label": "测试", "params": {"selector_type": "xpath", "selector_value": "//*[@resource-id=\"x\"]"}}
}
响应:
{"ok": true, "msg": "步骤已执行(测试)", "result": "命中"}
result:命中 / 未找到 / 已执行(无选择器命中语义的步骤)。
无"设备控制"权限返回 403。
15. Tailscale 管理(仅管理员)
工具页"Tailscale 管理"子分栏,调用 Tailscale 官方 API v2。所有接口仅管理员可用。
前置:.env 配置 TAILSCALE_API_KEY(Settings → API Access Tokens)与
TAILSCALE_TAILNET(tailnet 名,个人账号一般为邮箱前缀);未配置返回 502 并附提示。
设备 IP 由 tailnet 分配,API 不可修改,列表只读展示。
GET /api/tailscale/status
配置状态检查。
响应:
{"ok": true, "configured": false, "hint": "TAILSCALE_API_KEY(...);TAILSCALE_TAILNET(...)"}
GET /api/tailscale/devices
列出 tailnet 全部设备。
响应:
{"ok": true, "devices": [
{"id": "d1", "name": "dev-a", "hostname": "dev-a", "os": "linux",
"addresses": ["100.100.10.11"], "authorized": true,
"key_expiry_disabled": false, "online": true, "last_seen": "...", "tags": []}
]}
POST /api/tailscale/devices/:device_id
更新设备(按传入字段分发到 Tailscale 专属端点,POST /device/{id} 本身是 405):
name → /name 显示名;authorized → /authorized 授权开关;
key_expiry_disabled → /key(true=密钥永不过期,即关闭设备密钥验证,恢复后按原定过期时间执行)。
请求(JSON):
{"key_expiry_disabled": true}
POST /api/tailscale/devices/:device_id/ip
设置设备的 Tailscale IPv4 地址(未公开端点,实测可用)。
请求(JSON):
{"ipv4": "100.100.10.16"}
⚠ 改 IP 会断开设备当前 tailscale 会话;平台设备池 serial 随之变化,需同步更新 STF 设备池/分组/任务目标。
DELETE /api/tailscale/devices/:device_id
从 tailnet 移除设备(下次上线需重新授权)。
POST /api/tailscale/authkey
生成设备接入 auth key(key 只返回一次)。
请求(JSON):
{"description": "新设备接入", "reusable": false, "ephemeral": false,
"preauthorized": true, "expiry_seconds": 3600}
响应:
{"ok": true, "key": "tskey-auth-...", "id": "k1", "expires": "2026-08-11T01:00:00Z"}
16. 工具(仅管理员)
工具页(剪贴板注入 / 应用版本管理 / STF 设备管理)接口,均仅管理员可用。
POST /api/tools/clipboard/set
剪贴板注入:把指定文字写入一台或多台设备的剪贴板。
请求(JSON):
{"serials": ["100.100.10.11:5555", "0123456789ABCDEF"], "text": "要注入的文字"}
设备来源与维护终端一致(本地 adb 含 USB + STF 池)。
实现:u2 jsonrpc.setClipboard(实测 cmd clipboard 在 MIUI 上不存在),支持中文/引号/换行;
IP 设备先 adb connect(已连接跳过,绝不 disconnect),USB 设备首次自动推送 atx-agent。
响应:
{"ok": true, "results": {"100.100.10.11:5555": {"ok": true, "msg": "已注入"}},
"ok_count": 1, "fail_count": 0, "error": null}
POST /api/tools/appver
应用版本管理:查询所有设备上指定包名的安装情况与版本号(并发 10 台)。
请求(JSON):
{"pkg": "com.ss.android.ugc.aweme"}
响应:
{"ok": true, "total": 8, "fail": 0,
"results": {"100.100.10.11:5555": {"installed": true, "version_name": "28.5.0", "version_code": "280500"}}}
未安装返回 installed: false;查询失败的设备带 error 字段。
GET /api/stf/devmgmt/status
STF 设备管理状态:connect_devices.sh 脚本配置的 IP 列表 + 220 上 adb 实际连接状态。
响应:
{"ok": true,
"configured": ["100.100.10.10", "100.100.10.11"],
"connected": [{"serial": "100.100.10.10:5555", "state": "device"}]}
POST /api/stf/devmgmt/add
添加设备:写入 220 上 connect_devices.sh 的 DEVICES 列表 + 立即 docker exec adb adb connect。
设备不可达时 connect 会 15 秒超时兜底(220 侧 timeout),但仍保留在脚本中,cron 每 5 分钟自动重试。
请求(JSON):{"ip": "100.100.10.20"}(可带 :5555,自动归一化)
响应:
{"ok": true, "msgs": ["已写入脚本 /mnt/data/openstf/connect_devices.sh", "连接超时(设备当前不可达),已保留在脚本中,cron 会每 5 分钟自动重试"]}
POST /api/stf/devmgmt/remove
移除设备:从脚本 DEVICES 删除 + adb disconnect(设备从 STF 池下线,任务不再分配)。
注意:断开的是 220(STF provider 侧)的 adb 连接,与本机任务直连的 adb 相互独立。
请求(JSON):{"ip": "100.100.10.20"}