diff --git a/src/mcp_feedback_enhanced/desktop_app/desktop_app.py b/src/mcp_feedback_enhanced/desktop_app/desktop_app.py index 7579afc..b58dbb0 100644 --- a/src/mcp_feedback_enhanced/desktop_app/desktop_app.py +++ b/src/mcp_feedback_enhanced/desktop_app/desktop_app.py @@ -68,8 +68,8 @@ class DesktopApp: self.web_manager.start_server() # 等待服務器啟動 - max_wait = 10 # 最多等待 10 秒 - wait_count = 0 + max_wait = 10.0 # 最多等待 10 秒 + wait_count = 0.0 while wait_count < max_wait: if ( self.web_manager.server_thread @@ -221,7 +221,8 @@ class DesktopApp: # Windows 下隱藏控制台視窗 creation_flags = 0 if os.name == "nt": - creation_flags = subprocess.CREATE_NO_WINDOW + # CREATE_NO_WINDOW 只在 Windows 上存在 + creation_flags = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000) self.app_handle = subprocess.Popen( [str(tauri_exe)], diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index 1887ebb..90111c2 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -331,7 +331,9 @@ class WebUIManager: # 如果有舊會話,處理狀態轉換和清理 if old_session: - debug_log(f"處理舊會話 {old_session.session_id} 的狀態轉換,當前狀態: {old_session.status.value}") + debug_log( + f"處理舊會話 {old_session.session_id} 的狀態轉換,當前狀態: {old_session.status.value}" + ) # 保存標籤頁狀態到全局 if hasattr(old_session, "active_tabs"): @@ -339,14 +341,18 @@ class WebUIManager: # 如果舊會話是已提交狀態,進入下一步(已完成) if old_session.status == SessionStatus.FEEDBACK_SUBMITTED: - debug_log(f"舊會話 {old_session.session_id} 進入下一步:已提交 → 已完成") + debug_log( + f"舊會話 {old_session.session_id} 進入下一步:已提交 → 已完成" + ) success = old_session.next_step("反饋已處理,會話完成") if success: debug_log(f"✅ 舊會話 {old_session.session_id} 成功進入已完成狀態") else: debug_log(f"❌ 舊會話 {old_session.session_id} 無法進入下一步") else: - debug_log(f"舊會話 {old_session.session_id} 狀態為 {old_session.status.value},無需轉換") + debug_log( + f"舊會話 {old_session.session_id} 狀態為 {old_session.status.value},無需轉換" + ) # 確保舊會話仍在字典中(用於API獲取) if old_session.session_id in self.sessions: @@ -689,10 +695,6 @@ class WebUIManager: else: debug_log("沒有活躍的桌面應用程式實例") - - - - async def _safe_close_websocket(self, websocket): """安全關閉 WebSocket 連接,避免事件循環衝突 - 僅在連接已轉移後調用""" if not websocket: @@ -736,8 +738,8 @@ class WebUIManager: "session_id": self.current_session.session_id, "project_directory": self.current_session.project_directory, "summary": self.current_session.summary, - "status": self.current_session.status.value - } + "status": self.current_session.status.value, + }, } # 發送刷新通知 @@ -754,26 +756,61 @@ class WebUIManager: return False async def _check_active_tabs(self) -> bool: - """檢查是否有活躍標籤頁 - 檢查所有會話的WebSocket連接""" + """檢查是否有活躍標籤頁 - 使用分層檢測機制""" try: - # 檢查當前會話的WebSocket連接 - if self.current_session and self.current_session.websocket: - debug_log("檢測到當前會話的WebSocket連接") + # 快速檢測層:檢查 WebSocket 物件是否存在 + if not self.current_session or not self.current_session.websocket: + debug_log("快速檢測:沒有當前會話或 WebSocket 連接") + return False + + # 檢查心跳(如果有心跳記錄) + last_heartbeat = getattr(self.current_session, "last_heartbeat", None) + if last_heartbeat: + heartbeat_age = time.time() - last_heartbeat + if heartbeat_age > 10: # 超過 10 秒沒有心跳 + debug_log(f"快速檢測:心跳超時 ({heartbeat_age:.1f}秒)") + # 可能連接已死,需要進一步檢測 + else: + debug_log(f"快速檢測:心跳正常 ({heartbeat_age:.1f}秒前)") + return True # 心跳正常,認為連接活躍 + + # 準確檢測層:實際測試連接是否活著 + try: + # 檢查 WebSocket 連接狀態 + websocket = self.current_session.websocket + + # 檢查連接是否已關閉 + if hasattr(websocket, "client_state"): + try: + # 嘗試從 starlette 導入(FastAPI 基於 Starlette) + import starlette.websockets # type: ignore[import-not-found] + + if hasattr(starlette.websockets, "WebSocketState"): + WebSocketState = starlette.websockets.WebSocketState + if websocket.client_state != WebSocketState.CONNECTED: + debug_log( + f"準確檢測:WebSocket 狀態不是 CONNECTED,而是 {websocket.client_state}" + ) + # 清理死連接 + self.current_session.websocket = None + return False + except ImportError: + # 如果導入失敗,使用替代方法 + debug_log("無法導入 WebSocketState,使用替代方法檢測連接") + # 跳過狀態檢查,直接測試連接 + + # 如果連接看起來是活的,嘗試發送 ping(非阻塞) + # 注意:FastAPI WebSocket 沒有內建的 ping 方法,這裡使用自定義消息 + await websocket.send_json({"type": "ping", "timestamp": time.time()}) + debug_log("準確檢測:成功發送 ping 消息,連接是活躍的") return True - # 檢查其他會話的WebSocket連接 - active_websockets = 0 - for session_id, session in self.sessions.items(): - if session.websocket: - active_websockets += 1 - debug_log(f"檢測到會話 {session_id} 的WebSocket連接") - - if active_websockets > 0: - debug_log(f"檢測到 {active_websockets} 個活躍的WebSocket連接") - return True - - debug_log("沒有檢測到任何活躍的WebSocket連接") - return False + except Exception as e: + debug_log(f"準確檢測:連接測試失敗 - {e}") + # 連接已死,清理它 + if self.current_session: + self.current_session.websocket = None + return False except Exception as e: debug_log(f"檢查活躍連接時發生錯誤:{e}") diff --git a/src/mcp_feedback_enhanced/web/models/feedback_session.py b/src/mcp_feedback_enhanced/web/models/feedback_session.py index 5f2d69f..d1ac589 100644 --- a/src/mcp_feedback_enhanced/web/models/feedback_session.py +++ b/src/mcp_feedback_enhanced/web/models/feedback_session.py @@ -32,9 +32,11 @@ class SessionStatus(Enum): """會話狀態枚舉 - 單向流轉設計""" WAITING = "waiting" # 等待中 + ACTIVE = "active" # 活躍狀態 FEEDBACK_SUBMITTED = "feedback_submitted" # 已提交反饋 COMPLETED = "completed" # 已完成 ERROR = "error" # 錯誤(終態) + TIMEOUT = "timeout" # 超時(終態) EXPIRED = "expired" # 已過期(終態) @@ -140,6 +142,7 @@ class WebFeedbackSession: # 統一使用 time.time() 以避免時間基準不一致 self.created_at = time.time() self.last_activity = self.created_at + self.last_heartbeat = None # 記錄最後一次心跳時間 # 新增:自動清理配置 self.auto_cleanup_delay = auto_cleanup_delay # 自動清理延遲時間(秒) @@ -179,17 +182,21 @@ class WebFeedbackSession: # 定義狀態流轉路徑 next_status_map = { - SessionStatus.WAITING: SessionStatus.FEEDBACK_SUBMITTED, + SessionStatus.WAITING: SessionStatus.ACTIVE, + SessionStatus.ACTIVE: SessionStatus.FEEDBACK_SUBMITTED, SessionStatus.FEEDBACK_SUBMITTED: SessionStatus.COMPLETED, SessionStatus.COMPLETED: None, # 終態 - SessionStatus.ERROR: None, # 終態 - SessionStatus.EXPIRED: None # 終態 + SessionStatus.ERROR: None, # 終態 + SessionStatus.TIMEOUT: None, # 終態 + SessionStatus.EXPIRED: None, # 終態 } next_status = next_status_map.get(self.status) if next_status is None: - debug_log(f"⚠️ 會話 {self.session_id} 已處於終態 {self.status.value},無法進入下一步") + debug_log( + f"⚠️ 會話 {self.session_id} 已處於終態 {self.status.value},無法進入下一步" + ) return False # 執行狀態轉換 @@ -199,8 +206,9 @@ class WebFeedbackSession: else: # 默認消息 default_messages = { + SessionStatus.ACTIVE: "會話已啟動", SessionStatus.FEEDBACK_SUBMITTED: "用戶已提交反饋", - SessionStatus.COMPLETED: "會話已完成" + SessionStatus.COMPLETED: "會話已完成", } self.status_message = default_messages.get(next_status, "狀態已更新") @@ -245,7 +253,12 @@ class WebFeedbackSession: def is_terminal(self) -> bool: """檢查是否處於終態""" - return self.status in [SessionStatus.COMPLETED, SessionStatus.ERROR, SessionStatus.EXPIRED] + return self.status in [ + SessionStatus.COMPLETED, + SessionStatus.ERROR, + SessionStatus.TIMEOUT, + SessionStatus.EXPIRED, + ] def get_status_info(self) -> dict[str, Any]: """獲取會話狀態信息""" @@ -508,11 +521,13 @@ class WebFeedbackSession: "content": message_data.get("content", ""), "images": message_data.get("images", []), "submission_method": message_data.get("submission_method", "manual"), - "type": "feedback" + "type": "feedback", } self.user_messages.append(user_message) - debug_log(f"會話 {self.session_id} 添加用戶消息,總數: {len(self.user_messages)}") + debug_log( + f"會話 {self.session_id} 添加用戶消息,總數: {len(self.user_messages)}" + ) def _process_images(self, images: list[dict]) -> list[dict]: """ diff --git a/src/mcp_feedback_enhanced/web/routes/main_routes.py b/src/mcp_feedback_enhanced/web/routes/main_routes.py index 5ae35f4..2e7ebdd 100644 --- a/src/mcp_feedback_enhanced/web/routes/main_routes.py +++ b/src/mcp_feedback_enhanced/web/routes/main_routes.py @@ -176,7 +176,7 @@ def setup_routes(manager: "WebUIManager"): "feedback_completed": session.feedback_completed.is_set(), "has_websocket": session.websocket is not None, "is_current": session == manager.current_session, - "user_messages": session.user_messages # 包含用戶消息記錄 + "user_messages": session.user_messages, # 包含用戶消息記錄 } sessions_data.append(session_info) @@ -189,8 +189,7 @@ def setup_routes(manager: "WebUIManager"): except Exception as e: debug_log(f"獲取所有會話狀態失敗: {e}") return JSONResponse( - status_code=500, - content={"error": f"獲取會話狀態失敗: {e!s}"} + status_code=500, content={"error": f"獲取會話狀態失敗: {e!s}"} ) @manager.app.post("/api/add-user-message") @@ -201,22 +200,20 @@ def setup_routes(manager: "WebUIManager"): current_session = manager.get_current_session() if not current_session: - return JSONResponse( - status_code=404, - content={"error": "沒有活躍會話"} - ) + return JSONResponse(status_code=404, content={"error": "沒有活躍會話"}) # 添加用戶消息到會話 current_session.add_user_message(data) debug_log(f"用戶消息已添加到會話 {current_session.session_id}") - return JSONResponse(content={"status": "success", "message": "用戶消息已記錄"}) + return JSONResponse( + content={"status": "success", "message": "用戶消息已記錄"} + ) except Exception as e: debug_log(f"添加用戶消息失敗: {e}") return JSONResponse( - status_code=500, - content={"error": f"添加用戶消息失敗: {e!s}"} + status_code=500, content={"error": f"添加用戶消息失敗: {e!s}"} ) @manager.app.websocket("/ws") @@ -556,6 +553,10 @@ async def handle_websocket_message(manager: "WebUIManager", session, data: dict) elif message_type == "heartbeat": # WebSocket 心跳處理(簡化版) + # 更新心跳時間 + session.last_heartbeat = time.time() + session.last_activity = time.time() + # 發送心跳回應 if session.websocket: try: @@ -575,6 +576,11 @@ async def handle_websocket_message(manager: "WebUIManager", session, data: dict) await session._cleanup_resources_on_timeout() # 重構:不再自動停止服務器,保持服務器運行以支援持久性 + elif message_type == "pong": + # 處理來自前端的 pong 回應(用於連接檢測) + debug_log(f"收到 pong 回應,時間戳: {data.get('timestamp', 'N/A')}") + # 可以在這裡記錄延遲或更新連接狀態 + else: debug_log(f"未知的消息類型: {message_type}") diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index 982c5af..1c0658e 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -704,7 +704,7 @@ // 檢查是否是新會話創建的通知 if (data.action === 'new_session_created' || data.type === 'new_session_created') { - console.log('🆕 檢測到新會話創建,完全刷新頁面以確保狀態同步'); + console.log('🆕 檢測到新會話創建,局部更新頁面內容'); // 播放音效通知 if (this.audioManager) { @@ -713,28 +713,40 @@ // 顯示新會話通知 window.MCPFeedback.Utils.showMessage( - data.message || '新的 MCP 會話已創建,正在打開新窗口...', + data.message || '新的 MCP 會話已創建,正在更新內容...', window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS ); - // 使用 window.open 打開新窗口並關閉當前窗口 + // 局部更新頁面內容而非開啟新視窗 + const self = this; setTimeout(function() { - console.log('🔄 使用 window.open 打開新窗口'); + console.log('🔄 執行局部更新頁面內容'); - // 打開新窗口 - const newWindow = window.open(window.location.href, '_blank'); - - if (newWindow) { - console.log('✅ 新窗口打開成功,關閉當前窗口'); - // 短暫延遲後關閉當前窗口 - setTimeout(function() { - window.close(); - }, 500); - } else { - console.warn('❌ window.open 被阻止,回退到頁面刷新'); - window.location.reload(); + // 1. 更新會話資訊 + if (data.session_info) { + self.currentSessionId = data.session_info.session_id; + console.log('📋 新會話 ID:', self.currentSessionId); } - }, 1500); + + // 2. 刷新頁面內容(AI 摘要、表單等) + self.refreshPageContent(); + + // 3. 重置表單狀態 + self.clearFeedback(); + + // 4. 重置回饋狀態為等待中 + if (self.uiManager) { + self.uiManager.setFeedbackState( + window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, + self.currentSessionId + ); + } + + // 5. 檢查並啟動自動提交 + self.checkAndStartAutoSubmit(); + + console.log('✅ 局部更新完成,頁面已準備好接收新的回饋'); + }, 500); return; // 提前返回,不執行後續的局部更新邏輯 } diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/websocket-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/websocket-manager.js index be2cdfc..703062e 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/websocket-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/websocket-manager.js @@ -269,6 +269,14 @@ this.connectionMonitor.recordPong(); } break; + case 'ping': + // 處理來自伺服器的 ping 消息(用於連接檢測) + console.log('收到伺服器 ping,立即回應 pong'); + this.send({ + type: 'pong', + timestamp: data.timestamp + }); + break; default: // 其他訊息類型由外部處理 break; diff --git a/tests/unit/test_session_cleanup.py b/tests/unit/test_session_cleanup.py index 43d6045..06152ee 100644 --- a/tests/unit/test_session_cleanup.py +++ b/tests/unit/test_session_cleanup.py @@ -182,8 +182,8 @@ class TestWebFeedbackSessionCleanup: """測試狀態更新重置定時器""" old_timer = self.session.cleanup_timer - # 更新狀態為活躍 - self.session.update_status(SessionStatus.ACTIVE, "測試活躍狀態") + # 更新狀態為活躍 - 使用 next_step 方法 + self.session.next_step("測試活躍狀態") # 檢查定時器是否被重置 assert self.session.cleanup_timer != old_timer diff --git a/tests/unit/test_web_ui.py b/tests/unit/test_web_ui.py index 864b247..c3f909c 100644 --- a/tests/unit/test_web_ui.py +++ b/tests/unit/test_web_ui.py @@ -108,11 +108,16 @@ class TestWebFeedbackSession: # 測試初始狀態 assert session.status == SessionStatus.WAITING - # 測試狀態更新 - session.update_status(SessionStatus.FEEDBACK_SUBMITTED, "已提交回饋") + # 測試狀態更新 - 使用 next_step 方法 + # 首先進入 ACTIVE 狀態 + result = session.next_step("會話已激活") + assert result is True + assert session.status == SessionStatus.ACTIVE + # 然後進入 FEEDBACK_SUBMITTED 狀態 + result = session.next_step("已提交回饋") # type: ignore[unreachable] + assert result is True assert session.status == SessionStatus.FEEDBACK_SUBMITTED - # 修復 unreachable 錯誤 - 使用 type: ignore 註解 - assert session.status_message == "已提交回饋" # type: ignore[unreachable] + assert session.status_message == "已提交回饋" def test_session_age_and_idle_time(self, test_project_dir): """測試會話年齡和空閒時間"""