From fe37a849712399fa2a356b04bb75258615000fd3 Mon Sep 17 00:00:00 2001 From: Nixevol Date: Mon, 28 Sep 2026 02:35:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=86=E5=8F=91=E5=8C=85=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=AD=97=E8=8A=82=E7=A0=81=EF=BC=8C=E5=B9=B6=E8=A1=A5?= =?UTF-8?q?=E4=B8=8A=20MuMu=20=E6=8E=A2=E6=B5=8B=E5=92=8C=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=88=97=E8=A1=A8=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/package.py | 32 +++++++++++++++++ server/routes/adb.py | 5 +++ server/services/adb.py | 49 +++++++++++++++++++++++++++ src-tauri/src/lib.rs | 16 +++++---- ui/src/App.vue | 46 +++++++++++++++++++++---- ui/src/views/DownloadProgressPage.vue | 40 +++++++++++++++++----- 6 files changed, 167 insertions(+), 21 deletions(-) diff --git a/scripts/package.py b/scripts/package.py index 04603a9..7cd8cc3 100644 --- a/scripts/package.py +++ b/scripts/package.py @@ -180,6 +180,37 @@ def copy_server(stage: Path) -> None: ) +def hide_server_source(stage: Path) -> None: + server = stage / "server" + python = stage / "runtime" / "python" / "python.exe" + if not python.is_file(): + raise SystemExit("还没有内置 Python,不能编译服务端") + run( + [ + str(python), + "-m", + "compileall", + "-b", + "-q", + "-s", + str(server), + "-p", + "server", + str(server), + ], + server, + ) + for path in server.rglob("*.py"): + path.unlink() + for path in list(server.rglob("__pycache__")): + if path.is_dir(): + shutil.rmtree(path) + if any(server.rglob("*.py")): + raise SystemExit("服务端源码没有从分发包里去掉") + if not any(server.rglob("*.pyc")): + raise SystemExit("服务端没有编译出字节码") + + def make_zip(stage: Path) -> Path: suffix = "windows-x64" if sys.platform == "win32" else platform.system().lower() archive = DIST / f"{STAGE_NAME}-{suffix}.zip" @@ -220,6 +251,7 @@ def main() -> None: ui_dest = stage / "dist" / "ui" shutil.copytree(DIST / "ui", ui_dest) bundle_python(stage) + hide_server_source(stage) bundle_adb(stage) bundle_ffmpeg(stage) bundle_webview(stage) diff --git a/server/routes/adb.py b/server/routes/adb.py index 76ab680..a5e55d0 100644 --- a/server/routes/adb.py +++ b/server/routes/adb.py @@ -9,3 +9,8 @@ router = APIRouter(prefix="/api/adb") @router.get("/devices") def devices(): return call(lambda: {"devices": Adb.locate().list_devices()}) + + +@router.post("/probe-mumu") +def probe_mumu(): + return call(lambda: Adb.locate().probe_mumu()) diff --git a/server/services/adb.py b/server/services/adb.py index 90f911f..c3310f9 100644 --- a/server/services/adb.py +++ b/server/services/adb.py @@ -59,6 +59,14 @@ class Adb: }) return items + def probe_mumu(self) -> dict: + ports = sorted(_listening_local_ports() & _mumu_ports()) + if not ports: + raise RuntimeError("没有发现正在运行的 MuMu。请先打开模拟器,再探测。") + for port in ports: + self._run(["connect", f"127.0.0.1:{port}"], timeout=8) + return {"devices": self.list_devices(), "ports": ports} + def read_profile(self, serial: str) -> DeviceProfile: serial = self._ready(serial) profile = DeviceProfile.builtin() @@ -192,3 +200,44 @@ class Adb: def _run(self, args: list[str], timeout: int = 20) -> subprocess.CompletedProcess: return subprocess.run([self.executable, *args], capture_output=True, timeout=timeout) + + +def _mumu_ports() -> set[int]: + ports = {7555} + for index in range(8): + base = 16384 + index * 32 + ports.add(base) + ports.add(base + 1) + port = 5555 + while port <= 5599: + ports.add(port) + port += 2 + return ports + + +def _listening_local_ports() -> set[int]: + try: + proc = subprocess.run( + ["netstat", "-ano", "-p", "tcp"], + capture_output=True, + timeout=8, + check=False, + ) + except (OSError, subprocess.TimeoutExpired): + return set() + text = proc.stdout.decode("utf-8", "replace") + if "LISTENING" not in text.upper() and "监听" not in text: + text = proc.stdout.decode("gbk", "replace") + ports = set() + for line in text.splitlines(): + upper = line.upper() + if "LISTENING" not in upper and "监听" not in line: + continue + parts = line.split() + if len(parts) < 2 or ":" not in parts[1]: + continue + host, _, port_text = parts[1].rpartition(":") + if host not in ("127.0.0.1", "0.0.0.0") or not port_text.isdigit(): + continue + ports.add(int(port_text)) + return ports diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b3c6e2e..389a574 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,6 +11,7 @@ struct Backend { child: Mutex>, } +#[cfg(debug_assertions)] fn repo_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") @@ -19,14 +20,17 @@ fn repo_root() -> PathBuf { } fn app_root() -> PathBuf { - if cfg!(debug_assertions) { + #[cfg(debug_assertions)] + { return repo_root(); } - std::env::current_exe() - .ok() - .and_then(|path| path.parent().map(Path::to_path_buf)) - .filter(|path| path.join("server").is_dir()) - .unwrap_or_else(repo_root) + #[cfg(not(debug_assertions))] + { + std::env::current_exe() + .ok() + .and_then(|path| path.parent().map(Path::to_path_buf)) + .unwrap_or_else(|| PathBuf::from(".")) + } } fn python_command(root: &Path) -> PathBuf { diff --git a/ui/src/App.vue b/ui/src/App.vue index caa7c9d..1d4446f 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -470,6 +470,15 @@ function deviceLabel(item: Device) { return `${item.model} · ${item.serial} · ${mark}`; } +function applyDeviceList(list: Device[]) { + devices.value = list; + const stillThere = devices.value.some((item) => item.serial === serial.value && item.state === "device"); + if (!stillThere) { + const ready = devices.value.filter((item) => item.state === "device"); + serial.value = ready.length === 1 ? ready[0].serial : null; + } +} + async function loadDevices() { error.value = ""; devicesBusy.value = true; @@ -479,12 +488,7 @@ async function loadDevices() { if (!response.ok) { throw new Error(payload.detail || "读取设备失败"); } - devices.value = payload.devices || []; - const stillThere = devices.value.some((item) => item.serial === serial.value && item.state === "device"); - if (!stillThere) { - const ready = devices.value.filter((item) => item.state === "device"); - serial.value = ready.length === 1 ? ready[0].serial : null; - } + applyDeviceList(payload.devices || []); } catch (exc) { devices.value = []; serial.value = null; @@ -494,6 +498,29 @@ async function loadDevices() { } } +async function probeMumu() { + error.value = ""; + devicesBusy.value = true; + try { + const response = await fetch("/api/adb/probe-mumu", { method: "POST" }); + const payload = await response.json(); + if (!response.ok) { + throw new Error(payload.detail || "没有连上 MuMu"); + } + applyDeviceList(payload.devices || []); + const ready = devices.value.filter((item) => item.state === "device"); + if (!ready.length) { + const ports = Array.isArray(payload.ports) ? payload.ports.join("、") : ""; + throw new Error(ports ? `已尝试连接 MuMu 端口 ${ports},但设备还不可用。请在模拟器里允许调试后再探测。` : "没有连上 MuMu"); + } + } catch (exc) { + if (!devices.value.length) serial.value = null; + error.value = exc instanceof Error ? exc.message : "没有连上 MuMu"; + } finally { + devicesBusy.value = false; + } +} + watch(method, (value) => { error.value = ""; if (value === "adb") loadDevices(); @@ -854,6 +881,7 @@ onUnmounted(stopLive); placeholder="请选择设备" /> 刷新 + 探测 MuMu 将注册一个新的游客账号,并读取它的个人信息。 @@ -943,6 +971,12 @@ html { color-scheme: dark; } .shell .main-pane .n-layout-content > .n-layout-scroll-container { height: 100%; overflow: hidden; + display: flex; + flex-direction: column; +} +.shell .main-pane .n-layout-content > .n-layout-scroll-container > * { + flex: 1 1 auto; + min-height: 0; } .sider { height: 100%; min-height: 100%; display: flex; flex-direction: column; box-sizing: border-box; padding: 12px 8px; } .user-nav { diff --git a/ui/src/views/DownloadProgressPage.vue b/ui/src/views/DownloadProgressPage.vue index 3a23641..d277711 100644 --- a/ui/src/views/DownloadProgressPage.vue +++ b/ui/src/views/DownloadProgressPage.vue @@ -70,9 +70,10 @@ async function remove(id: string) { {{ resumeAll ? "继续" : "暂停" }} - -
没有正在下载的剧
-
+ +
+
没有正在下载的剧
+
@@ -115,10 +116,12 @@ async function remove(id: string) {
+
- -
还没有完成的下载
-
+ +
+
还没有完成的下载
+
@@ -147,15 +150,34 @@ async function remove(id: string) {
+