feat: Add browser-only launch mode for Kaspersky bypass
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -36,16 +36,41 @@ class SidecarManager:
|
|||||||
self._reader_thread: Optional[Thread] = None
|
self._reader_thread: Optional[Thread] = None
|
||||||
self._progress_callbacks: list[Callable] = []
|
self._progress_callbacks: list[Callable] = []
|
||||||
self._last_status: Dict[str, Any] = {}
|
self._last_status: Dict[str, Any] = {}
|
||||||
|
self._is_packaged = getattr(sys, 'frozen', False)
|
||||||
|
|
||||||
# Paths
|
# 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.project_dir = Path(__file__).parent.parent.parent
|
||||||
self.sidecar_dir = self.project_dir / "sidecar"
|
self.sidecar_dir = self.project_dir / "sidecar"
|
||||||
self.transcriber_path = self.sidecar_dir / "transcriber.py"
|
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"
|
self.venv_python = self.sidecar_dir / "venv" / "bin" / "python"
|
||||||
|
print(f"[Sidecar] Development mode: transcriber={self.transcriber_path}")
|
||||||
|
|
||||||
def is_available(self) -> bool:
|
def is_available(self) -> bool:
|
||||||
"""Check if sidecar is available (files exist)."""
|
"""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]:
|
def get_status(self) -> Dict[str, Any]:
|
||||||
"""Get current sidecar status."""
|
"""Get current sidecar status."""
|
||||||
@@ -68,7 +93,6 @@ class SidecarManager:
|
|||||||
return True # Already running
|
return True # Already running
|
||||||
|
|
||||||
if not self.is_available():
|
if not self.is_available():
|
||||||
print(f"[Sidecar] Not available: transcriber={self.transcriber_path.exists()}, venv={self.venv_python.exists()}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -80,13 +104,25 @@ class SidecarManager:
|
|||||||
|
|
||||||
print(f"[Sidecar] Starting with model={env['WHISPER_MODEL']}, device={env['WHISPER_DEVICE']}, compute={env['WHISPER_COMPUTE']}")
|
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(
|
self.process = subprocess.Popen(
|
||||||
[str(self.venv_python), str(self.transcriber_path), "--server"],
|
cmd,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
env=env,
|
env=env,
|
||||||
cwd=str(self.sidecar_dir),
|
cwd=cwd,
|
||||||
bufsize=1, # Line buffered
|
bufsize=1, # Line buffered
|
||||||
text=True
|
text=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"apiBaseUrl": "http://localhost:8000/api",
|
"apiBaseUrl": "http://localhost:8000/api",
|
||||||
"uploadTimeout": 600000,
|
"uploadTimeout": 600000,
|
||||||
"appTitle": "Meeting Assistant",
|
"appTitle": "Meeting Assistant",
|
||||||
|
"ui": {
|
||||||
|
"launchBrowser": false
|
||||||
|
},
|
||||||
"whisper": {
|
"whisper": {
|
||||||
"model": "medium",
|
"model": "medium",
|
||||||
"device": "cpu",
|
"device": "cpu",
|
||||||
|
|||||||
@@ -477,6 +477,51 @@ app.whenReady().then(async () => {
|
|||||||
// Load configuration first
|
// Load configuration first
|
||||||
loadConfig();
|
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
|
// Grant microphone permission automatically
|
||||||
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
|
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
|
||||||
console.log(`Permission request: ${permission}`, details);
|
console.log(`Permission request: ${permission}`, details);
|
||||||
@@ -512,7 +557,6 @@ app.whenReady().then(async () => {
|
|||||||
startBackendSidecar();
|
startBackendSidecar();
|
||||||
|
|
||||||
// Wait for backend to be ready before creating window
|
// Wait for backend to be ready before creating window
|
||||||
const backendConfig = appConfig?.backend || {};
|
|
||||||
if (backendConfig.embedded) {
|
if (backendConfig.embedded) {
|
||||||
const ready = await waitForBackendReady();
|
const ready = await waitForBackendReady();
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
|
|||||||
Reference in New Issue
Block a user