feat: 工具Tab新增应用版本管理(按包名并发查询所有设备安装版本)+ 文档同步更新(README/API/ARCHITECTURE/DEPLOY:工具Tab各工具、条件判断if_el+OCR、STF设备管理、.env密钥配置)

This commit is contained in:
2026-08-11 10:57:14 +08:00
parent 5dd605e3a0
commit 3bd56b4f59
7 changed files with 261 additions and 29 deletions
+19 -8
View File
@@ -229,14 +229,20 @@ self.set_progress(done=5, total=80, unit="视频",
| 配置项 | 默认值 | 说明 |
|-------|--------|------|
| `STF_URL` | `http://192.168.20.220:7100` | OpenSTF 服务地址 |
| `STF_TOKEN` | (需替换) | STF API Token,在 STF 个人设置里生成 |
| `STF_TOKEN` | (.env 配置) | STF API Token,在 STF 个人设置里生成(密钥类配置统一放 `.env`) |
| `ADB_PATH` | 自动识别 | adb 二进制路径,自动区分 Windows/Linux |
| `WEB_HOST` | `0.0.0.0` | Web 监听地址(0.0.0.0 支持局域网访问) |
| `WEB_PORT` | `18050` | Web 端口(避开 Windows 动态端口范围) |
| `DATA_DIR` | `data/` | 持久化数据目录 |
| `APK_DIR` | `data/apks/` | APK 文件存储目录 |
| `STF_SSH_TARGET` | `[email protected]` | 维护页重启 STF / STF 设备管理的 SSH 目标(需免密) |
| `STF_DOCKER_CONTAINER` | `stf` | 220 上 STF Docker 容器名 |
| `STF_ADB_CONTAINER` | `adb` | 220 上 adb Docker 容器名(STF 设备管理执行 adb 用) |
| `STF_SCRIPT_PATH` | `/mnt/data/openstf/connect_devices.sh` | STF 设备池脚本路径(DEVICES 列表由此维护) |
| `TAILSCALE_API_KEY` | (.env 配置) | Tailscale 管理 API key(Settings → API Access Tokens) |
| `TAILSCALE_TAILNET` | 按邮箱前缀 | tailnet 名/ID |
**必须修改的配置**:`STF_URL` 和 `STF_TOKEN`,改成你自己的 STF 服务地址和 Token。
**必须修改的配置**:`.env` 里的 `STF_TOKEN`(密钥类统一放 `.env`,不入 git);`STF_URL` 在 `config.py`。
---
@@ -244,17 +250,19 @@ self.set_progress(done=5, total=80, unit="视频",
### 页面结构
单页应用(`templates/admin/monitor.html`),6 个 Tab:
单页应用(`templates/admin/monitor.html`),7 个 Tab;任务/工具 Tab 内部再有页内子分栏:
| Tab | 功能 | 可见性 |
|-----|------|--------|
| 监控 | 设备状态大屏:在线/离线、运行任务、当前动作、进度条、前台 App、截图、停止 | 所有登录用户(设备操作按钮需"设备控制"权限) |
| 任务 | 任务计划 CRUD:新建/编辑/删除/启用停用/立即执行 | 所有登录用户可看,写操作需"任务管理"权限 |
| 应用 | APK 上传/安装/删除 | 需"应用管理"权限 |
| 任务 | 子分栏:任务计划(CRUD/启用停用/立即执行/下次运行时间)、自定义动作(打包复用) | 所有登录用户可看,写操作需"任务管理"权限 |
| 分组 | 设备分组管理:创建/编辑/删除分组 | 所有登录用户可看,写操作需"任务管理"权限 |
| 日志 | 实时日志查看:按模块切换(core/task/web/action) | 需"日志查看"权限 |
| 用户 | 用户管理:创建/删除/修改密码/分配权限 | 仅管理员 |
| 维护 | STF 容器一键重启、adb 远程终端(设备快捷选择/自动 `-s`、卸载 STF agent、重连设备)、Tailscale 管理(设备列表/改名/授权/密钥不过期/生成 auth key) | 仅管理员 |
| 维护 | STF 服务:一键重启容器 | 仅管理员 |
| 工具 | 子分栏:剪贴板注入、adb 远程终端(快捷命令/自动 `-s`/卸载 STF agent)、Tailscale 管理(改名/授权/密钥不过期/设置IP/auth key)、应用管理(APK 上传安装)、应用版本管理(按包名查所有设备版本)、设备已装应用、STF 设备管理(读写 220 脚本维护设备池) | 仅管理员 |
任务/工具 Tab 的子分栏会记住上次选中的位置;无权限的 tab 和按钮自动隐藏。
### 用户与权限
@@ -264,7 +272,7 @@ self.set_progress(done=5, total=80, unit="视频",
|--------|------|---------|
| 任务管理 `tasks` | 任务/自定义动作/分组的增删改、启停、立即执行 | 任务 Tab、分组 Tab 的写操作 |
| 设备控制 `devices` | 停止设备、释放占用、清除异常、前台扫描、元素抓取 | 监控 Tab 的设备操作按钮、步骤编辑器的"抓取元素" |
| 应用管理 `apks` | APK 上传、安装、删除 | 应用 Tab |
| 应用管理 `apks` | APK 上传、安装、删除 | 应用管理/设备已装应用(当前并入工具 Tab,工具页整体仅管理员可见) |
| 日志查看 `logs` | 日志页 | 日志 Tab |
规则:
@@ -332,10 +340,13 @@ self.set_progress(done=5, total=80, unit="视频",
| `wait_el` | 等待元素出现(条件等待,替代固定时长) |
| `input_text` | 输入文字(随机候选/指定文字,可选输入前先清空) |
| `click` | 点击元素(支持 xpath/description/resourceId/text 选择器) |
| `input_text` | 输入文字(随机候选/固定文字) |
| `wait` | 等待(可配置时长范围) |
| `loop` | 循环块(含子步骤,可配置循环次数) |
| `group` | 动作组(含子步骤,按序执行一次) |
| `if_el` | 条件判断:找元素(支持 xpath 等选择器或 **OCR识别** 截屏匹配图片文字),命中执行"找到时"分支,未命中执行"未找到时"分支;OCR 命中可自动点击。分支可嵌套循环/条件判断 |
步骤编辑器特性:操作库按分类分组、卡片可拖拽排序/跨层级嵌套(循环套循环)、☑ 多选打包自定义动作、
"测试此步骤"真机验证、"抓取元素"回填选择器、条件判断的 OCR 识别依赖 `rapidocr_onnxruntime`(跨平台)。
### 新增 App 任务
+75 -3
View File
@@ -595,9 +595,11 @@
---
## 13. 维护(仅管理员)
## 13. 维护 / 工具(仅管理员)
维护页两个接口都**仅管理员可用**(普通用户即使有业务权限也访问不了,返回 403)。
以下接口都**仅管理员可用**(普通用户即使有业务权限也访问不了,返回 403)。
维护页现仅含 STF 服务管理;adb 终端、Tailscale 管理、应用管理、STF 设备管理等
已集中到"工具"页(页内子分栏),API 不变。
### POST /api/stf/restart
@@ -672,7 +674,7 @@ adb 远程终端:用平台 adb 二进制执行任意 adb 命令(20s 超时
## 15. Tailscale 管理(仅管理员)
维护页"Tailscale 管理"分区,调用 Tailscale 官方 API v2。所有接口仅管理员可用。
工具页"Tailscale 管理"子分栏,调用 Tailscale 官方 API v2。所有接口仅管理员可用。
前置:`.env` 配置 `TAILSCALE_API_KEY`(Settings → API Access Tokens)与
`TAILSCALE_TAILNET`(tailnet 名,个人账号一般为邮箱前缀);未配置返回 502 并附提示。
设备 IP 由 tailnet 分配,API 不可修改,列表只读展示。
@@ -739,3 +741,73 @@ adb 远程终端:用平台 adb 二进制执行任意 adb 命令(20s 超时
```json
{"ok": true, "key": "tskey-auth-...", "id": "k1", "expires": "2026-08-11T01:00:00Z"}
```
---
## 16. 工具(仅管理员)
工具页(剪贴板注入 / 应用版本管理 / STF 设备管理)接口,均仅管理员可用。
### POST /api/tools/clipboard/set
剪贴板注入:把指定文字写入一台或多台设备的剪贴板。
**请求**(JSON):
```json
{"serials": ["100.100.10.11:5555", "0123456789ABCDEF"], "text": "要注入的文字"}
```
设备来源与维护终端一致(本地 adb 含 USB + STF 池)。
实现:u2 `jsonrpc.setClipboard`(实测 `cmd clipboard` 在 MIUI 上不存在),支持中文/引号/换行;
IP 设备先 adb connect(已连接跳过,绝不 disconnect),USB 设备首次自动推送 atx-agent。
**响应**:
```json
{"ok": true, "results": {"100.100.10.11:5555": {"ok": true, "msg": "已注入"}},
"ok_count": 1, "fail_count": 0, "error": null}
```
### POST /api/tools/appver
应用版本管理:查询所有设备上指定包名的安装情况与版本号(并发 10 台)。
**请求**(JSON):
```json
{"pkg": "com.ss.android.ugc.aweme"}
```
**响应**:
```json
{"ok": true, "total": 8, "fail": 0,
"results": {"100.100.10.11:5555": {"installed": true, "version_name": "28.5.0", "version_code": "280500"}}}
```
未安装返回 `installed: false`;查询失败的设备带 `error` 字段。
### GET /api/stf/devmgmt/status
STF 设备管理状态:connect_devices.sh 脚本配置的 IP 列表 + 220 上 adb 实际连接状态。
**响应**:
```json
{"ok": true,
"configured": ["100.100.10.10", "100.100.10.11"],
"connected": [{"serial": "100.100.10.10:5555", "state": "device"}]}
```
### POST /api/stf/devmgmt/add
添加设备:写入 220 上 `connect_devices.sh` 的 DEVICES 列表 + 立即 `docker exec adb adb connect`。
设备不可达时 connect 会 15 秒超时兜底(220 侧 `timeout`),但仍保留在脚本中,cron 每 5 分钟自动重试。
**请求**(JSON):`{"ip": "100.100.10.20"}`(可带 `:5555`,自动归一化)
**响应**:
```json
{"ok": true, "msgs": ["已写入脚本 /mnt/data/openstf/connect_devices.sh", "连接超时(设备当前不可达),已保留在脚本中,cron 会每 5 分钟自动重试"]}
```
### POST /api/stf/devmgmt/remove
移除设备:从脚本 DEVICES 删除 + `adb disconnect`(设备从 STF 池下线,任务不再分配)。
注意:断开的是 220(STF provider 侧)的 adb 连接,与本机任务直连的 adb 相互独立。
**请求**(JSON):`{"ip": "100.100.10.20"}`
+39 -4
View File
@@ -233,6 +233,32 @@ APK 上传/解析/批量安装。
元素树解析:递归提取每个节点的 `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 STF 设备管理(`core/stf_device_mgmt.py`)
工具页"STF 设备管理":维护 220 上 OpenSTF 设备池。
背景:220 的 adb 跑在 Docker 容器(`STF_ADB_CONTAINER`,默认 `adb`)里,设备池由
`/mnt/data/openstf/connect_devices.sh`(`STF_SCRIPT_PATH`)的 `DEVICES` 数组维护,cron 每 5 分钟补连。
| 函数 | 说明 |
|------|------|
| `status()` | 脚本配置 IP + 220 adb 实际连接状态 |
| `add_device(ip)` | 用 220 的 python3 精确改脚本 DEVICES 块 + `docker exec adb adb connect` |
| `remove_device(ip)` | 脚本删条目 + `adb disconnect` |
关键点:
- 脚本编辑用 python3 行级增删(sed 处理多行数组易误伤)
- `adb connect` 到不可达 IP 会挂 40s+,220 侧用 `timeout 15` 兜底,超时保留在脚本由 cron 重试
- 断开的是 220(STF provider 侧)的 adb 连接,与本机任务直连的 adb 相互独立
---
## 4. 任务系统设计
@@ -289,13 +315,22 @@ tasks/douyin/actions/like.py — @register_action(ACTIONS) LikeAction
### 5.2 步骤编辑器
`generic_steps` 任务的步骤编辑器:
- 左侧操作库(拖拽源)
- 中间画布(步骤卡片列表,HTML5 Drag API 排序)
`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 元素抓取模态框
### 5.3 页内子分栏
任务/工具 Tab 用通用 `showSubTab(tabId, name)` 实现页内子分栏:每个子分栏一个 `.sub-panel`,
`_activeSubs` 记住各 Tab 上次选中的子分栏。工具 Tab 集中了全部管理工具(剪贴板注入/adb 终端/
Tailscale/应用管理/应用版本/已装应用/STF 设备管理),维护 Tab 仅保留 STF 服务。
### 5.4 元素抓取模态框
独立的第二层模态框(`el-picker-overlay`,z-index 1100),不影响任务编辑窗口:
1. 选择设备 → 2. 加载截图 + 元素树 → 3. 点击元素/边界框 → 4. 回填选择器
+9 -4
View File
@@ -74,6 +74,9 @@ pip install -r requirements.txt
| uiautomator2 | >=3.0 | Android UI 自动化 |
| uiautodev | >=0.14 | UI 元素抓取 |
| pyaxmlparser | >=0.3.27 | APK 元信息解析 |
| rapidocr_onnxruntime | >=1.4 | 屏幕 OCR(条件判断的 OCR识别 选择器,中英文模型随包内置,跨平台) |
> 服务器(无显示器/Linux)环境建议把 opencv-python 换成 `opencv-python-headless`(rapidocr 依赖 cv2,两者取一)。
> uiautomator2 首次连接设备时会自动推送 atx-agent 到设备,无需手动安装。
@@ -102,16 +105,18 @@ STF_URL = "http://你的STF地址:端口"
| `WEB_HOST` | `0.0.0.0` | 仅本机访问改为 `127.0.0.1` |
| `WEB_PORT` | `18050` | 端口冲突时修改 |
| `ADB_PATH` | 自动识别 | 用自定义 adb 时修改 |
| `STF_SSH_TARGET` | `[email protected]` | 维护页"一键重启 STF 容器"的 SSH 目标(需免密登录) |
| `STF_SSH_TARGET` | `[email protected]` | 维护页"一键重启 STF 容器"与工具页"STF 设备管理"的 SSH 目标(需免密登录) |
| `STF_DOCKER_CONTAINER` | `stf` | 220 上 STF Docker 容器名(`docker ps` 查看) |
| `STF_ADB_CONTAINER` | `adb` | 220 上 adb Docker 容器名(STF 设备管理执行 `docker exec adb adb ...`) |
| `STF_SCRIPT_PATH` | `/mnt/data/openstf/connect_devices.sh` | STF 设备池脚本路径(工具页增删设备会读写其 DEVICES 列表) |
| `TAILSCALE_TAILNET` | 按邮箱前缀 | tailnet 名称/ID(个人账号一般为登录邮箱前缀) |
> **未配置 `WEB_SECRET_KEY`**:启动时随机生成(每次重启登录态失效,生产务必配置固定值)。
> **维护页 Tailscale 前置**:`.env` 写入 `TAILSCALE_API_KEY` 后重启服务;
> **工具页 Tailscale 管理前置**:`.env` 写入 `TAILSCALE_API_KEY` 后重启服务;
> 未配置时管理分区显示明确提示,不影响其他功能。
> **维护页 STF 重启前置**:本机需能免密 SSH 到部署机(把本机公钥加入目标机 `authorized_keys`),
> 且 `STF_DOCKER_CONTAINER` 与真实容器名一致;未配置好时按钮会返回明确错误。
> **维护页 STF 重启 / 工具页 STF 设备管理前置**:本机需能免密 SSH 到部署机(把本机公钥加入目标机 `authorized_keys`),
> 且 `STF_DOCKER_CONTAINER`/`STF_ADB_CONTAINER` 与真实容器名一致;未配置好时按钮会返回明确错误。
### 2.4 启动服务
+26
View File
@@ -350,6 +350,32 @@ function toggleClipDevice(cb){
else _clipSelected.delete(cb.value);
}
// ================== 工具:应用版本管理 ==================
async function queryAppVers(){
const pkg = document.getElementById('appver-pkg').value.trim();
if(!pkg){showToast('请输入包名','error');return;}
const btn = document.querySelector('#tools-sub-appver .btn-primary');
btn.disabled = true; const old = btn.textContent; btn.textContent = '查询中...';
try{
const r = await apiPost('/api/tools/appver', {pkg});
if(!r || !r.ok){showToast('查询失败: '+(r&&r.error||''),'error');return;}
const rows = Object.entries(r.results||{}).map(([serial,v])=>{
if(v.error)return '<tr><td style="font-family:monospace">'+esc(serial)+'</td>'+
'<td><span class="label label-warning">查询失败</span></td><td colspan="2">'+esc(v.error)+'</td></tr>';
if(v.installed)return '<tr><td style="font-family:monospace">'+esc(serial)+'</td>'+
'<td><span class="label label-success">已安装</span></td>'+
'<td>'+esc(v.version_name||'')+'</td><td style="font-family:monospace">'+esc(v.version_code||'')+'</td></tr>';
return '<tr><td style="font-family:monospace">'+esc(serial)+'</td>'+
'<td><span class="label label-default">未安装</span></td><td>-</td><td>-</td></tr>';
}).join('');
document.getElementById('tb-appver').innerHTML = rows || '<tr><td colspan="4" class="empty">无可用设备</td></tr>';
document.getElementById('appver-status').textContent = '共 '+(r.total||0)+' 台设备'+(r.fail?','+r.fail+' 台查询失败':'');
if(r.fail)showToast(r.fail+' 台设备查询失败,详见表格','error');
} finally {
btn.disabled = false; btn.textContent = old;
}
}
// ================== 工具:STF 设备管理 ==================
async function loadStfDevMgmt(){
const status = document.getElementById('stfdev-status');
+19 -1
View File
@@ -477,6 +477,7 @@ select.form-control{cursor:pointer}
<button class="sub-tab" data-sub="adb" onclick="showSubTab('tools','adb')">adb 远程终端</button>
<button class="sub-tab" data-sub="ts" onclick="showSubTab('tools','ts')">Tailscale 管理</button>
<button class="sub-tab" data-sub="apks" onclick="showSubTab('tools','apks')">应用管理</button>
<button class="sub-tab" data-sub="appver" onclick="showSubTab('tools','appver')">应用版本管理</button>
<button class="sub-tab" data-sub="devapps" onclick="showSubTab('tools','devapps')">设备已装应用</button>
<button class="sub-tab" data-sub="stfdev" onclick="showSubTab('tools','stfdev')">STF 设备管理</button>
</div>
@@ -588,7 +589,24 @@ select.form-control{cursor:pointer}
<div class="list-pager" id="pager-apks"></div>
</div>
<!-- 子分栏 5:设备已装应用 -->
<!-- 子分栏 5:应用版本管理 -->
<div class="sub-panel" id="tools-sub-appver">
<div class="toolbar">
<input type="text" id="appver-pkg" class="form-control" style="width:340px;font-family:monospace"
placeholder="包名,如 com.ss.android.ugc.aweme" onkeydown="if(event.key==='Enter')queryAppVers()">
<button class="btn btn-primary" onclick="queryAppVers()">查询版本</button>
<span id="appver-status" class="text-muted"></span>
</div>
<table class="table">
<thead><tr>
<th>设备</th><th>状态</th><th>版本号</th><th>versionCode</th>
</tr></thead>
<tbody id="tb-appver"><tr><td colspan="4" class="empty">输入包名后点击"查询版本"</td></tr></tbody>
</table>
<div class="help">查询所有设备(本地 adb 含 USB + STF 池)上该包名的安装情况与版本号,用于对比各设备 App 版本是否一致;未连接的 STF 设备会自动连接(绝不断开,不影响任务)。</div>
</div>
<!-- 子分栏 6:设备已装应用 -->
<div class="sub-panel" id="tools-sub-devapps">
<div class="toolbar">
<select id="apps-device" class="form-control" style="width:280px;display:inline-block">
+74 -9
View File
@@ -23,7 +23,7 @@ import shlex
import re
import subprocess
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError as FuturesTimeout
from flask import (Flask, jsonify, request, redirect, url_for, render_template,
Response, make_response, session)
@@ -880,13 +880,10 @@ def _blocked_adb_cmd(cmd):
return any(p in low for p in _ADB_BLOCKED_PATTERNS)
@app.route("/api/adb/devices")
@admin_required
def api_adb_devices():
"""维护终端设备列表(仅管理员):本地 adb 已连接 + STF 在线设备池。
def _merged_device_list():
"""本地 adb(含 USB)+ STF 在线设备池的合并列表([{serial, state}])。
供终端设备选择器使用——选中后自动附加 `-s <serial>`,
离线/未连接的设备也可选,配合"重连设备"按钮恢复。
供维护终端/剪贴板注入/应用版本管理等设备选择场景共用。
"""
states = {}
out = _adb("devices")
@@ -901,8 +898,18 @@ def api_adb_devices():
states[serial] = "stf"
except Exception:
pass
devices = [{"serial": s, "state": st} for s, st in sorted(states.items())]
return jsonify({"ok": True, "devices": devices})
return [{"serial": s, "state": st} for s, st in sorted(states.items())]
@app.route("/api/adb/devices")
@admin_required
def api_adb_devices():
"""维护终端设备列表(仅管理员):本地 adb 已连接 + STF 在线设备池。
供终端设备选择器使用——选中后自动附加 `-s <serial>`,
离线/未连接的设备也可选,配合"重连设备"按钮恢复。
"""
return jsonify({"ok": True, "devices": _merged_device_list()})
@app.route("/api/adb/cmd", methods=["POST"])
@@ -1032,6 +1039,64 @@ def api_stf_devmgmt_remove():
return jsonify({"ok": False, "error": str(e)}), 502
def _app_ver_on_device(serial, pkg):
"""单设备包版本查询:pm path 检查安装 → dumpsys 取 versionName/versionCode。
IP:port 设备先 adb connect(已连接自动跳过;绝不 disconnect,红线)。
查询是只读 shell 命令,无需 adb server 锁(锁只保护 connect/kill-server 类操作)。
"""
if ":" in serial:
try:
adb_connect(serial)
except Exception as e:
return {"installed": False, "error": f"adb 连接失败: {e}"}
try:
r = subprocess.run([ADB_PATH, "-s", serial, "shell", "pm", "path", pkg],
capture_output=True, timeout=15)
if b"package:" not in (r.stdout or b""):
return {"installed": False, "version_name": "", "version_code": ""}
r2 = subprocess.run([ADB_PATH, "-s", serial, "shell", "dumpsys", "package", pkg],
capture_output=True, timeout=20)
out2 = (r2.stdout or b"").decode("utf-8", errors="replace")
vm = re.search(r"versionName=(\S+)", out2)
vc = re.search(r"versionCode=(\d+)", out2)
return {"installed": True,
"version_name": vm.group(1) if vm else "",
"version_code": vc.group(1) if vc else ""}
except subprocess.TimeoutExpired:
return {"installed": False, "error": "查询超时"}
except Exception as e:
return {"installed": False, "error": str(e)}
@app.route("/api/tools/appver", methods=["POST"])
@admin_required
def api_tools_appver():
"""工具-应用版本管理:查询所有设备上指定包名的安装情况与版本号。
请求: {"pkg": "com.ss.android.ugc.aweme"}
设备来源:本地 adb(含 USB)+ STF 在线池,并发查询(最多 10 台同时)。
"""
pkg = (request.json or {}).get("pkg", "").strip()
if not re.match(r"^[A-Za-z0-9_.]+$", pkg or ""):
return jsonify({"ok": False, "error": "包名格式不正确(仅字母/数字/._)"}), 400
devices = _merged_device_list()
if not devices:
return jsonify({"ok": False, "error": "无可用设备"}), 404
results = {}
with ThreadPoolExecutor(max_workers=min(10, len(devices))) as pool:
futures = {pool.submit(_app_ver_on_device, d["serial"], pkg): d for d in devices}
for fut in as_completed(futures, timeout=90):
d = futures[fut]
try:
results[d["serial"]] = fut.result()
except Exception as e:
results[d["serial"]] = {"installed": False, "error": str(e)}
fail = sum(1 for v in results.values() if v.get("error"))
return jsonify({"ok": True, "results": results, "fail": fail,
"total": len(results)})
@app.route("/api/stf/restart", methods=["POST"])
@admin_required
def api_stf_restart():