diff --git a/src/mcp_feedback_enhanced/gui/__init__.py b/src/mcp_feedback_enhanced/gui/__init__.py index ea4fe2e..3bb7ac7 100644 --- a/src/mcp_feedback_enhanced/gui/__init__.py +++ b/src/mcp_feedback_enhanced/gui/__init__.py @@ -20,6 +20,6 @@ 重構: 模塊化設計 """ -from .main import feedback_ui +from .main import feedback_ui, feedback_ui_with_timeout -__all__ = ['feedback_ui'] \ No newline at end of file +__all__ = ['feedback_ui', 'feedback_ui_with_timeout'] \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/gui/main.py b/src/mcp_feedback_enhanced/gui/main.py index 5e4467d..594a260 100644 --- a/src/mcp_feedback_enhanced/gui/main.py +++ b/src/mcp_feedback_enhanced/gui/main.py @@ -7,9 +7,12 @@ GUI 主要入口點 提供 GUI 回饋介面的主要入口點函數。 """ +import threading +import time from typing import Optional from PySide6.QtWidgets import QApplication, QMainWindow from PySide6.QtGui import QFont +from PySide6.QtCore import QTimer import sys from .models import FeedbackResult @@ -51,4 +54,69 @@ def feedback_ui(project_directory: str, summary: str) -> Optional[FeedbackResult app.exec() # 返回結果 - return window.result \ No newline at end of file + return window.result + + +def feedback_ui_with_timeout(project_directory: str, summary: str, timeout: int) -> Optional[FeedbackResult]: + """ + 啟動帶超時的回饋收集 GUI 介面 + + Args: + project_directory: 專案目錄路徑 + summary: AI 工作摘要 + timeout: 超時時間(秒) + + Returns: + Optional[FeedbackResult]: 回饋結果,如果用戶取消或超時則返回 None + + Raises: + TimeoutError: 當超時時拋出 + """ + # 檢查是否已有 QApplication 實例 + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 設定全域微軟正黑體字體 + font = QFont("Microsoft JhengHei", 11) # 微軟正黑體,11pt + app.setFont(font) + + # 設定字體回退順序,確保中文字體正確顯示 + app.setStyleSheet(""" + * { + font-family: "Microsoft JhengHei", "微軟正黑體", "Microsoft YaHei", "微软雅黑", "SimHei", "黑体", sans-serif; + } + """) + + # 創建主窗口 + window = FeedbackWindow(project_directory, summary) + window.show() + + # 創建超時計時器 + timeout_timer = QTimer() + timeout_timer.setSingleShot(True) + timeout_timer.timeout.connect(lambda: _handle_timeout(window, app)) + timeout_timer.start(timeout * 1000) # 轉換為毫秒 + + # 運行事件循環直到窗口關閉 + app.exec() + + # 停止計時器(如果還在運行) + timeout_timer.stop() + + # 檢查是否超時 + if hasattr(window, '_timeout_occurred'): + raise TimeoutError(f"回饋收集超時({timeout}秒),GUI 介面已自動關閉") + + # 返回結果 + return window.result + + +def _handle_timeout(window: FeedbackWindow, app: QApplication) -> None: + """處理超時事件""" + # 標記超時發生 + window._timeout_occurred = True + # 強制關閉視窗 + window.force_close() + # 退出應用程式 + app.quit() \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/gui/window/feedback_window.py b/src/mcp_feedback_enhanced/gui/window/feedback_window.py index 8992d6e..e412690 100644 --- a/src/mcp_feedback_enhanced/gui/window/feedback_window.py +++ b/src/mcp_feedback_enhanced/gui/window/feedback_window.py @@ -447,17 +447,16 @@ class FeedbackWindow(QMainWindow): self.close() def _cancel_feedback(self) -> None: - """取消回饋""" - reply = QMessageBox.question( - self, t('app.confirmCancel'), - t('app.confirmCancelMessage'), - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - - if reply == QMessageBox.Yes: - self.result = None - self.close() + """取消回饋收集""" + debug_log("取消回饋收集") + self.result = "" + self.close() + + def force_close(self) -> None: + """強制關閉視窗(用於超時處理)""" + debug_log("強制關閉視窗(超時)") + self.result = "" + self.close() def _refresh_ui_texts(self) -> None: """刷新界面文字""" diff --git a/src/mcp_feedback_enhanced/server.py b/src/mcp_feedback_enhanced/server.py index aee8e22..6cd19a4 100644 --- a/src/mcp_feedback_enhanced/server.py +++ b/src/mcp_feedback_enhanced/server.py @@ -356,45 +356,37 @@ def process_images(images_data: List[dict]) -> List[MCPImage]: return mcp_images -def launch_gui(project_dir: str, summary: str) -> dict: +async def launch_gui_with_timeout(project_dir: str, summary: str, timeout: int) -> dict: """ - 啟動 Qt GUI 收集回饋 - - Args: - project_dir: 專案目錄路徑 - summary: AI 工作摘要 - - Returns: - dict: 收集到的回饋資料 + 啟動 GUI 模式並處理超時 """ - debug_log("啟動 Qt GUI 介面") + debug_log(f"啟動 GUI 模式(超時:{timeout}秒)") try: - from .gui import feedback_ui - result = feedback_ui(project_dir, summary) + from .gui import feedback_ui_with_timeout - if result is None: - # 用戶取消 + # 直接調用帶超時的 GUI 函數 + result = feedback_ui_with_timeout(project_dir, summary, timeout) + + if result: return { - "command_logs": "", - "interactive_feedback": "用戶取消了回饋。", + "logs": f"GUI 模式回饋收集完成", + "interactive_feedback": result.get("interactive_feedback", ""), + "images": result.get("images", []) + } + else: + return { + "logs": "用戶取消了回饋收集", + "interactive_feedback": "", "images": [] } - - # 轉換鍵名以保持向後兼容 - return { - "command_logs": result.get("command_logs", ""), - "interactive_feedback": result.get("interactive_feedback", ""), - "images": result.get("images", []) - } - - except ImportError as e: - debug_log(f"無法導入 GUI 模組: {e}") - return { - "command_logs": "", - "interactive_feedback": f"Qt GUI 模組導入失敗: {str(e)}", - "images": [] - } + + except TimeoutError as e: + # 超時異常 - 這是預期的行為 + raise e + except Exception as e: + debug_log(f"GUI 啟動失败: {e}") + raise Exception(f"GUI 啟動失败: {e}") # ===== MCP 工具定義 ===== @@ -462,7 +454,7 @@ async def interactive_feedback( if use_web_ui: result = await launch_web_ui_with_timeout(project_directory, summary, timeout) else: - result = launch_gui(project_directory, summary) + result = await launch_gui_with_timeout(project_directory, summary, timeout) # 處理取消情況 if not result: @@ -517,8 +509,8 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in # 使用新的 web 模組 from .web import launch_web_feedback_ui - # 直接運行 Web UI 會話 - return await launch_web_feedback_ui(project_dir, summary) + # 傳遞 timeout 參數給 Web UI + return await launch_web_feedback_ui(project_dir, summary, timeout) except ImportError as e: debug_log(f"無法導入 Web UI 模組: {e}") return { @@ -526,6 +518,13 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in "interactive_feedback": f"Web UI 模組導入失敗: {str(e)}", "images": [] } + except TimeoutError as e: + debug_log(f"Web UI 超時: {e}") + return { + "command_logs": "", + "interactive_feedback": f"回饋收集超時({timeout}秒),介面已自動關閉。", + "images": [] + } except Exception as e: error_msg = f"Web UI 錯誤: {e}" debug_log(f"❌ {error_msg}") diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json index 88e14a4..aaba1a4 100644 --- a/src/mcp_feedback_enhanced/web/locales/en/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json @@ -147,6 +147,12 @@ "upload": "Upload", "download": "Download" }, + "session": { + "timeout": "⏰ Session has timed out, interface will close automatically", + "timeoutWarning": "Session is about to timeout", + "timeoutDescription": "Due to prolonged inactivity, the session has timed out. The interface will automatically close in 3 seconds.", + "closing": "Closing..." + }, "dynamic": { "aiSummary": "Test Web UI Functionality\n\n🎯 **Test Items:**\n- Web UI server startup and operation\n- WebSocket real-time communication\n- Feedback submission functionality\n- Image upload and preview\n- Command execution functionality\n- Smart Ctrl+V image pasting\n- Multi-language interface functionality\n\n📋 **Test Steps:**\n1. Test image upload (drag-drop, file selection, clipboard)\n2. Press Ctrl+V in text box to test smart pasting\n3. Try switching languages (Traditional Chinese/Simplified Chinese/English)\n4. Test command execution functionality\n5. Submit feedback and images\n\nPlease test these features and provide feedback!", "terminalWelcome": "Welcome to Interactive Feedback Terminal\n========================================\nProject Directory: {sessionId}\nEnter commands and press Enter or click Execute button\nSupported commands: ls, dir, pwd, cat, type, etc.\n\n$ " diff --git a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json index 6ef3927..7eba251 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json @@ -147,6 +147,12 @@ "upload": "上传", "download": "下载" }, + "session": { + "timeout": "⏰ 会话已超时,界面将自动关闭", + "timeoutWarning": "会话即将超时", + "timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。", + "closing": "正在关闭..." + }, "dynamic": { "aiSummary": "测试 Web UI 功能\n\n🎯 **功能测试项目:**\n- Web UI 服务器启动和运行\n- WebSocket 实时通讯\n- 反馈提交功能\n- 图片上传和预览\n- 命令执行功能\n- 智能 Ctrl+V 图片粘贴\n- 多语言界面功能\n\n📋 **测试步骤:**\n1. 测试图片上传(拖拽、选择文件、剪贴板)\n2. 在文本框内按 Ctrl+V 测试智能粘贴\n3. 尝试切换语言(繁中/简中/英文)\n4. 测试命令执行功能\n5. 提交反馈和图片\n\n请测试这些功能并提供反馈!", "terminalWelcome": "欢迎使用交互反馈终端\n========================================\n项目目录: {sessionId}\n输入命令后按 Enter 或点击执行按钮\n支持的命令: ls, dir, pwd, cat, type 等\n\n$ " diff --git a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json index 18ec00a..72ae971 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json @@ -147,6 +147,12 @@ "upload": "上傳", "download": "下載" }, + "session": { + "timeout": "⏰ 會話已超時,介面將自動關閉", + "timeoutWarning": "會話即將超時", + "timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。", + "closing": "正在關閉..." + }, "dynamic": { "aiSummary": "測試 Web UI 功能\n\n🎯 **功能測試項目:**\n- Web UI 服務器啟動和運行\n- WebSocket 即時通訊\n- 回饋提交功能\n- 圖片上傳和預覽\n- 命令執行功能\n- 智能 Ctrl+V 圖片貼上\n- 多語言介面功能\n\n📋 **測試步驟:**\n1. 測試圖片上傳(拖拽、選擇檔案、剪貼簿)\n2. 在文字框內按 Ctrl+V 測試智能貼上\n3. 嘗試切換語言(繁中/簡中/英文)\n4. 測試命令執行功能\n5. 提交回饋和圖片\n\n請測試這些功能並提供回饋!", "terminalWelcome": "歡迎使用互動回饋終端\n========================================\n專案目錄: {sessionId}\n輸入命令後按 Enter 或點擊執行按鈕\n支援的命令: ls, dir, pwd, cat, type 等\n\n$ " diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index 0e5f991..6bc640a 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -174,13 +174,14 @@ def get_web_ui_manager() -> WebUIManager: return _web_ui_manager -async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict: +async def launch_web_feedback_ui(project_directory: str, summary: str, timeout: int = 600) -> dict: """ 啟動 Web 回饋介面並等待用戶回饋 Args: project_directory: 專案目錄路徑 summary: AI 工作摘要 + timeout: 超時時間(秒) Returns: dict: 回饋結果,包含 logs、interactive_feedback 和 images @@ -203,12 +204,19 @@ async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict: manager.open_browser(feedback_url) try: - # 等待用戶回饋 - result = await session.wait_for_feedback() + # 等待用戶回饋,傳遞 timeout 參數 + result = await session.wait_for_feedback(timeout) debug_log(f"收到用戶回饋,會話: {session_id}") return result + except TimeoutError: + debug_log(f"會話 {session_id} 超時") + # 資源已在 wait_for_feedback 中清理,這裡只需要記錄和重新拋出 + raise + except Exception as e: + debug_log(f"會話 {session_id} 發生錯誤: {e}") + raise finally: - # 清理會話 + # 清理會話(無論成功還是失敗) manager.remove_session(session_id) diff --git a/src/mcp_feedback_enhanced/web/models/feedback_session.py b/src/mcp_feedback_enhanced/web/models/feedback_session.py index dd1981f..b83fafb 100644 --- a/src/mcp_feedback_enhanced/web/models/feedback_session.py +++ b/src/mcp_feedback_enhanced/web/models/feedback_session.py @@ -37,13 +37,14 @@ class WebFeedbackSession: self.feedback_completed = threading.Event() self.process: Optional[subprocess.Popen] = None self.command_logs = [] + self._cleanup_done = False # 防止重複清理 # 確保臨時目錄存在 TEMP_DIR.mkdir(parents=True, exist_ok=True) async def wait_for_feedback(self, timeout: int = 600) -> dict: """ - 等待用戶回饋,包含圖片 + 等待用戶回饋,包含圖片,支援超時自動清理 Args: timeout: 超時時間(秒) @@ -51,21 +52,40 @@ class WebFeedbackSession: Returns: dict: 回饋結果 """ - loop = asyncio.get_event_loop() - - def wait_in_thread(): - return self.feedback_completed.wait(timeout) - - completed = await loop.run_in_executor(None, wait_in_thread) - - if completed: - return { - "logs": "\n".join(self.command_logs), - "interactive_feedback": self.feedback_result or "", - "images": self.images - } - else: - raise TimeoutError("等待用戶回饋超時") + try: + # 使用比 MCP 超時稍短的時間(提前處理,避免邊界競爭) + # 對於短超時(<30秒),提前1秒;對於長超時,提前5秒 + if timeout <= 30: + actual_timeout = max(timeout - 1, 5) # 短超時提前1秒,最少5秒 + else: + actual_timeout = timeout - 5 # 長超時提前5秒 + debug_log(f"會話 {self.session_id} 開始等待回饋,超時時間: {actual_timeout} 秒(原始: {timeout} 秒)") + + loop = asyncio.get_event_loop() + + def wait_in_thread(): + return self.feedback_completed.wait(actual_timeout) + + completed = await loop.run_in_executor(None, wait_in_thread) + + if completed: + debug_log(f"會話 {self.session_id} 收到用戶回饋") + return { + "logs": "\n".join(self.command_logs), + "interactive_feedback": self.feedback_result or "", + "images": self.images + } + else: + # 超時了,立即清理資源 + debug_log(f"會話 {self.session_id} 在 {actual_timeout} 秒後超時,開始清理資源...") + await self._cleanup_resources_on_timeout() + raise TimeoutError(f"等待用戶回饋超時({actual_timeout}秒),介面已自動關閉") + + except Exception as e: + # 任何異常都要確保清理資源 + debug_log(f"會話 {self.session_id} 發生異常: {e}") + await self._cleanup_resources_on_timeout() + raise async def submit_feedback(self, feedback: str, images: List[dict]): """ @@ -224,8 +244,66 @@ class WebFeedbackSession: except: pass + async def _cleanup_resources_on_timeout(self): + """超時時清理所有資源""" + if self._cleanup_done: + return # 避免重複清理 + + self._cleanup_done = True + debug_log(f"開始清理會話 {self.session_id} 的資源...") + + try: + # 1. 關閉 WebSocket 連接 + if self.websocket: + try: + # 先通知前端超時 + await self.websocket.send_json({ + "type": "session_timeout", + "message": "會話已超時,介面將自動關閉" + }) + await asyncio.sleep(0.1) # 給前端一點時間處理消息 + await self.websocket.close() + debug_log(f"會話 {self.session_id} WebSocket 已關閉") + except Exception as e: + debug_log(f"關閉 WebSocket 時發生錯誤: {e}") + finally: + self.websocket = None + + # 2. 終止正在運行的命令進程 + if self.process: + try: + self.process.terminate() + try: + self.process.wait(timeout=3) + debug_log(f"會話 {self.session_id} 命令進程已正常終止") + except subprocess.TimeoutExpired: + self.process.kill() + debug_log(f"會話 {self.session_id} 命令進程已強制終止") + except Exception as e: + debug_log(f"終止命令進程時發生錯誤: {e}") + finally: + self.process = None + + # 3. 設置完成事件(防止其他地方還在等待) + self.feedback_completed.set() + + # 4. 清理臨時數據 + self.command_logs.clear() + self.images.clear() + + debug_log(f"會話 {self.session_id} 資源清理完成") + + except Exception as e: + debug_log(f"清理會話 {self.session_id} 資源時發生錯誤: {e}") + def cleanup(self): - """清理會話資源""" + """同步清理會話資源(保持向後兼容)""" + if self._cleanup_done: + return + + self._cleanup_done = True + debug_log(f"同步清理會話 {self.session_id} 資源...") + if self.process: try: self.process.terminate() @@ -235,4 +313,7 @@ class WebFeedbackSession: self.process.kill() except: pass - self.process = None \ No newline at end of file + self.process = None + + # 設置完成事件 + self.feedback_completed.set() \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index 8fd1f50..9fdfa05 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -242,6 +242,10 @@ class FeedbackApp { // 顯示成功訊息 this.showSuccessMessage(); break; + case 'session_timeout': + console.log('會話超時:', data.message); + this.handleSessionTimeout(data.message); + break; default: console.log('未知的 WebSocket 消息:', data); } @@ -254,6 +258,54 @@ class FeedbackApp { this.showMessage(successMessage, 'success'); } + handleSessionTimeout(message) { + console.log('處理會話超時:', message); + + // 顯示超時訊息 + const timeoutMessage = message || (window.i18nManager ? + window.i18nManager.t('session.timeout', '⏰ 會話已超時,介面將自動關閉') : + '⏰ 會話已超時,介面將自動關閉'); + + this.showMessage(timeoutMessage, 'warning'); + + // 禁用所有互動元素 + this.disableAllInputs(); + + // 3秒後自動關閉頁面 + setTimeout(() => { + try { + window.close(); + } catch (e) { + // 如果無法關閉視窗(可能因為安全限制),重新載入頁面 + console.log('無法關閉視窗,重新載入頁面'); + window.location.reload(); + } + }, 3000); + } + + disableAllInputs() { + // 禁用所有輸入元素 + const inputs = document.querySelectorAll('input, textarea, button'); + inputs.forEach(input => { + input.disabled = true; + input.style.opacity = '0.5'; + }); + + // 特別處理提交和取消按鈕 + const submitBtn = document.getElementById('submitBtn'); + const cancelBtn = document.getElementById('cancelBtn'); + + if (submitBtn) { + submitBtn.textContent = '⏰ 已超時'; + submitBtn.disabled = true; + } + + if (cancelBtn) { + cancelBtn.textContent = '關閉中...'; + cancelBtn.disabled = true; + } + } + updateConnectionStatus(connected) { // 更新連接狀態指示器 const elements = document.querySelectorAll('.connection-indicator');