Files
auto_control/doc/ARCHITECTURE.md
T
butubb fd829a6063 docs: doc/ 全量同步 dev 现状——API 目录补全(AI 控制台/系统备份/自动发现等)、去 STF 过时口径、补 generic_steps 与配置键速查;确立「功能/配置改动须同步文档」红线
- doc/API.md:补方法/路径标题,权限分层修正,新增 AI 控制台(/api/agent/*)、系统备份(/api/system/backup/*)、设备自动发现(/api/devices/discovery/*)、tap_text/summary/health/devices-apps 等整节端点,去 STF 残留
- doc/TASK_DEV.md:STF 时代描述清理;新增 §2.10 generic_steps(18 节点与必填/嵌套/静默跳过语义)、§2.11 自定义动作与单步测试、/api/jobs 盲存校验语义、resolve_serials/抢占语义、模板构造函数签名修正
- doc/DEPLOY.md:数据备份改为推荐「系统→数据备份」功能并说明重启生效目录,端口表 STF7100→MCP8033,补 start.sh 生产链路与 MCP_PLATFORM_PASS 同步,故障排查去 STF
- doc/MCP.md:加「现状边界」(平台级任务 CRUD 未 MCP 化,规划见 AI_TASK_GEN §9),busy/平台会话说明,MCP_ALLOWED_SERIALS 语义纠正
- doc/MCP_DESIGN.md:加实现现状对照、错误码、独立容器改演进备选、里程碑状态、API 映射表按实现重写
- doc/ARCHITECTURE.md:Tab/子分栏/线程模型/数据表/蓝图表去 STF,补 device_discovery/agent/system_backup/经验巡检等
- doc/DEVELOPMENT.md:新增 §5.6「改动必须同步文档」红线、§2.3 配置键速查、蓝图化新增 API 流程、文档索引补登记
- doc/STF_REMOVAL.md:加历史记录状态横幅
- doc/AI_TASK_GEN.md:新增 AI 建任务设计稿(含 §9 需转 MCP 工具分层)
2026-09-09 16:05:56 +08:00

20 KiB
Raw Blame History

架构详解

本文面向想深入理解 auto_control 内部设计的开发者。如果你只想使用,看 README.md 即可。


1. 分层设计

平台按"配置 / 核心 / 任务 / 前端 / 数据 / 日志 / 工具"分层,职责清晰、互不交叉:

层 路径 职责
配置层 config.py 项目根配置:adb 路径、web 端口、USB 远程 adb server 等基础设施。不放任务参数
核心层 core/ 框架运行时:日志、设备池、adb 操作、Worker 基类、任务管理器、u2 辅助、Action 基类
任务层 tasks/ 每个 App 一个子包,自包含 task.py + actions/,互不依赖
前端层 templates/admin/ 单页应用(纯 HTML+CSS+JS,无框架)
数据层 data/ SQLite 持久化
日志层 logs/ 四类日志,10MB 滚动保留 5 份
工具层 bin/adb/ adb 可执行文件
脚本层 scripts/ 实用脚本

分层原则

  • 任务自包含:每个任务的参数、Worker、操作都放在 tasks/<app>/ 下,不污染全局
  • 核心不依赖任务:core/ 不 import tasks/,任务通过注册机制接入
  • 配置最小化:config.py 只放基础设施配置,任务参数在各自 task.py 顶部

2. 数据流

┌──────────────┐  创建/编辑任务    ┌─────────────┐  分发 worker   ┌──────────────┐
│ 单页应用前端  │ ───────────────► │ TaskManager │ ─────────────► │  Worker(设备) │
│ (monitor.html│                  └─────────────┘                └──────────────┘
│  fetch+DOM)  │                         ▲                            │
└──────────────┘                         │ 状态/心跳                    │ u2 操作
       │                                 │                            ▼
       │ JSON API                        │                   ┌─────────────────┐
       ▼                          ┌──────────────┐           │ 设备 adb        │
┌──────────────┐                  │  看门狗监控   │           │ (IP:5555 直连  │
│  web/ 蓝图包  │                  └──────────────┘           │  / USB 远程)   │
│  (Flask API) │                                            └─────────────────┘
└──────────────┘
       │
       ▼
┌──────────────┐
│ data/users.db│  SQLite 持久化(用户/分组/任务/设备池/待连接设备/自定义动作/APK记录/AI会话/经验库 + app_meta KV)
└──────────────┘

任务执行流程

  1. 前端创建 TaskJob(HTTP POST /api/jobs)
  2. TaskManager 保存到 SQLite,如启用 cron 则注册到 APScheduler
  3. 手动执行或 cron 触发时,_run_job 解析目标设备列表
  4. 每台设备起一个线程 _run_with_retry,含重试循环
  5. 线程内 task.create_worker() 创建 Worker,worker.start() 启动
  6. BaseWorker.run() 执行设备生命周期:占用 → 连接 → setup → run_task → teardown → 释放
  7. Worker 通过 _update_status() 实时上报状态到全局 _WORKERS 字典
  8. 前端轮询 /api/status(5 秒缓存)获取设备 + Worker 状态

3. 核心模块详解

3.1 DevicePool(core/device_pool.py)

设备池(已摘除 OpenSTF):SQLite devices 表 = 设备清单,本机 adb = 在线状态。

关键设计:

  • list_configured() — 清单(管理页维护,enabled=False 不参与调度)
  • list_online() — 本机 adb 在线设备;池内有 USB 设备(serial 无冒号)时合并 220 远程 adb server(USB_ADB_HOST:PORT,host 网络模式 5037)状态
  • list_ready() — 清单 ∩ 在线(任务调度用)
  • CRUD — add_device(upsert)/ remove_device / set_enabled
  • 单实例互斥由 TaskManager._running[serial] 内存锁保证(无跨实例占用概念)
  • 全模块不 connect/kill-server/disconnect(遵守共享 adb transport 红线)

3.2 STFDevice + BaseWorker(core/device_worker.py)

STFDevice — 单设备生命周期管理:

  • acquire():IP:5555 直连 adb connect;USB(serial 无冒号)校验 220 远程 adb server 可见性
  • release():无操作(不 disconnect,红线)
  • 互斥由 TaskManager _running 保证,设备池负责在线判断

BaseWorker — 通用 Worker 基类(继承 threading.Thread):

run() 主循环(不要重写):
  1. acquire 设备
  2. u2.connect(30s 超时保护)
  3. setup(d)        ← 子类可选钩子
  4. run_task(d)     ← 子类必须实现
  5. teardown(d)     ← 子类可选钩子
  6. finally: release 设备

超时保护:

  • u2.connect() 用 ThreadPoolExecutor + 30s 超时,防止 atx-agent 无响应永久 hang
  • d.info 用 ThreadPoolExecutor + 10s 超时

心跳看门狗(_Watchdog):

  • 后台线程,每 30 秒扫描一次
  • Worker 超过 120 秒无心跳 → 标记 error
  • 防止设备被占用却不干活

全局状态注册表(_WORKERS):

  • serial -> status dict,线程安全(_WORKERS_LOCK)
  • 供 web_server 读取实时状态,前端通过 /api/status 展示

3.3 TaskManager(core/task_manager.py)

统一管理:任务类型注册、设备分组、任务计划、定时调度、重试、持久化。

核心组成:

组件 说明
scheduler APScheduler BackgroundScheduler,cron 触发任务
groups 设备分组(内存业务对象,持久化到 SQLite)
jobs 任务计划(内存业务对象,持久化到 SQLite)
_running 运行中的 worker(serial -> worker 信息)
_stop_requested 用户请求停止的 serial 集合(阻止后续重试)
_fg_scanner 前台 App 扫描器(不打扰设备)

调度模式:

  • once:不注册 cron,手动执行
  • cron:注册启动 cron,到点启动所有目标设备
  • cron_stop:注册启动 cron + 停止 cron,到点停止本任务 worker

重试策略:

  • DeviceOfflineError:立即放弃,不重试(设备掉线短时间不会自愈)
  • 其他异常:按 retry.max_attempts 重试,间隔 retry.delay
  • 临时错误(端口耗尽):退避 max(delay, 120s)
  • 用户停止:加入 _stop_requested,阻止任何后续重试

并发控制:同一 serial 同时只允许一个 worker,避免冲突。

状态缓存:get_status() 带 5 秒缓存,避免每次 /api/status 都查库/adb 阻塞前端。

3.4 前台 App 扫描器(_ForegroundScanner)

设计原则:不打扰设备,扫描不会让设备退出当前 App。

设备状态 处理方式 是否打扰
worker 运行中(IP:5555) 复用已有 ADB 连接查询 否
worker 运行中(USB) 经 220 远程 adb server 查询 否
完全空闲 返回"空闲"(不主动 connect) 否

为什么不扫描空闲设备的前台 App:IP:5555 的 adb transport 是共享的(历史与 STF provider 共用),外部 connect/disconnect 会扰动共享连接,遵守既有红线。

3.5 ADB 操作(core/adb_helper.py)

全局锁串行化:_ADB_LOCK 确保所有 adb 调用串行执行,避免多线程竞争 adb server。

铁律:绝不 kill-server:

  • adb kill-server 会断开所有设备的 adb transport
  • 全部设备连接被重建,影响所有运行中的任务
  • 同理绝不 disconnect IP:5555(共享 transport 红线,见 DEVELOPMENT.md)
函数 说明
adb_connect(url, retries=5) adb connect(带重试,绝不 kill-server)
adb_connect_light(url) 轻量 connect(单次尝试,扫描专用)
adb_disconnect(url) adb disconnect
screenshot(serial) 截图(adb exec-out screencap -p,只读安全)
get_foreground_app(url) 获取前台 App 包名(dumpsys window)
identify_device(serial) 让设备响铃识别

3.6 数据模型(core/models.py)

SQLAlchemy 模型,存于 data/users.db:

模型 表名 说明
User user 后台用户(Flask-Login 认证,Werkzeug 哈希密码;is_admin 管理员 + perms 业务权限位)
DeviceGroup device_group 设备分组(serials 存 JSON)
TaskJob task_job 任务计划(target/params/schedule/retry 存 JSON)
CustomAction custom_action 自定义动作(步骤打包,steps 存 JSON)
ApkFile apk_file APK 文件元信息
Device device 设备池清单(替代 STF 池;enabled=False 不参与调度,含 model 型号列)
PendingDevice pending_device 自动发现「待连接池」(扫描发现、用户确认后才入正式池)

表名默认取类名小写(models.py 未写 __tablename__)。另有三张非模型表,由原生 SQL 幂等创建、不走 SCHEMA_MIGRATIONS:

  • app_meta(KV):_migrate_schema() 内建表,存 schema_version、discovery_*、agent 配置 agent_* 等;
  • agent_conversation / agent_experience / experience_audit:AI 控制台会话 / 经验库 / 经验巡检(web/agent_api.py 顶部 CREATE TABLE IF NOT EXISTS)。

数据库初始化(init_db):

  • 创建所有表
  • 首次启动创建默认管理员 admin/admin123
  • 自动迁移旧 groups.json / jobs.json 到 SQLite(迁移后归档为 .migrated)
  • 版本化 schema 迁移(SCHEMA_MIGRATIONS,当前到 v4,见 core/models.py):结构变更必须追加迁移条目,create_all 只建新表不加列

权限模型

  • User.perms 存业务权限位 JSON 数组(tasks/devices/apks/logs),is_admin=true 拥有全部权限(has_perm 短路)
  • 后端统一用 @perm_required(PERM_X) / @admin_required 装饰器拦截(web/auth.py),无权限返回 403; 查看类 GET 接口只要求登录;用户管理、adb 终端(/api/adb/cmd)强制 admin_required
  • adb 终端安全红线:拒绝 kill-server / disconnect(共享 adb transport)
  • 前端 loadMe() 拉取 /api/me,用 data-perm 属性隐藏无权限的 tab/按钮,行内按钮用 _can(perm) 判断
  • 安全兜底:前端隐藏只是 UX,权限强制在后端;新增路由时按"写操作必须带权限装饰器"的约定

3.7 APK 管理(core/apk_manager.py)

APK 上传/解析/批量安装。

设备连接策略(直连):

  • 直接 adb connect serial(serial 是 IP:5555)
  • 不经过占用/释放(单实例互斥由调度器内存锁保证)
  • 安装后不主动 disconnect(共享 adb transport 红线)

安装流程:

  1. 上传 APK → 保存到 data/apks/ → pyaxmlparser 解析包名/版本 → 入库
  2. 批量安装 → 后台线程 → 每台设备直连 adb install → 验证包名
  3. 跳过 worker 运行中的设备(避免打断任务)

3.8 元素抓取(core/uiauto_helper.py)

封装 uiautodev 本地服务(端口 20242)的客户端。

函数 说明
is_running() 探测 uiauto2 服务是否运行
list_devices() 获取 uiauto2 已连接的设备列表
get_screenshot(serial) 获取设备截图(JPEG)
get_elements(serial) 获取设备 UI 元素树(扁平化列表)

元素树解析:递归提取每个节点的 resource-id/text/content-desc/class/bounds 等属性,并推荐最佳选择器(优先 xpath)。

3.9 屏幕 OCR(core/ocr.py)

条件判断的 ocr 选择器实现:截屏 → RapidOCR(ONNX 推理,中英文模型随包内置)→ 关键词匹配 → 返回文字中心像素坐标(与 u2 d.click 一致)。

  • 跨平台(Windows/Linux/macOS),依赖 rapidocr_onnxruntime;服务器无显示器环境建议将 opencv-python 换成 opencv-python-headless
  • 引擎懒加载单例 + 并发加锁(识别约 0.2-0.5s/次)
  • 返回坐标约定:像素、原点左上

3.10 Web 层蓝图包(web/)

路由按功能域拆分(模块化开发底线,便于定位问题):

模块 职责
auth.py 登录/登出/CSRF/权限装饰器/页面路由(/、/wall)
monitor.py 状态/运行控制/设备操作/远程看屏(流+缩略图+触控)
tasks_api.py 任务计划/分组/自定义动作/元素抓取/步骤测试
admin_api.py 用户管理/日志
tools_api.py adb 终端/剪贴板注入/应用版本
devices_api.py 设备池管理 + 自动发现(扫描/确认/忽略/手动重连/型号采集)
apks_api.py 应用管理
tailscale_api.py Tailscale 管理
agent_api.py AI 控制台:会话 / 运行 / SSE / 停止 / 配置 / 经验库与巡检(读写 agent_conversation、agent_experience、app_meta agent_*)
system_api.py 系统数据备份导出 / 导入恢复(/api/system/backup/*,仅 admin)
common.py 跨模块共享(合并设备列表/屏幕状态)
context.py 共享对象注入(mgr/apk_mgr/device_pool)

web_server.py 只做装配与启动(266 行):app 创建;init_db 前消费待生效备份恢复(consume_pending_restore);初始化 device_pool / device_discovery;装配 TaskManager / ApkManager;注册 10 个蓝图(auth/monitor/tasks/admin/tools/devices/apks/tailscale/agent/system,web/__init__.py);注册经验巡检 APScheduler(03:47 Asia/Shanghai);uiautodev 子进程启停与设备池预连接线程;启动。


4. 任务系统设计

4.1 注册机制

tasks/__init__.py
  ├── from .base import BaseTask, register_task, list_task_types, get_task_class
  ├── from .douyin import task    # 触发 @register_task
  └── from .generic import task   # 触发 @register_task

@register_task 装饰器将 Task 类注册到全局 _TASK_TYPES 字典,key 为 task_type 字符串。

4.2 参数深合并

Job 下发时只传需要覆盖的字段,调度器做三层合并:

  1. 顶层字段:Job params 覆盖 DEFAULT_PARAMS
  2. actions 字段:参数级深合并
    • 前端没传的 action → 用默认
    • 前端传了 → enabled 和 params 分别合并
    • params 再深合并一层(保留前端没传的子参数)

示例:只想改点赞概率,Job params 只需:

{"actions": {"like": {"params": {"rate": 0.5}}}}

4.3 Action 系统

每个 App 有独立的 Action 注册表(create_action_registry()),互不污染。

core/actions/base.py        — BaseAction 全局基类 + should_trigger + register_action
tasks/douyin/actions/base.py — ACTIONS = create_action_registry() + list/get 函数
tasks/douyin/actions/like.py — @register_action(ACTIONS) LikeAction

循环导入坑:actions/__init__.py 必须先 from .base import ACTIONS,再 from . import like。


5. 前端设计

5.1 单页应用

templates/admin/monitor.html 是纯 HTML+CSS+JS 单页应用,无框架依赖。

  • Tab 切换:7 个顶级 Tab(监控/任务/日志/用户/工具/AI 控制台/系统),均在 monitor.html 内 .tab-panel 切换(纯 DOM 操作);独立页面仅 /login、/wall。原"分组"已无顶层入口(移到工具页子分栏)
  • 数据获取:fetch() 调 JSON API,5 秒轮询 /api/status
  • 状态渲染:设备表格、任务卡片、进度条、徽章,纯 DOM 操作

5.2 步骤编辑器

generic_steps 任务的步骤编辑器(任务弹窗已加宽到 1000px):

  • 左侧操作库(拖拽源,按 交互操作/屏幕与App/流程控制 分组)
  • 中间画布(步骤卡片列表,HTML5 Drag API 排序 + 跨层级嵌套:循环套循环、动作组)
  • 每个步骤卡片可展开参数表单
  • 选择器字段旁有"抓取元素"按钮(独立模态框)
  • 容器步骤(loop/group/if_el):卡片内嵌子步骤容器接收拖入;if_el 有"✅找到时/❌未找到时"两个独立分支容器,分支可嵌套任意步骤
  • 条件判断(if_el):选择器支持 xpath 等 UI 树选择器或 OCR识别(core/ocr.py,截屏匹配图片/画布文字,命中可自动点击)
  • 所有递归操作(选择打包、存自定义动作、校验、防循环自套)统一遍历 children/then/else 三个子数组

5.3 页内子分栏

任务/工具/系统 Tab 用通用 showSubTab(tabId, name) 实现页内子分栏:每个子分栏一个 .sub-panel, _activeSubs 记住各 Tab 上次选中的子分栏。现状子分栏:

  • 任务:任务计划 / 自定义动作
  • 工具:剪贴板注入 / adb 远程终端 / Tailscale 管理 / 应用管理 / 应用版本管理 / 设备已装应用 / 设备池管理 / 设备分组
  • 系统:数据备份 / 导入恢复

(原"分组"顶级 Tab 与"维护/STF 服务"子分栏已不存在——分组已移入工具页子分栏,STF 已摘除。)

5.4 元素抓取模态框

独立的第二层模态框(el-picker-overlay,z-index 1100),不影响任务编辑窗口:

  1. 选择设备 → 2. 加载截图 + 元素树 → 3. 点击元素/边界框 → 4. 回填选择器

6. 关键设计决策

6.1 为什么用 SQLite 而不是 JSON 文件

  • 支持用户/分组/任务的关系存储
  • 并发安全(WAL 模式)
  • 迁移旧 JSON 时归档为 .migrated,避免删空后重启又复原

6.2 为什么用 threading 而不是 asyncio

  • uiautomator2 是同步阻塞库,不适合 asyncio
  • 多设备并发用多线程即可,每台设备一个 Worker 线程
  • Flask threaded=True 处理并发 HTTP 请求

6.3 为什么绝不 kill-server

adb kill-server / adb disconnect 会断开共享的 adb transport(历史与 STF provider 共享;STF 摘除后红线仍保留——多 worker、前台扫描、设备自动发现共用同一 adb server),影响所有运行中的任务。连接失败就返回 False,由调用方处理。

6.4 为什么状态查询带缓存

状态源 = 本地 SQLite 设备池 + adb + 内存 worker 状态。5 秒缓存避免每次 /api/status 都查库/adb 阻塞 Flask;Worker 实时状态读内存,不受缓存影响。

6.5 为什么 DeviceOfflineError 不重试

设备掉线后短时间内不会自愈,重试只会浪费配额并阻塞调度器。让设备进入冷却,可依赖设备自动发现(device_discovery 对正式池断联设备每轮 adb 重连)恢复后再启用。


7. 线程模型

主线程(Flask)
  ├── HTTP 请求处理(threaded=True,每请求一线程)
  ├── TaskManager.scheduler(APScheduler,cron 触发任务计划)
  ├── 经验巡检 BackgroundScheduler(03:47 Asia/Shanghai,web_server 装配)
  ├── 设备自动发现线程(device_discovery._discovery_loop,默认 60s 一轮)
  ├── 设备池型号采集后台线程(device_pool._refresh_models_bg,启动/手动触发)
  ├── 设备池预连接线程(web_server._preconnect_pool_devices,重启后加速恢复)
  ├── AI Agent 运行线程(agent_api._agent_thread,单实例 + SSE 推送)
  ├── 看门狗线程(_Watchdog,30s 间隔)
  ├── uiautodev 子进程(PID + cmdline 校验,防容器 PID 复用误杀)
  └── Worker 线程(每台设备一个)
        ├── _run_with_retry 线程(重试循环)
        └── BaseWorker 线程(设备生命周期 + run_task)

MCP server(127.0.0.1:8033)是独立进程(scripts/start.sh 拉起),不是 web_server 的线程。

线程安全:

  • _WORKERS_LOCK:保护全局 worker 状态字典
  • _ADB_LOCK:串行化所有 adb 调用
  • TaskManager._lock:保护运行中任务字典
  • _ForegroundScanner._cache_lock:保护前台 App 缓存