Files
butubb b98e6deac1 feat(发布计划): 视频发布计划(批量上传配对 → 时间线 → 推送到手机 → 发布任务 → 分享链接)
一、平台侧(账号 → 发布计划页)
- 新表 video_plan(schema v9→v10):账号×发布日期×编号 → 素材 + 标题 + 发布状态 + 分享链接;
  状态机 pending/ready/pushing/publishing/done/failed/unknown/skipped(**failed 与 unknown 必须分开**:
  推送阶段的失败可安全重试;碰过抖音之后的岔子只能算"结果未知",绝不自动重发)
- 素材上传:文件名 `手机号_日期_编号`(编号可省)解析配对;标题 txt `标题内容_手机号_日期_编号`;
  内容寻址落盘 data/videos/YYYY-MM/(sha1 分块算,同名不存两份),**不进整库备份**但进 manifest 反查
- 新蓝图 web/video_plan_api.py:上传/时间线/统计/单条增删改/推送到手机/标记结果/裁决/链接导出 CSV/
  任务列表与一键新建、**就地编辑**(GET/PUT /tasks/<id>)、**一键推送**(POST /push_all,按设备分组、设备内串行)
- 账号页拆子分栏(台账 / 发布计划)+ static/admin/release.js;清理 job(04:41 僵尸回收+过期行、04:47 素材文件)
- 上传体积:MAX_CONTENT_LENGTH(默认 2GiB)+ 413 JSON + nginx client_max_body_size(修现有 APK 上传隐患)

二、任务侧(平台推素材,抖音流程你自己写)
- 新步骤 push_release「推送发布视频」:原子占位 → adb push → **touch 改成"现在"** → 清旧目录同名副本 →
  触发扫描并**按路径**校验相册索引 → 标题写进剪贴板;默认目录 /sdcard/DCIM/Camera
- 新步骤 mark_release「标记发布结果」:回写 done/failed/unknown,成功时抓作品分享链接、删手机素材
- input_text 支持 text_source=release_title(自动取计划标题 + 回读校验);
  if_el 的候选值来源新增 release(**本机当前发布计划**的抖音号/昵称,发布前校验"登的是不是要发的号")
- build_release_steps 骨架 15 步:⓪ 亮屏 → ① 打开抖音(等首页) → ② 点「我」→ ③ 等抖音号出现 →
  ④ 条件判断(账号) → then ⑤ 推送 ⑥⑦⑧⑨⑩⑪⑫ 抖音点击/填标题 → ⑬ 标记 / else 发通知跳过

三、修(推送这一路的检测机制)
- **uiautomator2 3.x 的 d.shell() 返回 ShellResponse(tuple 子类)不是 str**:`'x' in resp` 恒 False、
  `.strip()` 不存在 → "推上去的文件大小不对"每次都判失败(文件其实推上去了)、相册校验永远报没进、
  删除确认永远判没删掉。新增 publish_flow._sh() 统一取 .output;大小改成解析 ls -l 的大小列
- **adb push 保留本地 mtime** → 推 3 天前上传的素材在按时间排序的相册里排不到最前,
  "点第一个 = 刚推的那个"不成立 → 推完 touch
- 相册校验**按路径**比(MediaStore 的 _data 会把目录小写、/storage/emulated/0 ≡ /sdcard),
  只比文件名会被老目录的同名残留骗过去
- 屏幕没亮就启动抖音会永远停在启动页(UI 树为空)→ 后面"点我/等抖音号"必然 miss,
  最后报成误导人的"账号不符" → 骨架第一步固定加「亮屏」,open_app 等「首页」出现

四、其它
- core/ledger.serial_of():设备名 → 当前地址(设备换 IP 后快照是错的)
- 通知事件 task.video.published / task.video.failed;备份清单加 video_plan 与素材统计
- 文档同步:DATA_MODEL §2.11 + schema v10、API(新接口与语义)、TASK_DEV §4.7 专章、
  ARCHITECTURE(账号页子分栏/release.js/两个 job)、DEPLOY(表数/nginx)、NOTIFY、DEVELOPMENT、README
2026-09-28 15:59:09 +08:00

19 KiB
Raw Permalink Blame History

部署与运维(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 设备接入

设备需满足:

  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 获取代码与依赖

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>            # 不配则每次重启登录态失效
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 §4.2 与 .env.example 的「数据库」段):启动时若 「.env 声明的环境」与「库名」或「库中登记的 app_meta.deployment_env」不符,直接拒绝启动并打印 两边分别是什么。每次启动还会打印一条横幅写明当前连的是哪个库(生产用 WARNING 级), 页面顶部也常驻一个环境徽标(生产红底 PROD · <库>,开发灰底 DEV), 浏览器标签名也带环境前缀(开发 dev-设备自动化后台,生产 设备自动化后台)—— 同时开着两个环境时一眼分得清哪个标签是哪个。 两个逃生阀 DB_ALLOW_ENV_MISMATCH / DB_ALLOW_SQLITE_FALLBACK 默认关闭。

其余配置项与默认值见 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 做三件事

  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 发布流程

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 → 把当前库(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,由模型元数据自动派生:

SUMMARY_TABLES = tuple(sorted(t.name for t in db.metadata.tables.values()))

当前 17 张表: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 / device_account(账号台账)/ video_plan(视频发布计划)。 完整说明见 DATA_MODEL.md §6。

⚠ 视频素材(data/videos/)不进备份包(几十 GB 会把备份搞坏):manifest 里有 videos.included=false 与数量/字节数,预览页也会提示 —— 素材要另外备份。 计划、发布状态与分享链接都在 video_plan 表里,随备份一起走。

⚠️ 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 + 日志告警); 导入侧有反向自检(备份含未登记表 → 预览告警)。

回滚注意事项:把"含新表的备份"导回旧版本代码时,旧代码的 SUMMARY_TABLES 里没有那张新表 → 导入预览会报 extra_tables 告警。数据仍在快照里、不会丢, 属于预期行为(升级回新版即可正常识别)。

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 反代
location / {
    proxy_pass http://127.0.0.1:18050;
    proxy_set_header Host $host;
    # ⚠ 必须加:nginx 默认 client_max_body_size 只有 1m,
    # 不加的话「应用管理」传 APK(有 336MB 的包)和视频素材上传都会 413。
    client_max_body_size 2048m;
}

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() 建缺表 + _sync_columns() 补缺列(幂等,失败不阻塞启动,见 DATA_MODEL.md §4)
  • 旧 groups.json / jobs.json 首次启动自动迁移并归档为 .migrated
  • 跨版本恢复:用「导入恢复」上传旧库 → 预览会提示 schema 版本差异 → 应用后重启(新版本会自动补列/补空表)

SQLite → MySQL(一次性)

首次把平台从单文件 SQLite 换到 MySQL 时,用迁移脚本搬数据:

# 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
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。