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:
2026-09-10 22:19:18 +08:00
parent f4b5316436
commit 24d57d3b96
18 changed files with 2664 additions and 3667 deletions
+81 -31
View File
@@ -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
# ==============================================================================
+276 -318
View File
@@ -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/`) | 设备连接与底层操作 |
+210
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+294 -363
View File
@@ -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) 配置速查 |
+246
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 结果 └────────┬─────────┘
│ 内部调用
+64
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+109 -32
View File
@@ -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 用量显示。
+2
View File
@@ -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`
+2
View File
@@ -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
View File
@@ -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"}
+1 -1
View File
@@ -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">