Files
auto_control/doc/ARCHITECTURE.md
T
butubb 86c65236a2 fix: 监控页任务概况去掉「编辑/删除」,只留执行/停用 + 显示覆盖设备
监控页的任务卡片原来带「编辑」,但 openTaskModal 依赖 _taskTypes/_groupsList/
_devicesList,这三个列表只在任务 Tab 的 loadTasks() 里加载——从监控页直接点编辑,
下拉框是空的(这就是"编辑有问题")。按需求把编辑/删除从监控页移除(编辑统一去任务 Tab),
每张卡片只留:执行任务 + 停用任务/启用任务。

并给卡片加「覆盖设备」一行:
- 后端 /api/jobs(含 POST/PUT 返回的 job)新增 coverage = {mode, serials, total}:
  按 target 定义解析(all=设备池全量 / group=分组∩设备池 / serial=该设备),
  不做离线过滤、不写调度日志(该接口每 5s 轮询),与调度用的 resolve_serials 口径差异已写入文档
- 前端用已在手的 /api/status 标注在线状态,渲染成彩色 chip:
  在线=蓝、运行中=黄+⏳、离线或不在池=灰划线+✕,完整 serial/型号放 tooltip
- 文档:API.md §5 补 coverage 字段与三种模式口径、ARCHITECTURE §5.1 补卡片说明

自测:coverage 三种模式端到端(含分组过滤池外 serial)全通过;Edge headless 验卡片
(2 个按钮、无编辑/删除、chip 文案/样式/tooltip)全通过;离线分支与任务 Tab 编辑器回归通过
2026-09-10 21:15:23 +08:00

26 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)。
  • agent_action:动作经验库(命名动作 = 可复用单元,steps 用编辑器 schema 且带元素定位、禁坐标);由任务成功后从成功步骤蒸馏,执行前按名字/别名召回并注入(web/agent_api.py _distill_actions/_find_actions)。

备份覆盖(红线):持久化表须登记进 core/system_backup.py 的 SUMMARY_TABLES(→ 导出清单/预览可见、覆盖自检生效),并同步 doc/DEPLOY.md §3.5;未登记的表在备份预览里不可见,会被误判为"没备份"。

数据库初始化(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)。

XPath 序号语义(重要):同一属性多个实例时,生成 (//*[@resource-id="x"])[k](整体加括号 = 第 k 个匹配)。 不可写成 //*[@resource-id="x"][k]——那在 XPath 里是"在其父节点中排第 k",多实例时 [2..n] 会全部匹配不到 (2026-09-10 实测修复:抖音底部 4 个 tab 同 id,旧写法除 [1] 外全失效)。执行器 tasks/generic/task.py 对历史遗留的 //*[@attr=…][k] 形态做窄范围纠正(_norm_legacy_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、agent_action、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 操作

监控页「任务运行概况」卡片(2026-09-10 收敛):每张卡片只保留两个操作——执行任务(POST /api/jobs/:id/run) 与停用任务/启用任务(POST /api/jobs/:id/toggle,按当前状态切换文案);原"编辑/删除"已移除 (编辑与删除统一去「任务」Tab 操作,避免监控页误删)。卡片新增覆盖设备一行:设备清单来自 /api/jobs 的 coverage(后端按 target 定义解析,见 API.md §5),在线/运行中状态由前端 用已在手的 /api/status 数据标注成彩色 chip(在线=蓝、运行中=黄+⏳、离线或不在池=灰划线+✕, 完整 serial 与型号放 tooltip)。

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 AI 控制台的记忆面板

AI 控制台(顶级 Tab)右上角两个模态框,管理自进化记忆:

  • 🧠 经验库:任务级经验(agent_experience,整任务配方)+ 每日 AI 巡检建议(删除需人工确认)。
  • 🎬 动作库:动作级经验(agent_action)——命名动作(可含 1~N 步)+ 编辑器 schema 步骤 + 元素定位(禁坐标);由任务成功后从成功步骤自动蒸馏,执行前按名/别名召回注入;面板支持查看/编辑/删除/手动新建(保存经服务端校验,坐标步骤被拒)。
  • 会话列表显示会话 ID(前 8 位,等宽小字),点击即复制完整 ID——便于反馈问题时引用 conv=<id>。

5.4.1 回答渲染与 token(2026-09-10)

能力 落点 说明
Markdown 渲染 static/admin/markdown.js(renderMarkdown()) 自研轻量渲染器,不引 CDN(生产 220 在内网):标题/段落/软换行/粗斜体/删除线/行内代码/围栏代码块/有序无序列表(含嵌套)/引用/表格/分隔线/链接。先 esc() 转义再套标记,模型输出里的 HTML 只显示为文本(防注入)
推理链可折叠 agent.js _appendReasoning() + <details class="reasoning"> 流式思考时自动展开、正文开始时自动收起;用户手动点过 summary 后不再自动改(dataset.touched);摘要显示「思考过程(N 字)」
token 显示 mcp_agent/agent.py _accumulate_usage() + agent_api SSE usage 事件 每次模型调用完成后推本轮累计(prompt/completion/total/calls);单条消息脚注 + 顶栏「本会话累计」(历史 + 运行中)
推理链/用量落库 _agent_thread 把 usage、reasoning 写进会话 assistant 消息 刷新页面后仍可回看;回灌模型上下文时只取 role/content(不污染 token)

token 采集的兼容性:请求带 stream_options: {"include_usage": true},按「每次模型调用」取末尾 chunk 的 usage 累加(多轮工具调用会多次累加)。个别网关不认该参数会直接 HTTP 400 → _UsageUnsupported 捕获后自动关掉并重试一次(self._include_usage=False),不影响主流程。

推理链体积:只保留前 _REASONING_KEEP=6000 字符落库(会话消息上限 60 条),避免历史无限膨胀。

蒸馏健壮性(2026-09-10):经验/动作靠模型蒸馏落库。推理型模型会把 token 预算烧在 reasoning 上,导致 content 为空或被截断(finish_reason=length)→ 早期只读 content,经验/动作被静默丢弃("小红书·苏州饭店"案例)。现策略:

  1. 蒸馏调用关闭推理:"thinking": {"type": "disabled"}(该代理支持;实测关掉后 reasoning=0、正文正常,配方 3/3 合格)——这是关键修复;
  2. 配方用纯文本问法(不要放可照抄的占位示例,否则模型会原样当配方存下来)+ 质量门槛 _recipe_ok(过短/含省略号占位 → 丢弃并重试);
  3. 动作提炼用 JSON + 截断容忍提取(_loads_lenient 逐对象抢救)+ 顶层 {action,params} 形状归一 + 输入/产出限量(≤10 步输入、≤3 动作×4 步);
  4. 两类失败都有日志(经验提炼: / 动作提炼: 含样本),不再静默。

5.5 元素抓取模态框

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

  1. 选择设备 → 2. 加载截图 + 元素树 → 3. 点击元素/边界框 → 4. 回填选择器
  2. 抓取时直接验证(每条元素右侧两个按钮,不会与"点击回填"冲突):
    • 「▶ 点一下」:按元素 bounds 中心在设备上真点一次(POST /api/screen/tap,snap=1 自动吸附到可点元素),返回吸附结果并自动刷新截图——用于确认位置/是否可达;
    • 「✓ 测选择器」:用将填入的选择器真跑一次 click(POST /api/steps/test),返回 命中/未找到/已执行——用于确认回填的选择器在真实界面能命中(元素无有效选择器时不显示此按钮)。

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 缓存