Files
auto_control/doc/API.md
T
butubb 104964aa53 feat: 动作经验库(agent_action)——成功步骤蒸馏命名动作(带元素定位/禁坐标)+ 执行前召回注入
- 新表 agent_action(name/app/aliases/params/steps/preconditions/hits/时间),
  独立于人工维护的 custom_action(2B 决策):AI 自学动作不污染手建动作
- 沉淀:任务成功后从**成功**工具轨迹(_ACTION_TOOLS: open_app/tap_text/tap_element/
  type_text/clipboard/swipe/press_key/wake/sleep)用模型蒸馏为命名动作;steps 用
  编辑器 schema,**必须元素定位**(xpath/text/resourceId/description…),
  **显式剔除 click_xy 等坐标类**;on_tool 记录带 result 的结构化轨迹以判成败
- 兼容模型形状漂移:顶层 {action,params} 自动归一为 {name,steps};宽容 JSON 解析
  (围栏/尾逗号/中文引号/坏对象逐条抢救),实测模型常返回带语法错误的 JSON
- 召回:执行前按动作名/别名命中(或相似度≥0.34)取 top3,注入 system prompt
  「可复用动作」段(含元素定位),模型可跳过重新探索;hits 回写
- 文档同步:ARCHITECTURE §3.6(agent_action 表)、API.md(🧠 动作经验 伪卡片 + 动作库
  说明)、AI_TASK_GEN P1(沉淀进展)
实测:跑「打开抖音,点搜索」→ 沉淀「打开抖音」;下一轮同指令命中并注入;日志
「命中可复用动作 1 个」「动作提炼: 轨迹 5 步, 成功可沉淀 1 步」「动作经验已保存 1 条」
2026-09-10 13:53:04 +08:00

