feat: 新增通用步骤任务+元素抓取,完善全部项目文档
- 新增 generic_steps 通用步骤任务(可视化步骤编辑器编排流程,支持 open_app/click/swipe/input_text/wait/loop/group) - 新增 core/uiauto_helper.py 封装 uiautodev 元素抓取客户端 - web_server 新增元素抓取/截图/设备列表等 API - monitor.html 新增步骤编辑器、元素抓取模态框、独立关闭逻辑 - 新增 README.md 项目总览(快速上手/架构/配置/FAQ) - 新增 doc/ARCHITECTURE.md 架构详解、doc/DEPLOY.md 部署指南、doc/API.md 接口文档 - 修复 doc/TASK_DEV.md:移除已删除的 comment 引用,补充 generic 包,更新注册示例 - .gitignore 忽略 .claude/ 工具产物
This commit is contained in:
+556
@@ -0,0 +1,556 @@
|
||||
# API 接口文档
|
||||
|
||||
`platform-tools` Web 后台提供 JSON API,所有接口需登录后访问(Flask-Login session 认证)。
|
||||
|
||||
**Base URL**:`http://localhost:18050`
|
||||
|
||||
**认证方式**:Cookie Session(先 POST `/login` 获取 session cookie,后续请求带上)
|
||||
|
||||
**通用响应格式**:
|
||||
```json
|
||||
{"ok": true, "data": "..."}
|
||||
{"ok": false, "error": "错误信息"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 认证
|
||||
|
||||
### POST /login
|
||||
|
||||
用户登录。
|
||||
|
||||
**请求**(form-data):
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| username | string | 用户名 |
|
||||
| password | string | 密码 |
|
||||
|
||||
**响应**:成功重定向到 `/`,失败返回登录页(含 error 信息)。
|
||||
|
||||
### GET /logout
|
||||
|
||||
登出,重定向到登录页。
|
||||
|
||||
---
|
||||
|
||||
## 2. 页面路由
|
||||
|
||||
### GET /
|
||||
|
||||
单页应用首页(需登录)。响应头设置 `Cache-Control: no-store` 防止缓存。
|
||||
|
||||
### GET /login
|
||||
|
||||
登录页面(GET)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 设备状态
|
||||
|
||||
### GET /api/status
|
||||
|
||||
获取设备池 + Worker 综合状态(带 5 秒缓存)。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"server_time": 1700000000.0,
|
||||
"fg_scanning": false,
|
||||
"fg_last_scan": 1700000000.0,
|
||||
"devices": [
|
||||
{
|
||||
"serial": "192.168.1.100:5555",
|
||||
"model": "Pixel 6",
|
||||
"device_name": "测试机1",
|
||||
"present": true,
|
||||
"ready": true,
|
||||
"stf_occupied": false,
|
||||
"owner": "",
|
||||
"worker_status": "idle",
|
||||
"foreground_app": "空闲",
|
||||
"progress": {"done": 5, "total": 80, "unit": "视频", "action_counts": {"like": 3}},
|
||||
"current_action": "观看视频 6",
|
||||
"last_error": "",
|
||||
"running_job": "",
|
||||
"task_job": "",
|
||||
"attempt": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**worker_status 取值**:`idle` / `connecting` / `running` / `done` / `error` / `failed` / `released`
|
||||
|
||||
### POST /api/scan_foreground
|
||||
|
||||
手动触发前台 App 扫描(后台异步执行,不打扰设备)。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "msg": "扫描已启动"}
|
||||
{"ok": false, "error": "已有扫描在进行中"}
|
||||
```
|
||||
|
||||
### GET /api/devices
|
||||
|
||||
返回所有在线设备 serial 列表(供分组表单勾选用)。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "devices": ["192.168.1.100:5555", "192.168.1.101:5555"]}
|
||||
```
|
||||
|
||||
### GET /api/device/screenshot
|
||||
|
||||
获取设备当前画面截图(PNG)。
|
||||
|
||||
**参数**:
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| serial | string | 设备 serial |
|
||||
| t | int | 时间戳(避免缓存,前端自动加) |
|
||||
|
||||
**响应**:成功返回 `image/png`,失败返回 JSON 错误。
|
||||
|
||||
> 用 `adb exec-out screencap -p`,只读操作,任务运行中调用安全。
|
||||
|
||||
---
|
||||
|
||||
## 4. 任务类型
|
||||
|
||||
### GET /api/task_types
|
||||
|
||||
返回所有已注册任务类型。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"task_types": [
|
||||
{
|
||||
"task_type": "douyin_nurture",
|
||||
"name": "抖音养号",
|
||||
"description": "自动观看抖音视频...",
|
||||
"default_params": {"watch_count": 80, "...": "..."}
|
||||
},
|
||||
{
|
||||
"task_type": "generic_steps",
|
||||
"name": "通用步骤",
|
||||
"description": "通过步骤编辑器编排...",
|
||||
"default_params": {"steps": [...]}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/actions
|
||||
|
||||
返回指定任务类型支持的专属操作。
|
||||
|
||||
**参数**:
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| task_type | string | 任务类型 |
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"actions": [
|
||||
{"action_type": "like", "name": "点赞", "description": "...", "default_params": {"rate": 0.3}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 任务计划 CRUD
|
||||
|
||||
### GET /api/jobs
|
||||
|
||||
列出所有任务计划。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"jobs": [{"id": "abc123", "name": "抖音养号", "task_type": "douyin_nurture", "...": "..."}],
|
||||
"task_types": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/jobs
|
||||
|
||||
创建任务计划。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{
|
||||
"name": "抖音养号",
|
||||
"task_type": "douyin_nurture",
|
||||
"target": {"mode": "all"},
|
||||
"params": {"watch_count": 50, "actions": {"like": {"params": {"rate": 0.5}}}},
|
||||
"schedule": {"mode": "once"},
|
||||
"retry": {"max_attempts": 3, "delay": 60},
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "msg": "任务已创建", "job": {"id": "abc123", "...": "..."}}
|
||||
```
|
||||
|
||||
### PUT /api/jobs/:job_id
|
||||
|
||||
更新任务计划。只需传要更新的字段。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"params": {"watch_count": 100}}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "msg": "任务已更新", "job": {"...": "..."}}
|
||||
```
|
||||
|
||||
### DELETE /api/jobs/:job_id
|
||||
|
||||
删除任务计划。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "msg": "任务已删除"}
|
||||
```
|
||||
|
||||
### POST /api/jobs/:job_id/run
|
||||
|
||||
立即执行任务(异步,不阻塞)。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "msg": "任务 抖音养号 已触发"}
|
||||
```
|
||||
|
||||
### POST /api/jobs/:job_id/toggle
|
||||
|
||||
启用/停用任务。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"enabled": false}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "msg": "任务已停用"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 设备分组
|
||||
|
||||
### GET /api/groups
|
||||
|
||||
列出所有分组。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"groups": [
|
||||
{"name": "A组", "serials": ["192.168.1.100:5555"], "description": "测试组"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/groups
|
||||
|
||||
创建分组。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"name": "A组", "serials": ["192.168.1.100:5555"], "description": "测试组"}
|
||||
```
|
||||
|
||||
### PUT /api/groups/:name
|
||||
|
||||
更新分组。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"serials": ["192.168.1.100:5555", "192.168.1.101:5555"], "description": "更新描述"}
|
||||
```
|
||||
|
||||
### DELETE /api/groups/:name
|
||||
|
||||
删除分组。
|
||||
|
||||
---
|
||||
|
||||
## 7. 运行控制
|
||||
|
||||
### POST /api/stop_device
|
||||
|
||||
停止单台设备的 worker(并阻止后续重试)。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"serial": "192.168.1.100:5555"}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "msg": "已发送停止信号给 192.168.1.100:5555"}
|
||||
```
|
||||
|
||||
### POST /api/stop_all
|
||||
|
||||
停止所有运行中的 worker。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "stopped": ["192.168.1.100:5555", "192.168.1.101:5555"]}
|
||||
```
|
||||
|
||||
### POST /api/release
|
||||
|
||||
释放当前账户占用的所有 STF 设备(清理用)。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "released": ["192.168.1.100:5555"]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 用户管理
|
||||
|
||||
### GET /api/users
|
||||
|
||||
列出所有用户。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "users": [{"id": 1, "username": "admin", "is_admin": true}]}
|
||||
```
|
||||
|
||||
### POST /api/users
|
||||
|
||||
创建用户。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"username": "user1", "password": "pass123", "is_admin": false}
|
||||
```
|
||||
|
||||
### PUT /api/users/:uid
|
||||
|
||||
更新用户(修改密码/管理员权限)。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"password": "newpass", "is_admin": true}
|
||||
```
|
||||
|
||||
### DELETE /api/users/:uid
|
||||
|
||||
删除用户(不能删除 admin 和当前登录用户)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 日志
|
||||
|
||||
### GET /api/logs
|
||||
|
||||
查看日志文件内容。
|
||||
|
||||
**参数**:
|
||||
| 参数 | 类型 | 默认 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file | string | `core.log` | 日志文件名 |
|
||||
| lines | int | 300 | 返回最后 N 行 |
|
||||
|
||||
**可选文件**:`core.log` / `task.log` / `web.log` / `action.log`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"content": "2026-08-08 10:00:00 [INFO] [core.worker] ...",
|
||||
"file": "core.log",
|
||||
"files": {"core": "core.log", "task": "task.log", "web": "web.log", "action": "action.log"}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 自定义动作
|
||||
|
||||
### GET /api/custom_actions
|
||||
|
||||
列出所有自定义动作(步骤打包)。
|
||||
|
||||
### POST /api/custom_actions
|
||||
|
||||
创建自定义动作。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"name": "登录流程", "icon": "📦", "steps": [{"type": "click", "...": "..."}]}
|
||||
```
|
||||
|
||||
### PUT /api/custom_actions/:action_id
|
||||
|
||||
更新自定义动作。
|
||||
|
||||
### DELETE /api/custom_actions/:action_id
|
||||
|
||||
删除自定义动作。
|
||||
|
||||
---
|
||||
|
||||
## 11. 元素抓取(uiauto2)
|
||||
|
||||
### GET /api/uiauto/status
|
||||
|
||||
探测 uiauto2 本地服务是否运行。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "running": true}
|
||||
```
|
||||
|
||||
### GET /api/uiauto/devices
|
||||
|
||||
获取 uiauto2 已连接的设备列表。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "devices": [{"serial": "192.168.1.100:5555", "model": "Pixel 6"}]}
|
||||
```
|
||||
|
||||
### GET /api/uiauto/screenshot
|
||||
|
||||
通过 uiauto2 获取设备截图(JPEG)。
|
||||
|
||||
**参数**:
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| serial | string | 设备 serial |
|
||||
|
||||
**响应**:成功返回 `image/jpeg`,失败返回 JSON 错误。
|
||||
|
||||
### GET /api/uiauto/elements
|
||||
|
||||
获取设备 UI 元素树。
|
||||
|
||||
**参数**:
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| serial | string | 设备 serial |
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"elements": [
|
||||
{
|
||||
"depth": 0,
|
||||
"name": "android.widget.FrameLayout",
|
||||
"resource_id": "",
|
||||
"text": "",
|
||||
"description": "",
|
||||
"class": "android.widget.FrameLayout",
|
||||
"bounds": "[0,0][1080,2400]",
|
||||
"suggested": {"type": "xpath", "value": "//*"}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 应用管理(APK)
|
||||
|
||||
### GET /api/apks
|
||||
|
||||
列出所有已上传的 APK。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"apks": [
|
||||
{
|
||||
"id": "abc123",
|
||||
"display_name": "抖音",
|
||||
"package_name": "com.ss.android.ugc.aweme",
|
||||
"version_name": "25.0.0",
|
||||
"version_code": 2500,
|
||||
"size": 104857600,
|
||||
"upload_time": "2026-08-08 10:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/apks/upload
|
||||
|
||||
上传 APK 文件(自动解析包名/版本/应用名)。
|
||||
|
||||
**请求**(multipart/form-data):
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| file | file | APK 文件 |
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "apk": {"...": "..."}, "msg": "上传成功: 抖音"}
|
||||
```
|
||||
|
||||
### DELETE /api/apks/:apk_id
|
||||
|
||||
删除 APK 文件和记录。
|
||||
|
||||
### POST /api/apks/install
|
||||
|
||||
批量安装 APK 到指定设备。
|
||||
|
||||
**请求**(JSON):
|
||||
```json
|
||||
{"apk_id": "abc123", "serials": ["192.168.1.100:5555", "192.168.1.101:5555"]}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{"ok": true, "msg": "开始安装 抖音 到 2 台设备"}
|
||||
```
|
||||
|
||||
### GET /api/apks/install/status
|
||||
|
||||
获取安装任务实时状态。
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"status": {
|
||||
"apk_name": "抖音",
|
||||
"finished": false,
|
||||
"total": 2,
|
||||
"success": 1,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"installing": 1,
|
||||
"pending": 0,
|
||||
"items": {
|
||||
"192.168.1.100:5555": {"name": "Pixel 6", "status": "success", "msg": "安装成功(已验证)"},
|
||||
"192.168.1.101:5555": {"name": "Pixel 7", "status": "installing", "msg": "正在安装..."}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,340 @@
|
||||
# 架构详解
|
||||
|
||||
本文面向想深入理解 `platform-tools` 内部设计的开发者。如果你只想使用,看 [README.md](file:///d:/platform-tools/README.md) 即可。
|
||||
|
||||
---
|
||||
|
||||
## 1. 分层设计
|
||||
|
||||
平台按"配置 / 核心 / 任务 / 前端 / 数据 / 日志 / 工具"分层,职责清晰、互不交叉:
|
||||
|
||||
| 层 | 路径 | 职责 |
|
||||
|----|------|------|
|
||||
| 配置层 | `config.py` | 项目根配置:STF 地址、adb 路径、web 端口等基础设施。**不放任务参数** |
|
||||
| 核心层 | `core/` | 框架运行时:日志、STF 客户端、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 │ ┌─────────────────┐
|
||||
▼ ┌──────────────┐ │ STF Device / adb │
|
||||
┌──────────────┐ │ 看门狗监控 │ └─────────────────┘
|
||||
│ web_server │ └──────────────┘
|
||||
│ (Flask API) │
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ data/users.db│ SQLite 持久化(用户/分组/任务/自定义动作/APK记录)
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### 任务执行流程
|
||||
|
||||
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 STFClient(`core/stf_client.py`)
|
||||
|
||||
OpenSTF REST API 封装,负责设备池管理。
|
||||
|
||||
**关键设计**:
|
||||
- 所有请求带 `timeout=(5, 15)`,避免 STF 网关 504 时卡死
|
||||
- `remote_connect` 带重试(3 次),STF provider 慢启动时需要
|
||||
- 错误分类:`DeviceOfflineError`(不重试)/ `STFNetworkError` / `DeviceConflictError`(自动重试)
|
||||
- 占用冲突自动清理重试(偶发 "already in use" 脏状态)
|
||||
|
||||
**核心方法**:
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `list_all_devices()` | 返回 STF 上所有设备(含状态) |
|
||||
| `list_free_devices()` | 返回可占用的空闲设备 |
|
||||
| `list_my_devices()` | 返回当前账户已占用的设备 |
|
||||
| `occupy(serial)` | 占用设备(冲突自动重试) |
|
||||
| `release(serial)` | 释放设备(含远程断开) |
|
||||
| `remote_connect(serial)` | 建立远程 ADB 隧道,返回 remoteConnectUrl |
|
||||
| `remote_disconnect(serial)` | 断开远程 ADB 隧道 |
|
||||
|
||||
### 3.2 STFDevice + BaseWorker(`core/device_worker.py`)
|
||||
|
||||
**STFDevice** — 单设备生命周期管理:
|
||||
- `acquire()`:占用 → remoteConnect → adb connect
|
||||
- `release()`:adb disconnect → remoteDisconnect → STF release
|
||||
- 直连模式:serial 为 `IP:5555` 时直接 adb connect,不走 STF 桥接
|
||||
|
||||
**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 秒缓存,STF 请求慢时不阻塞前端。
|
||||
|
||||
### 3.4 前台 App 扫描器(`_ForegroundScanner`)
|
||||
|
||||
**设计原则:不打扰设备**,扫描不会让设备退出当前 App。
|
||||
|
||||
| 设备状态 | 处理方式 | 是否打扰 |
|
||||
|---------|---------|---------|
|
||||
| worker 运行中 | 复用已有 ADB 连接查询 | 否 |
|
||||
| 自己占用无 worker | STF remoteConnect 隧道查询 | 否 |
|
||||
| 完全空闲 | 返回"空闲"(不 adb connect,避免 STF 误判离线) | 否 |
|
||||
| 他人占用 | 标记"(他人占用)" | 否 |
|
||||
|
||||
> **为什么不扫描空闲设备的前台 App**:STF provider 内部通过 IP:5555 维持 adb 连接。外部 adb connect/disconnect 会让 adb server 断开该 transport,连带 STF provider 的连接断开,STF 误判设备 offline 并触发重连。
|
||||
|
||||
### 3.5 ADB 操作(`core/adb_helper.py`)
|
||||
|
||||
**全局锁串行化**:`_ADB_LOCK` 确保所有 adb 调用串行执行,避免多线程竞争 adb server。
|
||||
|
||||
**铁律:绝不 kill-server**:
|
||||
- `adb kill-server` 会断开所有设备的 adb transport
|
||||
- 导致 STF provider 对全部设备误判离线并触发重连
|
||||
- 影响所有运行中的任务
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `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 认证,SHA256 密码) |
|
||||
| `DeviceGroup` | device_group | 设备分组(serials 存 JSON) |
|
||||
| `TaskJob` | task_job | 任务计划(target/params/schedule/retry 存 JSON) |
|
||||
| `CustomAction` | custom_action | 自定义动作(步骤打包,steps 存 JSON) |
|
||||
| `ApkFile` | apk_file | APK 文件元信息 |
|
||||
|
||||
**数据库初始化**(`init_db`):
|
||||
- 创建所有表
|
||||
- 首次启动创建默认管理员 `admin/admin123`
|
||||
- 自动迁移旧 `groups.json` / `jobs.json` 到 SQLite(迁移后归档为 `.migrated`)
|
||||
|
||||
### 3.7 APK 管理(`core/apk_manager.py`)
|
||||
|
||||
APK 上传/解析/批量安装。
|
||||
|
||||
**设备连接策略(直连,绕过 STF)**:
|
||||
- 直接 `adb connect serial`(serial 是 IP:5555)
|
||||
- 不经过 STF occupy/release,避免 STF release 触发 agent 清理卸载 app
|
||||
- 安装后不主动 disconnect(STF provider 共享该 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)。
|
||||
|
||||
---
|
||||
|
||||
## 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 只需:
|
||||
```json
|
||||
{"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 切换**:5 个 Tab(监控/任务/分组/日志/用户),纯 DOM 操作
|
||||
- **数据获取**:`fetch()` 调 JSON API,5 秒轮询 `/api/status`
|
||||
- **状态渲染**:设备表格、任务卡片、进度条、徽章,纯 DOM 操作
|
||||
|
||||
### 5.2 步骤编辑器
|
||||
|
||||
`generic_steps` 任务的步骤编辑器:
|
||||
- 左侧操作库(拖拽源)
|
||||
- 中间画布(步骤卡片列表,HTML5 Drag API 排序)
|
||||
- 每个步骤卡片可展开参数表单
|
||||
- 选择器字段旁有"抓取元素"按钮(独立模态框)
|
||||
|
||||
### 5.3 元素抓取模态框
|
||||
|
||||
独立的第二层模态框(`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 transport,导致 STF provider 误判全部设备离线并触发重连。连接失败就返回 False,由调用方处理。
|
||||
|
||||
### 6.4 为什么状态查询带缓存
|
||||
|
||||
STF API 响应慢(设备多时 2-5 秒),每次 `/api/status` 都打 STF 会阻塞 Flask。带 5 秒缓存,Worker 状态实时读内存(无 IO)。
|
||||
|
||||
### 6.5 为什么 DeviceOfflineError 不重试
|
||||
|
||||
设备掉线后短时间内不会自愈,重试只会浪费配额并阻塞调度器。让设备进入冷却,由运维/STF 恢复后再启用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 线程模型
|
||||
|
||||
```
|
||||
主线程(Flask)
|
||||
├── HTTP 请求处理(threaded=True,每请求一线程)
|
||||
├── APScheduler 线程(cron 触发)
|
||||
├── 看门狗线程(_Watchdog,30s 间隔)
|
||||
├── uiautodev 子进程
|
||||
└── Worker 线程(每台设备一个)
|
||||
├── _run_with_retry 线程(重试循环)
|
||||
└── BaseWorker 线程(设备生命周期 + run_task)
|
||||
```
|
||||
|
||||
**线程安全**:
|
||||
- `_WORKERS_LOCK`:保护全局 worker 状态字典
|
||||
- `_ADB_LOCK`:串行化所有 adb 调用
|
||||
- `TaskManager._lock`:保护运行中任务字典
|
||||
- `_ForegroundScanner._cache_lock`:保护前台 App 缓存
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
# 部署指南
|
||||
|
||||
本文介绍如何从零部署 `platform-tools` 设备自动化后台。
|
||||
|
||||
---
|
||||
|
||||
## 1. 环境准备
|
||||
|
||||
### 1.1 Python 环境
|
||||
|
||||
- **Python 3.10+**(推荐 3.12)
|
||||
- 安装后确认 `python --version` 和 `pip` 可用
|
||||
|
||||
```bash
|
||||
# 验证
|
||||
python --version # 应输出 3.10+
|
||||
pip --version
|
||||
```
|
||||
|
||||
### 1.2 OpenSTF 服务
|
||||
|
||||
本项目依赖 OpenSTF 管理设备池。如已有 STF 服务,跳过本节。
|
||||
|
||||
STF 部署参考官方文档:https://github.com/DeviceFarmer/stf
|
||||
|
||||
**需要获取的信息**:
|
||||
- STF 服务地址(如 `http://192.168.20.220:7100`)
|
||||
- STF API Token(在 STF 个人设置 → API Keys 里生成)
|
||||
|
||||
### 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 调试**(设置 → 开发者选项)
|
||||
- 或通过 **adb 网络连接**(设置 → 开发者选项 → 无线调试,获取 IP:5555)
|
||||
- 已接入 STF 设备池(STF 显示 present=True, ready=True)
|
||||
|
||||
---
|
||||
|
||||
## 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 元信息解析 |
|
||||
|
||||
> uiautomator2 首次连接设备时会自动推送 atx-agent 到设备,无需手动安装。
|
||||
|
||||
### 2.3 修改配置
|
||||
|
||||
编辑 `config.py`,**必须修改**以下两项:
|
||||
|
||||
```python
|
||||
STF_URL = "http://你的STF地址:端口"
|
||||
STF_TOKEN = "你的STF_API_Token"
|
||||
```
|
||||
|
||||
其他配置按需调整:
|
||||
|
||||
| 配置项 | 默认值 | 何时修改 |
|
||||
|-------|--------|---------|
|
||||
| `WEB_HOST` | `0.0.0.0` | 仅本机访问改为 `127.0.0.1` |
|
||||
| `WEB_PORT` | `18050` | 端口冲突时修改 |
|
||||
| `ADB_PATH` | 自动识别 | 用自定义 adb 时修改 |
|
||||
|
||||
### 2.4 启动服务
|
||||
|
||||
**方式一:命令行启动**
|
||||
|
||||
```bash
|
||||
python web_server.py
|
||||
```
|
||||
|
||||
**方式二:Windows 一键启动**
|
||||
|
||||
双击 `start_web.bat`:
|
||||
- 自动提权到管理员
|
||||
- 添加防火墙规则(支持局域网访问)
|
||||
- 启动 web_server
|
||||
|
||||
### 2.5 验证部署
|
||||
|
||||
1. 控制台看到 `启动服务: http://localhost:18050/` 即成功
|
||||
2. 浏览器访问 `http://localhost:18050/`
|
||||
3. 用 `admin/admin123` 登录
|
||||
4. 监控页应显示 STF 设备池中的设备
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
### 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 密码
|
||||
- **修改 SECRET_KEY**:编辑 `web_server.py`,把 `app.config["SECRET_KEY"]` 改成随机字符串
|
||||
- **限制访问**:生产环境把 `WEB_HOST` 改为 `127.0.0.1`,配合反向代理
|
||||
- **防火墙**:只开放必要端口
|
||||
|
||||
### 3.4 日志管理
|
||||
|
||||
- 日志自动滚动(10MB 一份,保留 5 份)
|
||||
- 日志目录 `logs/`,可在"日志"Tab 在线查看
|
||||
- 长期运行建议定期清理或配置 logrotate
|
||||
|
||||
### 3.5 数据备份
|
||||
|
||||
- 数据库 `data/users.db` 包含用户/分组/任务数据
|
||||
- APK 文件在 `data/apks/`
|
||||
- 建议定期备份 `data/` 目录
|
||||
|
||||
---
|
||||
|
||||
## 4. 网络配置
|
||||
|
||||
### 4.1 端口说明
|
||||
|
||||
| 端口 | 服务 | 说明 |
|
||||
|------|------|------|
|
||||
| 18050 | Web 后台 | 主服务端口(config.py 可改) |
|
||||
| 20242 | uiautodev | 元素抓取服务(自动启动,固定端口) |
|
||||
| 7100 | STF | STF 服务端口(STF 自己的配置) |
|
||||
| 5555 | adb | 设备 adb 网络端口(设备端) |
|
||||
|
||||
### 4.2 Windows 端口问题
|
||||
|
||||
Windows 可能将某些端口范围划为动态排除范围,导致绑定失败(WinError 10013)。
|
||||
|
||||
```bash
|
||||
# 查看排除的端口范围
|
||||
netsh interface ipv4 show excludedportrange protocol=tcp
|
||||
```
|
||||
|
||||
如果 18050 在排除范围内,修改 `config.py` 的 `WEB_PORT` 到一个不在排除范围内的端口。
|
||||
|
||||
或运行 `scripts/fix_web.bat`(关闭系统代理 + 添加防火墙规则 + 刷新 DNS)。
|
||||
|
||||
### 4.3 局域网访问
|
||||
|
||||
- `WEB_HOST = "0.0.0.0"` 允许局域网访问
|
||||
- 需要添加防火墙入站规则(`start_web.bat` 会自动处理)
|
||||
- 局域网其他机器访问 `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` | 端口被占用 | 改端口或杀占用进程 |
|
||||
| STF 获取设备列表失败 | STF 地址/token 错误 | 检查 `config.py` 的 `STF_URL` 和 `STF_TOKEN` |
|
||||
|
||||
### 6.2 设备连接失败
|
||||
|
||||
| 现象 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| `DeviceOfflineError` | 设备掉线/STF provider 卡死 | 检查设备网络/STF 状态 |
|
||||
| `u2.connect 超时` | atx-agent 无响应 | 重启设备/重新推送 atx-agent |
|
||||
| `adb connect failed` | 设备网络不通/端口未开放 | 检查设备 IP 和 5555 端口 |
|
||||
| STF 设备显示离线 | STF 状态缓存 | 用"扫描前台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 — STF/adb/worker/task_manager
|
||||
# logs/task.log — 任务执行
|
||||
# logs/web.log — Web 请求
|
||||
# logs/action.log — 操作执行
|
||||
```
|
||||
+33
-61
@@ -42,16 +42,18 @@ platform-tools/
|
||||
│ ├── __init__.py # create_action_registry / register_action / should_trigger
|
||||
│ └── base.py # BaseAction 全局基类
|
||||
├── tasks/ # 任务定义层
|
||||
│ ├── __init__.py # 全局 _TASK_TYPES 注册表
|
||||
│ ├── __init__.py # 全局 _TASK_TYPES 注册表(import 各任务包触发注册)
|
||||
│ ├── base.py # BaseTask 基类
|
||||
│ └── douyin/ # 抖音养号(示例)
|
||||
│ ├── douyin/ # 抖音养号(示例)
|
||||
│ │ ├── __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/ # 通用步骤任务(可视化步骤编辑器编排)
|
||||
│ ├── __init__.py
|
||||
│ ├── task.py # DEFAULT_PARAMS + Worker + Task + @register_task
|
||||
│ └── actions/
|
||||
│ ├── __init__.py # 先 from .base import ACTIONS,再 from . import like, comment
|
||||
│ ├── base.py # ACTIONS = create_action_registry()
|
||||
│ ├── like.py # @register_action(ACTIONS) LikeAction
|
||||
│ └── comment.py # @register_action(ACTIONS) CommentAction
|
||||
│ └── task.py # STEP_TYPES + Worker + Task(按 steps 顺序执行)
|
||||
├── templates/admin/
|
||||
│ ├── monitor.html # 单页应用(5 Tab,纯前端渲染)
|
||||
│ └── login.html # 登录页
|
||||
@@ -135,7 +137,7 @@ def get_task_class(task_type):
|
||||
|
||||
### 2.5 Action — 操作
|
||||
|
||||
`Action` 是任务循环里执行的"原子操作"(点赞 / 评论 / 滑动 / 关注)。每个 app 有**独立的 Action 注册表**(通过 `create_action_registry()` 创建),互不污染。全局基类 `core/actions/base.py::BaseAction` 提供通用能力。
|
||||
`Action` 是任务循环里执行的"原子操作"(点赞 / 关注 / 滑动等)。每个 app 有**独立的 Action 注册表**(通过 `create_action_registry()` 创建),互不污染。全局基类 `core/actions/base.py::BaseAction` 提供通用能力。
|
||||
|
||||
### 2.6 进度上报(通用,适配任意 app)
|
||||
|
||||
@@ -326,7 +328,7 @@ from .base import (
|
||||
|
||||
# 再导入各操作模块,触发 @register_action(ACTIONS) 注册
|
||||
from . import like # noqa: F401
|
||||
# from . import comment # 新增 action 在此 import
|
||||
# 新增 action 在此 import,例如:from . import follow
|
||||
|
||||
__all__ = [
|
||||
"BaseAction", "register_action", "create_action_registry",
|
||||
@@ -342,7 +344,7 @@ __all__ = [
|
||||
"""快手养号任务定义。
|
||||
|
||||
本文件自包含所有快手养号参数,不依赖 core 的业务配置。
|
||||
快手专属操作(点赞/评论)在 actions/ 子包里,xpath 只适用于快手。
|
||||
快手专属操作(点赞/关注等)在 actions/ 子包里,xpath 只适用于快手。
|
||||
"""
|
||||
import time
|
||||
import random
|
||||
@@ -509,8 +511,9 @@ from . import task # noqa: F401 触发 @register_task 注册
|
||||
```python
|
||||
# tasks/__init__.py
|
||||
from .base import BaseTask, register_task, list_task_types, get_task_class
|
||||
from . import douyin # noqa: F401
|
||||
from . import kuaishou # noqa: F401 ← 新增这一行
|
||||
from .douyin import task # noqa: F401
|
||||
from .generic import task # noqa: F401
|
||||
from .kuaishou import task # noqa: F401 ← 新增这一行
|
||||
```
|
||||
|
||||
完成。重启 web 后,前端任务类型下拉自动出现 `kuaishou_nurture`。
|
||||
@@ -672,8 +675,10 @@ def run_task(self, d):
|
||||
|
||||
### 5.3 完整 Action 模板
|
||||
|
||||
下面以"关注"操作为例,展示一个完整 Action 的写法(多策略定位 + 概率触发 + 异常兜底):
|
||||
|
||||
```python
|
||||
# tasks/xxx/actions/comment.py
|
||||
# tasks/xxx/actions/follow.py
|
||||
import time
|
||||
import random
|
||||
|
||||
@@ -681,17 +686,16 @@ from core.actions import BaseAction, register_action, should_trigger
|
||||
from core.logger import get_logger
|
||||
from . import ACTIONS # 从 __init__ 导入本 app 注册表
|
||||
|
||||
_log = get_logger("action.xxx.comment")
|
||||
_log = get_logger("action.xxx.follow")
|
||||
|
||||
|
||||
@register_action(ACTIONS)
|
||||
class CommentAction(BaseAction):
|
||||
action_type = "comment"
|
||||
name = "评论"
|
||||
description = "看完视频后随机发一条评论"
|
||||
class FollowAction(BaseAction):
|
||||
action_type = "follow"
|
||||
name = "关注"
|
||||
description = "看完视频后随机关注作者"
|
||||
default_params = {
|
||||
"rate": 0.1,
|
||||
"texts": ["不错", "666", "学到了"],
|
||||
}
|
||||
|
||||
def execute(self, d, params, worker):
|
||||
@@ -699,51 +703,19 @@ class CommentAction(BaseAction):
|
||||
if not should_trigger(rate):
|
||||
return False
|
||||
|
||||
texts = params.get("texts") or ["不错"]
|
||||
text = random.choice(texts)
|
||||
|
||||
# 定位评论按钮(多策略组合,失败回退)
|
||||
for desc in ("评论", "未评论", "comment"):
|
||||
# 定位关注按钮(多策略组合,失败回退)
|
||||
for desc in ("关注", "未关注", "follow"):
|
||||
el = d(description=desc)
|
||||
if el.exists:
|
||||
el.click()
|
||||
break
|
||||
else:
|
||||
_log.info("未找到评论按钮")
|
||||
_log.info("未找到关注按钮")
|
||||
return False
|
||||
|
||||
time.sleep(1.5)
|
||||
|
||||
# 找输入框
|
||||
for rid in ("com.xxx:id/comment_input",):
|
||||
el = d(resourceId=rid)
|
||||
if el.exists:
|
||||
el.click()
|
||||
break
|
||||
else:
|
||||
d.press("back")
|
||||
return False
|
||||
|
||||
time.sleep(0.8)
|
||||
|
||||
# 输入文本(中文需切输入法)
|
||||
try:
|
||||
d.set_fastinput_ime(True)
|
||||
except Exception:
|
||||
pass
|
||||
d.send_keys(text)
|
||||
time.sleep(0.6)
|
||||
|
||||
# 发送
|
||||
el = d(text="发送")
|
||||
if el.exists:
|
||||
el.click()
|
||||
_log.info(f"评论成功: {text}")
|
||||
time.sleep(1.0)
|
||||
d.press("back")
|
||||
return True
|
||||
d.press("back")
|
||||
return False
|
||||
time.sleep(1.0)
|
||||
_log.info("关注成功")
|
||||
return True
|
||||
```
|
||||
|
||||
> **返回值约定**:`True`=成功执行;`False`=主动跳过(概率未中、元素不存在等);抛异常=执行失败,由 Worker 捕获并记录。
|
||||
@@ -931,10 +903,10 @@ d.xpath('//FrameLayout[2]/LinearLayout[1]/TextView[3]').click()
|
||||
```python
|
||||
# tasks/xxx/actions/__init__.py
|
||||
from .base import ACTIONS, ... # 1. 先建注册表
|
||||
from . import like, comment # 2. 再导入各 action,触发 @register_action
|
||||
from . import like # 2. 再导入各 action,触发 @register_action
|
||||
```
|
||||
|
||||
`tasks/__init__.py` 同理:先 `from .base import BaseTask`,再 `from . import douyin`。
|
||||
`tasks/__init__.py` 同理:先 `from .base import BaseTask`,再 `from . import douyin`、`from . import generic`。
|
||||
|
||||
### 10.2 中文输入
|
||||
|
||||
@@ -1036,7 +1008,7 @@ self.set_progress(videos_watched=5, round_idx=3)
|
||||
- [ ] `tasks/<app>/actions/__init__.py` 先 `from .base import ACTIONS` 再导入各 action
|
||||
- [ ] 每个 Action 有 `action_type` / `name` / `default_params` / `execute`,返回 `True/False`
|
||||
- [ ] `Task.create_worker` 做 actions 参数级深合并(照抄抖音模板)
|
||||
- [ ] `tasks/__init__.py` 已 `from . import <app>` 注册
|
||||
- [ ] `tasks/__init__.py` 已 `from .<app> import task` 注册
|
||||
- [ ] 日志用 `get_logger("task.<app>")` / `get_logger("action.<app>.<name>")`,无 `print`
|
||||
- [ ] 定位元素优先 `description` / `descriptionContains`,resourceId 多候选,xpath 用相对定位
|
||||
- [ ] 中文输入用 `set_fastinput_ime`,加 try/except
|
||||
|
||||
Reference in New Issue
Block a user