From fd203ef771f87c3f661c849a3154fc18a051a936 Mon Sep 17 00:00:00 2001 From: egg Date: Mon, 22 Dec 2025 18:15:45 +0800 Subject: [PATCH] feat: Add browser-only launch mode for Kaspersky bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `ui.launchBrowser` config option to launch browser directly - Fix sidecar_manager to support packaged mode paths - Set BROWSER_MODE env var for backend sidecar management - Skip Electron window when browser-only mode enabled Usage: Set `"ui": { "launchBrowser": true }` in config.json to bypass Kaspersky blocking by using system browser instead. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/app/sidecar_manager.py | 54 ++++++++++++++++++++++++++++------ client/config.json | 3 ++ client/src/main.js | 46 ++++++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/backend/app/sidecar_manager.py b/backend/app/sidecar_manager.py index 3a2713a..44d78c8 100644 --- a/backend/app/sidecar_manager.py +++ b/backend/app/sidecar_manager.py @@ -36,16 +36,41 @@ class SidecarManager: self._reader_thread: Optional[Thread] = None self._progress_callbacks: list[Callable] = [] self._last_status: Dict[str, Any] = {} + self._is_packaged = getattr(sys, 'frozen', False) - # Paths - self.project_dir = Path(__file__).parent.parent.parent - self.sidecar_dir = self.project_dir / "sidecar" - self.transcriber_path = self.sidecar_dir / "transcriber.py" - self.venv_python = self.sidecar_dir / "venv" / "bin" / "python" + # Paths - detect packaged vs development mode + if self._is_packaged: + # Packaged mode: executable at resources/backend/backend/backend.exe + # Sidecar at resources/sidecar/transcriber/transcriber.exe + exec_dir = Path(sys.executable).parent.parent # up from backend/backend.exe + resources_dir = exec_dir.parent # up from backend/ to resources/ + self.sidecar_dir = resources_dir / "sidecar" / "transcriber" + self.transcriber_path = self.sidecar_dir / ("transcriber.exe" if sys.platform == "win32" else "transcriber") + self.venv_python = None # Not used in packaged mode + print(f"[Sidecar] Packaged mode: transcriber={self.transcriber_path}") + else: + # Development mode + self.project_dir = Path(__file__).parent.parent.parent + self.sidecar_dir = self.project_dir / "sidecar" + self.transcriber_path = self.sidecar_dir / "transcriber.py" + if sys.platform == "win32": + self.venv_python = self.sidecar_dir / "venv" / "Scripts" / "python.exe" + else: + self.venv_python = self.sidecar_dir / "venv" / "bin" / "python" + print(f"[Sidecar] Development mode: transcriber={self.transcriber_path}") def is_available(self) -> bool: """Check if sidecar is available (files exist).""" - return self.transcriber_path.exists() and self.venv_python.exists() + if self._is_packaged: + # In packaged mode, just check the executable + available = self.transcriber_path.exists() + print(f"[Sidecar] is_available (packaged): {available}, path={self.transcriber_path}") + return available + else: + # Development mode - need both script and venv + available = self.transcriber_path.exists() and self.venv_python.exists() + print(f"[Sidecar] is_available (dev): {available}, script={self.transcriber_path.exists()}, venv={self.venv_python.exists()}") + return available def get_status(self) -> Dict[str, Any]: """Get current sidecar status.""" @@ -68,7 +93,6 @@ class SidecarManager: return True # Already running if not self.is_available(): - print(f"[Sidecar] Not available: transcriber={self.transcriber_path.exists()}, venv={self.venv_python.exists()}") return False try: @@ -80,13 +104,25 @@ class SidecarManager: print(f"[Sidecar] Starting with model={env['WHISPER_MODEL']}, device={env['WHISPER_DEVICE']}, compute={env['WHISPER_COMPUTE']}") + # Build command based on mode + if self._is_packaged: + # Packaged mode: run the executable directly + cmd = [str(self.transcriber_path)] + cwd = str(self.sidecar_dir) + else: + # Development mode: use venv python + cmd = [str(self.venv_python), str(self.transcriber_path), "--server"] + cwd = str(self.sidecar_dir.parent) if self._is_packaged else str(self.sidecar_dir) + + print(f"[Sidecar] Command: {cmd}, cwd={cwd}") + self.process = subprocess.Popen( - [str(self.venv_python), str(self.transcriber_path), "--server"], + cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, - cwd=str(self.sidecar_dir), + cwd=cwd, bufsize=1, # Line buffered text=True ) diff --git a/client/config.json b/client/config.json index 47f9d82..e21c27f 100644 --- a/client/config.json +++ b/client/config.json @@ -2,6 +2,9 @@ "apiBaseUrl": "http://localhost:8000/api", "uploadTimeout": 600000, "appTitle": "Meeting Assistant", + "ui": { + "launchBrowser": false + }, "whisper": { "model": "medium", "device": "cpu", diff --git a/client/src/main.js b/client/src/main.js index 4d685b1..dc411e4 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -477,6 +477,51 @@ app.whenReady().then(async () => { // Load configuration first loadConfig(); + const backendConfig = appConfig?.backend || {}; + const uiConfig = appConfig?.ui || {}; + const launchBrowser = uiConfig.launchBrowser === true; + + // Browser-only mode: start backend and open browser, no Electron UI + if (launchBrowser && backendConfig.embedded) { + console.log("=== Browser-Only Mode ==="); + + // Set BROWSER_MODE so backend manages sidecar + process.env.BROWSER_MODE = "true"; + + // Start backend sidecar + startBackendSidecar(); + + // Wait for backend to be ready + const ready = await waitForBackendReady(); + if (!ready) { + const { dialog } = require("electron"); + dialog.showErrorBox( + "Backend Startup Failed", + "後端服務啟動失敗。請檢查日誌以獲取詳細信息。" + ); + app.quit(); + return; + } + + // Open browser to login page + const host = backendConfig.host || "127.0.0.1"; + const port = backendConfig.port || 8000; + const loginUrl = `http://${host}:${port}/login`; + + console.log(`Opening browser: ${loginUrl}`); + await shell.openExternal(loginUrl); + + // Keep app running in background + // On macOS, we need to handle dock visibility + if (process.platform === "darwin") { + app.dock.hide(); + } + + console.log("Backend running. Close this window or press Ctrl+C to stop."); + return; + } + + // Standard Electron mode // Grant microphone permission automatically session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => { console.log(`Permission request: ${permission}`, details); @@ -512,7 +557,6 @@ app.whenReady().then(async () => { startBackendSidecar(); // Wait for backend to be ready before creating window - const backendConfig = appConfig?.backend || {}; if (backendConfig.embedded) { const ready = await waitForBackendReady(); if (!ready) {