fix: 删除任务/分组持久化(重启不再复活)+ feat: APK 安装支持本机 USB 设备

This commit is contained in:
2026-08-11 08:27:33 +08:00
parent 794f40a880
commit 5ccf622942
4 changed files with 75 additions and 26 deletions
+25 -16
View File
@@ -274,27 +274,36 @@ class ApkManager:
def _install_one(self, serial, apk_path, package_name=""): def _install_one(self, serial, apk_path, package_name=""):
"""直连设备安装 APK。 """直连设备安装 APK。
serial 即设备的 adb 网络地址(IP:5555),直接 adb connect 即可。 serial 为 adb 网络地址(IP:5555)时直接 adb connect;
本机 USB 有线设备(serial 无冒号,如 ZY322XXXX)已在 adb 中,无需 connect。
不经过 STF occupy/release,安装完断开连接,app 永久保留。 不经过 STF occupy/release,安装完断开连接,app 永久保留。
""" """
from .adb_helper import _adb from .adb_helper import _adb
self._set_item(serial, "installing", f"正在连接 {serial}...") self._set_item(serial, "installing", f"正在连接 {serial}...")
try: try:
# adb connect(带重试,设备网络可能需要几次握手) if ":" in serial:
connected = False # 网络设备:adb connect(带重试,设备网络可能需要几次握手)
last_out = "" connected = False
for attempt in range(1, 4): # 最多 3 次 last_out = ""
out = _adb("connect", serial) for attempt in range(1, 4): # 最多 3 次
last_out = out out = _adb("connect", serial)
if "connected" in out.lower() and "failed" not in out.lower(): last_out = out
connected = True if "connected" in out.lower() and "failed" not in out.lower():
break connected = True
_log.info(f"[{serial}] adb connect 第{attempt}次: {out}") break
time.sleep(1 + attempt) _log.info(f"[{serial}] adb connect 第{attempt}次: {out}")
if not connected: time.sleep(1 + attempt)
_log.warning(f"[{serial}] adb connect 失败: 输出={last_out}") if not connected:
self._set_item(serial, "failed", f"adb connect 失败: {last_out[:150]}") _log.warning(f"[{serial}] adb connect 失败: 输出={last_out}")
return self._set_item(serial, "failed", f"adb connect 失败: {last_out[:150]}")
return
else:
# 本机 USB 设备:确认已在 adb devices 列表中
dev_out = _adb("devices")
if serial not in (dev_out or ""):
self._set_item(serial, "failed",
"设备未在本地 adb 列表中,请检查 USB 连接/驱动")
return
# 安装 # 安装
self._set_item(serial, "installing", "正在安装...") self._set_item(serial, "installing", "正在安装...")
+13
View File
@@ -458,6 +458,12 @@ class TaskManager:
if name in self.groups: if name in self.groups:
del self.groups[name] del self.groups[name]
self._save_groups() self._save_groups()
# 同上:显式删数据库行,避免删除的分组重启后复活
with self._db():
row = GroupRow.query.filter_by(name=name).first()
if row:
db.session.delete(row)
db.session.commit()
return True return True
return False return False
@@ -492,6 +498,13 @@ class TaskManager:
if job_id in self.jobs: if job_id in self.jobs:
del self.jobs[job_id] del self.jobs[job_id]
self._save_jobs() self._save_jobs()
# _save_jobs 只做 upsert 不会删行:必须显式删数据库行,
# 否则删除的任务重启后会从数据库重新加载回来
with self._db():
row = JobRow.query.get(job_id)
if row:
db.session.delete(row)
db.session.commit()
return True return True
return False return False
+7 -8
View File
@@ -2365,11 +2365,10 @@ async function deleteApk(id){
async function openInstallModal(apkId){ async function openInstallModal(apkId){
const apk=_apksCache.find(a=>a.id===apkId); const apk=_apksCache.find(a=>a.id===apkId);
const apkName=apk?(apk.display_name||'APK'):'APK'; const apkName=apk?(apk.display_name||'APK'):'APK';
const sr=await apiGet('/api/status');if(!sr||!sr.ok)return; const r=await apiGet('/api/apks/install/devices');
const devices=sr.devices||[]; const online=(r&&r.ok)?(r.devices||[]):[];
const online=devices.filter(d=>d.present);
if(!online.length){ if(!online.length){
showToast('没有在线设备','error');return; showToast('没有可用设备(STF 池或本机 adb 均无)','error');return;
} }
const body= const body=
'<div style="margin-bottom:12px">'+ '<div style="margin-bottom:12px">'+
@@ -2383,11 +2382,11 @@ async function openInstallModal(apkId){
'</div>'+ '</div>'+
'<div id="install-dev-list" class="device-list">'+ '<div id="install-dev-list" class="device-list">'+
online.map(d=>{ online.map(d=>{
const running=d.worker_status==='running'||d.worker_status==='connecting'; const isUsb=d.source==='usb';
const st='<span class="label label-'+(running?'success':'default')+'" style="margin-left:6px">'+(running?'运行中':'空闲')+'</span>'; const st='<span class="label label-'+(isUsb?'primary':'default')+'" style="margin-left:6px">'+(isUsb?'本地USB':(d.source==='stf'?'STF':'adb'))+'</span>';
// serial 唯一,显示 IP + 型号便于区分同型号设备 // serial 唯一,显示 IP + 型号便于区分同型号设备
const label=(d.serial||'').replace(/:5555$/,'')+' · '+(d.device_name||d.model||'设备'); const label=(d.serial||'').replace(/:5555$/,'')+' · '+(d.model||(isUsb?'USB 有线设备':'设备'));
return '<label><input type="checkbox" value="'+esc(d.serial)+'"'+(running?'':' checked')+'>'+esc(label)+st+'</label>'; return '<label><input type="checkbox" value="'+esc(d.serial)+'" checked>'+esc(label)+st+'</label>';
}).join('')+ }).join('')+
'</div>'; '</div>';
+30 -2
View File
@@ -363,7 +363,7 @@ def api_groups_update(name):
@app.route("/api/groups/<name>", methods=["DELETE"]) @app.route("/api/groups/<name>", methods=["DELETE"])
@login_required @perm_required(PERM_TASKS)
def api_groups_delete(name): def api_groups_delete(name):
if mgr.delete_group(name): if mgr.delete_group(name):
return jsonify({"ok": True, "msg": "分组已删除"}) return jsonify({"ok": True, "msg": "分组已删除"})
@@ -728,8 +728,36 @@ def api_apks_delete(apk_id):
return jsonify({"ok": False, "error": msg}), 400 return jsonify({"ok": False, "error": msg}), 400
@app.route("/api/apks/install/devices")
@perm_required(PERM_APKS)
def api_apks_install_devices():
"""可安装设备列表(应用管理):STF 在线设备池 + 本机 adb 设备(含 USB 有线连接)。
source: stf=STF 池 / usb=本机有线 adb / adb=本机网络 adb。
"""
devices, seen = [], set()
try:
for d in stf.list_all_devices():
serial = d.get("serial", "")
if serial and d.get("present") and d.get("ready"):
devices.append({"serial": serial,
"model": d.get("model") or d.get("manufacturer") or "",
"source": "stf"})
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": "",
"source": "usb" if ":" not in parts[0] else "adb"})
seen.add(parts[0])
return jsonify({"ok": True, "devices": devices})
@app.route("/api/apks/install", methods=["POST"]) @app.route("/api/apks/install", methods=["POST"])
@login_required @perm_required(PERM_APKS)
def api_apks_install(): def api_apks_install():
"""批量安装 APK 到指定设备。参数: {apk_id, serials:[]}""" """批量安装 APK 到指定设备。参数: {apk_id, serials:[]}"""
data = request.json or {} data = request.json or {}