docs: doc/ 全量重整——按现状重写并建立文档索引;项目统一更名 auto_control
背景:文档长期落后于代码(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 种任务类型)
This commit is contained in:
+81
-31
@@ -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 目标(默认 [email protected])
|
||||
# [email protected]
|
||||
# 监听地址与端口是 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
|
||||
# ==============================================================================
|
||||
|
||||
@@ -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/<name>/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/`) | 设备连接与底层操作 |
|
||||
|
||||
@@ -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=<id>`。
|
||||
|
||||
---
|
||||
|
||||
## 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 只会显示为文本
|
||||
- **推理链**:`<details>` 折叠块,流式时展开、正文开始时自动收起;手动点过后不再自动改;摘要显示字数
|
||||
- **工具卡片**:每步 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` 等前端文件有缓存 |
|
||||
+3
-2
@@ -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. 背景与目标
|
||||
|
||||
|
||||
+486
-1202
File diff suppressed because it is too large
Load Diff
+294
-363
@@ -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/<app>/` 下,不污染全局
|
||||
- **核心不依赖任务**:`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/<app>/actions/base.py — ACTIONS = create_action_registry() + list/get 函数
|
||||
tasks/<app>/actions/xxx.py — @register_action(ACTIONS) XxxAction
|
||||
|
||||
> 当前没有 App 专属任务包(只剩 `tasks/generic/`,步骤全走通用 STEP_TYPES,不建专属 action 表);
|
||||
> 新增专属任务类型时按上面三行建 `tasks/<app>/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`:一个内联 `<style>` + 7 个 Tab 面板 + 7 个模态框容器 + 11 个 `<script src>`
|
||||
- 独立页面:`login.html`(登录)、`wall.html`(监控大屏,**完全自包含**,自带 CSS/JS,不加载 `static/admin/*.js`)
|
||||
- 服务端内联页:`GET /locate`(设备端定位大字页,免登录)
|
||||
- 响应头强制 `no-store`,避免后台改版后浏览器拿旧页面
|
||||
|
||||
- **Tab 切换**:7 个顶级 Tab(监控/任务/日志/用户/工具/AI 控制台/系统),均在 monitor.html 内 `.tab-panel` 切换(纯 DOM 操作);独立页面仅 `/login`、`/wall`。原"分组"已无顶层入口(移到工具页子分栏)
|
||||
- **数据获取**:`fetch()` 调 JSON API,5 秒轮询 `/api/status`
|
||||
- **状态渲染**:设备表格、任务卡片、进度条、徽章,纯 DOM 操作
|
||||
### 6.2 Tab 与子分栏
|
||||
|
||||
**监控页「任务运行概况」卡片(2026-09-10 收敛)**:每张卡片只保留两个操作——**执行任务**(`POST /api/jobs/:id/run`)
|
||||
与**停用任务/启用任务**(`POST /api/jobs/:id/toggle`,按当前状态切换文案);原"编辑/删除"已移除
|
||||
(编辑与删除统一去「任务」Tab 操作,避免监控页误删)。卡片新增**覆盖设备**一行:设备清单来自
|
||||
`/api/jobs` 的 `coverage`(后端按 target 定义解析,见 [API.md](API.md) §5),在线/运行中状态由前端
|
||||
用已在手的 `/api/status` 数据标注成彩色 chip(在线=蓝、运行中=黄+⏳、离线或不在池=灰划线+✕,
|
||||
完整 serial 与型号放 tooltip)。
|
||||
| 顶级 Tab | `data-tab` | 权限 | 子分栏 |
|
||||
|---------|-----------|------|--------|
|
||||
| 监控 | `monitor` | 登录即可 | — |
|
||||
| 任务 | `tasks` | 登录即可(写操作需 `tasks`) | `plan` 任务计划 / `actions` 自定义动作 |
|
||||
| 日志 | `logs` | `logs` | — |
|
||||
| 用户 | `users` | `admin` | — |
|
||||
| 工具 | `tools` | `admin` | `clipboard` / `adb` / `ts` / `apks` / `appver` / `devapps` / `devpool` / `groups` |
|
||||
| AI 控制台 | `agent` | `admin` | — |
|
||||
| 系统 | `system` | `admin` | `backup` 数据备份 / `restore` 导入恢复 |
|
||||
|
||||
### 5.2 步骤编辑器
|
||||
子分栏会记住上次选中位置(`_activeSubs`);`devpool` 子分栏自带 10s 轮询,切走即停。
|
||||
|
||||
`generic_steps` 任务的步骤编辑器(任务弹窗已加宽到 1000px):
|
||||
- 左侧操作库(拖拽源,按 交互操作/屏幕与App/流程控制 分组)
|
||||
- 中间画布(步骤卡片列表,HTML5 Drag API 排序 + 跨层级嵌套:循环套循环、动作组)
|
||||
- 每个步骤卡片可展开参数表单
|
||||
- 选择器字段旁有"抓取元素"按钮(独立模态框)
|
||||
- **容器步骤**(loop/group/if_el):卡片内嵌子步骤容器接收拖入;if_el 有"✅找到时/❌未找到时"两个独立分支容器,分支可嵌套任意步骤
|
||||
- **条件判断**(if_el):选择器支持 xpath 等 UI 树选择器或 OCR识别(`core/ocr.py`,截屏匹配图片/画布文字,命中可自动点击)
|
||||
- 所有递归操作(选择打包、存自定义动作、校验、防循环自套)统一遍历 children/then/else 三个子数组
|
||||
### 6.3 JS 分工
|
||||
|
||||
### 5.3 页内子分栏
|
||||
|
||||
任务/工具/系统 Tab 用通用 `showSubTab(tabId, name)` 实现页内子分栏:每个子分栏一个 `.sub-panel`,
|
||||
`_activeSubs` 记住各 Tab 上次选中的子分栏。现状子分栏:
|
||||
- **任务**:任务计划 / 自定义动作
|
||||
- **工具**:剪贴板注入 / adb 远程终端 / Tailscale 管理 / 应用管理 / 应用版本管理 / 设备已装应用 / 设备池管理 / 设备分组
|
||||
- **系统**:数据备份 / 导入恢复
|
||||
|
||||
(原"分组"顶级 Tab 与"维护/STF 服务"子分栏已不存在——分组已移入工具页子分栏,STF 已摘除。)
|
||||
|
||||
### 5.4 AI 控制台的记忆面板
|
||||
|
||||
AI 控制台(顶级 Tab)右上角两个模态框,管理自进化记忆:
|
||||
- **🧠 经验库**:任务级经验(`agent_experience`,整任务配方)+ 每日 AI 巡检建议(删除需人工确认)。
|
||||
- **🎬 动作库**:动作级经验(`agent_action`)——命名动作(可含 1~N 步)+ 编辑器 schema 步骤 + **元素定位(禁坐标)**;由任务成功后从**成功步骤**自动蒸馏,执行前按名/别名召回注入;面板支持查看/编辑/删除/手动新建(保存经服务端校验,坐标步骤被拒)。
|
||||
- **会话列表显示会话 ID**(前 8 位,等宽小字),点击即复制完整 ID——便于反馈问题时引用 `conv=<id>`。
|
||||
|
||||
#### 5.4.1 回答渲染与 token(2026-09-10)
|
||||
|
||||
| 能力 | 落点 | 说明 |
|
||||
| 文件 | 职责 | 备注 |
|
||||
|------|------|------|
|
||||
| **Markdown 渲染** | `static/admin/markdown.js`(`renderMarkdown()`) | 自研轻量渲染器,**不引 CDN**(生产 220 在内网):标题/段落/软换行/粗斜体/删除线/行内代码/围栏代码块/有序无序列表(含嵌套)/引用/表格/分隔线/链接。**先 `esc()` 转义再套标记**,模型输出里的 HTML 只显示为文本(防注入) |
|
||||
| **推理链可折叠** | `agent.js` `_appendReasoning()` + `<details class="reasoning">` | 流式思考时自动展开、正文开始时自动收起;用户手动点过 `summary` 后不再自动改(`dataset.touched`);摘要显示「思考过程(N 字)」 |
|
||||
| **token 显示** | `mcp_agent/agent.py` `_accumulate_usage()` + `agent_api` SSE `usage` 事件 | 每次模型调用完成后推**本轮累计**(`prompt/completion/total/calls`);单条消息脚注 + 顶栏「本会话累计」(历史 + 运行中) |
|
||||
| **推理链/用量落库** | `_agent_thread` 把 `usage`、`reasoning` 写进会话 assistant 消息 | 刷新页面后仍可回看;**回灌模型上下文时只取 `role`/`content`**(不污染 token) |
|
||||
| `base.js` | esc / CSRF / 权限 / API 封装 / Toast / Tab 与子分栏切换 / 模态框 / 常量表 | 所有模块的公共底座 |
|
||||
| `markdown.js` | 轻量 Markdown 渲染(`renderMarkdown`) | 无 CDN 依赖;**先转义再套标记** |
|
||||
| `list.js` | 统一列表组件:搜索 + 分页 + 排序 | 状态注册表 `_LIST_PAGERS`;`setListPager` **不重置**页码/搜索/排序(避免轮询刷新打断用户) |
|
||||
| `monitor.js` | 监控页:设备表、批量操作、异常汇总、任务概况卡片(含覆盖设备 chip) | 5s 轮询 + 脏检查(签名不变不重渲染) |
|
||||
| `editor.js` | 步骤编辑器(拖拽 / 参数表单 / 条件分支 / 元素抓取 / 测试此步骤)+ `saveTask` | 最大的前端文件;`_stepEditor` 单例 |
|
||||
| `tasks.js` | 任务 Tab:任务 CRUD / 调度解析 / 运行窗口 + 自定义动作 | |
|
||||
| `tools.js` | 工具 Tab:剪贴板、adb 终端、Tailscale、设备池、自动发现、远程看屏 | |
|
||||
| `apps.js` | 应用管理:APK 上传/安装/删除、设备已装应用 | |
|
||||
| `admin.js` | 分组、日志、用户 + **全局初始化入口**(末尾 `initCsrf(); loadMe(); showTab('monitor')`) | |
|
||||
| `agent.js` | AI 控制台:会话、SSE 流、Markdown / 推理链 / token 渲染、实时画面、经验库 / 动作库 | |
|
||||
| `system.js` | 系统 Tab:备份导出 / 导入预览与应用 | |
|
||||
|
||||
> **token 采集的兼容性**:请求带 `stream_options: {"include_usage": true}`,按「每次模型调用」取末尾 chunk 的 `usage` 累加(多轮工具调用会多次累加)。个别网关不认该参数会直接 **HTTP 400** → `_UsageUnsupported` 捕获后**自动关掉并重试一次**(`self._include_usage=False`),不影响主流程。
|
||||
>
|
||||
> **推理链体积**:只保留前 `_REASONING_KEEP`=6000 字符落库(会话消息上限 60 条),避免历史无限膨胀。
|
||||
加载顺序见根 [README](../README.md);都是全局脚本(非 ES module),靠加载顺序保证依赖。
|
||||
|
||||
> **蒸馏健壮性(2026-09-10)**:经验/动作靠**模型蒸馏**落库。推理型模型会把 token 预算烧在 `reasoning` 上,导致 `content` 为空或被截断(`finish_reason=length`)→ 早期只读 `content`,经验/动作被**静默丢弃**("小红书·苏州饭店"案例)。现策略:
|
||||
> 1. 蒸馏调用**关闭推理**:`"thinking": {"type": "disabled"}`(该代理支持;实测关掉后 reasoning=0、正文正常,配方 3/3 合格)——这是关键修复;
|
||||
> 2. 配方用**纯文本问法**(不要放可照抄的占位示例,否则模型会原样当配方存下来)+ 质量门槛 `_recipe_ok`(过短/含省略号占位 → 丢弃并重试);
|
||||
> 3. 动作提炼用 JSON + **截断容忍**提取(`_loads_lenient` 逐对象抢救)+ 顶层 `{action,params}` 形状归一 + 输入/产出限量(≤10 步输入、≤3 动作×4 步);
|
||||
> 4. 两类失败都有日志(`经验提炼:` / `动作提炼:` 含样本),不再静默。
|
||||
### 6.4 实时通道
|
||||
|
||||
### 5.5 元素抓取模态框
|
||||
| 通道 | 场景 | 机制 |
|
||||
|------|------|------|
|
||||
| **轮询** | 设备状态(5s)、日志(3s,可关)、自动发现(10s)、截图(3s)、APK 安装(3s)、AI 运行状态(8s) | `setInterval`,切 Tab 时统一清理 |
|
||||
| **SSE** | AI 控制台一轮会话 | `EventSource /api/agent/stream`;`onerror` **刻意不结束运行**,靠自动重连 + 服务端事件队列补发 |
|
||||
| **MJPEG** | 实时看屏 | `img.src = /api/screen/stream?...`,浏览器原生长连接 |
|
||||
|
||||
独立的第二层模态框(`el-picker-overlay`,z-index 1100),不影响任务编辑窗口:
|
||||
1. 选择设备 → 2. 加载截图 + 元素树 → 3. 点击元素/边界框 → 4. 回填选择器
|
||||
5. **抓取时直接验证**(每条元素右侧两个按钮,不会与"点击回填"冲突):
|
||||
- 「▶ 点一下」:按元素 `bounds` 中心在设备上真点一次(`POST /api/screen/tap`,`snap=1` 自动吸附到可点元素),返回吸附结果并自动刷新截图——用于确认位置/是否可达;
|
||||
- 「✓ 测选择器」:用**将填入的选择器**真跑一次 click(`POST /api/steps/test`),返回 `命中/未找到/已执行`——用于确认回填的选择器在真实界面能命中(元素无有效选择器时不显示此按钮)。
|
||||
### 6.5 前端权限
|
||||
|
||||
三条并存:① `data-perm` 属性(`loadMe()` 时统一隐藏无权限元素);② `_can(perm)`(动态拼 HTML 时决定是否出按钮);③ 后端装饰器兜底(403)。
|
||||
|
||||
> 前端隐藏只是体验优化,**安全完全依赖后端**;`data-perm` 只在页面加载时求值一次,改权限后需刷新页面。
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键设计决策
|
||||
## 7. 关键设计决策
|
||||
|
||||
### 6.1 为什么用 SQLite 而不是 JSON 文件
|
||||
|
||||
- 支持用户/分组/任务的关系存储
|
||||
- 并发安全(WAL 模式)
|
||||
- 迁移旧 JSON 时归档为 `.migrated`,避免删空后重启又复原
|
||||
|
||||
### 6.2 为什么用 threading 而不是 asyncio
|
||||
|
||||
- uiautomator2 是同步阻塞库,不适合 asyncio
|
||||
- 多设备并发用多线程即可,每台设备一个 Worker 线程
|
||||
- Flask `threaded=True` 处理并发 HTTP 请求
|
||||
|
||||
### 6.3 为什么绝不 kill-server
|
||||
|
||||
`adb kill-server` / `adb disconnect` 会断开共享的 adb transport(历史与 STF provider 共享;STF 摘除后红线仍保留——多 worker、前台扫描、设备自动发现共用同一 adb server),影响所有运行中的任务。连接失败就返回 False,由调用方处理。
|
||||
|
||||
### 6.4 为什么状态查询带缓存
|
||||
|
||||
状态源 = 本地 SQLite 设备池 + adb + 内存 worker 状态。5 秒缓存避免每次 `/api/status` 都查库/adb 阻塞 Flask;Worker 实时状态读内存,不受缓存影响。
|
||||
|
||||
### 6.5 为什么 DeviceOfflineError 不重试
|
||||
|
||||
设备掉线后短时间内不会自愈,重试只会浪费配额并阻塞调度器。让设备进入冷却,可依赖设备自动发现(device_discovery 对正式池断联设备每轮 adb 重连)恢复后再启用。
|
||||
| 决策 | 为什么 | 代价 / 注意 |
|
||||
|------|--------|------------|
|
||||
| **SQLite + WAL** 而非 MySQL/PG | 单机部署、零运维;WAL 支持多线程读写 | 写并发有限;必须设 `busy_timeout`;`data/` 不入 git |
|
||||
| **threading 而非 asyncio** | uiautomator2 是同步阻塞库;每设备一线程模型直观 | 线程数随设备数增长;跨线程访问 DB 必须自推 app context |
|
||||
| **状态放内存** 而非落库 | 状态每秒都变,落库纯属浪费 | 重启丢失运行中状态(进程重启 = 任务终止) |
|
||||
| **蓝图按功能域拆包** | `web_server.py` 只做装配,路由就近维护 | 拆包易出现 import 遗漏 → 用 `scripts/regression_test.py` 兜底 |
|
||||
| **单页应用 + 全局脚本** | 无构建步骤、无 npm 依赖,改完强刷即生效 | 11 个文件共享全局作用域,需手工维护加载顺序 |
|
||||
| **任务类型注册表** | 新增任务类型不动调度器 | 删改类型要做兼容(未知类型必须显式报错,不能静默) |
|
||||
| **自建设备池**(不用 STF) | 规避共享 adb transport 的历史坑,清单可控 | 需自己处理在线状态、型号、自动发现 |
|
||||
| **备份"重启生效"** | Windows 无法替换被持有的 db;调度器/worker 持有内存态 | 导入后必须重启(在 `init_db` 之前消费恢复任务) |
|
||||
| **uiautodev 独立子进程** | 元素抓取是重活,隔离崩溃、可单独重启 | 固定端口 20242;PID 文件防重复拉起(杀之前必须校验 cmdline,防误杀同容器进程) |
|
||||
| **MCP 独立端口** | 外部 AI 用标准协议接入,不侵入 Web 会话 | MCP 端点自身无鉴权,靠网络隔离;写操作用开关门控 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 线程模型
|
||||
## 8. 技术红线与实现位置
|
||||
|
||||
```
|
||||
主线程(Flask)
|
||||
├── HTTP 请求处理(threaded=True,每请求一线程)
|
||||
├── TaskManager.scheduler(APScheduler,cron 触发任务计划)
|
||||
├── 经验巡检 BackgroundScheduler(03:47 Asia/Shanghai,web_server 装配)
|
||||
├── 设备自动发现线程(device_discovery._discovery_loop,默认 60s 一轮)
|
||||
├── 设备池型号采集后台线程(device_pool._refresh_models_bg,启动/手动触发)
|
||||
├── 设备池预连接线程(web_server._preconnect_pool_devices,重启后加速恢复)
|
||||
├── AI Agent 运行线程(agent_api._agent_thread,单实例 + SSE 推送)
|
||||
├── 看门狗线程(_Watchdog,30s 间隔)
|
||||
├── uiautodev 子进程(PID + cmdline 校验,防容器 PID 复用误杀)
|
||||
└── Worker 线程(每台设备一个)
|
||||
├── _run_with_retry 线程(重试循环)
|
||||
└── BaseWorker 线程(设备生命周期 + run_task)
|
||||
```
|
||||
| 红线 | 为什么 | 代码里的体现 |
|
||||
|------|--------|-------------|
|
||||
| **绝不 `adb kill-server`** | 会断掉所有设备的 adb transport,运行中任务全废 | `adb_helper` 只 connect 不 kill;Web 层硬拦截 `kill-server` / `disconnect` 字符串 |
|
||||
| **绝不对 `IP:5555` 设备 `disconnect`** | 该地址的 adb transport 是共享的 | `adb_disconnect` 全项目**零调用方**;`STFDevice.release()` 空实现 |
|
||||
| **空闲设备扫描不碰 adb** | 避免扰动共享连接 | 前台扫描对空闲设备直接返回"空闲";设备发现用 socket 探测而非 adb |
|
||||
| **adb key 不变** | 设备信任当前 key,换 key 全部变 unauthorized | 部署沿用既有 `~/.android/adbkey` |
|
||||
| **生产只读原则** | 220 是生产机 | 任何写操作(重启 / pull / 改文件)都需负责人确认 |
|
||||
| **备份覆盖清单** | 漏登记 = 等于没备份 | `core/system_backup.py` 的 `SUMMARY_TABLES`,导出/导入双向自检(见 [DATA_MODEL.md](DATA_MODEL.md) §6) |
|
||||
| **文档同步** | 文档落后会误导开发与运维 | 功能/配置/接口改动的同一个 commit 里更新 `doc/`(索引见 [doc/README.md](README.md)) |
|
||||
|
||||
> MCP server(127.0.0.1:8033)是**独立进程**(scripts/start.sh 拉起),不是 web_server 的线程。
|
||||
---
|
||||
|
||||
**线程安全**:
|
||||
- `_WORKERS_LOCK`:保护全局 worker 状态字典
|
||||
- `_ADB_LOCK`:串行化所有 adb 调用
|
||||
- `TaskManager._lock`:保护运行中任务字典
|
||||
- `_ForegroundScanner._cache_lock`:保护前台 App 缓存
|
||||
## 9. 已知问题与"坑"
|
||||
|
||||
### 9.1 代码缺陷(截至 2026-09-10,详见 [backlog/TODO.md](backlog/TODO.md))
|
||||
|
||||
| 问题 | 现象 | 位置 |
|
||||
|------|------|------|
|
||||
| `GET /locate` 返回 500 | 设备定位大字页打不开(NameError:`render_template_string` / `_esc` 未导入) | `web/monitor.py` |
|
||||
| `POST /api/device/locate`(`show=true`)失败 | 在设备浏览器打开定位页那步抛异常(`urllib` 未导入),被转成 502 | `web/monitor.py` |
|
||||
| CSRF 未实际启用 | 前端会取并携带 `X-CSRF-Token`,但服务端**没有注册** `before_request` 校验 | `web_server.py` 只 import 了 `_csrf_protect` |
|
||||
| 回归脚本在 Windows 不可用 | `scripts/regression_test.py` 用了 `signal.alarm`(Windows 无此 API),直接报错退出 | `scripts/regression_test.py` |
|
||||
| 前端 `--card-line` 未定义 | AI 控制台相关深色容器边框失效(该变量只在 wall.html 定义) | `templates/admin/monitor.html` 的 `:root` |
|
||||
| `countParts` 未定义 | 进度为 0 时可能抛 ReferenceError(被上层 try/catch 吞掉,用户无感) | `static/admin/monitor.js` |
|
||||
|
||||
### 9.2 代码里写明的坑(改代码前务必读)
|
||||
|
||||
| 坑 | 说明 |
|
||||
|----|------|
|
||||
| **跨线程访问 DB 必须自推 app context** | 后台线程用 `with app.app_context()`(各模块 `_ctx()` / `_db()` 已封装) |
|
||||
| **`debug=False` 不热重载** | 改 `core/`、`tasks/` 后必须重启;改 JS 需强刷浏览器 |
|
||||
| **Windows adb 输出不能用 `text=True`** | adb 输出含非 GBK 字节会崩;必须收 bytes 再解码 |
|
||||
| **`u2.connect` / `d.info` 可能永久 hang** | 必须放 `ThreadPoolExecutor` 里加超时 |
|
||||
| **Windows TCP 端口耗尽(WinError 10048)** | 属临时错误,重试需更长退避(代码识别后打 `[transient]`,退避 `max(delay,120)` 秒) |
|
||||
| **抢占时不能在锁内 `stop_device`** | 会死锁 |
|
||||
| **`preempted_job` 必须在重试循环外** | 否则被抢占任务永不归还 |
|
||||
| **删除任务/分组必须显式删行** | 只 upsert 的话,重启会从库里"复活" |
|
||||
| **旧 JSON 迁移后必须归档** | 否则用户删空数据后重启又复原 |
|
||||
| **XPath 位置谓词语义** | `//*[@id="x"][k]` = 父节点下第 k 个;`(//*[@id="x"])[k]` = 第 k 个匹配。抓取器生成后者,执行器会自动纠正历史写法 |
|
||||
| **容器重启后 PID 会被复用** | 杀残留 uiautodev 前必须校验 `/proc/<pid>/cmdline`,否则可能误杀同容器进程 |
|
||||
| **`.env` 用 `setdefault` 注入** | 真实环境变量优先于 `.env` |
|
||||
| **备份必须重启才生效** | 恢复任务在 `init_db` 之前被消费 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 扩展点(改哪里)
|
||||
|
||||
| 想做什么 | 改哪里 | 别忘了 |
|
||||
|---------|--------|--------|
|
||||
| 加一种步骤类型 | `tasks/generic/task.py` 的 `STEP_TYPES` + `_exec_<type>`;`static/admin/editor.js` 的 `STEP_LIB` | 两处保持一致;更新 [TASK_DEV.md](TASK_DEV.md) |
|
||||
| 加一个专属任务类型 | `tasks/<app>/`(照 `tasks/generic/` 结构)+ `tasks/__init__.py` 注册 | 重启生效;更新 [TASK_DEV.md](TASK_DEV.md) |
|
||||
| 加一个 HTTP 接口 | 对应功能域的 `web/xxx_api.py` | 加鉴权装饰器;更新 [API.md](API.md) |
|
||||
| 加一个蓝图 | 新建 `web/xxx_api.py` + 在 `web/__init__.py` 注册 | 更新 [API.md](API.md) 与本文 §1 |
|
||||
| 加一张表 | `core/models.py` 模型 + `SCHEMA_MIGRATIONS`(老库) | **登记进备份覆盖清单** + [DATA_MODEL.md](DATA_MODEL.md)(红线) |
|
||||
| 加一个常驻线程 | 参考 `device_discovery` 的 `init_app` / `shutdown` 模式 | 更新本文 §3 与 [DEVELOPMENT.md](DEVELOPMENT.md) |
|
||||
| 加一个前端模块 | `static/admin/<name>.js` + 在 `monitor.html` 按序引入 | 更新本文 §6 与 [DEVELOPMENT.md](DEVELOPMENT.md) |
|
||||
| 加一个 MCP 工具 | `mcp_server/mcp_server.py`(需要时加 `direct_ops.py`) | 写操作要挂门控三连;更新 [MCP.md](MCP.md) |
|
||||
| 加一个配置键 | `config.py`(程序级)或 `.env`(密钥类) | 更新 `.env.example` + [DEVELOPMENT.md](DEVELOPMENT.md) 配置速查 |
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
# 数据模型(DATA_MODEL)
|
||||
|
||||
> 适用读者:改后端 / 排数据问题的开发者与运维。
|
||||
> 相关文档:[ARCHITECTURE.md](ARCHITECTURE.md)(运行时架构)、[DEPLOY.md](DEPLOY.md) §数据备份(备份覆盖红线)、[API.md](API.md)(消费这些数据的接口)。
|
||||
> **表结构以 `core/models.py` 与各模块的建表 SQL 为准**,本文是它们的映射说明。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总览
|
||||
|
||||
| 项 | 值 |
|
||||
|----|----|
|
||||
| 引擎 | SQLite(单文件 `data/users.db`) |
|
||||
| driver | Flask-SQLAlchemy(SQLAlchemy 2.x) |
|
||||
| 连接 PRAGMA | `journal_mode=WAL`、`busy_timeout=5000`、`synchronous=NORMAL` |
|
||||
| 建表方式 | ① 模型表:`db.create_all()`;② 版本化迁移 `SCHEMA_MIGRATIONS`;③ 幂等原生 `CREATE TABLE IF NOT EXISTS` |
|
||||
| 主库文件 | `data/users.db`(运行时另有 `-wal` / `-shm`) |
|
||||
| 当前 schema 版本 | `app_meta.schema_version = 4` |
|
||||
|
||||
**表清单(12 张业务表 + 1 张 SQLite 内部表)**
|
||||
|
||||
| # | 表 | 来源 | 用途 |
|
||||
|---|---|------|------|
|
||||
| 1 | `user` | 模型 | 登录用户与权限 |
|
||||
| 2 | `device_group` | 模型 | 设备分组(JSON 存 serial 列表) |
|
||||
| 3 | `task_job` | 模型 | 任务计划 |
|
||||
| 4 | `custom_action` | 模型 | 自定义动作(可复用步骤包) |
|
||||
| 5 | `apk_file` | 模型 | APK 记录 |
|
||||
| 6 | `device` | 模型 + 迁移 v2/v3 | 设备池 |
|
||||
| 7 | `pending_device` | 模型 + 迁移 v4 | 待确认的发现设备 |
|
||||
| 8 | `app_meta` | 原生(迁移前建) | KV 配置(schema 版本、AI 配置、发现配置) |
|
||||
| 9 | `agent_conversation` | 原生(`web/agent_api.py`) | AI 控制台会话 |
|
||||
| 10 | `agent_experience` | 原生(`web/agent_api.py`) | 经验库(任务级配方) |
|
||||
| 11 | `experience_audit` | 原生(`web/agent_api.py`) | 经验巡检结论 |
|
||||
| 12 | `agent_action` | 原生(`web/agent_api.py`) | 动作库(命名动作) |
|
||||
| — | `sqlite_sequence` | SQLite 内部 | `AUTOINCREMENT` 的附带产物(备份自检时按 `sqlite_` 前缀排除) |
|
||||
|
||||
> **没有外键、没有关系(relationship)、没有索引**:全部靠应用层维护一致性。分组 ↔ 设备是多对多的 **JSON 列表**(`device_group.serials`),删除设备不会级联清理分组里的 serial。
|
||||
|
||||
---
|
||||
|
||||
## 2. 模型表(`core/models.py`)
|
||||
|
||||
### 2.1 `user` — 登录用户
|
||||
|
||||
| 列 | 类型 | 默认 | 说明 |
|
||||
|----|------|------|------|
|
||||
| `id` | Integer | — | 主键 |
|
||||
| `username` | String(80) | — | **唯一**,非空 |
|
||||
| `password_hash` | String(255) | — | werkzeug 加盐哈希;兼容旧裸 SHA-256(校验通过后自动升级) |
|
||||
| `is_admin` | Boolean | `True` | 管理员不受权限位限制 |
|
||||
| `perms` | Text | `"[]"` | JSON 数组:`tasks` / `devices` / `apks` / `logs` |
|
||||
|
||||
方法:`get_perms` / `set_perms` / `has_perm` / `set_password` / `check_password`。
|
||||
|
||||
### 2.2 `device_group` — 设备分组
|
||||
|
||||
| 列 | 类型 | 默认 | 说明 |
|
||||
|----|------|------|------|
|
||||
| `id` | Integer | — | 主键 |
|
||||
| `name` | String(80) | — | **唯一**,非空 |
|
||||
| `serials` | Text | `"[]"` | 组内设备 serial 的 JSON 数组 |
|
||||
| `description` | Text | `""` | 备注 |
|
||||
|
||||
### 2.3 `task_job` — 任务计划
|
||||
|
||||
| 列 | 类型 | 默认 | 说明 |
|
||||
|----|------|------|------|
|
||||
| `id` | String(32) | — | 主键,uuid 前 8 位 |
|
||||
| `name` | String(120) | — | 非空 |
|
||||
| `task_type` | String(60) | `"generic_steps"` | 必须已注册 |
|
||||
| `target` | Text | `'{"mode":"all"}'` | JSON |
|
||||
| `params` | Text | `"{}"` | JSON(与任务类默认值合并后使用) |
|
||||
| `schedule` | Text | `'{"mode":"once"}'` | JSON |
|
||||
| `retry` | Text | `'{"max_attempts":1,"delay":60}'` | JSON |
|
||||
| `enabled` | Boolean | `True` | 是否参与调度 |
|
||||
|
||||
### 2.4 `custom_action` — 自定义动作
|
||||
|
||||
| 列 | 类型 | 默认 | 说明 |
|
||||
|----|------|------|------|
|
||||
| `id` | String(32) | — | 主键 |
|
||||
| `name` | String(120) | — | 非空 |
|
||||
| `icon` | String(4) | `"📦"` | 展示图标 |
|
||||
| `steps` | Text | `"[]"` | JSON,schema 与 `generic_steps` 的 `params.steps` 一致 |
|
||||
| `created_at` | String(20) | `""` | 时间串 |
|
||||
|
||||
### 2.5 `apk_file` — APK 记录
|
||||
|
||||
| 列 | 类型 | 默认 | 说明 |
|
||||
|----|------|------|------|
|
||||
| `id` | String(32) | — | 主键;磁盘文件名为 `<id>.apk` |
|
||||
| `filename` | String(255) | — | 原始文件名 |
|
||||
| `display_name` / `package_name` / `version_name` | String | `""` | 解析结果 |
|
||||
| `version_code` / `size` | Integer | `0` | |
|
||||
| `upload_time` | String(20) | `""` | |
|
||||
|
||||
### 2.6 `device` — 设备池
|
||||
|
||||
| 列 | 类型 | 默认 | 说明 |
|
||||
|----|------|------|------|
|
||||
| `serial` | String(120) | — | 主键:`IP:5555` 或 USB 序列号 |
|
||||
| `name` | String(80) | `""` | 备注名 |
|
||||
| `model` | String(120) | `""` | 型号(迁移 v3 追加) |
|
||||
| `enabled` | Boolean | `True` | 停用则不参与调度 |
|
||||
| `note` | Text | `""` | |
|
||||
| `created_at` | String(20) | `""` | |
|
||||
|
||||
### 2.7 `pending_device` — 待确认的发现设备
|
||||
|
||||
| 列 | 类型 | 默认 | 说明 |
|
||||
|----|------|------|------|
|
||||
| `serial` | String(120) | — | 主键 |
|
||||
| `source` | String(20) | `""` | `lan` / `tailscale` |
|
||||
| `first_seen` / `last_seen` | String(20) | `""` | 时间串 |
|
||||
|
||||
> ⚠️ SQLAlchemy 模型的 `default=` 是 **Python 侧默认值**,SQLite 建表语句里没有 `DEFAULT` 子句;只有原生建表的表才有真正的 SQL DEFAULT。
|
||||
|
||||
---
|
||||
|
||||
## 3. 非模型表
|
||||
|
||||
### 3.1 `app_meta` — KV 配置
|
||||
|
||||
| 列 | 类型 |
|
||||
|----|------|
|
||||
| `key` | TEXT PRIMARY KEY |
|
||||
| `value` | TEXT |
|
||||
|
||||
由 `_migrate_schema()` 建表(启动必执行)。**所有键见 §5**。
|
||||
|
||||
### 3.2 AI 相关四张表(`web/agent_api.py`,原生建表)
|
||||
|
||||
| 表 | 列 |
|
||||
|----|----|
|
||||
| `agent_conversation` | `id`(PK) · `title` · `messages`(JSON) · `created_at` · `updated_at` |
|
||||
| `agent_experience` | `id`(PK AUTOINCREMENT) · `task_prompt` · `recipe` · `tool_seq` · `hits` · `created_at` |
|
||||
| `experience_audit` | `id`(PK) · `exp_id` · `verdict`(keep/delete) · `score`(REAL) · `reason` · `hits` · `action`(pending/kept/deleted) · `audited_at` |
|
||||
| `agent_action` | `id`(PK) · `name` · `app` · `aliases`(JSON) · `params`(JSON) · `steps`(JSON) · `preconditions` · `hits` · `source_prompt` · `created_at` · `updated_at` |
|
||||
|
||||
> ⚠️ 这四张表**不走 `SCHEMA_MIGRATIONS`,无版本管理**,由各模块首次使用时 `CREATE TABLE IF NOT EXISTS` 幂等创建;建表失败会被 `except: pass` 吞掉(后续 SQL 才会报错)。新增此类表时,务必同时登记进备份覆盖清单(§6)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 迁移机制
|
||||
|
||||
### 4.1 版本化迁移 `SCHEMA_MIGRATIONS`
|
||||
|
||||
| 版本 | 内容 | SQL 幂等性 |
|
||||
|------|------|-----------|
|
||||
| 1 | `user` 增加 `perms` | `ALTER TABLE`(非幂等,靠 duplicate column 兜底) |
|
||||
| 2 | 建 `device` 表(**无 `model` 列**) | 幂等(`IF NOT EXISTS`) |
|
||||
| 3 | `device` 增加 `model` | `ALTER TABLE`(同上兜底) |
|
||||
| 4 | 建 `pending_device` 表 | 幂等 |
|
||||
|
||||
执行器 `_migrate_schema()`:
|
||||
|
||||
1. 先 `CREATE TABLE IF NOT EXISTS app_meta(...)`(幂等)
|
||||
2. 读 `app_meta.schema_version`(缺省 0)
|
||||
3. 只执行 `version > current` 的迁移;**SQL 报 `duplicate column name` 时回滚该语句但仍记版本号**(自愈:`create_all` 已建列而版本未记录时不再卡住),其它异常才抛出
|
||||
4. 每条迁移后 `INSERT OR REPLACE app_meta('schema_version', v)` 并提交
|
||||
5. 整体包 try:失败只记 error 日志,**不阻塞启动**
|
||||
|
||||
### 4.2 旧 JSON 迁移(一次性)
|
||||
|
||||
启动时若存在 `data/groups.json` / `data/jobs.json`:对应表为空则导入,随后把文件重命名为 `<name>.json.migrated` 归档。**库非空但 JSON 仍在 → 直接归档**,防止"用户删空数据后重启又复原"。
|
||||
|
||||
`init_db(app)` 顺序:`db.init_app` → `create_all()` → `_migrate_schema()` → `_ensure_default_admin()`(首次创建 `admin/admin123`)→ `_migrate_old_json()`。
|
||||
|
||||
---
|
||||
|
||||
## 5. `app_meta` 键清单
|
||||
|
||||
| key | 用途 | 写入方 |
|
||||
|-----|------|--------|
|
||||
| `schema_version` | 迁移版本游标 | `core/models.py` |
|
||||
| `agent_api_base` | AI 接口地址 | AI 控制台配置页 |
|
||||
| `agent_model` | 模型名 | 同上 |
|
||||
| `agent_api_key` | API Key(**明文存库**) | 同上 |
|
||||
| `agent_default_serial` | 默认目标设备 | 同上 |
|
||||
| `agent_max_steps` | 最大步数(钳制 1-200,默认 40) | 同上 |
|
||||
| `discovery_enabled` | 自动发现开关(`"1"`/`"0"`) | 工具页「设备池管理」 |
|
||||
| `discovery_subnets` | 扫描网段 JSON 数组 | 同上 |
|
||||
| `discovery_interval` | 扫描周期秒(10-3600) | 同上 |
|
||||
| `discovery_port` | adb 探测端口(1-65535) | 同上 |
|
||||
|
||||
> ⚠️ `agent_api_key` 是**明文存储**,导出备份的 zip 里也含它——备份预览会固定给出"含敏感信息"告警。
|
||||
|
||||
---
|
||||
|
||||
## 6. 备份覆盖清单(红线)
|
||||
|
||||
`core/system_backup.py` 的 `SUMMARY_TABLES`(12 张,与库中实际业务表一一对应):
|
||||
|
||||
| 表 | 中文标签 |
|
||||
|----|---------|
|
||||
| `app_meta` | 系统配置(app_meta) |
|
||||
| `user` | 用户 |
|
||||
| `device_group` | 设备分组 |
|
||||
| `task_job` | 任务计划 |
|
||||
| `custom_action` | 自定义动作 |
|
||||
| `apk_file` | APK 记录 |
|
||||
| `device` | 设备池 |
|
||||
| `pending_device` | 待连接设备 |
|
||||
| `agent_conversation` | AI 会话 |
|
||||
| `agent_experience` | 经验库 |
|
||||
| `experience_audit` | 经验巡检 |
|
||||
| `agent_action` | 动作库 |
|
||||
|
||||
**双向自检**:
|
||||
|
||||
- **导出侧**:登记在 `SUMMARY_TABLES` 但快照里缺失 → 写入 `manifest.coverage_missing` + 日志告警
|
||||
- **导入侧**:备份里出现未登记的表(排除 `sqlite_` 前缀)→ 预览告警"请登记进 `core/system_backup.py` 的覆盖清单"(字段 `extra_tables`)
|
||||
- `REQUIRED_TABLES = (app_meta, user, task_job, device_group)`:缺任一直接拒绝导入
|
||||
|
||||
> **红线**:**新增任何持久化表(含原生建表的 agent 类表),必须同时登记进 `SUMMARY_TABLES` 与 `TABLE_LABELS`**,并更新 [DEPLOY.md](DEPLOY.md) §数据备份。历史教训:`agent_action` 曾漏登记,导致"动作库看起来没备份"(数据其实在快照里,只是清单没列)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据目录
|
||||
|
||||
| 路径 | 内容 | 进 git |
|
||||
|------|------|--------|
|
||||
| `data/users.db`(+`-wal`/`-shm`) | SQLite 主库 | 否 |
|
||||
| `data/apks/*.apk` | 上传的 APK | 否 |
|
||||
| `data/backups/` | 导出临时 zip、`pre_restore_*.db`(导入前安全网)、`restore_failed_*` | 否 |
|
||||
| `data/restore_staging/<token>/` | 导入暂存(TTL 1800s 自动清理) | 否 |
|
||||
| `data/restore_pending/` | 待生效恢复任务(重启时消费) | 否 |
|
||||
| `data/mcp_audit.log` | MCP 调用审计(路径由 `MCP_AUDIT_FILE` 指定) | 否 |
|
||||
| `data/uiauto.pid` | uiautodev 子进程 PID | 否 |
|
||||
| `data/*.json.migrated` | 旧 JSON 迁移归档 | 否 |
|
||||
| `logs/*.log` | 运行日志 | 否 |
|
||||
|
||||
> `data/` 与 `logs/` 全部是运行时产物,**任何文件都不入 git**。生产机上的库由「系统 → 数据备份导出/导入」或整目录手工备份。
|
||||
|
||||
---
|
||||
|
||||
## 8. 约定与注意事项
|
||||
|
||||
1. **主键用 uuid 前 8 位字符串**(`task_job` / `custom_action` / `apk_file` / `agent_conversation`):可读性好,理论上有碰撞概率(8 位 hex = 32 bit)。
|
||||
2. **JSON 字段一律 Text 存储**,读写通过 `get_*/set_*` 方法;解析失败回退默认值。
|
||||
3. **时间统一为字符串**(`"YYYY-MM-DD HH:MM"` 或 `"YYYY-MM-DD HH:MM:SS"`),排序依赖字符串序。
|
||||
4. **删除必须显式删行**(`delete_job` / `delete_group`):只 upsert 会导致重启后数据"复活"。
|
||||
5. **跨线程访问 DB 必须自推 app context**(后台线程里用 `with app.app_context()`)。
|
||||
6. **不要手工改库结构**:走 `SCHEMA_MIGRATIONS`(模型表)或幂等建表(原生表),否则 `create_all` 与版本号会不一致。
|
||||
7. **改表结构后记得**:① 更新本文;② 若是新表,登记备份覆盖清单(§6 红线)。
|
||||
+242
-324
@@ -1,369 +1,287 @@
|
||||
# 部署指南
|
||||
# 部署与运维(DEPLOY)
|
||||
|
||||
本文介绍如何从零部署 `platform-tools` 设备自动化后台。
|
||||
> 适用读者:部署与运维 `auto_control` 的人。
|
||||
> 相关文档:[DEVELOPMENT.md](DEVELOPMENT.md)(开发流程/红线)、[DATA_MODEL.md](DATA_MODEL.md) §数据目录、[ARCHITECTURE.md](ARCHITECTURE.md) §启动装配。
|
||||
|
||||
---
|
||||
|
||||
## 1. 环境准备
|
||||
|
||||
### 1.1 Python 环境
|
||||
### 1.1 主机与 Python
|
||||
|
||||
- **Python 3.10+**(推荐 3.12)
|
||||
- 安装后确认 `python --version` 和 `pip` 可用
|
||||
- Windows / Linux / macOS 均可;生产跑在 Linux 容器里
|
||||
- `pip install -r requirements.txt`
|
||||
|
||||
```bash
|
||||
# 验证
|
||||
python --version # 应输出 3.10+
|
||||
pip --version
|
||||
```
|
||||
项目自带的 **adb 二进制**在 `bin/adb/`(Windows 为 `adb.exe` + 依赖 dll;Linux/macOS 为 `adb`,需 `chmod +x`)。`config.py` 按平台自动选路径,换成自己的 adb 覆盖该目录即可。
|
||||
|
||||
### 1.2 设备池(已摘除 OpenSTF 依赖)
|
||||
|
||||
设备池由平台自身管理(SQLite `devices` 表 + 本机 adb 在线状态),**不再依赖 OpenSTF**。
|
||||
新增设备在管理后台「工具 → 设备池管理」添加(serial 形如 `100.100.10.x:5555`)。
|
||||
|
||||
USB 设备(serial 无冒号)插在部署机(220)时,经 220 的 adb server 驱动:
|
||||
- 220 的 adb 容器需为 **host 网络模式**(5037 已监听所有网卡,含 Tailscale)
|
||||
- 平台配置 `USB_ADB_HOST`(默认 `100.100.10.1`)/ `USB_ADB_PORT`(默认 5037)
|
||||
|
||||
### 1.3 adb 工具
|
||||
|
||||
项目自带 adb 二进制在 `bin/adb/` 目录:
|
||||
|
||||
- **Windows**:`bin/adb/adb.exe` + 依赖 dll(已包含)
|
||||
- **Linux**:`bin/adb/adb`(需 `chmod +x`)
|
||||
- **macOS**:`bin/adb/adb`(需 `chmod +x`)
|
||||
|
||||
如需替换为自己的 adb 版本,把对应平台的 adb 放进 `bin/adb/` 即可,`config.py` 会自动识别操作系统。
|
||||
|
||||
### 1.4 设备准备
|
||||
### 1.2 设备接入
|
||||
|
||||
设备需满足:
|
||||
- 开启 **USB 调试**(设置 → 开发者选项)
|
||||
- 转网络调试:USB 连上后 `adb -s <serial> tcpip 5555`(管理后台「adb 终端」有一键快捷命令)
|
||||
- 设备加入 Tailscale(同一账号,获得 100.100.10.x IP)
|
||||
- 在「工具 → 设备池管理」添加 `IP:5555`(自动尝试连接,任务运行时 u2 自动推送 atx-agent)
|
||||
|
||||
---
|
||||
1. 开启 **USB 调试**
|
||||
2. 转网络调试:USB 连上后 `adb -s <serial> tcpip 5555`(后台「工具 → adb 终端」有快捷命令)
|
||||
3. 与平台**网络互通**(生产用 Tailscale,设备与部署机在同一 tailnet,serial 形如 `100.100.10.x:5555`)
|
||||
4. 在「工具 → 设备池管理」加入设备池(**只连上 adb 不算入池,不参与调度**)
|
||||
|
||||
## 2. 安装部署
|
||||
**USB 设备**(serial 无冒号)经部署机的 adb server 驱动:
|
||||
|
||||
### 2.1 获取代码
|
||||
- adb 容器需 **host 网络模式**(5037 监听所有网卡,含 Tailscale)
|
||||
- 平台通过 `USB_ADB_HOST`(默认 `100.100.10.1`)/ `USB_ADB_PORT`(默认 5037)访问它
|
||||
|
||||
```bash
|
||||
# 方式一:直接拷贝项目目录
|
||||
# 方式二:解压打包文件(python scripts/pack.py 生成的 zip)
|
||||
```
|
||||
> **adb key 必须沿用既有 key**(设备信任它)。换 key 会让全部设备变 `unauthorized`。
|
||||
|
||||
### 2.2 安装依赖
|
||||
|
||||
```bash
|
||||
cd platform-tools
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**依赖清单**(`requirements.txt`):
|
||||
|
||||
| 包 | 版本 | 用途 |
|
||||
|----|------|------|
|
||||
| Flask | >=2.3,<4.0 | Web 框架 |
|
||||
| Flask-Login | >=0.6 | 用户认证 |
|
||||
| Flask-SQLAlchemy | >=3.0,<4.0 | SQLite ORM |
|
||||
| APScheduler | >=3.10,<4.0 | 定时调度 |
|
||||
| requests | >=2.28 | HTTP 客户端 |
|
||||
| uiautomator2 | >=3.0 | Android UI 自动化 |
|
||||
| uiautodev | >=0.14 | UI 元素抓取 |
|
||||
| pyaxmlparser | >=0.3.27 | APK 元信息解析 |
|
||||
| rapidocr_onnxruntime | >=1.4 | 屏幕 OCR(条件判断的 OCR识别 选择器,中英文模型随包内置,跨平台) |
|
||||
| fastmcp | >=2.0 | MCP Server 与 AI 控制台 Agent(Streamable HTTP 服务端/客户端) |
|
||||
| paramiko | >=3.0 | SSH 运维预留(历史 STF SSH 通道退役;配置 `STF_SSH_PASSWORD` 时走密码认证,未配置则退回系统 ssh 免密) |
|
||||
|
||||
> 服务器(无显示器/Linux)环境建议把 opencv-python 换成 `opencv-python-headless`(rapidocr 依赖 cv2,两者取一)。
|
||||
|
||||
> uiautomator2 首次连接设备时会自动推送 atx-agent 到设备,无需手动安装。
|
||||
|
||||
### 2.3 修改配置
|
||||
|
||||
**密钥类配置统一放项目根目录 `.env`**(已被 `.gitignore` 排除,不会提交到 git;
|
||||
`config.py` 不再内置任何密钥):
|
||||
|
||||
```ini
|
||||
# .env 示例
|
||||
WEB_SECRET_KEY=随机字符串(会话密钥,python -c "import secrets;print(secrets.token_hex(32))" 生成)
|
||||
TAILSCALE_API_KEY=你的Tailscale_API_key
|
||||
# USB_ADB_HOST=100.100.10.1 # 220 的 Tailscale IP(USB 设备远程 adb server,默认值已可用)
|
||||
```
|
||||
|
||||
其他配置按需调整:
|
||||
|
||||
| 配置项 | 默认值 | 何时修改 |
|
||||
|-------|--------|---------|
|
||||
| `WEB_HOST` | `0.0.0.0` | 仅本机访问改为 `127.0.0.1` |
|
||||
| `WEB_PORT` | `18050` | 端口冲突时修改 |
|
||||
| `ADB_PATH` | 自动识别 | 用自定义 adb 时修改 |
|
||||
| `USB_ADB_HOST` | `100.100.10.1` | USB 设备所在部署机(220)的 Tailscale IP |
|
||||
| `USB_ADB_PORT` | `5037` | 220 adb 容器监听端口(host 网络模式) |
|
||||
| `TAILSCALE_TAILNET` | 按邮箱前缀 | tailnet 名称/ID(个人账号一般为登录邮箱前缀) |
|
||||
| `DISCOVERY_PORT` | `5555` | 设备自动发现扫描的 adb 端口(网段默认局域网 + Tailscale,可在设备池面板改) |
|
||||
| `DISCOVERY_INTERVAL` | `60` | 自动发现扫描间隔(秒) |
|
||||
|
||||
> 配置来源区分:`WEB_HOST`/`WEB_PORT`/`ADB_PATH` 是 **config.py 常量**(直接改文件,不经 .env);
|
||||
> `TAILSCALE_API_KEY`/`TAILSCALE_TAILNET`/`USB_ADB_HOST`/`USB_ADB_PORT`/`DISCOVERY_PORT`/`DISCOVERY_INTERVAL` 由 config.py 以环境变量读取,**可在 .env 覆盖**;
|
||||
> `BACKUP_DIR`/`RESTORE_STAGING_DIR`/`RESTORE_PENDING_DIR` 是常量(硬编码到 `data/`,见 §3.5)。
|
||||
|
||||
> **未配置 `WEB_SECRET_KEY`**:启动时随机生成(每次重启登录态失效,生产务必配置固定值)。
|
||||
> **工具页 Tailscale 管理前置**:`.env` 写入 `TAILSCALE_API_KEY` 后重启服务;
|
||||
> 未配置时管理分区显示明确提示,不影响其他功能。
|
||||
> (历史遗留的 `STF_URL`/`STF_TOKEN`/`STF_SSH_*` 等配置:`config.py` 仍读取但无任何功能使用,仅历史保留;生产 `.env` 可留空。)
|
||||
|
||||
### 2.4 启动服务
|
||||
|
||||
**命令行启动**
|
||||
|
||||
```bash
|
||||
python web_server.py
|
||||
```
|
||||
|
||||
(推荐配合 `scripts/supervise.sh` 进程守护,见 3.1 节)
|
||||
|
||||
**生产容器(220)入口 `scripts/start.sh`**(docker-compose 的 python-app 服务 command):
|
||||
|
||||
1. **依赖就绪守卫**:flask/u2/uiautodev/rapidocr/cv2/fastmcp 全可用则跳过安装;否则 `pip install -r requirements.txt` 并做 cv2 环境修复(卸 GUI opencv → 装 headless)
|
||||
2. **后台拉起 MCP server**:`MCP_ENABLED` 默认 `1` 时执行 `python3 -m mcp_server.mcp_server`(`MCP_ALLOW_WRITE=1`、`MCP_PLATFORM_USER` 默认 admin、`MCP_PLATFORM_PASS` 兜底 `admin123`),日志 `/tmp/mcp_server.log`
|
||||
3. **前台启动主服务**:`exec python -u web_server.py`
|
||||
|
||||
注意:
|
||||
|
||||
- `scripts/supervise.sh` **只守护 web_server,不拉起 MCP**——容器场景 MCP 由 start.sh 拉起,不要用 supervise.sh 替代容器入口
|
||||
- **AI 控制台依赖 MCP**:`AGENT_MCP_URL` 默认 `http://127.0.0.1:8033/mcp`(`mcp_agent/config.py`)
|
||||
- 手动起 web_server 需另启 MCP:`MCP_ALLOW_WRITE=1 python -m mcp_server.mcp_server`
|
||||
- **改过 admin 密码务必同步 `MCP_PLATFORM_PASS`**(start.sh 兜底 `admin123` 会登录失败)
|
||||
- 发布链路:dev 开发测试完成 → 负责人确认合并 main → 220 `git pull` → 重启 python-app 容器生效(详见 DEVELOPMENT §6)
|
||||
|
||||
### 2.5 验证部署
|
||||
|
||||
1. 控制台看到 `启动服务: http://localhost:18050/` 即成功
|
||||
2. 浏览器访问 `http://localhost:18050/`
|
||||
3. 用 `admin/admin123` 登录
|
||||
4. 监控页应显示设备池中的设备(本机 adb 在线 + SQLite 配置清单,serial 形如 `100.100.10.x:5555`)
|
||||
|
||||
---
|
||||
|
||||
## 3. 生产部署建议
|
||||
|
||||
### 3.1 进程守护
|
||||
|
||||
用进程守护工具确保服务自动重启:
|
||||
|
||||
**Windows(NSSM)**:
|
||||
```bat
|
||||
nssm install platform-tools "C:\Python312\python.exe" "D:\platform-tools\web_server.py"
|
||||
nssm start platform-tools
|
||||
```
|
||||
|
||||
**Linux(systemd)**:
|
||||
```ini
|
||||
# /etc/systemd/system/platform-tools.service
|
||||
[Unit]
|
||||
Description=Platform Tools Web Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www
|
||||
WorkingDirectory=/opt/platform-tools
|
||||
ExecStart=/usr/bin/python3 web_server.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
**通用脚本(Linux/macOS,无需 systemd)**:
|
||||
|
||||
项目自带 `scripts/supervise.sh`:web_server 崩溃自动重启、带退避和重启上限(防崩溃死循环)。
|
||||
|
||||
```bash
|
||||
# 用 venv 的 python 守护
|
||||
PYTHON=./.venv/bin/python bash scripts/supervise.sh
|
||||
```
|
||||
|
||||
> 进程守护只保证服务重启,不恢复已运行的任务(worker 状态在内存)。
|
||||
>
|
||||
> **崩溃后占用自愈(需人工核实)**:历史 `AUTO_RELEASE_STALE_OCCUPY` 机制已随 STF 摘除失效(全库代码不再读取该配置)。设备池的占用互斥仍在(任务 running/connecting 的设备会被锁),但崩溃后残留占用如何释放、是否自动清理,需按当前实现核实后补写本节。
|
||||
|
||||
**Docker(生产 python-app 容器)**:
|
||||
|
||||
docker-compose 已配置 `restart: unless-stopped`,web_server 进程退出 → 容器退出 → Docker 自动重启。建议再加健康检查,让编排感知服务存活:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yaml 的 python-app 服务下添加
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:18050/api/health', timeout=3)"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
```
|
||||
|
||||
### 3.2 反向代理(可选)
|
||||
|
||||
如需 HTTPS 或 80 端口,用 Nginx 反向代理:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name auto.example.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:18050;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 安全加固
|
||||
|
||||
- **修改默认密码**:登录后立即在"用户"Tab 修改 admin 密码
|
||||
- **最小权限分配**:需要多人使用后台时,在"用户"Tab 创建普通用户并只勾选必要权限
|
||||
(任务管理/设备控制/应用管理/日志查看),不要把 admin 密码共享出去
|
||||
- **会话密钥 SECRET_KEY**:在 `.env` 写固定 `WEB_SECRET_KEY`(`python -c "import secrets;print(secrets.token_hex(32))"` 生成);未配置时启动随机生成(每次重启登录态失效,生产务必配置固定值)
|
||||
- **限制访问**:生产环境把 `WEB_HOST` 改为 `127.0.0.1`,配合反向代理
|
||||
- **防火墙**:只开放必要端口
|
||||
|
||||
### 3.4 日志管理
|
||||
|
||||
- 日志自动滚动(10MB 一份,保留 5 份)
|
||||
- 日志目录 `logs/`,可在"日志"Tab 在线查看
|
||||
- 长期运行建议定期清理或配置 logrotate
|
||||
|
||||
### 3.5 数据备份
|
||||
|
||||
**优先推荐平台功能「系统 → 数据备份导出/导入」(仅 admin)**:
|
||||
|
||||
- **导出**:`POST /api/system/backup/export` → 用 sqlite 在线备份 API 对 `data/users.db` 做一致快照,打包 zip(`users.db` + `manifest.json` + 可选 `apks/*.apk`)
|
||||
- **导入**:上传 zip/`.db` → 校验预览(完整性/必需表/schema 版本告警)→ 确认后自动把当前库快照到 `data/backups/pre_restore_*.db`(安全网可回滚)→ 落 `data/restore_pending/` → **重启 web_server 生效**(web_server 在 `init_db` 前自动消费恢复任务)
|
||||
|
||||
**备份覆盖清单(红线)**:覆盖清单 = `core/system_backup.py` 的 `SUMMARY_TABLES`,当前包含
|
||||
`app_meta`(系统配置) / `user` / `device_group` / `task_job` / `custom_action` / `apk_file` / `device` /
|
||||
`pending_device` / `agent_conversation` / `agent_experience` / `experience_audit` / `agent_action`(动作库)。
|
||||
|
||||
> **新增任何持久化表,必须同步登记进该清单**——否则导出清单/预览里看不到它,会被误判为"没有备份"
|
||||
> (2026-09-10 动作库 `agent_action` 即因此被误判:数据其实在 `users.db` 快照里,只是清单漏列)。
|
||||
> 导出侧有**覆盖自检**(登记表若缺失 → `manifest.coverage_missing` + 日志告警);导入侧有**反向自检**
|
||||
> (备份含未登记表 → 预览告警提示登记)。
|
||||
|
||||
目录与常量:
|
||||
|
||||
| 目录 | 用途 |
|
||||
|---|---|
|
||||
| `data/backups/` | 导出临时 zip + 恢复前快照 `pre_restore_*.db` + 校验失败的 `restore_failed_*` |
|
||||
| `data/restore_staging/` | 导入暂存,TTL 30 分钟未应用自动清理 |
|
||||
| `data/restore_pending/` | 待生效恢复任务(重启时消费) |
|
||||
|
||||
常量 `BACKUP_DIR` / `RESTORE_STAGING_DIR` / `RESTORE_PENDING_DIR`(config.py L63-68,非 .env,已 .gitignore)。
|
||||
|
||||
手工备份 `data/` 目录仍可作兜底,但**整库恢复建议走上述功能**(在线一致快照 + 预恢复备份 + 重启原子生效,避免手工替换被 WAL/占用文件破坏)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 网络配置
|
||||
|
||||
### 4.1 端口说明
|
||||
### 1.3 端口
|
||||
|
||||
| 端口 | 服务 | 说明 |
|
||||
|------|------|------|
|
||||
| 18050 | Web 后台 | 主服务端口(config.py 可改) |
|
||||
| 20242 | uiautodev | 元素抓取服务(自动启动,固定端口) |
|
||||
| 8033 | MCP | MCP 手机控制 Server(scripts/start.sh 自动拉起,`MCP_ENABLED=0` 可关) |
|
||||
| 5555 | adb | 设备 adb 网络端口(设备端) |
|
||||
|
||||
### 4.2 Windows 端口问题(仅 Windows)
|
||||
|
||||
Windows 可能将某些端口范围划为动态排除范围,导致绑定失败(WinError 10013)。
|
||||
|
||||
```bash
|
||||
# 查看排除的端口范围
|
||||
netsh interface ipv4 show excludedportrange protocol=tcp
|
||||
```
|
||||
|
||||
如果 18050 在排除范围内,修改 `config.py` 的 `WEB_PORT` 到一个不在排除范围内的端口。
|
||||
|
||||
### 4.3 局域网访问
|
||||
|
||||
- `WEB_HOST = "0.0.0.0"` 允许局域网访问
|
||||
- 需要添加防火墙入站规则(Windows 手动添加,或参考 `netsh advfirewall firewall add rule`)
|
||||
- 局域网其他机器访问 `http://部署机IP:18050/`
|
||||
| 18050 | Web 后台 | `config.py` 的 `WEB_PORT`;绑定失败会自动回退候选端口 |
|
||||
| 8033 | MCP Server | `MCP_HTTP_PORT`;由 `scripts/start.sh` 拉起 |
|
||||
| 20242 | uiautodev | 元素抓取;`web_server.py` 启动时自动 Popen |
|
||||
| 5555 | 设备 adb | 设备侧端口 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 更新升级
|
||||
## 2. 安装与启动
|
||||
|
||||
### 5.1 代码更新
|
||||
### 2.1 获取代码与依赖
|
||||
|
||||
```bash
|
||||
# 1. 停止服务
|
||||
# 2. 替换代码文件(或解压新的 zip)
|
||||
# 3. 重新安装依赖(如有新增)
|
||||
cd auto_control
|
||||
pip install -r requirements.txt
|
||||
# 4. 启动服务
|
||||
```
|
||||
|
||||
主要依赖:Flask / Flask-Login / Flask-SQLAlchemy / APScheduler / uiautomator2 / uiautodev / rapidocr_onnxruntime / **opencv-python-headless** / pyaxmlparser / fastmcp / paramiko。
|
||||
|
||||
> ⚠️ **服务器与容器环境必须用 `opencv-python-headless`**:GUI 版 `opencv-python` 依赖 X11 库,`python:slim` 容器里 `import cv2` 直接崩 → OCR 步骤抛异常 → 任务失败退出。版本锁 `<5`(5.x wheel 没有 cv2 模块)。`scripts/start.sh` 会自动做这个替换。
|
||||
|
||||
### 2.2 配置
|
||||
|
||||
**密钥类配置统一放项目根 `.env`**(不入 git,模板见 `.env.example`)。加载方式:逐行解析 + `os.environ.setdefault`(**真实环境变量优先**)。
|
||||
|
||||
生产至少配:
|
||||
|
||||
```ini
|
||||
WEB_SECRET_KEY=<随机 64 hex> # 不配则每次重启登录态失效
|
||||
# USB_ADB_HOST=100.100.10.1 # USB 设备所在的部署机(默认值通常可用)
|
||||
# TAILSCALE_API_KEY=<...> # 需要 Tailscale 管理功能时
|
||||
# MCP_PLATFORM_PASS=<admin 的密码> # 改过 admin 密码必须同步,否则 MCP 登录失败
|
||||
```
|
||||
|
||||
其余配置项与默认值见 [DEVELOPMENT.md](DEVELOPMENT.md) §配置速查。
|
||||
|
||||
### 2.3 启动
|
||||
|
||||
```bash
|
||||
python web_server.py
|
||||
```
|
||||
|
||||
### 5.2 数据库迁移
|
||||
启动成功日志:
|
||||
|
||||
- SQLite 表结构变化时,`init_db()` 会自动 `db.create_all()` 创建新表
|
||||
- 旧 `groups.json` / `jobs.json` 首次启动自动迁移到 SQLite
|
||||
- 迁移后 JSON 文件归档为 `.migrated`(保留备份,不再迁移)
|
||||
```
|
||||
[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/
|
||||
```
|
||||
|
||||
### 5.3 注意事项
|
||||
|
||||
- **修改 `core/` 目录下的文件后必须重启 web_server**(`debug=False` 不热重载)
|
||||
- **修改 `templates/` 下的 HTML 文件**:Flask 模板默认不缓存,但建议重启确保生效
|
||||
- **修改 `tasks/` 下的文件后必须重启**(任务注册在启动时完成)
|
||||
默认账号 **admin / admin123**(**登录后立即改密**)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 故障排查
|
||||
## 3. 生产部署(220 容器)
|
||||
|
||||
### 6.1 启动失败
|
||||
生产环境固定为 **192.168.20.220** 的 `/mnt/data/openstf/auto_control`,由 `docker-compose` 的 **`python-app`** 容器运行:
|
||||
|
||||
| 现象 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| `ModuleNotFoundError: No module named 'flask'` | 依赖未安装 | `pip install -r requirements.txt` |
|
||||
| `WinError 10013` | 端口被排除/权限不足 | 改端口或用管理员运行 |
|
||||
| `WinError 10048` | 端口被占用 | 改端口或杀占用进程 |
|
||||
| 监控页设备列表为空 | 设备不在池中 / adb 连不上 | 「工具 → 设备池管理」确认已添加且在线,检查设备网络与 5555 端口 |
|
||||
| AI 控制台报「MCP server(8033) 不可达」 | MCP Server 未启动/不可达 | 启动 MCP Server:本机 `MCP_ALLOW_WRITE=1 MCP_PLATFORM_USER=admin MCP_PLATFORM_PASS=<密码> MCP_AUDIT_FILE=data/mcp_audit.log python -m mcp_server.mcp_server`;220 容器由 `scripts/start.sh` 自动拉起(`MCP_ENABLED=0` 会关) |
|
||||
| 项 | 值 |
|
||||
|----|----|
|
||||
| 宿主目录 | `/mnt/data/openstf/auto_control`(git 仓库,分支 `main`) |
|
||||
| 容器挂载 | 宿主目录 → 容器 `/app` |
|
||||
| 网络 | `network_mode: host` |
|
||||
| 入口 | `scripts/start.sh`(`command`) |
|
||||
| 重启策略 | `restart: unless-stopped` |
|
||||
| 同级容器 | `adb`(USB 远程 adb server,5037)、以及该机器上其它无关服务 |
|
||||
|
||||
### 6.2 设备连接失败
|
||||
> ⚠️ **`docker-compose.yaml` 不在仓库里**,只存在于 220 上;仓库里只有 `scripts/start.sh` 这一半。
|
||||
|
||||
| 现象 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| `DeviceOfflineError` | u2 连接超时 / atx-agent 无响应 | 检查设备网络,重启设备或重新推送 atx-agent |
|
||||
| `u2.connect 超时` | atx-agent 无响应 | 重启设备/重新推送 atx-agent |
|
||||
| `adb connect failed` | 设备网络不通/端口未开放 | 检查设备 IP 和 5555 端口 |
|
||||
| 设备显示离线 | 状态缓存/误报 | 管理后台设备池「重连」,或「扫描前台 App」复测 |
|
||||
### 3.1 容器入口 `scripts/start.sh` 做三件事
|
||||
|
||||
### 6.3 任务不执行
|
||||
1. **依赖就绪守卫**:检查 9 个模块 + `cv2` 可用性;全部可用就跳过安装
|
||||
2. **依赖安装与修复**(仅在需要时):`pip install -r requirements.txt` → 卸载 GUI `opencv-python` → 装 `opencv-python-headless>=4.8,<5` → 再验 cv2,仍异常则 `--force-reinstall`
|
||||
3. **拉起 MCP + 前台启动 Web**:
|
||||
|
||||
| 现象 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| 任务列表有任务但不执行 | 任务未启用 / cron 未到点 | 检查 `enabled` 和 `schedule` |
|
||||
| 立即执行无反应 | 无可用设备 | 检查设备池是否有空闲设备 |
|
||||
| worker 状态 error | 查看日志的 `last_error` | 查看 `logs/core.log` 和 `logs/task.log` |
|
||||
| 看门狗误杀 | 长操作未心跳 | 在长循环内加 `self.heartbeat()` |
|
||||
```
|
||||
MCP_ALLOW_WRITE=1 # 脚本内强制开启写操作
|
||||
MCP_PLATFORM_USER=${…:-admin}
|
||||
MCP_PLATFORM_PASS=${…:-admin123} # ← 改过 admin 密码必须覆盖它
|
||||
MCP_AUDIT_FILE=${…:-/tmp/mcp_audit.log}
|
||||
python3 -m mcp_server.mcp_server > /tmp/mcp_server.log 2>&1 &
|
||||
exec python -u web_server.py
|
||||
```
|
||||
|
||||
### 6.4 日志查看
|
||||
> **`scripts/supervise.sh` 只守护 web_server,不拉起 MCP**——容器场景不要用它替代 `start.sh`。
|
||||
|
||||
### 3.2 发布流程
|
||||
|
||||
```bash
|
||||
# 查看核心日志
|
||||
# 方式一:Web 后台"日志"Tab
|
||||
# 方式二:直接看文件
|
||||
# logs/core.log — adb/worker/task_manager
|
||||
# logs/task.log — 任务执行
|
||||
# logs/web.log — Web 请求
|
||||
# logs/action.log — 操作执行
|
||||
cd /mnt/data/openstf/auto_control
|
||||
git fetch origin && git checkout main && git pull --ff-only origin main
|
||||
docker restart python-app
|
||||
```
|
||||
|
||||
重启后验证:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:18050/api/health # {"ok":true,"status":"up",...}
|
||||
ss -ltnp | grep -E ':(18050|8033|20242)' # 三个端口都在
|
||||
docker exec python-app tail -5 /tmp/mcp_server.log # MCP 已启动
|
||||
tail -20 logs/web.log # 无 ERROR/Traceback
|
||||
```
|
||||
|
||||
**发布前检查清单**:
|
||||
|
||||
- [ ] 本机 `dev` 已合并 `main` 并推送(且经负责人确认)
|
||||
- [ ] 新增依赖已进 `requirements.txt`
|
||||
- [ ] **新增持久化表已登记进备份覆盖清单**(`core/system_backup.py`,红线)
|
||||
- [ ] 文档已同步(`doc/`)
|
||||
- [ ] 若改过 admin 密码 → 同步容器环境变量 `MCP_PLATFORM_PASS`
|
||||
- [ ] 启动日志里没有"任务类型已不存在"告警(有则说明库里有历史遗留任务,需人工处理)
|
||||
|
||||
> ⚠️ **重启会终止正在运行的任务**(worker 状态在内存)。选低峰期,或先在监控页「停止全部」。
|
||||
|
||||
---
|
||||
|
||||
## 4. 进程守护(非容器场景)
|
||||
|
||||
| 方式 | 用法 | 说明 |
|
||||
|------|------|------|
|
||||
| `scripts/supervise.sh` | `PYTHON=./.venv/bin/python bash scripts/supervise.sh` | 崩溃自动重启;300s 内最多重启 10 次;**不拉起 MCP** |
|
||||
| systemd | `ExecStart=/usr/bin/python3 web_server.py` + `Restart=always` | Linux 通用 |
|
||||
| NSSM | `nssm install auto_control <python> web_server.py` | Windows |
|
||||
| Docker | `restart: unless-stopped` | 生产用法 |
|
||||
|
||||
> 进程守护只保证**服务重启**,不恢复已运行的任务。生产建议加健康检查打 `/api/health`(`interval 30s` / `timeout 5s` / `retries 3` / `start_period 10s`)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据备份(重要)
|
||||
|
||||
### 5.1 平台内置(推荐)
|
||||
|
||||
**「系统 → 数据备份 / 导入恢复」**(仅管理员):
|
||||
|
||||
- **导出**:`POST /api/system/backup/export` → sqlite 在线备份 API 做一致快照 → 打包 zip(`users.db` + `manifest.json` + 可选 `apks/*.apk`)
|
||||
- **导入**:上传 zip/`.db` → 校验预览(完整性 / 必需表 / schema 版本 / 未登记表告警)→ 确认后自动把当前库快照到 `data/backups/pre_restore_*.db`(安全网)→ 落 `data/restore_pending/` → **重启服务生效**
|
||||
|
||||
### 5.2 备份覆盖清单(红线)
|
||||
|
||||
覆盖清单 = `core/system_backup.py` 的 `SUMMARY_TABLES`,当前 12 张表:`app_meta` / `user` / `device_group` / `task_job` / `custom_action` / `apk_file` / `device` / `pending_device` / `agent_conversation` / `agent_experience` / `experience_audit` / `agent_action`。完整说明见 [DATA_MODEL.md](DATA_MODEL.md) §6。
|
||||
|
||||
> **新增任何持久化表,必须同步登记进该清单**——否则导出预览里看不到它,会被误判为"没有备份"(2026-09-10 动作库 `agent_action` 就踩过:数据其实在快照里,只是清单漏列)。
|
||||
> 导出侧有**覆盖自检**(登记表缺失 → `manifest.coverage_missing` + 日志告警);导入侧有**反向自检**(备份含未登记表 → 预览告警)。
|
||||
|
||||
### 5.3 目录与手工备份
|
||||
|
||||
| 目录 | 用途 |
|
||||
|------|------|
|
||||
| `data/backups/` | 导出临时 zip、`pre_restore_*.db`、`restore_failed_*` |
|
||||
| `data/restore_staging/` | 导入暂存(TTL 30 分钟自动清理) |
|
||||
| `data/restore_pending/` | 待生效恢复任务(重启时消费) |
|
||||
|
||||
手工整目录备份 `data/` 仍可作兜底,但**整库恢复建议走内置功能**(在线一致快照 + 预恢复备份 + 重启原子生效,避免手工替换被 WAL/占用文件破坏)。
|
||||
|
||||
> ⚠️ 备份 zip 含**用户口令哈希与 AI 控制台 API Key(明文)**,注意保管与传输。
|
||||
|
||||
---
|
||||
|
||||
## 6. 安全加固
|
||||
|
||||
- **改默认密码**:登录后立即改 admin 密码;需要多人使用时在「用户」页建普通用户并只勾必要权限位
|
||||
- **固定会话密钥**:`.env` 写 `WEB_SECRET_KEY`(`python -c "import secrets;print(secrets.token_hex(32))"`)
|
||||
- **限制访问面**:`WEB_HOST` 改 `127.0.0.1` + Nginx 反代,或靠防火墙只放必要端口
|
||||
- **MCP 端点(8033)自身无鉴权**:它只做**出站**登录平台,不对入站做校验 → **必须靠网络隔离**(同机/内网),不要直接暴露公网
|
||||
- **写操作门控**:`MCP_ALLOW_WRITE=0`(默认)时 MCP 只读
|
||||
- **HTTPS/80 端口**:用 Nginx 反代
|
||||
|
||||
```nginx
|
||||
location / { proxy_pass http://127.0.0.1:18050; proxy_set_header Host $host; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 升级与迁移
|
||||
|
||||
### 7.1 代码升级
|
||||
|
||||
```bash
|
||||
git pull --ff-only origin main
|
||||
docker restart python-app # 容器场景
|
||||
# 或 systemd: systemctl restart auto_control
|
||||
```
|
||||
|
||||
- **改 `core/`、`tasks/`、`templates/` 后必须重启**(`debug=False` 不热重载)
|
||||
- 改前端 JS 后浏览器需**强刷**(Ctrl+Shift+R)
|
||||
|
||||
### 7.2 数据库迁移
|
||||
|
||||
- 表结构变化由 `init_db()` 自动处理:`create_all()` 建新表 + `SCHEMA_MIGRATIONS` 补列(幂等,失败不阻塞启动)
|
||||
- 旧 `groups.json` / `jobs.json` 首次启动自动迁移并归档为 `.migrated`
|
||||
- **跨版本恢复**:用「导入恢复」上传旧库 → 预览会提示 schema 版本差异 → 应用后重启(新版本会自动补迁移)
|
||||
|
||||
### 7.3 跨机迁移(换部署机)
|
||||
|
||||
1. 新机装依赖、放好 `bin/adb/` 与 **同一把 adb key**
|
||||
2. 复制 `.env`
|
||||
3. 老机「系统 → 数据备份」导出 zip → 新机「导入恢复」上传并应用
|
||||
4. **重启新机服务**(恢复任务在 `init_db` 之前被消费)
|
||||
5. 核对设备池在线状态与任务列表
|
||||
|
||||
---
|
||||
|
||||
## 8. 故障排查
|
||||
|
||||
### 8.1 启动类
|
||||
|
||||
| 现象 | 原因 / 处理 |
|
||||
|------|------------|
|
||||
| `ModuleNotFoundError: No module named 'flask'` | 依赖未装:`pip install -r requirements.txt` |
|
||||
| `WinError 10013`(Windows) | 端口被排除/权限不足:`netsh interface ipv4 show excludedportrange protocol=tcp` 查看,或改 `WEB_PORT` |
|
||||
| `WinError 10048` | 端口被占用:换端口或杀占用进程 |
|
||||
| 启动日志只有一半 | 以 WSGI 方式 import 了模块(只跑了 import 期的装配,没跑 `__main__` 分支);直接 `python web_server.py` |
|
||||
| 登录后立刻掉线 | 没配 `WEB_SECRET_KEY`(每次重启换密钥) |
|
||||
| 容器里 `import cv2` 崩 | 装成了 GUI 版 opencv:换 `opencv-python-headless`(`start.sh` 会自动修) |
|
||||
|
||||
### 8.2 设备类
|
||||
|
||||
| 现象 | 处理 |
|
||||
|------|------|
|
||||
| 设备显示离线 | 「设备池管理 → 一键重连」;确认设备在线、网络互通(生产:同 tailnet) |
|
||||
| 加了设备但不被调度 | 只连上 adb 不够,必须在**设备池**中且 `enabled=true` |
|
||||
| `u2.connect 超时` | atx-agent 无响应:重启设备或重新推送 atx-agent(基类有 30s 超时保护) |
|
||||
| 大量设备同时连接时超时 | Windows 端口耗尽(`WinError 10048`):代码会打 `[transient]` 并退避 120s 重试;减少并发或调整系统 TIME_WAIT |
|
||||
| 设备 `unauthorized` | adb key 变了:恢复原 key |
|
||||
|
||||
### 8.3 任务类
|
||||
|
||||
| 现象 | 处理 |
|
||||
|------|------|
|
||||
| 任务不执行 | 检查 `enabled`、`schedule`、是否在运行窗口内、目标设备是否在线 |
|
||||
| 立即执行无反应 | 无可用设备(`resolve_serials` 为空);看 `logs/core.log` |
|
||||
| 任务"成功"但没做事 | 步骤类型未知/缺必填参数会被**告警跳过**;查 `logs/task.log` 的 WARNING |
|
||||
| 执行报"任务类型 xxx 已不存在" | 库里残留了已删除类型的任务(如历史 `douyin_nurture`):在「任务」页删除或改用现有类型。启动日志也会点名列出 |
|
||||
| 长任务被看门狗杀 | 120s 无心跳:业务循环里要周期性 `self.heartbeat()` |
|
||||
| 任务卡在 running | 监控页「停止选中」;必要时重启服务(重启会清空内存状态) |
|
||||
|
||||
### 8.4 其它
|
||||
|
||||
| 现象 | 处理 |
|
||||
|------|------|
|
||||
| 元素抓取按钮不可用 | uiautodev(:20242)没起来;`web_server.py` 启动时自动拉起,手动可 `python -m uiautodev server --no-browser` |
|
||||
| 抓元素超时 | 部分设备 dump 慢(~18s)超过平台超时(8s),见 [backlog/TODO.md](backlog/TODO.md) |
|
||||
| AI 控制台报"MCP server(8033) 不可达" | MCP 没启动:容器由 `start.sh` 拉起,本机手动 `MCP_ALLOW_WRITE=1 python -m mcp_server.mcp_server` |
|
||||
| AI 控制台报 401 | 模型 API Key 无效/过期:在「AI 控制台 ⚙ 配置」重填 |
|
||||
| 备份导入后没变化 | **必须重启服务**,恢复任务在重启时才被消费 |
|
||||
|
||||
日志位置:`logs/core.log`(adb/调度)、`logs/task.log`(任务执行)、`logs/web.log`(Web/AI)、`logs/action.log`;容器内 MCP 日志 `/tmp/mcp_server.log`。
|
||||
|
||||
+166
-221
@@ -1,291 +1,236 @@
|
||||
# 开发手册(DEVELOPMENT)
|
||||
|
||||
面向本项目开发者:开发流程、git 工作流、环境说明、技术红线、本地开发、常见开发任务。
|
||||
> 适用读者:所有参与 `auto_control` 开发的人。**动手前先读 §2 技术红线**。
|
||||
> 相关文档:[ARCHITECTURE.md](ARCHITECTURE.md)(架构,先懂再改)、[DATA_MODEL.md](DATA_MODEL.md)(表结构)、[doc/README.md](README.md)(文档索引与维护约定)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 开发流程与 git 工作流
|
||||
## 1. 开发流程
|
||||
|
||||
### 1.1 分支策略
|
||||
|
||||
| 分支 | 用途 |
|
||||
|------|------|
|
||||
| `dev` | **开发分支**,所有新功能/修复都在这里开发 |
|
||||
| `main` | **生产分支**(主分支),只放已确认的稳定版本 |
|
||||
| `dev` | 开发分支,所有新功能/修复从它切出、合并回它 |
|
||||
| `main` | 生产分支,只放已确认的稳定版本 |
|
||||
| `fix/xxx` · `feat/xxx` · `chore/xxx` | 单个改动的临时分支(从 `dev` 切出) |
|
||||
|
||||
生产环境 = 部署机 `192.168.20.220` 的 `/mnt/data/openstf/auto_control`(python-app 容器运行 `web_server.py`)。
|
||||
生产环境 = 部署机 `192.168.20.220` 的 `/mnt/data/openstf/auto_control`(`python-app` 容器)。
|
||||
|
||||
### 1.2 git 操作铁律(重要)
|
||||
### 1.2 git 铁律
|
||||
|
||||
**所有 git 操作都必须先经项目负责人明确确认后才能执行**,包括但不限于:
|
||||
**所有 git 操作都必须先经项目负责人明确确认**,包括但不限于 `commit` / `push`(**即使 push 到 dev 也要确认**)/ `merge` / `rebase` / `reset` / `branch -D`。
|
||||
|
||||
- `commit` / `push`(**即使 push 到 dev 也要确认**)
|
||||
- `merge`(dev → main)
|
||||
- `revert` / `checkout` / `reset` / `branch -D` 等
|
||||
|
||||
**标准流程:**
|
||||
**标准流程**:
|
||||
|
||||
```
|
||||
1. 在 dev 分支开发、本地测试
|
||||
2. 完成改动 → 把改动清单 + 建议 commit 信息 列给负责人
|
||||
3. 负责人确认 → 才能 commit + push dev
|
||||
4. 需要发布 → 负责人确认后再合并到 main
|
||||
5. 部署生产 → 负责人明确指示后才 pull 到 220
|
||||
① 本机新建分支 fix/xxx 或 feat/xxx(从 dev 切出)
|
||||
② 分支上开发 + 自测 跑通本机(服务/接口/页面),能自测就别只靠"看代码没问题"
|
||||
③ 交负责人确认 ★ 未经确认不合 dev
|
||||
④ 合并到 dev 确认通过后(保持线性:rebase 后 ff-merge)
|
||||
⑤ dev 整体就绪 功能齐全、验证完毕
|
||||
⑥ 合并到 main ★ 负责人确认后
|
||||
⑦ 生产 220 部署 git pull → docker restart python-app → 验证(见 DEPLOY.md §3)
|
||||
```
|
||||
|
||||
> 不允许"开发完顺手就 commit/push"。即使是一次性小改动,也要先确认。
|
||||
> 每个改动**单独分支 + 单独 commit**,主题单一,便于评审与回退。不允许"开发完顺手 commit/push"。
|
||||
|
||||
---
|
||||
|
||||
## 2. 环境说明
|
||||
## 2. 技术红线(违反会打断共享 adb transport 或造成生产事故)
|
||||
|
||||
| 环境 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| 开发机 | 本机(192.168.20.57) | `.venv` + 本地运行 `web_server.py` |
|
||||
| 生产机 | 部署机 220 的 `auto_control` | python-app 容器,`network_mode: host` |
|
||||
| STF 服务 | ~~`192.168.20.220:7100`~~ | 已停用(代码已摘除依赖)。注意:此处"已停用"与 [STF_REMOVAL.md](STF_REMOVAL.md) 的"待人工确认"项矛盾(220 侧是否已 `docker stop stf` 未核实),需以 220 实际为准 |
|
||||
| adb 容器 | 220 上 `adb`(host 网络 5037) | USB 设备远程 adb server;网络设备补连用 |
|
||||
| 设备 | Tailscale `100.100.10.x:5555` | Xiaomi 舰队,本机 `100.100.10.2` 在 tailnet 内 |
|
||||
| uiautodev | 本机 `20242` | 元素抓取服务(web_server 自动拉起) |
|
||||
| # | 红线 | 为什么 | 代码里的体现 |
|
||||
|---|------|--------|-------------|
|
||||
| 1 | **绝不 `adb kill-server`** | 会断掉所有设备的 adb transport,运行中任务全废 | `core/adb_helper.py` 只 connect 不 kill;Web 层硬拦截 `kill-server`/`disconnect` 字符串 |
|
||||
| 2 | **绝不对 `IP:5555` 设备 `adb disconnect`** | 该地址的 adb transport 是共享的 | `adb_disconnect` **全项目零调用方**;`STFDevice.release()` 空实现 |
|
||||
| 3 | **空闲设备扫描不主动 connect/disconnect** | 避免扰动共享连接 | 前台扫描对空闲设备直接返回"空闲";设备发现用 socket 探测 |
|
||||
| 4 | **adb key 保持历史 key 不变** | 设备信任该 key,换 key 全部 `unauthorized` | 部署沿用 `~/.android/adbkey` |
|
||||
| 5 | **生产(220)默认只读** | 生产事故成本高 | 任何写操作(pull/重启/改文件)都需负责人确认 |
|
||||
| 6 | **新增持久化表必须登记备份覆盖清单** | 漏登记 = 等于没备份 | `core/system_backup.py` 的 `SUMMARY_TABLES` + `TABLE_LABELS`,详见 [DEPLOY.md](DEPLOY.md) §5.2 |
|
||||
| 7 | **功能/配置/接口改动必须同步文档** | 文档落后会误导开发与运维 | 见 §6;索引 [doc/README.md](README.md) |
|
||||
|
||||
### 2.1 adb key(关键)
|
||||
### 其它开发约束
|
||||
|
||||
- 本机 `~/.android/adbkey` 沿用历史 key(原取自 STF adb 容器,全部设备都信任)
|
||||
- **不要随意更换 key**——设备会变 unauthorized 连不上
|
||||
- 旧 key 备份在 `~/.android/adbkey.local.bak`
|
||||
- 生产环境的容器也需要用这把 key(部署时处理)
|
||||
|
||||
### 2.2 设备连接方式
|
||||
|
||||
- 设备 serial 是 `IP:5555`(Tailscale 地址),**直连**优先(只 connect、绝不 disconnect)
|
||||
- USB 设备(serial 无冒号):插本机走本地 adb;插 220 走远程 adb server(`USB_ADB_HOST:5037`)
|
||||
- 本机已在 tailnet 内,直连可靠且快(<1s)
|
||||
- 设备加入/退出平台:工具 → 设备池管理(SQLite 清单,自动连接 + 型号采集)
|
||||
|
||||
### 2.3 配置键速查(`config.py` / `.env`)
|
||||
|
||||
`.env` 加载方式:项目根目录逐行解析、`os.environ.setdefault`(环境变量已设则不覆盖)。以下默认值以 `config.py` 为准:
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
|------|------|------|
|
||||
| `WEB_HOST` / `WEB_PORT` | `0.0.0.0` / `18050` | Web 后台监听(18050 避开 Windows 动态端口范围) |
|
||||
| `DATA_DIR` / `APK_DIR` | `data/` / `data/apks/` | 持久化数据目录 / APK 存储目录 |
|
||||
| `BACKUP_DIR` / `RESTORE_STAGING_DIR` / `RESTORE_PENDING_DIR` | `data/backups` / `data/restore_staging` / `data/restore_pending` | 系统备份:导出 zip、导入暂存、待重启生效的恢复目录 |
|
||||
| `ADB_PATH` | `bin/adb/adb`(Windows 为 `adb.exe`) | 按平台自动识别,代码只拼路径 |
|
||||
| `USB_ADB_HOST` / `USB_ADB_PORT` | `100.100.10.1` / `5037` | 220 的 adb 容器(host 网络),驱动远程 USB 设备 |
|
||||
| `DISCOVERY_PORT` / `DISCOVERY_SUBNETS` / `DISCOVERY_INTERVAL` | `5555` / 局域网+Tailscale 网段 / `60` | 设备自动发现;可在工具页设备池面板改,存 `app_meta` `discovery_*` 覆盖默认 |
|
||||
| `TAILSCALE_API_KEY` / `TAILSCALE_TAILNET` | 空 / 空 | 工具页 Tailscale 管理(官方 API v2) |
|
||||
| `WEB_SECRET_KEY` | 未配置则随机生成 | 会话密钥(web_server 读 `.env`;不配则重启登录态失效) |
|
||||
| `STF_URL` / `STF_TOKEN` / `STF_SSH_*` | 废弃 | STF 摘除后仅历史保留,代码不再使用 |
|
||||
- **`web_server.py` 以 `debug=False` 运行**:改 `core/`、`tasks/`、`templates/` 后**必须重启**;改前端 JS 后**强刷浏览器**
|
||||
- **任务参数放各自 `tasks/<app>/` 顶部**,不放 `config.py`
|
||||
- **运行时数据不提交 git**:`data/`、`logs/` 全是运行时产物
|
||||
- **不要移除分页与错峰**:监控/列表页已分页(100 台设备只渲染 10 行/页);任务批量触发已错峰(`_START_STAGGER_SEC`)
|
||||
- **不要手工改库结构**:走 `SCHEMA_MIGRATIONS`(模型表)或幂等原生建表
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术红线(开发限制)—— 违反会打断共享 adb transport,需人工恢复
|
||||
## 3. 本地开发
|
||||
|
||||
这些是踩过坑后总结的,**任何修改都不能引入**。违反任何一条都会导致设备连接被全部重建(历史原因:STF provider 共享同一 adb transport,摘除 STF 后仍保留此约束):
|
||||
|
||||
1. **绝不 `adb kill-server`**
|
||||
- 会断开所有设备的 adb transport,全部设备连接被重建,运行中任务中断
|
||||
- 见 `core/adb_helper.py`
|
||||
|
||||
2. **绝不对 `IP:5555` 设备 `adb disconnect`**
|
||||
- 该地址的 adb transport 是共享的(历史与 STF provider 共用),disconnect 会断掉全部相关连接
|
||||
- 直连模式下 `release()` 不 disconnect
|
||||
- 见 `core/device_worker.py` `STFDevice.release()`
|
||||
|
||||
3. **空闲设备扫描不主动 connect/disconnect**
|
||||
- IP:5555 的 transport 由多方共享(历史与 STF provider 共用),外部 connect/disconnect 会扰动共享连接
|
||||
- `_ForegroundScanner._scan_free` 对空闲设备直接返回"空闲",不碰 adb
|
||||
- 见 `core/task_manager.py`
|
||||
|
||||
4. **adb key 保持历史 key 不变**(见 2.1,设备信任该 key)
|
||||
|
||||
5. **直连优先,不引入第三方桥接**(见 2.2)
|
||||
|
||||
### 3.1 其他开发限制
|
||||
|
||||
- **`web_server.py` 以 `debug=False` 运行,不热重载**:改 `core/`、`tasks/`、`templates/`、`static/` 后必须重启 web_server(前端 HTML 改完强刷浏览器)
|
||||
- **任务参数放各自 `tasks/<app>/task.py` 顶部,不放 `config.py`**
|
||||
- **生产环境(220)默认只读**:任何写操作(改文件/重启容器/部署)都必须先经负责人确认
|
||||
- **数据库是 SQLite**(`data/users.db`,WAL 模式):运行时数据不提交 git
|
||||
- **前端 JS 已拆分多文件**(均位于 `static/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` 的顺序用 `<script src>` 加载(`markdown.js` 提供 `renderMarkdown()`,AI 控制台回答渲染用;依赖 `base.js` 的 `esc()`,故排在其后、agent.js 之前)
|
||||
- **监控页/大列表已加分页**:100 台设备也只渲染 10 行/页,不要移除分页逻辑
|
||||
- **任务批量触发已错峰**(`_START_STAGGER_SEC`):避免大量设备同时启动造成 adb 连接风暴,不要移除
|
||||
|
||||
---
|
||||
|
||||
## 4. 本地开发手册
|
||||
|
||||
### 4.1 首次安装
|
||||
### 3.1 安装
|
||||
|
||||
```bash
|
||||
# 用 Python 3.12 建 venv(README 推荐版本)
|
||||
/opt/homebrew/bin/python3.12 -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
|
||||
# macOS:adb 用系统自带的(bin/adb/adb 是被 gitignore 的符号链接)
|
||||
ln -sf /opt/homebrew/bin/adb bin/adb/adb
|
||||
|
||||
# 确认 adb key(必须是 STF 容器的 key,否则设备连不上)
|
||||
ls -la ~/.android/adbkey
|
||||
python -m venv .venv
|
||||
# Windows: .venv\Scripts\activate Linux/macOS: source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 4.2 启动
|
||||
确认 adb key(沿用既有 key,否则设备连不上):`ls -la ~/.android/adbkey`。
|
||||
|
||||
### 3.2 启动
|
||||
|
||||
```bash
|
||||
.venv/bin/python web_server.py
|
||||
# 访问 http://localhost:18050/ 账号 admin/admin123
|
||||
python web_server.py
|
||||
# 访问 http://localhost:18050/ admin / admin123
|
||||
```
|
||||
|
||||
启动日志看到以下即成功:
|
||||
```
|
||||
[INFO] [core.worker] 心跳看门狗已启动
|
||||
[INFO] [web] 管理后台: http://localhost:18050/ (admin/admin123)
|
||||
[INFO] [web] 启动服务: http://localhost:18050/
|
||||
需要 AI 控制台 / MCP 时,**另起一个进程**:
|
||||
|
||||
```bash
|
||||
MCP_ALLOW_WRITE=1 MCP_PLATFORM_USER=admin MCP_PLATFORM_PASS=<密码> \
|
||||
python -m mcp_server.mcp_server # 默认 http://127.0.0.1:8033/mcp
|
||||
```
|
||||
|
||||
### 4.3 测试
|
||||
(容器里由 `scripts/start.sh` 自动拉起,不需要手动。)
|
||||
|
||||
- **后端逻辑**:直接 `.venv/bin/python -c "..."` 调用(如 `tasks/`、`core/` 的函数)
|
||||
- **前端 UI**:Playwright(系统 `python3` 已装),脚本示例见下
|
||||
- **浏览器冒烟**:切 6 个 tab、开任务编辑器,确认无 JS 错误
|
||||
### 3.3 调试
|
||||
|
||||
```python
|
||||
# Playwright 冒烟示例(python3 运行)
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(headless=True)
|
||||
pg = b.new_page()
|
||||
pg.goto("http://127.0.0.1:18050/login")
|
||||
pg.fill("input[name=username]", "admin")
|
||||
pg.fill("input[name=password]", "admin123")
|
||||
pg.click("input[type=submit], button[type=submit]")
|
||||
pg.wait_for_load_state("networkidle")
|
||||
# ... 检查各 tab、编辑器
|
||||
b.close()
|
||||
```
|
||||
| 目的 | 做法 |
|
||||
|------|------|
|
||||
| 看日志 | `logs/core.log`(adb/调度)、`task.log`(任务)、`web.log`(Web/AI)、`action.log`;或「日志」Tab |
|
||||
| 看设备/任务状态 | 监控页;或 `GET /api/status`、`GET /api/health` |
|
||||
| 后端逻辑验证 | 直接 `python -c "…"` 调用 `core/`、`tasks/` 的函数(如构造 worker 检查参数合并) |
|
||||
| 前端验证 | 无 npm/构建,改完强刷;浏览器控制台看报错 |
|
||||
| 接口 500 巡检 | `python scripts/regression_test.py`(**⚠️ 当前在 Windows 上会因 `signal.alarm` 报错**,Linux/macOS 可用) |
|
||||
| 停止设备/清异常 | 监控页「停止全部 / 停止选中 / 清除全部异常」 |
|
||||
|
||||
### 4.4 常用调试
|
||||
> **改了数据库/配置想复原**:删 `data/users.db*` 会丢数据,别这么干;用「系统 → 备份/导入」或先手工复制一份 `data/`。
|
||||
|
||||
- **看日志**:`logs/` 下 `core.log` / `task.log` / `web.log` / `action.log`(10MB 滚动,保留 5 份)
|
||||
- **看设备/任务状态**:浏览器监控页,或 `GET /api/status`、`GET /api/health`
|
||||
- **停止设备/清异常**:监控页工具条用"停止全部 / 停止选中 / 清除全部异常"(`AUTO_RELEASE_STALE_OCCUPY` 等 STF occupy 残留清理配置代码已不再读取);崩溃残留的 worker 状态重启即清零
|
||||
- **打包项目**:`python scripts/pack.py`
|
||||
### 3.4 写测试的约定(重要)
|
||||
|
||||
本仓库没有单元测试框架,验证靠"跑起来 + 真实调用"。写验证脚本时**必须**:
|
||||
|
||||
- **不要碰真实数据**:需要会话/任务/分组时**自建**(如 `POST /api/agent/conversations` 建专属会话),绝不要依赖"当前选中项",也不要删自己没建的东西
|
||||
- **改配置前后都要回读校验**:改前 GET 存原值,收尾写回后**再 GET 比对**,不一致要显式报错
|
||||
- 临时数据用完即删,并核对"集合已复原"
|
||||
|
||||
> 教训:曾用浏览器脚本跑 AI 控制台冒烟,脚本清空 `localStorage` 后前端自动选中了**用户最近的会话**,收尾的"删除测试会话"把用户真实会话删了;同一脚本还把 AI 配置改成了假值。恢复手段见 [DATA_MODEL.md](DATA_MODEL.md) §7 与 git 历史。
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置速查
|
||||
|
||||
### 4.1 配置文件与优先级
|
||||
|
||||
- `config.py`:**程序级常量**(端口、路径、USB 远程 adb 等),改它要重启
|
||||
- `.env`(项目根,不入 git):**密钥与可覆盖配置**,逐行解析 + `os.environ.setdefault`(**真实环境变量优先**)
|
||||
- `app_meta`(数据库 KV):**运行时可改的配置**(AI 配置、设备发现参数),在界面上改
|
||||
|
||||
### 4.2 `config.py` 常量
|
||||
|
||||
| 常量 | 默认值 | 来源 | 说明 |
|
||||
|------|--------|------|------|
|
||||
| `ADB_PATH` | `bin/adb/adb(.exe)` | 按平台自动 | **不可用 env 覆盖** |
|
||||
| `WEB_HOST` / `WEB_PORT` | `0.0.0.0` / `18050` | 硬编码 | 18050 避开 Windows 动态端口段 |
|
||||
| `DATA_DIR` / `APK_DIR` | `data/` / `data/apks/` | 代码计算 | |
|
||||
| `BACKUP_DIR` / `RESTORE_STAGING_DIR` / `RESTORE_PENDING_DIR` | `data/backups` / `data/restore_staging` / `data/restore_pending` | 代码计算 | 备份相关 |
|
||||
| `USB_ADB_HOST` / `USB_ADB_PORT` | `100.100.10.1` / `5037` | `.env` 可覆盖 | USB 设备所在部署机的远程 adb server |
|
||||
| `TAILSCALE_API_KEY` / `TAILSCALE_TAILNET` | 空 | `.env` | Tailscale 管理功能 |
|
||||
| `DISCOVERY_PORT` / `DISCOVERY_INTERVAL` | `5555` / `60` | `.env` 可覆盖 | 设备发现(网段在工具页配置,存 `app_meta`) |
|
||||
| `DISCOVERY_SUBNETS` | 局域网 + Tailscale 网段 | **硬编码列表**(当前无 env 支持) | 默认扫描网段 |
|
||||
| `STF_*` | 空 | `.env` | **已废弃**,仅历史保留,代码不再使用 |
|
||||
|
||||
### 4.3 进程读取的环境变量(不在 config.py)
|
||||
|
||||
| 变量 | 默认 | 谁读 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `WEB_SECRET_KEY` | 未配置则随机 | `web_server.py` | 会话密钥,**生产必须固定** |
|
||||
| `DISABLE_SCHEDULER` | 未设 | `core/task_manager.py` | 设任意值则**不启动 cron 调度器**(测试用) |
|
||||
| `MCP_ENABLED` | `1` | `scripts/start.sh` | =0 则不后台拉起 MCP |
|
||||
| `MCP_ALLOW_WRITE` | `0`(脚本内强制 1) | `mcp_server/config.py` | 写操作总开关 |
|
||||
| `MCP_PLATFORM_URL` / `_USER` / `_PASS` | `http://127.0.0.1:18050` / `admin` / 空 | `mcp_server/config.py` | 登录平台的凭据(改过 admin 密码要同步) |
|
||||
| `MCP_ALLOWED_SERIALS` | 空=不限 | 同上 | 逗号分隔白名单(语义缺口见 [backlog](backlog/TODO.md)) |
|
||||
| `MCP_HTTP_HOST` / `_PORT` | `0.0.0.0` / `8033` | 同上 | |
|
||||
| `MCP_SCREENSHOT_WIDTH` / `MCP_JPEG_QUALITY` | `540` / `70` | 同上 | 返回给模型的截图层参数 |
|
||||
| `MCP_AUDIT_FILE` | `/var/log/mcp/audit.log`(脚本兜底 `/tmp/mcp_audit.log`) | 同上 | 审计日志路径 |
|
||||
| `AGENT_API_BASE` / `AGENT_MODEL` / `AGENT_API_KEY` / `DEEPSEEK_API_KEY` | DeepSeek 默认地址 / 默认模型 / 空 | `mcp_agent/config.py` | **仅 CLI 用**;Web 端 AI 配置优先读数据库 `app_meta` |
|
||||
| `AGENT_MCP_URL` / `AGENT_MAX_STEPS` / `AGENT_TIMEOUT` / `AGENT_LANG` | `http://127.0.0.1:8033/mcp` / 40 / 120 / zh | 同上 | |
|
||||
| `ANDROID_ADB_SERVER_ADDRESS` / `_HOST` / `_PORT` | 未设 | **adb 客户端自身**(非本项目代码) | 把 adb 调用指向远程 server;两套变量名都要设 |
|
||||
|
||||
> `mcp_server/config.py` 与 `mcp_agent/config.py` **不读 `.env`**(只读进程环境变量),与根 `config.py` 的行为不同。
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见开发任务
|
||||
|
||||
### 5.1 新增 App 任务类型
|
||||
> 每一项的"改哪里"清单也见 [ARCHITECTURE.md](ARCHITECTURE.md) §10。
|
||||
|
||||
参照 `tasks/generic/` 结构,详见 **[doc/TASK_DEV.md](TASK_DEV.md)**(6 步模板)。
|
||||
注意:当前平台**只保留 `generic_steps` 一种任务类型**;大多数 App 操作直接用步骤编辑器
|
||||
编排即可,不必新增任务类型。
|
||||
### 5.1 新增 HTTP 接口
|
||||
|
||||
```
|
||||
tasks/<app>/
|
||||
__init__.py # from . import task
|
||||
task.py # DEFAULT_PARAMS + Worker + @register_task
|
||||
actions/ # 专属操作(可选)
|
||||
```
|
||||
1. 在对应功能域的 `web/xxx_api.py` 加 `@bp.route(...)` + 鉴权装饰器(`@login_required` / `@perm_required(PERM_X)` / `@admin_required`)
|
||||
2. 新蓝图需在 `web/__init__.py` 的 `register_blueprints` 里注册
|
||||
3. 更新 [API.md](API.md)(路由索引表 + 详细小节)
|
||||
|
||||
### 5.2 新增专属操作
|
||||
### 5.2 新增数据库字段/表
|
||||
|
||||
在 `tasks/<app>/actions/` 建 `.py`,继承 `BaseAction` + `@register_action(ACTIONS)`,在 `__init__.py` import。
|
||||
- 模型改 `core/models.py`;新表 `create_all()` 会建
|
||||
- **老库**要在 `SCHEMA_MIGRATIONS` 里加迁移(版本号递增 + SQL)
|
||||
- **新增表**:登记进 `core/system_backup.py` 的 `SUMMARY_TABLES` + `TABLE_LABELS`(**红线**)
|
||||
- 更新 [DATA_MODEL.md](DATA_MODEL.md) 与 [DEPLOY.md](DEPLOY.md) §5.2
|
||||
|
||||
### 5.3 修改前端
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `templates/admin/monitor.html` | HTML 结构 + CSS + `<script src>` 引用 |
|
||||
| `static/admin/`(base/markdown/list/monitor/editor/tasks/tools/apps/admin/agent/system.js) | 前端 JS(按 monitor.html 中 `<script src>` 顺序拆分加载,功能归属见各文件;`markdown.js` = 轻量 Markdown 渲染器,AI 控制台回答用) |
|
||||
| 改什么 | 文件 |
|
||||
|--------|------|
|
||||
| 页面结构 / 样式 / 引入脚本 | `templates/admin/monitor.html` |
|
||||
| 公共工具(API/权限/Toast/Tab) | `static/admin/base.js` |
|
||||
| 列表分页排序 | `static/admin/list.js` |
|
||||
| 各功能域逻辑 | `static/admin/{monitor,editor,tasks,tools,apps,admin,agent,system,markdown}.js` |
|
||||
|
||||
改完**强刷浏览器**(Cmd/Ctrl+Shift+R),必要时重启 web_server。
|
||||
新增 JS 模块:建文件 → 在 `monitor.html` 里按依赖顺序加 `<script src>` → 更新 [ARCHITECTURE.md](ARCHITECTURE.md) §6.3 与本文 §5.3。
|
||||
|
||||
### 5.4 新增 API
|
||||
### 5.4 新增步骤类型 / 任务类型
|
||||
|
||||
路由按功能域放在 `web/` 蓝图包(`web/__init__.py` 的 `register_blueprints(app)` 统一注册 10 个蓝图:auth / monitor / tasks / admin / tools / devices / apks / tailscale / agent / system)。
|
||||
见 [TASK_DEV.md](TASK_DEV.md)(步骤类型要同时改后端 `STEP_TYPES` 与前端 `STEP_LIB`)。
|
||||
|
||||
- 新增 API:在对应功能域的 `web/xxx_api.py` 里加 `@bp.route(...)` + 权限装饰器(如 `admin_required`)
|
||||
- 新建蓝图:需在 `web/__init__.py` 里 import 并加进 `register_blueprints` 的注册元组
|
||||
- `web_server.py` 只做 app 装配(初始化、`register_blueprints(app)`、常驻线程启动),一般不改
|
||||
- 最后更新 **[doc/API.md](API.md)**
|
||||
### 5.5 新增 MCP 工具
|
||||
|
||||
### 5.5 新增数据库字段/表
|
||||
在 `mcp_server/mcp_server.py` 加 `@mcp.tool()` 函数;写操作必须挂门控(`_check_write` → `_check_serial` → `_ensure_device_free`);更新 [MCP.md](MCP.md)。
|
||||
|
||||
- 模型改 `core/models.py`,首次建表用 `create_all()`;SQLAlchemy 模型未写 `__tablename__` 时默认表名 = 小写类名
|
||||
- **已有数据的老库**:在 `core/models.py` 的 `SCHEMA_MIGRATIONS` 里加迁移(版本号递增 + SQL)
|
||||
- `app_meta`(KV 配置表)由 `core/models.py` 的 `_migrate_schema()` 建表并维护 `schema_version`
|
||||
- `agent_conversation` / `agent_experience` / `experience_audit` / `agent_action`(动作经验库)由 `web/agent_api.py` 内的原生 `CREATE TABLE IF NOT EXISTS` 幂等创建(模块内首次用时执行),**不经 `SCHEMA_MIGRATIONS`,无版本管理**
|
||||
### 5.6 新增常驻线程
|
||||
|
||||
### 5.6 改动必须同步文档
|
||||
参考 `core/device_discovery.py` 的 `init_app(app)` / `shutdown()` 模式;更新 [ARCHITECTURE.md](ARCHITECTURE.md) §3。
|
||||
|
||||
任何功能/配置/接口/页面改动,须与代码同一 commit 同步更新对应文档:
|
||||
---
|
||||
|
||||
| 改动类型 | 对应文档 |
|
||||
|------|------|
|
||||
## 6. 文档同步(红线)
|
||||
|
||||
**任何功能 / 配置 / 接口 / 表结构的增删改,都要在同一个 commit 里更新对应文档。**
|
||||
|
||||
| 改动类型 | 必须更新 |
|
||||
|---------|---------|
|
||||
| HTTP 接口 | [API.md](API.md) |
|
||||
| 数据表 / schema | [ARCHITECTURE.md](ARCHITECTURE.md) §3.6 |
|
||||
| `config.py` / `.env` 键增删 | 本文档 §2 + [.env.example](../.env.example) |
|
||||
| 页面 Tab / 子分栏 / 前端拆分 | [ARCHITECTURE.md](ARCHITECTURE.md) §5 |
|
||||
| 任务 / 步骤 | [TASK_DEV.md](TASK_DEV.md) |
|
||||
| 常驻线程 / 进程与装配 | [ARCHITECTURE.md](ARCHITECTURE.md) §7 |
|
||||
| MCP 工具 | [MCP.md](MCP.md) 与 [MCP_DESIGN.md](MCP_DESIGN.md) |
|
||||
| 对外接入 / 数字员工知识库 | [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md) 与 [staffdeck/JOB_SPEC.md](staffdeck/JOB_SPEC.md) |
|
||||
| 表结构 / 迁移 | [DATA_MODEL.md](DATA_MODEL.md)(+ 备份覆盖清单) |
|
||||
| `config.py` / `.env` 键 | 本文 §4 + [DEPLOY.md](DEPLOY.md) + `.env.example` |
|
||||
| 页面 Tab / 子分栏 / 前端模块 | [ARCHITECTURE.md](ARCHITECTURE.md) §6 + 本文 §5.3 |
|
||||
| 任务类型 / 步骤 schema | [TASK_DEV.md](TASK_DEV.md) |
|
||||
| 常驻线程 / 装配顺序 | [ARCHITECTURE.md](ARCHITECTURE.md) §2-3 |
|
||||
| MCP 工具 | [MCP.md](MCP.md) + [MCP_DESIGN.md](MCP_DESIGN.md) |
|
||||
| 对外接入约定 | [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md) |
|
||||
| 暂缓项 / 已知问题 | [backlog/TODO.md](backlog/TODO.md) |
|
||||
|
||||
> **备份覆盖红线(2026-09-10 新增)**:**新增任何持久化表**(业务数据)时,必须同步把它登记进
|
||||
> `core/system_backup.py` 的 `SUMMARY_TABLES`(并在 `TABLE_LABELS` 给中文名)+ 更新
|
||||
> [DEPLOY.md](DEPLOY.md) §3.5 的覆盖清单。理由:清单漏登记 → 导出预览看不到该表 → 会被误判为
|
||||
> "没有备份"(动作库 `agent_action` 就踩过)。导出侧有覆盖自检、导入侧有未登记表反向告警。
|
||||
|
||||
注:STF_REMOVAL.md 是历史迁移记录,不改写。
|
||||
新增文档时:登记进 [doc/README.md](README.md) §1 与根 [README](../README.md) 的文档索引。
|
||||
历史文档([STF_REMOVAL.md](STF_REMOVAL.md))只增不改。
|
||||
|
||||
---
|
||||
|
||||
## 6. 发布流程(团队约定,2026-09-10 更新)
|
||||
## 7. 常见坑速查
|
||||
|
||||
**每个改动都走分支,确认后再并 dev;dev 整体就绪后才并 main 上生产。**
|
||||
| 坑 | 说明 |
|
||||
|----|------|
|
||||
| 跨线程访问 DB | 必须自推 app context(`with app.app_context()`) |
|
||||
| 改了不生效 | `core/`、`tasks/`、`templates/` 改动要重启;JS 要强刷 |
|
||||
| Windows adb 输出 | 不能用 `text=True`(非 GBK 字节会崩),要收 bytes 再解码 |
|
||||
| u2 卡死 | `u2.connect` / `d.info` 可能永久 hang,必须加超时 |
|
||||
| 抢占死锁 | 不能在 `TaskManager._lock` 内调 `stop_device` |
|
||||
| 删除不生效 | 删任务/分组必须显式删行,否则重启会"复活" |
|
||||
| 前端写了 `data-perm` 仍可见 | 它只在 `loadMe()` 时求值一次;改权限后需刷新 |
|
||||
| 新步骤没生效 | 后端 `STEP_TYPES` 与前端 `STEP_LIB` 要同时改 |
|
||||
| 备份导入后没变化 | 必须重启服务 |
|
||||
| `.env` 不生效 | `setdefault` 语义:**已存在的环境变量优先**,检查是否被系统环境覆盖 |
|
||||
|
||||
```
|
||||
① 本机新建分支 fix/xxx 或 feat/xxx(从 dev 切出)
|
||||
② 分支上开发 + 自测 本机跑通(服务/接口/页面)
|
||||
③ 交负责人确认 ★ 未经确认不合 dev
|
||||
④ 合并到 dev 确认通过后(fast-forward 或 merge)
|
||||
⑤ dev 整体就绪 dev 上功能齐全、验证完毕
|
||||
⑥ 合并到 main ★ 负责人确认后
|
||||
⑦ 生产 220 部署 git pull → 重启 python-app 容器 → 验证
|
||||
```
|
||||
|
||||
**生产 220 部署(目录 `/mnt/data/openstf/auto_control`,容器 `python-app` 挂到 `/app`)**:
|
||||
|
||||
```bash
|
||||
cd /mnt/data/openstf/auto_control
|
||||
git fetch origin && git checkout main && git pull --ff-only origin main
|
||||
docker restart python-app # 入口 scripts/start.sh:依赖守卫 → 拉起 MCP → exec web_server
|
||||
# 验证:curl -s http://127.0.0.1:18050/api/health ; ss -ltnp | grep -E ':(18050|8033|20242)'
|
||||
```
|
||||
|
||||
**发布前检查**:生产容器 adb key、依赖(新增依赖看 `requirements.txt`)、数据库迁移(`create_all` 自动补新表)、
|
||||
以及"新增持久化表是否已登记进备份覆盖清单"(见 §5.6 红线)。
|
||||
|
||||
**数据迁移**:用平台自带「系统 → 数据备份导出/导入」;导入后需**重启容器**才生效(见 [DEPLOY.md](DEPLOY.md) §3.5)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 文档索引
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [README](../README.md) | 项目总览、快速上手 |
|
||||
| **[DEVELOPMENT.md](DEVELOPMENT.md)** | 本文档:流程/准则/限制/手册 |
|
||||
| [TASK_DEV.md](TASK_DEV.md) | 任务开发指南(新增 App 任务模板) |
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | 架构详解(分层、数据流、设计决策) |
|
||||
| [API.md](API.md) | 全部 HTTP 接口说明 |
|
||||
| [DEPLOY.md](DEPLOY.md) | 部署指南(环境、生产、故障排查) |
|
||||
| [STF_REMOVAL.md](STF_REMOVAL.md) | 摘除 STF 的历史迁移记录(2026-08,不随现状改写) |
|
||||
| [MCP.md](MCP.md) | MCP 手机控制使用手册(工具清单/用法) |
|
||||
| [MCP_DESIGN.md](MCP_DESIGN.md) | MCP 架构与演进设计 |
|
||||
| [AI_TASK_GEN.md](AI_TASK_GEN.md) | AI 建任务设计文档 |
|
||||
| [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md) | 给 StaffDeck 数字员工的知识库(MCP 接入/工具/约定/红线) |
|
||||
| [staffdeck/JOB_SPEC.md](staffdeck/JOB_SPEC.md) | 数字员工岗位说明(岗位描述/看板摘要/执行约束) |
|
||||
| [backlog/TODO.md](backlog/TODO.md) | 待完成项(已确认但暂缓的功能/优化,完成时移出并同步文档) |
|
||||
更完整的"代码里写明的坑"见 [ARCHITECTURE.md](ARCHITECTURE.md) §9.2。
|
||||
|
||||
+136
-106
@@ -1,147 +1,172 @@
|
||||
# MCP 手机控制 Server(`mcp_server/`)
|
||||
# MCP 手机控制手册(MCP.md)
|
||||
|
||||
多模态 AI(DeepSeek/Claude 等)通过 [MCP(Model Context Protocol)](https://modelcontextprotocol.io) 实时操作 Android 手机的统一出口。AI 控制台(`mcp_agent/`)与外部 MCP 客户端都经它控制设备池中的手机。
|
||||
多模态 AI(DeepSeek / Claude 等)通过 [MCP(Model Context Protocol)](https://modelcontextprotocol.io) 操作 Android 手机的统一出口。平台自带的「AI 控制台」(`mcp_agent/`)与外部 MCP 客户端使用的都是这一批工具。
|
||||
|
||||
> 本文档 = 使用手册(工具清单/用法)。架构与演进设计见 [MCP_DESIGN.md](MCP_DESIGN.md)。
|
||||
> 本文 = **使用手册**(工具清单 / 用法 / 接入)。设计取舍与演进见 [MCP_DESIGN.md](MCP_DESIGN.md);AI 控制台的会话/经验/动作机制见 [AI_CONSOLE.md](AI_CONSOLE.md)。
|
||||
|
||||
## 架构
|
||||
---
|
||||
|
||||
## 1. 架构与边界
|
||||
|
||||
```
|
||||
AI 控制台 / 外部 MCP 客户端
|
||||
│ Streamable HTTP
|
||||
│ Streamable HTTP(http://<host>:8033/mcp)
|
||||
▼
|
||||
MCP Server (:8033, mcp_server/mcp_server.py) ← 19 个 de_* 工具
|
||||
│ 平台 HTTP API(登录 + CSRF)
|
||||
MCP Server(mcp_server/mcp_server.py) ← 19 个 de_* 工具
|
||||
│ 平台 HTTP API(登录 + CSRF) 或 轻量 adb/u2 直连
|
||||
▼
|
||||
auto_control 平台 (:18050) ← 设备池/任务/看屏
|
||||
auto_control 平台(:18050) ← 设备池 / 任务 / 看屏
|
||||
│ adb / uiautomator2 / uiautodev / OCR
|
||||
▼
|
||||
Android 设备(IP:5555)
|
||||
```
|
||||
|
||||
- 轻量通道直连(adb monkey 开 App、u2 输入、本地 OCR)不绕平台,省时省 token
|
||||
- 坐标换算、UI 吸附、剪贴板注入等细节全部在 Server 层消化,模型只需给意图
|
||||
- **轻量通道直连**(adb monkey 开 App、u2 输入、本地 OCR)不绕平台,省时间省 token
|
||||
- **坐标换算、UI 吸附、剪贴板注入**等细节全部在 Server 层消化,模型只给"意图"
|
||||
- **只暴露设备层能力**:平台级的任务增改/分组/设备池/APK/备份等 REST 只给 Web 前端用,未做成 MCP 工具(补齐规划见 [AI_TASK_GEN.md](AI_TASK_GEN.md) §9)
|
||||
|
||||
## 运行与配置
|
||||
---
|
||||
|
||||
服务跑在 220 的 `python-app` 容器内(`scripts/start.sh` 自动拉起,端口 8033)。
|
||||
生产访问方式:`http://192.168.20.220:8033/mcp`;本机调试 `python3 -m mcp_server.mcp_server`。
|
||||
## 2. 运行与配置
|
||||
|
||||
生产跑在 220 的 `python-app` 容器内(`scripts/start.sh` 自动拉起),端点 `http://192.168.20.220:8033/mcp`。
|
||||
|
||||
```bash
|
||||
# 本机手动启动
|
||||
MCP_ALLOW_WRITE=1 MCP_PLATFORM_USER=admin MCP_PLATFORM_PASS=<平台密码> \
|
||||
MCP_AUDIT_FILE=data/mcp_audit.log python -m mcp_server.mcp_server
|
||||
```
|
||||
|
||||
| 环境变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `MCP_PLATFORM_URL` | `http://127.0.0.1:18050` | 平台地址(Agent 与平台同机时用本机) |
|
||||
| `MCP_PLATFORM_USER` / `MCP_PLATFORM_PASS` | `admin` / start.sh 兜底 `admin123`(手动直跑时默认空) | 平台登录账号 |
|
||||
| `MCP_ALLOW_WRITE` | `0` | **写门控**:=1 才允许点击/输入/开关 App 等写操作(只读工具不受限) |
|
||||
| `MCP_ALLOWED_SERIALS` | 空 | 设备白名单(逗号分隔):**非空=只允许列出的 serial**;为空时代码只校验 serial 非空、**不校验是否在平台设备池内**(设计稿语义未实现) |
|
||||
| `MCP_HTTP_HOST` / `MCP_HTTP_PORT` | `0.0.0.0` / `8033` | 监听地址 |
|
||||
| `MCP_SCREENSHOT_WIDTH` | `540` | 截图返回宽度上限(px),模型看到的图即该坐标系 |
|
||||
| `MCP_PLATFORM_URL` | `http://127.0.0.1:18050` | 平台地址 |
|
||||
| `MCP_PLATFORM_USER` / `MCP_PLATFORM_PASS` | `admin` / 空(`start.sh` 兜底 `admin123`) | 平台登录凭据。**改过 admin 密码必须同步**,否则登录失败 |
|
||||
| `MCP_ALLOW_WRITE` | `0`(`start.sh` 内强制 `1`) | **写门控**:=1 才允许点击/输入/开关 App(只读工具不受限) |
|
||||
| `MCP_ALLOWED_SERIALS` | 空 | 设备白名单(逗号分隔)。非空 = 只允许列出的 serial;**为空时只校验 serial 非空,不按平台设备池过滤**(语义缺口见 [backlog](backlog/TODO.md)) |
|
||||
| `MCP_HTTP_HOST` / `MCP_HTTP_PORT` | `0.0.0.0` / `8033` | 监听 |
|
||||
| `MCP_SCREENSHOT_WIDTH` | `540` | 截图返回宽度上限(模型看到的坐标系) |
|
||||
| `MCP_JPEG_QUALITY` | `70` | 截图 JPEG 质量 |
|
||||
| `MCP_AUDIT_FILE` | `/var/log/mcp/audit.log` | 审计日志(每次工具调用一行) |
|
||||
| `MCP_AUDIT_FILE` | `/var/log/mcp/audit.log`(容器兜底 `/tmp/mcp_audit.log`) | 审计日志路径 |
|
||||
| `MCP_PLATFORM_TIMEOUT` | `30` | 平台请求超时(秒) |
|
||||
| `MCP_ENABLED` | `1` | **只被 `scripts/start.sh` 消费**:=0 则不后台拉起 MCP |
|
||||
|
||||
生产(220 容器)已设 `MCP_ALLOW_WRITE=1`;`MCP_ALLOWED_SERIALS` 未设(= 代码只校验 serial 非空,**不限制到平台设备池内**;如需收紧请配置白名单)。
|
||||
> `mcp_server/config.py` **只读进程环境变量,不读 `.env`**;`scripts/supervise.sh` 不拉起 MCP(容器场景由 `start.sh` 负责)。
|
||||
|
||||
> **MCP_ENABLED**:由 `scripts/start.sh` 消费(默认 `1`,=0 可关掉后台拉起的 MCP)。
|
||||
> **MCP_PLATFORM_PASS**:start.sh 兜底 `admin123`——改过平台 admin 密码必须同步该变量,否则 MCP 登录平台失败。
|
||||
> **supervise.sh 不拉起 MCP**:只守护 web_server;容器场景 MCP 由 start.sh 后台拉起(见 DEPLOY §2.4)。
|
||||
---
|
||||
|
||||
## 坐标空间(重要约定)
|
||||
## 3. 坐标空间(重要)
|
||||
|
||||
- `de_screenshot` 返回 ≤540px 宽的 JPEG(display 空间),并附 `native_size`(设备原生分辨率)
|
||||
- `de_tap` / `de_swipe` 的坐标一律使用 **de_screenshot 返回图像的坐标系**,Server 按比例换算为原生坐标
|
||||
- **先截图、后点击**:Server 需要最近一次截图才能建立坐标空间(未截图就点击会报「请先执行 de_screenshot」)
|
||||
- `de_tap` 带**自动吸附**:点击点若落在某个可点击元素内,实际会点该元素中心——坐标只需大致对准,偏十几像素也能点准;点空白处则按原坐标
|
||||
- 返回的 `snapped/label` 可核对吸附结果(snapped=true 表示已吸到元素,label 为该元素文案)
|
||||
- `de_screenshot` 返回 **≤540px 宽**的 JPEG(display 空间),同时给出 `native_size`(设备原生分辨率)与 `screen_state`
|
||||
- `de_tap` / `de_swipe` 的坐标一律用 **`de_screenshot` 返回图像的坐标系**,Server 按比例换算成原生坐标
|
||||
- **必须先截图再点击**:Server 需要最近一次截图来建立坐标空间,否则报 `invalid_param`「请先对该设备执行 de_screenshot」
|
||||
- `de_tap` 带**自动吸附**:落点若在某个可点击元素内,实际点该元素中心 → 坐标**只需大致对准**;返回 `snapped` / `label` 便于核对
|
||||
|
||||
## 工具清单(19 个)
|
||||
---
|
||||
|
||||
### 设备与状态
|
||||
## 4. 工具清单(19 个)
|
||||
|
||||
| 工具 | 用途 |
|
||||
|---|---|
|
||||
| `de_list_devices` | 列出可控制设备:serial / 型号 / 在线 / 任务状态 / 前台 App。**开局第一步** |
|
||||
| `de_foreground_app(serial)` | 当前前台 App 包名(dumpsys,MIUI 焦点为空时自动兜底) |
|
||||
统一返回约定:
|
||||
|
||||
### 观察屏幕(感知)
|
||||
```json
|
||||
{"ok": true, "data": { … }}
|
||||
{"ok": false, "error": {"code": "device_busy", "message": "设备正在执行任务…"}}
|
||||
```
|
||||
|
||||
| 工具 | 用途 |
|
||||
|---|---|
|
||||
| `de_screenshot(serial)` | 截图并返回图像(≤540px JPEG)+ 尺寸 + 亮/熄屏状态。多模态模型直接看图 |
|
||||
| `de_ui_tree(serial, limit=150)` | 当前界面元素树(text/id/desc/class/bounds)。**可点击元素排前**;limit 1-300 控制条数防 token 膨胀。用于确认界面上有什么 |
|
||||
| `de_ocr(serial)` | OCR 识别当前屏幕文字(UI 树没有的图片/WebView 文字也能识别),返回 [{text, score}] |
|
||||
错误码:`invalid_param` · `device_not_allowed` · `write_disabled` · `device_busy` · `platform_unavailable` · `device_offline` · `text_not_found`。
|
||||
|
||||
### 点击与滑动(操作)
|
||||
### 4.1 设备与状态(只读)
|
||||
|
||||
| 工具 | 用途 |
|
||||
|---|---|
|
||||
| `de_tap_text(serial, text)` | **按屏幕文字点击**(推荐):给一个屏幕上可见的文字(子串匹配)即找到并点其中心。原生控件走 UI 树,WebView/图片文字自动 OCR 兜底。找不到返回明确错误 |
|
||||
| `de_tap_element(serial, by, value, index=1)` | 按元素点击:`by` = text / id / desc(精确)或 text_contains / desc_contains(模糊)。多命中用 index 取第几个 |
|
||||
| `de_tap(serial, x, y)` | 坐标点击(截图坐标系,自动吸附,见上)。**纯图形目标(视频画面/无文字图标)才用它** |
|
||||
| `de_swipe(serial, x1,y1,x2,y2, duration=0.2)` | 滑动(截图坐标系;长按=同点起止 + duration≥1) |
|
||||
| `de_press_key(serial, key)` | 按键:back / home / recent / menu / power / volume_up / volume_down / enter / delete / search / camera |
|
||||
| 工具 | 参数 | 返回 / 说明 |
|
||||
|------|------|------------|
|
||||
| `de_list_devices` | — | `[{serial, model, online, task_job, worker_status, foreground_app}]`。**开局第一步** |
|
||||
| `de_foreground_app` | `serial` | `{foreground_app}` 当前前台包名(dumpsys,MIUI 焦点为空时兜底) |
|
||||
| `de_list_tasks` | — | 平台任务计划 `[{id,name,task_type,enabled,schedule}]`(只读) |
|
||||
|
||||
### 输入与剪贴板
|
||||
### 4.2 观察屏幕(只读)
|
||||
|
||||
| 工具 | 用途 |
|
||||
|---|---|
|
||||
| `de_type_text(serial, text)` | 向当前界面输入框输入文字(支持中文,直设 EditText 不依赖剪贴板/粘贴) |
|
||||
| `de_set_clipboard(serial, text)` | 写入设备剪贴板(ClipInject 通道注入 + 读回验证) |
|
||||
| `de_read_clipboard(serial)` | 读取设备当前剪贴板内容 |
|
||||
| 工具 | 参数 | 返回 / 说明 |
|
||||
|------|------|------------|
|
||||
| `de_screenshot` | `serial` | `{image:{type:"image",data:<base64>,mimeType:"image/jpeg"}, width, height, native_size, screen_state}`。多模态模型直接看图 |
|
||||
| `de_ui_tree` | `serial`, `limit`(150,1-300) | `{count, elements:[{text,id,desc,class,clickable,bounds}]}`,**可点击元素排前**;`limit` 控 token |
|
||||
| `de_ocr` | `serial` | `{count, texts:[{text,score}]}`(≤100 条)。UI 树拿不到的图片/WebView 文字用它 |
|
||||
| `de_read_clipboard` | `serial` | `{clipboard}` |
|
||||
| `de_list_apps` | `serial`, `keyword`("") | `{count, packages}` 第三方已装包名(`pm list packages -3`,≤200) |
|
||||
|
||||
### App 管理(轻量 adb 直连,不建 u2 会话)
|
||||
### 4.3 点击与滑动(**写操作**)
|
||||
|
||||
| 工具 | 用途 |
|
||||
|---|---|
|
||||
| `de_open_app(serial, package)` | 打开 App(adb monkey 直启,无需知道 activity——最快的打开路径) |
|
||||
| `de_stop_app(serial, package)` | 强制停止 App(am force-stop) |
|
||||
| `de_list_apps(serial, keyword="")` | 列出第三方已装应用(pm list packages -3),keyword 可过滤(如 "douyin") |
|
||||
| 工具 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `de_tap_text` | `serial`, `text`(≤100 字符) | **推荐**:按屏幕可见文字点击。原生控件走 UI 树,WebView/图片文字自动 **OCR 兜底**;找不到 → `text_not_found` |
|
||||
| `de_tap_element` | `serial`, `by`(`text`/`id`/`desc`/`text_contains`/`desc_contains`), `value`, `index`(1) | 按元素属性点击,无需坐标;多命中用 `index` |
|
||||
| `de_tap` | `serial`, `x`, `y` | 坐标点击(截图坐标系 + 自动吸附)。**纯图形目标才用** |
|
||||
| `de_swipe` | `serial`, `x1,y1,x2,y2`, `duration`(0.2) | 滑动(长按 = 同点起止 + `duration≥1`) |
|
||||
| `de_press_key` | `serial`, `key` | `back` / `home` / `recent` / `menu` / `power` / `volume_up` / `volume_down` / `enter` / `delete` / `search` / `camera` |
|
||||
|
||||
> 上表三个工具走 `direct_ops` 轻量 **adb 直连**(monkey / am force-stop / pm list packages),不建 u2 会话。
|
||||
### 4.4 输入与剪贴板(**写操作**)
|
||||
|
||||
### 亮屏与熄屏(平台 screen_all 通道)
|
||||
| 工具 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `de_type_text` | `serial`, `text` | 向当前输入框输入(支持中文,直设 EditText,不依赖剪贴板) |
|
||||
| `de_set_clipboard` | `serial`, `text` | 写入设备剪贴板(ClipInject 通道 + 读回验证) |
|
||||
|
||||
| 工具 | 用途 |
|
||||
|---|---|
|
||||
| `de_sleep(serial)` | 熄屏(**运行中任务会中断,慎用**) |
|
||||
| `de_wake(serial)` | 亮屏并解锁(熄屏时先调它再截图) |
|
||||
### 4.5 App 管理(**写操作**,走 adb 直连不建 u2 会话)
|
||||
|
||||
> `de_sleep`/`de_wake` 走平台 `POST /api/device/screen_all`(`platform_client`),同样不在 MCP 进程内建 u2 会话。
|
||||
| 工具 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `de_open_app` | `serial`, `package` | `adb monkey` 直启(最快路径,无需知道 activity) |
|
||||
| `de_stop_app` | `serial`, `package` | `am force-stop` |
|
||||
|
||||
### 平台联动
|
||||
### 4.6 亮屏 / 熄屏(**写操作**)
|
||||
|
||||
| 工具 | 用途 |
|
||||
|---|---|
|
||||
| `de_list_tasks()` | 列出平台任务计划(名称/类型/启用/调度),了解已自动化的工作 |
|
||||
| 工具 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `de_wake` | `serial` | 亮屏并解锁(熄屏时先调它再截图) |
|
||||
| `de_sleep` | `serial` | 熄屏(**会中断正在运行的任务,慎用**) |
|
||||
|
||||
### 现状边界
|
||||
> 后两组分别经 `direct_ops`(adb)与平台 `POST /api/device/screen_all` 实现,不在 MCP 进程内建 u2 会话。
|
||||
|
||||
本 Server 目前只有**设备层**的 `de_*`(控制/感知/只读)+ 平台**只读**的 `de_list_tasks`;平台级**任务增改/提交/CRUD、分组/设备池/自定义动作/APK/备份**等 REST 路由只给 Web 前端用,**未暴露 MCP 工具**。
|
||||
### 4.7 写操作门控(三连)
|
||||
|
||||
补齐分层规划见 [AI_TASK_GEN.md](AI_TASK_GEN.md) §9。**每新增/修改/删除一个 MCP 工具或平台配置,必须同步更新本手册与 [MCP_DESIGN.md](MCP_DESIGN.md)(doc 同步红线)**。
|
||||
所有写工具在执行前依次检查:
|
||||
|
||||
## 推荐使用模式(操作手机的正确姿势)
|
||||
1. `_check_write()` —— `MCP_ALLOW_WRITE=1`?否则 `write_disabled`
|
||||
2. `_check_serial()` —— serial 非空 / 在白名单内?否则 `device_not_allowed`
|
||||
3. `_ensure_device_free()` —— 设备 `worker_status` 不是 `running`/`connecting`?否则 `device_busy`(**AI 不与任务抢设备**)
|
||||
|
||||
> 设备状态查询有 **5 秒缓存**;查询本身异常时**放行**(不阻塞),由平台侧兜底。
|
||||
|
||||
---
|
||||
|
||||
## 5. 推荐使用模式
|
||||
|
||||
1. **`de_list_devices`** 确认目标设备在线
|
||||
2. **`de_screenshot`** 看图理解当前界面(图像会在下一次模型回合送达)
|
||||
3. **点击定位优先级**(从高到低):
|
||||
- 目标有可见文字 → **`de_tap_text`**(一次调用完成「找到并点击」,最可靠)
|
||||
- 文字有歧义/多候选 → `de_ui_tree` 确认后 `de_tap_element`(text_contains 模糊匹配)
|
||||
- 纯图形目标 → `de_tap` 坐标(**无需精算**,Server 自动吸附到可点元素中心)
|
||||
4. 输入文字:先点中输入框(de_tap_text / de_tap),再 `de_type_text`
|
||||
5. 每次关键操作后 `de_screenshot` 验证:界面变化 = 成功;无变化 = 未命中,换 de_tap_text / de_tap_element 重新定位,**不要重复点同一坐标**
|
||||
6. 完成/失败时用中文总结:做了什么、当前状态、注意事项
|
||||
7. 效率约束:界面未变不重复截图/点同位置;连续 6 步无进展停止并总结
|
||||
2. **`de_screenshot`** 看图理解当前界面(图像会在下一回合送达模型)
|
||||
3. **点击定位优先级**:
|
||||
- 目标有可见文字 → **`de_tap_text`**(一次调用完成"找到并点击",最可靠)
|
||||
- 文字有歧义/多候选 → `de_ui_tree` 确认后用 `de_tap_element`(可用 `text_contains` 模糊匹配)
|
||||
- 纯图形目标 → `de_tap` 坐标(**无需精算**,会自动吸附)
|
||||
4. **输入文字**:先点中输入框,再 `de_type_text`
|
||||
5. **每步验证**:关键操作后再 `de_screenshot`——界面变了 = 成功;没变 = 未命中 → 换 `de_tap_text`/`de_tap_element` 重新定位,**不要重复点同一坐标**
|
||||
6. **收尾**:用中文总结做了什么、当前状态、注意事项
|
||||
7. **效率**:界面未变时不重复截图/点击;连续 6 步无进展就停止并总结
|
||||
|
||||
## 安全与审计
|
||||
---
|
||||
|
||||
- **写门控**:`MCP_ALLOW_WRITE=0`(默认)时点击/输入/开关 App 全部拒绝,只读工具可用
|
||||
- **任务占用互斥**:写工具操作前调 `_ensure_device_free` 检查设备 `worker_status`,running/connecting 直接拒 `device_busy`(只读工具不受限),AI 不与任务抢设备
|
||||
- **平台会话 + CSRF**:平台登录与会话由 `platform_client` 内部处理(`MCP_PLATFORM_USER/PASS` 登录拿 cookie、失效自动重登;POST 自动带 `X-CSRF-Token`),不向客户端暴露平台凭据
|
||||
- **设备白名单**:`MCP_ALLOWED_SERIALS` 非空时限制可操作的 serial;空名单时代码只校验 serial 非空,不按平台设备池过滤
|
||||
- **审计日志**:每次调用记录 `{ts, tool, serial, args, result}` 到 `MCP_AUDIT_FILE`(220 上 `/tmp/mcp_audit.log`)
|
||||
- 操作对象限定平台设备池;`adb kill-server` / `adb disconnect` 属项目红线,任何工具不触碰
|
||||
## 6. 安全与审计
|
||||
|
||||
## 客户端接入示例
|
||||
| 机制 | 说明 |
|
||||
|------|------|
|
||||
| **写门控** | `MCP_ALLOW_WRITE=0`(默认)时所有写操作被拒,只读可用 |
|
||||
| **任务互斥** | 写操作前检查设备是否在跑任务,忙则 `device_busy` |
|
||||
| **平台会话** | `platform_client` 内部登录(`MCP_PLATFORM_USER/PASS`)、会话失效自动重登、POST 自动带 `X-CSRF-Token`;**平台凭据不暴露给客户端** |
|
||||
| **白名单** | `MCP_ALLOWED_SERIALS` 非空时限制可操作 serial |
|
||||
| **审计** | 每次调用(**含只读**)写一行 JSON 到 `MCP_AUDIT_FILE`:`{ts, tool, serial, args 摘要, result 摘要}` |
|
||||
| **红线** | 任何工具都不执行 `adb kill-server` / `adb disconnect` |
|
||||
| **端点鉴权** | ⚠️ MCP HTTP 端点自身**没有** token/账号校验,只做出站登录 → **必须靠网络隔离**(同机/内网),不要直接暴露公网 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 客户端接入示例
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
@@ -150,24 +175,29 @@ from fastmcp import Client
|
||||
async def main():
|
||||
async with Client("http://192.168.20.220:8033/mcp", timeout=30) as c:
|
||||
devs = await c.call_tool("de_list_devices", {})
|
||||
serial = devs.data["data"][0]["serial"] # 取第一台在线设备
|
||||
serial = devs.data["data"][0]["serial"] # 第一台设备
|
||||
shot = await c.call_tool("de_screenshot", {"serial": serial})
|
||||
img = shot.data["data"]["image"] # base64 JPEG(多模态模型可直接看图)
|
||||
img = shot.data["data"]["image"] # base64 JPEG(给多模态模型看)
|
||||
await c.call_tool("de_tap_text", {"serial": serial, "text": "搜索"})
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## AI 控制台(内置 Agent)
|
||||
> 给外部数字员工的使用约定与红线,另见 [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md)。
|
||||
|
||||
Web 端「AI 控制台」Tab 内建的 Agent(`mcp_agent/`,OpenAI 兼容协议:DeepSeek 等)也通过本 Server 的同一批工具跑任务:
|
||||
---
|
||||
|
||||
- 流式输出 + 每步 MCP 工具调用实时展示(含截图缩略)
|
||||
- 多轮会话记忆(同会话上下文保留,新建会话清空)
|
||||
- **自进化经验记忆**:一轮成功操作会被提炼成「配方」存入 `agent_experience` 表,下次相似任务自动注入参考(命中/写入均有 🧠 提示卡)
|
||||
- 模型与 API Key 在 AI 控制台右上角 ⚙ 配置,存平台 `app_meta`
|
||||
## 8. AI 控制台(内置 Agent)
|
||||
|
||||
> **依赖提示**:AI 控制台依赖本 MCP Server(默认 `http://127.0.0.1:8033/mcp`)。若未启动,发起指令会直接失败——平台已把 SDK 的含糊报错(`Server returned an error response` 等)映射为明确文案:**「MCP server(8033) 不可达 …」**。
|
||||
> 本机(非容器)需手动拉起:
|
||||
> `MCP_ALLOW_WRITE=1 MCP_PLATFORM_USER=admin MCP_PLATFORM_PASS=<平台密码> MCP_AUDIT_FILE=data/mcp_audit.log python -m mcp_server.mcp_server`
|
||||
> 220 生产容器由 `scripts/start.sh` 自动拉起(`MCP_ENABLED=0` 会关)。
|
||||
Web 的「AI 控制台」Tab 内建 Agent(`mcp_agent/`,OpenAI 兼容模型)用的就是本 Server 的同一批工具:
|
||||
|
||||
- 流式输出 + 每步 MCP 调用实时展示(含截图缩略)
|
||||
- 多轮会话(同会话保留上下文)
|
||||
- **自进化记忆**:经验库(任务配方)+ 动作库(命名动作),相似任务自动注入参考
|
||||
- 模型与 Key 在控制台右上角 ⚙ 配置(存平台 `app_meta`)
|
||||
|
||||
> **依赖提示**:AI 控制台依赖本 MCP Server(默认 `http://127.0.0.1:8033/mcp`)。未启动时平台会把 SDK 的含糊报错映射成明确文案「**MCP server(8033) 不可达 …**」。
|
||||
|
||||
---
|
||||
|
||||
> **维护约定**:新增 / 修改 / 删除任何一个 MCP 工具或相关配置,必须**同步更新本文与 [MCP_DESIGN.md](MCP_DESIGN.md)**(doc 同步红线)。
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
# MCP 手机控制(Mobile Control MCP Server)设计文档
|
||||
|
||||
> 分支:dev | 状态:设计稿/演进(实现现状以 doc/MCP.md 为准) | 日期:2026-09-04
|
||||
> 状态:**设计稿 / 演进记录**——凡与实现不符处,以 [MCP.md](MCP.md)(使用手册,含 19 个工具的权威清单)与 `mcp_server/` 代码为准。
|
||||
> 本文保留设计取舍与里程碑,便于回溯"为什么这么做";文中的"演进备选"均**未落地**。
|
||||
> 最后核对:2026-09-10(对照 dev 现状)。
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
@@ -25,7 +27,7 @@
|
||||
```
|
||||
┌──────────────┐ MCP (Streamable HTTP / stdio) ┌──────────────────┐
|
||||
│ 多模态 AI │ ────────────────────────────────▶ │ MCP Server │
|
||||
│ Claude Desktop│ tools + image content block │ (220 独立进程) │
|
||||
│ Claude Desktop│ tools + image content block │ (与平台同容器) │
|
||||
│ Claude Code │ ◀──────────────────────────────── │ mcp_server/ 包 │
|
||||
└──────────────┘ 截图图像 / JSON 结果 └────────┬─────────┘
|
||||
│ 内部调用
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# auto_control 文档总索引
|
||||
|
||||
本目录是 `auto_control`(Android 多设备自动化任务平台)的**唯一权威文档源**。代码即事实,文档与代码不一致时以代码为准,并**顺手把文档改对**(见文末维护约定)。
|
||||
|
||||
> 项目名称统一为 **auto_control**(历史文档里出现过的 `platform-tools` 均为旧名,已全部改名)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 文档地图
|
||||
|
||||
| 文档 | 内容 | 主要读者 |
|
||||
|------|------|---------|
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | **架构详解**:分层、启动装配顺序、线程与并发模型、设备生命周期、任务调度链路、状态机、关键设计决策与扩展点 | 所有开发者(先读这篇) |
|
||||
| [DATA_MODEL.md](DATA_MODEL.md) | **数据模型**:SQLite 表与字段、schema 迁移、非模型表、`app_meta` 配置键、数据目录、备份覆盖清单 | 后端开发、运维 |
|
||||
| [API.md](API.md) | **HTTP 接口全量**:按蓝图分组的路由表、鉴权、请求/响应示例、非 JSON 响应、错误分支 | 前端开发、外部接入 |
|
||||
| [TASK_DEV.md](TASK_DEV.md) | **任务与步骤开发**:TaskType/TaskJob 概念、18 种步骤全表、选择器与定位、自定义动作、新增任务类型模板 | 写任务的开发 |
|
||||
| [MCP.md](MCP.md) | **MCP 手机控制手册**:19 个 `de_*` 工具用法、写操作门控、坐标换算、接入示例 | 接入方、数字员工 |
|
||||
| [MCP_DESIGN.md](MCP_DESIGN.md) | **MCP 设计文档**:边界划分、错误码、白名单/审计设计、演进方向 | 平台开发者 |
|
||||
| [AI_CONSOLE.md](AI_CONSOLE.md) | **AI 控制台**:会话/SSE、经验库、动作库、巡检、Markdown 渲染、推理链、token 统计 | 使用者、平台开发者 |
|
||||
| [AI_TASK_GEN.md](AI_TASK_GEN.md) | **AI 建任务**:设计稿与里程碑(P0 未实现,属规划) | 平台开发者 |
|
||||
| [DEPLOY.md](DEPLOY.md) | **部署与运维**:环境准备、生产容器、数据备份导出/导入、故障排查 | 运维、部署者 |
|
||||
| [DEVELOPMENT.md](DEVELOPMENT.md) | **开发手册**:git 流程、技术红线、本地开发与调试、常见开发任务、文档同步约定 | 所有开发者 |
|
||||
| [STF_REMOVAL.md](STF_REMOVAL.md) | **历史记录**:摘除 OpenSTF 的迁移过程(阶段 0-3) | 追溯背景时参考 |
|
||||
| [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md) | 给 StaffDeck 数字员工的知识库(MCP 接入/工具/约定/红线) | 外部 AI 接入方 |
|
||||
| [staffdeck/JOB_SPEC.md](staffdeck/JOB_SPEC.md) | 数字员工岗位说明(岗位描述/看板摘要/执行约束) | 外部 AI 接入方 |
|
||||
| [backlog/TODO.md](backlog/TODO.md) | 已确认但暂缓的待办(含已知问题) | 所有开发者 |
|
||||
|
||||
项目根目录的 [README.md](../README.md) 是**项目总览与快速上手**(面向第一次接触项目的人),细节都在本目录。
|
||||
|
||||
---
|
||||
|
||||
## 2. 推荐的阅读路径
|
||||
|
||||
| 你的目的 | 按顺序读 |
|
||||
|---------|---------|
|
||||
| **第一次接触项目** | 根 [README.md](../README.md) → [ARCHITECTURE.md](ARCHITECTURE.md) → [DATA_MODEL.md](DATA_MODEL.md) |
|
||||
| **搭环境跑起来** | 根 [README.md](../README.md) 的「快速上手」→ [DEPLOY.md](DEPLOY.md) |
|
||||
| **写/改任务** | [TASK_DEV.md](TASK_DEV.md) → [ARCHITECTURE.md](ARCHITECTURE.md) §任务调度 |
|
||||
| **改后端/前端** | [DEVELOPMENT.md](DEVELOPMENT.md)(流程+红线+本地开发)→ [ARCHITECTURE.md](ARCHITECTURE.md) → [API.md](API.md) |
|
||||
| **对外提供手机控制** | [MCP.md](MCP.md) → [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md) |
|
||||
| **排故障** | [DEPLOY.md](DEPLOY.md) §故障排查 → [DEVELOPMENT.md](DEVELOPMENT.md) §调试 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 文档维护约定(红线)
|
||||
|
||||
> 与「doc 同步红线」一致:**任何功能/配置/接口/表结构的增删改,必须在同一个 commit 里同步更新对应文档。**
|
||||
|
||||
| 改动类型 | 必须同步的文档 |
|
||||
|---------|--------------|
|
||||
| HTTP 接口(新增/改参数/改返回/改鉴权) | [API.md](API.md) |
|
||||
| 数据库表/字段/迁移 | [DATA_MODEL.md](DATA_MODEL.md) + [ARCHITECTURE.md](ARCHITECTURE.md) |
|
||||
| **新增持久化表** | 还要登记进 `core/system_backup.py` 的 `SUMMARY_TABLES` + [DEPLOY.md](DEPLOY.md) §3.5(**备份覆盖红线**) |
|
||||
| `config.py` / `.env` 键 | [DEVELOPMENT.md](DEVELOPMENT.md) 配置速查 + [DEPLOY.md](DEPLOY.md) + `.env.example` |
|
||||
| 页面 Tab / 子分栏 / 前端 JS 拆分 | [ARCHITECTURE.md](ARCHITECTURE.md) §前端 + [DEVELOPMENT.md](DEVELOPMENT.md) |
|
||||
| 任务类型 / 步骤 schema | [TASK_DEV.md](TASK_DEV.md) |
|
||||
| 常驻线程 / 进程装配 | [ARCHITECTURE.md](ARCHITECTURE.md) §线程与并发 |
|
||||
| MCP 工具 | [MCP.md](MCP.md) + [MCP_DESIGN.md](MCP_DESIGN.md) |
|
||||
| 对外接入约定 | [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md) |
|
||||
| 暂缓项 / 已知问题 | [backlog/TODO.md](backlog/TODO.md)(完成时移出并同步相关文档) |
|
||||
|
||||
**新增文档时**:在本文 §1 表格里登记一行,并在根 README 的「更多文档」里加链接——否则等于没写。
|
||||
|
||||
**历史文档不追改**:[STF_REMOVAL.md](STF_REMOVAL.md) 是迁移阶段的历史记录,只增不改,不随现状改写。
|
||||
+340
-1063
File diff suppressed because it is too large
Load Diff
+109
-32
@@ -1,54 +1,131 @@
|
||||
# 待完成项(Backlog)
|
||||
# 待完成项 / 已知问题(Backlog)
|
||||
|
||||
> 用途:记录**已确认但暂缓**的功能/优化,避免丢线索。完成时应移出本文件,并按「doc 同步红线」更新对应文档。
|
||||
> 维护:新增条目写清 **背景 / 期望 / 涉及文件 / 验收**;完成后删除条目并在相关 doc 体现。
|
||||
> 用途:记录**已确认但暂缓**的功能、优化与**已知缺陷**,避免丢线索。完成时移出本文件,并按「doc 同步红线」更新对应文档。
|
||||
> 维护:新条目写清 **背景 / 期望 / 涉及文件 / 验收**;缺陷写清 **现象 / 复现 / 影响**。
|
||||
> 相关:[doc/README.md](../README.md)(文档索引与维护约定)。
|
||||
|
||||
---
|
||||
|
||||
## 设备与连接
|
||||
## A. 已知缺陷(尚未修)
|
||||
|
||||
- [ ] **adb 远程终端加「目标切换」(本机 / 220)**
|
||||
- 背景:终端默认走 `.env` 的 `ANDROID_ADB_SERVER_ADDRESS=192.168.20.220`(adb 客户端生效,非本项目代码读取),因此终端里看不到**本机 USB 设备**(实测本机 adb 有 `bebeoft4ztgiskba`,平台终端只显示 220 侧的 `192.168.20.206:5555`)。
|
||||
- 期望:`web/tools_api.py` 的 `/api/adb/devices`、`/api/adb/cmd` 支持 `target=local|remote`;前端 `static/admin/tools.js` 加下拉;默认值按需。
|
||||
- 涉及:`web/tools_api.py`、`static/admin/tools.js`、`doc/API.md`、`doc/DEPLOY.md`、`doc/DEVELOPMENT.md`(配置速查)。
|
||||
- 验收:切换「本机」时终端能列出本机 USB + 本机已连 `IP:5555`;切「220」时列出 220 侧设备;`adb cmd` 同样生效。
|
||||
### A1. `GET /locate` 返回 500(设备定位大字页打不开)
|
||||
|
||||
- **现象**:访问 `/locate?serial=…` 直接 500。
|
||||
- **原因**:`web/monitor.py` 的 `locate_page()` 用了 `render_template_string` 与 `_esc`,但该文件只导入了 `render_template`,也没有 `_esc` 定义。
|
||||
- **复现**:`curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:18050/locate?serial=1.2.3.4:5555"` → 500(路由扫描脚本已确认)。
|
||||
- **影响**:「工具 → 设备池管理 → 定位」在设备上打开的大字页不可用(亮屏那半仍生效)。
|
||||
- **涉及**:`web/monitor.py`(补 import / 补转义实现,或改用 `render_template` + 模板文件)。
|
||||
- **验收**:`/locate` 返回 200 且页面正确显示 serial/IP;纳入回归脚本。
|
||||
|
||||
### A2. `POST /api/device/locate`(`show=true`)失败
|
||||
|
||||
- **现象**:带 `show=true` 调用时抛异常 → 502。
|
||||
- **原因**:`web/monitor.py` 用了 `urllib.parse.quote` 但未 `import urllib`。
|
||||
- **影响**:无法远程把设备浏览器打开到定位页。
|
||||
- **涉及**:`web/monitor.py`。**验收**:`show=true` 时设备浏览器打开定位页且返回 200。
|
||||
|
||||
### A3. CSRF 实际未启用
|
||||
|
||||
- **现象**:`/api/csrf` 会签发 token、前端所有非 GET 请求都会带 `X-CSRF-Token`,但服务端**从不校验**。
|
||||
- **原因**:`web_server.py` 只 `from web.auth import _csrf_protect`,**没有注册** `app.before_request(_csrf_protect)`;而 `web/auth.py` 的 docstring 声称"由 web_server 注册"。
|
||||
- **影响**:CSRF 防护形同虚设(同网段内可伪造写请求)。
|
||||
- **涉及**:`web_server.py`(注册钩子)+ 需要回归所有写接口(前端、脚本、MCP 的 `platform_client` 都已带 token,风险点是第三方直接调 API)。
|
||||
- **验收**:注册后所有写接口在缺 token 时返回 403,且前端/MCP 全链路正常。
|
||||
|
||||
### A4. 路由回归脚本在 Windows 上不可用
|
||||
|
||||
- **现象**:`python scripts/regression_test.py` 直接报 `AttributeError: module 'signal' has no attribute 'alarm'` 退出。
|
||||
- **原因**:脚本用了 Unix 专有的 `signal.alarm`(第 29 行),docstring 里的示例路径也是 `.venv/bin/python`。
|
||||
- **影响**:本机(Windows)开发时没有"一条命令扫全接口 500"的手段——A1/A2 这类 import 遗漏本可被它第一时间发现。
|
||||
- **涉及**:`scripts/regression_test.py`(把 `signal.alarm` 换成跨平台超时,或 `hasattr` 守卫)。
|
||||
- **验收**:Windows 与 Linux 都能跑;能扫出 A1。
|
||||
|
||||
### A5. 前端 `--card-line` 变量未定义
|
||||
|
||||
- **现象**:AI 控制台的会话/经验/动作模态框边框失效(`border:1px solid var(--card-line)` 解析为无效值)。
|
||||
- **原因**:该变量只在 `templates/admin/wall.html` 的 `:root` 定义,`monitor.html` 的 `:root` 里没有,但被引用了 5 处。
|
||||
- **涉及**:`templates/admin/monitor.html`(`:root` 补 `--card-line:#232936`)。
|
||||
|
||||
### A6. `monitor.js` 引用了未定义变量 `countParts`
|
||||
|
||||
- **现象**:进度为 0 的边界分支会抛 `ReferenceError`(被外层 `try/catch` 吞掉,用户无感)。
|
||||
- **涉及**:`static/admin/monitor.js`(该分支的进度渲染)。
|
||||
|
||||
### A7. 其它小问题(低优先)
|
||||
|
||||
- `static/admin/custom.css` 是**死文件**(无任何引用)。
|
||||
- `templates/admin/monitor.html` 里 `.agent-shell` 有**完全重复的 CSS 声明**(后者覆盖前者的 `min-height`)。
|
||||
- 设备表空态 `colspan="11"` 与实际列数(10)不符。
|
||||
- `core/device_worker.py` 的 `_u2_connect_remote` 有两个结构相同的 `except Exception`,**后者是死代码**。
|
||||
- `core/ssh_client.py` **全项目无人 import**(`STF_SSH_*` 配置也只被它读取);要么接上用途,要么删除。
|
||||
- 若干按功能域拆包时留下的**重复函数定义**:`_merged_device_list`(`web/common.py` / `apks_api.py` / `tools_api.py` 各一份)、`tools_api.py` 的三个辅助函数、`monitor.py` 的屏相关函数、`tasks_api.py` 的 `_job_next_run`(各定义两次)。
|
||||
|
||||
---
|
||||
|
||||
## B. 设备与连接
|
||||
|
||||
- [ ] **adb 远程终端加「目标切换」(本机 / 220)**
|
||||
- 背景:终端默认走 `.env` 的 `ANDROID_ADB_SERVER_ADDRESS=192.168.20.220`(adb 客户端生效,非本项目代码读取),因此终端里看不到**本机 USB 设备**。
|
||||
- 期望:`/api/adb/devices`、`/api/adb/cmd` 支持 `target=local|remote`;前端 `tools.js` 加下拉。
|
||||
- 涉及:`web/tools_api.py`、`static/admin/tools.js`、`doc/API.md`、`doc/DEVELOPMENT.md`(配置速查)。
|
||||
- 验收:切「本机」能列本机 USB + 已连 `IP:5555`;切「220」列 220 侧设备;`adb cmd` 同样生效。
|
||||
|
||||
- [ ] **`.env` 的 adb 路由键要在文档里说明(或去掉)**
|
||||
- 背景:`ANDROID_ADB_SERVER_ADDRESS/HOST/PORT` 会被 `.env` 注入 `os.environ`,**adb 客户端**据此把所有 adb 调用指向 220;`ANDROID_ADB_SERVER_HOST` 是非标准键(adb 不认)。
|
||||
- 期望:在 `doc/DEVELOPMENT.md` 配置速查 / `doc/DEPLOY.md` 写明该行为与取舍;或本地去掉这三行改走本机 adb(`USB_ADB_HOST` 保留,平台对 220 的 USB 访问走显式 `-H/-P`)。
|
||||
- `ANDROID_ADB_SERVER_ADDRESS/HOST/PORT` 会被 `.env` 注入 `os.environ`,**adb 客户端**据此把所有调用指向 220;`ANDROID_ADB_SERVER_HOST` 是非标准键(adb 不认)。
|
||||
- 期望:在配置速查里写明该行为与取舍,或本地去掉这三行改走本机 adb。
|
||||
|
||||
- [ ] **`.gitignore` 漏项**:`data/mcp_audit.log` 与 `data/uiauto.pid` 未被忽略(`git status` 会显示为未跟踪);`data/apks/` 目录无占位文件。
|
||||
|
||||
- [ ] 220 的 Tailscale / adb server(5037) 可达性巡检(曾出现连不通超时)。
|
||||
|
||||
## 编辑器 / 元素抓取
|
||||
---
|
||||
|
||||
- [ ] **未命中要"明确提示",别混成"执行了这步"**
|
||||
- 背景(2026-09-10):click/long_click/wait_el/swipe_until/if_el 未命中时只写 `[WARNING] ... 未找到元素`,前端进度仍按"执行了一步"计数、任务日志 INFO 行也照常打 → 用户看到像"成功",实际没点(db03f16e 案例就是这么误判的)。
|
||||
- 期望:执行器把**未命中**作为该步结果(✗)返回并计入统计(如"未命中 N 步");前端任务进度/日志**标红 ✗** 并给出所用选择器;任务结束汇总未命中步骤数。
|
||||
- 涉及:`tasks/generic/task.py`(各 handler 返回/记录命中结果)、`static/admin/tasks.js`/`monitor.js`(渲染)、`doc/API.md`(如需新增字段)、`doc/TASK_DEV.md`。
|
||||
## C. 编辑器 / 元素抓取
|
||||
|
||||
- [ ] **未命中要「明确提示」,别混成「执行了这步」**
|
||||
- 背景(2026-09-10):click/long_click/wait_el/swipe_until/if_el 未命中时只写 `[WARNING] ... 未找到元素`,前端进度仍按"执行了一步"计数 → 用户看到像"成功",实际没点(db03f16e 案例就是这么误判的)。
|
||||
- 期望:执行器把**未命中**作为该步结果(✗)返回并计入统计;前端标红 ✗ 并给出所用选择器;任务结束汇总未命中步骤数。
|
||||
- 涉及:`tasks/generic/task.py`、`static/admin/{tasks,monitor}.js`、`doc/TASK_DEV.md`。
|
||||
|
||||
- [ ] **元素抓取(点击元素)超时——部分设备 dump 慢于平台超时**
|
||||
- 背景(2026-09-10 实测):`core/uiauto_helper.py:24` `_TIMEOUT=(1,8)`;`192.168.20.72:5555` dump 2.4s 正常,而 `192.168.20.206:5555` 与 USB 设备 dump **~18s** → 平台报「uiauto2 请求超时」,编辑器抓不到元素、无法选取/回填/测试点击。设备本身正常(直接请求 uiautodev 45s 内 HTTP 200)。
|
||||
- 期望:提高读超时(如 `(2, 30)`)+ 前端 `/api/uiauto/elements` 加载中给明确提示;可选加「同一界面短时缓存」或「抓取前提示切到轻界面」。
|
||||
- 涉及:`core/uiauto_helper.py`、`web/tasks_api.py`(/api/uiauto/elements)、`static/admin/editor.js`、`doc/API.md`、`doc/ARCHITECTURE.md`(§3.8)。
|
||||
- [ ] **元素抓取超时——部分设备 dump 慢于平台超时**
|
||||
- 背景(2026-09-10 实测):`core/uiauto_helper.py` 的 `_TIMEOUT=(1,8)`;某设备 dump **~18s** → 平台报「uiauto2 请求超时」,编辑器抓不到元素(设备本身正常)。
|
||||
- 期望:读超时提到 `(2, 30)` + 前端加载中有明确提示;可选"同界面短时缓存"。
|
||||
- 涉及:`core/uiauto_helper.py`、`web/tasks_api.py`、`static/admin/editor.js`、`doc/ARCHITECTURE.md`。
|
||||
|
||||
- [ ] **序号型 XPath / 界面就绪 的防呆**(2026-09-10 db03f16e 案例)
|
||||
- 背景:`(//*[@resource-id="…"])[6]` 依赖"抓取那一刻该属性有 ≥6 个实例";运行时界面不同(App 仍在闪屏页)→ 序号必然失配。
|
||||
- 期望:① `open_app` 提示勾选「等待首页」;② 抓取器对**带序号**的选择器加醒目提示;③ 抓取弹窗提醒"请先把设备停在任务运行到该步时的同一界面再抓"。
|
||||
- 涉及:`core/uiauto_helper.py`、`static/admin/editor.js`、`tasks/generic/task.py`、`doc/TASK_DEV.md`。
|
||||
|
||||
- [ ] 自定义动作支持 `action_ref` 引用型节点(现状:拖入画布会**展开成 group**,改动需同步执行器 + 编辑器)。
|
||||
|
||||
- [ ] **序号型 XPath / 界面就绪 的防呆**(2026-09-10 db03f16e 案例)
|
||||
- 背景:`(//*[@resource-id="…"])[6]` 这类**带序号**的选择器依赖"抓取那一刻该属性有 ≥6 个实例";运行时界面不同(实测抖音冷启动 5s 后仍在 `.splash.SplashActivity`,`content_layout` 只有 5 个)→ 序号必然失配。且步骤只有 `open_app(wait_home=false)+wait`,没有"等首页就绪"。
|
||||
- 期望:①`open_app` 默认/提示勾选「等待首页」,或在编辑器给"点击前自动等某元素"的建议;②抓取器对**带 `#k/N` 序号**的选择器加醒目提示("界面变化会失配,优先用文字/唯一 id");③抓取弹窗提醒"请先把设备停在任务运行到该步时的同一界面再抓"。
|
||||
- 涉及:`core/uiauto_helper.py`、`static/admin/editor.js`(picker 提示)、`tasks/generic/task.py`(open_app 等待策略)、`doc/TASK_DEV.md`。
|
||||
---
|
||||
|
||||
## AI 控制台 / 经验与动作
|
||||
## D. AI 控制台 / 经验与动作
|
||||
|
||||
- [ ] 「🧠 经验库 / 🎬 动作库」合为一个面板(标签页切换)。
|
||||
- [ ] AI 建任务 P0:平台级 MCP 工具(只读清单 `task_types/groups/pool` + `submit_task(draft)` 校验)+ 把**动作库**当作 generic_steps 的预制件复用(见 `doc/AI_TASK_GEN.md` §9)。
|
||||
- [ ] 新增 MCP `de_screen_text`(文本化看屏:前台包名 + screen_state + 可点元素文本 + OCR),并把操作纪律写进工具 description(对外部客户端尤其有用)。
|
||||
- [ ] 经验召回改进:现为 bigram + `ORDER BY hits`(候选只看 top50、hits 对所有命中行回写 → 马太效应);改为对称相似度/更合理候选集,hits 仅在真正注入时 +1。
|
||||
- [ ] AI 建任务 P0:平台级 MCP 工具(只读清单 `task_types/groups/pool` + `submit_task(draft)` 校验)+ 把**动作库**当作 generic_steps 的预制件复用(见 [AI_TASK_GEN.md](../AI_TASK_GEN.md) §9)。
|
||||
- [ ] 新增 MCP `de_screen_text`(文本化看屏:前台包名 + screen_state + 可点元素文本 + OCR),并把操作纪律写进工具 description。
|
||||
- [ ] 经验召回改进:现为 bigram + `ORDER BY hits`(候选只看 top50、hits 对所有命中行回写 → 马太效应);改为对称相似度 / 更合理候选集,`hits` 仅在真正注入时 +1。
|
||||
- [ ] 经验/动作入库依赖模型蒸馏成功:加失败重试与可视化观测(现在只在日志里)。
|
||||
|
||||
## 其他
|
||||
---
|
||||
|
||||
- [ ] `platform-tools` → `auto_control` 命名统一(README/服务名/单元名,待定)。
|
||||
- [ ] STF 容器状态确认:`doc/DEVELOPMENT.md`(已停用)与 `doc/STF_REMOVAL.md`(待人工确认)说法矛盾,需以 220 实际为准后统一。
|
||||
- [ ] MCP 白名单语义缺口:`MCP_ALLOWED_SERIALS` 为空时并未按设计限制为"平台设备池内"(当前只校验非空),见 `doc/MCP.md`;如需收紧需补实现。
|
||||
## E. 工程化 / 文档
|
||||
|
||||
- [ ] **MCP 白名单语义缺口**:`MCP_ALLOWED_SERIALS` 为空时并未按设计限制为"平台设备池内"(当前只校验非空),见 [MCP.md](../MCP.md);如需收紧需补实现。
|
||||
- [ ] **`MCP_DESIGN.md` 里的"演进备选"(独立 mcp-server 容器)未落地**:已在文中标注,若确定不做可删除该节,避免后续误读。
|
||||
- [ ] **STF 容器状态确认**:`doc/DEVELOPMENT.md`(已停用)与 `doc/STF_REMOVAL.md`(待人工确认)说法矛盾,需以 220 实际为准后统一。
|
||||
- [ ] **`DISCOVERY_SUBNETS` 无 env 支持**:`config.py` 里是硬编码列表,其余发现配置走 `app_meta`;若要支持 `.env` 覆盖需补实现。
|
||||
|
||||
---
|
||||
|
||||
## F. 已完成(留档,便于追溯)
|
||||
|
||||
- [x] `platform-tools` → `auto_control` 命名统一(2026-09-10:文档/README/`scripts/pack.py` 产物名)。
|
||||
- [x] 删除抖音养号任务类型(`douyin_nurture`),平台只保留 `generic_steps`;残留旧类型任务改为**启动告警 + 执行时明确报错**。
|
||||
- [x] `generic_steps` 去掉默认步骤;空步骤任务执行时明确报错(不再静默空跑)。
|
||||
- [x] 重试耗尽时的 `last_error` 带上真实失败原因(不再只有"重试 N 次失败")。
|
||||
- [x] 监控页任务卡去掉「编辑/删除」,只留执行/停用 + 显示**覆盖设备**。
|
||||
- [x] 备份覆盖清单补 `agent_action` / `app_meta` + 导出/导入双向自检。
|
||||
- [x] AI 控制台:Markdown 渲染、推理链可折叠、token 用量显示。
|
||||
|
||||
@@ -152,3 +152,5 @@ P0 接收与澄清 → P1 选设备与预检(含"屏幕是否点亮/解锁") →
|
||||
- 知识库(工具/约定/红线):`doc/staffdeck/KNOWLEDGE_BASE.md`
|
||||
- 工具手册:`doc/MCP.md`;平台接口:`doc/API.md`
|
||||
- 安全与配置:`doc/DEPLOY.md`、`doc/DEVELOPMENT.md`
|
||||
- AI 控制台机制 / 架构 / 数据:`doc/AI_CONSOLE.md`、`doc/ARCHITECTURE.md`、`doc/DATA_MODEL.md`
|
||||
- 文档总索引:`doc/README.md`
|
||||
|
||||
@@ -248,4 +248,6 @@ P0 接收与澄清 → P1 选设备与预检(§6) → P2 到达起点 → P3 主
|
||||
## 12. 当前边界与相关文档
|
||||
- MCP 无任务 CRUD、无独立鉴权(靠网络隔离)。
|
||||
- `doc/MCP.md`(工具手册)|`doc/MCP_DESIGN.md`(设计与现状对照)|`doc/API.md`(REST 目录)
|
||||
- 平台内部机制:`doc/AI_CONSOLE.md`(AI 控制台)、`doc/ARCHITECTURE.md`(架构)、`doc/DATA_MODEL.md`(数据)
|
||||
- 文档总索引:`doc/README.md`
|
||||
- 岗位职责与授权分级:`doc/staffdeck/JOB_SPEC.md`
|
||||
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
"""打包脚本:把项目代码打成 zip 压缩包,排除运行时产物。
|
||||
|
||||
用法:python scripts/pack.py
|
||||
输出:项目根目录下 platform-tools.zip
|
||||
输出:项目根目录下 auto_control.zip
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT = os.path.join(os.path.dirname(ROOT), "platform-tools.zip")
|
||||
OUT = os.path.join(os.path.dirname(ROOT), "auto_control.zip")
|
||||
|
||||
SKIP_DIRS = {"__pycache__", ".git", ".idea", "venv", ".venv", "node_modules"}
|
||||
SKIP_EXTS = {".pyc", ".pyo"}
|
||||
|
||||
@@ -922,7 +922,7 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
|
||||
<b>导出</b>当前系统数据为 zip:含 <code>users.db</code> 在线一致快照(<b>全部业务表</b>:用户 / 任务计划 / 设备分组 /
|
||||
自定义动作 / APK 记录 / 设备池 / 待连接设备 / AI 会话 / 经验库 / 经验巡检 / <b>动作库</b> / 系统配置)+ 可选的
|
||||
<code>apks/</code> 应用文件。恢复用:换机 / 整库迁移 / 出问题前留底。
|
||||
<br><span class="text-muted">清单见预览的「表数据行数」;新增持久化表须登记进备份覆盖清单(红线,见 doc/DEPLOY.md §3.5)。</span>
|
||||
<br><span class="text-muted">清单见预览的「表数据行数」;新增持久化表须登记进备份覆盖清单(红线,见 doc/DEPLOY.md §5.2)。</span>
|
||||
</div>
|
||||
<div class="toolbar" style="gap:8px">
|
||||
<label style="display:inline-flex;align-items:center;gap:4px;margin:0 6px">
|
||||
|
||||
Reference in New Issue
Block a user