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:
+90
-4
@@ -127,7 +127,7 @@ class ApkManager:
|
|||||||
|
|
||||||
# ================== 列表 ==================
|
# ================== 列表 ==================
|
||||||
def list_all(self):
|
def list_all(self):
|
||||||
"""列出所有 APK。"""
|
"""列出所有 APK(**每个上传版本一行**,内部用)。"""
|
||||||
try:
|
try:
|
||||||
with self._db():
|
with self._db():
|
||||||
rows = ApkRow.query.order_by(ApkRow.upload_time.desc()).all()
|
rows = ApkRow.query.order_by(ApkRow.upload_time.desc()).all()
|
||||||
@@ -136,6 +136,42 @@ class ApkManager:
|
|||||||
_log.error(f"列出 APK 失败: {e}")
|
_log.error(f"列出 APK 失败: {e}")
|
||||||
return []
|
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):
|
def delete(self, apk_id):
|
||||||
"""删除 APK 文件和数据库记录。"""
|
"""删除 APK 文件和数据库记录。"""
|
||||||
@@ -161,6 +197,47 @@ class ApkManager:
|
|||||||
_log.error(f"删除 APK 失败: {e}")
|
_log.error(f"删除 APK 失败: {e}")
|
||||||
return False, str(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):
|
def install(self, apk_id, serials):
|
||||||
"""批量安装 APK 到指定设备(后台线程执行)。
|
"""批量安装 APK 到指定设备(后台线程执行)。
|
||||||
@@ -198,13 +275,22 @@ class ApkManager:
|
|||||||
# 获取 worker 状态(判断设备是否运行中)
|
# 获取 worker 状态(判断设备是否运行中)
|
||||||
worker_status = {w["serial"]: w for w in get_all_worker_status()}
|
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 = {}
|
items = {}
|
||||||
for s in serials:
|
for s in serials:
|
||||||
dev = all_devices.get(s, {})
|
|
||||||
model = dev.get("model") or dev.get("manufacturer") or s
|
|
||||||
items[s] = {
|
items[s] = {
|
||||||
"name": model,
|
"name": pool_names.get(s) or s,
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"msg": "",
|
"msg": "",
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-8
@@ -29,15 +29,22 @@ async function loadApks(){
|
|||||||
await fillAppsDeviceList();
|
await fillAppsDeviceList();
|
||||||
}
|
}
|
||||||
function renderApkRow(a){
|
function renderApkRow(a){
|
||||||
|
// 一行 = 一个应用(列表已按包名合并,显示最新版);旧版本给个数提示 + 清理入口
|
||||||
|
const n=a.version_count||1;
|
||||||
|
const verCell=(a.version_name?esc(a.version_name):'-')+
|
||||||
|
(n>1?' <span style="font-size:11px;color:#94a3b8">(另有 '+(n-1)+' 个旧版本)</span>':'');
|
||||||
|
const clean=n>1?'<button class="btn btn-xs" onclick="cleanOldVersions(\''+a.id+'\',\''+esc(a.display_name||'')+'\','+(n-1)+')" title="只保留最新版,删掉旧版本">清理旧版本</button> ':'';
|
||||||
|
const title=n>1?('共 '+n+' 个版本:'+ (a.versions||[]).map(v=>v.version_name||'?').join(' / ')):'';
|
||||||
return '<tr>'+
|
return '<tr>'+
|
||||||
'<td><strong>'+esc(a.display_name||'(未命名)')+'</strong></td>'+
|
'<td><strong>'+esc(a.display_name||'(未命名)')+'</strong></td>'+
|
||||||
'<td style="font-family:monospace;font-size:12px;color:#64748b">'+esc(a.package_name||'-')+'</td>'+
|
'<td style="font-family:monospace;font-size:12px;color:#64748b">'+esc(a.package_name||'-')+'</td>'+
|
||||||
'<td>'+(a.version_name?esc(a.version_name):'-')+'</td>'+
|
'<td title="'+esc(title)+'">'+verCell+'</td>'+
|
||||||
'<td>'+fmtSize(a.size)+'</td>'+
|
'<td>'+fmtSize(a.size)+'</td>'+
|
||||||
'<td style="font-size:12px;color:#94a3b8">'+esc(a.upload_time)+'</td>'+
|
'<td style="font-size:12px;color:#94a3b8">'+esc(a.upload_time)+'</td>'+
|
||||||
'<td>'+
|
'<td>'+
|
||||||
'<button class="btn btn-primary btn-xs" onclick="openInstallModal(\''+a.id+'\')">安装</button> '+
|
'<button class="btn btn-primary btn-xs" onclick="openInstallModal(\''+a.id+'\')">安装</button> '+
|
||||||
'<button class="btn btn-danger btn-xs" onclick="deleteApk(\''+a.id+'\')">删除</button>'+
|
clean+
|
||||||
|
'<button class="btn btn-danger btn-xs" onclick="deleteApk(\''+a.id+'\','+n+')">删除</button>'+
|
||||||
'</td>'+
|
'</td>'+
|
||||||
'</tr>';
|
'</tr>';
|
||||||
}
|
}
|
||||||
@@ -50,7 +57,7 @@ async function fillAppsDeviceList(){
|
|||||||
const cur=sel.value;
|
const cur=sel.value;
|
||||||
const devices=(sr.devices||[]).filter(d=>d.present);
|
const devices=(sr.devices||[]).filter(d=>d.present);
|
||||||
sel.innerHTML='<option value="">选择设备...</option>'+devices.map(d=>
|
sel.innerHTML='<option value="">选择设备...</option>'+devices.map(d=>
|
||||||
'<option value="'+esc(d.serial)+'">'+esc((d.serial||'').replace(/:5555$/,'')+' ('+(d.device_name||d.model||'未知设备')+')')+'</option>'
|
'<option value="'+esc(d.serial)+'">'+esc(devText(d.device_name||d.name, d.serial))+'</option>'
|
||||||
).join('');
|
).join('');
|
||||||
if(cur)sel.value=cur;
|
if(cur)sel.value=cur;
|
||||||
}
|
}
|
||||||
@@ -140,9 +147,20 @@ async function uploadApk(input){
|
|||||||
input.value='';
|
input.value='';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteApk(id){
|
async function deleteApk(id, versionCount){
|
||||||
if(!confirm('确认删除此APK文件?'))return;
|
const n=versionCount||1;
|
||||||
const r=await apiDelete('/api/apks/'+id);
|
const msg = n>1
|
||||||
|
? ('确认删除这个应用?\n\n它一共有 '+n+' 个版本,会**全部**删掉(想只留最新版请用「清理旧版本」)。')
|
||||||
|
: '确认删除此APK文件?';
|
||||||
|
if(!confirm(msg))return;
|
||||||
|
// 一行 = 一个应用 → 删除即删掉该包的所有版本
|
||||||
|
const r=await apiDelete('/api/apks/'+id+'?scope=package');
|
||||||
|
handleResult(r,loadApks);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanOldVersions(id, name, oldCount){
|
||||||
|
if(!confirm('清理「'+name+'」的 '+oldCount+' 个旧版本?\n\n只保留最新版,旧版本文件会被删除。'))return;
|
||||||
|
const r=await apiDelete('/api/apks/'+id+'?scope=older');
|
||||||
handleResult(r,loadApks);
|
handleResult(r,loadApks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,8 +186,8 @@ async function openInstallModal(apkId){
|
|||||||
online.map(d=>{
|
online.map(d=>{
|
||||||
const isUsb=d.source==='usb';
|
const isUsb=d.source==='usb';
|
||||||
const st='<span class="label label-'+(isUsb?'primary':'default')+'" style="margin-left:6px">'+(isUsb?'本地USB':(d.source==='pool'?'设备池':'adb'))+'</span>';
|
const st='<span class="label label-'+(isUsb?'primary':'default')+'" style="margin-left:6px">'+(isUsb?'本地USB':(d.source==='pool'?'设备池':'adb'))+'</span>';
|
||||||
// serial 唯一,显示 IP + 型号便于区分同型号设备
|
// 一律「名称 · 地址」——同型号多台时靠名称区分;没有名称的设备(不在池里)只显示地址
|
||||||
const label=(d.serial||'').replace(/:5555$/,'')+' · '+(d.model||(isUsb?'USB 有线设备':'设备'));
|
const label=devText(d.name, d.serial);
|
||||||
return '<label><input type="checkbox" value="'+esc(d.serial)+'" checked>'+esc(label)+st+'</label>';
|
return '<label><input type="checkbox" value="'+esc(d.serial)+'" checked>'+esc(label)+st+'</label>';
|
||||||
}).join('')+
|
}).join('')+
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async function loadToolsDevices(force){
|
|||||||
const stCls = d.state==='device' ? 'label-success' : 'label-warning';
|
const stCls = d.state==='device' ? 'label-success' : 'label-warning';
|
||||||
return '<label data-serial="'+esc(d.serial)+'">'
|
return '<label data-serial="'+esc(d.serial)+'">'
|
||||||
+'<input type="checkbox" value="'+esc(d.serial)+'"'+( _clipSelected.has(d.serial)?' checked':'')+' onchange="toggleClipDevice(this)">'
|
+'<input type="checkbox" value="'+esc(d.serial)+'"'+( _clipSelected.has(d.serial)?' checked':'')+' onchange="toggleClipDevice(this)">'
|
||||||
+'<span style="font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis">'+esc(d.serial)+'</span>'
|
+'<span style="font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis">'+esc(devText(d.name,d.serial))+'</span>'
|
||||||
+'<span class="label '+stCls+'">'+st+'</span></label>';
|
+'<span class="label '+stCls+'">'+st+'</span></label>';
|
||||||
}).join('');
|
}).join('');
|
||||||
status.textContent = '共 '+_clipDevices.length+' 台设备';
|
status.textContent = '共 '+_clipDevices.length+' 台设备';
|
||||||
@@ -252,12 +252,14 @@ async function queryAppVers(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function renderAppVerRow(x){
|
function renderAppVerRow(x){
|
||||||
if(x.error)return '<tr><td style="font-family:monospace">'+esc(x.serial)+'</td>'+
|
// 设备列显示「名称 · 地址」(名称来自设备池;没名字就只显示地址)
|
||||||
|
const dev='<td style="font-family:monospace">'+esc(devText(x.name,x.serial))+'</td>';
|
||||||
|
if(x.error)return '<tr>'+dev+
|
||||||
'<td><span class="label label-warning">查询失败</span></td><td colspan="2">'+esc(x.error)+'</td></tr>';
|
'<td><span class="label label-warning">查询失败</span></td><td colspan="2">'+esc(x.error)+'</td></tr>';
|
||||||
if(x.installed)return '<tr><td style="font-family:monospace">'+esc(x.serial)+'</td>'+
|
if(x.installed)return '<tr>'+dev+
|
||||||
'<td><span class="label label-success">已安装</span></td>'+
|
'<td><span class="label label-success">已安装</span></td>'+
|
||||||
'<td>'+esc(x.version_name||'')+'</td><td style="font-family:monospace">'+esc(x.version_code||'')+'</td></tr>';
|
'<td>'+esc(x.version_name||'')+'</td><td style="font-family:monospace">'+esc(x.version_code||'')+'</td></tr>';
|
||||||
return '<tr><td style="font-family:monospace">'+esc(x.serial)+'</td>'+
|
return '<tr>'+dev+
|
||||||
'<td><span class="label label-default">未安装</span></td><td>-</td><td>-</td></tr>';
|
'<td><span class="label label-default">未安装</span></td><td>-</td><td>-</td></tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-8
@@ -17,8 +17,12 @@ bp = Blueprint("apks", __name__)
|
|||||||
@bp.route("/api/apks")
|
@bp.route("/api/apks")
|
||||||
@login_required
|
@login_required
|
||||||
def api_apks_list():
|
def api_apks_list():
|
||||||
"""列出所有已上传的 APK。"""
|
"""列出已上传的 APK —— **按包名合并,一个应用一行(最新版本)**。
|
||||||
return jsonify({"ok": True, "apks": context.apk_mgr.list_all()})
|
|
||||||
|
同一个应用传过 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"])
|
@bp.route("/api/apks/upload", methods=["POST"])
|
||||||
@perm_required(PERM_APKS)
|
@perm_required(PERM_APKS)
|
||||||
@@ -36,8 +40,19 @@ def api_apks_upload():
|
|||||||
@bp.route("/api/apks/<apk_id>", methods=["DELETE"])
|
@bp.route("/api/apks/<apk_id>", methods=["DELETE"])
|
||||||
@perm_required(PERM_APKS)
|
@perm_required(PERM_APKS)
|
||||||
def api_apks_delete(apk_id):
|
def api_apks_delete(apk_id):
|
||||||
"""删除 APK 文件和记录。"""
|
"""删除 APK。
|
||||||
ok, msg = context.apk_mgr.delete(apk_id)
|
|
||||||
|
默认只删这一个版本(列表里一行 = 一个应用,所以界面通常传 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:
|
if ok:
|
||||||
return jsonify({"ok": True, "msg": msg})
|
return jsonify({"ok": True, "msg": msg})
|
||||||
return jsonify({"ok": False, "error": msg}), 400
|
return jsonify({"ok": False, "error": msg}), 400
|
||||||
@@ -52,9 +67,11 @@ def api_apks_install_devices():
|
|||||||
devices, seen = [], set()
|
devices, seen = [], set()
|
||||||
try:
|
try:
|
||||||
online = set(device_pool.list_online())
|
online = set(device_pool.list_online())
|
||||||
for serial in device_pool.list_configured():
|
for d in device_pool.list_configured(enabled_only=False):
|
||||||
if serial in online:
|
serial = d.get("serial")
|
||||||
devices.append({"serial": serial, "model": "", "source": "pool"})
|
if serial in online and serial not in seen:
|
||||||
|
devices.append({"serial": serial, "model": "",
|
||||||
|
"name": d.get("name") or "", "source": "pool"})
|
||||||
seen.add(serial)
|
seen.add(serial)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -63,7 +80,7 @@ def api_apks_install_devices():
|
|||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) >= 2 and parts[0] and parts[0] not in seen:
|
if len(parts) >= 2 and parts[0] and parts[0] not in seen:
|
||||||
devices.append({"serial": parts[0], "model": "",
|
devices.append({"serial": parts[0], "model": "",
|
||||||
"source": "usb" if ":" not in parts[0] else "adb"})
|
"name": "", "source": "usb" if ":" not in parts[0] else "adb"})
|
||||||
seen.add(parts[0])
|
seen.add(parts[0])
|
||||||
return jsonify({"ok": True, "devices": devices})
|
return jsonify({"ok": True, "devices": devices})
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -7,10 +7,12 @@ _log = get_logger("web")
|
|||||||
|
|
||||||
|
|
||||||
def _merged_device_list():
|
def _merged_device_list():
|
||||||
"""本地 adb(含 USB)+ 设备池的合并列表([{serial, state}])。
|
"""本地 adb(含 USB)+ 设备池的合并列表([{serial, name, state}])。
|
||||||
|
|
||||||
供维护终端/剪贴板注入/应用版本管理等设备选择场景共用。
|
供维护终端/剪贴板注入/应用版本管理等设备选择场景共用。
|
||||||
状态:device/offline=本机 adb 实际状态;pool=设备池已配置但本机未连接。
|
状态:device/offline=本机 adb 实际状态;pool=设备池已配置但本机未连接。
|
||||||
|
name 来自设备池(平台给设备起的名字,如 A08)—— 界面上统一按「名称 · 地址」
|
||||||
|
显示,同型号多台时才分得清是哪一台;池里没有的设备 name 为空,界面回退显示地址。
|
||||||
"""
|
"""
|
||||||
states = {}
|
states = {}
|
||||||
out = _adb("devices")
|
out = _adb("devices")
|
||||||
@@ -18,13 +20,21 @@ def _merged_device_list():
|
|||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) >= 2 and parts[0] and not parts[0].startswith("*"):
|
if len(parts) >= 2 and parts[0] and not parts[0].startswith("*"):
|
||||||
states[parts[0]] = parts[1]
|
states[parts[0]] = parts[1]
|
||||||
|
names = {}
|
||||||
|
try:
|
||||||
|
for d in device_pool.list_configured(enabled_only=False):
|
||||||
|
if d.get("serial"):
|
||||||
|
names[d["serial"]] = d.get("name") or ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
for serial in device_pool.list_configured():
|
for serial in device_pool.list_configured():
|
||||||
if serial not in states:
|
if serial not in states:
|
||||||
states[serial] = "pool"
|
states[serial] = "pool"
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return [{"serial": s, "state": st} for s, st in sorted(states.items())]
|
return [{"serial": s, "name": names.get(s, ""), "state": st}
|
||||||
|
for s, st in sorted(states.items())]
|
||||||
|
|
||||||
|
|
||||||
def _device_screen_state(serial):
|
def _device_screen_state(serial):
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ def api_dev_agent_bootstrap():
|
|||||||
from web import context
|
from web import context
|
||||||
apks = []
|
apks = []
|
||||||
try:
|
try:
|
||||||
for a in context.apk_mgr.list_all():
|
# 用「按包名合并」的列表:同一个应用只出现一行(最新版),
|
||||||
|
# 设备上不会看到同一个 App 的 v1.2 / v1.3 两行
|
||||||
|
for a in context.apk_mgr.list_latest():
|
||||||
apks.append({
|
apks.append({
|
||||||
"id": a.get("id", ""),
|
"id": a.get("id", ""),
|
||||||
"display_name": a.get("display_name") or a.get("filename") or "",
|
"display_name": a.get("display_name") or a.get("filename") or "",
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ def api_tools_appver():
|
|||||||
results[d["serial"]] = fut.result()
|
results[d["serial"]] = fut.result()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results[d["serial"]] = {"installed": False, "error": str(e)}
|
results[d["serial"]] = {"installed": False, "error": str(e)}
|
||||||
|
# 结果里带上设备名:界面按「名称 · 地址」显示,同型号多台才分得清
|
||||||
|
if isinstance(results.get(d["serial"]), dict):
|
||||||
|
results[d["serial"]]["name"] = d.get("name") or ""
|
||||||
fail = sum(1 for v in results.values() if v.get("error"))
|
fail = sum(1 for v in results.values() if v.get("error"))
|
||||||
return jsonify({"ok": True, "results": results, "fail": fail,
|
return jsonify({"ok": True, "results": results, "fail": fail,
|
||||||
"total": len(results)})
|
"total": len(results)})
|
||||||
|
|||||||
Reference in New Issue
Block a user