用户场景(他原话):一台手机登录 5 个抖音号、一共 5 台手机,每个任务只让其中一个
目标号评论;每天跑一次但不知道什么时候跑完,于是"一直重复跑" → 结果
"一个手机还没评论到,一个手机都评论两次了"。
**根因不是"单设备重复",是跨设备没有共享的判断 + 进度不可见。** 所以做两件事:
① 幂等;② 把"谁做过了、还差谁"摆到台面上(不然只能靠重跑确认,而重跑又在制造重复)。
- `core/models.py`:新表 `done_mark`(迁移账本补 v7)。**判据只有 `scope_key` 的
唯一索引**——多台设备会同时判断"没做过","先查后插"有竞态(两台都插),
唯一索引 + `INSERT ... ON DUPLICATE KEY`/`INSERT OR IGNORE` 的**受影响行数**才原子。
- `core/dedup.py`(新):`build_key`(`任务|身份|时间桶`)/ `check` / `mark` /
`list_marks`(带"今天做了几台/几个号"统计)/ `delete_mark` / `clear_job` / `purge_old`。
自建 app context(照 device_pool 的 `_ctx()`),任务线程/Web/清理都不用关心。
- 任务侧两个部件(**检查在前、记账在后**):
· `if_el` 新增条件类型 `selector_type="dedup"`:命中=这个身份做过了 → 走 then 分支。
身份元素在 `ident_type`/`ident_value`(留空 = 用设备 serial,一号一机场景)。
· 新步骤 `mark_done`「记为已做」(22 种步骤):放动作**成功之后**。
拆两步的用意:动作失败就不记账,下次重跑还会重试该设备 —— 失败不丢。
- 有效期(`dedup_reset` = day/all/hours)放**任务级**:检查与记账两处各填一份的话,
填不一致就算出两个 key、去重会**静默失效**,所以强制只配一处(编辑器顶部下拉)。
- 三条防误伤规则(都有测试兜着):
· 身份读不到 / 身份值过长 → **不去重、当没做过照常执行**。绝不能把"读不到"
当成空身份——那会让所有设备共用一个 key、第一台记账后其余全被误判成"做过"。
· `kind='all'`(只做一次)的记录**永不清理**(清了等于语义失效);清理只删 day/hours。
· 去重的两个易错点在保存时直接告警:身份元素两边不一致、有检查没记账/有记账没检查。
- 「任务 → 去重记录」新子分栏(`static/admin/dedup.js`):统计行 + 明细表 +
删单条(那个号重跑)/ 清空任务(整批重跑)。接口 3 个(GET/delete/clear,PERM_TASKS)。
- 每日 04:23 清理(挂现有 APScheduler),`TABLE_LABELS` 补中文名(备份覆盖自动派生)。
- AI 建任务草稿校验同步:`dedup` 走自己的规则(要 ident_value、xpath 前缀校验),
没填身份元素只警告不拦(用设备当身份是合法用法);普通条件空选择器仍然拦。
- 文档:TASK_DEV §4.6(去重专章 + App 内检测的兜底配方与它的三个局限)、
DATA_MODEL §2.9、API 三个接口、ARCHITECTURE(分层/装配/子分栏/JS 分工/清理)、
DEPLOY §5.2(15 张表)、步骤数 21→22 全库同步。
自测:单元 + 集成 33 项(**含 8 线程抢同一个身份、恰好一个成功**的原子性断言,
以及"all 记录不被清理""身份读不到不去重""清了能重跑")、
**真机端到端**(cs1 上"检查→动作→记账"跑两遍:第二遍被拦、换 serial 的"另一台设备"
同样被拦、删记录后能重跑)、草稿校验 5 项、GET 冒烟 56 路由 0 个 500。
(注:本分支基于 feat/if-el-multi-value,因为它俩都要改 task.py 的 STEP_TYPES 与
editor.js 的 STEP_LIB 同一区域,分开从 dev 拉必然冲突——这份是超集,合一次两份都进。)
364 lines
18 KiB
Markdown
364 lines
18 KiB
Markdown
# 部署与运维(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> # 不配则每次重启登录态失效
|
||
DEPLOY_ENV=prod # 声明"这是生产环境",与库名 auto_control 绑定
|
||
DB_HOST=<MySQL 主机> # 数据库目标(不配则回退 SQLite,生产会拒绝启动)
|
||
DB_USER=<账号>
|
||
DB_PASSWORD=<口令>
|
||
DB_NAME=auto_control
|
||
# USB_ADB_HOST=100.100.10.1 # USB 设备所在的部署机(默认值通常可用)
|
||
# TAILSCALE_API_KEY=<...> # 需要 Tailscale 管理功能时
|
||
# MCP_PLATFORM_PASS=<admin 的密码> # 改过 admin 密码必须同步,否则 MCP 登录失败
|
||
```
|
||
|
||
**防混库**(`DEPLOY_ENV` 与库名/库标签双向校验,配置项说明见 [DEVELOPMENT.md](DEVELOPMENT.md) §4.2
|
||
与 `.env.example` 的「数据库」段):启动时若
|
||
「`.env` 声明的环境」与「库名」或「库中登记的 `app_meta.deployment_env`」不符,**直接拒绝启动**并打印
|
||
两边分别是什么。每次启动还会打印一条横幅写明当前连的是哪个库(生产用 WARNING 级),
|
||
页面顶部也常驻一个环境徽标(生产红底 `PROD · <库>`,开发灰底 `DEV`),
|
||
**浏览器标签名也带环境前缀**(开发 `dev-设备自动化后台`,生产 `设备自动化后台`)——
|
||
同时开着两个环境时一眼分得清哪个标签是哪个。
|
||
两个逃生阀 `DB_ALLOW_ENV_MISMATCH` / `DB_ALLOW_SQLITE_FALLBACK` 默认关闭。
|
||
|
||
其余配置项与默认值见 [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` → 把当前库(MySQL 或回退模式的 SQLite)
|
||
整库一致快照成一份 SQLite 归档 → 打包 zip(`users.db` + `manifest.json` + 可选 `apks/*.apk`)
|
||
- **导入**:上传 zip/`.db` → 校验预览(完整性 / 必需表 / schema 版本 / 未登记表 / **来源环境**)
|
||
→ 确认后自动把当前库导出一份 `data/backups/pre_restore_*.zip`(安全网,可再导入回来)
|
||
→ 落 `data/restore_pending/` → **重启服务生效**
|
||
|
||
> **SQLite 在这里是"备份交换格式",不是运行时数据库。** 所以归档里永远是一个 `users.db`,
|
||
> 无论平台连的是 MySQL 还是 SQLite —— 前端接口、校验逻辑、老备份的兼容性都不用变,
|
||
> 也不依赖 `mysqldump` 这类外部二进制(容器是 `python:slim`,没有 MySQL 客户端)。
|
||
|
||
**恢复是怎么生效的**:重启时 `consume_pending_restore()` 在**单个事务内** `DELETE` 全表 +
|
||
分块 `INSERT`,任何一步失败就回滚——当前数据保持原样,不会留下半新半旧的库。
|
||
坏归档会被挪到 `data/backups/restore_failed_*/` 且不阻塞启动。
|
||
|
||
**跨环境导入默认拒绝**:备份的 manifest 里记了来源环境(`deployment_env`),
|
||
若与当前库环境不一致(例如拿生产备份灌 dev 库),预览会弹黄框告警、直接应用会被拒绝;
|
||
确需跨环境时在预览页勾选「允许跨环境导入」。
|
||
|
||
### 5.2 备份覆盖清单(红线)
|
||
|
||
覆盖清单 = `core/system_backup.py` 的 `SUMMARY_TABLES`,**由模型元数据自动派生**:
|
||
|
||
```python
|
||
SUMMARY_TABLES = tuple(sorted(t.name for t in db.metadata.tables.values()))
|
||
```
|
||
|
||
当前 15 张表:`app_meta` / `user` / `device_group` / `task_job` / `custom_action` /
|
||
`apk_file` / `device` / `pending_device` / `agent_conversation` / `agent_experience` /
|
||
`experience_audit` / `agent_action` / `device_install_log` / `task_step_log` / `done_mark`。
|
||
完整说明见 [DATA_MODEL.md](DATA_MODEL.md) §6。
|
||
|
||
> ⚠️ `task_step_log`(任务步骤明细)是会持续增长的表:它按 `KEEP_DAYS`
|
||
> (默认 14 天,见 `core/step_log.py`)自动清理,但备份包里会带上保留期内的全部行。
|
||
> 设备多、任务密时导出 zip 会明显变大——需要更小的包就调小那个常量。
|
||
> `done_mark`(去重账本)也会增长,但量级是"设备数 × 天数",可以忽略;它同样有保留期
|
||
> (默认 180 天,且 `kind='all'` 的记录**永不清理**)。
|
||
|
||
> **新增一张 ORM 表,就自动进了覆盖清单**,不可能再漏(2026-09-10 动作库 `agent_action`
|
||
> 曾因手工维护漏登记,数据其实在快照里,只是清单没列 → 被误判为"没有备份")。
|
||
> 还需要手工做的只有:给新表补 `TABLE_LABELS` 的中文标签。
|
||
> 导出侧有**覆盖自检**(登记表缺失 → `manifest.coverage_missing` + 日志告警);
|
||
> 导入侧有**反向自检**(备份含未登记表 → 预览告警)。
|
||
|
||
### 5.3 目录与手工备份
|
||
|
||
| 目录 | 用途 | 可否用环境变量改 |
|
||
|------|------|------------------|
|
||
| `data/backups/` | 导出临时 zip、`pre_restore_*.zip`(导入前安全网)、`restore_failed_*` | `DATA_BACKUP_DIR` |
|
||
| `data/restore_staging/` | 导入暂存(TTL 30 分钟自动清理) | `DATA_RESTORE_STAGING_DIR` |
|
||
| `data/restore_pending/` | 待生效恢复任务(重启时消费) | `DATA_RESTORE_PENDING_DIR` |
|
||
|
||
> 这三个环境变量主要给**自动化测试**用:测试必须把恢复目录指到临时位置,
|
||
> 否则测试造出来的"待生效恢复任务"会在服务下次重启时被当成用户的操作消费掉。
|
||
|
||
连 MySQL 时 `data/users.db` 及其 `-wal`/`-shm` **不再被读写**(只有回退模式才用),
|
||
可以留作历史归档。手工整目录备份 `data/` 仍可作兜底,但**整库恢复建议走内置功能**
|
||
(一致快照 + 预恢复备份 + 重启时事务替换)。
|
||
|
||
> ⚠️ 备份 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()` 建缺表 + `_sync_columns()` 补缺列(幂等,失败不阻塞启动,见 [DATA_MODEL.md](DATA_MODEL.md) §4)
|
||
- 旧 `groups.json` / `jobs.json` 首次启动自动迁移并归档为 `.migrated`
|
||
- **跨版本恢复**:用「导入恢复」上传旧库 → 预览会提示 schema 版本差异 → 应用后重启(新版本会自动补列/补空表)
|
||
|
||
#### SQLite → MySQL(一次性)
|
||
|
||
首次把平台从单文件 SQLite 换到 MySQL 时,用迁移脚本搬数据:
|
||
|
||
```bash
|
||
# 0) 先在 .env 里配好 DB_HOST/DB_USER/DB_PASSWORD/DB_NAME(或用 --target-url)
|
||
# 1) 干跑:审计源库 + 建目标 schema,不搬数据
|
||
python scripts/migrate_sqlite_to_mysql.py --sqlite data/users.db --env dev --dry-run
|
||
# 2) 正式搬(目标库已有的数据会被整表替换;单事务,失败自动回滚)
|
||
python scripts/migrate_sqlite_to_mysql.py --sqlite data/users.db --env dev
|
||
# 3) 复验(只比对不写)
|
||
python scripts/migrate_sqlite_to_mysql.py --sqlite data/users.db --env dev --mode verify
|
||
```
|
||
|
||
脚本做的事:源库完整性校验 → **列宽审计**(SQLite 不强制长度,MySQL 严格模式下超长会直接报错,
|
||
所以先拦下来)→ 建目标 schema(复用平台的 `create_all` + 补列 + 唯一索引)→ 逐表搬行
|
||
(单事务)→ **逐表行数 + 全行 SHA-256 比对** → 写库环境标签与 `schema_version`。
|
||
|
||
> `--env prod` 必须额外加 `--allow-prod`,交互终端还要手打 `prod` 确认(这是唯一会往生产库灌数据的入口)。
|
||
> 目标库若已登记为别的环境(`app_meta.deployment_env`),除非 `--force-env` 否则拒绝。
|
||
|
||
建库建账号见 `scripts/sql/init_mysql_5.7.sql`(utf8mb4 + `utf8mb4_bin`,两个库对应 dev/prod)。
|
||
|
||
### 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`。
|