"""应用管理 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。""" return jsonify({"ok": True, "apks": context.apk_mgr.list_all()}) @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/", methods=["DELETE"]) @perm_required(PERM_APKS) def api_apks_delete(apk_id): """删除 APK 文件和记录。""" 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 serial in device_pool.list_configured(): if serial in online: devices.append({"serial": serial, "model": "", "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": "", "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())]