diff --git a/src/mcp_feedback_enhanced/desktop/__init__.py b/src/mcp_feedback_enhanced/desktop/__init__.py index 28740b2..e1d979b 100644 --- a/src/mcp_feedback_enhanced/desktop/__init__.py +++ b/src/mcp_feedback_enhanced/desktop/__init__.py @@ -15,8 +15,10 @@ 版本: 2.3.0 """ +import asyncio import os import sys +import time from typing import Optional from ..debug import web_debug_log as debug_log @@ -64,7 +66,7 @@ def is_desktop_available() -> bool: async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> dict: """ - 啟動桌面應用收集回饋 + 啟動桌面應用收集回饋(優化版本,支援並行啟動) Args: project_dir: 專案目錄路徑 @@ -74,61 +76,120 @@ async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> di Returns: dict: 收集到的回饋資料 """ - debug_log("啟動桌面應用...") + debug_log("啟動桌面應用(並行優化版本)...") + start_time = time.time() try: - # 創建 Electron 管理器 - from .electron_manager import ElectronManager + # 並行任務1:創建 Electron 管理器和依賴檢查 + async def init_electron_manager(): + from .electron_manager import ElectronManager - manager = ElectronManager() + manager = ElectronManager() + # 預先檢查依賴(如果實現了異步版本) + if hasattr(manager, "ensure_dependencies_async"): + await manager.ensure_dependencies_async() + return manager - # 首先啟動 Web 服務器(桌面應用需要載入 Web UI) - from ..web import get_web_ui_manager + # 並行任務2:初始化 Web 管理器和會話 + async def init_web_manager(): + from ..web import get_web_ui_manager - web_manager = get_web_ui_manager() + web_manager = get_web_ui_manager() - # 創建會話 - web_manager.create_session(project_dir, summary) - session = web_manager.get_current_session() + # 確保異步初始化完成 + if hasattr(web_manager, "_init_async_components"): + await web_manager._init_async_components() - if not session: - raise RuntimeError("無法創建回饋會話") + # 創建會話 + web_manager.create_session(project_dir, summary) + session = web_manager.get_current_session() - # 啟動 Web 服務器(如果尚未啟動) - if not web_manager.server_thread or not web_manager.server_thread.is_alive(): - debug_log("啟動 Web 服務器...") - web_manager.start_server() + if not session: + raise RuntimeError("無法創建回饋會話") - # 等待 Web 服務器完全啟動 - import time + return web_manager, session - debug_log("等待 Web 服務器啟動...") - time.sleep(5) # 增加等待時間 + # 並行執行初始化任務 + debug_log("並行執行初始化任務...") + init_results = await asyncio.gather( + init_electron_manager(), init_web_manager(), return_exceptions=True + ) - # 驗證 Web 服務器是否正常運行 - if web_manager.server_thread and web_manager.server_thread.is_alive(): - debug_log(f"✅ Web 服務器成功啟動在端口: {web_manager.port}") + # 檢查初始化結果 + if isinstance(init_results[0], Exception): + raise init_results[0] + if isinstance(init_results[1], Exception): + raise init_results[1] + + manager = init_results[0] + web_result = init_results[1] + if isinstance(web_result, tuple) and len(web_result) == 2: + web_manager, session = web_result else: - raise RuntimeError("Web 服務器啟動失敗") + raise RuntimeError("Web 管理器初始化返回格式錯誤") + + init_time = time.time() - start_time + debug_log(f"並行初始化完成,耗時: {init_time:.2f}秒") + + # 並行任務3:啟動 Web 服務器 + async def start_web_server(): + if ( + not web_manager.server_thread + or not web_manager.server_thread.is_alive() + ): + debug_log("啟動 Web 服務器...") + web_manager.start_server() + + # 智能等待服務器啟動(減少固定等待時間) + await _wait_for_server_startup(web_manager) + return web_manager.port + + # 並行任務4:準備 Electron 環境 + async def prepare_electron(): + # 如果有預熱方法,在這裡調用 + if hasattr(manager, "preheat_async"): + await manager.preheat_async() + return True + + # 並行執行服務器啟動和 Electron 準備 + debug_log("並行啟動 Web 服務器和準備 Electron 環境...") + startup_results = await asyncio.gather( + start_web_server(), prepare_electron(), return_exceptions=True + ) + + if isinstance(startup_results[0], Exception): + raise startup_results[0] + if isinstance(startup_results[1], Exception): + debug_log(f"Electron 預熱失敗(非致命): {startup_results[1]}") + + server_port = startup_results[0] + + # 類型檢查:確保 manager 不是 Exception + if isinstance(manager, Exception): + raise manager # 設置 Web 服務器端口 - manager.set_web_server_port(web_manager.port) - debug_log(f"桌面應用將連接到: http://localhost:{web_manager.port}") + manager.set_web_server_port(server_port) # type: ignore[union-attr] + debug_log(f"桌面應用將連接到: http://localhost:{server_port}") # 啟動桌面應用 - desktop_success = await manager.launch_desktop_app(summary, project_dir) + desktop_success = await manager.launch_desktop_app(summary, project_dir) # type: ignore[union-attr] if desktop_success: - debug_log("桌面應用啟動成功,等待用戶回饋...") + total_startup_time = time.time() - start_time + debug_log( + f"桌面應用啟動成功,總耗時: {total_startup_time:.2f}秒,等待用戶回饋..." + ) try: # 等待用戶回饋 result = await session.wait_for_feedback(timeout) debug_log("收到桌面應用用戶回饋") - return result + return result # type: ignore[no-any-return] finally: # 確保 Electron 進程被正確清理 debug_log("清理 Electron 進程...") - await manager.cleanup_async() + if not isinstance(manager, Exception): + await manager.cleanup_async() # type: ignore[union-attr] debug_log("Electron 進程清理完成") else: debug_log("桌面應用啟動失敗,回退到 Web 模式") @@ -142,8 +203,8 @@ async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> di debug_log("回退到 Web 模式") # 確保清理 Electron 進程 try: - if "manager" in locals(): - await manager.cleanup_async() + if "manager" in locals() and not isinstance(manager, Exception): + await manager.cleanup_async() # type: ignore[union-attr] except Exception as cleanup_error: debug_log(f"清理 Electron 進程時出錯: {cleanup_error}") @@ -153,6 +214,41 @@ async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> di return await launch_web_feedback_ui(project_dir, summary, timeout) +async def _wait_for_server_startup(web_manager, max_wait: int = 10) -> bool: + """智能等待服務器啟動""" + debug_log("智能等待 Web 服務器啟動...") + + for attempt in range(max_wait): + if web_manager.server_thread and web_manager.server_thread.is_alive(): + # 嘗試連接測試 + try: + import aiohttp + + timeout = aiohttp.ClientTimeout(total=1) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get( + f"http://{web_manager.host}:{web_manager.port}/" + ) as response: + if response.status == 200: + debug_log( + f"✅ Web 服務器啟動驗證成功,耗時: {attempt + 1}秒" + ) + return True + except Exception: + pass + + await asyncio.sleep(1) + debug_log(f"等待 Web 服務器啟動... ({attempt + 1}/{max_wait})") + + # 回退到線程檢查 + if web_manager.server_thread and web_manager.server_thread.is_alive(): + debug_log("✅ Web 服務器線程運行正常(回退驗證)") + return True + + debug_log("❌ Web 服務器啟動失敗") + return False + + class ElectronManager: """Electron 管理器 - 預留接口""" diff --git a/src/mcp_feedback_enhanced/desktop/electron_manager.py b/src/mcp_feedback_enhanced/desktop/electron_manager.py index 5b76bcb..a86c02f 100644 --- a/src/mcp_feedback_enhanced/desktop/electron_manager.py +++ b/src/mcp_feedback_enhanced/desktop/electron_manager.py @@ -163,6 +163,10 @@ class ElectronManager: debug_log(f"依賴檢查失敗 [錯誤ID: {error_id}]: {e}") return False + async def ensure_dependencies_async(self) -> bool: + """異步確保依賴已安裝(別名方法)""" + return await self.ensure_dependencies() + def cleanup(self): """同步清理資源(向後兼容)""" try: diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index 03f1fbd..33963c4 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -7,6 +7,7 @@ Web UI 主要管理類 """ import asyncio +import concurrent.futures import os import threading import time @@ -96,25 +97,64 @@ class WebUIManager: self.server_thread: threading.Thread | None = None self.server_process = None - self.i18n = get_i18n_manager() - # 添加模式檢測支援 + # 初始化標記,用於追蹤異步初始化狀態 + self._initialization_complete = False + self._initialization_lock = threading.Lock() + + # 同步初始化基本組件 + self._init_basic_components() + + debug_log(f"WebUIManager 基本初始化完成,將在 {self.host}:{self.port} 啟動") + debug_log(f"回饋模式: {self.mode}") + + def _init_basic_components(self): + """同步初始化基本組件""" + # 基本組件初始化(必須同步) + self.i18n = get_i18n_manager() self.mode = self._detect_feedback_mode() self.desktop_manager: Any = None - # 如果是桌面模式,嘗試初始化桌面管理器 - if self.mode == "desktop": - self._init_desktop_manager() - - # 設置靜態文件和模板 + # 設置靜態文件和模板(必須同步) self._setup_static_files() self._setup_templates() - # 設置路由 + # 設置路由(必須同步) setup_routes(self) - debug_log(f"WebUIManager 初始化完成,將在 {self.host}:{self.port} 啟動") - debug_log(f"回饋模式: {self.mode}") + async def _init_async_components(self): + """異步初始化組件(並行執行)""" + with self._initialization_lock: + if self._initialization_complete: + return + + debug_log("開始並行初始化組件...") + start_time = time.time() + + # 創建並行任務 + tasks = [] + + # 任務1:桌面管理器初始化 + if self.mode == "desktop": + tasks.append(self._init_desktop_manager_async()) + + # 任務2:I18N 預載入(如果需要) + tasks.append(self._preload_i18n_async()) + + # 並行執行所有任務 + if tasks: + results = await asyncio.gather(*tasks, return_exceptions=True) + + # 檢查結果 + for i, result in enumerate(results): + if isinstance(result, Exception): + debug_log(f"並行初始化任務 {i} 失敗: {result}") + + with self._initialization_lock: + self._initialization_complete = True + + elapsed = time.time() - start_time + debug_log(f"並行初始化完成,耗時: {elapsed:.2f}秒") def _detect_feedback_mode(self) -> str: """檢測回饋模式""" @@ -125,7 +165,7 @@ class WebUIManager: return "auto" def _init_desktop_manager(self): - """初始化桌面管理器(如果可用)""" + """初始化桌面管理器(如果可用)- 同步版本""" try: # 嘗試導入桌面模組 from ..desktop import ElectronManager @@ -139,6 +179,46 @@ class WebUIManager: debug_log(f"桌面管理器初始化失敗: {e}") self.desktop_manager = None + async def _init_desktop_manager_async(self): + """異步初始化桌面管理器""" + + def init_desktop(): + try: + from ..desktop import ElectronManager + + manager = ElectronManager() + debug_log("桌面管理器異步初始化成功") + return manager + except ImportError: + debug_log("桌面模組不可用,將在需要時回退到 Web 模式") + return None + except Exception as e: + debug_log(f"桌面管理器異步初始化失敗: {e}") + return None + + # 在線程池中執行同步初始化 + loop = asyncio.get_event_loop() + with concurrent.futures.ThreadPoolExecutor() as executor: + self.desktop_manager = await loop.run_in_executor(executor, init_desktop) + + async def _preload_i18n_async(self): + """異步預載入 I18N 資源""" + + def preload_i18n(): + try: + # 觸發翻譯載入(如果尚未載入) + self.i18n.get_supported_languages() + debug_log("I18N 資源預載入完成") + return True + except Exception as e: + debug_log(f"I18N 資源預載入失敗: {e}") + return False + + # 在線程池中執行 + loop = asyncio.get_event_loop() + with concurrent.futures.ThreadPoolExecutor() as executor: + await loop.run_in_executor(executor, preload_i18n) + def should_use_desktop_mode(self) -> bool: """判斷是否應該使用桌面模式""" if self.mode == "web": @@ -415,7 +495,7 @@ class WebUIManager: debug_log(f"廣播消息失敗: {e}") def start_server(self): - """啟動 Web 伺服器""" + """啟動 Web 伺服器(優化版本,支援並行初始化)""" def run_server_with_retry(): max_retries = 5 @@ -435,8 +515,20 @@ class WebUIManager: access_log=False, ) - server = uvicorn.Server(config) - asyncio.run(server.serve()) + server_instance = uvicorn.Server(config) + + # 創建事件循環並啟動服務器 + async def serve_with_async_init(server=server_instance): + # 在服務器啟動的同時進行異步初始化 + server_task = asyncio.create_task(server.serve()) + init_task = asyncio.create_task(self._init_async_components()) + + # 等待兩個任務完成 + await asyncio.gather( + server_task, init_task, return_exceptions=True + ) + + asyncio.run(serve_with_async_init()) break except OSError as e: