Files
auto_control/web/apks_api.py
T
butubb 25e4fbb053 feat: 应用列表按包名合并 + 剪贴板/安装/版本管理显示设备名
两件事都是"列表看不清楚"的问题:

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 有名字/没名字两种形态都正确。
2026-09-13 16:29:38 +08:00

141 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""应用管理 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())]