两件事都是"列表看不清楚"的问题:
1) **同一个应用传多个版本会并排显示多行**(v1.2 / v1.3 各占一行),看着像重复。
现在按 **package_name 合并成一个应用一行**,显示最新版:
- `apk_manager.list_latest()`:版本按 version_code 排序取最新,其余版本放进
`versions` 字段;解析不出包名的 APK 各自独立成行(不硬合并)
- 管理页版本列显示 `1.3(另有 1 个旧版本)`,多一个「清理旧版本」按钮
- 「删除」语义改为**删掉这个应用的所有版本**(行=应用),另给
`?scope=older` 只清理旧版本;老的单条删除行为保留(不带 scope)
- **设备端商店也走同一个列表** —— 手机上不会再看到同一个 App 的 v1.2/v1.3 两行
2) **设备只显示地址**(`192.168.20.100:5555`),同型号多台时分不清是哪一台。
三处补齐设备名(平台给设备起的名字,如 A08),统一「名称 · 地址」:
- 剪贴板注入的设备勾选列表(`/api/adb/devices` 经 `_merged_device_list` 带 name)
- 应用管理的安装弹窗(`/api/apks/install/devices` 带 name)
- 应用版本管理表格(`/api/tools/appver` 结果里带 name)
- 顺带:安装进度表里的「设备」列原来显示**型号**(而且是 STF 时代残留的空 dict),
改为显示设备名
没名字的设备(不在池里)自动回退只显示地址,不显示空占位
验证:上传同包名 v1.1/v1.2/v1.3 三个版本 → 列表只出现一行(v1.3,标注共 4 个版本)、
设备端 bootstrap 也只剩 2 个应用;`scope=older` 只留最新、`scope=package` 整包删除;
三个面板在浏览器里渲染无 JS 报错,devText 有名字/没名字两种形态都正确。
141 lines
5.5 KiB
Python
141 lines
5.5 KiB
Python
"""应用管理 API(APK 上传/安装/删除/设备已装应用)。"""
|
||
import re
|
||
import shlex
|
||
import subprocess
|
||
|
||
from flask import Blueprint, jsonify, request
|
||
from flask_login import login_required
|
||
|
||
from core import device_pool
|
||
from core.adb_helper import adb_connect_light, _adb
|
||
from web import context
|
||
from web.auth import perm_required, PERM_APKS
|
||
from web.common import _merged_device_list
|
||
|
||
bp = Blueprint("apks", __name__)
|
||
|
||
@bp.route("/api/apks")
|
||
@login_required
|
||
def api_apks_list():
|
||
"""列出已上传的 APK —— **按包名合并,一个应用一行(最新版本)**。
|
||
|
||
同一个应用传过 v1.2 / v1.3 时不再并排显示两行;每行带 versions
|
||
(全部版本)与 version_count,界面可提示"另有 N 个旧版本"并按需清理。
|
||
"""
|
||
return jsonify({"ok": True, "apks": context.apk_mgr.list_latest()})
|
||
|
||
@bp.route("/api/apks/upload", methods=["POST"])
|
||
@perm_required(PERM_APKS)
|
||
def api_apks_upload():
|
||
"""上传 APK 文件(multipart/form-data,字段名 file)。"""
|
||
file = request.files.get("file")
|
||
if not file or not file.filename:
|
||
return jsonify({"ok": False, "error": "未选择文件"}), 400
|
||
info = context.apk_mgr.upload(file)
|
||
if info:
|
||
return jsonify({"ok": True, "apk": info,
|
||
"msg": f"上传成功: {info['display_name']}"})
|
||
return jsonify({"ok": False, "error": "上传失败,请检查文件格式"}), 500
|
||
|
||
@bp.route("/api/apks/<apk_id>", methods=["DELETE"])
|
||
@perm_required(PERM_APKS)
|
||
def api_apks_delete(apk_id):
|
||
"""删除 APK。
|
||
|
||
默认只删这一个版本(列表里一行 = 一个应用,所以界面通常传 scope):
|
||
?scope=package → 删掉**这个应用的所有版本**
|
||
?scope=older → 只清理**旧版本**,保留最新那版
|
||
不带 scope 时保持老行为(单条删除),给脚本/外部调用留兼容。
|
||
"""
|
||
scope = (request.args.get("scope") or "").strip()
|
||
if scope in ("package", "older"):
|
||
ok, msg = context.apk_mgr.delete_package(
|
||
apk_id, keep_latest=(scope == "older"))
|
||
else:
|
||
ok, msg = context.apk_mgr.delete(apk_id)
|
||
if ok:
|
||
return jsonify({"ok": True, "msg": msg})
|
||
return jsonify({"ok": False, "error": msg}), 400
|
||
|
||
@bp.route("/api/apks/install/devices")
|
||
@perm_required(PERM_APKS)
|
||
def api_apks_install_devices():
|
||
"""可安装设备列表(应用管理):设备池在线设备 + 本机 adb 设备(含 USB 有线连接)。
|
||
|
||
source: pool=设备池 / usb=本机有线 adb / adb=本机网络 adb。
|
||
"""
|
||
devices, seen = [], set()
|
||
try:
|
||
online = set(device_pool.list_online())
|
||
for d in device_pool.list_configured(enabled_only=False):
|
||
serial = d.get("serial")
|
||
if serial in online and serial not in seen:
|
||
devices.append({"serial": serial, "model": "",
|
||
"name": d.get("name") or "", "source": "pool"})
|
||
seen.add(serial)
|
||
except Exception:
|
||
pass
|
||
out = _adb("devices")
|
||
for line in (out or "").splitlines()[1:]:
|
||
parts = line.split()
|
||
if len(parts) >= 2 and parts[0] and parts[0] not in seen:
|
||
devices.append({"serial": parts[0], "model": "",
|
||
"name": "", "source": "usb" if ":" not in parts[0] else "adb"})
|
||
seen.add(parts[0])
|
||
return jsonify({"ok": True, "devices": devices})
|
||
|
||
@bp.route("/api/apks/install", methods=["POST"])
|
||
@perm_required(PERM_APKS)
|
||
def api_apks_install():
|
||
"""批量安装 APK 到指定设备。参数: {apk_id, serials:[]}"""
|
||
data = request.json or {}
|
||
apk_id = data.get("apk_id", "")
|
||
serials = data.get("serials", [])
|
||
ok, msg = context.apk_mgr.install(apk_id, serials)
|
||
if ok:
|
||
return jsonify({"ok": True, "msg": msg})
|
||
return jsonify({"ok": False, "error": msg}), 400
|
||
|
||
@bp.route("/api/apks/install/status")
|
||
@login_required
|
||
def api_apks_install_status():
|
||
"""获取安装任务实时状态。"""
|
||
status = context.apk_mgr.get_install_status()
|
||
return jsonify({"ok": True, "status": status})
|
||
|
||
|
||
# ================== API:维护(仅管理员) ==================
|
||
# 维护功能:STF 容器一键重启 + adb 远程终端。两个接口都仅管理员可用。
|
||
# adb 终端安全红线(技术约束):绝不 kill-server / 绝不 disconnect IP:5555——
|
||
# STF provider 共享该 adb transport,断开会让 STF 误判全部设备离线并触发重连。
|
||
_ADB_BLOCKED_PATTERNS = ("kill-server", "disconnect")
|
||
|
||
|
||
def _blocked_adb_cmd(cmd):
|
||
"""命中红线的 adb 命令(kill-server / disconnect)直接拒绝。"""
|
||
low = cmd.lower()
|
||
return any(p in low for p in _ADB_BLOCKED_PATTERNS)
|
||
|
||
|
||
def _merged_device_list():
|
||
"""本地 adb(含 USB)+ 设备池的合并列表([{serial, state}])。
|
||
|
||
供维护终端/剪贴板注入/应用版本管理等设备选择场景共用。
|
||
状态:device/offline=本机 adb 实际状态;pool=设备池已配置但本机未连接;
|
||
stf_not_ready=STF agent 未就绪(迁移期保留,阶段 3 摘除 STF 后删除)。
|
||
"""
|
||
states = {}
|
||
out = _adb("devices")
|
||
for line in (out or "").splitlines()[1:]:
|
||
parts = line.split()
|
||
if len(parts) >= 2 and parts[0] and not parts[0].startswith("*"):
|
||
states[parts[0]] = parts[1]
|
||
try:
|
||
for serial in device_pool.list_configured():
|
||
if serial not in states:
|
||
states[serial] = "pool"
|
||
except Exception:
|
||
pass
|
||
return [{"serial": s, "state": st} for s, st in sorted(states.items())]
|
||
|