MCP 客户端(streamable_http)在工具服务不可达/返回非 MCP 响应时只抛 'Server returned an error response',用户无法判断原因。改为: - 后端 web/agent_api.py:_execute 包裹 _load_tools/run_stream,识别连接类错误 (Server returned an error response/ConnectError/refused 等)后抛明确文案: 'MCP server(8033) 不可达:无法加载设备工具(<url>)。请确认 MCP server 已启动…' - 前端 static/admin/agent.js 加 _friendlyAgentError() 兜底映射并去掉 'RuntimeError:' 之类前缀,三处错误展示统一使用 - 文档同步:MCP.md(依赖提示+本机启动命令)、API.md(SSE error 文案说明)、 DEPLOY.md(故障排查新增一行) 实测:停掉 MCP 复现 → 前端显示明确文案;启动 MCP 后 AI 控制台正常完成任务
361 lines
14 KiB
Markdown
361 lines
14 KiB
Markdown
# 部署指南
|
||
|
||
本文介绍如何从零部署 `platform-tools` 设备自动化后台。
|
||
|
||
---
|
||
|
||
## 1. 环境准备
|
||
|
||
### 1.1 Python 环境
|
||
|
||
- **Python 3.10+**(推荐 3.12)
|
||
- 安装后确认 `python --version` 和 `pip` 可用
|
||
|
||
```bash
|
||
# 验证
|
||
python --version # 应输出 3.10+
|
||
pip --version
|
||
```
|
||
|
||
### 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 设备准备
|
||
|
||
设备需满足:
|
||
- 开启 **USB 调试**(设置 → 开发者选项)
|
||
- 转网络调试:USB 连上后 `adb -s <serial> tcpip 5555`(管理后台「adb 终端」有一键快捷命令)
|
||
- 设备加入 Tailscale(同一账号,获得 100.100.10.x IP)
|
||
- 在「工具 → 设备池管理」添加 `IP:5555`(自动尝试连接,任务运行时 u2 自动推送 atx-agent)
|
||
|
||
---
|
||
|
||
## 2. 安装部署
|
||
|
||
### 2.1 获取代码
|
||
|
||
```bash
|
||
# 方式一:直接拷贝项目目录
|
||
# 方式二:解压打包文件(python scripts/pack.py 生成的 zip)
|
||
```
|
||
|
||
### 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` 前自动消费恢复任务)
|
||
|
||
目录与常量:
|
||
|
||
| 目录 | 用途 |
|
||
|---|---|
|
||
| `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 端口说明
|
||
|
||
| 端口 | 服务 | 说明 |
|
||
|------|------|------|
|
||
| 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/`
|
||
|
||
---
|
||
|
||
## 5. 更新升级
|
||
|
||
### 5.1 代码更新
|
||
|
||
```bash
|
||
# 1. 停止服务
|
||
# 2. 替换代码文件(或解压新的 zip)
|
||
# 3. 重新安装依赖(如有新增)
|
||
pip install -r requirements.txt
|
||
# 4. 启动服务
|
||
python web_server.py
|
||
```
|
||
|
||
### 5.2 数据库迁移
|
||
|
||
- SQLite 表结构变化时,`init_db()` 会自动 `db.create_all()` 创建新表
|
||
- 旧 `groups.json` / `jobs.json` 首次启动自动迁移到 SQLite
|
||
- 迁移后 JSON 文件归档为 `.migrated`(保留备份,不再迁移)
|
||
|
||
### 5.3 注意事项
|
||
|
||
- **修改 `core/` 目录下的文件后必须重启 web_server**(`debug=False` 不热重载)
|
||
- **修改 `templates/` 下的 HTML 文件**:Flask 模板默认不缓存,但建议重启确保生效
|
||
- **修改 `tasks/` 下的文件后必须重启**(任务注册在启动时完成)
|
||
|
||
---
|
||
|
||
## 6. 故障排查
|
||
|
||
### 6.1 启动失败
|
||
|
||
| 现象 | 原因 | 解决 |
|
||
|------|------|------|
|
||
| `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` 会关) |
|
||
|
||
### 6.2 设备连接失败
|
||
|
||
| 现象 | 原因 | 解决 |
|
||
|------|------|------|
|
||
| `DeviceOfflineError` | u2 连接超时 / atx-agent 无响应 | 检查设备网络,重启设备或重新推送 atx-agent |
|
||
| `u2.connect 超时` | atx-agent 无响应 | 重启设备/重新推送 atx-agent |
|
||
| `adb connect failed` | 设备网络不通/端口未开放 | 检查设备 IP 和 5555 端口 |
|
||
| 设备显示离线 | 状态缓存/误报 | 管理后台设备池「重连」,或「扫描前台 App」复测 |
|
||
|
||
### 6.3 任务不执行
|
||
|
||
| 现象 | 原因 | 解决 |
|
||
|------|------|------|
|
||
| 任务列表有任务但不执行 | 任务未启用 / cron 未到点 | 检查 `enabled` 和 `schedule` |
|
||
| 立即执行无反应 | 无可用设备 | 检查设备池是否有空闲设备 |
|
||
| worker 状态 error | 查看日志的 `last_error` | 查看 `logs/core.log` 和 `logs/task.log` |
|
||
| 看门狗误杀 | 长操作未心跳 | 在长循环内加 `self.heartbeat()` |
|
||
|
||
### 6.4 日志查看
|
||
|
||
```bash
|
||
# 查看核心日志
|
||
# 方式一:Web 后台"日志"Tab
|
||
# 方式二:直接看文件
|
||
# logs/core.log — adb/worker/task_manager
|
||
# logs/task.log — 任务执行
|
||
# logs/web.log — Web 请求
|
||
# logs/action.log — 操作执行
|
||
```
|