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 有名字/没名字两种形态都正确。
This commit is contained in:
2026-09-13 16:29:38 +08:00
parent fa07ed0804
commit 25e4fbb053
7 changed files with 165 additions and 27 deletions
+90 -4
View File
@@ -127,7 +127,7 @@ class ApkManager:
# ================== 列表 ==================
def list_all(self):
"""列出所有 APK。"""
"""列出所有 APK(**每个上传版本一行**,内部用)。"""
try:
with self._db():
rows = ApkRow.query.order_by(ApkRow.upload_time.desc()).all()
@@ -136,6 +136,42 @@ class ApkManager:
_log.error(f"列出 APK 失败: {e}")
return []
def list_latest(self):
"""按**包名**合并后的列表:一个应用一行,只显示最新版本。
为什么:同一个应用传过多个版本(v1.2 / v1.3)时,列表里会并排出现两行——
用户看到的是"同一个应用怎么重复了"。这里合并成一行(最新版),
其余版本的信息放在 `versions` 里,界面可以提示"另有 N 个旧版本"并按需清理。
管理页和设备端商店都用这个(设备上选应用时同样不该出现两行同一个 App)。
解析不出包名的 APK 各自独立成行,不做合并(避免把无关文件混在一起)。
"""
rows = self.list_all()
groups = {}
for r in rows:
pkg = (r.get("package_name") or "").strip()
key = pkg if pkg else ("__single__" + str(r.get("id")))
groups.setdefault(key, []).append(r)
def sort_key(x):
return (x.get("version_code") or 0, x.get("upload_time") or "")
out = []
for items in groups.values():
items.sort(key=sort_key, reverse=True)
top = dict(items[0])
top["version_count"] = len(items)
top["versions"] = [{
"id": i.get("id"),
"version_name": i.get("version_name") or "",
"version_code": i.get("version_code") or 0,
"size": i.get("size") or 0,
"upload_time": i.get("upload_time") or "",
} for i in items]
out.append(top)
out.sort(key=lambda x: x.get("upload_time") or "", reverse=True)
return out
# ================== 删除 ==================
def delete(self, apk_id):
"""删除 APK 文件和数据库记录。"""
@@ -161,6 +197,47 @@ class ApkManager:
_log.error(f"删除 APK 失败: {e}")
return False, str(e)
def delete_package(self, apk_id, keep_latest=False):
"""按**包名**删除:一个应用的所有版本一起处理。
keep_latest=True → 只删旧版本、保留最新那版("清理旧版本")
keep_latest=False → 整个应用删掉(所有版本)
没有包名的 APK 退回单条删除。
"""
try:
with self._db():
row = ApkRow.query.get(apk_id)
if not row:
return False, "APK 不存在"
pkg = (row.package_name or "").strip()
if not pkg:
return self.delete(apk_id)
items = ApkRow.query.filter_by(package_name=pkg).all()
items.sort(key=lambda r: ((r.version_code or 0),
(r.upload_time or "")), reverse=True)
targets = items[1:] if keep_latest else items
if not targets:
return True, "没有需要清理的旧版本"
name = row.display_name or pkg
for r in targets:
p = os.path.join(APK_DIR, r.filename)
if os.path.exists(p):
try:
os.remove(p)
except Exception as e:
_log.warning(f"删除 APK 文件失败: {e}")
db.session.delete(r)
db.session.commit()
_log.info(f"已删除 {name} 的 {len(targets)} 个版本"
f"({'保留最新' if keep_latest else '整个应用'})")
if keep_latest:
return True, f"已清理 {name} 的 {len(targets)} 个旧版本(保留最新)"
return True, f"已删除 {name}(共 {len(targets)} 个版本)"
except Exception as e:
_log.error(f"按包名删除失败: {e}")
return False, str(e)
# ================== 批量安装 ==================
def install(self, apk_id, serials):
"""批量安装 APK 到指定设备(后台线程执行)。
@@ -198,13 +275,22 @@ class ApkManager:
# 获取 worker 状态(判断设备是否运行中)
worker_status = {w["serial"]: w for w in get_all_worker_status()}
# 设备名(平台给设备起的名字,如 A08)—— 进度表里显示这个而不是型号:
# 8 台同型号时型号根本区分不了,名字才行
pool_names = {}
try:
from core import device_pool
for d in device_pool.list_configured(enabled_only=False):
if d.get("serial"):
pool_names[d["serial"]] = d.get("name") or ""
except Exception as e:
_log.warning(f"读取设备名失败(进度表将显示地址): {e}")
# 构造安装任务
items = {}
for s in serials:
dev = all_devices.get(s, {})
model = dev.get("model") or dev.get("manufacturer") or s
items[s] = {
"name": model,
"name": pool_names.get(s) or s,
"status": "pending",
"msg": "",
}