Files
auto_control/doc/API.md
T
butubb 79cdf61ed2 feat: 自动认领开关 + 全站按设备名称显示
一、指纹匹配自动认领(可选,默认关)
- 发现设置新增「指纹匹配自动认领」勾选(app_meta: discovery_auto_claim,默认 0)
- 打开后:扫描发现某设备指纹与池中已有记录一致(同一台换了 IP)→ 自动迁移记录到新地址
  并同步分组/任务引用,零点击;关闭时维持"识别自动 + 人工点一次确认"
- 默认关的原因:认领会改写分组/任务引用(数据结构变动),交人工确认更稳妥
- 扫描结果与状态行会显示本轮自动认领了几台

二、设备名称在界面上呈现(凡选择/展示设备处都显示名称)
- 新增前端 helper `devText(name, serial)`(base.js):有名称→「名称 · serial」
- 监控页设备表:名称加粗为主、地址作副行(未命名显示橙色提醒);任务概况的覆盖设备
  chip 也优先显示名称(tooltip 保留完整地址)
- AI 控制台:目标设备下拉、实时画面设备下拉、目标/运行中提示都带名称(serial→name 映射)
- 任务编辑器「指定设备」下拉、分组编辑的设备勾选列表:带名称
- 后端 `/api/devices` 新增 `items`([{serial,name,model}],`devices` 保持兼容);
  `/api/agent/devices` 增加 `name` 字段
- MCP `de_list_devices` 返回 `name`,并在工具说明与 Agent 系统提示里要求"汇报用名称、
  调工具用 serial"

文档:API.md(items/name/auto_claim + §6.2.1 名称呈现表)、MCP.md(工具返回)

自测(全通过):自动认领端到端(开开关→扫描→自动迁址 + 名称保留 + 分组/任务引用同步 +
待连接池清理 + 开关默认关且可持久化);名称显示浏览器验证(监控页/覆盖设备 chip/AI 目标与
观看下拉/任务编辑器/分组弹窗/发现设置开关);设备指纹与人工认领回归
2026-09-11 11:16:24 +08:00

687 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 非 JSON 响应汇总](#14-非-json-响应汇总)
- [15. 错误分支速查](#15-错误分支速查)
- [16. 已知问题](#16-已知问题)
---
## 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 已签发、前端已携带,但**伪造请求不会被拦**。见 [§16 已知问题](#16-已知问题)。
### 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)见 [§14](#14-非-json-响应汇总)。
---
## 2. 接口总索引
> 共 **107 条路由**。`鉴权` 列:`—` 无、`L` 登录、`T/D/A/G` = tasks/devices/apks/logs 权限位、`Admin` 仅管理员。
### 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/uiauto/status` | L | uiautodev 服务是否在跑 |
| GET | `/api/uiauto/devices` | D | 抓元素可选设备 |
| GET | `/api/uiauto/elements` | D | 设备 UI 元素树 |
| GET | `/api/uiauto/screenshot` | D | uiautodev 截图(JPEG) |
| POST | `/api/steps/test` | D | 真机试执行单个步骤 |
### 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 | 读日志文件尾部 N 行 |
### 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 | 一键重连全部池内网络设备 |
| 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 | 保存发现配置 |
### 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 会话 |
| GET | `/api/agent/run` | 运行状态(刷新恢复用) |
| 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` | 删除动作 |
### 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` | 应用恢复(重启生效) |
---
## 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
}]}
```
`worker_status`:`idle` / `connecting` / `running` / `done` / `error` / `released` / `failed`。
### 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 则对全部在线设备 |
| `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)。
---
## 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` |
| 设备池 / 待连接池 / 断联设备表 | 名称列 + 指纹列 |
| MCP `de_list_devices` | 返回 `name` 字段,提示 AI 汇报时用名称 |
### 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 设备 + 池内在线补全) |
| `GET /api/uiauto/elements?serial=` | — | 扁平元素列表,每项带 `suggested`(推荐选择器)、`bounds`、`depth` |
| `GET /api/uiauto/screenshot?serial=` | — | JPEG |
| `POST /api/steps/test` | `{"serial","step":{…}}` | 真机试执行单个步骤 → `命中 / 未找到 / 已执行` |
`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` | — | 安装进度(前端 3s 轮询) |
---
## 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 | — | 返回日志尾部 + 可选文件清单 |
---
## 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":"…"}` | 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_apks": false}`。响应为 **zip 附件**:
```
users.db # sqlite 在线备份 API 做的一致快照
apks/*.apk # include_apks=true 时
manifest.json # {format:"auto_control_backup", version, created_at, schema_version,
# include_apk, tables:[{table,label,rows}], apks:[…], coverage_missing?}
```
### POST /api/system/backup/preview
multipart 上传 `.zip` 或 `.db`(字段 `file`)→ 校验并暂存:
```json
{"ok": true, "token": "e172dc75e2f7",
"preview": {"integrity": "ok", "schema_version": 4, "current_schema_version": 4,
"tables": [{"table":"agent_action","label":"动作库","rows":4}],
"missing_optional": [], "extra_tables": [],
"warnings": ["备份为全量快照,含用户口令哈希、AI 控制台 API Key 等敏感信息…"]}}
```
- 完整性 `PRAGMA integrity_check` 必须 `ok`;缺必需表(`app_meta`/`user`/`task_job`/`device_group`)直接拒绝
- `extra_tables` 非空 = 备份含**未登记进覆盖清单**的表(提示去登记)
- 暂存 **TTL 30 分钟**,过期自动清理
- 文件非法 / 未选文件 → 400
### POST /api/system/backup/apply
`{"token":"…"}` → 先自动把当前库快照到 `data/backups/pre_restore_<ts>.db`(安全网),再把暂存库落到 `data/restore_pending/`。
**必须重启服务才生效**(`web_server.py` 在 `init_db` 之前消费该目录)。token 无效/暂存缺失 → 400。
---
## 14. 非 JSON 响应汇总
| 方法 | 路径 | 响应类型 | 说明 |
|------|------|---------|------|
| GET | `/` `/wall` `/login` `/locate` | `text/html` | 四个页面(`/locate` 免登录) |
| 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` | 附件下载 |
---
## 15. 错误分支速查
| 码 | 典型触发 |
|----|---------|
| **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 连接异常 |
---
## 16. 已知问题
| 问题 | 影响 | 位置 |
|------|------|------|
| `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)。