Files
auto_control/doc/DEPLOY.md
T
butubb 0a1b4d6122 feat: 设备身份改为「名称 + 指纹」——更换 IP 自动认领,分组/任务引用自动同步
背景:设备池原先拿 serial(IP)当身份。设备一换 IP(DHCP 重新分配)旧记录就成了连不上的
僵尸条目(表现为"断联·自动重连中"但设备并没关机),分组与 serial 模式的任务还吊着死地址。
2026-09-11 实际发生:.70 变成 .71、.72 消失,平台两个条目永远连不上。

实现
- **设备名称必填且唯一**:加入设备必须填名称;库层面用部分唯一索引兜底
  (ux_device_name / ux_device_fingerprint,WHERE 非空 → 兼容历史空值),管理页可改名
- **设备指纹**(ro.serialno):网络设备添加/确认/扫描/采集型号时自动读取;
  身份三层拆分——名称(人可读,稳定)、指纹(机器识别,稳定)、serial(当前地址,可变)
- **自动认领**:添加或确认设备时指纹命中池中已有记录 → 迁移原记录到新地址
  (名称/型号/备注/启用状态/添加时间全保留),不新增条目
- **人工认领** `POST /api/devices/pool/relocate`:旧地址已断联、指纹没采过时的兜底——
  人工指认"这条就是那台,现在在 X",迁移并同步引用
- **引用同步**:认领/迁址时把 device_group.serials 与 task_job.target.serial 的旧地址
  换成新地址。⚠️ 必须同时改**内存**:分组/任务在 TaskManager 里另有内存副本且调度用内存对象,
  只改库不重启不生效 → device_pool 迁址后回调 TaskManager.sync_device_serial
  (装配层用 set_move_hook 注册;device_pool 不能反向 import task_manager,会循环依赖)
- **前端**:待连接池新增「识别」列(指纹命中时提示"≈ 名称(原 IP)",按钮变「认领为 X」);
  设备池新增「名称/指纹」列与「改名/换地址」操作;断联设备表也加「换地址」(用户看到断联就在这里)
- 添加设备接口改用 adb_connect_light(单次短超时),避免不可达 IP 让请求卡 30s+;重名校验提前到 adb 之前

文档:API.md §6 重写(设备身份/认领/新接口)、DATA_MODEL.md(新列 + v5/v6 迁移 + 唯一索引)、
ARCHITECTURE.md §4.0(身份三层与引用同步的内存坑)、DEPLOY.md 排查表加"断联但没关机"条目

自测(全通过):名称必填/唯一/改名/重名拒绝(含库层面约束);指纹采集(真实读到 .71 的
gy7lskwkkvj7c6b6);自动认领(指纹命中→迁址+保留名称+带指纹+未命中不误判);人工认领
(迁址+名称保留+分组与任务引用同步);浏览器验证设备池/断联表/待连接池三个界面
2026-09-11 11:03:38 +08:00

289 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 部署与运维(DEPLOY)
> 适用读者:部署与运维 `auto_control` 的人。
> 相关文档:[DEVELOPMENT.md](DEVELOPMENT.md)(开发流程/红线)、[DATA_MODEL.md](DATA_MODEL.md) §数据目录、[ARCHITECTURE.md](ARCHITECTURE.md) §启动装配。
---
## 1. 环境准备
### 1.1 主机与 Python
- **Python 3.10+**(推荐 3.12)
- Windows / Linux / macOS 均可;生产跑在 Linux 容器里
- `pip install -r requirements.txt`
项目自带的 **adb 二进制**在 `bin/adb/`(Windows 为 `adb.exe` + 依赖 dll;Linux/macOS 为 `adb`,需 `chmod +x`)。`config.py` 按平台自动选路径,换成自己的 adb 覆盖该目录即可。
### 1.2 设备接入
设备需满足:
1. 开启 **USB 调试**
2. 转网络调试:USB 连上后 `adb -s <serial> tcpip 5555`(后台「工具 → adb 终端」有快捷命令)
3. 与平台**网络互通**(生产用 Tailscale,设备与部署机在同一 tailnet,serial 形如 `100.100.10.x:5555`)
4. 在「工具 → 设备池管理」加入设备池(**只连上 adb 不算入池,不参与调度**)
**USB 设备**(serial 无冒号)经部署机的 adb server 驱动:
- adb 容器需 **host 网络模式**(5037 监听所有网卡,含 Tailscale)
- 平台通过 `USB_ADB_HOST`(默认 `100.100.10.1`)/ `USB_ADB_PORT`(默认 5037)访问它
> **adb key 必须沿用既有 key**(设备信任它)。换 key 会让全部设备变 `unauthorized`。
### 1.3 端口
| 端口 | 服务 | 说明 |
|------|------|------|
| 18050 | Web 后台 | `config.py` 的 `WEB_PORT`;绑定失败会自动回退候选端口 |
| 8033 | MCP Server | `MCP_HTTP_PORT`;由 `scripts/start.sh` 拉起 |
| 20242 | uiautodev | 元素抓取;`web_server.py` 启动时自动 Popen |
| 5555 | 设备 adb | 设备侧端口 |
---
## 2. 安装与启动
### 2.1 获取代码与依赖
```bash
cd auto_control
pip install -r requirements.txt
```
主要依赖: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
```
启动成功日志:
```
[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/
```
默认账号 **admin / admin123**(**登录后立即改密**)。
---
## 3. 生产部署(220 容器)
生产环境固定为 **192.168.20.220** 的 `/mnt/data/openstf/auto_control`,由 `docker-compose` 的 **`python-app`** 容器运行:
| 项 | 值 |
|----|----|
| 宿主目录 | `/mnt/data/openstf/auto_control`(git 仓库,分支 `main`) |
| 容器挂载 | 宿主目录 → 容器 `/app` |
| 网络 | `network_mode: host` |
| 入口 | `scripts/start.sh`(`command`) |
| 重启策略 | `restart: unless-stopped` |
| 同级容器 | `adb`(USB 远程 adb server,5037)、以及该机器上其它无关服务 |
> ⚠️ **`docker-compose.yaml` 不在仓库里**,只存在于 220 上;仓库里只有 `scripts/start.sh` 这一半。
### 3.1 容器入口 `scripts/start.sh` 做三件事
1. **依赖就绪守卫**:检查 9 个模块 + `cv2` 可用性;全部可用就跳过安装
2. **依赖安装与修复**(仅在需要时):`pip install -r requirements.txt` → 卸载 GUI `opencv-python` → 装 `opencv-python-headless>=4.8,<5` → 再验 cv2,仍异常则 `--force-reinstall`
3. **拉起 MCP + 前台启动 Web**:
```
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
```
> **`scripts/supervise.sh` 只守护 web_server,不拉起 MCP**——容器场景不要用它替代 `start.sh`。
### 3.2 发布流程
```bash
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) |
| **设备"断联"但其实没关机** | 多数是**换了 IP**(DHCP 重新分配):设备池管理点「**换地址**」把记录迁到新地址(名称与分组/任务引用自动保留);已采集指纹的设备会被扫描自动识别为"已有设备换了地址",确认即认领 |
| 加了设备但不被调度 | 只连上 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`。