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 工具分层)
This commit is contained in:
+192
-69
@@ -1,6 +1,8 @@
|
||||
# 任务开发指南
|
||||
|
||||
面向 `platform-tools`(STF + uiautomator2 + Flask 单页应用,多设备并发任务执行框架)的新开发者。描述架构、核心概念,并给出从 0 到 1 新增一个 app 任务所需的全部模板与规范。看完本文即可上手开发新任务。
|
||||
面向 `platform-tools`(uiautomator2 + Flask 单页应用,本地 SQLite 设备池 + adb 直连(IP:5555 / USB 远程 server)多设备并发执行框架)的新开发者。描述架构、核心概念,并给出从 0 到 1 新增一个 app 任务所需的全部模板与规范。看完本文即可上手开发新任务。
|
||||
|
||||
> 平台曾在代码层依赖 OpenSTF,现已完全摘除(STF 占用/释放、remoteConnect 桥接均已退役),迁移背景见 doc/STF_REMOVAL.md。项目代码目录为 `auto_control`,文档/README 仍沿用旧名 `platform-tools`——**项目是否正式更名待人工核实**。
|
||||
|
||||
---
|
||||
|
||||
@@ -12,11 +14,11 @@
|
||||
|
||||
| 层 | 路径 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| 配置层 | `config.py` | 项目根配置:STF 服务地址 / adb 路径 / web 端口等基础设施。**不放任务参数**(任务参数属于 `tasks/`) |
|
||||
| 核心层 | `core/` | 框架运行时:`logger` 日志、`stf_client` STF API 封装、`adb_helper` adb 操作、`device_worker` Worker 基类、`task_manager` 调度器、`u2_helper` uiautomator2 通用操作、`actions` 全局 Action 基类 |
|
||||
| 配置层 | `config.py` | 项目根配置:adb 路径 / web 端口 / 数据与备份目录 / USB 远程 adb(220)/ Tailscale / 设备发现 / `.env` 注入。STF/SSH 键已废弃,仅历史保留。**不放任务参数**(任务参数属于 `tasks/`) |
|
||||
| 核心层 | `core/` | 框架运行时:`logger` 日志、`device_pool` 设备池(本地清单 + adb 在线)、`device_discovery` 设备自动发现、`models` 数据模型、`adb_helper` adb 操作(全局锁)、`device_worker` Worker 基类 + STFDevice、`task_manager` 调度器、`u2_helper` / `uiauto_helper` / `ocr` / `clipboard_helper`、`apk_manager` 应用管理、`system_backup` 数据备份、`tailscale_client`、`actions` 全局 Action 基类 |
|
||||
| 任务层 | `tasks/` | 每个 app 一个子包,自包含 `task.py` + `actions/`,互不依赖 |
|
||||
| 前端层 | `templates/admin/monitor.html` | 单页应用(纯 HTML+CSS+JS,无框架):设备监控 / 任务管理 / 分组 / 日志 / 用户 5 个 Tab |
|
||||
| 数据层 | `data/` | SQLite 持久化:`users.db`(用户 + 设备分组 + 任务计划) |
|
||||
| 前端层 | `templates/admin/` + `static/admin/` | 单页应用(纯 HTML+CSS+JS,无框架):监控 / 任务 / 日志 / 用户 / 工具 / AI 控制台 / 系统 7 个 Tab;工具页等按子分栏分组;JS 拆分为 `static/admin/` 下的 `base/list/monitor/editor/tasks/tools/apps/admin/agent/system` |
|
||||
| 数据层 | `data/` | SQLite 持久化:`users.db`(用户 / 设备分组 / 任务计划 / 自定义动作 / APK 文件 / 设备池 device / 待连接池 pending_device / app_meta / AI 会话与经验库 等表) |
|
||||
| 日志层 | `logs/` | 四类日志:`core.log` / `task.log` / `web.log` / `action.log`,10MB 滚动保留 5 份 |
|
||||
| 文档层 | `doc/` | 项目文档 |
|
||||
| 工具层 | `bin/adb/` | adb 可执行文件 |
|
||||
@@ -27,40 +29,64 @@
|
||||
```
|
||||
platform-tools/
|
||||
├── config.py # 根配置(部署值从 .env 读,不放任务参数)
|
||||
├── web_server.py # Flask 入口(JSON API + 登录页 + 单页应用)
|
||||
├── web_server.py # Flask 入口(app 装配 + init_db + 启动调度/看门狗/设备发现)
|
||||
├── web/ # Web 蓝图包(路由按功能域拆分,web_server.py 只做装配)
|
||||
│ ├── 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 管理
|
||||
│ ├── system_api.py # 系统备份/导入恢复
|
||||
│ └── agent_api.py # AI 控制台(会话/SSE/经验库)
|
||||
├── core/ # 核心程序层
|
||||
│ ├── __init__.py
|
||||
│ ├── logger.py # 日志器(分文件、10MB 滚动)
|
||||
│ ├── stf_client.py # STF API 封装
|
||||
│ ├── adb_helper.py # adb 操作(全局锁串行化)
|
||||
│ ├── device_worker.py # BaseWorker 基类 + STFDevice + 看门狗
|
||||
│ ├── device_pool.py # 设备池:SQLite devices 清单 + adb 在线(list_ready 供调度)
|
||||
│ ├── device_discovery.py # 设备自动发现(扫描 5555 → 待连接池,用户确认入池)
|
||||
│ ├── models.py # SQLAlchemy 模型(User/DeviceGroup/TaskJob/Device/PendingDevice/CustomAction/ApkFile)
|
||||
│ ├── adb_helper.py # adb 操作(全局锁串行化;支持 220 远程 server -H/-P)
|
||||
│ ├── device_worker.py # BaseWorker 基类 + STFDevice(acquire/release) + 心跳看门狗
|
||||
│ ├── task_manager.py # TaskManager 调度器 + 前台 App 扫描器
|
||||
│ ├── u2_helper.py # uiautomator2 通用操作(ensure_app_running/wait_for_app_home)
|
||||
│ ├── models.py # SQLAlchemy 模型(User/DeviceGroup/TaskJob)
|
||||
│ ├── u2_helper.py # uiautomator2 通用操作(ensure_app_running/wait_for_app_home/random_sleep)
|
||||
│ ├── uiauto_helper.py # uiautodev 本地服务客户端(步骤编辑器"抓取元素")
|
||||
│ ├── ocr.py # 屏幕 OCR(RapidOCR;if_el 的 ocr 选择器用)
|
||||
│ ├── clipboard_helper.py # 剪贴板注入(ClipInject APK 通道)
|
||||
│ ├── apk_manager.py # APK 上传/解析/批量安装
|
||||
│ ├── system_backup.py # 数据备份导出/导入(重启生效)
|
||||
│ ├── tailscale_client.py # Tailscale API v2 客户端
|
||||
│ ├── ssh_client.py # SSH 客户端(仅历史手动运维 220 用)
|
||||
│ └── actions/
|
||||
│ ├── __init__.py # create_action_registry / register_action / should_trigger
|
||||
│ └── base.py # BaseAction 全局基类
|
||||
├── tasks/ # 任务定义层
|
||||
│ ├── __init__.py # 全局 _TASK_TYPES 注册表(import 各任务包触发注册)
|
||||
│ ├── base.py # BaseTask 基类
|
||||
│ ├── douyin/ # 抖音养号(示例)
|
||||
│ ├── __init__.py # 聚合导出 + import 各任务包触发注册(from .douyin/.generic import task)
|
||||
│ ├── base.py # BaseTask 基类 + _TASK_TYPES + register_task/list_task_types/get_task_class
|
||||
│ ├── douyin/ # 抖音养号(task_type=douyin_nurture,示例)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── task.py # DEFAULT_PARAMS + Worker + Task + @register_task
|
||||
│ │ └── actions/
|
||||
│ │ ├── __init__.py # 先 from .base import ACTIONS,再 from . import like
|
||||
│ │ ├── base.py # ACTIONS = create_action_registry()
|
||||
│ │ └── like.py # @register_action(ACTIONS) LikeAction
|
||||
│ └── generic/ # 通用步骤任务(可视化步骤编辑器编排)
|
||||
│ └── generic/ # 通用步骤任务(task_type=generic_steps,步骤编辑器编排)
|
||||
│ ├── __init__.py
|
||||
│ └── task.py # STEP_TYPES + Worker + Task(按 steps 顺序执行)
|
||||
│ └── task.py # STEP_TYPES + Worker + Task(按 steps 顺序执行)+ test_step
|
||||
├── templates/admin/
|
||||
│ ├── monitor.html # 单页应用(5 Tab,纯前端渲染)
|
||||
│ └── login.html # 登录页
|
||||
│ ├── monitor.html # 单页应用(7 Tab + 页内子分栏,纯前端渲染)
|
||||
│ ├── login.html # 登录页
|
||||
│ └── wall.html # 监控大屏(只读轮播展示)
|
||||
├── static/admin/ # 前端 JS(base/list/monitor/editor/tasks/tools/apps/admin/agent/system.js)+ custom.css
|
||||
├── data/ # 持久化数据
|
||||
│ └── users.db # SQLite(用户/分组/任务)
|
||||
│ ├── users.db # SQLite(用户/分组/任务/自定义动作/APK/设备池/待连接池/app_meta/AI 会话)
|
||||
│ └── apks/ # 上传的 APK 文件
|
||||
├── logs/ # 日志(10MB 滚动保留 5 份)
|
||||
├── doc/ # 文档
|
||||
├── bin/adb/ # adb 工具
|
||||
├── mcp_server/ # MCP 服务端(AI 控制台 19 个 de_* 设备工具)
|
||||
├── mcp_agent/ # MCP Agent 链路(DeepSeek 多模态,AI 控制台后端)
|
||||
└── scripts/ # 实用脚本
|
||||
```
|
||||
|
||||
@@ -69,23 +95,27 @@ platform-tools/
|
||||
```
|
||||
┌──────────────┐ 创建 Job ┌─────────────┐ 分发 ┌──────────────┐
|
||||
│ 单页应用前端 │ ───────────► │ TaskManager │ ──────► │ Worker(设备) │
|
||||
│ (monitor.html│ └─────────────┘ └──────────────┘
|
||||
│ fetch + DOM)│ ▲ │
|
||||
└──────────────┘ │ 心跳/状态 │ u2 操作
|
||||
│ │ ▼
|
||||
│ JSON API │ ┌────────────────┐
|
||||
▼ │ │ STF Device / adb│
|
||||
┌──────────────┐ ┌──────────────┐ └────────────────┘
|
||||
│ web_server │ │ 看门狗监控 │
|
||||
│ (Flask API) │ └──────────────┘
|
||||
└──────────────┘
|
||||
│ (monitor.html│ │互斥:_running│ └──────┬───────┘
|
||||
│ fetch + DOM)│ └──────┬──────┘ │ STFDevice.acquire:
|
||||
└──────────────┘ │ │ · IP:5555 → adb connect
|
||||
│ JSON API │ 心跳/状态 │ · USB → 220 远程 adb server
|
||||
▼ │ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌────────────────┐
|
||||
│ web_server │ │ 看门狗监控 │ │ 设备(adb+u2) │
|
||||
│ (Flask API) │ └──────────────┘ │ u2 操作执行任务 │
|
||||
└──────┬───────┘ └────────────────┘
|
||||
│
|
||||
│ device_pool.list_ready() = SQLite 清单 ∩ (本机 adb + 220 远程)在线
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ data/users.db│ SQLite 持久化(用户/分组/任务)
|
||||
│ data/users.db│ SQLite 持久化(设备池/分组/任务/自定义动作/...)
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
调度与扫描的设备数据源是 `core/device_pool`(清单取自 SQLite `device` 表、在线状态取自
|
||||
`adb devices`),**不再查询 STF**;同一 serial 同时只允许一个 worker,互斥由
|
||||
`TaskManager._running` 保证(见 §2.2、§4.1)。
|
||||
|
||||
### 1.4 关键设计决策
|
||||
|
||||
- **Flask + Flask-Login**:已移除 Flask-Admin(自定义场景下过于受限),改用纯 Flask + 单页应用
|
||||
@@ -100,7 +130,9 @@ platform-tools/
|
||||
|
||||
### 2.1 TaskType — 任务类型
|
||||
|
||||
一个 `TaskType` 描述"做什么"(例如抖音养号、快手养号),由 `Task` 子类 + `Worker` 子类 + `DEFAULT_PARAMS` 组成。每个 `TaskType` 注册到全局 `_TASK_TYPES` 字典(在 `tasks/__init__.py`),key 为任务类型字符串,value 为 `Task` 类。
|
||||
一个 `TaskType` 描述"做什么"(例如抖音养号、通用步骤),由 `Task` 子类 + `Worker` 子类 + `DEFAULT_PARAMS` 组成。每个 `TaskType` 注册到全局 `_TASK_TYPES` 字典,key 为任务类型字符串,value 为 `Task` 类。
|
||||
|
||||
> **注册位置**:`_TASK_TYPES` 及 `register_task` / `list_task_types` / `get_task_class` **定义在 `tasks/base.py`**;`tasks/__init__.py` 只做两件事——`from .base import ...` 聚合导出,以及 `from .douyin/.generic import task` 触发各任务包注册。**当前已注册的 task_type 仅 `douyin_nurture` 与 `generic_steps`**,前端"新建任务"下拉来自 `GET /api/task_types`。新增 task_type 需在 `tasks/` 下建子包、用 `@register_task` 装饰,并在 `tasks/__init__.py` 加 `from .xxx import task`,然后**重启 web** 生效。
|
||||
|
||||
```python
|
||||
# tasks/base.py
|
||||
@@ -119,20 +151,30 @@ def get_task_class(task_type):
|
||||
|
||||
`TaskJob` 是"什么时候、在哪些设备上、用什么参数执行某个 TaskType"的持久化计划,存于 SQLite(`data/users.db` 的 `task_job` 表)。包含字段:
|
||||
|
||||
- `task_type` — 任务类型(对应 `_TASK_TYPES` 的 key)
|
||||
- `target` — 目标设备:`{"mode": "all"|"group"|"serial", "group_name": "", "serial": ""}`
|
||||
- `params` — 任务参数(与 `DEFAULT_PARAMS` 深合并)
|
||||
- `schedule` — 调度策略:`{"mode": "once"|"cron", "cron": "0 9 * * *"}`
|
||||
- `task_type` — 任务类型(对应 `_TASK_TYPES` 的 key;当前仅 `douyin_nurture` / `generic_steps`)
|
||||
- `target` — 目标设备:`{"mode": "all"|"group"|"serial", "group_name": "", "serial": ""}`,默认 `{"mode":"all"}`
|
||||
- `params` — 任务参数(与 `DEFAULT_PARAMS` 深合并)。含两个隐藏开关:
|
||||
- `skip_offline`(默认 **true**)— serial/group 模式先跳过本机 adb 不可达(离线)的设备
|
||||
- `preempt`(默认 **false**)— all 模式是否抢占正在运行其他任务的设备
|
||||
- `schedule` — 调度策略:`{"mode": "once"|"cron"|"cron_stop", "cron": "...", "stop_cron": "..."}`;cron/cron_stop 可含 `window` 运行窗口
|
||||
- `retry` — 重试策略:`{"max_attempts": 1, "delay": 60}`
|
||||
- `enabled` — 是否启用
|
||||
|
||||
**`resolve_serials` 语义**(`core/task_manager.py`,按 `target` 展开实际要跑的设备,数据源为 `core.device_pool`):
|
||||
- `mode="serial"` — 只跑目标**单台**设备
|
||||
- `mode="group"` — 取分组 serial 列表,并**过滤到设备池内**(不在池内的手动旧 IP 不参与调度)
|
||||
- `mode="all"` — **不是字面"全部设备"**:不开 `preempt` = `device_pool.list_ready()`(设备池清单 ∩ 在线,即当前可调度的在线空闲设备);开 `preempt` = 取设备池**全部在线设备**(含正在跑其他任务的,执行时逐个抢占)
|
||||
- 单台设备同时只允许一个 worker(`TaskManager._running` 互斥);`preempt` 抢占结束后,调度器会**自动重新拉起被抢占的原任务**(重试循环 `finally` 归还设备)
|
||||
|
||||
> **`/api/jobs` 校验语义**:新建/更新任务(`POST/PUT /api/jobs`)后端**只校验 `name` 非空 + `task_type` 已注册**,其余字段(target/params/schedule/retry)JSON 原样盲存、不做参数合法性校验——**编辑器是唯一参数正确性关卡**,保存 generic_steps 前务必用编辑器 validate/"测试此步骤"自校验。
|
||||
|
||||
### 2.3 DeviceGroup — 设备分组
|
||||
|
||||
设备分组存于 SQLite(`device_group` 表),便于按批次/项目/客户分组下发任务。一个 Job 可指定 `target.mode="group"`,调度器展开为组内全部设备序列号。
|
||||
设备分组存于 SQLite(`device_group` 表),便于按批次/项目/客户分组下发任务。一个 Job 可指定 `target.mode="group"`,调度器展开为组内设备序列号,并过滤到设备池内(见 §2.2 resolve_serials)。
|
||||
|
||||
### 2.4 Worker — 单设备执行线程
|
||||
|
||||
每个被调度的设备对应一个 `Worker` 实例,跑在独立线程中,继承 `BaseWorker`(`core/device_worker.py`)。Worker 负责一台设备的完整生命周期:申请设备 → 连接 u2 → setup → run_task → teardown → 释放设备。
|
||||
每个被调度的设备对应一个 `Worker` 实例,跑在独立线程中,继承 `BaseWorker`(`core/device_worker.py`)。Worker 负责一台设备的完整生命周期:`STFDevice.acquire`(serial 含冒号 → `adb connect` 直连 IP:5555;USB 无冒号 → 先本机 adb、不在则走 220 远程 adb server)→ 连接 u2 → setup → run_task → teardown → `release`(空操作,绝不 disconnect/kill-server)。**同一 serial 同时只允许一个 worker,互斥由 `TaskManager._running` 保证**(不再有 STF occupy/release)。
|
||||
|
||||
### 2.5 Action — 操作
|
||||
|
||||
@@ -206,17 +248,21 @@ while not self.stopped():
|
||||
| `cron` | `cron` | 定时启动:到 cron 时间点自动启动 worker |
|
||||
| `cron_stop` | `cron` + `stop_cron` | 定时启停:启动 cron 到点启动,停止 cron 到点停止本任务的 worker |
|
||||
|
||||
**cron_stop 模式**只停止**本 job 启动的 worker**,不影响其他正在运行的任务。典型用法:
|
||||
**cron_stop 模式**只停止**本 job 启动的 worker**,不影响其他正在运行的任务。`cron` / `cron_stop`
|
||||
还可带 `window` **运行窗口**(每天重复,支持跨午夜如 `21:00-09:00`):cron 触发点落在窗口外时
|
||||
本次不启动,调度器会找窗口内下一个触发点;未配置/非法窗口 = 不限制。**手动执行不受 window 限制**。
|
||||
典型用法:
|
||||
```json
|
||||
{
|
||||
"schedule": {
|
||||
"mode": "cron_stop",
|
||||
"cron": "0 9 * * *",
|
||||
"stop_cron": "0 18 * * *"
|
||||
"stop_cron": "0 18 * * *",
|
||||
"window": {"start": "09:00", "end": "18:00"}
|
||||
}
|
||||
}
|
||||
```
|
||||
含义:每天 9 点自动启动任务,18 点自动停止。
|
||||
含义:每天 9:00-18:00 为运行窗口;9 点自动启动任务,18 点自动停止。
|
||||
|
||||
### 2.9 心跳看门狗
|
||||
|
||||
@@ -224,12 +270,79 @@ while not self.stopped():
|
||||
|
||||
**长耗时操作必须周期性调用 `self.heartbeat()`**,否则会被误杀。
|
||||
|
||||
### 2.10 generic_steps 通用步骤任务
|
||||
|
||||
`task_type="generic_steps"`(`tasks/generic/task.py`)是把任意 App 操作编排成"步骤链"的**通用任务**:
|
||||
前端**步骤编辑器**拖拽节点 → 保存为 `params.steps`(JSON 数组)→ worker 按顺序执行。顶层 steps
|
||||
只**顺序执行一次**——需要重复跑的操作必须显式放进 `loop` 节点(见下表)。任务级参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"max_duration": 0,
|
||||
"steps": [ { "id": "step_1", "type": "open_app", "label": "打开抖音", "params": {...} } ]
|
||||
}
|
||||
```
|
||||
|
||||
- `max_duration` — 最大运行时长(秒),0=不限时
|
||||
- `steps` — 步骤数组。每步 `{id, type, label, params}`;`id` 前端生成保证唯一
|
||||
- **进度上报**:`done` = 累计已执行的**非容器**步骤数(loop/group/if_el 不计入,避免监控噪音)、`total=0`、`unit="操作"`(前端显示"已执行 N 次操作")
|
||||
- **公共参数**:每步都可有 `probability`(0-100,默认 100,<100 时按百分比概率决定本次是否执行该步)
|
||||
- **嵌套深度上限 5**:loop/group 的 `children`、if_el 的 `then/else` 递归嵌套超过 5 层会被跳过并告警
|
||||
- 步骤执行会做**选择器健康跟踪**:某 selector 连续未命中达阈值记 `last_warning`,提示 App 改版导致选择器失效
|
||||
|
||||
**全量 18 种节点**(`STEP_TYPES`):
|
||||
|
||||
| type | 作用 | 必填 params | 子步骤字段 |
|
||||
| --- | --- | --- | --- |
|
||||
| `open_app` | 启动 App | `package`;`wait_home`/`home_feature` 可选 | - |
|
||||
| `stop_app` | 强制结束 App(冷启动) | `package` | - |
|
||||
| `screen_on` | 亮屏(息屏时唤醒并滑动解锁) | - | - |
|
||||
| `screen_off` | 息屏 | - | - |
|
||||
| `keep_screen` | 保持亮屏/恢复自动息屏(`svc power stayon`) | `mode`=on/off | - |
|
||||
| `key_event` | 按键(返回/Home/回车/菜单等) | `key` | - |
|
||||
| `swipe` | 滑动 | `direction`(up/down/left/right);`duration_min`/`duration_max` | - |
|
||||
| `swipe_until` | 滑动直到元素出现(可找到后点击) | `selector_type`+`selector_value`;`direction`/`max_swipes`/`click_when_found` | - |
|
||||
| `click` | 点击元素 | `selector_type`+`selector_value`;`wait_timeout` | - |
|
||||
| `click_xy` | 点击坐标(屏幕百分比,中心=50/50) | `x`/`y` | - |
|
||||
| `long_click` | 长按元素 | `selector_type`+`selector_value`;`duration` | - |
|
||||
| `wait_el` | 等待元素出现(条件等待) | `selector_type`+`selector_value`;`timeout` | - |
|
||||
| `input_text` | 在当前焦点输入框输入 | `mode`=random/fixed;`texts`(随机候选) 或 `fixed_text`;`clear_first` | - |
|
||||
| `clipboard` | 剪贴板注入(ClipInject 通道) | `text`;`paste`=是否立即粘贴 | - |
|
||||
| `wait` | 等待时长 | `min`/`max` | - |
|
||||
| `loop` | 循环块 | `loop_mode`=rounds/time/forever;`max_iterations` 或 `loop_duration` | `children` |
|
||||
| `group` | 动作组(按序执行一次,可折叠复用) | - | `children` |
|
||||
| `if_el` | 条件判断:命中→then,超时→else | `selector_type`+`selector_value`;`timeout` | `then` / `else` |
|
||||
|
||||
**选择器 `selector_type`** 允许:`xpath` / `description` / `text` / `resourceId` / `descriptionContains` /
|
||||
`className`;**仅 `if_el` 额外支持 `ocr`**(截屏 OCR 按文字匹配,UI 树里没有的文字也能找到,可选 `ocr_click` 命中后自动点击)。
|
||||
带选择器的步骤(click/long_click/swipe_until/wait_el/if_el)都必须填 `selector_value`。
|
||||
|
||||
> **静默跳过语义**:worker 对**未知 type / 缺必填**(如 `package`、`selector_value` 为空)**只打 warning 跳过,不会报错失败**——任务会"看起来成功但啥也没干"。因此写任务必须**自行校验**:用编辑器内置 validate + "测试此步骤"逐个验证选择器(见 §2.11)。
|
||||
> **权威 schema**:`tasks/generic/task.py` 的 `STEP_TYPES`(后端执行器)与 `static/admin/editor.js` 的 `STEP_LIB`(前端操作库)**必须保持同步**——改节点结构两边要一起改。
|
||||
> **AI 辅助生成**:用一句话需求 → AI 生成 generic_steps 任务(步骤 JSON → 编辑器预填 → 人工确认)的规划见 **doc/AI_TASK_GEN.md**。
|
||||
|
||||
### 2.11 自定义动作与单步测试
|
||||
|
||||
**自定义动作**(`CustomAction` 表)把常用步骤序列打包成可复用动作,供任何 generic_steps 任务拖入:
|
||||
- `POST /api/custom_actions` 只校验 **`name` 非空 + 至少 1 个步骤**,否则 400;`steps` 整段以 JSON 存库
|
||||
- **保存前前端先剥掉步骤 `id`**(`_stripIds`),避免同一动作多次拖入后 id 冲突;拖入画布时前端把该动作**展开成一个 `group` 节点**(`{type:"group", children: 动作步骤}`),**没有** `action_ref` 这类"引用型"节点——动作是复制展开而非引用
|
||||
- 更新/删除:`PUT/DELETE /api/custom_actions/<id>`(更新同样要求 name + ≥1 步)
|
||||
|
||||
**单步测试**(编辑器"测试此步骤"):`POST /api/steps/test` 传 `{serial, step}`,在指定设备上
|
||||
adb + u2 **只读连接**试执行单步并验证选择器,返回 `result` = **"命中" / "未找到" / "已执行"**
|
||||
(后端 `tasks/generic/task.py::test_step` + 前端 `editor.js _testStep`)。与运行中的任务互不干扰。
|
||||
|
||||
---
|
||||
|
||||
## 3. 新增一个 app 任务(完整步骤)
|
||||
|
||||
以"快手养号"为例。完整步骤 6 步,全部代码可直接复制。
|
||||
|
||||
> **签名提醒**:以下模板的 `Worker.__init__` / `Task.create_worker` **已去掉 `stf_client` / `stf`
|
||||
> 参数**——现行签名是 `BaseWorker.__init__(self, serial, params=None, daemon=True)`、
|
||||
> `Task.create_worker(self, serial, params)`(对照 `tasks/douyin/task.py`),新增任务照此抄,
|
||||
> 不要再带 stf 形参。
|
||||
|
||||
### 步骤 1:在 `tasks/` 下建 `kuaishou/` 子包
|
||||
|
||||
```
|
||||
@@ -378,8 +491,8 @@ DEFAULT_PARAMS = {
|
||||
class KuaishouWorker(BaseWorker):
|
||||
"""快手养号 worker。"""
|
||||
|
||||
def __init__(self, stf_client, serial, params=None):
|
||||
super().__init__(stf_client, serial, params)
|
||||
def __init__(self, serial, params=None):
|
||||
super().__init__(serial, params)
|
||||
p = {**DEFAULT_PARAMS, **(self.params or {})}
|
||||
self.watch_count = int(p["watch_count"])
|
||||
self.watch_min = float(p["watch_min"])
|
||||
@@ -477,7 +590,7 @@ class KuaishouTask(BaseTask):
|
||||
def get_action_class(cls, action_type):
|
||||
return get_action_class(action_type)
|
||||
|
||||
def create_worker(self, stf, serial, params):
|
||||
def create_worker(self, serial, params):
|
||||
merged = {**DEFAULT_PARAMS, **(params or {})}
|
||||
# actions 字段参数级深合并(保留前端没传的操作默认值)
|
||||
default_actions = DEFAULT_PARAMS["actions"]
|
||||
@@ -495,7 +608,7 @@ class KuaishouTask(BaseTask):
|
||||
merged_cfg["params"] = merged_params
|
||||
merged_actions[atype] = merged_cfg
|
||||
merged["actions"] = merged_actions
|
||||
return KuaishouWorker(stf, serial, params=merged)
|
||||
return KuaishouWorker(serial, params=merged)
|
||||
```
|
||||
|
||||
### 步骤 6:注册任务包
|
||||
@@ -526,10 +639,11 @@ from .kuaishou import task # noqa: F401 ← 新增这一行
|
||||
### 4.1 生命周期
|
||||
|
||||
```
|
||||
acquire(serial) # 向 STF 申请设备占用
|
||||
│
|
||||
STFDevice.acquire(serial) # 连上设备(互斥由 TaskManager._running 保证):
|
||||
│ # · IP:5555 → adb connect 直连(绝不 disconnect)
|
||||
│ # · USB 无冒号 → 本机 adb,不在则 220 远程 adb server
|
||||
▼
|
||||
adb connect + u2.connect # 连接 uiautomator2(带 30s 超时保护)
|
||||
u2.connect # 连接 uiautomator2(带 30s 超时保护)
|
||||
│
|
||||
▼
|
||||
setup(d) # 子类可选钩子(启动 app、授权、关闭弹窗)
|
||||
@@ -541,7 +655,7 @@ run_task(d) ◄── 必须实现 # 任务主循环
|
||||
teardown(d) # 子类可选钩子(退出 app、清理)
|
||||
│
|
||||
▼
|
||||
release(serial) # 释放 STF 占用
|
||||
STFDevice.release() # 空操作(不 disconnect、不 kill-server)
|
||||
```
|
||||
|
||||
任意阶段抛出 `DeviceOfflineError` → 立即终止,**不重试**。其他异常 → 按 Job 的 `retry` 策略重试。
|
||||
@@ -732,7 +846,7 @@ class FollowAction(BaseAction):
|
||||
`create_worker` 时执行三层合并:
|
||||
|
||||
```python
|
||||
def create_worker(self, stf, serial, params):
|
||||
def create_worker(self, serial, params):
|
||||
merged = {**DEFAULT_PARAMS, **(params or {})}
|
||||
# actions 字段参数级深合并
|
||||
default_actions = DEFAULT_PARAMS["actions"]
|
||||
@@ -751,7 +865,7 @@ def create_worker(self, stf, serial, params):
|
||||
merged_cfg["params"] = merged_params
|
||||
merged_actions[atype] = merged_cfg
|
||||
merged["actions"] = merged_actions
|
||||
return MyWorker(stf, serial, params=merged)
|
||||
return MyWorker(serial, params=merged)
|
||||
```
|
||||
|
||||
即:
|
||||
@@ -812,32 +926,36 @@ logger 名前缀决定写入哪个文件:
|
||||
|
||||
---
|
||||
|
||||
## 8. STF 设备调试
|
||||
## 8. 设备调试(STF 已摘除)
|
||||
|
||||
### 8.1 常见错误
|
||||
平台已在代码层完全摘除 OpenSTF(occupy/release、remoteConnect 桥接、网页看屏均已退役),
|
||||
调度与设备操作直接基于 adb 真实现状。迁移过程、决策与回滚方式见 **doc/STF_REMOVAL.md**,
|
||||
本文不再展开 STF 排障。以下结论在无 STF 时代仍然成立:
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| HTTP 504 | 设备掉线 / STF 卡住 | 抛 `DeviceOfflineError`,不重试 |
|
||||
| `DeviceOfflineError` | u2 连不上 / adb 远程不通 | 立即释放,跳过该设备 |
|
||||
| `present=True` 但操作失败 | STF 状态有缓存,`present` 不代表真在线 | 用前台 App 扫描复测 |
|
||||
| u2.connect 永久 hang | atx-agent 无响应 | 基类已加 30s 超时保护,超时抛异常 |
|
||||
### 8.1 不重试原则
|
||||
|
||||
### 8.2 前台 App 扫描(不打扰设备)
|
||||
`DeviceOfflineError` 一律不重试——设备掉线后短时间内不会自愈,重试只会占用调度队列并阻塞调度器。
|
||||
该错误由 `STFDevice.acquire`(adb connect 失败 / 设备不在本机与 220 远程 adb server)或 u2 连接失败
|
||||
触发,设备直接进入冷却。
|
||||
|
||||
Web 提供"扫描前台App"按钮(`/api/scan_foreground`),按设备状态分三类处理:
|
||||
### 8.2 u2.connect 30s 超时
|
||||
|
||||
`u2.connect()` 在 atx-agent 无响应时会永久 hang。基类用 `ThreadPoolExecutor + future.result(timeout=30)`
|
||||
包裹(USB 设备经 220 远程 adb server 建连接同样带 30s 保护),超时抛异常并标记 status=error。
|
||||
子类无需处理,但不要绕过超时保护在 run_task 里直接调 `u2.connect()`。
|
||||
|
||||
### 8.3 前台 App 扫描(不打扰设备)
|
||||
|
||||
Web 仍提供"扫描前台 App"按钮(`POST /api/scan_foreground`),按设备状态分类处理,**不打扰设备**:
|
||||
|
||||
| 设备状态 | 处理方式 | 是否打扰 |
|
||||
| --- | --- | --- |
|
||||
| worker 运行中 | 复用已有 ADB 连接查询 | 否 |
|
||||
| 完全空闲 | `adb connect` → `dumpsys` → `adb disconnect` | 否 |
|
||||
| 他人占用 | 标记"(他人占用)" | 否 |
|
||||
| worker 运行中(IP:5555) | 复用已有 ADB 连接(remote_adb_url)查询 | 否 |
|
||||
| worker 运行中(USB) | 经 220 远程 adb server 查询 | 否 |
|
||||
| 空闲设备 | **不主动 adb connect**,直接返回"空闲" | 否 |
|
||||
|
||||
**绝不使用 STF occupy/release**——会唤醒 STF agent 导致设备退回桌面。
|
||||
|
||||
### 8.3 不重试原则
|
||||
|
||||
`DeviceOfflineError` 一律不重试——设备掉线后短时间内不会自愈,重试只会浪费配额并阻塞调度器。让设备进入冷却,由运维/STF 恢复后再启用。
|
||||
**无"他人占用"概念**(单实例部署,设备互斥由 TaskManager._running 保证)。空闲设备不主动 connect,
|
||||
是因为 IP:5555 的 adb transport 为共享连接,反复 connect/disconnect 会扰动现有连接。
|
||||
|
||||
---
|
||||
|
||||
@@ -930,7 +1048,7 @@ finally:
|
||||
|
||||
- 多线程并发调 adb 会触发 adb server 竞争,导致连接抖动
|
||||
- **禁止**在任务代码里调 `adb kill-server`——会踢掉所有设备的连接
|
||||
- 设备申请/释放走 `stf_client`,与 adb 锁配合避免冲突
|
||||
- 设备申请/释放走 `device_pool`(清单/在线) + `STFDevice.acquire`(IP:5555 直连 / USB 走 220 远程 server),与 `adb_helper` 全局锁配合避免冲突
|
||||
|
||||
```python
|
||||
# ✅ 正确:用 adb_helper 封装
|
||||
@@ -1013,4 +1131,9 @@ self.set_progress(videos_watched=5, round_idx=3)
|
||||
- [ ] 中文输入用 `set_fastinput_ime`,加 try/except
|
||||
- [ ] adb 操作走 `adb_helper`,未自起 subprocess,未 `kill-server`
|
||||
|
||||
面向 generic_steps / 步骤编辑器:
|
||||
- [ ] 编排 generic_steps 用编辑器 validate + "测试此步骤"逐条自校验(未知 type / 缺必填只会 warning 跳过,不会报错失败)
|
||||
- [ ] 新增/修改步骤节点时,`tasks/generic/task.py` 的 `STEP_TYPES` 与 `static/admin/editor.js` 的 `STEP_LIB` 同步更新
|
||||
- [ ] 新增 task_type 后,`tasks/__init__.py` 的 import、`GET /api/task_types` 返回、前端"新建任务"下拉一致(改完需重启 web)
|
||||
|
||||
完成上述清单后,重启 web,前端单页应用即可看到新任务类型并可下发。
|
||||
|
||||
Reference in New Issue
Block a user