diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index f4aa907..ac110d8 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -261,7 +261,7 @@ class WebUIManager: # 處理會話更新通知 if old_websocket: - # 有舊連接,立即發送會話更新通知 + # 有舊連接,立即發送會話更新通知並轉移連接 self._old_websocket_for_update = old_websocket self._new_session_for_update = session debug_log("已保存舊 WebSocket 連接,準備發送會話更新通知") @@ -269,10 +269,13 @@ class WebUIManager: # 立即發送會話更新通知 import asyncio try: - # 在後台任務中發送通知 + # 在後台任務中發送通知並轉移連接 asyncio.create_task(self._send_immediate_session_update()) except Exception as e: debug_log(f"創建會話更新任務失敗: {e}") + # 即使任務創建失敗,也要嘗試直接轉移連接 + session.websocket = old_websocket + debug_log("任務創建失敗,直接轉移 WebSocket 連接到新會話") self._pending_session_update = True else: # 沒有舊連接,標記需要發送會話更新通知(當新 WebSocket 連接建立時) @@ -505,8 +508,21 @@ class WebUIManager: old_websocket = self._old_websocket_for_update new_session = self._new_session_for_update - # 檢查舊連接是否仍然有效 - if old_websocket and not old_websocket.client_state.DISCONNECTED: + # 改進的連接有效性檢查 + websocket_valid = False + if old_websocket: + try: + # 檢查 WebSocket 連接狀態 + if hasattr(old_websocket, 'client_state'): + websocket_valid = old_websocket.client_state != old_websocket.client_state.DISCONNECTED + else: + # 如果沒有 client_state 屬性,嘗試發送測試消息來檢查連接 + websocket_valid = True + except Exception as check_error: + debug_log(f"檢查 WebSocket 連接狀態失敗: {check_error}") + websocket_valid = False + + if websocket_valid: try: # 發送會話更新通知 await old_websocket.send_json({ @@ -523,11 +539,18 @@ class WebUIManager: # 延遲一小段時間讓前端處理消息 await asyncio.sleep(0.2) + # 將 WebSocket 連接轉移到新會話 + new_session.websocket = old_websocket + debug_log("已將 WebSocket 連接轉移到新會話") + except Exception as send_error: debug_log(f"發送會話更新通知失敗: {send_error}") - - # 安全關閉舊連接 - await self._safe_close_websocket(old_websocket) + # 如果發送失敗,仍然嘗試轉移連接 + new_session.websocket = old_websocket + debug_log("發送失敗但仍轉移 WebSocket 連接到新會話") + else: + debug_log("舊 WebSocket 連接無效,設置待更新標記") + self._pending_session_update = True # 清理臨時變數 delattr(self, '_old_websocket_for_update') @@ -544,29 +567,24 @@ class WebUIManager: self._pending_session_update = True async def _safe_close_websocket(self, websocket): - """安全關閉 WebSocket 連接,避免事件循環衝突""" + """安全關閉 WebSocket 連接,避免事件循環衝突 - 僅在連接已轉移後調用""" if not websocket: return + # 注意:此方法現在主要用於清理,因為連接已經轉移到新會話 + # 只有在確認連接沒有被新會話使用時才關閉 try: # 檢查連接狀態 - if websocket.client_state.DISCONNECTED: + if hasattr(websocket, 'client_state') and websocket.client_state.DISCONNECTED: debug_log("WebSocket 已斷開,跳過關閉操作") return - # 嘗試正常關閉 - await asyncio.wait_for(websocket.close(code=1000, reason="會話更新"), timeout=2.0) - debug_log("已正常關閉舊 WebSocket 連接") + # 由於連接已轉移到新會話,這裡不再主動關閉 + # 讓新會話管理這個連接的生命週期 + debug_log("WebSocket 連接已轉移到新會話,跳過關閉操作") - except asyncio.TimeoutError: - debug_log("WebSocket 關閉超時,強制斷開") - except RuntimeError as e: - if "attached to a different loop" in str(e): - debug_log(f"WebSocket 事件循環衝突,忽略關閉錯誤: {e}") - else: - debug_log(f"WebSocket 關閉時發生運行時錯誤: {e}") except Exception as e: - debug_log(f"關閉 WebSocket 連接時發生未知錯誤: {e}") + debug_log(f"檢查 WebSocket 連接狀態時發生錯誤: {e}") async def _check_active_tabs(self) -> bool: """檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API""" diff --git a/src/mcp_feedback_enhanced/web/routes/main_routes.py b/src/mcp_feedback_enhanced/web/routes/main_routes.py index 7830534..a1efb7b 100644 --- a/src/mcp_feedback_enhanced/web/routes/main_routes.py +++ b/src/mcp_feedback_enhanced/web/routes/main_routes.py @@ -158,9 +158,13 @@ def setup_routes(manager: 'WebUIManager'): return await websocket.accept() - session.websocket = websocket - debug_log(f"WebSocket 連接建立: 當前活躍會話") + # 檢查會話是否已有 WebSocket 連接 + if session.websocket and session.websocket != websocket: + debug_log("會話已有 WebSocket 連接,替換為新連接") + + session.websocket = websocket + debug_log(f"WebSocket 連接建立: 當前活躍會話 {session.session_id}") # 發送連接成功消息 try: @@ -198,7 +202,14 @@ def setup_routes(manager: 'WebUIManager'): while True: data = await websocket.receive_text() message = json.loads(data) - await handle_websocket_message(manager, session, message) + + # 重新獲取當前會話,以防會話已切換 + current_session = manager.get_current_session() + if current_session and current_session.websocket == websocket: + await handle_websocket_message(manager, current_session, message) + else: + debug_log("會話已切換或 WebSocket 連接不匹配,忽略消息") + break except WebSocketDisconnect: debug_log(f"WebSocket 連接正常斷開") @@ -208,8 +219,9 @@ def setup_routes(manager: 'WebUIManager'): debug_log(f"WebSocket 錯誤: {e}") finally: # 安全清理 WebSocket 連接 - if session.websocket == websocket: - session.websocket = None + current_session = manager.get_current_session() + if current_session and current_session.websocket == websocket: + current_session.websocket = None debug_log("已清理會話中的 WebSocket 連接") @manager.app.post("/api/save-settings") diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index 762f427..c91257f 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -170,6 +170,13 @@ class FeedbackApp { this.heartbeatInterval = null; this.heartbeatFrequency = 30000; // 30秒 WebSocket 心跳 + // 新增:WebSocket 連接狀態管理 + this.connectionReady = false; + this.pendingSubmission = null; + this.connectionCheckInterval = null; + this.sessionUpdatePending = false; + this.reconnectDelay = 1000; // 重連延遲,會逐漸增加 + // UI 狀態 this.currentTab = 'feedback'; @@ -857,11 +864,11 @@ class FeedbackApp { } /** - * 檢查是否可以提交回饋 + * 檢查是否可以提交回饋(舊版本,保持兼容性) */ canSubmitFeedback() { - const canSubmit = this.feedbackState === 'waiting_for_feedback' && this.isConnected; - console.log(`🔍 檢查提交權限: feedbackState=${this.feedbackState}, isConnected=${this.isConnected}, canSubmit=${canSubmit}`); + const canSubmit = this.feedbackState === 'waiting_for_feedback' && this.isConnected && this.connectionReady; + console.log(`🔍 檢查提交權限: feedbackState=${this.feedbackState}, isConnected=${this.isConnected}, connectionReady=${this.connectionReady}, canSubmit=${canSubmit}`); return canSubmit; } @@ -1025,11 +1032,13 @@ class FeedbackApp { this.websocket.onopen = () => { this.isConnected = true; + this.connectionReady = false; // 等待連接確認 this.updateConnectionStatus('connected', '已連接'); console.log('WebSocket 連接已建立'); - // 重置重連計數器 + // 重置重連計數器和延遲 this.reconnectAttempts = 0; + this.reconnectDelay = 1000; // 開始 WebSocket 心跳 this.startWebSocketHeartbeat(); @@ -1042,6 +1051,23 @@ class FeedbackApp { console.log('🔄 WebSocket 重連後重置處理狀態'); this.setFeedbackState('waiting_for_feedback'); } + + // 如果有待處理的會話更新,處理它 + if (this.sessionUpdatePending) { + console.log('🔄 處理待處理的會話更新'); + this.sessionUpdatePending = false; + } + + // 如果有待提交的回饋,處理它 + if (this.pendingSubmission) { + console.log('🔄 處理待提交的回饋'); + setTimeout(() => { + if (this.connectionReady && this.pendingSubmission) { + this.submitFeedbackInternal(this.pendingSubmission); + this.pendingSubmission = null; + } + }, 500); // 等待連接完全就緒 + } }; this.websocket.onmessage = (event) => { @@ -1055,6 +1081,7 @@ class FeedbackApp { this.websocket.onclose = (event) => { this.isConnected = false; + this.connectionReady = false; console.log('WebSocket 連接已關閉, code:', event.code, 'reason:', event.reason); // 停止心跳 @@ -1072,15 +1099,23 @@ class FeedbackApp { } else { this.updateConnectionStatus('disconnected', '已斷開'); + // 會話更新導致的正常關閉,立即重連 + if (event.code === 1000 && event.reason === '會話更新') { + console.log('🔄 會話更新導致的連接關閉,立即重連...'); + this.sessionUpdatePending = true; + setTimeout(() => { + this.setupWebSocket(); + }, 200); // 短延遲後重連 + } // 只有在非正常關閉時才重連 - if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) { + else if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; - const delay = Math.min(3000 * this.reconnectAttempts, 15000); // 最大延遲15秒 - console.log(`${delay / 1000}秒後嘗試重連... (第${this.reconnectAttempts}次)`); + this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 15000); // 指數退避,最大15秒 + console.log(`${this.reconnectDelay / 1000}秒後嘗試重連... (第${this.reconnectAttempts}次)`); setTimeout(() => { console.log(`🔄 開始重連 WebSocket... (第${this.reconnectAttempts}次)`); this.setupWebSocket(); - }, delay); + }, this.reconnectDelay); } else if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.log('❌ 達到最大重連次數,停止重連'); this.showMessage('WebSocket 連接失敗,請刷新頁面重試', 'error'); @@ -1138,6 +1173,18 @@ class FeedbackApp { switch (data.type) { case 'connection_established': console.log('WebSocket 連接確認'); + this.connectionReady = true; + + // 如果有待提交的回饋,現在可以提交了 + if (this.pendingSubmission) { + console.log('🔄 連接就緒,提交待處理的回饋'); + setTimeout(() => { + if (this.pendingSubmission) { + this.submitFeedbackInternal(this.pendingSubmission); + this.pendingSubmission = null; + } + }, 100); + } break; case 'heartbeat_response': // 心跳回應,更新標籤頁活躍狀態 @@ -1209,8 +1256,11 @@ class FeedbackApp { document.title = `MCP Feedback - ${projectName}`; } - // 使用局部更新替代整頁刷新 - this.refreshPageContent(); + // 確保 WebSocket 連接就緒 + this.ensureWebSocketReady(() => { + // 使用局部更新替代整頁刷新 + this.refreshPageContent(); + }); } else { // 如果沒有會話信息,仍然重置狀態 console.log('⚠️ 會話更新沒有包含會話信息,僅重置狀態'); @@ -1220,6 +1270,51 @@ class FeedbackApp { console.log('✅ 會話更新處理完成'); } + /** + * 確保 WebSocket 連接就緒 + */ + ensureWebSocketReady(callback, maxWaitTime = 5000) { + const startTime = Date.now(); + + const checkConnection = () => { + if (this.isConnected && this.connectionReady) { + console.log('✅ WebSocket 連接已就緒'); + if (callback) callback(); + return; + } + + const elapsed = Date.now() - startTime; + if (elapsed >= maxWaitTime) { + console.log('⚠️ WebSocket 連接等待超時,強制執行回調'); + if (callback) callback(); + return; + } + + // 如果連接斷開,嘗試重連 + if (!this.isConnected) { + console.log('🔄 WebSocket 未連接,嘗試重連...'); + this.setupWebSocket(); + } + + // 繼續等待 + setTimeout(checkConnection, 200); + }; + + checkConnection(); + } + + /** + * 檢查是否可以提交回饋 + */ + canSubmitFeedback() { + const canSubmit = this.isConnected && + this.connectionReady && + this.feedbackState === 'waiting_for_feedback'; + + console.log(`🔍 檢查提交權限: isConnected=${this.isConnected}, connectionReady=${this.connectionReady}, feedbackState=${this.feedbackState}, canSubmit=${canSubmit}`); + return canSubmit; + } + async refreshPageContent() { console.log('🔄 局部更新頁面內容...'); @@ -1684,22 +1779,46 @@ class FeedbackApp { // 檢查是否可以提交回饋 if (!this.canSubmitFeedback()) { - console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState, '連接狀態:', this.isConnected); + console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState, '連接狀態:', this.isConnected, '連接就緒:', this.connectionReady); if (this.feedbackState === 'feedback_submitted') { this.showMessage('回饋已提交,請等待下次 MCP 調用', 'warning'); } else if (this.feedbackState === 'processing') { this.showMessage('正在處理中,請稍候', 'warning'); - } else if (!this.isConnected) { - this.showMessage('WebSocket 未連接,正在嘗試重連...', 'error'); - // 嘗試重新建立連接 - this.setupWebSocket(); + } else if (!this.isConnected || !this.connectionReady) { + // 收集回饋數據,等待連接就緒後提交 + const feedbackData = this.collectFeedbackData(); + if (feedbackData) { + this.pendingSubmission = feedbackData; + this.showMessage('WebSocket 連接中,回饋將在連接就緒後自動提交...', 'info'); + + // 確保 WebSocket 連接 + this.ensureWebSocketReady(() => { + if (this.pendingSubmission) { + this.submitFeedbackInternal(this.pendingSubmission); + this.pendingSubmission = null; + } + }); + } } else { this.showMessage(`當前狀態不允許提交: ${this.feedbackState}`, 'warning'); } return; } + // 收集回饋數據並提交 + const feedbackData = this.collectFeedbackData(); + if (!feedbackData) { + return; + } + + this.submitFeedbackInternal(feedbackData); + } + + /** + * 收集回饋數據 + */ + collectFeedbackData() { // 根據當前佈局模式獲取回饋內容 let feedback = ''; if (this.layoutMode.startsWith('combined')) { @@ -1712,9 +1831,25 @@ class FeedbackApp { if (!feedback && this.images.length === 0) { this.showMessage('請提供回饋文字或上傳圖片', 'warning'); - return; + return null; } + return { + feedback: feedback, + images: [...this.images], // 創建副本 + settings: { + image_size_limit: this.imageSizeLimit, + enable_base64_detail: this.enableBase64Detail + } + }; + } + + /** + * 內部提交回饋方法 + */ + submitFeedbackInternal(feedbackData) { + console.log('📤 內部提交回饋...'); + // 設置處理狀態 this.setFeedbackState('processing'); @@ -1722,12 +1857,9 @@ class FeedbackApp { // 發送回饋 this.websocket.send(JSON.stringify({ type: 'submit_feedback', - feedback: feedback, - images: this.images, - settings: { - image_size_limit: this.imageSizeLimit, - enable_base64_detail: this.enableBase64Detail - } + feedback: feedbackData.feedback, + images: feedbackData.images, + settings: feedbackData.settings })); // 清空表單