1322 lines
38 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.
# API 接口文档
`platform-tools` Web 后台提供 JSON API,绝大多数接口需登录后访问(Flask-Login session 认证);免登录例外见下方权限模型。
**Base URL**:`http://localhost:18050`
**认证方式**:Cookie Session(先 POST `/login` 获取 session cookie,后续请求带上)
**通用响应格式**:
```json
{"ok": true, "data": "..."}
{"ok": false, "error": "错误信息"}
```
---
**权限模型**(v2 起):
- 所有接口需登录(Flask-Login session);**免登录例外**:`GET /api/health`(探活)、`GET /locate`(设备端定位页,只显示 serial 文本)、`/login` 与静态资源
- **多数查看类 GET**(状态/任务/分组/自定义动作/APK 列表等)仅需登录即可用;设备维护/看屏/元素抓取类 GET 需对应 `devices` 权限
- **写操作按权限位授权**(管理员拥有全部权限):
| 权限位 | 中文 | 覆盖接口 |
|--------|------|---------|
| `tasks` | 任务管理 | 任务/自定义动作/分组的增删改、启停、立即执行 |
| `devices` | 设备控制 | 停止设备、清除异常、定位、前台扫描、远程看屏/触控、元素抓取、设备池管理、自动发现 |
| `apks` | 应用管理 | APK 上传、安装、删除 |
| `logs` | 日志查看 | `GET /api/logs` |
- **仅管理员可用**(普通用户即使被授予业务权限也无法访问):AI 控制台 `/api/agent/*`、系统备份 `/api/system/backup/*`、Tailscale `/api/tailscale/*`、用户管理 `/api/users`、adb 终端 `/api/adb/*`、工具 `/api/tools/*`
- 403 文案两种:业务权限缺失 → `{"ok": false, "error": "无权限执行此操作(需要权限: X)"}`;仅管理员接口被非管理员访问 → `{"ok": false, "error": "仅管理员可执行此操作"}`
- 当前用户权限查询:`GET /api/me`
---
## 1. 认证
### POST /login
用户登录。
**请求**(form-data):
| 参数 | 类型 | 说明 |
|------|------|------|
| username | string | 用户名 |
| password | string | 密码 |
**响应**:成功重定向到 `/`,失败返回登录页(含 error 信息)。
### GET /logout
登出,重定向到登录页。
### GET /api/me
当前登录用户信息(含权限位),前端据此隐藏无权限的功能入口。需登录。
**响应**:
```json
{"ok": true, "user": {
"id": 1, "username": "admin", "is_admin": true,
"perms": ["tasks", "devices", "apks", "logs"]
}}
```
管理员返回全部权限位;普通用户返回其被授予的业务权限数组。
### GET /api/csrf
获取当前会话的 CSRF token(登录后先获取一次;变更类请求需在 `X-CSRF-Token` 请求头携带)。
**响应**:
```json
{"ok": true, "token": "…"}
```
---
## 2. 页面路由
### GET /
单页应用首页(需登录)。响应头设置 `Cache-Control: no-store, no-cache, must-revalidate, max-age=0`(并带 `Pragma: no-cache`)防止缓存。
### GET /login
登录页面(GET)。
### GET /wall
监控大屏页面(需登录,全屏深色控制室风格,供挂墙/电视展示):设备卡片网格
(缩略图/型号/状态/当前动作/进度)、顶部统计与时钟;状态每 5s 刷新、缩略图每 2.5s 轮询。
20 台设备整体开销约 0.2 核 CPU + 100KB/s 带宽,普通电脑无压力。
---
## 3. 设备状态
### GET /api/status
获取设备池 + Worker 综合状态(带 5 秒缓存;worker 状态实时读内存)。需登录。
字段说明:`server_time` 服务端时间戳;`fg_scanning`/`fg_last_scan` 前台 App 扫描状态;`devices` 设备数组。
**响应**:
```json
{
"ok": true,
"server_time": 1700000000.0,
"fg_scanning": false,
"fg_last_scan": 1700000000.0,
"devices": [
{
"serial": "192.168.1.100:5555",
"model": "Pixel 6",
"device_name": "测试机1",
"present": true,
"ready": true,
"owner": "",
"worker_status": "running",
"foreground_app": "抖音",
"progress": {"done": 5, "total": 80, "unit": "视频", "action_counts": {"like": 3}},
"current_action": "观看视频 6",
"last_error": "",
"last_warning": "",
"running_job": "",
"task_job": "",
"attempt": 0,
"end_time": 0
}
]
}
```
`worker_status` 取值:`idle` / `connecting` / `running` / `done` / `error` / `failed`
### GET /api/summary
失败/异常任务汇总(供监控页"异常汇总"面板),观察设备长期健康度。需登录。
**响应**:
```json
{
"ok": true,
"counts": {"total": 8, "running": 1, "done": 5, "error": 1, "failed": 1, "idle": 0},
"errors": [
{"serial": "192.168.1.100:5555", "model": "Pixel 6", "status": "failed",
"last_error": "重试3次失败", "task": "抖音养号", "attempt": 3, "updated": 1700000000.0}
]
}
```
`counts` 各状态计数;`errors` 为 `error`/`failed` 且有 `last_error` 的异常设备
(按最近心跳倒序,最多 50 条;已不在设备池的陈旧失败记录不展示)。
### GET /api/health
轻量健康检查(免登录,供运维探活):进程存活 + 设备/任务摘要,不暴露敏感信息。
**响应**:
```json
{"ok": true, "status": "up", "time": 1700000000.0, "device_total": 8,
"device_online": 6, "device_running": 2, "device_error": 1, "jobs": 3}
```
### POST /api/scan_foreground
手动触发前台 App 扫描(后台异步执行,不打扰设备)。
(权限:设备控制)
**响应**:
```json
{"ok": true, "msg": "扫描已启动"}
{"ok": false, "error": "已有扫描在进行中"}
```
### GET /api/devices
返回所有在线设备 serial 列表(供分组表单勾选用)。
**响应**:
```json
{"ok": true, "devices": ["192.168.1.100:5555", "192.168.1.101:5555"]}
```
### GET /api/devices/<serial>/apps
获取指定设备上已安装的应用列表(包名 + versionCode/versionName + APK 路径 + 应用名)。需登录。
**响应**:
```json
{"ok": true, "apps": [
{"package": "com.ss.android.ugc.aweme", "path": "/data/app/.../base.apk",
"version_code": 2500, "version_name": "25.0.0", "label": "抖音"}
]}
```
### GET /api/device/screenshot
获取设备当前画面截图(PNG)。
**参数**:
| 参数 | 类型 | 说明 |
|------|------|------|
| serial | string | 设备 serial |
| t | int | 时间戳(避免缓存,前端自动加) |
**响应**:成功返回 `image/png`,失败返回 JSON 错误。
> 用 `adb exec-out screencap -p`,只读操作,任务运行中调用安全。
---
## 4. 任务类型
### GET /api/task_types
返回所有已注册任务类型。
**响应**:
```json
{
"ok": true,
"task_types": [
{
"task_type": "douyin_nurture",
"name": "抖音养号",
"description": "自动观看抖音视频...",
"default_params": {"watch_count": 80, "...": "..."}
},
{
"task_type": "generic_steps",
"name": "通用步骤",
"description": "通过步骤编辑器编排...",
"default_params": {"steps": [...]}
}
]
}
```
### GET /api/actions
返回指定任务类型支持的专属操作。
**参数**:
| 参数 | 类型 | 说明 |
|------|------|------|
| task_type | string | 任务类型 |
**响应**:
```json
{
"ok": true,
"actions": [
{"action_type": "like", "name": "点赞", "description": "...", "default_params": {"rate": 0.3}}
]
}
```
---
## 5. 任务计划 CRUD
### GET /api/jobs
列出所有任务计划。
**响应**:
```json
{
"ok": true,
"jobs": [{"id": "abc123", "name": "抖音养号", "task_type": "douyin_nurture",
"next_run": "2026-08-12 09:00", "...": "..."}],
"task_types": [...]
}
```
`next_run`:下次真正执行时间(已按运行窗口跳过窗口外触发点,格式 `YYYY-MM-DD HH:MM`);手动任务/已停用为 `null`。
### POST /api/jobs
创建任务计划。
(权限:任务管理)
**请求**(JSON):
```json
{
"name": "抖音养号",
"task_type": "douyin_nurture",
"target": {"mode": "all"},
"params": {"watch_count": 50, "actions": {"like": {"params": {"rate": 0.5}}}},
"schedule": {"mode": "cron", "cron": "0 */2 * * *"},
"retry": {"max_attempts": 3, "delay": 60},
"enabled": true
}
```
**响应**:
```json
{"ok": true, "msg": "任务已创建", "job": {"id": "abc123", "...": "..."}}
```
`schedule` 字段格式:
| 字段 | 说明 |
|------|------|
| `mode` | `once` 手动 / `cron` 定时启动 / `cron_stop` 定时启动+停止 |
| `cron` | 标准 5 段 cron:`分 时 日 月 周`(周 `0`/`7`=周日);如 `0 */2 * * *` 每 2 小时整点 |
| `stop_cron` | (cron_stop 必填)到点停止本任务 worker |
| `window` | 可选,运行窗口 `{"start": "21:00", "end": "09:00"}`(每天重复,支持跨午夜)。窗口外定时触发和手动执行(`POST /api/jobs/:id/run`)均不启动,手动执行返回错误提示 |
### PUT /api/jobs/<job_id>
更新任务计划。只需传要更新的字段。
(权限:任务管理)
**请求**(JSON):
```json
{"params": {"watch_count": 100}}
```
**响应**:
```json
{"ok": true, "msg": "任务已更新", "job": {"...": "..."}}
```
### DELETE /api/jobs/<job_id>
删除任务计划。不存在返回 404。
(权限:任务管理)
**响应**:
```json
{"ok": true, "msg": "任务已删除"}
```
### POST /api/jobs/<job_id>/run
立即执行任务(异步,不阻塞)。
(权限:任务管理)
**响应**:
```json
{"ok": true, "msg": "任务 抖音养号 已触发"}
```
### POST /api/jobs/<job_id>/toggle
启用/停用任务。
**请求**(JSON):
```json
{"enabled": false}
```
**响应**:
```json
{"ok": true, "msg": "任务已停用"}
```
---
## 6. 设备分组
### GET /api/groups
列出所有分组。
**响应**:
```json
{
"ok": true,
"groups": [
{"name": "A组", "serials": ["192.168.1.100:5555"], "description": "测试组"}
]
}
```
### POST /api/groups
创建分组。重名返回 400。
(权限:任务管理)
**请求**(JSON):
```json
{"name": "A组", "serials": ["192.168.1.100:5555"], "description": "测试组"}
```
**响应**:`{"ok": true, "msg": "分组已创建"}`
### PUT /api/groups/<name>
更新分组(serials/description 按需传字段)。`<name>` 不存在返回 404。
(权限:任务管理)
**请求**(JSON):
```json
{"serials": ["192.168.1.100:5555", "192.168.1.101:5555"], "description": "更新描述"}
```
**响应**:`{"ok": true, "msg": "分组已更新"}`
### DELETE /api/groups/<name>
删除分组。不存在返回 404。
(权限:任务管理)
**响应**:`{"ok": true, "msg": "分组已删除"}`
---
## 7. 运行控制
### POST /api/stop_device
停止单台设备的 worker(并阻止后续重试)。
(权限:设备控制)
**请求**(JSON):
```json
{"serial": "192.168.1.100:5555"}
```
**响应**:
```json
{"ok": true, "msg": "已发送停止信号给 192.168.1.100:5555"}
```
### POST /api/stop_all
停止所有运行中的 worker。
(权限:设备控制)
**响应**:
```json
{"ok": true, "stopped": ["192.168.1.100:5555", "192.168.1.101:5555"]}
```
> 旧「释放设备占用」端点已随 STF 摘除移除,此接口不再存在;设备互斥由调度器内存锁保证。
### POST /api/device/clear_error
清除单台设备的异常状态(`error`/`failed` → `idle`),供设备列表"清除异常"按钮使用。
设备正在运行或等待重试时返回 400。
(权限:设备控制)
**请求**(JSON):
```json
{"serial": "192.168.1.100:5555"}
```
**响应**:
```json
{"ok": true, "msg": "已清除 192.168.1.100:5555 的异常状态"}
```
### POST /api/device/clear_all_errors
一键清除所有异常/失败设备(自动跳过正在运行/等待重试的)。
(权限:设备控制)
**响应**:
```json
{"ok": true, "cleared": 2, "msg": "已清除 2 台设备的异常状态"}
```
---
## 8. 用户管理
用户管理接口**仅管理员可用**(非管理员返回 403 `仅管理员可执行此操作`)。`uid` 为用户 id(整数)。
### GET /api/users
列出所有用户。
**响应**:
```json
{"ok": true, "users": [
{"id": 1, "username": "admin", "is_admin": true, "perms": []},
{"id": 2, "username": "user1", "is_admin": false, "perms": ["tasks", "devices"]}
]}
```
`perms`:用户被授予的业务权限位数组(存储值;管理员以 `is_admin` 为准,perms 照常保存,取消管理员后按 perms 生效)。
### POST /api/users
创建用户。
**请求**(JSON):
```json
{"username": "user1", "password": "pass123", "is_admin": false, "perms": ["tasks"]}
```
`perms` 可选,默认无业务权限;`is_admin` 默认 false。
**响应**:`{"ok": true, "msg": "用户已创建"}`
### PUT /api/users/<uid>
更新用户(改密码 / 管理员权限 / 权限位),只需传要改的字段。`uid` 不存在返回 404。
**请求**(JSON):
```json
{"password": "newpass", "is_admin": true}
```
**响应**:`{"ok": true, "msg": "用户已更新"}`
> 不能取消最后一个管理员(返回 400)。
### DELETE /api/users/<uid>
删除用户。`uid` 不存在返回 404。
**响应**:`{"ok": true, "msg": "用户已删除"}`
> 不能删除默认管理员 `admin`、不能删除当前登录用户、也不能删除最后一个管理员(均返回 400)。
---
## 9. 日志
### GET /api/logs
查看日志文件内容。
(权限:日志查看)
**参数**:
| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| file | string | `core.log` | 日志文件名 |
| lines | int | 300 | 返回最后 N 行 |
**可选文件**:`core.log` / `task.log` / `web.log` / `action.log`
**响应**:
```json
{
"ok": true,
"content": "2026-08-08 10:00:00 [INFO] [core.worker] ...",
"file": "core.log",
"files": ["core.log", "task.log", "web.log", "action.log"]
}
```
`files`:可选日志文件名数组。
---
## 10. 自定义动作
### GET /api/custom_actions
列出所有自定义动作(步骤打包)。需登录。
**响应**:
```json
{"ok": true, "actions": [
{"id": "a1b2c3d4", "name": "登录流程", "icon": "📦",
"steps": [{"type": "click", "...": "..."}], "created_at": "2026-08-08 10:00:00"}
]}
```
### POST /api/custom_actions
创建自定义动作。
(权限:任务管理)
**请求**(JSON):
```json
{"name": "登录流程", "icon": "📦", "steps": [{"type": "click", "...": "..."}]}
```
**响应**:`{"ok": true, "msg": "动作已保存", "action": {...}}`
### PUT /api/custom_actions/<action_id>
更新自定义动作(name/icon/steps,按需传字段)。不存在返回 404。
(权限:任务管理)
**请求**(JSON):
```json
{"name": "登录流程 v2", "steps": [{"type": "click", "...": "..."}]}
```
**响应**:`{"ok": true, "msg": "已更新", "action": {...}}`
### DELETE /api/custom_actions/<action_id>
删除自定义动作。不存在返回 404。
(权限:任务管理)
**响应**:`{"ok": true, "msg": "已删除"}`
---
## 11. 元素抓取(uiauto2)
### GET /api/uiauto/status
探测 uiauto2 本地服务是否运行。
**响应**:
```json
{"ok": true, "running": true}
```
### GET /api/uiauto/devices
获取 uiauto2 已连接的设备列表。uiautodev 本地服务未运行(或列表获取失败)返回 503。
(权限:设备控制)
**响应**:
```json
{"ok": true, "devices": [{"serial": "192.168.1.100:5555", "model": "Pixel 6"}]}
```
### GET /api/uiauto/screenshot
通过 uiauto2 获取设备截图(JPEG)。uiautodev 本地服务未运行返回 503。
(权限:设备控制)
**参数**:
| 参数 | 类型 | 说明 |
|------|------|------|
| serial | string | 设备 serial |
**响应**:成功返回 `image/jpeg`,失败返回 JSON 错误。
### GET /api/uiauto/elements
获取设备 UI 元素树。uiautodev 本地服务未运行返回 503。
(权限:设备控制)
**参数**:
| 参数 | 类型 | 说明 |
|------|------|------|
| serial | string | 设备 serial |
**响应**:
```json
{
"ok": true,
"elements": [
{
"depth": 0,
"name": "android.widget.FrameLayout",
"resource_id": "",
"text": "",
"description": "",
"class": "android.widget.FrameLayout",
"bounds": "[0,0][1080,2400]",
"suggested": {"type": "xpath", "value": "//*"}
}
]
}
```
---
## 12. 应用管理(APK)
### GET /api/apks
列出所有已上传的 APK。
**响应**:
```json
{
"ok": true,
"apks": [
{
"id": "abc123",
"display_name": "抖音",
"package_name": "com.ss.android.ugc.aweme",
"version_name": "25.0.0",
"version_code": 2500,
"size": 104857600,
"upload_time": "2026-08-08 10:00:00"
}
]
}
```
### POST /api/apks/upload
上传 APK 文件(自动解析包名/版本/应用名)。
(权限:应用管理)
**请求**(multipart/form-data):
| 字段 | 类型 | 说明 |
|------|------|------|
| file | file | APK 文件 |
**响应**:
```json
{"ok": true, "apk": {"...": "..."}, "msg": "上传成功: 抖音"}
```
### DELETE /api/apks/<apk_id>
删除 APK 文件和记录。
(权限:应用管理)
**响应**:`{"ok": true, "msg": "..."}`;删除失败返回 400。
### POST /api/apks/install
批量安装 APK 到指定设备。
(权限:应用管理)
**请求**(JSON):
```json
{"apk_id": "abc123", "serials": ["192.168.1.100:5555", "192.168.1.101:5555"]}
```
**响应**:
```json
{"ok": true, "msg": "开始安装 抖音 到 2 台设备"}
```
### GET /api/apks/install/devices
可安装设备列表:设备池在线设备 + 本机 adb 设备(含 USB 有线连接)。
安装弹窗用此列表,USB 设备安装时跳过 adb connect 直接安装。
`source` 取值:`pool`=设备池在线;`usb`=本机 USB 有线(serial 无冒号);`adb`=本机网络 adb(serial 含冒号)。
(权限:应用管理)
**响应**:
```json
{"ok": true, "devices": [
{"serial": "100.100.10.11:5555", "model": "22120RN86C", "source": "pool"},
{"serial": "192.168.1.5:5555", "model": "", "source": "adb"},
{"serial": "ZY322ABCDEF", "model": "", "source": "usb"}
]}
```
### GET /api/apks/install/status
获取安装任务实时状态。
**响应**:
```json
{
"ok": true,
"status": {
"apk_name": "抖音",
"finished": false,
"total": 2,
"success": 1,
"failed": 0,
"skipped": 0,
"installing": 1,
"pending": 0,
"items": {
"192.168.1.100:5555": {"name": "Pixel 6", "status": "success", "msg": "安装成功(已验证)"},
"192.168.1.101:5555": {"name": "Pixel 7", "status": "installing", "msg": "正在安装..."}
}
}
}
```
---
## 13. 维护 / 工具
设备池管理、自动发现、远程看屏/触控、定位、维护终端等集中在"工具"页(页内子分栏)。
**权限说明**:设备池管理、自动发现、远程看屏/触控、定位等接口需 `devices` 权限;
**adb 终端仅管理员可用**(见各节标注)。
### GET /api/devices/pool
设备池清单(SQLite devices 表),含实时在线状态。**权限**:`devices`。
**响应**:
```json
{"ok": true, "devices": [
{"serial": "100.100.10.20:5555", "name": "", "model": "22120RN86C",
"enabled": true, "online": true, "note": "", "created_at": "2026-08-17 11:10"}
]}
```
### POST /api/devices/pool/add
添加/更新设备(upsert)。**权限**:`devices`。
**请求**(JSON):`{"serial": "100.100.10.20:5555", "name": "备注", "note": ""}`
IP:5555 设备添加后立即尝试 adb connect,并后台采集型号(getprop ro.product.model)。
### POST /api/devices/pool/toggle
启用/停用设备(停用后不参与任务调度)。**权限**:`devices`。
**请求**(JSON):`{"serial": "...", "enabled": false}`
### POST /api/devices/pool/remove
从设备池删除(不再参与调度,不影响设备本身)。**权限**:`devices`。
**请求**(JSON):`{"serial": "..."}`
### POST /api/devices/pool/reconnect
一键重连:并发 adb connect 池内全部 IP:5555 设备(后台执行),完成后自动批量刷新型号。**权限**:`devices`。
### POST /api/devices/pool/refresh_models
批量采集池内在线设备的型号(后台执行)。**权限**:`devices`。
### GET /api/devices/discovery
自动发现状态 + 待连接列表(pending)+ 正式池断联设备。前端约 10s 轮询一次。**权限**:`devices`。
**响应**:
```json
{
"ok": true,
"enabled": true, "subnets": ["192.168.1.0/24"], "interval": 60, "port": 5555,
"scanning": false,
"last_scan": "2026-08-08 10:00", "last_result": {"found": 12, "verified": 3, "new": 1},
"last_error": "", "pending_count": 2,
"pending": [{"serial": "192.168.20.5:5555", "source": "lan",
"first_seen": "2026-08-08 10:00", "last_seen": "2026-08-08 10:05", "online": true}],
"pool_offline": [{"serial": "192.168.1.100:5555", "model": "Pixel 6", "name": "测试机1"}]
}
```
`pending` 只列当前在线设备(离线候选不可确认,下轮扫描自动更新);`pool_offline`
为正式池中已断联设备(发现线程每轮自动重连,也可手动触发重连)。
### POST /api/devices/discovery/scan
手动触发一轮扫描(后台执行,约 5-30 秒)。**权限**:`devices`。
扫描只验证并把新设备放进待连接池(pending),**确认后才入正式设备池**。
**响应**:
```json
{"ok": true, "msg": "扫描已启动(后台执行,约 5-30 秒)",
"result": {"found": 12, "verified": 3, "new": 1}}
```
扫描进行中返回 409;未配置网段返回 400 并附原因。
### POST /api/devices/discovery/confirm
确认连接:把待连接设备加入正式设备池并后台 adb connect/采型号。**权限**:`devices`。
**请求**(JSON):`{"serial": "192.168.20.5:5555", "name": "客厅机"}`
**响应**:`{"ok": true, "msg": "已加入设备池", "is_new": true}`
### POST /api/devices/discovery/ignore
忽略:从待连接列表删除(下轮扫描可能再次发现)。**权限**:`devices`。
**请求**(JSON):`{"serial": "..."}`
**响应**:`{"ok": true, "msg": "已忽略"}`
### POST /api/devices/discovery/reconnect
手动立即重连正式池中的断联设备(后台 adb connect + 采型号)。**权限**:`devices`。
日常无需手动——发现线程每轮(默认 60s)自动重连断联设备。
**请求**(JSON):`{"serial": "..."}`
**响应**:`{"ok": true, "msg": "重连已启动(约 5-15 秒生效)"}`
### POST /api/devices/discovery/settings
保存自动发现配置(部分字段更新)。**权限**:`devices`。
**请求**(JSON):`{"enabled": true, "subnets": ["192.168.1.0/24"], "interval": 60, "port": 5555}`
`interval` 需在 10-3600 秒之间;`subnets` 逐项校验 CIDR,非法返回 400。
**响应**:`{"ok": true, "msg": "已保存"}`
### GET /api/screen/stream
远程看屏:MJPEG 实时画面流(`multipart/x-mixed-replace`)。**权限**:`devices`。
`?serial=xxx` 指定设备;浏览器 `<img src>` 直接渲染,客户端断开自动停止。数据源 u2(atx-agent minicap,约 3-6 帧/秒)。
### GET /api/screen/thumb
大屏缩略图:单张 JPEG(360px 宽,质量 55)。**权限**:`devices`。
`?serial=xxx` 指定设备。监控大屏按设备每 ~2.5s 轮询一帧(页面不可见时暂停);
一次性请求(非流),与任务并发安全(与任务截图同走 u2 minicap)。
### POST /api/screen/tap
点击设备屏幕。**权限**:`devices`。
**请求**(JSON):`{"serial": "...", "x": 360, "y": 800}`(设备原生分辨率坐标)
### POST /api/screen/swipe
滑动。**请求**(JSON):`{"serial": "...", "x1": 360, "y1": 1200, "x2": 360, "y2": 600, "duration": 0.2}`
### POST /api/screen/key
按键。**请求**(JSON):`{"serial": "...", "key": "back"}`。
支持:back/home/recent/menu/power/volume_up/volume_down/enter/delete/search/camera
### POST /api/screen/text
输入文字(需焦点在输入框)。**请求**(JSON):`{"serial": "...", "text": "你好"}`
### POST /api/screen/tap_text
按屏幕文字点击:先在 UI 树里做 text/description 子串匹配点元素中心(原生控件);
未命中则截图 OCR 找文字中心(WebView/图片/画布渲染文字)。**权限**:`devices`。
**请求**(JSON):`{"serial": "...", "text": "立即下载"}`
**响应**:
```json
{"ok": true, "found": true, "method": "ui", "matched": "立即下载", "x": 540, "y": 1200}
```
`method`:`ui` 或 `ocr`;`found=false` 表示屏幕确实没有该文字(业务结果,非设备错误)。
### GET /api/screen/size
获取设备屏幕原生分辨率(只读,供坐标换算——截图常是缩放图、操作需原生坐标)。**权限**:`devices`。
离线/不可达返回 503。
`?serial=xxx`
**响应**:`{"ok": true, "width": 1080, "height": 2400}`
### POST /api/device/screen_all
批量亮屏/息屏(并发)。**权限**:`devices`。
**请求**(JSON):
```json
{"mode": "on", "serials": ["100.100.10.11:5555"]}
```
`serials` 可选:指定设备(离线自动过滤);不带则作用于全部在线设备。息屏会中断运行中的任务,前端有确认提示。
### POST /api/device/locate
定位设备:点亮屏幕并解锁(WAKEUP → dismiss-keyguard → MENU 兜底)。
`show=true` 时额外用设备浏览器打开平台 `/locate` 大字定位页(更醒目,但会切换前台,任务运行中慎用)。**权限**:`devices`。
**请求**(JSON):`{"serial": "192.168.1.100:5555", "show": true}`
**响应**:`{"ok": true, "msg": "192.168.1.100:5555 屏幕已点亮;已打开大字定位页(按返回键退出)"}`
### POST /api/device/locate/stop
结束定位:优先 force-stop 定位时启动的浏览器(无论前后台都能关掉);无记录时前台是
浏览器则 force-stop,否则按返回键轻量退出(不误杀任务应用)。**权限**:`devices`。
**请求**(JSON):`{"serial": "..."}`
**响应**:`{"ok": true, "msg": "已关闭浏览器 com.android.chrome"}`
### GET /api/adb/devices
维护终端设备列表(**仅管理员**):本地 adb 已连接(含 offline)+ 设备池已配置设备(标记 `pool`)。
供终端设备选择器使用——选中后前端自动附加 `-s <serial>`。
**响应**:
```json
{"ok": true, "devices": [
{"serial": "100.100.10.11:5555", "state": "device"},
{"serial": "100.100.10.13:5555", "state": "offline"},
{"serial": "100.100.10.12:5555", "state": "pool"}
]}
```
### POST /api/adb/cmd
adb 远程终端(**仅管理员**):用平台 adb 二进制执行任意 adb 命令(20s 超时)。
**请求**(JSON):
```json
{"cmd": "adb -s 100.100.10.11:5555 shell ls /sdcard"}
```
(开头的 `adb` 前缀可省略)
**安全红线**:包含 `kill-server` / `disconnect` 的命令直接拒绝(400)——会断开共享的 adb transport,导致全部设备连接重建。
**响应**:
```json
{"ok": true, "stdout": "...", "stderr": "", "code": 0}
```
---
## 14. 步骤测试(需"设备控制"权限)
### POST /api/steps/test
在指定设备上单步试执行(步骤编辑器"测试此步骤"按钮),验证选择器是否命中。
只读连接(adb connect + u2),与运行中任务互不干扰。
**请求**(JSON):
```json
{
"serial": "100.100.10.11:5555",
"step": {"type": "click", "label": "测试", "params": {"selector_type": "xpath", "selector_value": "//*[@resource-id=\"x\"]"}}
}
```
**响应**:
```json
{"ok": true, "msg": "步骤已执行(测试)", "result": "命中"}
```
`result`:`命中` / `未找到` / `已执行`(无选择器命中语义的步骤)。
无"设备控制"权限返回 403。
---
## 15. Tailscale 管理(仅管理员)
工具页"Tailscale 管理"子分栏,调用 Tailscale 官方 API v2。所有接口仅管理员可用。
前置:`.env` 配置 `TAILSCALE_API_KEY`(Settings → API Access Tokens)与
`TAILSCALE_TAILNET`(tailnet 名,个人账号一般为邮箱前缀)。
`GET /api/tailscale/status` 未配置时返回 `200 {"ok": true, "configured": false}` 并附 `hint` 提示;
其余接口在未配置/调用失败时返回 `502` 并附错误信息。
设备 IP 由 tailnet 分配,API 不可修改,列表只读展示。
### GET /api/tailscale/status
配置状态检查。
**响应**:
```json
{"ok": true, "configured": false, "hint": "TAILSCALE_API_KEY(...);TAILSCALE_TAILNET(...)"}
```
### GET /api/tailscale/devices
列出 tailnet 全部设备。
**响应**:
```json
{"ok": true, "devices": [
{"id": "d1", "name": "dev-a", "hostname": "dev-a", "os": "linux",
"addresses": ["100.100.10.11"], "authorized": true,
"key_expiry_disabled": false, "online": true, "last_seen": "...", "tags": []}
]}
```
### POST /api/tailscale/devices/:device_id
更新设备(按传入字段分发到 Tailscale 专属端点,`POST /device/{id}` 本身是 405):
`name` → `/name` 显示名;`authorized` → `/authorized` 授权开关;
`key_expiry_disabled` → `/key`(true=密钥永不过期,即关闭设备密钥验证,恢复后按原定过期时间执行)。
**请求**(JSON):
```json
{"key_expiry_disabled": true}
```
### POST /api/tailscale/devices/:device_id/ip
设置设备的 Tailscale IPv4 地址(未公开端点,实测可用)。
**请求**(JSON):
```json
{"ipv4": "100.100.10.16"}
```
⚠ 改 IP 会断开设备当前 tailscale 会话;平台设备池 serial 随之变化,需同步更新设备池/分组/任务目标。
### DELETE /api/tailscale/devices/:device_id
从 tailnet 移除设备(下次上线需重新授权)。
### POST /api/tailscale/authkey
生成设备接入 auth key(key 只返回一次)。
**请求**(JSON):
```json
{"description": "新设备接入", "reusable": false, "ephemeral": false,
"preauthorized": true, "expiry_seconds": 3600}
```
**响应**:
```json
{"ok": true, "key": "tskey-auth-...", "id": "k1", "expires": "2026-08-11T01:00:00Z"}
```
---
## 16. 工具(仅管理员)
工具页(剪贴板注入 / 应用版本管理)接口,均仅管理员可用。
### POST /api/tools/clipboard/set
剪贴板注入:把指定文字写入一台或多台设备的剪贴板。
**请求**(JSON):
```json
{"serials": ["100.100.10.11:5555", "0123456789ABCDEF"], "text": "要注入的文字"}
```
设备来源与维护终端一致(本地 adb 含 USB + 设备池)。
实现:通过 ClipInject(`com.example.clipinject`)透明 Activity 前台聚焦后写入剪贴板
(shell 启动前台 Activity 不受后台启动限制),再用 u2 读回比对校验,支持中文/引号/换行。
不再使用 u2 setClipboard / 自动推送 atx-agent(Android 10+ 禁止后台写剪贴板,旧 u2
调用"成功"但内容被系统静默丢弃)。
IP 设备先 adb connect(已连接跳过,绝不 disconnect);**设备未安装 ClipInject 时** am start
返回 unable to resolve Intent,接口明确报错提示先安装。
**响应**:
```json
{"ok": true, "results": {"100.100.10.11:5555": {"ok": true, "msg": "已注入"}},
"ok_count": 1, "fail_count": 0, "error": null}
```
### POST /api/tools/appver
应用版本管理:查询所有设备上指定包名的安装情况与版本号(并发 10 台)。
**请求**(JSON):
```json
{"pkg": "com.ss.android.ugc.aweme"}
```
**响应**:
```json
{"ok": true, "total": 8, "fail": 0,
"results": {"100.100.10.11:5555": {"installed": true, "version_name": "28.5.0", "version_code": "280500"}}}
```
未安装返回 `installed: false`;查询失败的设备带 `error` 字段。
---
## 17. AI 控制台(仅管理员)
浏览器内 AI 助手(DeepSeek 式多会话):用 MCP 工具操作指定设备、多轮上下文延续、
成功后自动提炼经验记忆。**单实例:同时只允许一个 Agent 运行。** 以下接口均仅管理员可用。
### GET /api/agent/config
读 Agent 配置。`api_key` 打码回显在 `api_key_masked` 字段。
**响应**:
```json
{"ok": true, "api_base": "https://api.deepseek.com", "model": "deepseek-v4-flash-vision-exp",
"api_key_masked": "sk-***abcd", "default_serial": "", "max_steps": "40"}
```
### POST /api/agent/config
保存 Agent 配置(部分更新)。**请求**(JSON):
`{"api_base": "...", "model": "...", "api_key": "...", "default_serial": "...", "max_steps": 40}`
(`max_steps` 1-200,默认 40)
**响应**:`{"ok": true, "msg": "已保存"}`
### GET /api/agent/devices
AI 可用设备列表(在线状态 + 是否有任务运行,前端据此把 busy 设备禁选)。
**响应**:
```json
{"ok": true, "devices": [
{"serial": "192.168.1.100:5555", "model": "Pixel 6", "online": true,
"busy": false, "worker_status": "idle", "task_job": ""}
]}
```
### POST /api/agent/run
启动 Agent(后台线程执行,立即返回 `run_id`)。**请求**(JSON):
`{"prompt": "打开抖音并点赞前 3 条视频", "serial": "192.168.1.100:5555", "conversation_id": "abc..."}`
`serial` 也可省略、用配置的 `default_serial`;`conversation_id` 绑定会话(历史从会话加载)。
**校验**:未配 API Key/模型名 → 400;设备不在池/离线 → 400;设备正有任务运行 → 409;
已有 Agent 运行中 → 409。
**响应**:`{"ok": true, "run_id": "8f3a2c9d"}`
### GET /api/agent/run
当前 Agent 运行状态(多窗口/页面刷新恢复用)。
**响应**:
```json
{"ok": true, "state": "running"|"idle"|"done", "run_id": "...", "prompt": "...",
"serial": "...", "started": "10:00:01", "answer": "...", "error": "",
"history": [{"role": "user", "content": "..."}]}
```
### GET /api/agent/stream?run_id=
订阅事件流(SSE,EventSource)。事件:
- `event: delta` `{text, kind: content|reasoning}` — 流式文本增量
- `event: step` `{tool, args, image?}` — 工具调用完成(MCP 步骤,image 为缩略截图)。另有三类伪卡片:`tool="🧠 经验记忆"` 表示命中任务级经验(args 形如「命中 N 条同类历史经验,已注入参考:<配方摘要>」)或本轮已写入经验库;`tool="🧠 动作经验"` 表示命中**可复用动作**(「命中 N 个可复用动作,已注入参考:<动作名>」,执行前注入)或本轮已沉淀动作(「已沉淀 N 个可复用动作」,含元素定位、禁坐标)
- `event: done` `{answer}` — 完成
- `event: error` `{message}` — 失败(若因 MCP Server 未启动/不可达,message 为明确文案「MCP server(8033) 不可达 …」,不再是 SDK 原始的 `Server returned an error response`)
- 空闲时每 15s 发一行 `: keepalive` 注释防超时;`done`/`error` 后关流
### POST /api/agent/stop
中断当前运行的 Agent(下一个检查点生效,数秒内)。无运行中任务返回 400。
**响应**:`{"ok": true, "msg": "已请求停止"}`
### POST /api/agent/clear
清空当前对话历史。
**响应**:`{"ok": true, "msg": "已清空"}`
### GET /api/agent/conversations
会话列表(按最近更新倒序)。
**响应**:
```json
{"ok": true, "conversations": [
{"id": "abc...", "title": "打开抖音点赞", "updated_at": "2026-08-08 10:00", "count": 3}
]}
```
`count` 为轮数(用户+助手消息对数)。
### POST /api/agent/conversations
新建会话(空消息)。
**响应**:`{"ok": true, "id": "新会话id", "title": "新会话"}`
### GET /api/agent/conversations/<conv_id>
会话详情(全部消息文本)。不存在返回 404。
**响应**:
```json
{"ok": true, "id": "...", "title": "...", "messages": [{"role": "user", "content": "..."}],
"created_at": "...", "updated_at": "..."}
```
### DELETE /api/agent/conversations/<conv_id>
删除会话(消息一并删除,不可恢复)。
**响应**:`{"ok": true, "msg": "会话已删除"}`
### POST /api/agent/conversations/<conv_id>/rename
重命名会话。**请求**(JSON):`{"title": "新标题"}`
**响应**:`{"ok": true, "msg": "已重命名"}`
### GET /api/agent/experience
经验记忆库列表(自进化,含最近一次巡检结论)。另有一张**动作经验库**表 `agent_action`(命名动作 + 编辑器 schema 步骤 + 元素定位、禁坐标):任务成功后自动从**成功步骤**蒸馏沉淀,执行前按动作名/别名召回并注入;当前无独立查询接口(命中/沉淀在 AI 控制台的 🧠 卡片可见)。
**响应**:
```json
{"ok": true, "running": false, "last": "2026-08-08 03:47", "last_summary": "评审 5 条,建议删除 1 条(待人工确认)",
"experiences": [{"id": 1, "task_prompt": "打开抖音并点赞", "recipe": "...", "tool_seq": "...",
"hits": 3, "created_at": "...", "audit": {"verdict": "delete", "score": 3,
"reason": "...", "action": "pending", "at": "..."}}]}
```
`audit` 为最近一次 AI 巡检结论(无则 null)。
### POST /api/agent/experience/delete
人工确认删除经验(真删,巡检绝不自动删)。**请求**(JSON):`{"id": 1}`
**响应**:`{"ok": true, "msg": "经验 #1 已删除"}`
### POST /api/agent/experience/audit
手动触发一轮经验巡检(后台线程,AI 评审只建议不删)。巡检进行中返回 409。
**响应**:`{"ok": true, "msg": "巡检已启动,完成后刷新列表查看建议"}`
### POST /api/agent/experience/keep
人工保留经验(撤销"建议删除",后续巡检不再重复建议)。**请求**(JSON):`{"id": 1}`
**响应**:`{"ok": true, "msg": "经验 #1 已保留"}`
---
## 18. 系统备份(仅管理员)
整库备份导出/导入(users.db + 可选 APK 文件)。导入涉及整库替换,仅管理员可用。
### POST /api/system/backup/export
生成导出 zip 并作为附件返回(含 users.db 一致快照 + manifest.json + 可选 `apks/`)。
**请求**(JSON):`{"include_apk": true}`
**响应**:`application/zip` 附件下载(`download_name` 形如 `export_20260808_101000.zip`)。
### POST /api/system/backup/preview
上传备份文件(multipart 字段 `file`,支持 .zip 或 .db)→ 暂存并校验 → 返回预览。
校验失败返回 400。
**响应**:
```json
{"ok": true, "token": "12位hex", "preview": {
"file_name": "export_xxx.zip", "file_size": 123456,
"integrity": "ok", "schema_version": 4, "current_schema_version": 4,
"tables": [{"table": "user", "label": "用户", "rows": 3}],
"missing_optional": [], "warnings": ["备份为全量快照:含敏感信息,请妥善保管"]
}}
```
### POST /api/system/backup/apply
确认应用导入:自动备份当前库到 `BACKUP_DIR/pre_restore_*.db`(安全网),再把暂存库
落为「待生效恢复任务」。**重启 web_server 后生效**。token 无效/过期返回 400。
**请求**(JSON):`{"token": "12位hex"}`
**响应**:
```json
{"ok": true, "backup_name": "pre_restore_20260808_101000.db",
"message": "恢复任务已生成:当前库已自动备份,重启 web_server 后即应用导入的数据"}
```