🐛 修復 websocket 不正常斷開問題

This commit is contained in:
Minidoracat 2025-06-06 22:29:49 +08:00
parent ad44568c7c
commit 466dcffaa9
4 changed files with 117 additions and 31 deletions

View File

@ -332,30 +332,34 @@ class WebUIManager:
old_websocket = self._old_websocket_for_update old_websocket = self._old_websocket_for_update
new_session = self._new_session_for_update new_session = self._new_session_for_update
# 發送會話更新通知 # 檢查舊連接是否仍然有效
await old_websocket.send_json({ if old_websocket and not old_websocket.client_state.DISCONNECTED:
"type": "session_updated", try:
"message": "新會話已創建,正在更新頁面內容", # 發送會話更新通知
"session_info": { await old_websocket.send_json({
"project_directory": new_session.project_directory, "type": "session_updated",
"summary": new_session.summary, "message": "新會話已創建,正在更新頁面內容",
"session_id": new_session.session_id "session_info": {
} "project_directory": new_session.project_directory,
}) "summary": new_session.summary,
debug_log("已通過舊 WebSocket 連接發送會話更新通知") "session_id": new_session.session_id
}
})
debug_log("已通過舊 WebSocket 連接發送會話更新通知")
# 延遲一小段時間讓前端處理消息
await asyncio.sleep(0.2)
except Exception as send_error:
debug_log(f"發送會話更新通知失敗: {send_error}")
# 安全關閉舊連接
await self._safe_close_websocket(old_websocket)
# 清理臨時變數 # 清理臨時變數
delattr(self, '_old_websocket_for_update') delattr(self, '_old_websocket_for_update')
delattr(self, '_new_session_for_update') delattr(self, '_new_session_for_update')
# 延遲一小段時間讓前端處理消息,然後關閉舊連接
await asyncio.sleep(0.1)
try:
await old_websocket.close()
debug_log("已關閉舊 WebSocket 連接")
except Exception as e:
debug_log(f"關閉舊 WebSocket 連接失敗: {e}")
else: else:
# 沒有舊連接,設置待更新標記 # 沒有舊連接,設置待更新標記
self._pending_session_update = True self._pending_session_update = True
@ -366,6 +370,31 @@ class WebUIManager:
# 回退到待更新標記 # 回退到待更新標記
self._pending_session_update = True self._pending_session_update = True
async def _safe_close_websocket(self, websocket):
"""安全關閉 WebSocket 連接,避免事件循環衝突"""
if not websocket:
return
try:
# 檢查連接狀態
if websocket.client_state.DISCONNECTED:
debug_log("WebSocket 已斷開,跳過關閉操作")
return
# 嘗試正常關閉
await asyncio.wait_for(websocket.close(code=1000, reason="會話更新"), timeout=2.0)
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}")
async def _check_active_tabs(self) -> bool: async def _check_active_tabs(self) -> bool:
"""檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API""" """檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API"""
try: try:

View File

@ -324,7 +324,9 @@ class WebFeedbackSession:
"message": "會話已超時,介面將自動關閉" "message": "會話已超時,介面將自動關閉"
}) })
await asyncio.sleep(0.1) # 給前端一點時間處理消息 await asyncio.sleep(0.1) # 給前端一點時間處理消息
await self.websocket.close()
# 安全關閉 WebSocket
await self._safe_close_websocket()
debug_log(f"會話 {self.session_id} WebSocket 已關閉") debug_log(f"會話 {self.session_id} WebSocket 已關閉")
except Exception as e: except Exception as e:
debug_log(f"關閉 WebSocket 時發生錯誤: {e}") debug_log(f"關閉 WebSocket 時發生錯誤: {e}")
@ -401,4 +403,29 @@ class WebFeedbackSession:
self.process = None self.process = None
# 設置完成事件 # 設置完成事件
self.feedback_completed.set() self.feedback_completed.set()
async def _safe_close_websocket(self):
"""安全關閉 WebSocket 連接,避免事件循環衝突"""
if not self.websocket:
return
try:
# 檢查連接狀態
if hasattr(self.websocket, 'client_state') and self.websocket.client_state.DISCONNECTED:
debug_log("WebSocket 已斷開,跳過關閉操作")
return
# 嘗試正常關閉
await asyncio.wait_for(self.websocket.close(code=1000, reason="會話清理"), timeout=2.0)
debug_log(f"會話 {self.session_id} WebSocket 已正常關閉")
except asyncio.TimeoutError:
debug_log(f"會話 {self.session_id} WebSocket 關閉超時")
except RuntimeError as e:
if "attached to a different loop" in str(e):
debug_log(f"會話 {self.session_id} WebSocket 事件循環衝突,忽略關閉錯誤: {e}")
else:
debug_log(f"會話 {self.session_id} WebSocket 關閉時發生運行時錯誤: {e}")
except Exception as e:
debug_log(f"會話 {self.session_id} 關閉 WebSocket 時發生未知錯誤: {e}")

View File

