一、标题行解析(core/video_plan.parse_title_line) - 原来只认「标题内容_手机号_日期_编号」(右锚定)。现在**两种都认**: ① 标题内容_手机号_日期_编号 (推荐,右锚定 —— 标题里带下划线也不会错配) ② 手机号_日期_编号_标题 (左锚定,跟视频文件名同序 = "文件名去掉扩展名 + 标题") 两种里都**先试带编号的**,保证同一行永远只有一种解释;编号都可省略。 - 新拒收一条:`手机号_日期_1`(只有编号、没标题)**直接拒** —— 不能把它当标题"1"(静默生成一条标题是"1"的文案,比拒收危险得多); 标题真是纯数字的用写法①。 二、`#` 开头的行(parse_titles_text) - 原来是"以 `#` 开头就整行忽略" → **`#中秋快乐_...` 这种正常标题会被静默丢掉**。 - 改成**先按标题行解析,解析得出就当标题;解析不出且以 `#` 开头才算注释**。 `# 这是注释` 照样忽略,`#话题` 开头的标题照收(`#` 保留在标题里)。 三、其它 - 文案同步:上传标题面板/帮助文案、doc/API.md 的 upload_titles 语义(两种写法、`#` 规则、拒收条件) - 测试:新增 8 条(两种写法 × 带/不带编号 × 标题含下划线与 #话题、只有编号要拒收、整段注释与报错)
62 KiB
HTTP 接口文档(API)
适用读者:前端开发、外部接入方、排接口问题的运维。 相关文档:ARCHITECTURE.md(分层与装配)、DATA_MODEL.md(数据)、MCP.md(给 AI 的工具层,不是 HTTP)。 代码位置:全部路由在
web/包下,10 个蓝图,全部url_prefix为空(路径即代码里写的路径)。
目录
- 1. 通用约定
- 2. 接口总索引
- 3. 认证与页面
- 4. 设备状态与运行控制
- 5. 任务类型 / 任务计划 / 分组 / 自定义动作
- 6. 设备池与自动发现
- 7. 远程看屏与设备操作
- 8. 元素抓取与步骤测试
- 9. 应用管理(APK)
- 10. 用户与日志
- 11. 运维工具(adb / 剪贴板 / 应用版本 / Tailscale)
- 12. AI 控制台
- 13. 系统备份
- 14. 设备端应用商店
- 15. 非 JSON 响应汇总
- 16. 错误分支速查
- 17. 已知问题
1. 通用约定
1.1 认证
- 基于 Flask-Login session(Cookie)。
- 未登录访问受保护接口 → 302 重定向到
/login?next=<原路径>(不是 401 JSON)。前端apiGet/apiPost/...收到 401/302 会跳登录页。 - 登录:
POST /login(表单username/password)。失败返回 HTTP 200 + HTML 登录页(带错误文案,不区分"用户不存在/密码错",防枚举)。
1.2 权限
| 装饰器 | 语义 | 未通过时 |
|---|---|---|
@login_required |
登录即可 | 302 → /login |
@perm_required(PERM_X) |
需权限位 tasks / devices / apks / logs |
403 {"ok":false,"error":"无权限执行此操作(需要权限: …)"} |
@admin_required |
仅管理员 | 403 {"ok":false,"error":"仅管理员可执行此操作"} |
管理员 is_admin=true 恒通过权限位检查。前端用 data-perm / _can(perm) 隐藏入口,只是体验优化,安全依赖后端。
1.3 CSRF
GET /api/csrf取 token(存在 session);前端对所有非 GET 请求带X-CSRF-Token。- ⚠️ 当前服务端并未强制校验(
web_server.py只 import 了_csrf_protect,没有注册before_request)。即 token 已签发、前端已携带,但伪造请求不会被拦。见 §17 已知问题。
1.4 响应约定
- 绝大多数接口返回 JSON:成功
{"ok": true, ...},失败{"ok": false, "error": "中文原因"}(部分接口额外带msg)。 - HTTP 状态码与
ok并存:参数错误用 400,权限 403,不存在 404,冲突 409,依赖不可用 502/503。少数接口用{"ok":false}+ HTTP 200(如POST /api/jobs/<id>/run的"任务类型不存在")。以本文每节标注为准。 - 时间统一字符串:
"YYYY-MM-DD HH:MM"(展示)或"YYYY-MM-DD HH:MM:SS"(cron 相关)。 - 非 JSON 接口(HTML / 图片 / SSE / MJPEG / zip)见 §15。
2. 接口总索引
本表列出全部业务路由(页面路由除外;同一路径的不同方法合并成一行)。
鉴权列:—无、L登录、T/D/A/G= tasks/devices/apks/logs 权限位、Admin仅管理员。 想看实际注册了多少条:python -c "from web_server import app; print(len([r for r in app.url_map.iter_rules() if r.rule.startswith('/api')]))"。
2.1 auth(web/auth.py)
| 方法 | 路径 | 鉴权 | 功能 |
|---|---|---|---|
| GET | / |
L | 单页应用首页(HTML) |
| GET | /wall |
L | 监控大屏页面(HTML) |
| GET | /login |
— | 登录页(HTML) |
| POST | /login |
— | 登录(表单;成功 302,失败 200 + HTML) |
| GET | /logout |
L | 登出 → 302 /login |
| GET | /api/csrf |
L | 取/生成 CSRF token |
| GET | /api/me |
L | 当前用户信息(id/username/is_admin/perms) |
2.2 monitor(web/monitor.py)
| 方法 | 路径 | 鉴权 | 功能 |
|---|---|---|---|
| GET | /api/status |
L | 设备全量状态 + 服务器时间 + 前台扫描状态 |
| GET | /api/summary |
L | 状态计数 + 异常设备列表(最多 50) |
| GET | /api/health |
— | 健康检查(免登录探活) |
| POST | /api/scan_foreground |
D | 触发前台 App 扫描 |
| GET | /api/devices |
L | 在线设备列表:devices(serial 列表) + items([{serial,name,model}],界面显示设备名用) |
| GET | /api/devices/<serial>/apps |
L | 指定设备已安装应用列表 |
| GET | /api/device/screenshot |
L | 单张设备截图(PNG) |
| GET | /api/screen/stream |
D | 远程看屏 MJPEG 流 |
| GET | /api/screen/thumb |
D | 大屏缩略图(JPEG,带头 X-Screen-State) |
| GET | /api/screen/size |
D | 屏幕原生分辨率 |
| POST | /api/screen/tap |
D | 远程点击(可 snap=1 吸附元素) |
| POST | /api/screen/swipe |
D | 远程滑动 |
| POST | /api/screen/key |
D | 远程按键(白名单) |
| POST | /api/screen/text |
D | 远程输入文字 |
| POST | /api/screen/tap_text |
D | 按屏幕文字点击(UI 树 → OCR 兜底) |
| POST | /api/stop_device |
D | 停止单台设备任务 |
| POST | /api/stop_all |
D | 停止全部任务 |
| POST | /api/device/clear_error |
D | 清除单台设备异常状态 |
| POST | /api/device/clear_all_errors |
D | 清除全部异常(跳过运行中) |
| POST | /api/device/screen_all |
D | 批量亮屏/息屏 |
| POST | /api/device/locate |
D | 点亮屏幕 + 在设备上打开定位大字页 |
| POST | /api/device/locate/stop |
D | 结束定位 |
| GET | /locate |
— | 设备端定位大字页(HTML,免登录) |
2.3 tasks(web/tasks_api.py)
| 方法 | 路径 | 鉴权 | 功能 |
|---|---|---|---|
| GET | /api/task_types |
L | 全部已注册任务类型 |
| GET | /api/actions |
L | 指定 task_type 支持的专属操作 |
| GET | /api/jobs |
L | 任务计划列表(含 next_run、coverage) |
| POST | /api/jobs |
T | 新建任务 |
| PUT | /api/jobs/<job_id> |
T | 更新任务(白名单字段) |
| DELETE | /api/jobs/<job_id> |
T | 删除任务 |
| POST | /api/jobs/<job_id>/run |
T | 立即执行(异步) |
| POST | /api/jobs/<job_id>/toggle |
T | 启用/停用 |
| GET | /api/groups |
L | 分组列表 |
| POST | /api/groups |
T | 新建分组 |
| PUT | /api/groups/<name> |
T | 更新分组 |
| DELETE | /api/groups/<name> |
T | 删除分组 |
| GET | /api/custom_actions |
L | 自定义动作列表 |
| POST | /api/custom_actions |
T | 新建自定义动作 |
| PUT | /api/custom_actions/<action_id> |
T | 更新自定义动作 |
| DELETE | /api/custom_actions/<action_id> |
T | 删除自定义动作 |
| GET | /api/step_defaults |
L | 步骤默认值(生效值 + 出厂值 + 字段规格,供「动作配置」页画表单) |
| POST | /api/step_defaults |
T | 保存步骤默认值(只落与出厂值不同的字段) |
| POST | /api/gesture/record/start |
D | 开始录制手势(手机端 getevent,返回 token) |
| POST | /api/gesture/record/stop |
D | 停止录制并取回轨迹 gestures:[[[x,y,t_ms],…]] |
| GET | /api/uiauto/status |
L | uiautodev 服务是否在跑 |
| GET | /api/uiauto/devices |
D | 抓元素可选设备 |
| GET | /api/uiauto/elements |
D | 设备 UI 元素树 |
| GET | /api/uiauto/snapshot |
D | 一次取齐截图 + 元素树(原生 u2,抓取弹窗用) |
| GET | /api/uiauto/screenshot |
D | uiautodev 截图(JPEG) |
| POST | /api/steps/test |
D | 真机试执行单个步骤 |
| GET | /api/done_marks?job=&limit= |
L | 去重记录列表 + 统计({marks, stats{total,devices,identities,today_devices,today_identities}}) |
| POST | /api/done_marks/delete |
T | 删一条去重记录({id})→ 该设备/身份下次会重新执行 |
| POST | /api/done_marks/clear |
T | 清空某任务的全部去重记录({job})→ 整批重跑 |
| GET | /api/ledger?device=&q=&limit= |
D | 账号台账列表 + 统计({accounts, stats{total,devices,can_post_video,shown}, device_names}) |
| GET | /api/ledger/by_device |
D | 按设备分组({devices:{设备号:{count,accounts[]}}, counts:{设备号:N}}) |
| POST | /api/ledger |
D | 新增账号(抖音号重复 → 400) |
| PUT | /api/ledger/<acc_id> |
D | 改一条账号 |
| DELETE | /api/ledger/<acc_id> |
D | 删一条账号 |
| POST | /api/ledger/import |
D | 从表格粘贴文本批量导入(dry_run=true 只预览不写库) |
| POST | /api/video_plan/upload_video |
D | 上传单个视频素材(文件名 手机号_日期_编号)→ 解析 + 配对 + 入计划 |
| POST | /api/video_plan/upload_titles |
D | 上传标题(txt 文本或文件)→ 配到已有计划行 |
| GET | /api/video_plan/timeline |
D | 发布计划时间线(按日期分组的卡片 + 统计 + 磁盘) |
| GET | /api/video_plan/stats |
D | 今天要发/已发/待标题/过期未发 + 素材占用与磁盘余量 |
| GET/PUT/DELETE | /api/video_plan/<id> |
D | 单条计划:查看 / 改标题 / 删除(连素材文件) |
| GET | /api/video_plan/<id>/video |
D | 预览平台上的素材(文件清理后 → 410) |
| POST | /api/video_plan/<id>/push |
D | 把素材推到手机(只推送、不发布;后台线程) |
| POST | /api/video_plan/push_all |
D | 一键推送:某天(默认今天)所有待发布/失败的计划 → 各自手机(每台设备一个线程、设备内串行) |
| POST | /api/video_plan/<id>/mark |
D | 人工标记结果(done/failed/unknown,推完之后用) |
| POST | /api/video_plan/<id>/skip|retry|resolve |
D | 跳过 / 重试 / 裁决「结果未知」 |
| GET | /api/video_plan/links |
D | 已发布作品的分享链接(format=csv 导出,给铺评论用) |
| GET | /api/video_plan/tasks |
T | 可编辑的任务列表(全部通用步骤任务 + is_release 标记:状态/调度/下次运行 + 完整步骤) |
| POST | /api/video_plan/tasks |
T | 一键新建标准发布任务(骨架 15 步:亮屏 → 打开抖音 → 校验账号 → 推送 → 抖音点击 → 填标题 → 标记) |
| GET/PUT | /api/video_plan/tasks/<job_id> |
T | 发布任务的就地编辑(就在「发布计划」页改名字/目标/时间/启停/步骤) |
| POST | /api/video_plan/tasks/<job_id>/adopt |
T | 给已有任务插上平台两步(推送放最前 / 标记放最后,自己的步骤不动) |
2.4 admin(web/admin_api.py)
| 方法 | 路径 | 鉴权 | 功能 |
|---|---|---|---|
| GET | /api/users |
Admin | 用户列表 |
| POST | /api/users |
Admin | 新建用户 |
| PUT | /api/users/<int:uid> |
Admin | 更新用户(改密/权限/管理员) |
| DELETE | /api/users/<int:uid> |
Admin | 删除用户 |
| GET | /api/logs |
G | 读日志(关键字/级别/时间过滤,返回结构化行) |
| GET | /api/logs/download |
G | 下载日志(同样支持过滤;无过滤即整个文件) |
| GET | /api/step_logs |
G | 任务步骤明细(按设备/任务/结果/时间/关键字过滤,分页) |
| GET | /api/step_logs/runs |
G | 按 run_id 归组的一次运行概览(几步、失败几步) |
| GET | /api/step_logs/filters |
G | 步骤明细的筛选项(明细里出现过的设备/任务 + 结果枚举) |
| GET | /api/step_logs/download |
G | 导出步骤明细 CSV(带 UTF-8 BOM,Excel 直接打开) |
2.5 devices(web/devices_api.py)
| 方法 | 路径 | 鉴权 | 功能 |
|---|---|---|---|
| GET | /api/devices/pool |
D | 设备池清单(附实时在线状态 + 设备指纹) |
| POST | /api/devices/pool/add |
D | 添加/更新设备(名称必填唯一,按指纹自动认领换 IP 的设备) |
| POST | /api/devices/pool/rename |
D | 重命名设备(名称唯一) |
| POST | /api/devices/pool/relocate |
D | 人工认领:把记录迁到新地址并同步分组/任务引用 |
| POST | /api/devices/pool/remove |
D | 从池中删除 |
| POST | /api/devices/pool/toggle |
D | 启用/停用(停用不参与调度) |
| POST | /api/devices/pool/reconnect |
D | 一键重连全部池内网络设备 |
| GET | /api/devices/qrcode?serial= |
D | 生成该设备的配置二维码(设备端 Agent 扫码即配好) |
| POST | /api/devices/pool/refresh_models |
D | 批量采集型号 |
| GET | /api/devices/discovery |
D | 发现状态 + 待连接 + 断联列表 |
| POST | /api/devices/discovery/scan |
D | 手动触发一轮扫描 |
| POST | /api/devices/discovery/confirm |
D | 确认连接(待连接 → 设备池) |
| POST | /api/devices/discovery/ignore |
D | 忽略待连接设备 |
| POST | /api/devices/discovery/reconnect |
D | 立即重连指定断联设备 |
| POST | /api/devices/discovery/settings |
D | 保存发现配置 |
| GET | /api/devices/battery |
D | 电量采集器状态 + 生效配置({settings, scanning, last_scan:[时间,成功,目标数], cached}) |
| POST | /api/devices/battery/settings |
D | 保存电量监控配置(越界钳制,返回钳制后的生效值) |
| POST | /api/devices/battery/scan |
D | 立即采集一次(后台执行;上一轮没跑完返回 409) |
2.6 apks(web/apks_api.py)
| 方法 | 路径 | 鉴权 | 功能 |
|---|---|---|---|
| GET | /api/apks |
L | APK 列表 |
| POST | /api/apks/upload |
A | 上传 APK(multipart,字段 file) |
| DELETE | /api/apks/<apk_id> |
A | 删除 APK |
| POST | /api/apks/install |
A | 批量安装到指定设备 |
| GET | /api/apks/install/devices |
A | 可安装设备列表 |
| GET | /api/apks/install/status |
L | 安装进度 |
2.7 tools(web/tools_api.py)
| 方法 | 路径 | 鉴权 | 功能 |
|---|---|---|---|
| GET | /api/adb/devices |
Admin | 维护终端设备列表(本机 adb + 设备池) |
| POST | /api/adb/cmd |
Admin | 执行 adb 命令(20s 超时;拦截 kill-server/disconnect) |
| POST | /api/tools/clipboard/set |
Admin | 剪贴板注入到多台设备 |
| POST | /api/tools/appver |
Admin | 查指定包名在所有在线设备的版本 |
2.8 tailscale(web/tailscale_api.py,全部 Admin)
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | /api/tailscale/status |
配置状态(key/tailnet 是否就绪) |
| GET | /api/tailscale/devices |
tailnet 设备列表 |
| POST | /api/tailscale/devices/<device_id> |
更新设备(按字段分发:改名 / 授权 / 密钥不过期) |
| POST | /api/tailscale/devices/<device_id>/ip |
设置设备 IPv4(会断开 tailscale 会话) |
| DELETE | /api/tailscale/devices/<device_id> |
从 tailnet 移除 |
| POST | /api/tailscale/authkey |
生成设备接入 auth key |
2.9 agent(web/agent_api.py,全部 Admin)
| 方法 | 路径 | 功能 |
|---|---|---|
| GET/POST | /api/agent/config |
读写 AI 配置(key 打码回显) |
| GET | /api/agent/devices |
AI 可用设备(含名称与 busy 标记) |
| POST | /api/agent/run |
启动一轮 AI 会话(mode:"chat"=聊天 / "designer"=AI 建任务) |
| GET | /api/agent/run |
运行状态(刷新恢复用;含 mode/draft) |
| GET | /api/agent/stream |
SSE 事件流 |
| POST | /api/agent/stop |
中断当前运行 |
| POST | /api/agent/clear |
清空对话历史 |
| GET/POST | /api/agent/conversations |
会话列表 / 新建会话 |
| GET/DELETE | /api/agent/conversations/<conv_id> |
会话详情 / 删除 |
| POST | /api/agent/conversations/<conv_id>/rename |
重命名会话 |
| GET | /api/agent/experience |
经验库列表 + 巡检状态 |
| POST | /api/agent/experience/audit |
手动触发巡检 |
| POST | /api/agent/experience/delete |
删除经验(人工确认) |
| POST | /api/agent/experience/keep |
保留经验(撤销建议删除) |
| GET | /api/agent/actions |
动作库列表 |
| POST | /api/agent/actions/save |
新增/编辑动作(禁坐标) |
| POST | /api/agent/actions/delete |
删除动作 |
| GET | /api/agent/task_draft |
AI 建任务:读回最近一份草稿({saved, running, mode, run_id, draft, draft_error}) |
| POST | /api/agent/task_draft |
回存草稿并重新校验(不通过返回 400 + errors) |
| POST | /api/agent/task_draft/create |
直接创建:取暂存草稿 → 再校验 → add_job → 清草稿(body 可带 {serial, overrides};草稿体不接受客户端提交) |
| POST | /api/agent/task_draft/clear |
丢弃草稿 |
AI 建任务(designer 模式)要点(详见 AI_TASK_GEN.md):
POST /api/agent/run额外接受mode:"designer"与settings(页面上的任务设置,作为草稿 的 overrides:name/target_mode/group_name/serial/schedule/max_duration)。 designer 跑不绑会话(单轮,不吃聊天历史、也不污染会话)。- designer 模式下 Agent 多一个平台级本地工具
submit_task(draft)(不在 MCP 层): 服务端用core/task_draft.validate_draft校验,失败把errors回灌给模型让它改; 通过也只暂存(运行态 +app_meta.agent_task_draft),不落库——入库仍要用户在 步骤编辑器里确认后走POST /api/jobs。 done事件在 designer 下多带{mode, draft, warnings, draft_error}。- 设备忙 409、单实例运行、权限同聊天模式。
2.10 system(web/system_api.py,全部 Admin)
| 方法 | 路径 | 功能 |
|---|---|---|
| POST | /api/system/backup/export |
导出 zip(JSON body) |
| POST | /api/system/backup/preview |
上传备份校验预览(multipart,字段 file) |
| POST | /api/system/backup/apply |
应用恢复(重启生效) |
2.11 notify(web/notify_api.py,全部 Admin)
通知 / Webhook 配置与发送记录(事件目录、推送格式、限流语义见 NOTIFY.md):
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | /api/notify/webhooks |
全部 webhook(url 打码、secret 只回 secret_set)+ 格式清单 |
| POST | /api/notify/webhooks |
新建(body 见 NOTIFY.md §4) |
| PUT | /api/notify/webhooks/<id> |
更新(url/secret 省略或为打码值 → 保持原值) |
| DELETE | /api/notify/webhooks/<id> |
删除 |
| POST | /api/notify/webhooks/<id>/test |
同步发一条测试消息(不占业务令牌桶,单独限 10 次/分) |
| POST | /api/notify/preview |
预览真实请求体 + UTF-8 字节数 + 是否截断 |
| GET | /api/notify/events |
事件目录(含字段清单) |
| GET | /api/notify/logs?limit= |
最近发送记录(内存 200 条,重启清空) |
| POST | /api/notify/settings |
全局开关与默认聚合/限流 |
2.12 device_agent(web/device_agent_api.py)
设备端专用(无登录会话,靠 X-Device-Token 鉴权;平台未启用时统一 404):
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | /api/device/agent/bootstrap |
设备拉自己的身份 + 可安装应用清单 |
| GET | /api/device/agent/apk/<apk_id> |
下载 APK 文件 |
| POST | /api/device/agent/report |
上报下载/安装结果 |
管理端(登录 + 应用管理权限):
| 方法 | 路径 | 功能 |
|---|---|---|
| GET | /api/agent-store/config |
读配置(含设备令牌) |
| POST | /api/agent-store/config |
启用/停用、重置令牌 |
| GET | /api/agent-store/logs |
设备端安装记录 |
| POST | /api/agent/show-info |
让设备上的 Agent 显示/关闭身份大字页(监控页「打开设备端 Agent」按钮) |
完整协议、鉴权与版本约定见 DEVICE_AGENT.md。
3. 认证与页面
POST /login
表单 username / password。
- 成功:
login_user+ 写 session 的 CSRF token → 302 到?next=或/ - 失败:200 + 登录页 HTML(含"用户名或密码错误")
GET /logout · GET /api/csrf · GET /api/me
// GET /api/csrf
{"ok": true, "token": "…"}
// GET /api/me
{"ok": true, "user": {"id": 1, "username": "admin", "is_admin": true,
"perms": ["tasks","devices","apks","logs"]}}
管理员返回全部权限位。页面:GET /(单页应用)、GET /wall(大屏)、GET /locate(设备端定位页,免登录,只显示 serial 文本)。
4. 设备状态与运行控制
GET /api/status
监控页 5s 轮询的主接口。
{"ok": true, "server_time": 1700000000.0, "fg_scanning": false, "fg_last_scan": 0,
"devices": [{
"serial": "192.168.20.206:5555", "model": "22120RN86C", "device_name": "", "present": true, "ready": true,
"worker_status": "running", "task_job": "测试抖音评论", "attempt": 1, "max_attempts": 1,
"current_action": "点击搜索", "progress": {"done": 5, "total": 0, "unit": "操作", "elapsed": 42},
"foreground_app": "抖音", "last_error": "", "last_warning": "", "end_time": 0,
"battery": {"level": 85, "charging": true, "at": 1700000000.0, "tier": 0}
}]}
worker_status:idle / connecting / running / done / error / released / failed。
battery:后台线程采的缓存值(默认 60s 一轮,dumpsys battery 只读),没采到时为 null。
tier 是告警档位(0 正常 / 1 低电量 / 2 严重),由后端按配置阈值算好——
前端只按 tier 上色,不在 JS 里重算阈值(阈值只在「工具 → 设备发现 → 电量监控」一处定义)。
设备离线时保留最后一次读数(at 是采集时刻),页面置灰显示。详见 NOTIFY.md §3 的
device.battery.low。
GET /api/summary
{"ok": true, "counts": {"running":1,"done":2,"error":0,"idle":2,"failed":0},
"errors": [{"serial":"…","task":"…","last_error":"…","attempt":3,"updated":1700000000.0}]}
GET /api/health
免登录探活:{"ok":true,"status":"up","time":…,"device_total":3,"device_online":3,"device_running":0,"device_error":0,"jobs":2}(组件异常时 status:"down" + 500)。
运行控制
| 接口 | 请求 | 说明 |
|---|---|---|
POST /api/stop_device |
{"serial":"…"} |
无运行中任务 → 400 |
POST /api/stop_all |
— | 停止全部 |
POST /api/device/clear_error |
{"serial":"…"} |
运行中/重试等待中拒绝清理 |
POST /api/device/clear_all_errors |
— | 跳过 running/connecting |
POST /api/device/screen_all |
{"mode":"on"|"off", "serials":[…]} |
不传 serials 则对全部在线设备。熄屏是单向的(KEYCODE_SLEEP),不会把已息屏的设备唤醒;亮屏用 KEYCODE_WAKEUP + dismiss-keyguard |
POST /api/scan_foreground |
— | 已扫描中时返回 {"ok":false}(HTTP 200) |
POST /api/device/locate |
{"serial":"…","show":true} |
show=true 时在设备上打开 /locate |
POST /api/device/locate/stop |
{"serial":"…"} |
结束定位 |
GET /api/devices |
— | {"ok":true,"devices":["100.100.10.x:5555", …]} |
GET /api/devices/<serial>/apps |
— | 该设备已安装应用(包名/版本/路径) |
⚠️
/api/device/locate的show=true分支当前会失败(urllib未导入 → 502);GET /locate本身返回 500。见 §16。
5. 任务类型 / 任务计划 / 分组 / 自定义动作
GET /api/task_types
{"ok": true, "task_types": [
{"task_type": "generic_steps", "name": "通用步骤", "description": "…",
"default_params": {"max_duration": 0}}]}
当前平台只有
generic_steps一种类型。default_params不含steps——步骤只能由编辑器产出。
GET /api/actions?task_type=…
返回该任务类型支持的"专属操作"列表(generic_steps 返回步骤类型清单 STEP_TYPES)。
GET /api/jobs
{"ok": true, "jobs": [{
"id": "abc123", "name": "刷视频-上午", "task_type": "generic_steps", "enabled": true,
"target": {"mode": "all"}, "params": {"max_duration": 0, "steps": [ … ]},
"schedule": {"mode": "once"}, "retry": {"max_attempts": 1, "delay": 60},
"next_run": "2026-09-11 09:00",
"coverage": {"mode": "all", "total": 3, "serials": ["192.168.20.206:5555", "…"]}
}], "task_types": [ … ]}
next_run:下次真正执行时间(已按运行窗口跳过窗口外触发点);手动/停用为nullcoverage:任务覆盖的设备(监控页「任务运行概况」展示用,每次请求现算)——按target定义解析,不因设备当前是否空闲而变,也不做离线过滤、不写调度日志:
target.mode |
coverage.serials |
|---|---|
all |
设备池全部启用设备 |
group |
分组的 serial ∩ 设备池启用设备(池外的手填 IP 不计入) |
serial |
仅该 serial(即使不在池中也照实返回) |
与调度口径的差异:真正跑的时候走
TaskJob.resolve_serials()(all取"池内 ∩ 在线",serial/group默认跳过离线设备)。coverage是"定义层覆盖面",供人看"这台任务管哪些设备"。
POST /api/jobs
{"name": "刷视频-上午", "task_type": "generic_steps",
"target": {"mode": "all"},
"params": {"max_duration": 0, "steps": [{"id":"step_1","type":"open_app","label":"打开抖音",
"params":{"package":"com.ss.android.ugc.aweme"}}]},
"schedule": {"mode": "cron", "cron": "0 9 * * *"},
"retry": {"max_attempts": 3, "delay": 60}, "enabled": true}
响应:{"ok": true, "msg": "任务已创建", "job": {…含 coverage…}}
schedule 字段:
| 字段 | 说明 |
|---|---|
mode |
once 手动 / cron 定时启动 / cron_stop 定时启动+停止 |
cron |
标准 5 段 分 时 日 月 周(周 0/7 = 周日),如 0 */2 * * * |
stop_cron |
cron_stop 必填,到点停止本任务 |
window |
可选运行窗口 {"start":"21:00","end":"09:00"}(支持跨午夜)。窗口外定时与手动执行都不启动 |
params.steps要一起传:后端不提供默认步骤、也不做参数校验(编辑器是参数正确性的唯一关卡)。不传 steps 也能建成功,但执行时会立即报错"通用步骤任务没有可执行步骤"。
PUT /api/jobs/<job_id> · DELETE /api/jobs/<job_id>
- PUT:只更新请求里出现的字段(
name/task_type/target/params/schedule/retry/enabled);task_type必须已注册,否则 400 - DELETE:
{"ok":true,"msg":"任务已删除"};不存在 404
POST /api/jobs/<job_id>/run
立即执行(起后台线程,不阻塞)。HTTP 恒 200,成功与否看 ok:
{"ok": true, "msg": "任务 刷视频-上午 已触发"}
{"ok": false, "error": "任务类型 douyin_nurture 已不存在(该类型已被删除),请删除此任务或改用现有类型"}
POST /api/jobs/<job_id>/toggle
{"enabled": true|false} → {"ok":true,"msg":"任务已启用"};不存在 404。
分组
| 接口 | 请求 | 响应 |
|---|---|---|
GET /api/groups |
— | {"ok":true,"groups":[{"name":"A组","serials":[…],"description":""}]} |
POST /api/groups |
{"name","serials":[],"description"} |
名字空/重名 → 400 |
PUT /api/groups/<name> |
{"serials":[],"description"} |
不存在 → 404 |
DELETE /api/groups/<name> |
— | 不存在 → 404 |
自定义动作
| 接口 | 请求 | 说明 |
|---|---|---|
GET /api/custom_actions |
— | 列表(按创建时间倒序) |
POST /api/custom_actions |
{"name","icon","steps":[…]} |
name/steps 空 → 400 |
PUT /api/custom_actions/<id> |
同上(部分更新) | 不存在 → 404 |
DELETE /api/custom_actions/<id> |
— | 不存在 → 404 |
steps 的 schema 与 generic_steps 的 params.steps 完全一致,见 TASK_DEV.md。
去重记录(「任务 → 去重记录」页)
| 接口 | 请求 | 响应要点 |
|---|---|---|
GET /api/done_marks |
?job=<任务ID>&limit=200 |
{ok, marks:[{id,scope_key,kind,job_id,job_name,serial,device_name,identity,created_at}], stats:{total,devices,identities,today_devices,today_identities}}。job 省略 = 全部任务 |
POST /api/done_marks/delete |
{"id"} |
删一条 → 该设备/身份下次会重新执行;不存在 → 404;缺 id → 400 |
POST /api/done_marks/clear |
{"job"} |
清空该任务的全部记录(整批重跑)→ {ok,msg,deleted};缺 job → 400 |
账本表与语义见 DATA_MODEL.md §2.9;任务侧怎么用见 TASK_DEV.md §4.6。
账号台账(「账号」页)
数据在 device_account 表(DATA_MODEL.md §2.10),服务层 core/ledger.py,
权限 devices。
| 接口 | 请求 | 响应要点 |
|---|---|---|
GET /api/ledger |
?device=<设备号>&q=<搜索>&limit=500 |
{ok, accounts:[{id,device_name,serial,phone,nickname,douyin_id,registered_at,sim_in_device,can_post_video,bio,note,created_at,updated_at}], stats:{total,devices,can_post_video,shown}, device_names:["A01",…]}。q 匹配抖音号/手机号/账号名 |
GET /api/ledger/by_device |
— | {ok, devices:{A01:{count,accounts:[…]}}, counts:{A01:4}}(设备池"账号 N"列 + 点开看明细都用它,省一次请求) |
POST /api/ledger |
9 个字段(device_name/douyin_id 必填) |
{ok,msg,account};缺必填或抖音号重复 → 400 |
PUT /api/ledger/<id> |
局部字段 | {ok,msg,account};不存在 → 404;抖音号撞车 → 400 |
DELETE /api/ledger/<id> |
— | {ok,msg};不存在 → 404 |
POST /api/ledger/import |
{"text":"…","delimiter":"\\t","mode":"skip|overwrite","dry_run":true} |
{ok, counts:{total,added,updated,skipped,failed}, rows:[{line,action,device_name,douyin_id,nickname,error,warns}], errors:[{line,reason}], mode, dry_run} |
导入语义(解析规则见 core/ledger.parse_paste):
- 分隔符默认 Tab(Excel 直接粘贴就是 Tab);也支持
|与,。不做"多个空格当分隔"—— 账号名里本来就有双空格(实测有牛奶面包 你吃吗?)。 - 带表头时按列名映射(顺序随意、缺列留空、认不出的列忽略);不带表头则按 设备号/手机号/账号名称/抖音号/注册时间/卡在机内/可发视频/简介/备注 的固定顺序。
- 「卡在机内 / 可发视频」认
是/有/1/true→ True,否/空→ False;认不出的值按"否"处理 并在该行warns里留痕。 - 抖音号非纯数字也会
warns留痕(不拦)——它会让任务比对匹配不上。 mode=skip:库里有同号 → 跳过;mode=overwrite:整行替换(空单元格会覆盖掉原值)。dry_run=true走同一条解析+判重路径但一个字节都不写(界面默认先预览再导入)。- 缺设备号或抖音号的行会被跳过,并在
errors里带行号说明。
⚠ 台账里的抖音号是纯号,只当"比对用的候选值";不要拿它填「去重」的身份元素 (身份是元素原文逐字算 key,格式不同会让去重静默失效)。
视频发布计划(「账号 → 发布计划」页)
表与状态机见 DATA_MODEL.md §2.11,任务侧怎么自动发布见
TASK_DEV.md §4.7;权限 devices。
| 接口 | 请求 | 响应要点 |
|---|---|---|
POST /api/video_plan/upload_video |
multipart file(单文件)+ replace |
{ok,name,action:add|skip|replace,plan,msg};解析/配对不通过 → 400 + error(人话原因)。前端逐文件串行发(才能给每个文件一条进度与结果) |
POST /api/video_plan/upload_titles |
{text, dry_run} 或 multipart file(txt) |
{ok, counts:{total,attached,skipped,rejected}, rows:[{line,phone,date,seq,title,action,error}], msg};每行两种写法都认:标题内容_手机号_日期_编号(推荐,右锚定,标题里可带下划线)或 手机号_日期_编号_标题(左锚定,跟视频文件名同序),编号都可省略;标题里可以有 #话题;空行忽略,# 开头的行只有认不出手机号/日期时才当注释;拒收条件:手机号不在台账/命中多个、日期不合法、标题为空或 >100 字、该槽位已有标题、没有对应视频 |
GET /api/video_plan/timeline |
?range=today|week|all&from=&to=&status=&account_id=&q=&limit= |
{ok, days:[{date,weekday,total,done,plans:[…]}], stats, disk, today} |
GET /api/video_plan/stats |
— | {ok, stats, disk, today};stats 里三个口径都给:today_ready(今天要发几个号,按发布日期)/ today_done(今天完成)/ published_today(今天实际发的,按 published_at,补发也算今天) |
GET /api/video_plan/<id> · PUT · DELETE |
PUT {"title"} |
PUT 只改标题(改完 pending→ready);DELETE 先删行再删素材文件 |
GET /api/video_plan/<id>/video |
— | 素材预览(send_file,支持 Range);文件已被清理 → 410 |
POST /api/video_plan/<id>/push |
— | 把素材推到手机(只推送、不发布):推文件 → 触发相册刷新 → 标题写进剪贴板;已推送/结果未知 → 409。发布动作由你写在任务里(见 TASK_DEV.md §4.7) |
POST /api/video_plan/push_all |
{date?, retry_failed?} |
一键推送:某天(默认今天)ready/failed 且未超尝试上限的计划,按设备分组推(每台设备一个线程、设备内串行;设备间并行)。返回 {ok,count,devices,no_device,msg};没绑上设备地址的条数单独在 no_device 里报出来。全部设备都解析不到 → 400 |
POST /api/video_plan/<id>/mark |
{status: done|failed|unknown, why} |
人工标记结果(推完之后用):done 补 published_at;failed/unknown 把 why 记进 last_error |
POST /api/video_plan/<id>/skip |
{reason} |
人工跳过 |
POST /api/video_plan/<id>/retry |
— | failed→ready,attempts 清零 |
POST /api/video_plan/<id>/resolve |
{published, note} |
unknown 的人工裁决:确认真发出去了 → done;确认没发 → failed(可重试) |
GET /api/video_plan/links |
?from=&to=&device=&phone=&format=csv |
已发布作品的分享链接;CSV 带 BOM(Excel 打开中文不乱码) |
GET /api/video_plan/tasks |
— | {ok, tasks:[{id,name,enabled,mode,cron,next_run,target,steps,step_count,schedule,retry,is_release,has_push}], release_count, other_count}:全部通用步骤任务(不止发布任务 —— 只列发布任务的话,手写的抖音发布流程会一条都不显示,人会以为"这页没有能改的地方"),界面按 is_release(有 push_release 或 mark_release,含嵌套)分两组。带完整 steps 是为就地编辑(抓选择器);step_count 数的是含嵌套的总步数。权限 T |
POST /api/video_plan/tasks |
{"name","target":{"mode":"all|group|serial","group_name","serial"},"time":"10:00","enabled"} |
一键新建:骨架 15 步(亮屏 → 打开抖音(等首页) → 点「我」→ if_el(cmp_source=release) 校验账号 → then: push_release → 「+」(预填 descriptionContains=拍摄) → 相册/第一个视频/下一步/输入框/发布(空选择器占位)→ input_text(text_source=release_title) → mark_release;else: notify 跳过);走 TaskManager 建(内存+库一起更新,直接写库调度器不认)。⚠ target 分组键名是 group_name(写成 group 会静默解析出 0 台设备);分组/单设备没给具体目标 → 400。权限 T |
GET/PUT /api/video_plan/tasks/<job_id> |
PUT {name?, time?, target?, steps?, enabled?} |
发布任务的就地编辑(「账号 → 发布计划」页里改,不用跳任务页)。只放开这几个字段,没传的字段原样保留;steps 整块替换但 params 里的通用参数(去重有效期、抢占…)深合并保留。校验:空名字/坏时间/steps 不是数组/什么都不传 → 400,未知任务 → 404。权限 T。底层仍是 TaskManager.update_job()(内存+库一起改) |
POST /api/video_plan/tasks/<job_id>/adopt |
— | 给已有任务插上平台那两步:push_release 放最前、mark_release 放最后(中间你自己的步骤一步不动;位置理由见 core.video_plan.insert_release_steps)。只对「通用步骤」任务;没有步骤 → 400,已经有这两步 → 409,未知任务 → 404。权限 T |
逐文件上传的幂等:同一 (手机号,日期,编号) 已有行时 —— 文件内容相同 → action=skip(不重复落盘);
内容不同 → 拒收,前端给「覆盖」按钮(二次确认后带 replace=1 重传)。
标题同理:同槽位已有别的标题 → 拒收(不静默覆盖,文案是发出去就改不了的东西)。
⚠ 上传体积:
MAX_CONTENT_LENGTH(默认 2GiB,.env的MAX_UPLOAD_MB可调)必须大于 「应用管理」最大的 APK(实测 336MB),否则 APK 上传会被一起卡死;超限返回 JSON 413。 上反代时 nginx 那侧还要client_max_body_size(见 DEPLOY.md §6)。
6. 设备池与自动发现
6.1 设备身份:名称 + 指纹
| 概念 | 说明 |
|---|---|
名称 name |
必填且唯一,设备在平台里的人可读标识(分组/任务/日志都按它认设备) |
| serial | 设备的当前连接地址(IP:5555 或 USB 序列号)——可变 |
指纹 fingerprint |
ro.serialno,识别"同一台物理设备"的稳定标识(只对网络设备采集;USB 的 serial 本身已稳定) |
为什么需要:设备池原先拿 serial(IP)当身份,设备一换 IP 就变成"陌生新设备", 旧记录永远连不上,分组与 serial 模式的任务还吊着死地址。现在:
- 添加/确认设备时读取指纹 → 若命中池中已有设备(同一台换了地址)→ 自动认领
- 认领 = 迁移原记录(名称/型号/备注/启用状态/添加时间全保留)+ 把
device_group.serials与task_job.target.serial里的旧地址同步换成新地址(库里与内存一起改) - 旧地址已断联、指纹也没采过时(自动认领无从匹配)→ 用
pool/relocate人工认领
指纹在设备在线时自动采集(添加/确认/启动刷新/「采集型号」按钮都会补); 设备列表的「指纹」列:🔑 = 已采集,— = 尚未采集(离线设备采不到)。
6.2 设备池
| 接口 | 请求 | 说明 |
|---|---|---|
GET /api/devices/pool |
— | 池内设备 + 实时 online + fingerprint |
POST /api/devices/pool/add |
{"serial","name","note"?} |
name 必填(空 → 400)、唯一(重名 → 400)。IP:5555 会轻量 adb connect 并读指纹,命中则自动认领(响应 claimed=true + old_serial) |
POST /api/devices/pool/rename |
{"serial","name"} |
改名(唯一校验;不存在 → 404) |
POST /api/devices/pool/relocate |
{"old_serial","new_serial"} |
人工认领:迁移记录 + 同步引用;旧地址不在池 → 400,新地址已在池 → 400 |
POST /api/devices/pool/remove |
{"serial"} |
不存在 → 404 |
POST /api/devices/pool/toggle |
{"serial","enabled"} |
停用则不参与调度 |
POST /api/devices/pool/reconnect |
— | 后台并发重连全部网络设备 |
POST /api/devices/pool/refresh_models |
— | 后台批量采型号 + 补齐缺失指纹 |
6.2.1 名称在界面上的呈现
凡"选择设备 / 展示设备"的地方都以名称为主、地址为辅:
| 位置 | 呈现 |
|---|---|
| 监控页设备表 | 名称加粗为主行,serial 作副行(未命名显示橙色"未命名"提醒) |
| 任务运行概况的覆盖设备 chip | 名称(无名才退地址),tooltip 里带完整 serial |
| AI 控制台「目标设备」「观看设备」下拉 | 名称 · serial · 型号 |
| 任务编辑器「指定设备」下拉 | 名称 · serial |
| 分组编辑的设备勾选列表 | 名称 · serial |
| 设备池 / 待连接池 / 断联设备表 | 名称列 + 指纹列 |
| 剪贴板注入的设备勾选列表 | 名称 · serial(无名只显示地址) |
| 应用管理的安装弹窗设备列表 | 名称 · serial |
| 应用版本管理表格的「设备」列 | 名称 · serial |
| 应用安装进度表的「设备」列 | 名称(取自设备池;无名退地址) |
MCP de_list_devices |
返回 name 字段,提示 AI 汇报时用名称 |
这几个接口的字段来源:
/api/adb/devices、/api/apks/install/devices走web/common.py:_merged_device_list()(带name);/api/tools/appver把name塞进每个设备的结果里。
6.2.2 设备配置二维码(扫码配置)
| 接口 | 鉴权 | 请求 | 说明 |
|---|---|---|---|
GET /api/devices/qrcode?serial=<地址> |
设备权限 | — | 返回 {png(base64 data-url), payload, warn} |
给设备端 Agent 扫的配置二维码,一次写入平台地址 + 设备令牌 + 该设备指纹:
payload是二维码里的原始 JSON:{"v":1,"server":…,"token":…,"fingerprint":…,"name":…}warn非空时必须显示给操作者,最常见两种:- "你是用 localhost 打开平台的" → 二维码里的地址手机会连不上,要改用局域网地址重开本页
- "这台设备还没采集到指纹" → 扫码后平台认不出它是哪一台
- 前置条件:先在「应用管理 → 设备端应用商店」启用(令牌从那儿来),否则 400
- 设备端如何解析见 DEVICE_AGENT.md §5.3
6.3 自动发现
| 接口 | 请求 | 说明 |
|---|---|---|
GET /api/devices/discovery |
— | 发现状态 + 待连接 + 池内断联(前端 10s 轮询)。待连接项带 fingerprint 与 match({"serial","name"} = 指纹命中的池内设备) |
POST /api/devices/discovery/scan |
— | 后台扫描一轮;已有扫描 → 409 |
POST /api/devices/discovery/confirm |
{"serial","name"?} |
确认入池。新设备必须有名称(空 → 400);指纹命中已有设备时不需要名称——保留原记录与原名,直接认领到新地址 |
POST /api/devices/discovery/ignore |
{"serial"} |
从待连接删除 |
POST /api/devices/discovery/reconnect |
{"serial"} |
只接受池内设备,否则 404 |
POST /api/devices/discovery/settings |
{"enabled","subnets","interval","port","auto_claim"} |
部分更新,存 app_meta。auto_claim=指纹匹配时自动认领(默认 false:认领会改写分组/任务引用,默认交人工确认;打开后扫描到"同一台设备换了地址"自动迁移) |
扫描只做 socket 探测 + 只读校验,不会把设备直接拉进设备池(必须人工确认)。 扫描时会顺带读一遍候选设备的指纹(写入待连接池),用于提示"这台是已有设备换了地址"。
7. 远程看屏与设备操作
| 接口 | 请求 | 说明 |
|---|---|---|
GET /api/screen/stream?serial=&q=&fps= |
— | MJPEG 流(multipart/x-mixed-replace) |
GET /api/screen/thumb?serial= |
— | 360px 宽 JPEG 缩略图,响应头 X-Screen-State: on/off/unknown |
GET /api/screen/size?serial= |
— | 原生分辨率 {"ok":true,"width":…,"height":…} |
POST /api/screen/tap |
{"serial","x","y","snap":1?} |
snap=1 先吸附到最小可点击元素中心 |
POST /api/screen/swipe |
{"serial","x1","y1","x2","y2","duration"?} |
|
POST /api/screen/key |
{"serial","key"} |
白名单:back/home/recent/menu/power/volume_up/volume_down… |
POST /api/screen/text |
{"serial","text"} |
u2 send_keys(需焦点在输入框) |
POST /api/screen/tap_text |
{"serial","text"} |
先 UI 树子串匹配,未命中转 OCR;文字 ≤100 字符 |
坐标均为设备原生像素(/api/screen/size 给基准)。u2 调用异常统一 503,并清理 60s 连接缓存。
8. 元素抓取与步骤测试
| 接口 | 请求 | 响应要点 |
|---|---|---|
GET /api/uiauto/status |
— | uiautodev(:20242)是否在跑,前端据此禁/启用"抓取元素" |
GET /api/uiauto/devices |
— | 可选设备列表(uiautodev 设备 + 池内在线补全)。每项 {serial, name, model, product, status};name 是平台给设备起的名字(cs1/A08),池外设备退回 serial——uiautodev 自带的 name 是设备 codename(实测一柜子全是 earth),不能用来认设备 |
GET /api/uiauto/elements?serial= |
— | 扁平元素列表(走 uiautodev),每项带 suggested、bounds、depth |
GET /api/uiauto/snapshot?serial= |
— | 一次取齐:截图 + 元素树(原生 u2 同一连接背靠背)→ {image,width,height,elements,unstable,screen_state,cost_ms};抓取弹窗与 MCP 的 de_snapshot 都用它,失败返回 502 |
GET /api/uiauto/screenshot?serial= |
— | JPEG |
POST /api/steps/test |
{"serial","step":{…}} |
真机试执行单个步骤 → 命中 / 未找到 / 已执行 |
suggested(推荐选择器)字段:{type, value, semantic?, via?, indexed?, occ?, total?, broad?, invalid?, reason?}
| 字段 | 含义 |
|---|---|
semantic + via |
语义选择器://*[@id="x" and @text="y"],via 是用于限定的第二属性(text/content-desc/class 或 a+b)——不依赖同类元素个数,换设备/换版本仍命中 |
indexed + occ/total |
退化形式 (//*[@id="x"])[k]:同 id 且同文字分不开时才用,界面一变即失配,前端黄标提醒 |
broad |
结构路径兜底(//hierarchy/*[i]/*[j]),最脆 |
invalid + reason |
无任何可用属性,前端禁止回填 |
GET /api/uiauto/elements 在 uiautodev 不可用时返回 503。设备 dump 慢时可能超时(见 backlog)。
9. 应用管理(APK)
| 接口 | 请求 | 说明 |
|---|---|---|
GET /api/apks |
— | 已上传 APK 列表(含解析出的包名/版本/大小) |
POST /api/apks/upload |
multipart file |
未选文件 → 400;解析失败 → 500 |
DELETE /api/apks/<apk_id> |
— | 删文件 + 记录 |
POST /api/apks/install |
{"apk_id","serials":[…]} |
后台并发 5 台安装;已在装 → 失败提示 |
GET /api/apks/install/devices |
— | 可安装设备(pool / usb / adb 三个来源) |
GET /api/apks/install/status |
— | 安装进度(前端 1.5s 轮询) |
每台设备的状态机:
pending(等待)→installing(带文字的实时进度)→success/failed/skipped。 安装分两段,items[serial].msg会跟着更新:
- 推送到设备:
推送中 62%(211.0/336.1 MB · 2.6 MB/s · 剩约 48s)—— 用分块写 (adb exec-in)拿字节级进度;不用adb push是因为它只在结束时吐一行汇总, 大包(抖音 336MB≈2 分钟)整个传输过程界面上只能干等 ⚠️adb exec-in返回 ≠ 设备上写完:数据还在设备侧(adbd → shell →cat >)缓冲里 继续落盘。所以收尾必须轮询文件大小直到长齐(_wait_remote_size,最长 20s), 量一次就判"推送不完整"会把成功的传输误报成失败(2026-09-14 的真实案例:同一份 5.9MB 的 APK 在多台设备上报了 8 次假失败,实际文件最终都是完整的)。真不齐才退回adb push重传。
- 设备上安装:
正在设备上安装...(大包要 1-2 分钟,无进度可读)——pm install在设备端解包,系统没给进度接口参考耗时(336MB 抖音 / 单台):推送 ~134s + 安装 ~65s ≈ 3.3 分钟;多台并发(上限 5)时 带宽共享,整体时间取决于 WiFi。
10. 用户与日志
| 接口 | 鉴权 | 请求 | 错误 |
|---|---|---|---|
GET /api/users |
Admin | — | — |
POST /api/users |
Admin | {"username","password","is_admin"?,"perms"?} |
用户名/密码空、重名 → 400 |
PUT /api/users/<int:uid> |
Admin | {"password"?,"is_admin"?,"perms"?} |
取消最后一个管理员 → 400;不存在 404 |
DELETE /api/users/<int:uid> |
Admin | — | 删 admin/删自己/删最后一个管理员 → 400 |
GET /api/logs?file=core.log&lines=300 |
G | q 关键字、level 最低级别、since/until 时间 |
见下 §10.1 |
GET /api/logs/download?file=core.log |
G | 同上(参数一致) | 以附件返回 .txt,文件名带时间戳 |
10.1 日志查询(GET /api/logs)
参数:
| 参数 | 说明 |
|---|---|
file |
日志文件名,白名单校验(只能取接口返回的 files[].name,含 .1/.2 滚动历史);非法值返回 ok:false |
lines |
返回最近多少条命中(默认 300,上限 5000) |
q |
关键字,大小写不敏感,匹配整行原文 |
level |
最低级别:ERROR / WARNING / INFO / DEBUG(含以上);留空为全部 |
since / until |
时间范围,接受 YYYY-MM-DDTHH:MM(datetime-local)或 YYYY-MM-DD(按整天) |
返回:
{"ok": true, "file": "core.log",
"files": [{"name": "core.log", "label": "核心(adb/调度) · core.log",
"size": 1175910, "mtime": 1789519242, "rotated": false}],
"rows": [{"ts": "2026-09-16 08:26:50", "level": "INFO",
"module": "core.disc", "msg": "发现: …", "cont": false}],
"matched": 11423, "scanned": 11423, "truncated": true}
rows保持文件原顺序(旧→新),即"最近 N 条命中"按时间正序排列;cont=true是续行(traceback 的缩进行等):它本身没有时间/级别前缀, 过滤时继承上一条带前缀的行,所以按 ERROR 筛选不会把堆栈拆散;matched是命中总数,truncated=true表示更早的命中没返回(应缩小时间范围或加关键字);scanned是实际扫描行数(上限 50 万行)。
10.2 任务步骤明细(/api/step_logs*)
数据源是 task_step_log 表(结构化,与上面的文本日志不是一回事):每一次步骤
执行一条,能按设备/任务/结果/时间过滤、能按运行归组、能导出 CSV。写入侧见
DATA_MODEL.md §2.8 与 core/step_log.py。
| 接口 | 参数 | 返回 |
|---|---|---|
GET /api/step_logs |
serial job_id result run_id q since until limit(≤2000,默认 200) offset |
rows(最近的在前)、total、stats(本次进程的 queued/written/dropped/failed)、keep_days、max_rows_per_run |
GET /api/step_logs/runs |
serial job_id since until limit(≤500) |
runs[]:run_id started_at ended_at steps failures |
GET /api/step_logs/filters |
— | devices[] / jobs[](只列明细里真的出现过的,不依赖设备池与任务表)、results[] |
GET /api/step_logs/download |
同列表接口 | 附件 .csv(UTF-8 带 BOM),最多 2 万行,按时间正序 |
result 取值:ok / miss(handler 返回 False,如元素没找到)/ error(抛异常)/
unknown(未知步骤类型)/ skip(概率未触发)/ cap(本次运行已达记录上限)。
run_id 是"设备 × 任务 × 第几次尝试",同一行里能拿到 step_path(如 2.1.3)
还原嵌套结构——步骤明细面板的「最近运行概览 → 查看」就是按它过滤。
稳定性:记录走异步队列(
record()零阻塞),队列满会丢弃(计数在stats.dropped, 页面提示栏会显示),所以明细允许缺条 —— 权威结论仍看任务状态与 NOTIFY.md 的事件。
11. 运维工具(adb / 剪贴板 / 应用版本 / Tailscale)
adb
| 接口 | 请求 | 说明 |
|---|---|---|
GET /api/adb/devices |
— | 维护终端看到的设备(本机 adb + 设备池合并) |
POST /api/adb/cmd |
{"cmd":"…"} |
执行 adb 命令 |
/api/adb/cmd 的安全约束:命中 kill-server / disconnect 直接拒绝(红线);空命令 400;超 20s 返回"命令执行超时(20s)";与 worker 共用 _ADB_LOCK。
其它
| 接口 | 请求 | 说明 |
|---|---|---|
POST /api/tools/clipboard/set |
{"serials":[…],"text":"…"} |
设备端 Agent 通道写入并读回校验(旧版独立 ClipInject 自动兜底);serials 空或非列表 → 400 |
POST /api/tools/appver |
{"package":"com.xxx"} |
并发查所有在线设备(≤10 并发);包名须匹配 ^[A-Za-z0-9_.]+$ |
Tailscale(全部 Admin)
| 接口 | 请求 | 说明 |
|---|---|---|
GET /api/tailscale/status |
— | 是否已配置 TAILSCALE_API_KEY/TAILSCALE_TAILNET |
GET /api/tailscale/devices |
— | 上游 API 失败 → 502 |
POST /api/tailscale/devices/<id> |
{"name"?} / {"authorized"?} / {"key_expiry_disabled"?} |
按字段分发 |
POST /api/tailscale/devices/<id>/ip |
{"ipv4"} |
会断开该设备的 tailscale 会话;设备池 serial 就是 tailnet IP,改完需同步设备池 |
DELETE /api/tailscale/devices/<id> |
— | |
POST /api/tailscale/authkey |
{"description"} |
description 必须 ASCII;key 只显示一次 |
12. AI 控制台
配置存在 app_meta(agent_*)。
配置
// GET /api/agent/config
{"ok": true, "api_base": "https://api.deepseek.com", "model": "deepseek-v4-flash-vision-exp",
"api_key": "(原文)", "api_key_masked": "sk-***abcd", "default_serial": "", "max_steps": "40"}
// POST /api/agent/config (部分更新)
{"api_base": "…", "model": "…", "api_key": "…", "default_serial": "…", "max_steps": 40}
运行
| 接口 | 说明 |
|---|---|
POST /api/agent/run |
{"prompt","serial"?,"conversation_id"?} → {"ok":true,"run_id":"8f3a2c9d"}。校验:prompt 空/未配 Key/未配模型/未选设备且无默认 → 400;设备不在池/离线 → 400;设备 busy 或已有 Agent 运行中 → 409 |
GET /api/agent/run |
{"ok":true,"state":"idle|running|done","run_id","prompt","serial","started","answer","error","usage":{…},"history":[…]} |
GET /api/agent/stream?run_id= |
SSE,事件见下 |
POST /api/agent/stop |
下一个检查点生效;无运行中任务 → 400 |
POST /api/agent/clear |
清空运行态历史 |
SSE 事件:
| event | payload | 说明 |
|---|---|---|
delta |
{"text","kind":"content"|"reasoning"} |
流式文本增量(正文 / 推理链) |
step |
{"tool","args","image"?} |
工具调用完成;image 为缩略截图。伪卡片:tool="🧠 经验记忆"(命中/写入经验)、tool="🧠 动作经验"(命中/沉淀动作) |
usage |
{"prompt_tokens","completion_tokens","total_tokens","calls"} |
本轮累计 token,每完成一次模型调用推一次 |
done |
{"answer","usage"} |
完成 |
error |
{"message"} |
失败(MCP 不可达时给出明确文案) |
空闲时每 15s 发 : keepalive;done/error 后关流。
会话
| 接口 | 说明 |
|---|---|
GET /api/agent/conversations |
{"conversations":[{"id","title","updated_at","count"}]} |
POST /api/agent/conversations |
新建空会话 → {"ok":true,"id":"…"} |
GET /api/agent/conversations/<conv_id> |
消息列表(assistant 消息可带 usage、reasoning);不存在 404 |
DELETE /api/agent/conversations/<conv_id> |
删除会话及其消息 |
POST /api/agent/conversations/<conv_id>/rename |
{"title"};空标题 400 |
经验库 / 动作库
| 接口 | 说明 |
|---|---|
GET /api/agent/experience |
经验列表 + 最近巡检结论 + 巡检运行状态 |
POST /api/agent/experience/audit |
手动触发巡检;进行中 → 409 |
POST /api/agent/experience/delete |
{"id"} 人工删除(巡检永远不会自动删) |
POST /api/agent/experience/keep |
{"id"} 撤销"建议删除" |
GET /api/agent/actions |
动作库列表(命名动作 + 元素定位步骤) |
POST /api/agent/actions/save |
{"id"?,"name","app","aliases","params","steps"};含坐标的步骤被拒 → 400 |
POST /api/agent/actions/delete |
{"id"} |
机制详见 AI_CONSOLE.md。
13. 系统备份
POST /api/system/backup/export
JSON body(不是表单):{"include_apk": true}(默认 true)。响应为 zip 附件:
users.db # 当前库(MySQL 或回退 SQLite)整库一致快照成的 SQLite 归档
apks/*.apk # include_apk=true 时
manifest.json # {format:"auto_control_backup", version:2, created_at, schema_version,
# db_backend, deployment_env, source_db_id, include_apk,
# tables:[{table,label,rows}], apks:[…], coverage_missing?}
SQLite 在这里是备份交换格式而非运行时库:MySQL 下导出时把连接提到 REPEATABLE READ, 保证 12 张表读的是同一时刻。
POST /api/system/backup/preview
multipart 上传 .zip 或 .db(字段 file)→ 校验并暂存:
{"ok": true, "token": "e172dc75e2f7",
"preview": {"integrity": "ok", "schema_version": 6, "current_schema_version": 6,
"source_env": "dev", "current_env": "dev", "source_backend": "mysql",
"source_db_id": "704fbbb0…",
"tables": [{"table":"agent_action","label":"动作库","rows":5}],
"missing_optional": [], "extra_tables": [],
"warnings": ["备份为全量数据,含用户口令哈希、AI 配置里的 API Key 等敏感信息…"]}}
- 完整性
PRAGMA integrity_check必须ok;缺必需表(app_meta/user/task_job/device_group)直接拒绝 extra_tables非空 = 备份含未登记的表(提示去登记)source_env≠current_env时warnings会多一条跨环境告警- 暂存 TTL 30 分钟,过期自动清理
- 文件非法 / 未选文件 → 400
POST /api/system/backup/apply
{"token":"…", "force_env_mismatch": false} → 先自动把当前库导出成
data/backups/pre_restore_<ts>.zip(安全网,可直接再导入回来),再把暂存归档落到
data/restore_pending/。
- 必须重启服务才生效:
web_server.py在init_db之后、TaskManager之前消费该目录 - 重启时单事务整库替换(DELETE 全表 + 分块 INSERT),失败自动回滚,当前数据不受影响
- 备份来源环境与当前库不符时默认拒绝(400),要跨环境须显式传
force_env_mismatch: true - token 无效/暂存缺失 → 400
14. 设备端应用商店
设备侧三个接口(bootstrap / apk / report)的完整协议、鉴权、错误码与版本约定
见 DEVICE_AGENT.md——那份文档同时是设备端 APK 仓库的对接契约。
管理侧(登录 + 应用管理权限):
GET /api/agent-store/config
{"ok": true, "enabled": false, "token": "…", "token_created_at": "2026-09-13 14:38:11",
"api_version": 1, "base_url_hint": "http://192.168.20.220:18050"}
base_url_hint 由请求的 Host 回填,直接可作为设备端要填的平台地址。
POST /api/agent-store/config
{"enabled": true} 启用(并自动生成令牌);{"regenerate_token": true} 重置令牌
(旧令牌立即失效)。返回 {"ok": true, "enabled": …, "token": …}。
GET /api/agent-store/logs?limit=50
{"ok": true, "logs": [{"id": 1, "device_name": "A08", "serial": "192.168.20.100:5555",
"fingerprint": "…", "apk_id": "56b114aa", "package_name": "com.example.clipinject",
"version_name": "1.0", "action": "install_fail", "message": "MIUI 拦截未确认",
"created_at": "2026-09-13 14:40:00"}]}
action ∈ download / install_ok / install_fail;记录表最多保留 500 条(自动裁旧)。
15. 非 JSON 响应汇总
| 方法 | 路径 | 响应类型 | 说明 |
|---|---|---|---|
| GET | / /wall /login /locate |
text/html |
四个页面(/locate 免登录;已被设备端 Agent 的身份页取代,仅作历史保留) |
| GET | /api/device/screenshot |
image/png |
原始截图(Cache-Control: no-store) |
| GET | /api/screen/thumb |
image/jpeg |
360px 缩略图 + X-Screen-State |
| GET | /api/screen/stream |
multipart/x-mixed-replace |
MJPEG 实时流 |
| GET | /api/uiauto/screenshot |
image/jpeg |
抓元素时的画面 |
| GET | /api/agent/stream |
text/event-stream |
SSE |
| POST | /api/system/backup/export |
application/zip + Content-Disposition |
附件下载 |
16. 错误分支速查
| 码 | 典型触发 |
|---|---|
| 400 | 参数缺失/非法:任务名空、未知 task_type、分组名空/重名、用户名为空/重名、actions steps 空、备份 token 无效、包名不合法、文字超 100 字符、缺 serial/x/y 等 |
| 403 | 权限位不足 / 非管理员 |
| 404 | 资源不存在:任务 / 分组 / 用户 / 动作 / 待连接记录 / 会话 / 设备不在池 |
| 405 | 方法不匹配(如对只支持 PUT/DELETE 的路径发 GET) |
| 408 | POST /api/adb/cmd 超 20s(返回文案"命令执行超时(20s)") |
| 409 | 冲突:设备忙(AI 不与任务抢设备)、已有 Agent 运行中、巡检进行中、已有发现扫描在跑 |
| 500 | 未预期异常(截图层失败、APK 上传解析失败、备份 IO 失败…) |
| 502 | 上游依赖失败:Tailscale API、设备定位(/api/device/locate) |
| 503 | 依赖不可用:get_status 异常、uiautodev 未启动/抓取失败、u2 连接异常 |
17. 已知问题
| 问题 | 影响 | 位置 |
|---|---|---|
GET /locate 返回 500 |
设备定位大字页打不开(NameError:render_template_string / _esc 未导入) |
web/monitor.py |
POST /api/device/locate 的 show=true 分支失败 |
无法在设备上打开定位页(urllib 未导入 → 502) |
web/monitor.py |
| CSRF 未强制校验 | /api/csrf 会发 token、前端会带 X-CSRF-Token,但服务端没有注册校验钩子 → 伪造请求不会被拦 |
web_server.py |
POST /api/agent/run 的设备校验可能被跳过 |
校验包在 try/except: pass 里,状态服务异常时直接放行(MCP busy 锁兜底) |
web/agent_api.py |
这些均已登记在 backlog/TODO.md。