# 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//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//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/` | T | 更新任务(白名单字段) | | DELETE | `/api/jobs/` | T | 删除任务 | | POST | `/api/jobs//run` | T | 立即执行(异步) | | POST | `/api/jobs//toggle` | T | 启用/停用 | | GET | `/api/groups` | L | 分组列表 | | POST | `/api/groups` | T | 新建分组 | | PUT | `/api/groups/` | T | 更新分组 | | DELETE | `/api/groups/` | T | 删除分组 | | GET | `/api/custom_actions` | L | 自定义动作列表 | | POST | `/api/custom_actions` | T | 新建自定义动作 | | PUT | `/api/custom_actions/` | T | 更新自定义动作 | | DELETE | `/api/custom_actions/` | 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/` | Admin | 更新用户(改密/权限/管理员) | | DELETE | `/api/users/` | 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/` | 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/` | 更新设备(按字段分发:改名 / 授权 / 密钥不过期) | | POST | `/api/tailscale/devices//ip` | 设置设备 IPv4(**会断开 tailscale 会话**) | | DELETE | `/api/tailscale/devices/` | 从 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/` | 会话详情 / 删除 | | POST | `/api/agent/conversations//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` | 应用恢复(重启生效) | ### 2.11 device_agent(`web/device_agent_api.py`) **设备端专用**(无登录会话,靠 `X-Device-Token` 鉴权;平台未启用时统一 404): | 方法 | 路径 | 功能 | |------|------|------| | GET | `/api/device/agent/bootstrap` | 设备拉自己的身份 + 可安装应用清单 | | GET | `/api/device/agent/apk/` | 下载 APK 文件 | | POST | `/api/device/agent/report` | 上报下载/安装结果 | **管理端**(登录 + 应用管理权限): | 方法 | 路径 | 功能 | |------|------|------| | GET | `/api/agent-store/config` | 读配置(含设备令牌) | | POST | `/api/agent-store/config` | 启用/停用、重置令牌 | | GET | `/api/agent-store/logs` | 设备端安装记录 | > 完整协议、鉴权与版本约定见 [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 }]} ``` `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//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/ · DELETE /api/jobs/ - PUT:只更新请求里出现的字段(`name`/`task_type`/`target`/`params`/`schedule`/`retry`/`enabled`);`task_type` 必须已注册,否则 400 - DELETE:`{"ok":true,"msg":"任务已删除"}`;不存在 404 ### POST /api/jobs//run 立即执行(起后台线程,不阻塞)。**HTTP 恒 200**,成功与否看 `ok`: ```json {"ok": true, "msg": "任务 刷视频-上午 已触发"} {"ok": false, "error": "任务类型 douyin_nurture 已不存在(该类型已被删除),请删除此任务或改用现有类型"} ``` ### POST /api/jobs//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/` | `{"serials":[],"description"}` | 不存在 → 404 | | `DELETE /api/groups/` | — | 不存在 → 404 | ### 自定义动作 | 接口 | 请求 | 说明 | |------|------|------| | `GET /api/custom_actions` | — | 列表(按创建时间倒序) | | `POST /api/custom_actions` | `{"name","icon","steps":[…]}` | name/steps 空 → 400 | | `PUT /api/custom_actions/` | 同上(部分更新) | 不存在 → 404 | | `DELETE /api/custom_actions/` | — | 不存在 → 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/` | — | 删文件 + 记录 | | `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/` | Admin | `{"password"?,"is_admin"?,"perms"?}` | 取消最后一个管理员 → 400;不存在 404 | | `DELETE /api/users/` | 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/` | `{"name"?}` / `{"authorized"?}` / `{"key_expiry_disabled"?}` | 按字段分发 | | `POST /api/tailscale/devices//ip` | `{"ipv4"}` | **会断开该设备的 tailscale 会话**;设备池 serial 就是 tailnet IP,改完需同步设备池 | | `DELETE /api/tailscale/devices/` | — | | | `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/` | 消息列表(assistant 消息可带 `usage`、`reasoning`);不存在 404 | | `DELETE /api/agent/conversations/` | 删除会话及其消息 | | `POST /api/agent/conversations//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_.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` 免登录) | | 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)。