From 24d57d3b961116e537e3586eba892d48d2aeb620 Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Thu, 10 Sep 2026 22:19:18 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20doc/=20=E5=85=A8=E9=87=8F=E9=87=8D?= =?UTF-8?q?=E6=95=B4=E2=80=94=E2=80=94=E6=8C=89=E7=8E=B0=E7=8A=B6=E9=87=8D?= =?UTF-8?q?=E5=86=99=E5=B9=B6=E5=BB=BA=E7=AB=8B=E6=96=87=E6=A1=A3=E7=B4=A2?= =?UTF-8?q?=E5=BC=95=EF=BC=9B=E9=A1=B9=E7=9B=AE=E7=BB=9F=E4=B8=80=E6=9B=B4?= =?UTF-8?q?=E5=90=8D=20auto=5Fcontrol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:文档长期落后于代码(Tab 数、任务类型、接口示例等多处与现状不符), 且信息分散重复。这次按当前代码状态逐篇重写,并建立统一的文档体系。 新增 - doc/README.md:文档总索引(文档地图 / 推荐阅读路径 / **文档维护约定**) - doc/DATA_MODEL.md:数据模型(7 张模型表 + 5 张非模型表、迁移机制、app_meta 键、 数据目录、备份覆盖清单与双向自检) - doc/AI_CONSOLE.md:AI 控制台机制(会话与 SSE、经验库/动作库蒸馏与召回、巡检、 Markdown 渲染、推理链、token 统计、故障排查) 重写(按现状,去掉过时与重复) - README.md:7 个 Tab、18 种步骤、设备生命周期、调度/窗口语义、常见问题;修掉 「6 个 Tab / 分组为顶级 Tab」等过时内容与损坏的目录树 - doc/ARCHITECTURE.md:补启动装配顺序(import 期副作用、A~G 七阶段)、线程与锁清单、 设备状态机、调度全链路、前端结构与实时通道、设计决策、**已知缺陷与踩坑清单**、扩展点 - doc/API.md:按蓝图重建「接口总索引」(107 条路由含鉴权)+ 分域详细说明 + 非 JSON 响应汇总 + 错误分支速查 - doc/TASK_DEV.md:18 种步骤全表(参数/默认值/语义)、容器与公共参数、 选择器与 XPath 序号语义、抓取器建议规则、新增任务类型骨架 - doc/DEPLOY.md:容器入口 start.sh 三件事、发布流程与检查清单、备份覆盖红线、 按现象分类的故障排查 - doc/DEVELOPMENT.md:流程/红线/本地开发/**测试与写测试的约定**/配置速查/文档同步 - doc/MCP.md:19 个工具的参数级清单、坐标空间、写门控三连、安全与审计 - doc/MCP_DESIGN.md、doc/AI_TASK_GEN.md:标注设计 vs 实现现状,补交叉链接 - doc/backlog/TODO.md:新增「已知缺陷」小节(含复现与影响)+ 已完成留档 - .env.example:按代码实际读取的键重写(补 USB/DISCOVERY/MCP/AGENT,删死配置) 其它 - 项目名统一 auto_control:README/文档/scripts/pack.py 产物名;代码内的 doc 章节引用(templates/admin/monitor.html)同步更新 - 校验:16 篇文档 156 条相对链接全部可解析;文档中的关键数字与代码核对一致 (19 个 MCP 工具 / 18 种步骤 / 12 张备份表 / 1 种任务类型) --- .env.example | 112 +- README.md | 594 +++++------ doc/AI_CONSOLE.md | 210 ++++ doc/AI_TASK_GEN.md | 5 +- doc/API.md | 1688 +++++++++---------------------- doc/ARCHITECTURE.md | 657 ++++++------ doc/DATA_MODEL.md | 246 +++++ doc/DEPLOY.md | 566 +++++------ doc/DEVELOPMENT.md | 387 +++---- doc/MCP.md | 242 +++-- doc/MCP_DESIGN.md | 6 +- doc/README.md | 64 ++ doc/TASK_DEV.md | 1403 +++++++------------------ doc/backlog/TODO.md | 141 ++- doc/staffdeck/JOB_SPEC.md | 2 + doc/staffdeck/KNOWLEDGE_BASE.md | 2 + scripts/pack.py | 4 +- templates/admin/monitor.html | 2 +- 18 files changed, 2664 insertions(+), 3667 deletions(-) create mode 100644 doc/AI_CONSOLE.md create mode 100644 doc/DATA_MODEL.md create mode 100644 doc/README.md diff --git a/.env.example b/.env.example index cda825d..06c9266 100644 --- a/.env.example +++ b/.env.example @@ -1,41 +1,91 @@ -# 环境变量示例:复制为 .env 并按需修改(.env 不进 git) -# 生产环境建议用环境变量/密钥管理注入,不要把真实 token 写进 git。 -# 全部可配置项见 config.py(_env() 读取,未配置时用代码内默认值); -# 标"必须"的项务必设置,其余可留空用默认。 +# ============================================================================== +# auto_control 环境变量示例 +# - 复制为项目根目录的 .env 再按需修改(.env 已被 .gitignore 排除,不会进 git) +# - 加载方式:config.py 逐行解析后用 os.environ.setdefault 注入 +# → 因此"真实环境变量"优先于 .env(容器/CI 里用 env 覆盖更方便) +# - 标【必须】的项务必设置;其余留空即用代码默认值 +# - 完整说明见 doc/DEVELOPMENT.md §4 配置速查、doc/DEPLOY.md §2.2 +# ============================================================================== -# ==================== STF 平台 ==================== -# STF 服务地址(默认 http://192.168.20.220:7100) -STF_URL=http://192.168.20.220:7100 - -# STF API Token(必须:STF 个人设置 → API Keys 生成) -STF_TOKEN=请填写你的STF_token - -# 启动时自动释放残留 STF 占用(单实例无人值守开 true;多实例共用账户勿开,会误放另一实例的任务) -# AUTO_RELEASE_STALE_OCCUPY=true - -# ==================== Web 会话 ==================== -# Web 会话密钥(必须:生产设置为随机长字符串,重启不失效) +# ==================== Web 服务 ==================== +# 会话密钥【必须,生产务必固定】:不配则每次启动随机生成,重启后登录态失效。 # 生成:python -c "import secrets; print(secrets.token_hex(32))" WEB_SECRET_KEY=请填写随机密钥 -# ==================== SSH 到部署机(维护页重启 STF / 工具页 STF 设备管理) ==================== -# SSH 目标(默认 stf@192.168.20.220) -# STF_SSH_TARGET=stf@192.168.20.220 +# 监听地址与端口是 config.py 里的常量(不进 .env):WEB_HOST=0.0.0.0、WEB_PORT=18050 +# 需要修改直接改 config.py(18050 是为了避开 Windows 动态端口段) -# SSH 认证密码(推荐):配置后走 paramiko **纯密码**登录(不碰本地密钥/SSH agent, -# 不会弹授权框,跨平台无需 sshpass);留空则退回系统 ssh 免密密钥(BatchMode=yes)。 -STF_SSH_PASSWORD=请填写SSH密码 -# 220 上 STF / adb 的 Docker 容器名(默认 stf / adb,一般不用改) -# STF_DOCKER_CONTAINER=stf -# STF_ADB_CONTAINER=adb +# ==================== 设备与 adb ==================== +# USB 设备(serial 无冒号)所在的部署机 —— 平台经它的 adb server 驱动这些设备。 +# 默认 100.100.10.1:5037(220 的 Tailscale IP + adb 容器端口,host 网络模式)。 +# USB_ADB_HOST=100.100.10.1 +# USB_ADB_PORT=5037 -# STF 设备池脚本路径(工具页"STF 设备管理"增删设备时读写其 DEVICES 列表,默认值见 config.py) -# STF_SCRIPT_PATH=/mnt/data/openstf/connect_devices.sh +# 设备自动发现(扫描网段找开放 5555 的设备)。 +# 扫描网段在「工具 → 设备池管理」界面里配置(存数据库 app_meta),此处只管端口与周期。 +# DISCOVERY_PORT=5555 +# DISCOVERY_INTERVAL=60 -# ==================== Tailscale 管理(工具页) ==================== -# API key(必须:Tailscale 后台 → Settings → API Access Tokens 生成) +# 把本机的 adb 客户端指向远程 adb server(注意:这三个键由 adb/adbutils 自己读取, +# 不是本项目代码读的;两个变量名都要设,adb 实际认 ADDRESS,部分库读 HOST)。 +# 用途:让本机 adb 直接看到 220 侧插着的 USB 设备。 +# ANDROID_ADB_SERVER_ADDRESS=192.168.20.220 +# ANDROID_ADB_SERVER_HOST=192.168.20.220 +# ANDROID_ADB_SERVER_PORT=5037 + + +# ==================== Tailscale 管理(工具 → Tailscale 管理) ==================== +# API key:Tailscale 后台 → Settings → API Access Tokens 生成 TAILSCALE_API_KEY=请填写Tailscale_API_key -# tailnet 名/ID(默认按邮箱前缀,如 1422726308@gmail.com) -# TAILSCALE_TAILNET=1422726308@gmail.com +# tailnet 名/ID(一般填登录邮箱,如 user@example.com) +# TAILSCALE_TAILNET=user@example.com + + +# ==================== MCP Server(外部 AI 接入,:8033) ==================== +# 注意:mcp_server 只读进程环境变量,**不读本文件**! +# 容器场景由 scripts/start.sh 用下面这些变量拉起进程; +# 本机手动启动请直接在命令行前加环境变量(见 doc/MCP.md §2)。 +# +# MCP_ENABLED=1 # 仅 start.sh 消费:=0 则不自动拉起 MCP +# MCP_ALLOW_WRITE=1 # 写操作总开关(0=只读;start.sh 内强制为 1) +# MCP_PLATFORM_URL=http://127.0.0.1:18050 +# MCP_PLATFORM_USER=admin +# MCP_PLATFORM_PASS=请填写平台admin密码 # 【改过 admin 密码必须同步,否则 MCP 登录失败】 +# MCP_ALLOWED_SERIALS= # 设备白名单(逗号分隔);空=不限制(语义缺口见 backlog) +# MCP_HTTP_HOST=0.0.0.0 +# MCP_HTTP_PORT=8033 +# MCP_SCREENSHOT_WIDTH=540 # 返回给模型的截图宽度 +# MCP_JPEG_QUALITY=70 +# MCP_AUDIT_FILE=/tmp/mcp_audit.log +# MCP_PLATFORM_TIMEOUT=30 + + +# ==================== AI Agent(mcp_agent,命令行/独立运行时用) ==================== +# 注意:同样只读进程环境变量、不读本文件。 +# Web 的「AI 控制台」配置走数据库(app_meta 的 agent_* 键),与本组变量互不影响。 +# +# AGENT_API_BASE=https://api.deepseek.com +# AGENT_MODEL=deepseek-v4-flash-vision-exp +# AGENT_API_KEY=请填写模型 API Key +# DEEPSEEK_API_KEY= # AGENT_API_KEY 的兼容别名 +# AGENT_MCP_URL=http://127.0.0.1:8033/mcp +# AGENT_DEFAULT_SERIAL= +# AGENT_MAX_STEPS=40 +# AGENT_TIMEOUT=120 +# AGENT_LANG=zh + + +# ==================== 开发/测试 ==================== +# 设置后不启动 cron 调度器(跑测试脚本时避免真实触发任务、占用设备) +# DISABLE_SCHEDULER=1 + + +# ============================================================================== +# 已废弃的历史配置(STF 已从代码层摘除,下列键代码不再使用,保留仅为兼容旧 .env) +# STF_URL / STF_TOKEN / STF_SSH_TARGET / STF_SSH_PASSWORD / +# STF_DOCKER_CONTAINER / STF_ADB_CONTAINER / STF_SCRIPT_PATH / +# AUTO_RELEASE_STALE_OCCUPY +# 迁移背景见 doc/STF_REMOVAL.md +# ============================================================================== diff --git a/README.md b/README.md index 671a819..d74ec66 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,58 @@ -# 设备自动化后台(platform-tools) +# auto_control — Android 多设备自动化任务平台 -基于 **uiautomator2 + Flask + 自建设备池** 的 Android 多设备自动化任务执行平台(已摘除 OpenSTF 依赖)。 +基于 **uiautomator2 + Flask + 自建设备池** 的 Android 多设备自动化平台(已摘除 OpenSTF 依赖,见 [doc/STF_REMOVAL.md](doc/STF_REMOVAL.md))。 -提供 Web 管理后台,支持多设备并发任务执行、定时调度、设备分组管理、设备池管理(新增/停用/删除/型号采集)、APK 批量安装、网页远程看屏(MJPEG 实时流 + 触控)、UI 元素抓取等功能。内置通用步骤任务(可视化步骤编辑器编排,覆盖任意 App 的操作),可扩展专属任务类型。 +一台机器上统管一批 Android 设备:定时/手动下发任务、并发执行、实时看屏与远程触控、元素抓取与步骤编排、APK 批量安装、数据备份导出/导入;并通过 **MCP** 把手机控制能力开放给外部 AI(数字员工)。 + +``` + ┌──────────── Web 管理后台(单页应用,:18050)────────────┐ + 浏览器 ────────────▶ │ 监控 │ 任务 │ 日志 │ 用户 │ 工具 │ AI 控制台 │ 系统 │ + └───────────────────────┬─────────────────────────────────┘ + │ Flask 蓝图(web/) + ┌───────────────────────▼──────┐ ┌──────────────────────┐ + │ 调度 TaskManager │ │ AI Agent(mcp_agent)│ + │ 设备池 device_pool │ └──────────┬───────────┘ + │ Worker(每设备一线程) │ │ MCP :8033 + └───────┬──────────────┬───────┘ ┌──────────▼───────────┐ + │ │ │ MCP Server(de_* 工具)│ + adb 直连 uiautodev └──────────┬───────────┘ + IP:5555 / USB :20242(抓元素) │ + ▼ ▼ + Android 设备 ◀──────────────────────── Android 设备 +``` --- ## 目录 +- [核心能力](#核心能力) - [快速上手](#快速上手) - [项目结构](#项目结构) - [核心概念](#核心概念) -- [配置说明](#配置说明) - [Web 管理后台](#web-管理后台) -- [任务系统](#任务系统) -- [日志系统](#日志系统) -- [常用脚本](#常用脚本) +- [任务与步骤](#任务与步骤) +- [AI 与 MCP](#ai-与-mcp) +- [配置说明](#配置说明) +- [日志](#日志) - [常见问题](#常见问题) -- [更多文档](#更多文档) +- [文档索引](#文档索引) + +--- + +## 核心能力 + +| 能力 | 说明 | +|------|------| +| **多设备并发任务** | 每设备一个 Worker 线程;同一设备同时只跑一个任务(可配置"抢占"打断其他任务) | +| **设备池** | SQLite 清单 + adb 在线状态;支持手工添加、网段自动发现、一键重连、型号采集、启用/停用 | +| **任务调度** | 手动 / cron 定时 / 定时启停;运行窗口;失败重试(含端口耗尽类的长退避) | +| **步骤编辑器** | 可视化拖拽编排 18 种步骤(含循环/条件/OCR),可打包成"自定义动作"复用 | +| **元素抓取** | 拉取设备 UI 元素树 → 点选回填选择器;支持"点一下"与"测选择器"真机验证 | +| **实时看屏** | MJPEG 实时流 + 点击/滑动/按键/文字输入;全屏监控大屏(`/wall`) | +| **应用管理** | APK 上传/解析/批量安装;设备已装应用与版本查询;剪贴板注入 | +| **AI 控制台** | 用自然语言驱动 AI 操作指定设备(MCP 工具 + 截图),流式输出、Markdown 渲染、推理链折叠、token 统计;成功操作自动沉淀「经验库 / 动作库」并在相似任务中召回 | +| **MCP 接入** | 19 个 `de_*` 工具,把手机控制开放给外部 AI;写操作有开关、设备忙时拒绝、全量审计 | +| **备份导出/导入** | 一键导出 zip(库快照 + manifest + 可选 APK),导入前校验预览、自动预备份、重启生效 | --- @@ -26,9 +61,8 @@ ### 环境要求 - **Python 3.10+**(推荐 3.12) -- **Windows / Linux / macOS**均可(adb 二进制需放对应平台版本到 `bin/adb/`) -- 设备需开启 **USB 调试**(USB 连上后 `adb tcpip 5555` 转网络调试),并加入 Tailscale 获得 `100.100.10.x` IP -- 新增设备在后台「工具 → 设备池管理」添加 `IP:5555`(自动连接,任务运行时 u2 自动推送 atx-agent) +- Windows / Linux / macOS 均可(`bin/adb/` 需放对应平台的 adb 二进制) +- 设备开启 **USB 调试**;网络调试设备建议 `adb tcpip 5555` 并接入同一网络(本项目生产环境走 Tailscale `100.100.10.x`,见 [doc/DEPLOY.md](doc/DEPLOY.md)) ### 三步启动 @@ -36,403 +70,327 @@ # 1. 安装依赖 pip install -r requirements.txt -# 2. 修改配置(可选):USB 设备在 220 上时配置 USB_ADB_HOST/PORT(默认 100.100.10.1:5037 已可用) -# 密钥类配置放 .env(WEB_SECRET_KEY / TAILSCALE_API_KEY) +# 2. 配置(可选):把 .env.example 复制成 .env,至少填 WEB_SECRET_KEY +# 密钥类配置一律放 .env(不入 git) -# 3. 启动 Web 后台 +# 3. 启动 python web_server.py ``` -启动后访问 **http://localhost:18050/**,默认账号 `admin` / `admin123`。 +启动后访问 **http://localhost:18050/**,默认账号 `admin` / `admin123`(**首次登录请立即改密**)。 ### 验证启动 -控制台看到以下日志即表示启动成功: - ``` [INFO] [core.worker] 心跳看门狗已启动 [INFO] [core.tm] 从数据库加载 X 个分组, X 个任务 +[INFO] [web] uiautodev 服务已启动 (PID=...) [INFO] [web] 管理后台: http://localhost:18050/ (admin/admin123) +[INFO] [web] 设备池预连接完成: X 台在线 [INFO] [web] 启动服务: http://localhost:18050/ ``` -### 首次使用流程 +### 首次使用 -1. **登录后台** → 用 `admin/admin123` 登录,建议立即修改密码 -2. **添加设备** → "工具 → 设备池管理"添加设备(serial 形如 `100.100.10.20:5555`,自动连接并采集型号) -3. **查看设备** → 首页"监控"Tab 展示设备池状态(在线/型号/任务) -4. **创建分组**(可选)→ "分组"Tab 按批次/项目给设备分组 -5. **创建任务** → "任务"Tab 新建任务,选择任务类型、目标设备、参数、调度 -6. **执行任务** → 任务列表点"立即执行",或在"监控"Tab 勾选设备批量操作(亮屏/息屏/停止) -7. **查看日志** → "日志"Tab 实时查看运行日志 +1. **登录** → `admin/admin123`,改密 +2. **加设备** → 「工具 → 设备池管理」添加 `IP:5555`(自动连接 + 采集型号);也可用「设备自动发现」扫描网段后确认入池 +3. **看设备** → 「监控」页设备表(在线/型号/任务/进度/前台 App) +4. **建任务** → 「任务 → 任务计划 → 新建任务」(只有 `generic_steps` 一种类型)→ 拖步骤 → 选目标设备 → 保存 +5. **跑任务** → 任务行「执行」,或在「任务」/「监控」页操作 +6. **看日志** → 「日志」页按模块实时查看 --- ## 项目结构 ``` -platform-tools/ -├── config.py # 根配置(部署配置统一从 .env 读,模板见 .env.example) -├── web_server.py # Flask 入口(app 装配 + 蓝图注册 + 启动,193 行) -├── web/ # Web 层蓝图包(按功能域拆分,路由都在这里) -│ ├── auth.py # 登录/CSRF/权限装饰器/页面路由(/、/wall) -│ ├── monitor.py # 状态/运行控制/设备操作/远程看屏 -│ ├── tasks_api.py # 任务计划/分组/自定义动作/元素抓取 -│ ├── admin_api.py # 用户管理/日志 -│ ├── tools_api.py # adb 终端/剪贴板注入/应用版本 -│ ├── devices_api.py # 设备池管理 -│ ├── apks_api.py # 应用管理 -│ ├── tailscale_api.py # Tailscale 管理 -│ ├── common.py # 跨模块共享工具(合并设备列表/屏幕状态) -│ └── context.py # 共享对象注入(mgr/apk_mgr/device_pool) -├── requirements.txt # Python 依赖清单 +auto_control/ +├── config.py # 程序级配置常量(部署配置从 .env 读,模板 .env.example) +├── web_server.py # 入口:app 装配 + 蓝图注册 + 恢复消费 + 拉起 uiautodev + 启动 +├── requirements.txt # 依赖清单 │ -├── core/ # 核心基础设施层 -│ ├── logger.py # 统一日志(分文件、10MB 滚动) -│ ├── device_pool.py # 设备池(SQLite 清单 + adb 在线状态 + 型号采集) -│ ├── adb_helper.py # adb 命令封装(全局锁,绝不 kill-server) -│ ├── device_worker.py # BaseWorker 基类 + 设备生命周期 + 心跳看门狗 -│ ├── task_manager.py # TaskManager 调度器 + 前台App扫描器 -│ ├── u2_helper.py # uiautomator2 通用辅助函数 -│ ├── uiauto_helper.py # uiautodev 元素抓取客户端 -│ ├── apk_manager.py # APK 上传/解析/批量安装 -│ ├── ocr.py # 屏幕 OCR(RapidOCR,条件判断 OCR 选择器) -│ ├── ssh_client.py # SSH 统一执行(paramiko 纯密码 / 免密密钥,预留运维用) -│ ├── tailscale_client.py # Tailscale API v2 客户端 -│ ├── models.py # SQLAlchemy 数据模型 + 数据库初始化 -│ └── actions/ # 全局 Action 框架 +├── web/ # Web 层(蓝图包,路由都在这里,全部无 url_prefix) +│ ├── auth.py # 登录/登出/CSRF/权限装饰器/页面路由(/、/wall) +│ ├── monitor.py # 设备状态/运行控制/设备操作/远程看屏(MJPEG、点击、按键) +│ ├── tasks_api.py # 任务计划/分组/自定义动作/元素抓取/步骤测试 +│ ├── admin_api.py # 用户管理/日志 +│ ├── tools_api.py # adb 终端/剪贴板注入/应用版本查询 +│ ├── devices_api.py # 设备池管理 + 自动发现 +│ ├── apks_api.py # APK 上传/安装/删除 +│ ├── tailscale_api.py # Tailscale 管理(改名/授权/密钥/IP/auth key) +│ ├── agent_api.py # AI 控制台(会话/SSE/经验库/动作库/巡检) +│ ├── system_api.py # 系统数据备份导出/导入 +│ ├── common.py # 跨模块共享工具(合并设备列表、屏幕状态) +│ └── context.py # 共享对象注入(mgr / apk_mgr / device_pool) │ -├── tasks/ # 任务定义层(每个 App 一个子包) -│ ├── __init__.py # 全局任务注册表 -│ ├── base.py # BaseTask 基类 + @register_task 装饰器 -│ └── generic/ # 通用步骤任务(可视化编辑器编排,当前唯一任务类型) -│ └── task.py # 步骤执行引擎(open_app/click/swipe/if_el...) +├── core/ # 基础设施层 +│ ├── models.py # SQLAlchemy 模型 + 建表 + 版本化迁移 + 旧 JSON 迁移 +│ ├── device_pool.py # 设备池(清单/在线状态/型号) +│ ├── device_worker.py # BaseWorker + 设备生命周期 + 心跳看门狗 + 全局状态表 +│ ├── task_manager.py # 调度器:分组/任务/APScheduler/重试/运行控制 +│ ├── adb_helper.py # adb 命令封装(全局锁,红线:绝不 kill-server) +│ ├── device_discovery.py # 网段扫描发现设备 → 待连接池 +│ ├── u2_helper.py # uiautomator2 通用辅助(等首页/安全点击等) +│ ├── uiauto_helper.py # uiautodev 客户端(元素树 + XPath 建议) +│ ├── ocr.py # 屏幕 OCR(RapidOCR,条件判断用) +│ ├── clipboard_helper.py # 剪贴板注入(ClipInject 通道) +│ ├── apk_manager.py # APK 上传/解析/批量安装 +│ ├── system_backup.py # 数据备份导出/导入(重启生效) +│ ├── tailscale_client.py # Tailscale API v2 客户端 +│ ├── ssh_client.py # SSH 封装(当前无人调用,预留) +│ ├── logger.py # 分文件日志(core/task/web/action) +│ └── actions/ # 全局 Action 框架(BaseAction + 注册器) │ -├── templates/admin/ # 前端页面 -│ ├── monitor.html # 单页应用(监控/任务/分组/日志/用户/工具) -│ └── login.html # 登录页 +├── tasks/ # 任务定义层 +│ ├── base.py # BaseTask + _TASK_TYPES + register_task +│ └── generic/ # 通用步骤任务(task_type=generic_steps,当前唯一类型) +│ └── task.py # STEP_TYPES(18 种步骤)+ Worker + 执行器 │ -├── static/admin/ # 前端 JS 模块(monitor.html 按依赖顺序加载) -│ ├── base.js # 通用基础:API/CSRF/权限/Tab切换/子分栏/模态框 -│ ├── list.js # 统一列表组件(搜索+分页+排序) -│ ├── monitor.js # 监控页(设备表/截图/异常汇总) -│ ├── editor.js # 步骤编辑器(拖拽/条件判断/元素抓取) -│ ├── tasks.js # 任务 Tab + 自定义动作 -│ ├── tools.js # 工具 Tab(剪贴板/设备池管理/adb终端/Tailscale/远程看屏) -│ ├── apps.js # 工具 Tab-应用管理(APK/设备已装应用) -│ ├── admin.js # 管理 Tab(分组/日志/用户)+ 初始化 +├── mcp_server/ # MCP Server(19 个 de_* 工具,:8033) +│ ├── mcp_server.py # 工具定义 + 平台登录 + 门控 +│ ├── platform_client.py # 平台 HTTP 客户端(复用 Web 账号) +│ ├── direct_ops.py # 直连设备的 adb/u2 操作 +│ ├── audit.py # 调用审计(JSON Lines) +│ └── config.py # MCP_* 环境变量 +│ +├── mcp_agent/ # AI Agent 编排层(OpenAI 兼容模型 → MCP 工具) +│ ├── agent.py # Agent:流式循环 + 工具调用 + 截图 + token 统计 +│ ├── config.py # AGENT_* 环境变量 +│ └── cli.py # 命令行入口 +│ +├── templates/admin/ # 页面 +│ ├── monitor.html # 主单页应用(7 个顶级 Tab + 模态框 + 内联样式) +│ ├── login.html # 登录页 +│ └── wall.html # 监控大屏(独立页面,自包含) +├── static/admin/ # 前端 JS(11 个文件,按顺序同步加载,见下) ├── static/fonts/ # 自托管字体(Bricolage Grotesque + IBM Plex Mono) │ -├── data/ -│ -├── data/ # 运行时数据 -│ ├── users.db # SQLite(用户/分组/任务/自定义动作/APK记录) -│ └── apks/ # 上传的 APK 文件存储 -│ -├── logs/ # 日志文件(自动生成,10MB 滚动保留 5 份) -├── bin/adb/ # adb 可执行文件(Windows: adb.exe + dll) -├── scripts/ # 实用脚本 -│ ├── pack.py # 打包项目为 zip(排除运行时产物) -│ └── supervise.sh # 进程守护(崩溃自动重启) -└── doc/ # 项目文档 - ├── API.md # API 文档 - ├── ARCHITECTURE.md # 架构详解 - ├── DEPLOY.md # 部署指南 - ├── DEVELOPMENT.md # 开发指南 - └── TASK_DEV.md # 任务开发指南(新增 App 任务模板) - ├── ARCHITECTURE.md # 架构详解 - ├── DEPLOY.md # 部署指南 - └── API.md # API 接口文档 +├── data/ # 运行时数据(不入 git) +│ ├── users.db # SQLite 主库 +│ ├── apks/ # 上传的 APK +│ ├── backups/ # 导出 zip / 预恢复快照 +│ ├── restore_staging/ # 导入暂存(TTL 30 分钟) +│ └── restore_pending/ # 待重启生效的恢复任务 +├── logs/ # 日志(10MB 滚动,保留 5 份) +├── bin/adb/ # adb 二进制 +├── scripts/ # pack.py / start.sh / supervise.sh / regression_test.py +└── doc/ # 完整文档(入口 → doc/README.md) +``` + +前端 JS 加载顺序(`templates/admin/monitor.html` 底部,全部为全局脚本,无模块隔离): + +``` +base.js → markdown.js → list.js → monitor.js → editor.js → tasks.js + → tools.js → apps.js → admin.js → agent.js → system.js ``` --- ## 核心概念 -### 设备生命周期 +### 设备与 Worker -``` -设备池选定设备(TaskManager._running 内存锁保证互斥) - ↓ -IP:5555 → adb connect 直连;USB(serial 无冒号)→ 本机 adb 或 220 远程 adb server - ↓ -u2.connect(连接 uiautomator2,自动推送 atx-agent) - ↓ -Worker.run_task(执行业务逻辑) - ↓ -释放(不 disconnect,遵守共享 adb transport 红线) -``` +一台设备对应一个 `Worker` 线程(`core/device_worker.py` 的 `BaseWorker`)。基类封装了设备获取、adb/u2 连接(带超时保护)、状态上报、异常分类、停止信号、心跳看门狗;子类只实现 `run_task(d)`。 -> 单实例部署下互斥由调度器内存锁保证;多实例场景可扩展 SQLite 行锁(见 doc/STF_REMOVAL.md 阶段 4)。 +执行链路:`设备池选定 → adb connect 直连(IP:5555)或经 220 远程 adb server(USB)→ u2.connect(自动推送 atx-agent)→ run_task → 释放(不断开连接)`。 -### Worker — 单设备执行线程 +**同一设备同时只允许一个 Worker**:由 `TaskManager._running` 内存锁保证;任务可开 `preempt` 抢占(先停掉正在跑的任务再接管,结束后归还)。 -每台设备对应一个 `Worker` 线程,继承 `BaseWorker`(`core/device_worker.py`)。基类已封装: +### 任务类型(TaskType) -- 设备获取(IP:5555 直连 / USB 远程 adb server,try/finally 保证释放) -- adb 连接 + u2.connect(带 30 秒超时保护) -- 状态上报(实时推送到前端监控大屏) -- 异常捕获(设备离线不重试,其他异常按策略重试) -- stop 停止信号(循环里检查 `self.stopped()`) -- 心跳看门狗(120 秒无心跳自动标记卡死) +| task_type | 名称 | 说明 | +|-----------|------|------| +| `generic_steps` | 通用步骤 | 步骤编辑器编排的流程(**当前唯一类型**) | -子类只需实现 `run_task(d)` 方法专注业务逻辑。 +新增专属任务类型的方法见 [doc/TASK_DEV.md](doc/TASK_DEV.md)。 -### TaskType — 任务类型 +### 任务计划(TaskJob) -| 任务类型 | task_type | 说明 | -|---------|-----------|------| -| 通用步骤 | `generic_steps` | 可视化步骤编辑器编排流程,支持循环/点击/滑动/条件/OCR 等 | +描述"什么任务、跑哪些设备、什么参数、何时跑、失败怎么重试": -### TaskJob — 任务计划 - -一个 TaskJob 描述"什么时候、在哪些设备上、用什么参数执行什么任务": - -| 字段 | 说明 | 示例 | -|------|------|------| -| `task_type` | 任务类型 | `"generic_steps"` | -| `target` | 目标设备 | `{"mode": "all"}` 或 `{"mode": "group", "group_name": "A组"}` 或 `{"mode": "serial", "serial": "192.168.1.100:5555"}` | -| `params` | 任务参数(与默认值深合并) | `{"max_duration": 0, "steps": [{"type": "open_app", "params": {"package": "com.ss.android.ugc.aweme"}}]}` | -| `schedule` | 调度策略 | `{"mode": "once"}` 或 `{"mode": "cron", "cron": "0 9 * * *"}` 或 `{"mode": "cron_stop", "cron": "0 9 * * *", "stop_cron": "0 18 * * *"}` | -| `retry` | 重试策略 | `{"max_attempts": 3, "delay": 60}` | -| `enabled` | 是否启用 | `true` | - -### 调度模式 - -| 模式 | 行为 | +| 字段 | 说明 | |------|------| -| `once` | 手动执行(前端点"立即执行") | -| `cron` | 定时启动:到 cron 时间点自动启动所有目标设备 | -| `cron_stop` | 定时启停:启动 cron 到点启动,停止 cron 到点停止本任务 worker | +| `task_type` | 任务类型(不传默认 `generic_steps`) | +| `target` | `{"mode":"all"}` / `{"mode":"group","group_name":"A组"}` / `{"mode":"serial","serial":"100.100.10.20:5555"}` | +| `params` | 任务参数,与任务类默认值合并;含两个隐藏开关 `skip_offline`(默认 true)、`preempt`(默认 false) | +| `schedule` | `{"mode":"once"}` / `{"mode":"cron","cron":"0 9 * * *"}` / `{"mode":"cron_stop","cron":...,"stop_cron":...}`,可选 `window` 运行窗口 | +| `retry` | `{"max_attempts":1,"delay":60}` | +| `enabled` | 是否参与调度 | -**简单设置**:编辑任务时选"定时启动/定时启动+停止"后,频率用下拉选择——每天(选时间)、每小时(整点)、每隔 N 小时、每周(选星期+时间)——cron 表达式自动生成,不需要懂 cron 语法。老手可在"自定义 cron(高级)"里直接填。 +**调度模式**:`once` 仅手动;`cron` 到点启动;`cron_stop` 到点启动 + 到点停止。cron 为标准 5 段 `分 时 日 月 周`,编辑器提供"每天/每小时/每隔 N 小时/每周"的可视化选择自动生成,也可手填。 -cron 表达式为标准 5 段格式:`分 时 日 月 周`(如 `0 9 * * *` = 每天 9:00,`0 */2 * * *` = 每 2 小时整点,周 `0`/`7` 均为周日) +**运行窗口**(`schedule.window`,如 `21:00-09:00`,支持跨午夜):窗口外定时触发与手动执行都不启动。 -**运行窗口**:任务可勾选"启用运行窗口",设置每天允许运行的时间段(如 `21:00-09:00` = 晚 9 点到次日早 9 点,支持跨午夜)。窗口外**定时触发和手动执行都不会启动**(手动执行会提示"当前不在运行窗口内")。典型用法:`每小时`定时 + 窗口 `09:00-21:00`,即只在白天每小时跑一次。 - -### 设备分组 - -设备分组存于 SQLite,便于按批次/项目分组下发任务。一个 Job 指定 `target.mode="group"` 时,调度器展开为组内全部设备。 +**目标解析**(`TaskJob.resolve_serials`):`all` = 设备池 ∩ 在线(开了抢占则取全部在线池内设备);`group` = 分组 ∩ 设备池;`serial` = 指定设备。后两者默认跳过离线设备。 ### 进度上报 -Worker 通过 `self.set_progress()` 上报通用进度字段,前端统一解析展示: +Worker 通过 `set_progress()` 上报统一字段,前端统一渲染: ```python -self.set_progress(done=5, total=80, unit="视频", - action_counts={"like": 3}) +self.set_progress(done=5, total=80, unit="视频", action_counts={"like": 3}, elapsed=120) ``` -前端展示:进度条 + `5/80 视频` + `点赞 3` 徽章。 - ---- - -## 配置说明 - -所有核心配置在 [config.py](file:///d:/platform-tools/config.py),**任务参数不放在这里**(放各自 `tasks/xxx.py` 顶部)。 - -| 配置项 | 默认值 | 说明 | -|-------|--------|------| -| `ADB_PATH` | 自动识别 | adb 二进制路径,自动区分 Windows/Linux | -| `WEB_HOST` | `0.0.0.0` | Web 监听地址(0.0.0.0 支持局域网访问) | -| `WEB_PORT` | `18050` | Web 端口(避开 Windows 动态端口范围) | -| `DATA_DIR` | `data/` | 持久化数据目录 | -| `APK_DIR` | `data/apks/` | APK 文件存储目录 | -| `USB_ADB_HOST` | `100.100.10.1` | USB 设备所在部署机(220)的 Tailscale IP(USB 设备远程 adb server) | -| `USB_ADB_PORT` | `5037` | 220 adb 容器监听端口(host 网络模式) | -| `TAILSCALE_API_KEY` | (.env 配置) | Tailscale 管理 API key(Settings → API Access Tokens) | -| `TAILSCALE_TAILNET` | 按邮箱前缀 | tailnet 名/ID | - -**必须修改的配置**:`.env` 里的 `WEB_SECRET_KEY`(会话密钥,不入 git);其余均为可选(默认值开箱即用)。 +前端展示为进度条 + `5/80 视频` + 计数徽章 + 运行时长。 --- ## Web 管理后台 -### 页面结构 +单页应用(`templates/admin/monitor.html`),**7 个顶级 Tab**;「任务 / 工具 / 系统」内部还有页内子分栏(会记住上次选中位置)。 -单页应用(`templates/admin/monitor.html`),6 个 Tab;任务/工具 Tab 内部再有页内子分栏: - -| Tab | 功能 | 可见性 | +| Tab | 内容 | 可见性 | |-----|------|--------| -| 监控 | 设备状态大屏:在线/离线、型号、运行任务、当前动作、进度条、前台 App、截图、勾选批量操作(亮屏/息屏/停止);导航栏「📺 大屏」打开全屏监控墙(/wall,缩略图+统计+时钟,挂墙/电视用) | 所有登录用户(设备操作按钮需"设备控制"权限) | -| 任务 | 子分栏:任务计划(CRUD/启用停用/立即执行/下次运行时间/**离线设备自动跳过**)、自定义动作(打包复用) | 所有登录用户可看,写操作需"任务管理"权限 | -| 分组 | 设备分组管理:创建/编辑/删除分组 | 所有登录用户可看,写操作需"任务管理"权限 | -| 日志 | 实时日志查看:按模块切换(core/task/web/action) | 需"日志查看"权限 | -| 用户 | 用户管理:创建/删除/修改密码/分配权限 | 仅管理员 | -| 工具 | 子分栏:剪贴板注入、adb 远程终端(快捷命令/自动 `-s`)、**设备池管理**(新增/停用/删除/一键重连/型号采集)、**远程看屏**(MJPEG 实时流 + 点击/滑动/按键/文字)、Tailscale 管理(改名/授权/密钥不过期/设置IP/auth key)、应用管理(APK 上传安装)、应用版本管理(按包名查所有设备版本)、设备已装应用 | 仅管理员 | +| **监控** | 统计卡片 + 设备表(在线/型号/任务状态/进度/前台 App/最近错误,支持排序、搜索、分页、多选批量操作)+ 异常汇总 + **任务运行概况**(每张任务卡:执行任务 / 停用任务 + 覆盖设备彩色 chip);导航栏「📺 大屏」打开 `/wall` | 所有登录用户(设备操作按钮需"设备控制") | +| **任务** | 子分栏:**任务计划**(CRUD / 启停 / 执行 / 下次运行时间 / 离线跳过 / 步骤编辑器)、**自定义动作**(步骤打包复用) | 可看;写操作需"任务管理" | +| **日志** | 实时日志(按文件切换、可自动刷新) | 需"日志查看" | +| **用户** | 用户 CRUD、改密、分配权限 | 仅管理员 | +| **工具** | 8 个子分栏:剪贴板注入 / adb 远程终端 / Tailscale 管理 / 应用管理 / 应用版本管理 / 设备已装应用 / **设备池管理**(含自动发现)/ 设备分组 | 仅管理员 | +| **AI 控制台** | 会话列表 + 对话区(Markdown 渲染、推理链折叠、token 统计)+ 实时画面(MJPEG)+ 目标设备选择;右上角:经验库 / 动作库 / 模型配置 | 仅管理员 | +| **系统** | 子分栏:数据备份(导出 zip)/ 导入恢复(上传→校验预览→应用,重启生效) | 仅管理员 | -任务/工具 Tab 的子分栏会记住上次选中的位置;无权限的 tab 和按钮自动隐藏。 +### 权限模型 -### 用户与权限 +默认账号 `admin/admin123`(管理员,权限不受限)。权限位: -默认账号 `admin` / `admin123`(管理员,拥有全部权限)。管理员可在"用户"Tab 创建普通用户并分配权限: +| 权限位 | 键 | 覆盖 | +|--------|----|------| +| 任务管理 | `tasks` | 任务/分组/自定义动作的写操作 | +| 设备控制 | `devices` | 停止设备、亮息屏、定位、清异常、前台扫描、元素抓取、步骤测试 | +| 应用管理 | `apks` | APK 上传/安装/删除 | +| 日志查看 | `logs` | 日志 Tab | -| 权限位 | 说明 | 覆盖功能 | -|--------|------|---------| -| 任务管理 `tasks` | 任务/自定义动作/分组的增删改、启停、立即执行 | 任务 Tab、分组 Tab 的写操作 | -| 设备控制 `devices` | 停止设备、释放占用、清除异常、前台扫描、元素抓取 | 监控 Tab 的设备操作按钮、步骤编辑器的"抓取元素" | -| 应用管理 `apks` | APK 上传、安装、删除 | 应用管理/设备已装应用(当前并入工具 Tab,工具页整体仅管理员可见) | -| 日志查看 `logs` | 日志页 | 日志 Tab | - -规则: -- **管理员拥有全部权限**,不受权限位限制 - 查看类接口(设备/任务/分组列表、状态、截图)所有登录用户可用 -- **用户管理仅管理员可用**;不能删除/取消最后一个管理员 -- 无权限的 tab 和按钮在界面上自动隐藏(后端同样拦截,返回 403) - -### 前台 App 扫描 - -监控页"扫描前台App"按钮,获取所有设备当前前台 App。**不打扰设备**: - -| 设备状态 | 处理方式 | -|---------|---------| -| worker 运行中(IP:5555) | 复用已有 ADB 连接查询 | -| worker 运行中(USB) | 经 220 远程 adb server 查询 | -| 完全空闲 | 返回"空闲"(不主动 connect,避免扰动共享 adb transport) | - -### 截图功能 - -监控页每台设备可查看实时截图。用 `adb exec-out screencap -p`,只读操作,**任务运行中也能安全调用**(不抢占 u2 的 atx-agent 通道)。 - -### 元素抓取 - -任务编辑器的"抓取元素"按钮可拉取设备当前 UI 元素树,点击元素一键回填选择器。依赖本地运行的 uiautodev 服务(端口 20242),`web_server.py` 启动时会自动拉起。 +- 后端 `@perm_required` / `@admin_required` 拦截并返回 403;前端用 `data-perm` 属性与 `_can(perm)` 隐藏入口(**仅隐藏,安全依赖后端**) +- 不能删除/降级最后一个管理员 --- -## 任务系统 +## 任务与步骤 -### 通用步骤(generic_steps) +`generic_steps` 的执行内容全在 `params.steps`(JSON 数组),由步骤编辑器产出。**没有默认步骤**——空步骤任务执行时会明确报错。 -通过可视化步骤编辑器编排任务流程,worker 按步骤顺序执行。支持的步骤类型: +**18 种步骤**(完整参数见 [doc/TASK_DEV.md](doc/TASK_DEV.md)): -| 步骤类型 | 说明 | -|---------|------| -| `open_app` | 启动 App(指定包名,可选等待首页) | -| `stop_app` | 强制结束 App(am force-stop,清后台,下次打开冷启动) | -| `screen_on` | 亮屏(息屏时唤醒并滑动解锁) | -| `screen_off` | 息屏 | -| `keep_screen` | 保持亮屏/恢复自动息屏(充电时屏幕常亮,适合长任务) | -| `key_event` | 按键:返回/Home/回车/菜单等(退出评论、返回上一页) | -| `swipe` | 滑动(上/下/左/右,可配置时长) | -| `swipe_until` | 滑动直到元素出现(最多 N 次,可选找到后点击) | -| `click_xy` | 点击坐标(屏幕百分比,无选择器时兜底) | -| `long_click` | 长按元素(选择器 + 时长) | -| `wait_el` | 等待元素出现(条件等待,替代固定时长) | -| `input_text` | 输入文字(随机候选/指定文字,可选输入前先清空) | -| `click` | 点击元素(支持 xpath/description/resourceId/text 选择器) | -| `wait` | 等待(可配置时长范围) | -| `loop` | 循环块(含子步骤,可配置循环次数) | -| `group` | 动作组(含子步骤,按序执行一次) | -| `if_el` | 条件判断:找元素(支持 xpath 等选择器或 **OCR识别** 截屏匹配图片文字),命中执行"找到时"分支,未命中执行"未找到时"分支;OCR 命中可自动点击。分支可嵌套循环/条件判断 | +| 类别 | 步骤 | +|------|------| +| 屏幕 | `screen_on` 亮屏 · `screen_off` 息屏 · `keep_screen` 保持亮屏 | +| 应用与输入 | `open_app` 打开 App · `stop_app` 结束 App · `input_text` 输入文字 · `clipboard` 剪贴板注入 · `key_event` 按键 | +| 交互 | `click` 点击元素 · `click_xy` 点击坐标 · `long_click` 长按 · `swipe` 滑动 · `swipe_until` 滑动直到元素出现 · `wait_el` 等待元素 · `wait` 等待时长 | +| 流程 | `loop` 循环块 · `group` 动作组 · `if_el` 条件判断(元素或 **OCR 识别**) | -步骤编辑器特性:操作库按分类分组、卡片可拖拽排序/跨层级嵌套(循环套循环)、☑ 多选打包自定义动作、 -"测试此步骤"真机验证、"抓取元素"回填选择器、条件判断的 OCR 识别依赖 `rapidocr_onnxruntime`(跨平台)。 +要点: -### 新增 App 任务 - -参照 `tasks/generic/` 结构,即可新增一个专属任务类型,详见 [doc/TASK_DEV.md](doc/TASK_DEV.md)。 -(注:当前平台只保留 `generic_steps` 一种类型;绝大多数 App 操作直接用步骤编辑器编排即可,不必新建类型。) +- 每一步都可有 `probability`(0-100,默认 100)决定本次是否执行 +- 容器类步骤(`loop`/`group`/`if_el`)可嵌套,**深度上限 5 层** +- 选择器支持 `xpath` / `description` / `text` / `resourceId` / `descriptionContains` / `className`(`ocr` 仅条件判断) +- **优先用文字/id 定位**,坐标 (`click_xy`) 是最脆的方式 +- 未知步骤类型、缺必填参数只告警跳过,不会中断任务链(排查时留意"看起来成功但没做事") --- -## 日志系统 +## AI 与 MCP -日志按模块分文件,自动滚动(10MB 一份,保留 5 份历史): +### AI 控制台 -| 文件 | 模块前缀 | 内容 | -|------|---------|------| -| `logs/core.log` | `core.*` | adb/worker/task_manager/设备池 核心程序 | +在「AI 控制台」选一台设备,用自然语言下指令,AI 通过 MCP 工具看屏幕、点按、输入,边做边把过程流式显示出来。配置(模型 / API Key / 默认设备 / 最大步数)存在数据库 `app_meta`,不落 `.env`。 + +自带两个"自进化记忆": + +- **经验库**:任务成功后把操作套路蒸馏成配方,下次相似任务自动召回注入;每日 03:47 由 AI 巡检建议清理(删除永远需人工确认) +- **动作库**:把成功步骤沉淀为带元素定位的命名动作(禁坐标),可复用、可编辑 + +详细机制见 [doc/AI_CONSOLE.md](doc/AI_CONSOLE.md)。 + +### MCP(外部 AI 接入) + +MCP Server 监听 `:8033`,暴露 19 个 `de_*` 工具(截屏、点击、滑动、输入、OCR、元素树、应用管理等)。写操作需 `MCP_ALLOW_WRITE=1`;设备正在跑任务时拒绝(`device_busy`);每次调用写审计日志。 + +```bash +# 本机手动启动(生产容器由 scripts/start.sh 自动拉起) +MCP_ALLOW_WRITE=1 MCP_PLATFORM_USER=admin MCP_PLATFORM_PASS=<密码> \ + python -m mcp_server.mcp_server +``` + +工具清单与接入示例见 [doc/MCP.md](doc/MCP.md)。 + +--- + +## 配置说明 + +**密钥类配置统一放项目根 `.env`**(不入 git;模板见 [.env.example](.env.example))。加载规则:逐行解析后用 `os.environ.setdefault` 注入,**真实环境变量优先**。 + +| 配置 | 默认值 | 说明 | +|------|--------|------| +| `WEB_SECRET_KEY` | 未配置则随机 | 会话密钥;不配则每次重启登录态失效(**生产必须固定**) | +| `WEB_HOST` / `WEB_PORT` | `0.0.0.0` / `18050` | Web 监听(**常量,改 config.py**) | +| `ADB_PATH` | 自动识别 `bin/adb/` | adb 二进制(常量,按平台自动选) | +| `DATA_DIR` / `APK_DIR` | `data/` / `data/apks/` | 持久化目录(常量) | +| `USB_ADB_HOST` / `USB_ADB_PORT` | `100.100.10.1` / `5037` | USB 设备所在的部署机(远程 adb server) | +| `DISCOVERY_PORT` / `DISCOVERY_INTERVAL` | `5555` / `60` | 设备自动发现(网段也在工具页配置,存 `app_meta`) | +| `TAILSCALE_API_KEY` / `TAILSCALE_TAILNET` | 空 | Tailscale 管理功能 | +| `WEB_SECRET_KEY` 之外的一切密钥 | — | 一律进 `.env`,**不要写进 config.py** | + +> **任务参数不放 config.py**——放各自 `tasks//task.py` 顶部的 `DEFAULT_PARAMS`。 + +完整键表(含 `MCP_*` / `AGENT_*`)见 [doc/DEVELOPMENT.md](doc/DEVELOPMENT.md) §配置速查。 + +--- + +## 日志 + +按模块分文件,单文件 10MB 滚动、保留 5 份: + +| 文件 | 前缀 | 内容 | +|------|------|------| +| `logs/core.log` | `core.*` | adb / device_worker / task_manager / 设备池 | | `logs/task.log` | `task.*` | 任务执行(worker 业务逻辑) | -| `logs/web.log` | `web.*` | Web 请求/管理 | -| `logs/action.log` | `action.*` | 操作执行(点赞/评论等) | - -使用方式: +| `logs/web.log` | `web.*` | Web 请求与管理操作 | +| `logs/action.log` | `action.*` | 操作执行 | ```python from core.logger import get_logger -log = get_logger("task.generic") # 写入 task.log -log.info(f"[{self.serial}] 开始任务") +log = get_logger("task.generic") # → logs/task.log ``` -Web 后台"日志"Tab 可实时查看各模块日志。 - ---- - -## 常用脚本 - -| 脚本 | 用途 | -|------|------| -| `python web_server.py` | 启动 Web 管理后台 | -| `python scripts/pack.py` | 打包项目为 zip(排除日志/数据库/APK) | -| `bash scripts/supervise.sh` | 进程守护(web_server 崩溃自动重启) | +「日志」Tab 可在线查看。 --- ## 常见问题 -### 端口被占用(WinError 10013/10048) - -`web_server.py` 会自动重试候选端口(原端口 → 127.0.0.1:原端口 → 127.0.0.1:原端口+1~+5)。如果全部失败,检查 Windows 动态端口范围: - -```bash -netsh interface ipv4 show excludedportrange protocol=tcp -``` - -修改 `config.py` 中的 `WEB_PORT` 到一个不在排除范围内的端口。 - -### 设备显示离线 - -先确认本机 `adb devices` 能看到设备(工具 → 设备池管理 → 一键重连);看不到则检查 Tailscale 是否在线、设备是否加入了正确的 tailnet(平台与设备必须在同一 tailnet)。 - -### 新增设备后任务不调度它 - -新设备需在「工具 → 设备池管理」添加(serial 为 `IP:5555`),仅连上 adb 不会进入设备池。添加后自动连接并采集型号,下一轮任务即可调度。 - -### u2.connect 卡死 - -`u2.connect()` 在 atx-agent 无响应时会永久 hang。基类已用 `ThreadPoolExecutor + 30 秒超时` 保护,超时自动放弃。如果频繁超时,检查设备 atx-agent 是否正常(重启设备或重新推送 atx-agent)。 - -### 任务运行中看门狗误杀 - -看门狗 120 秒无心跳会标记卡死。长耗时操作(如长视频等待)需在循环内周期性调用 `self.heartbeat()`。 - -### 中文输入失败 - -uiautomator2 默认 IME 不支持中文,需切到 FastInput 输入法: - -```python -try: - d.set_fastinput_ime(True) - d.send_keys("中文内容") -finally: - d.set_fastinput_ime(False) -``` - -### 修改 core/ 后不生效 - -`web_server.py` 以 `debug=False` 运行,Python 不会热重载。修改 `core/` 目录下的文件后**必须重启 web_server 进程**。 - -### 元素抓取按钮不可用 - -依赖 uiautodev 服务(端口 20242)。确保已安装 `pip install uiautodev`。`web_server.py` 启动时会自动拉起该服务。 +| 现象 | 处理 | +|------|------| +| 端口被占用(WinError 10013/10048) | 启动会自动回退候选端口(`18050` → `127.0.0.1:18050` → `127.0.0.1:18051..18055`);仍失败则改 `WEB_PORT`(注意 Windows 动态端口排除段) | +| 设备显示离线 | 「工具 → 设备池管理」一键重连;确认设备在线且同一网络(生产走 Tailscale 同 tailnet) | +| 新加的设备不被调度 | 必须**加入设备池**(仅 adb 连上不算);确认该设备为"启用"状态 | +| `u2.connect` 卡死 | 基类有 30s 超时保护;频繁超时说明 atx-agent 异常,重启设备或重新推送 | +| 任务运行中被看门狗标记卡死 | 看门狗 120s 无心跳即判定;长循环内需周期性 `self.heartbeat()` | +| 任务"成功"但什么都没做 | 步骤类型未知/缺必填参数会被**告警跳过**;查 `logs/task.log` 的 WARNING | +| 中文输入失败 | u2 默认 IME 不支持中文,需 `set_fastinput_ime(True)`(`input_text` 步骤已处理) | +| 改了 `core/` 不生效 | 服务以 `debug=False` 运行、不热重载;**必须重启**。前端改动还需强刷浏览器(Ctrl+Shift+R) | +| 元素抓取按钮不可用 | 依赖本机 uiautodev(:20242),`web_server.py` 启动时自动拉起;未安装时 `pip install uiautodev` | --- -## 更多文档 +## 文档索引 + +**完整文档入口 → [doc/README.md](doc/README.md)**(文档地图、阅读路径、维护约定) | 文档 | 内容 | |------|------| -| [doc/DEVELOPMENT.md](doc/DEVELOPMENT.md) | **开发手册**:开发流程、git 工作流、技术红线、环境、本地开发 | -| [doc/TASK_DEV.md](doc/TASK_DEV.md) | 任务开发指南(新增 App 任务的完整模板和规范) | -| [doc/ARCHITECTURE.md](doc/ARCHITECTURE.md) | 架构详解(分层设计、数据流、关键设计决策) | -| [doc/DEPLOY.md](doc/DEPLOY.md) | 部署指南(环境准备、设备池配置、生产部署) | -| [doc/STF_REMOVAL.md](doc/STF_REMOVAL.md) | STF 摘除迁移记录(阶段 0-3 已完成,含多实例锁方案) | -| [doc/API.md](doc/API.md) | API 接口文档(全部 HTTP 接口说明) | +| [doc/ARCHITECTURE.md](doc/ARCHITECTURE.md) | 架构详解(分层、装配顺序、线程模型、设备生命周期、调度链路、设计决策) | +| [doc/DATA_MODEL.md](doc/DATA_MODEL.md) | 数据模型(表结构、迁移、`app_meta`、数据目录、备份覆盖清单) | +| [doc/API.md](doc/API.md) | HTTP 接口全量说明 | +| [doc/TASK_DEV.md](doc/TASK_DEV.md) | 任务与步骤开发(18 种步骤、选择器、自定义动作、新增任务类型) | +| [doc/MCP.md](doc/MCP.md) · [doc/MCP_DESIGN.md](doc/MCP_DESIGN.md) | MCP 使用手册 / 设计文档 | +| [doc/AI_CONSOLE.md](doc/AI_CONSOLE.md) · [doc/AI_TASK_GEN.md](doc/AI_TASK_GEN.md) | AI 控制台机制 / AI 建任务设计(规划) | +| [doc/DEPLOY.md](doc/DEPLOY.md) | 部署与运维(含生产容器、备份导入、故障排查) | +| [doc/DEVELOPMENT.md](doc/DEVELOPMENT.md) | 开发手册(git 流程、技术红线、本地开发、文档同步约定) | +| [doc/STF_REMOVAL.md](doc/STF_REMOVAL.md) | 摘除 OpenSTF 的历史记录 | -> **注意**:所有 git 操作(含 push 到 dev)都需负责人确认后才能执行,详见开发手册。修改 `core/`、`tasks/`、`templates/` 后需重启服务/强刷浏览器才生效。 +> ⚠️ **改动必须同步文档**:功能/配置/接口/表结构的任何增删改,都要在同一个 commit 里更新对应文档(红线,详见 [doc/README.md](doc/README.md) §3)。 +> ⚠️ **git 操作需负责人确认**(含 push 到 dev),详见 [doc/DEVELOPMENT.md](doc/DEVELOPMENT.md)。 --- @@ -440,10 +398,10 @@ finally: | 组件 | 用途 | |------|------| -| Flask + Flask-Login | Web 后台 + 用户认证 | -| Flask-SQLAlchemy | SQLite 数据持久化 | -| APScheduler | 定时任务调度 | -| uiautomator2 | Android UI 自动化 | -| uiautodev | UI 元素抓取(步骤编辑器"抓取元素") | +| Flask + Flask-Login + Flask-SQLAlchemy | Web 后台 / 认证 / SQLite ORM | +| APScheduler | 定时任务调度(任务 + 经验巡检两个独立调度器) | +| uiautomator2 / uiautodev | Android UI 自动化 / 元素抓取 | +| rapidocr_onnxruntime (+ opencv-headless) | 屏幕 OCR | | pyaxmlparser | APK 元信息解析 | -| 自建设备池 | 设备清单/在线状态/型号(SQLite + adb) | +| fastmcp | MCP Server(Streamable HTTP)+ AI 控制台 Agent | +| adb(项目自带 `bin/adb/`) | 设备连接与底层操作 | diff --git a/doc/AI_CONSOLE.md b/doc/AI_CONSOLE.md new file mode 100644 index 0000000..9f28e0c --- /dev/null +++ b/doc/AI_CONSOLE.md @@ -0,0 +1,210 @@ +# AI 控制台(AI_CONSOLE) + +> 适用读者:使用 AI 控制台的人 + 改这部分代码的开发者。 +> 相关文档:[API.md](API.md) §12(接口)、[MCP.md](MCP.md)(AI 用的工具层)、[AI_TASK_GEN.md](AI_TASK_GEN.md)("一句话建任务"的设计稿,尚未实现)。 + +--- + +## 1. 它是什么 + +「AI 控制台」是后台的一个顶级 Tab(仅管理员):选一台设备,用自然语言下指令,AI 通过 MCP 工具**看屏幕、点按、输入**,边做边把过程和结论流式显示出来。 + +``` +你:「打开小红书搜索苏州好吃的饭店,把前 5 条列出来」 +AI:de_list_devices → de_screenshot → de_tap_text("搜索") → de_type_text("苏州好吃的饭店") + → de_screenshot → de_ui_tree → … → 汇总结论(Markdown 表格) +``` + +两个"自进化记忆"让它越用越顺: + +| 记忆 | 存什么 | 怎么产生 | 怎么用 | +|------|--------|---------|--------| +| **🧠 经验库** | 任务级**操作配方**(这一步该怎么做) | 一轮任务成功后由模型蒸馏 | 相似任务开始时召回注入 system prompt | +| **🎬 动作库** | **命名动作**(可复用的动作单元,带元素定位) | 从**成功步骤**蒸馏,禁坐标 | 相似任务开始时召回注入,可直接复用定位 | + +--- + +## 2. 使用 + +### 2.1 配置(首次必做) + +右上角 **⚙ 配置**: + +| 项 | 说明 | +|----|------| +| API Base | OpenAI 兼容地址(默认 `https://api.deepseek.com`) | +| 模型名 | 如 `deepseek-v4-flash-vision-exp`(需支持**视觉**,因为要看截图) | +| API Key | 只存数据库 `app_meta`,回显打码 | +| 默认设备 | 不选目标设备时用它 | +| 最大步数 | 1-200,默认 40(每轮模型调用算一步) | + +> 配置存在数据库(`app_meta` 的 `agent_*` 键),**不在 `.env`**。`CLI(mcp_agent/cli.py)` 走的是环境变量 `AGENT_*`,两套互不影响。 + +### 2.2 跑一轮 + +1. 选 **🎯 目标设备**(AI **只操作你选定的设备**;有任务在跑的设备不可选) +2. 输入指令,Enter 发送 +3. 右侧「📺 实时画面」自动跟随 AI 操作的设备(MJPEG) +4. 中途可「■ 停止」(下一个检查点生效,通常几秒内) +5. 刷新/换窗口:会话与运行状态都会自动恢复(见 §3.3) + +### 2.3 会话管理 + +左侧会话列表 = 多轮对话(DeepSeek 风格):不同话题建不同会话,历史消息会作为上下文续上(最近 12 轮)。会话条目上显示 ID(前 8 位,点击复制),便于反馈问题时引用 `conv=`。 + +--- + +## 3. 机制 + +### 3.1 执行链路 + +``` +POST /api/agent/run web/agent_api.py 起后台线程(单实例:同时只允许一个) + │ + ├─ 经验召回 _find_experiences(prompt) ─┐ + └─ 动作召回 _find_actions(prompt) ├─ 拼成 extra_context 注入 system prompt + ┘ + ▼ +mcp_agent.Agent.run_stream(prompt, serial, history, on_delta, on_tool, on_usage, …) + │ 循环(最多 max_steps 轮): + │ ① 流式调模型(OpenAI 兼容 /chat/completions) + │ ② 有 tool_calls → 执行 MCP 工具 → 结果回灌 → 继续 + │ 截图工具的结果会转成 image_url 追加,模型"看得见" + │ ③ 无 tool_calls → 本轮即最终回答 + ▼ + 事件 → queue → SSE /api/agent/stream → 前端 +``` + +- 一轮整体超时 **900 秒**;达到步数上限会让模型做一次收尾总结 +- 工具调用由 MCP Server 执行(`:8033`),后者再调平台 HTTP 接口/直连设备 +- **设备忙时拒绝**:目标设备正在跑任务 → `409`("AI 不与任务抢设备") + +### 3.2 会话消息模型 + +`agent_conversation.messages`(JSON 数组): + +```json +[{"role": "user", "content": "…"}, + {"role": "assistant", "content": "最终回答(Markdown)", + "usage": {"prompt_tokens": 3480, "completion_tokens": 126, "total_tokens": 3606, "calls": 1}, + "reasoning": "模型的推理链(最多保留 6000 字符)"}] +``` + +> `usage` / `reasoning` **只用于前端展示与回看**;回灌给模型的历史只取 `role` / `content`(不污染上下文预算)。 + +### 3.3 SSE 事件与刷新恢复 + +| event | payload | 前端行为 | +|-------|---------|---------| +| `delta` | `{"text","kind":"content"\|"reasoning"}` | 正文增量渲染 Markdown;推理增量进入可折叠「💭 思考过程」 | +| `step` | `{"tool","args","image"?}` | 追加工具卡片(含缩略截图,点击放大);伪卡片提示经验/动作命中与沉淀 | +| `usage` | `{"prompt_tokens","completion_tokens","total_tokens","calls"}` | 刷新单条消息脚注与顶栏「本会话累计」 | +| `done` | `{"answer","usage"}` | 最终答案 + 收尾 | +| `error` | `{"message"}` | 展示错误(MCP 不可达等已转成明确文案) | + +**刷新/重连不丢进度**:服务端事件队列保留积压,页面重新订阅(`GET /api/agent/stream?run_id=`)后会补发 delta/step/usage/done;`EventSource.onerror` **刻意不结束运行**,靠自动重连续上。另外 `GET /api/agent/run` 提供状态快照(其他窗口/8s 轮询用)。 + +### 3.4 token 统计 + +- 请求带 `stream_options.include_usage`,服务端在**末尾 chunk** 返回 usage +- 按「每次模型调用」累加(多轮工具调用会累加多次);`calls` = 模型调用次数 +- 个别网关不认该参数会直接 400/422 → **自动关掉并重试一次**,不影响主流程(只是没有 token 数字) +- 展示位置:单条消息脚注(`🪙 3,606 tokens(↑3,480 ↓126 · 1 次调用)`)+ 顶栏「🪙 本会话 3,606 tokens」 + +### 3.5 回答渲染 + +- **Markdown**:自研轻量渲染器(`static/admin/markdown.js`,**无 CDN 依赖**,生产在内网) + 支持标题/段落/列表(含嵌套)/表格/代码块/引用/链接;**先 `esc()` 转义再套标记**,所以模型输出里的 HTML 只会显示为文本 +- **推理链**:`
` 折叠块,流式时展开、正文开始时自动收起;手动点过后不再自动改;摘要显示字数 +- **工具卡片**:每步 MCP 调用一行(工具名 + 参数 + 截图缩略) + +--- + +## 4. 经验库 + +### 4.1 产生(蒸馏) + +一轮任务**成功执行过工具**后,把「任务描述 + 工具序列」交给模型,提炼成一段**可复用的操作配方**(纯文本,不含具体坐标)。 + +健壮性设计(都是踩坑后加的): + +| 措施 | 原因 | +|------|------| +| 蒸馏调用**关闭推理**(`thinking: {"type":"disabled"}`) | 推理模型会把 token 预算烧在 reasoning 上,导致 `content` 为空/被截断 → 经验被静默丢弃 | +| 纯文本问法 + 质量门槛 | 早期提示词里放了可照抄的占位示例,模型会把 `"1. …\n2. …"` 原样当配方存下来 | +| 截断容忍解析 | JSON 被截断时逐个对象抢救 | +| 失败有日志 | 不再静默 | + +### 4.2 召回 + +相似任务开始时按 bigram 相似度检索(阈值 + 取前 2 条),拼进 system prompt,并在对话里推一张「🧠 经验记忆」卡片("命中 N 条同类历史经验,已注入参考")。命中会累加 `hits`。 + +### 4.3 巡检(质量治理) + +- 每日 **03:47**(独立 APScheduler)自动跑一轮:把经验交给模型评审,输出「保留 / 建议删除」+ 评分 + 理由,写入 `experience_audit` +- 也可在面板里手动触发 +- **删除永远需要人工确认**(巡检只打标 `pending`,面板上显示 ⚠ 建议删除 + 理由,人工点「确认删除」或「保留」) +- 「保留」会记 `action='kept'`,后续不再重复建议 + +### 4.4 面板 + +「🧠 经验库」按钮打开:列表(任务描述、配方摘要、引用次数、巡检建议)+ 删除/保留操作。 + +--- + +## 5. 动作库 + +### 5.1 与经验库的区别 + +| | 经验库 | 动作库 | +|---|--------|--------| +| 粒度 | 整个任务的操作套路(文字) | 单个可复用动作(结构化步骤) | +| 内容 | 自由文本配方 | 命名动作 + `steps`(编辑器 schema,**带元素定位**) | +| 用途 | 让 AI 知道"这类任务一般怎么做" | 让 AI 直接复用"打开抖音"这种动作,跳过重新探索 | + +### 5.2 产生(蒸馏) + +从本轮**成功**的步骤轨迹里提炼命名动作(如「打开抖音」)。硬约束: + +- **禁坐标**:带 `click_xy` 的步骤不会被沉淀(坐标换个设备/分辨率就失效) +- 白名单步骤类型(18 种去掉 `click_xy`、`keep_screen`) +- 每类型有必填参数校验(如 `click` 必须有选择器) +- 输入/产出限量:最多 10 步输入、最多 3 个动作 × 4 步 +- 保存时服务端**再校验一次**,含坐标的提交直接 400 + +### 5.3 召回与使用 + +相似任务时按动作名/别名匹配(子串或 bigram 相似度),注入「## 可复用动作」段,并推「🧠 动作经验」卡片。AI 被提示"优先按其中的元素定位操作;若与当前界面不符,再自行截图确认"。 + +### 5.4 面板 + +「🎬 动作库」:查看/编辑/删除/手动新建。编辑时直接改 `steps` JSON(有格式说明),保存走同一套校验。 + +--- + +## 6. 配置项(`app_meta`) + +| key | 含义 | 默认 | +|-----|------|------| +| `agent_api_base` | 模型接口地址 | `https://api.deepseek.com` | +| `agent_model` | 模型名 | — | +| `agent_api_key` | API Key(**明文存库**) | — | +| `agent_default_serial` | 默认目标设备 | 空 | +| `agent_max_steps` | 最大步数(钳制 1-200) | 40 | + +其它常量:单轮超时 900s、推理链落库上限 6000 字符、会话消息上限 60 条、历史上下文取最近 12 轮。 + +--- + +## 7. 故障排查 + +| 现象 | 原因 / 处理 | +|------|------------| +| 报「MCP server(8033) 不可达」 | MCP 没启动:容器由 `start.sh` 拉起;本机手动 `MCP_ALLOW_WRITE=1 python -m mcp_server.mcp_server` | +| 报模型 API **401** | API Key 无效/过期 → ⚙ 配置重填 | +| 报"请先选择目标设备" | 没选设备且没配默认设备 | +| 报"设备正在执行任务…"(409) | AI 不与任务抢设备:等任务结束,或去监控页停止该任务 | +| 没有 token 数字 | 该网关不支持 `stream_options.include_usage`(已自动降级,功能不受影响) | +| 经验/动作没沉淀 | 只有**成功执行过工具**才会蒸馏;看 `logs/web.log` 的「经验提炼 / 动作提炼」日志 | +| 推理链看不到 | 模型未返回 `reasoning_content`(非推理模型),或本轮没有推理输出 | +| 界面样式/脚本异常 | 强刷浏览器(Ctrl+Shift+R)——`markdown.js` 等前端文件有缓存 | diff --git a/doc/AI_TASK_GEN.md b/doc/AI_TASK_GEN.md index c376b70..49bfe97 100644 --- a/doc/AI_TASK_GEN.md +++ b/doc/AI_TASK_GEN.md @@ -1,7 +1,8 @@ # AI 建任务(AI Task Generator)设计文档 -> 分支:dev | 状态:设计稿(未实现) | 日期:2026-09-09 -> 关联:AI 控制台(web/agent_api.py)、MCP 设备工具(mcp_server/)、任务/步骤编辑器(tasks/generic、static/admin/editor.js) +> 状态:**设计稿,尚未实现**(P0 未开工)——本文描述的是「要做什么、为什么这么做」,不是现状。 +> 现状请读:[AI_CONSOLE.md](AI_CONSOLE.md)(AI 控制台已实现的能力)、[TASK_DEV.md](TASK_DEV.md)(任务与步骤)、[MCP.md](MCP.md)(已实现的 19 个工具)。 +> 关联代码:`web/agent_api.py`、`mcp_server/`、`mcp_agent/`、`tasks/generic/`、`static/admin/editor.js`。最后核对:2026-09-10。 ## 1. 背景与目标 diff --git a/doc/API.md b/doc/API.md index 3800da9..8b1f03b 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1,1359 +1,643 @@ -# API 接口文档 +# HTTP 接口文档(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": "错误信息"} -``` +> 适用读者:前端开发、外部接入方、排接口问题的运维。 +> 相关文档:[ARCHITECTURE.md](ARCHITECTURE.md)(分层与装配)、[DATA_MODEL.md](DATA_MODEL.md)(数据)、[MCP.md](MCP.md)(给 AI 的工具层,不是 HTTP)。 +> 代码位置:全部路由在 `web/` 包下,**10 个蓝图,全部 `url_prefix` 为空**(路径即代码里写的路径)。 --- -**权限模型**(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. 通用约定](#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.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 | 在线设备 serial 列表(分组表单用) | +| 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 | 添加/更新设备 | +| 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` | 应用恢复(重启生效) | + +--- + +## 3. 认证与页面 ### POST /login -用户登录。 +表单 `username` / `password`。 -**请求**(form-data): -| 参数 | 类型 | 说明 | -|------|------|------| -| username | string | 用户名 | -| password | string | 密码 | +- 成功:`login_user` + 写 session 的 CSRF token → **302** 到 `?next=` 或 `/` +- 失败:**200** + 登录页 HTML(含"用户名或密码错误") -**响应**:成功重定向到 `/`,失败返回登录页(含 error 信息)。 +### GET /logout · GET /api/csrf · GET /api/me -### 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 +// GET /api/csrf {"ok": true, "token": "…"} +// GET /api/me +{"ok": true, "user": {"id": 1, "username": "admin", "is_admin": true, + "perms": ["tasks","devices","apks","logs"]}} ``` ---- - -## 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 带宽,普通电脑无压力。 +管理员返回全部权限位。页面:`GET /`(单页应用)、`GET /wall`(大屏)、`GET /locate`(设备端定位页,免登录,只显示 serial 文本)。 --- -## 3. 设备状态 +## 4. 设备状态与运行控制 ### GET /api/status -获取设备池 + Worker 综合状态(带 5 秒缓存;worker 状态实时读内存)。需登录。 +监控页 5s 轮询的主接口。 -字段说明:`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 - } - ] -} +{"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` / `failed` +`worker_status`:`idle` / `connecting` / `running` / `done` / `error` / `released` / `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} - ] -} +{"ok": true, "counts": {"running":1,"done":2,"error":0,"idle":2,"failed":0}, + "errors": [{"serial":"…","task":"…","last_error":"…","attempt":3,"updated":1700000000.0}]} ``` -`counts` 各状态计数;`errors` 为 `error`/`failed` 且有 `last_error` 的异常设备 -(按最近心跳倒序,最多 50 条;已不在设备池的陈旧失败记录不展示)。 ### 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)。 -**响应**: -```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//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 | 时间戳(避免缓存,前端自动加) | +| `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` | — | 该设备已安装应用(包名/版本/路径) | -**响应**:成功返回 `image/png`,失败返回 JSON 错误。 - -> 用 `adb exec-out screencap -p`,只读操作,任务运行中调用安全。 +> ⚠️ `/api/device/locate` 的 `show=true` 分支当前会失败(`urllib` 未导入 → 502);`GET /locate` 本身返回 500。见 [§16](#16-已知问题)。 --- -## 4. 任务类型 +## 5. 任务类型 / 任务计划 / 分组 / 自定义动作 ### GET /api/task_types -返回所有已注册任务类型。 - -**响应**: ```json -{ - "ok": true, - "task_types": [ - { - "task_type": "generic_steps", - "name": "通用步骤", - "description": "通过步骤编辑器编排...", - "default_params": {"max_duration": 0} - } - ] -} +{"ok": true, "task_types": [ + {"task_type": "generic_steps", "name": "通用步骤", "description": "…", + "default_params": {"max_duration": 0}}]} ``` -### GET /api/actions +> 当前平台**只有 `generic_steps` 一种类型**。`default_params` **不含 `steps`**——步骤只能由编辑器产出。 -返回指定任务类型支持的专属操作。 +### GET /api/actions?task_type=… -**参数**: -| 参数 | 类型 | 说明 | -|------|------|------| -| task_type | string | 任务类型 | - -**响应**: -```json -{ - "ok": true, - "actions": [ - {"action_type": "like", "name": "点赞", "description": "...", "default_params": {"rate": 0.3}} - ] -} -``` - ---- - -## 5. 任务计划 CRUD +返回该任务类型支持的"专属操作"列表(`generic_steps` 返回步骤类型清单 `STEP_TYPES`)。 ### GET /api/jobs -列出所有任务计划。 - -**响应**: ```json -{ - "ok": true, - "jobs": [{"id": "abc123", "name": "刷视频-上午", "task_type": "generic_steps", - "next_run": "2026-08-12 09:00", - "coverage": {"mode": "all", "total": 3, - "serials": ["192.168.20.206:5555", "..."]}, - "...": "..."}], - "task_types": [...] -} +{"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`:下次真正执行时间(已按运行窗口跳过窗口外触发点,格式 `YYYY-MM-DD HH:MM`);手动任务/已停用为 `null`。 -`coverage`:**任务覆盖的设备**(监控页「任务运行概况」展示用,每次请求现算)。按 `target` 定义解析, -不因设备当前是否空闲而变(在线/运行中由前端拿 `/api/status` 标注),也不做离线过滤、不写调度日志: +- `next_run`:下次真正执行时间(已按运行窗口跳过窗口外触发点);手动/停用为 `null` +- **`coverage`**:任务**覆盖的设备**(监控页「任务运行概况」展示用,每次请求现算)——按 `target` 定义解析,**不因设备当前是否空闲而变**,也不做离线过滤、不写调度日志: -| target.mode | coverage.serials | +| `target.mode` | `coverage.serials` | |------|------| | `all` | 设备池全部启用设备 | -| `group` | 该分组的 serial ∩ 设备池启用设备(池外的手填 IP 不计入,与调度口径一致) | +| `group` | 分组的 serial ∩ 设备池启用设备(池外的手填 IP 不计入) | | `serial` | 仅该 serial(即使不在池中也照实返回) | -> 口径差异:真正调度时走 `TaskJob.resolve_serials()`(`all` 取"池内∩在线"、并跳过离线设备); -> `coverage` 是**定义层**的覆盖面,供人看"这台任务管哪些设备"。POST/PUT 的 `job` 里同样带 `coverage`。 +> 与**调度**口径的差异:真正跑的时候走 `TaskJob.resolve_serials()`(`all` 取"池内 ∩ 在线",`serial`/`group` 默认跳过离线设备)。`coverage` 是"定义层覆盖面",供人看"这台任务管哪些设备"。 ### POST /api/jobs -创建任务计划。 - -(权限:任务管理) - -**请求**(JSON): ```json -{ - "name": "刷视频-上午", - "task_type": "generic_steps", - "target": {"mode": "all"}, - "params": {"max_duration": 0, "steps": [{"type": "open_app", "params": {"package": "com.ss.android.ugc.aweme"}}]}, - "schedule": {"mode": "cron", "cron": "0 */2 * * *"}, - "retry": {"max_attempts": 3, "delay": 60}, - "enabled": true -} +{"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} ``` -**响应**: -```json -{"ok": true, "msg": "任务已创建", "job": {"id": "abc123", "...": "..."}} -``` +响应:`{"ok": true, "msg": "任务已创建", "job": {…含 coverage…}}` -> **`params.steps` 要一起传**:`generic_steps` 的执行内容全在 `params.steps`,后端**不提供默认步骤**、 -> 也不做参数校验(编辑器是参数正确性的唯一关卡)。不传 steps 的任务能建成功,但执行时会立即报错 -> 「通用步骤任务没有可执行步骤」,请在「任务」页用步骤编辑器补步骤。 +**`schedule` 字段**: -`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`)均不启动,手动执行返回错误提示 | +| `cron` | 标准 5 段 `分 时 日 月 周`(周 `0`/`7` = 周日),如 `0 */2 * * *` | +| `stop_cron` | `cron_stop` 必填,到点停止本任务 | +| `window` | 可选运行窗口 `{"start":"21:00","end":"09:00"}`(支持跨午夜)。**窗口外定时与手动执行都不启动** | -### PUT /api/jobs/ +> `params.steps` 要一起传:后端**不提供默认步骤**、也不做参数校验(编辑器是参数正确性的唯一关卡)。不传 steps 也能建成功,但**执行时会立即报错**"通用步骤任务没有可执行步骤"。 -更新任务计划。只需传要更新的字段。 +### PUT /api/jobs/ · DELETE /api/jobs/ -(权限:任务管理) - -**请求**(JSON): -```json -{"params": {"max_duration": 1800}} -``` - -**响应**: -```json -{"ok": true, "msg": "任务已更新", "job": {"...": "..."}} -``` - -### DELETE /api/jobs/ - -删除任务计划。不存在返回 404。 - -(权限:任务管理) - -**响应**: -```json -{"ok": true, "msg": "任务已删除"} -``` +- 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": true, "msg": "任务 刷视频-上午 已触发"} +{"ok": false, "error": "任务类型 douyin_nurture 已不存在(该类型已被删除),请删除此任务或改用现有类型"} ``` ### POST /api/jobs//toggle -启用/停用任务。 +`{"enabled": true|false}` → `{"ok":true,"msg":"任务已启用"}`;不存在 404。 -**请求**(JSON): -```json -{"enabled": false} -``` +### 分组 -**响应**: -```json -{"ok": true, "msg": "任务已停用"} -``` +| 接口 | 请求 | 响应 | +|------|------|------| +| `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. 设备池与自动发现 -### GET /api/groups +| 接口 | 请求 | 说明 | +|------|------|------| +| `GET /api/devices/pool` | — | 池内设备 + 实时 `online` 状态 | +| `POST /api/devices/pool/add` | `{"serial","name?","note?"}` | upsert;`IP:5555` 立即尝试 connect;后台采型号 | +| `POST /api/devices/pool/remove` | `{"serial"}` | 不存在 → 404 | +| `POST /api/devices/pool/toggle` | `{"serial","enabled"}` | 停用则不参与调度 | +| `POST /api/devices/pool/reconnect` | — | 后台并发 10 线程重连全部网络设备 | +| `POST /api/devices/pool/refresh_models` | — | 后台批量采型号 | +| `GET /api/devices/discovery` | — | 发现状态 + 待连接 + 池内断联(前端 10s 轮询) | +| `POST /api/devices/discovery/scan` | — | 后台扫描一轮(约 5-30s);已有扫描 → **409** | +| `POST /api/devices/discovery/confirm` | `{"serial"}` | 待连接 → 设备池 + connect + 采型号 | +| `POST /api/devices/discovery/ignore` | `{"serial"}` | 从待连接删除 | +| `POST /api/devices/discovery/reconnect` | `{"serial"}` | 只接受池内设备,否则 404 | +| `POST /api/devices/discovery/settings` | `{"enabled","subnets","interval","port"}` | 部分更新,存 `app_meta` | -列出所有分组。 - -**响应**: -```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/ - -更新分组(serials/description 按需传字段)。`` 不存在返回 404。 - -(权限:任务管理) - -**请求**(JSON): -```json -{"serials": ["192.168.1.100:5555", "192.168.1.101:5555"], "description": "更新描述"} -``` - -**响应**:`{"ok": true, "msg": "分组已更新"}` - -### DELETE /api/groups/ - -删除分组。不存在返回 404。 - -(权限:任务管理) - -**响应**:`{"ok": true, "msg": "分组已删除"}` +> 自动发现**只做 socket 探测 + 只读校验**,不会把设备直接拉进设备池(必须人工确认)。 --- -## 7. 运行控制 +## 7. 远程看屏与设备操作 -### POST /api/stop_device +| 接口 | 请求 | 说明 | +|------|------|------| +| `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 字符 | -停止单台设备的 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 台设备的异常状态"} -``` +坐标均为**设备原生像素**(`/api/screen/size` 给基准)。u2 调用异常统一 503,并清理 60s 连接缓存。 --- -## 8. 用户管理 +## 8. 元素抓取与步骤测试 -用户管理接口**仅管理员可用**(非管理员返回 403 `仅管理员可执行此操作`)。`uid` 为用户 id(整数)。 +| 接口 | 请求 | 响应要点 | +|------|------|---------| +| `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/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` 不存在返回 404。 - -**请求**(JSON): -```json -{"password": "newpass", "is_admin": true} -``` - -**响应**:`{"ok": true, "msg": "用户已更新"}` -> 不能取消最后一个管理员(返回 400)。 - -### DELETE /api/users/ - -删除用户。`uid` 不存在返回 404。 - -**响应**:`{"ok": true, "msg": "用户已删除"}` -> 不能删除默认管理员 `admin`、不能删除当前登录用户、也不能删除最后一个管理员(均返回 400)。 +`GET /api/uiauto/elements` 在 uiautodev 不可用时返回 **503**。设备 dump 慢时可能超时(见 [backlog](backlog/TODO.md))。 --- -## 9. 日志 +## 9. 应用管理(APK) -### GET /api/logs +| 接口 | 请求 | 说明 | +|------|------|------| +| `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. 用户与日志 -**参数**: -| 参数 | 类型 | 默认 | 说明 | +| 接口 | 鉴权 | 请求 | 错误 | |------|------|------|------| -| 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`:可选日志文件名数组。 +| `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 | — | 返回日志尾部 + 可选文件清单 | --- -## 10. 自定义动作 +## 11. 运维工具(adb / 剪贴板 / 应用版本 / Tailscale) -### GET /api/custom_actions +### adb -列出所有自定义动作(步骤打包)。需登录。 - -**响应**: -```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/ - -更新自定义动作(name/icon/steps,按需传字段)。不存在返回 404。 - -(权限:任务管理) - -**请求**(JSON): -```json -{"name": "登录流程 v2", "steps": [{"type": "click", "...": "..."}]} -``` - -**响应**:`{"ok": true, "msg": "已更新", "action": {...}}` - -### DELETE /api/custom_actions/ - -删除自定义动作。不存在返回 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 | +| `GET /api/adb/devices` | — | 维护终端看到的设备(本机 adb + 设备池合并) | +| `POST /api/adb/cmd` | `{"cmd":"…"}` | 执行 adb 命令 | -**响应**:成功返回 `image/jpeg`,失败返回 JSON 错误。 +`/api/adb/cmd` 的安全约束:**命中 `kill-server` / `disconnect` 直接拒绝**(红线);空命令 400;超 20s 返回"命令执行超时(20s)";与 worker 共用 `_ADB_LOCK`。 -### GET /api/uiauto/elements +### 其它 -获取设备 UI 元素树。uiautodev 本地服务未运行返回 503。 - -(权限:设备控制) - -**参数**: -| 参数 | 类型 | 说明 | +| 接口 | 请求 | 说明 | |------|------|------| -| serial | string | 设备 serial | +| `POST /api/tools/clipboard/set` | `{"serials":[…],"text":"…"}` | ClipInject 通道写入并读回校验;serials 空或非列表 → 400 | +| `POST /api/tools/appver` | `{"package":"com.xxx"}` | 并发查所有在线设备(≤10 并发);包名须匹配 `^[A-Za-z0-9_.]+$` | -**响应**: -```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": "//*"} - } - ] -} -``` +### Tailscale(全部 Admin) ---- - -## 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 文件和记录。 - -(权限:应用管理) - -**响应**:`{"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": "正在安装..."} - } - } -} -``` +| `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 只显示一次 | --- -## 13. 维护 / 工具 +## 12. AI 控制台 -设备池管理、自动发现、远程看屏/触控、定位、维护终端等集中在"工具"页(页内子分栏)。 -**权限说明**:设备池管理、自动发现、远程看屏/触控、定位等接口需 `devices` 权限; -**adb 终端仅管理员可用**(见各节标注)。 +配置存在 `app_meta`(`agent_*`)。 -### 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` 指定设备;浏览器 `` 直接渲染,客户端断开自动停止。数据源 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 `。 - -**响应**: -```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 +// GET /api/agent/config {"ok": true, "api_base": "https://api.deepseek.com", "model": "deepseek-v4-flash-vision-exp", - "api_key_masked": "sk-***abcd", "default_serial": "", "max_steps": "40"} + "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/config +### 运行 -保存 Agent 配置(部分更新)。**请求**(JSON): -`{"api_base": "...", "model": "...", "api_key": "...", "default_serial": "...", "max_steps": 40}` -(`max_steps` 1-200,默认 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` | 清空运行态历史 | -**响应**:`{"ok": true, "msg": "已保存"}` +**SSE 事件**: -### GET /api/agent/devices +| 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 不可达时给出明确文案) | -AI 可用设备列表(在线状态 + 是否有任务运行,前端据此把 busy 设备禁选)。 +空闲时每 15s 发 `: keepalive`;`done`/`error` 后关流。 -**响应**: -```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 +| 接口 | 说明 | +|------|------| +| `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 | -启动 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。 +| 接口 | 说明 | +|------|------| +| `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"}` | -**响应**:`{"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": "", - "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "calls": 0}, - "history": [{"role": "user", "content": "..."}]} -``` - -`usage` 为本轮累计 token 用量(`calls` = 模型调用次数;运行中实时增长,失败也保留已花费的)。 - -### 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: usage` `{prompt_tokens, completion_tokens, total_tokens, calls}` — **本轮累计** token 用量,每完成一次模型调用推一次(前端实时刷新计数与费用感) -- `event: done` `{answer, usage}` — 完成(`usage` 同上一节,最终累计) -- `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/ - -会话详情(全部消息文本)。不存在返回 404。 - -**响应**: -```json -{"ok": true, "id": "...", "title": "...", - "messages": [{"role": "user", "content": "..."}, - {"role": "assistant", "content": "...", - "usage": {"prompt_tokens": 0, "completion_tokens": 0, - "total_tokens": 0, "calls": 0}, - "reasoning": "模型推理链(≤6000 字符,可空)"}], - "created_at": "...", "updated_at": "..."} -``` - -> assistant 消息可带 `usage`(本轮 token 用量)与 `reasoning`(推理链,上限 `_REASONING_KEEP`=6000 字符)。 -> 两者**仅供前端展示/回看**,回灌模型上下文时只取 `role`/`content`(见 `_agent_thread`)。 - -### DELETE /api/agent/conversations/ - -删除会话(消息一并删除,不可恢复)。 - -**响应**:`{"ok": true, "msg": "会话已删除"}` - -### POST /api/agent/conversations//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": "巡检已启动,完成后刷新列表查看建议"}` - -### GET /api/agent/actions -动作经验库列表(命名动作 + 编辑器 schema 步骤 + 元素定位、禁坐标)。仅管理员。 -响应:`{"ok":true,"actions":[{id,name,app,aliases,params,steps,preconditions,hits,updated_at}]}`。 -由任务成功后的**成功步骤**自动蒸馏沉淀;执行前按动作名/别名召回并注入 system prompt。 - -### POST /api/agent/actions/delete -删除动作:`{id}`。仅管理员。 - -### POST /api/agent/actions/save -新增/编辑动作:`{id?, name, app?, aliases?, params?, steps, preconditions?}`。仅管理员。 -`steps` 可为数组或 JSON 字符串,经服务端校验(白名单 type + 必填;**拒绝坐标 click_xy**)。 -校验失败返回 400 并附原因。 - -### POST /api/agent/experience/keep - -人工保留经验(撤销"建议删除",后续巡检不再重复建议)。**请求**(JSON):`{"id": 1}` - -**响应**:`{"ok": true, "msg": "经验 #1 已保留"}` +机制详见 [AI_CONSOLE.md](AI_CONSOLE.md)。 --- -## 18. 系统备份(仅管理员) - -整库备份导出/导入(users.db + 可选 APK 文件)。导入涉及整库替换,仅管理员可用。 +## 13. 系统备份 ### POST /api/system/backup/export -生成导出 zip 并作为附件返回(含 users.db 一致快照 + manifest.json + 可选 `apks/`)。 -**请求**(JSON):`{"include_apk": true}` -**响应**:`application/zip` 附件下载(`download_name` 形如 `export_20260808_101000.zip`)。 +**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 字段 `file`,支持 .zip 或 .db)→ 暂存并校验 → 返回预览。 -校验失败返回 400。 +multipart 上传 `.zip` 或 `.db`(字段 `file`)→ 校验并暂存: -**响应**: ```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": ["备份为全量快照:含敏感信息,请妥善保管"] -}} +{"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 -确认应用导入:自动备份当前库到 `BACKUP_DIR/pre_restore_*.db`(安全网),再把暂存库 -落为「待生效恢复任务」。**重启 web_server 后生效**。token 无效/过期返回 400。 +`{"token":"…"}` → 先自动把当前库快照到 `data/backups/pre_restore_.db`(安全网),再把暂存库落到 `data/restore_pending/`。 -**请求**(JSON):`{"token": "12位hex"}` -**响应**: -```json -{"ok": true, "backup_name": "pre_restore_20260808_101000.db", - "message": "恢复任务已生成:当前库已自动备份,重启 web_server 后即应用导入的数据"} -``` +**必须重启服务才生效**(`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)。 diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 224790b..d820623 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -1,441 +1,372 @@ -# 架构详解 +# 架构详解(ARCHITECTURE) -本文面向想深入理解 `auto_control` 内部设计的开发者。如果你只想使用,看 [README.md](../README.md) 即可。 +> 适用读者:要改后端 / 前端 / 任务引擎的开发者。 +> 相关文档:[DATA_MODEL.md](DATA_MODEL.md)(表结构)、[API.md](API.md)(接口清单)、[DEVELOPMENT.md](DEVELOPMENT.md)(流程与红线)。 +> 文中引用为 `文件:行号`,以当前代码为准;**行号会随改动漂移,函数名与常量名是稳定锚点**。 --- -## 1. 分层设计 +## 1. 总览 -平台按"配置 / 核心 / 任务 / 前端 / 数据 / 日志 / 工具"分层,职责清晰、互不交叉: +### 1.1 分层 -| 层 | 路径 | 职责 | -|----|------|------| -| 配置层 | `config.py` | 项目根配置:adb 路径、web 端口、USB 远程 adb server 等基础设施。**不放任务参数** | -| 核心层 | `core/` | 框架运行时:日志、设备池、adb 操作、Worker 基类、任务管理器、u2 辅助、Action 基类 | -| 任务层 | `tasks/` | 每个 App 一个子包,自包含 `task.py` + `actions/`,互不依赖 | -| 前端层 | `templates/admin/` | 单页应用(纯 HTML+CSS+JS,无框架) | -| 数据层 | `data/` | SQLite 持久化 | -| 日志层 | `logs/` | 四类日志,10MB 滚动保留 5 份 | -| 工具层 | `bin/adb/` | adb 可执行文件 | -| 脚本层 | `scripts/` | 实用脚本 | +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ 表现层 templates/admin/*.html + static/admin/*.js(单页应用,无框架) │ +│ 7 个顶级 Tab;轮询 / SSE / MJPEG 三类实时通道 │ +└───────────────────────────┬──────────────────────────────────────────┘ + │ fetch JSON / SSE / MJPEG / 表单 +┌───────────────────────────▼──────────────────────────────────────────┐ +│ Web 层 web/(10 个 Flask 蓝图,全部 url_prefix 为空) │ +│ auth 鉴权 · monitor 状态与设备操作 · tasks 任务 · admin 用户与日志 │ +│ tools 运维 · devices 设备池 · apks 应用 · tailscale · agent AI 控制台 │ +│ system 备份 │ +└───────────────────────────┬──────────────────────────────────────────┘ + │ 直接函数调用(共享对象由 web/context.py 注入) +┌───────────────────────────▼──────────────────────────────────────────┐ +│ 领域层 task_manager(调度) device_worker(执行) device_pool(池) │ +│ system_backup(备份) apk_manager(应用) device_discovery │ +└───────────────────────────┬──────────────────────────────────────────┘ + │ +┌───────────────────────────▼──────────────────────────────────────────┐ +│ 基础层 adb_helper · u2_helper · uiauto_helper · ocr · clipboard │ +│ models(SQLite)· logger · config │ +└──────────────────────────────────────────────────────────────────────┘ + ▲ +┌───────────────────────────┴──────────────────────────────────────────┐ +│ 任务定义层 tasks/(BaseTask 注册表 + generic/ 通用步骤引擎) │ +└───────────────────────────┬──────────────────────────────────────────┘ + ▲ ▲ +┌───────────────────────────┴────────┐ ┌───────────┴──────────────────┐ +│ MCP Server(mcp_server/,:8033) │ │ AI Agent(mcp_agent/) │ +│ 19 个 de_* 工具,供外部 AI 调用 │ │ OpenAI 兼容模型 → MCP 工具 │ +└────────────────────────────────────┘ └──────────────────────────────┘ +``` -### 分层原则 +### 1.2 各层职责边界 -- **任务自包含**:每个任务的参数、Worker、操作都放在 `tasks//` 下,不污染全局 -- **核心不依赖任务**:`core/` 不 import `tasks/`,任务通过注册机制接入 -- **配置最小化**:`config.py` 只放基础设施配置,任务参数在各自 `task.py` 顶部 +| 层 | 做什么 | 不做什么 | +|----|--------|---------| +| 表现层 | 渲染、交互、轮询/流式拉取、按权限隐藏入口 | 不校验权限(只隐藏);不做业务判断 | +| Web 层 | 参数校验、鉴权装饰器、JSON 序列化、调用领域层 | 不直接操作 adb/u2(`monitor` 的看屏/截图除外,那本身就是"设备操作") | +| 领域层 | 调度、并发、重试、状态机、持久化 | 不感知 HTTP | +| 基础层 | adb / u2 / OCR / 数据库 / 日志的原子能力 | 不含业务规则 | +| 任务定义层 | 任务类型注册 + 具体任务执行逻辑 | 不感知调度与设备获取(`BaseWorker` 已封装) | + +**装配方向**:`web_server.py` 是唯一组装点;`web/context.py` 注入 `mgr` / `apk_mgr` / `device_pool`,避免 Web 层与领域层循环 import。 --- -## 2. 数据流 +## 2. 启动与装配顺序 -``` -┌──────────────┐ 创建/编辑任务 ┌─────────────┐ 分发 worker ┌──────────────┐ -│ 单页应用前端 │ ───────────────► │ TaskManager │ ─────────────► │ Worker(设备) │ -│ (monitor.html│ └─────────────┘ └──────────────┘ -│ fetch+DOM) │ ▲ │ -└──────────────┘ │ 状态/心跳 │ u2 操作 - │ │ ▼ - │ JSON API │ ┌─────────────────┐ - ▼ ┌──────────────┐ │ 设备 adb │ -┌──────────────┐ │ 看门狗监控 │ │ (IP:5555 直连 │ -│ web/ 蓝图包 │ └──────────────┘ │ / USB 远程) │ -│ (Flask API) │ └─────────────────┘ -└──────────────┘ - │ - ▼ -┌──────────────┐ -│ data/users.db│ SQLite 持久化(用户/分组/任务/设备池/待连接设备/自定义动作/APK记录/AI会话/经验库 + app_meta KV) -└──────────────┘ -``` +理解启动顺序很关键——**很多副作用发生在 import 期**。 -### 任务执行流程 +### 2.1 阶段 A:import 期副作用(`web_server.py:6-25`) -1. 前端创建 TaskJob(HTTP POST `/api/jobs`) -2. `TaskManager` 保存到 SQLite,如启用 cron 则注册到 APScheduler -3. 手动执行或 cron 触发时,`_run_job` 解析目标设备列表 -4. 每台设备起一个线程 `_run_with_retry`,含重试循环 -5. 线程内 `task.create_worker()` 创建 Worker,`worker.start()` 启动 -6. `BaseWorker.run()` 执行设备生命周期:占用 → 连接 → setup → run_task → teardown → 释放 -7. Worker 通过 `_update_status()` 实时上报状态到全局 `_WORKERS` 字典 -8. 前端轮询 `/api/status`(5 秒缓存)获取设备 + Worker 状态 +| 顺序 | 触发 | 副作用 | +|------|------|--------| +| 1 | `from core.task_manager import TaskManager` | 链式触发:`config.py` 模块体 → **读取根目录 `.env`**(`os.environ.setdefault`);`core/logger.py` → 创建 `logs/`;`tasks/__init__.py` → **任务类型注册**(`@register_task` 在 import 期执行) | +| 2 | `get_logger` / `core.models` | 得到 `db` / `init_db` / `User` | +| 3 | `from config import WEB_HOST, WEB_PORT` | 读取端口常量 | + +> `.env` 是"import config 的副作用",因此 `web_server.py:30` 读 `WEB_SECRET_KEY` 时它已生效。 + +### 2.2 阶段 B~D:Flask 与领域对象 + +| 阶段 | 位置 | 做了什么 | +|------|------|---------| +| **B** | `:28-41` | `Flask(__name__)`;会话密钥(`.env` 的 `WEB_SECRET_KEY`,缺失则随机生成并 warning);`TEMPLATES_AUTO_RELOAD=True`;`SQLALCHEMY_DATABASE_URI=sqlite:///data/users.db`;`LoginManager` + `login_view="auth.login"` | +| **C** | `:43-50` | **恢复任务消费** `consume_pending_restore()` —— 必须在 engine 首次打开 `users.db` **之前**(Windows 无法替换被持有的文件)。失败只记日志,不阻塞启动 | +| **D** | `:53-61` | `init_db(app)`(建表 → 版本化迁移 → 默认管理员 → 旧 JSON 迁移);`device_pool.init_app`(**起一次性线程**,3s 后采集型号);`device_discovery.init_app`(**起常驻扫描线程**);`TaskManager(app=app)`(APScheduler + 看门狗 + 从库加载分组/任务 + 重注册 cron);`ApkManager(app=app)` | + +### 2.3 阶段 E~G:蓝图、巡检调度器、真正启动 + +| 阶段 | 位置 | 做了什么 | +|------|------|---------| +| **E** | `:64-67` | `context.init(...)`;`register_blueprints(app)`(10 个蓝图);`agent_api.set_app(app)`(供后台线程推 app context) | +| **F** | `:71-80` | **第二个独立 APScheduler**:`CronTrigger(hour=3, minute=47)` 挂经验库巡检;失败仅 warning | +| **G** | `:253-265`(`__main__`) | `_ensure_uiauto_running()`(拉起 uiautodev:20242,写 `data/uiauto.pid`,`atexit` 清理)→ `_preconnect_pool_devices()`(后台并发 connect 池内网络设备)→ `_run_server()`(候选端口依次 bind:`0.0.0.0:18050` → `127.0.0.1:18050` → `127.0.0.1:18051..18055`);退出时 `mgr.shutdown()` + `device_discovery.shutdown()` + 停 uiautodev | + +> ⚠️ **阶段 A~F 在 import 期就会起线程/调度器**,只有 uiautodev 拉起与预连接在 `__main__` 分支。以 WSGI 方式 import 本模块会得到"半个启动"的进程——本地调试请直接 `python web_server.py`。 --- -## 3. 核心模块详解 +## 3. 线程与并发模型 -### 3.1 DevicePool(`core/device_pool.py`) +### 3.1 常驻线程一览 -设备池(已摘除 OpenSTF):SQLite `devices` 表 = 设备清单,本机 adb = 在线状态。 +| 名称 | 启动位置 | 职责 | 周期 | +|------|---------|------|------| +| Flask 请求线程 | `app.run(threaded=True)` | 每请求一线程 | — | +| **任务调度器** `BackgroundScheduler` | `TaskManager.__init__` | cron 触发 / 停止任务 | 按 cron | +| **巡检调度器** `BackgroundScheduler` | `web_server.py` | 经验库 AI 巡检 | 每日 03:47 | +| `worker-watchdog` | `start_watchdog()` | `running/connecting` 心跳超时 → 标 `error` | 30s 检查 / 120s 阈值 | +| `device-discovery` | `device_discovery.init_app` | 网段扫描 + 断联重连 | 首轮延迟 15s,之后 interval(默认 60s) | +| `_refresh_models_bg` | `device_pool.init_app` | 启动后采集全部在线设备型号 | 一次性(3s 后) | +| `BaseWorker` × N | `TaskManager._run_with_retry` | 单设备任务执行 | 任务期 | +| `_run_with_retry` × N | 同上 | 单设备重试循环 | 任务期 | +| `fg-scan-once` | `_ForegroundScanner.scan_once` | 前台 App 扫描 | 手动触发,`Event` 防重入 | +| `apk-install` | `ApkManager.install` | 并发 5 台安装 APK | 安装期,全局单任务 | +| Agent 执行线程 | `web/agent_api.py` | AI 控制台一轮会话 | 按需 | +| 巡检手动线程 | `web/agent_api.py` | 手动触发巡检 | 按需 | -**关键设计**: -- `list_configured()` — 清单(管理页维护,enabled=False 不参与调度) -- `list_online()` — 本机 adb 在线设备;池内有 USB 设备(serial 无冒号)时合并 220 - 远程 adb server(`USB_ADB_HOST:PORT`,host 网络模式 5037)状态 -- `list_ready()` — 清单 ∩ 在线(任务调度用) -- CRUD — `add_device`(upsert)/ `remove_device` / `set_enabled` -- 单实例互斥由 `TaskManager._running[serial]` 内存锁保证(无跨实例占用概念) -- 全模块不 connect/kill-server/disconnect(遵守共享 adb transport 红线) +两个 APScheduler 相互独立,时区均固定 `Asia/Shanghai`。 -### 3.2 STFDevice + BaseWorker(`core/device_worker.py`) +### 3.2 锁与并发保护 -**STFDevice** — 单设备生命周期管理: -- `acquire()`:IP:5555 直连 adb connect;USB(serial 无冒号)校验 220 远程 adb server 可见性 -- `release()`:无操作(不 disconnect,红线) -- 互斥由 TaskManager `_running` 保证,设备池负责在线判断 +| 锁 | 位置 | 保护对象 | 说明 | +|----|------|---------|------| +| `_ADB_LOCK` | `core/adb_helper.py` | adb connect 串行化 | connect 很快,串行不影响整体并发;不覆盖长命令 | +| `_WORKERS_LOCK` | `core/device_worker.py` | 全局设备状态表 `_WORKERS` | "检查+更新"在同一锁内,避免与任务启动竞态 | +| `TaskManager._lock` | `core/task_manager.py` | `_running` / `_stop_requested` | **抢占时禁止在锁内调 `stop_device`(死锁)** | +| `_engine_lock` | `core/ocr.py` | OCR 引擎懒加载 + 推理串行 | 推理 0.2~0.5s,锁开销可忽略 | +| `_scan_lock` / `_stop_event` | `core/device_discovery.py` | 定时/手动扫描互斥 | `acquire(blocking=False)` | +| `_status_cache_lock` | `core/task_manager.py` | 状态缓存(TTL 5s) | 避免 `/api/status` 每次都查库 + adb | -**BaseWorker** — 通用 Worker 基类(继承 threading.Thread): +**无锁部分**:`device_pool` 与 `models` 不持显式锁,依赖"每次操作独立 app context + SQLite WAL + `busy_timeout=5000`"。 -``` -run() 主循环(不要重写): - 1. acquire 设备 - 2. u2.connect(30s 超时保护) - 3. setup(d) ← 子类可选钩子 - 4. run_task(d) ← 子类必须实现 - 5. teardown(d) ← 子类可选钩子 - 6. finally: release 设备 -``` +### 3.3 错峰与心跳 -**超时保护**: -- `u2.connect()` 用 `ThreadPoolExecutor + 30s 超时`,防止 atx-agent 无响应永久 hang -- `d.info` 用 `ThreadPoolExecutor + 10s 超时` +- **错峰启动**:`_START_STAGGER_SEC = 0.2`,第 i 台设备延迟 `i × 0.2s` 启动(100 台 ≈ 20s 铺开),避免批量触发时的 adb 连接风暴。 +- **心跳看门狗**:任何状态写入都会刷新 `last_heartbeat`;`running/connecting` 设备超过 120s 无心跳 → `status="error"` + `last_error="心跳超时…"`。长耗时的业务循环必须周期性 `self.heartbeat()`(`set_action` / `set_progress` 也会刷新)。 +- **单实例约束**:同一 serial 同时只有一个 worker(`TaskManager._running`);重复触发同任务跳过,不同任务未开抢占也跳过。 -**心跳看门狗**(`_Watchdog`): -- 后台线程,每 30 秒扫描一次 -- Worker 超过 120 秒无心跳 → 标记 `error` -- 防止设备被占用却不干活 +--- -**全局状态注册表**(`_WORKERS`): -- `serial -> status dict`,线程安全(`_WORKERS_LOCK`) -- 供 `web_server` 读取实时状态,前端通过 `/api/status` 展示 +## 4. 设备生命周期 -### 3.3 TaskManager(`core/task_manager.py`) +### 4.1 入池(三条路径) -统一管理:任务类型注册、设备分组、任务计划、定时调度、重试、持久化。 - -**核心组成**: - -| 组件 | 说明 | -|------|------| -| `scheduler` | APScheduler BackgroundScheduler,cron 触发任务 | -| `groups` | 设备分组(内存业务对象,持久化到 SQLite) | -| `jobs` | 任务计划(内存业务对象,持久化到 SQLite) | -| `_running` | 运行中的 worker(serial -> worker 信息) | -| `_stop_requested` | 用户请求停止的 serial 集合(阻止后续重试) | -| `_fg_scanner` | 前台 App 扫描器(不打扰设备) | - -**调度模式**: -- `once`:不注册 cron,手动执行 -- `cron`:注册启动 cron,到点启动所有目标设备 -- `cron_stop`:注册启动 cron + 停止 cron,到点停止本任务 worker - -**重试策略**: -- `DeviceOfflineError`:立即放弃,不重试(设备掉线短时间不会自愈) -- 其他异常:按 `retry.max_attempts` 重试,间隔 `retry.delay` -- 临时错误(端口耗尽):退避 max(delay, 120s) -- 用户停止:加入 `_stop_requested`,阻止任何后续重试 - -**并发控制**:同一 serial 同时只允许一个 worker,避免冲突。 - -**状态缓存**:`get_status()` 带 5 秒缓存,避免每次 /api/status 都查库/adb 阻塞前端。 - -### 3.4 前台 App 扫描器(`_ForegroundScanner`) - -**设计原则:不打扰设备**,扫描不会让设备退出当前 App。 - -| 设备状态 | 处理方式 | 是否打扰 | -|---------|---------|---------| -| worker 运行中(IP:5555) | 复用已有 ADB 连接查询 | 否 | -| worker 运行中(USB) | 经 220 远程 adb server 查询 | 否 | -| 完全空闲 | 返回"空闲"(不主动 connect) | 否 | - -> **为什么不扫描空闲设备的前台 App**:IP:5555 的 adb transport 是共享的(历史与 STF provider 共用),外部 connect/disconnect 会扰动共享连接,遵守既有红线。 - -### 3.5 ADB 操作(`core/adb_helper.py`) - -**全局锁串行化**:`_ADB_LOCK` 确保所有 adb 调用串行执行,避免多线程竞争 adb server。 - -**铁律:绝不 kill-server**: -- `adb kill-server` 会断开所有设备的 adb transport -- 全部设备连接被重建,影响所有运行中的任务 -- 同理绝不 disconnect IP:5555(共享 transport 红线,见 DEVELOPMENT.md) - -| 函数 | 说明 | -|------|------| -| `adb_connect(url, retries=5)` | adb connect(带重试,绝不 kill-server) | -| `adb_connect_light(url)` | 轻量 connect(单次尝试,扫描专用) | -| `adb_disconnect(url)` | adb disconnect | -| `screenshot(serial)` | 截图(adb exec-out screencap -p,只读安全) | -| `get_foreground_app(url)` | 获取前台 App 包名(dumpsys window) | -| `identify_device(serial)` | 让设备响铃识别 | - -### 3.6 数据模型(`core/models.py`) - -SQLAlchemy 模型,存于 `data/users.db`: - -| 模型 | 表名 | 说明 | +| 路径 | 入口 | 过程 | |------|------|------| -| `User` | user | 后台用户(Flask-Login 认证,Werkzeug 哈希密码;`is_admin` 管理员 + `perms` 业务权限位) | -| `DeviceGroup` | device_group | 设备分组(serials 存 JSON) | -| `TaskJob` | task_job | 任务计划(target/params/schedule/retry 存 JSON) | -| `CustomAction` | custom_action | 自定义动作(步骤打包,steps 存 JSON) | -| `ApkFile` | apk_file | APK 文件元信息 | -| `Device` | device | 设备池清单(替代 STF 池;enabled=False 不参与调度,含 model 型号列) | -| `PendingDevice` | pending_device | 自动发现「待连接池」(扫描发现、用户确认后才入正式池) | +| 手工添加 | `POST /api/devices/pool/add` | `device_pool.add_device`(upsert)→ 有 `:` 则 `adb connect` → 后台采集型号 | +| 自动发现确认 | `POST /api/devices/discovery/confirm` | 扫描写 `pending_device` → 确认后 `add_device` + 删 pending + connect + 采型号 | +| 启动预连接 | `_preconnect_pool_devices()` | 进程启动时并发 connect 池内网络设备(仅 `IP:5555`) | -> 表名默认取类名小写(models.py 未写 `__tablename__`)。另有三张非模型表,由原生 SQL 幂等创建、**不走 SCHEMA_MIGRATIONS**: -> - `app_meta`(KV):`_migrate_schema()` 内建表,存 `schema_version`、`discovery_*`、agent 配置 `agent_*` 等; -> - `agent_conversation` / `agent_experience` / `experience_audit`:AI 控制台会话 / 任务级经验(配方)/ 经验巡检(`web/agent_api.py` 顶部 `CREATE TABLE IF NOT EXISTS`)。 -> - `agent_action`:**动作经验库**(命名动作 = 可复用单元,steps 用编辑器 schema 且带元素定位、禁坐标);由任务成功后从**成功步骤**蒸馏,执行前按名字/别名召回并注入(`web/agent_api.py` `_distill_actions/_find_actions`)。 -> -> **备份覆盖(红线)**:持久化表须登记进 `core/system_backup.py` 的 `SUMMARY_TABLES`(→ 导出清单/预览可见、覆盖自检生效),并同步 `doc/DEPLOY.md` §3.5;未登记的表在备份预览里不可见,会被误判为"没备份"。 +断联设备的自动重连由发现线程每轮执行(只重连 `IP:5555`)。 -**数据库初始化**(`init_db`): -- 创建所有表 -- 首次启动创建默认管理员 `admin/admin123` -- 自动迁移旧 `groups.json` / `jobs.json` 到 SQLite(迁移后归档为 `.migrated`) -- 版本化 schema 迁移(`SCHEMA_MIGRATIONS`,**当前到 v4**,见 `core/models.py`):结构变更必须追加迁移条目,`create_all` 只建新表不加列 +### 4.2 可用性判定 -### 权限模型 +``` +list_configured() 设备池中 enabled=True 的 serial +list_online() 本机 adb devices 中 state=device(池内有 USB 设备时并查 220 远程 adb server) +list_ready() 两者交集 ← 调度 "all" 模式取这个 +``` -- `User.perms` 存业务权限位 JSON 数组(`tasks`/`devices`/`apks`/`logs`),`is_admin=true` 拥有全部权限(`has_perm` 短路) -- 后端统一用 `@perm_required(PERM_X)` / `@admin_required` 装饰器拦截(web/auth.py),无权限返回 403; - 查看类 GET 接口只要求登录;用户管理、adb 终端(`/api/adb/cmd`)强制 `admin_required` -- adb 终端安全红线:拒绝 `kill-server` / `disconnect`(共享 adb transport) -- 前端 `loadMe()` 拉取 `/api/me`,用 `data-perm` 属性隐藏无权限的 tab/按钮,行内按钮用 `_can(perm)` 判断 -- 安全兜底:**前端隐藏只是 UX,权限强制在后端**;新增路由时按"写操作必须带权限装饰器"的约定 +### 4.3 执行期状态字段 -### 3.7 APK 管理(`core/apk_manager.py`) +设备状态存在内存注册表 `_WORKERS[serial]`(**不落库,重启即清零**): -APK 上传/解析/批量安装。 - -**设备连接策略(直连)**: -- 直接 `adb connect serial`(serial 是 IP:5555) -- 不经过占用/释放(单实例互斥由调度器内存锁保证) -- 安装后不主动 disconnect(共享 adb transport 红线) - -**安装流程**: -1. 上传 APK → 保存到 `data/apks/` → pyaxmlparser 解析包名/版本 → 入库 -2. 批量安装 → 后台线程 → 每台设备直连 adb install → 验证包名 -3. 跳过 worker 运行中的设备(避免打断任务) - -### 3.8 元素抓取(`core/uiauto_helper.py`) - -封装 uiautodev 本地服务(端口 20242)的客户端。 - -| 函数 | 说明 | +| 字段 | 含义 | |------|------| -| `is_running()` | 探测 uiauto2 服务是否运行 | -| `list_devices()` | 获取 uiauto2 已连接的设备列表 | -| `get_screenshot(serial)` | 获取设备截图(JPEG) | -| `get_elements(serial)` | 获取设备 UI 元素树(扁平化列表) | +| `status` | `idle` / `connecting` / `running` / `done` / `error` / `released` / `failed` | +| `serial` / `model` / `device_name` | 标识 | +| `task_job` / `attempt` / `max_attempts` | 当前任务与重试进度 | +| `current_action` / `progress` | 当前动作与进度(前端直接渲染) | +| `last_error` / `last_warning` | 最近错误 / 选择器健康告警 | +| `last_heartbeat` / `end_time` | 看门狗与时长上限 | +| `present` / `ready` | 是否在线(对外 `/api/status` 字段) | -元素树解析:递归提取每个节点的 `resource-id/text/content-desc/class/bounds` 等属性,并推荐最佳选择器(优先 xpath)。 +### 4.4 状态迁移 -**XPath 序号语义(重要)**:同一属性多个实例时,生成 **`(//*[@resource-id="x"])[k]`**(整体加括号 = 第 k 个匹配)。 -不可写成 `//*[@resource-id="x"][k]`——那在 XPath 里是"**在其父节点中排第 k**",多实例时 `[2..n]` 会全部匹配不到 -(2026-09-10 实测修复:抖音底部 4 个 tab 同 id,旧写法除 `[1]` 外全失效)。执行器 `tasks/generic/task.py` -对**历史遗留**的 `//*[@attr=…][k]` 形态做窄范围纠正(`_norm_legacy_xpath`,只改前缀、不动结构路径的兄弟序号)。 +**Worker 侧**(`BaseWorker.run`): -### 3.9 屏幕 OCR(`core/ocr.py`) +``` +connecting ──获取设备──▶ u2 连接 ──▶ running ──▶ setup ──▶ run_task ──▶ teardown + │ + 未被 stop ──▶ done │ + 异常 ──▶ error(DeviceOfflineError 单独分类,不重试) + finally ──▶ 仅当仍为 running/connecting 时置 released +``` -条件判断的 `ocr` 选择器实现:截屏 → RapidOCR(ONNX 推理,中英文模型随包内置)→ 关键词匹配 → 返回文字中心像素坐标(与 u2 `d.click` 一致)。 +> `finally` 里的状态判断是为了**不覆盖业务结果**(`done`/`error` 必须保留)。 -- **跨平台**(Windows/Linux/macOS),依赖 `rapidocr_onnxruntime`;服务器无显示器环境建议将 opencv-python 换成 opencv-python-headless -- 引擎懒加载单例 + 并发加锁(识别约 0.2-0.5s/次) -- 返回坐标约定:像素、原点左上 +**调度侧**(`_run_with_retry`):worker 结束后读 `status` → `done` 即成功返回;否则按 `max_attempts` 重试(`[transient]` 错误额外加长退避)→ 重试耗尽置 `status="failed"`,并把**真实失败原因**拼进 `last_error`(截断 200 字符)。 -### 3.10 Web 层蓝图包(`web/`) +### 4.5 释放 -路由按功能域拆分(模块化开发底线,便于定位问题): - -| 模块 | 职责 | -|------|------| -| `auth.py` | 登录/登出/CSRF/权限装饰器/页面路由(/、/wall) | -| `monitor.py` | 状态/运行控制/设备操作/远程看屏(流+缩略图+触控) | -| `tasks_api.py` | 任务计划/分组/自定义动作/元素抓取/步骤测试 | -| `admin_api.py` | 用户管理/日志 | -| `tools_api.py` | adb 终端/剪贴板注入/应用版本 | -| `devices_api.py` | 设备池管理 + 自动发现(扫描/确认/忽略/手动重连/型号采集) | -| `apks_api.py` | 应用管理 | -| `tailscale_api.py` | Tailscale 管理 | -| `agent_api.py` | AI 控制台:会话 / 运行 / SSE / 停止 / 配置 / 经验库与巡检 / **动作库**(读写 `agent_conversation`、`agent_experience`、`agent_action`、app_meta `agent_*`) | -| `system_api.py` | 系统数据备份导出 / 导入恢复(`/api/system/backup/*`,仅 admin) | -| `common.py` | 跨模块共享(合并设备列表/屏幕状态) | -| `context.py` | 共享对象注入(mgr/apk_mgr/device_pool) | - -`web_server.py` 只做装配与启动(266 行):app 创建;`init_db` 前消费待生效备份恢复(`consume_pending_restore`);初始化 device_pool / device_discovery;装配 TaskManager / ApkManager;注册 10 个蓝图(auth/monitor/tasks/admin/tools/devices/apks/tailscale/agent/system,`web/__init__.py`);注册经验巡检 APScheduler(03:47 Asia/Shanghai);uiautodev 子进程启停与设备池预连接线程;启动。 +`STFDevice.release()` 是**空实现**——直连模式下**绝不 disconnect**(共享 adb transport 红线)。类名 `STFDevice` / `STFError` 是 STF 时代的历史命名,功能上已与 STF 无关。 --- -## 4. 任务系统设计 +## 5. 任务调度链路 -### 4.1 注册机制 +### 5.1 完整调用链 ``` -tasks/__init__.py - ├── from .base import BaseTask, register_task, list_task_types, get_task_class - └── from .generic import task # 触发 @register_task(当前唯一任务类型) +① 注册 add_job / update_job / toggle_job + → _add_cron:scheduler.add_job(_on_cron_trigger, CronTrigger.from_crontab(...), + id=f"job_{id}_start", replace_existing=True) + (cron_stop 额外注册 job_{id}_stop;启动时 _load() 为 enabled 任务重注册) + +② 触发 APScheduler → _on_cron_trigger(job_id) + → 任务存在?→ _in_run_window(schedule)?→ _run_job(job) + +③ 解析 job.resolve_serials(self) # all=池内在线 / group=分组∩池 / serial=指定 + → 为空则 warning 返回 + +④ 任务类 get_task_class(job.task_type) → 未知则 error 返回(run_job_now 会直接报错) + → task_cls();max_attempts = max(1, retry.max_attempts) + +⑤ 铺开 对每台设备起线程 _run_with_retry(task, serial, job, ..., idx * 0.2s) + +⑥ 单设备 _run_with_retry: + sleep(错峰) → 停止检查 → 单实例/抢占判定 → 登记 _running[serial] + → _update_status(task_job=..., attempt=...) + → worker = task.create_worker(serial, job.params) + → worker.start() → worker.join() + → 读 status:done 成功;否则重试([transient] 退避 max(delay,120)s) + → 耗尽:status="failed" + last_error(含真实原因) + → finally:清停止标志;本任务若是抢占任务则归还设备(重跑被抢占任务) + +⑦ 上报 worker 内 _update_status → 内存注册表 + → TaskManager.get_status(5s 缓存) + → _merge_status(合并池信息、清理陈旧条目) + → GET /api/status → 前端 5s 轮询渲染 + +⑧ 停止 stop_device / stop_all → _stop_requested.add + worker.stop()(置 Event) + 业务循环检查 self.stopped() + +⑨ 下次运行时间 next_run_of → _next_run_time(考虑运行窗口,最多向后探测 200 次) ``` -`@register_task` 装饰器将 Task 类注册到全局 `_TASK_TYPES` 字典,key 为 `task_type` 字符串。 +### 5.2 抢占机制 -### 4.2 参数深合并 - -Job 下发时只传需要覆盖的字段,调度器做三层合并: - -1. **顶层字段**:Job params 覆盖 DEFAULT_PARAMS -2. **actions 字段**:参数级深合并 - - 前端没传的 action → 用默认 - - 前端传了 → `enabled` 和 `params` 分别合并 - - `params` 再深合并一层(保留前端没传的子参数) - -示例:只想改点赞概率,Job params 只需: -```json -{"actions": {"like": {"params": {"rate": 0.5}}}} -``` - -### 4.3 Action 系统 - -每个 App 有独立的 Action 注册表(`create_action_registry()`),互不污染。 - -``` -core/actions/base.py — BaseAction 全局基类 + should_trigger + register_action -tasks//actions/base.py — ACTIONS = create_action_registry() + list/get 函数 -tasks//actions/xxx.py — @register_action(ACTIONS) XxxAction - -> 当前没有 App 专属任务包(只剩 `tasks/generic/`,步骤全走通用 STEP_TYPES,不建专属 action 表); -> 新增专属任务类型时按上面三行建 `tasks//actions/`。 -``` - -**循环导入坑**:`actions/__init__.py` 必须先 `from .base import ACTIONS`,再 `from . import like`。 +任务参数 `preempt=true` 时:`all` 模式目标集合变为"全部在线池内设备"(含正在跑的);遇到设备已被占用时在**锁外**调 `stop_device` 并最多等 30s 接管;本任务结束后自动重新启动被抢占的任务(`preempted_job` 必须定义在重试循环外,否则归还信息会丢)。 --- -## 5. 前端设计 +## 6. 前端架构 -### 5.1 单页应用 +### 6.1 单页应用 -`templates/admin/monitor.html` 是纯 HTML+CSS+JS 单页应用,无框架依赖。 +- 主页面 `templates/admin/monitor.html`:一个内联 `