一、标题行解析(core/video_plan.parse_title_line) - 原来只认「标题内容_手机号_日期_编号」(右锚定)。现在**两种都认**: ① 标题内容_手机号_日期_编号 (推荐,右锚定 —— 标题里带下划线也不会错配) ② 手机号_日期_编号_标题 (左锚定,跟视频文件名同序 = "文件名去掉扩展名 + 标题") 两种里都**先试带编号的**,保证同一行永远只有一种解释;编号都可省略。 - 新拒收一条:`手机号_日期_1`(只有编号、没标题)**直接拒** —— 不能把它当标题"1"(静默生成一条标题是"1"的文案,比拒收危险得多); 标题真是纯数字的用写法①。 二、`#` 开头的行(parse_titles_text) - 原来是"以 `#` 开头就整行忽略" → **`#中秋快乐_...` 这种正常标题会被静默丢掉**。 - 改成**先按标题行解析,解析得出就当标题;解析不出且以 `#` 开头才算注释**。 `# 这是注释` 照样忽略,`#话题` 开头的标题照收(`#` 保留在标题里)。 三、其它 - 文案同步:上传标题面板/帮助文案、doc/API.md 的 upload_titles 语义(两种写法、`#` 规则、拒收条件) - 测试:新增 8 条(两种写法 × 带/不带编号 × 标题含下划线与 #话题、只有编号要拒收、整段注释与报错)
1010 lines
62 KiB
Markdown
1010 lines
62 KiB
Markdown
# HTTP 接口文档(API)
|
||
|
||
> 适用读者:前端开发、外部接入方、排接口问题的运维。
|
||
> 相关文档:[ARCHITECTURE.md](ARCHITECTURE.md)(分层与装配)、[DATA_MODEL.md](DATA_MODEL.md)(数据)、[MCP.md](MCP.md)(给 AI 的工具层,不是 HTTP)。
|
||
> 代码位置:全部路由在 `web/` 包下,**10 个蓝图,全部 `url_prefix` 为空**(路径即代码里写的路径)。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [1. 通用约定](#1-通用约定)
|
||
- [2. 接口总索引](#2-接口总索引)
|
||
- [3. 认证与页面](#3-认证与页面)
|
||
- [4. 设备状态与运行控制](#4-设备状态与运行控制)
|
||
- [5. 任务类型 / 任务计划 / 分组 / 自定义动作](#5-任务类型--任务计划--分组--自定义动作)
|
||
- [6. 设备池与自动发现](#6-设备池与自动发现)
|
||
- [7. 远程看屏与设备操作](#7-远程看屏与设备操作)
|
||
- [8. 元素抓取与步骤测试](#8-元素抓取与步骤测试)
|
||
- [9. 应用管理(APK)](#9-应用管理apk)
|
||
- [10. 用户与日志](#10-用户与日志)
|
||
- [11. 运维工具(adb / 剪贴板 / 应用版本 / Tailscale)](#11-运维工具adb--剪贴板--应用版本--tailscale)
|
||
- [12. AI 控制台](#12-ai-控制台)
|
||
- [13. 系统备份](#13-系统备份)
|
||
- [14. 设备端应用商店](#14-设备端应用商店)
|
||
- [15. 非 JSON 响应汇总](#15-非-json-响应汇总)
|
||
- [16. 错误分支速查](#16-错误分支速查)
|
||
- [17. 已知问题](#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 已知问题](#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](#15-非-json-响应汇总)。
|
||
|
||
---
|
||
|
||
## 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](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](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](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
|
||
|
||
```json
|
||
// 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 轮询的主接口。
|
||
|
||
```json
|
||
{"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](NOTIFY.md) §3 的
|
||
`device.battery.low`。
|
||
|
||
### GET /api/summary
|
||
|
||
```json
|
||
{"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](#16-已知问题)。
|
||
|
||
---
|
||
|
||
## 5. 任务类型 / 任务计划 / 分组 / 自定义动作
|
||
|
||
### GET /api/task_types
|
||
|
||
```json
|
||
{"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
|
||
|
||
```json
|
||
{"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`:下次真正执行时间(已按运行窗口跳过窗口外触发点);手动/停用为 `null`
|
||
- **`coverage`**:任务**覆盖的设备**(监控页「任务运行概况」展示用,每次请求现算)——按 `target` 定义解析,**不因设备当前是否空闲而变**,也不做离线过滤、不写调度日志:
|
||
|
||
| `target.mode` | `coverage.serials` |
|
||
|------|------|
|
||
| `all` | 设备池全部启用设备 |
|
||
| `group` | 分组的 serial ∩ 设备池启用设备(池外的手填 IP 不计入) |
|
||
| `serial` | 仅该 serial(即使不在池中也照实返回) |
|
||
|
||
> 与**调度**口径的差异:真正跑的时候走 `TaskJob.resolve_serials()`(`all` 取"池内 ∩ 在线",`serial`/`group` 默认跳过离线设备)。`coverage` 是"定义层覆盖面",供人看"这台任务管哪些设备"。
|
||
|
||
### POST /api/jobs
|
||
|
||
```json
|
||
{"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`:
|
||
|
||
```json
|
||
{"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](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](DATA_MODEL.md) §2.9;任务侧怎么用见
|
||
[TASK_DEV.md](TASK_DEV.md) §4.6。
|
||
|
||
### 账号台账(「账号」页)
|
||
|
||
数据在 `device_account` 表([DATA_MODEL.md](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](DATA_MODEL.md) §2.11,任务侧怎么自动发布见
|
||
[TASK_DEV.md](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](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](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](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](backlog/TODO.md))。
|
||
|
||
---
|
||
|
||
## 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` 会跟着更新:
|
||
> 1. **推送到设备**:`推送中 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` 重传。
|
||
|
||
> 2. **设备上安装**:`正在设备上安装...(大包要 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`(按整天) |
|
||
|
||
返回:
|
||
|
||
```json
|
||
{"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](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](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_*`)。
|
||
|
||
### 配置
|
||
|
||
```json
|
||
// 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](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`)→ 校验并暂存:
|
||
|
||
```json
|
||
{"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](DEVICE_AGENT.md)——那份文档同时是设备端 APK 仓库的对接契约。
|
||
|
||
**管理侧**(登录 + 应用管理权限):
|
||
|
||
### GET /api/agent-store/config
|
||
|
||
```json
|
||
{"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
|
||
|
||
```json
|
||
{"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](backlog/TODO.md)。
|