@ -192,11 +192,16 @@ def setup_routes(manager: 'WebUIManager'):
await handle_websocket_message(manager, session, message) await handle_websocket_message(manager, session, message)
except WebSocketDisconnect: except WebSocketDisconnect:
debug_log(f"WebSocket 連接斷開") debug_log(f"WebSocket 連接正常斷開")
except ConnectionResetError:
debug_log(f"WebSocket 連接被重置")
except Exception as e: except Exception as e:
debug_log(f"WebSocket 錯誤: {e}") debug_log(f"WebSocket 錯誤: {e}")
finally: finally:
session.websocket = None # 安全清理 WebSocket 連接
if session.websocket == websocket:
session.websocket = None
debug_log("已清理會話中的 WebSocket 連接")
@manager.app.post("/api/save-settings") @manager.app.post("/api/save-settings")
async def save_settings(request: Request): async def save_settings(request: Request):

View File

@ -800,7 +800,9 @@ class FeedbackApp {
* 檢查是否可以提交回饋 * 檢查是否可以提交回饋
*/ */
canSubmitFeedback() { canSubmitFeedback() {
return this.feedbackState === 'waiting_for_feedback' && this.isConnected; const canSubmit = this.feedbackState === 'waiting_for_feedback' && this.isConnected;
console.log(`🔍 檢查提交權限: feedbackState=${this.feedbackState}, isConnected=${this.isConnected}, canSubmit=${canSubmit}`);
return canSubmit;
} }
/** /**
@ -989,6 +991,12 @@ class FeedbackApp {
// 停止心跳 // 停止心跳
this.stopWebSocketHeartbeat(); this.stopWebSocketHeartbeat();
// 重置回饋狀態,避免卡在處理狀態
if (this.feedbackState === 'processing') {
console.log('🔄 WebSocket 斷開,重置處理狀態');
this.setFeedbackState('waiting_for_feedback');
}
if (event.code === 4004) { if (event.code === 4004) {
// 沒有活躍會話 // 沒有活躍會話
this.updateConnectionStatus('disconnected', '沒有活躍會話'); this.updateConnectionStatus('disconnected', '沒有活躍會話');
@ -998,7 +1006,10 @@ class FeedbackApp {
// 只有在非正常關閉時才重連 // 只有在非正常關閉時才重連
if (event.code !== 1000) { if (event.code !== 1000) {
console.log('3秒後嘗試重連...'); console.log('3秒後嘗試重連...');
setTimeout(() => this.setupWebSocket(), 3000); setTimeout(() => {
console.log('🔄 開始重連 WebSocket...');
this.setupWebSocket();
}, 3000);
} }
} }
}; };
@ -1107,12 +1118,16 @@ class FeedbackApp {
// 顯示更新通知 // 顯示更新通知
this.showSuccessMessage(data.message || '會話已更新,正在局部更新內容...'); this.showSuccessMessage(data.message || '會話已更新,正在局部更新內容...');
// 重置回饋狀態為等待新回饋
this.setFeedbackState('waiting_for_feedback');
// 更新會話信息 // 更新會話信息
if (data.session_info) { if (data.session_info) {
this.currentSessionId = data.session_info.session_id; const newSessionId = data.session_info.session_id;
console.log(`📋 會話 ID 更新: ${this.currentSessionId} -> ${newSessionId}`);
// 重置回饋狀態為等待新回饋(使用新的會話 ID
this.setFeedbackState('waiting_for_feedback', newSessionId);
// 更新當前會話 ID
this.currentSessionId = newSessionId;
// 更新頁面標題 // 更新頁面標題
if (data.session_info.project_directory) { if (data.session_info.project_directory) {
@ -1122,6 +1137,10 @@ class FeedbackApp {
// 使用局部更新替代整頁刷新 // 使用局部更新替代整頁刷新
this.refreshPageContent(); this.refreshPageContent();
} else {
// 如果沒有會話信息,仍然重置狀態
console.log('⚠️ 會話更新沒有包含會話信息,僅重置狀態');
this.setFeedbackState('waiting_for_feedback');
} }
console.log('✅ 會話更新處理完成'); console.log('✅ 會話更新處理完成');
@ -1576,16 +1595,22 @@ class FeedbackApp {
// 所有事件監聽器已在 setupEventListeners() 中統一設置 // 所有事件監聽器已在 setupEventListeners() 中統一設置
submitFeedback() { submitFeedback() {
console.log('📤 嘗試提交回饋...');
// 檢查是否可以提交回饋 // 檢查是否可以提交回饋
if (!this.canSubmitFeedback()) { if (!this.canSubmitFeedback()) {
console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState); console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState, '連接狀態:', this.isConnected);
if (this.feedbackState === 'feedback_submitted') { if (this.feedbackState === 'feedback_submitted') {
this.showMessage('回饋已提交,請等待下次 MCP 調用', 'warning'); this.showMessage('回饋已提交,請等待下次 MCP 調用', 'warning');
} else if (this.feedbackState === 'processing') { } else if (this.feedbackState === 'processing') {
this.showMessage('正在處理中,請稍候', 'warning'); this.showMessage('正在處理中,請稍候', 'warning');
} else if (!this.isConnected) { } else if (!this.isConnected) {
this.showMessage('WebSocket 未連接', 'error'); this.showMessage('WebSocket 未連接,正在嘗試重連...', 'error');
// 嘗試重新建立連接
this.setupWebSocket();
} else {
this.showMessage(`當前狀態不允許提交: ${this.feedbackState}`, 'warning');
} }
return; return;
} }