背景:设备池原先拿 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);自动认领(指纹命中→迁址+保留名称+带指纹+未命中不误判);人工认领
(迁址+名称保留+分组与任务引用同步);浏览器验证设备池/断联表/待连接池三个界面
14 KiB
部署与运维(DEPLOY)
适用读者:部署与运维
auto_control的人。 相关文档:DEVELOPMENT.md(开发流程/红线)、DATA_MODEL.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 设备接入
设备需满足:
- 开启 USB 调试
- 转网络调试:USB 连上后
adb -s <serial> tcpip 5555(后台「工具 → adb 终端」有快捷命令) - 与平台网络互通(生产用 Tailscale,设备与部署机在同一 tailnet,serial 形如
100.100.10.x:5555) - 在「工具 → 设备池管理」加入设备池(只连上 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 获取代码与依赖
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(真实环境变量优先)。
生产至少配:
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 §配置速查。
2.3 启动
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 做三件事
- 依赖就绪守卫:检查 9 个模块 +
cv2可用性;全部可用就跳过安装 - 依赖安装与修复(仅在需要时):
pip install -r requirements.txt→ 卸载 GUIopencv-python→ 装opencv-python-headless>=4.8,<5→ 再验 cv2,仍异常则--force-reinstall - 拉起 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 发布流程
cd /mnt/data/openstf/auto_control
git fetch origin && git checkout main && git pull --ff-only origin main
docker restart python-app
重启后验证:
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 §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 反代
location / { proxy_pass http://127.0.0.1:18050; proxy_set_header Host $host; }
7. 升级与迁移
7.1 代码升级
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 跨机迁移(换部署机)
- 新机装依赖、放好
bin/adb/与 同一把 adb key - 复制
.env - 老机「系统 → 数据备份」导出 zip → 新机「导入恢复」上传并应用
- 重启新机服务(恢复任务在
init_db之前被消费) - 核对设备池在线状态与任务列表
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 |
| 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。