♻️ 優化 WebSocket 連接管理

This commit is contained in:
Minidoracat 2025-06-08 00:52:30 +08:00
parent da8128c5bb
commit 42dee74c89
3 changed files with 209 additions and 47 deletions

View File

@ -261,7 +261,7 @@ class WebUIManager:
# 處理會話更新通知 # 處理會話更新通知
if old_websocket: if old_websocket:
# 有舊連接,立即發送會話更新通知 # 有舊連接,立即發送會話更新通知並轉移連接
self._old_websocket_for_update = old_websocket self._old_websocket_for_update = old_websocket
self._new_session_for_update = session self._new_session_for_update = session
debug_log("已保存舊 WebSocket 連接,準備發送會話更新通知") debug_log("已保存舊 WebSocket 連接,準備發送會話更新通知")
@ -269,10 +269,13 @@ class WebUIManager:
# 立即發送會話更新通知 # 立即發送會話更新通知
import asyncio import asyncio
try: try:
# 在後台任務中發送通知 # 在後台任務中發送通知並轉移連接
asyncio.create_task(self._send_immediate_session_update()) asyncio.create_task(self._send_immediate_session_update())
except Exception as e: except Exception as e:
debug_log(f"創建會話更新任務失敗: {e}") debug_log(f"創建會話更新任務失敗: {e}")
# 即使任務創建失敗,也要嘗試直接轉移連接
session.websocket = old_websocket
debug_log("任務創建失敗,直接轉移 WebSocket 連接到新會話")
self._pending_session_update = True self._pending_session_update = True
else: else:
# 沒有舊連接,標記需要發送會話更新通知(當新 WebSocket 連接建立時) # 沒有舊連接,標記需要發送會話更新通知(當新 WebSocket 連接建立時)
@ -505,8 +508,21 @@ 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
# 檢查舊連接是否仍然有效 # 改進的連接有效性檢查
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: try:
# 發送會話更新通知 # 發送會話更新通知
await old_websocket.send_json({ await old_websocket.send_json({
@ -523,11 +539,18 @@ class WebUIManager:
# 延遲一小段時間讓前端處理消息 # 延遲一小段時間讓前端處理消息
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
# 將 WebSocket 連接轉移到新會話
new_session.websocket = old_websocket
debug_log("已將 WebSocket 連接轉移到新會話")
except Exception as send_error: except Exception as send_error:
debug_log(f"發送會話更新通知失敗: {send_error}") debug_log(f"發送會話更新通知失敗: {send_error}")
# 如果發送失敗,仍然嘗試轉移連接
# 安全關閉舊連接 new_session.websocket = old_websocket
await self._safe_close_websocket(old_websocket) debug_log("發送失敗但仍轉移 WebSocket 連接到新會話")
else:
debug_log("舊 WebSocket 連接無效,設置待更新標記")
self._pending_session_update = True
# 清理臨時變數 # 清理臨時變數
delattr(self, '_old_websocket_for_update') delattr(self, '_old_websocket_for_update')
@ -544,29 +567,24 @@ class WebUIManager:
self._pending_session_update = True self._pending_session_update = True
async def _safe_close_websocket(self, websocket): async def _safe_close_websocket(self, websocket):
"""安全關閉 WebSocket 連接,避免事件循環衝突""" """安全關閉 WebSocket 連接,避免事件循環衝突 - 僅在連接已轉移後調用"""
if not websocket: if not websocket:
return return
# 注意:此方法現在主要用於清理,因為連接已經轉移到新會話
# 只有在確認連接沒有被新會話使用時才關閉
try: try:
# 檢查連接狀態 # 檢查連接狀態
if websocket.client_state.DISCONNECTED: if hasattr(websocket, 'client_state') and websocket.client_state.DISCONNECTED:
debug_log("WebSocket 已斷開,跳過關閉操作") debug_log("WebSocket 已斷開,跳過關閉操作")
return 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: except Exception as e:
debug_log(f"關閉 WebSocket 連接時發生未知錯誤: {e}") debug_log(f"檢查 WebSocket 連接狀態時發生錯誤: {e}")
async def _check_active_tabs(self) -> bool: async def _check_active_tabs(self) -> bool:
"""檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API""" """檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API"""

View File

@ -158,9 +158,13 @@ def setup_routes(manager: 'WebUIManager'):
return return
await websocket.accept() 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: try:
@ -198,7 +202,14 @@ def setup_routes(manager: 'WebUIManager'):
while True: while True:
data = await websocket.receive_text() data = await websocket.receive_text()
message = json.loads(data) 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: except WebSocketDisconnect:
debug_log(f"WebSocket 連接正常斷開") debug_log(f"WebSocket 連接正常斷開")
@ -208,8 +219,9 @@ def setup_routes(manager: 'WebUIManager'):
debug_log(f"WebSocket 錯誤: {e}") debug_log(f"WebSocket 錯誤: {e}")
finally: finally:
# 安全清理 WebSocket 連接 # 安全清理 WebSocket 連接
if session.websocket == websocket: current_session = manager.get_current_session()
session.websocket = None if current_session and current_session.websocket == websocket:
current_session.websocket = None
debug_log("已清理會話中的 WebSocket 連接") debug_log("已清理會話中的 WebSocket 連接")
@manager.app.post("/api/save-settings") @manager.app.post("/api/save-settings")

View File

@ -170,6 +170,13 @@ class FeedbackApp {
this.heartbeatInterval = null; this.heartbeatInterval = null;
this.heartbeatFrequency = 30000; // 30秒 WebSocket 心跳 this.heartbeatFrequency = 30000; // 30秒 WebSocket 心跳
// 新增WebSocket 連接狀態管理
this.connectionReady = false;
this.pendingSubmission = null;
this.connectionCheckInterval = null;
this.sessionUpdatePending = false;
this.reconnectDelay = 1000; // 重連延遲,會逐漸增加
// UI 狀態 // UI 狀態
this.currentTab = 'feedback'; this.currentTab = 'feedback';
@ -857,11 +864,11 @@ class FeedbackApp {
} }
/** /**
* 檢查是否可以提交回饋 * 檢查是否可以提交回饋舊版本保持兼容性
*/ */
canSubmitFeedback() { canSubmitFeedback() {
const canSubmit = this.feedbackState === 'waiting_for_feedback' && this.isConnected; const canSubmit = this.feedbackState === 'waiting_for_feedback' && this.isConnected && this.connectionReady;
console.log(`🔍 檢查提交權限: feedbackState=${this.feedbackState}, isConnected=${this.isConnected}, canSubmit=${canSubmit}`); console.log(`🔍 檢查提交權限: feedbackState=${this.feedbackState}, isConnected=${this.isConnected}, connectionReady=${this.connectionReady}, canSubmit=${canSubmit}`);
return canSubmit; return canSubmit;
} }
@ -1025,11 +1032,13 @@ class FeedbackApp {
this.websocket.onopen = () => { this.websocket.onopen = () => {
this.isConnected = true; this.isConnected = true;
this.connectionReady = false; // 等待連接確認
this.updateConnectionStatus('connected', '已連接'); this.updateConnectionStatus('connected', '已連接');
console.log('WebSocket 連接已建立'); console.log('WebSocket 連接已建立');
// 重置重連計數器 // 重置重連計數器和延遲
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
// 開始 WebSocket 心跳 // 開始 WebSocket 心跳
this.startWebSocketHeartbeat(); this.startWebSocketHeartbeat();
@ -1042,6 +1051,23 @@ class FeedbackApp {
console.log('🔄 WebSocket 重連後重置處理狀態'); console.log('🔄 WebSocket 重連後重置處理狀態');
this.setFeedbackState('waiting_for_feedback'); 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) => { this.websocket.onmessage = (event) => {
@ -1055,6 +1081,7 @@ class FeedbackApp {
this.websocket.onclose = (event) => { this.websocket.onclose = (event) => {
this.isConnected = false; this.isConnected = false;
this.connectionReady = false;
console.log('WebSocket 連接已關閉, code:', event.code, 'reason:', event.reason); console.log('WebSocket 連接已關閉, code:', event.code, 'reason:', event.reason);
// 停止心跳 // 停止心跳
@ -1072,15 +1099,23 @@ class FeedbackApp {
} else { } else {
this.updateConnectionStatus('disconnected', '已斷開'); 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++; this.reconnectAttempts++;
const delay = Math.min(3000 * this.reconnectAttempts, 15000); // 最大延遲15秒 this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 15000); // 指數退避,最大15秒
console.log(`${delay / 1000}秒後嘗試重連... (第${this.reconnectAttempts}次)`); console.log(`${this.reconnectDelay / 1000}秒後嘗試重連... (第${this.reconnectAttempts}次)`);
setTimeout(() => { setTimeout(() => {
console.log(`🔄 開始重連 WebSocket... (第${this.reconnectAttempts}次)`); console.log(`🔄 開始重連 WebSocket... (第${this.reconnectAttempts}次)`);
this.setupWebSocket(); this.setupWebSocket();
}, delay); }, this.reconnectDelay);
} else if (this.reconnectAttempts >= this.maxReconnectAttempts) { } else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('❌ 達到最大重連次數,停止重連'); console.log('❌ 達到最大重連次數,停止重連');
this.showMessage('WebSocket 連接失敗,請刷新頁面重試', 'error'); this.showMessage('WebSocket 連接失敗,請刷新頁面重試', 'error');
@ -1138,6 +1173,18 @@ class FeedbackApp {
switch (data.type) { switch (data.type) {
case 'connection_established': case 'connection_established':
console.log('WebSocket 連接確認'); console.log('WebSocket 連接確認');
this.connectionReady = true;
// 如果有待提交的回饋,現在可以提交了
if (this.pendingSubmission) {
console.log('🔄 連接就緒,提交待處理的回饋');
setTimeout(() => {
if (this.pendingSubmission) {
this.submitFeedbackInternal(this.pendingSubmission);
this.pendingSubmission = null;
}
}, 100);
}
break; break;
case 'heartbeat_response': case 'heartbeat_response':
// 心跳回應,更新標籤頁活躍狀態 // 心跳回應,更新標籤頁活躍狀態
@ -1209,8 +1256,11 @@ class FeedbackApp {
document.title = `MCP Feedback - ${projectName}`; document.title = `MCP Feedback - ${projectName}`;
} }
// 使用局部更新替代整頁刷新 // 確保 WebSocket 連接就緒
this.refreshPageContent(); this.ensureWebSocketReady(() => {
// 使用局部更新替代整頁刷新
this.refreshPageContent();
});
} else { } else {
// 如果沒有會話信息,仍然重置狀態 // 如果沒有會話信息,仍然重置狀態
console.log('⚠️ 會話更新沒有包含會話信息,僅重置狀態'); console.log('⚠️ 會話更新沒有包含會話信息,僅重置狀態');
@ -1220,6 +1270,51 @@ class FeedbackApp {
console.log('✅ 會話更新處理完成'); 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() { async refreshPageContent() {
console.log('🔄 局部更新頁面內容...'); console.log('🔄 局部更新頁面內容...');
@ -1684,22 +1779,46 @@ class FeedbackApp {
// 檢查是否可以提交回饋 // 檢查是否可以提交回饋
if (!this.canSubmitFeedback()) { if (!this.canSubmitFeedback()) {
console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState, '連接狀態:', this.isConnected); console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState, '連接狀態:', this.isConnected, '連接就緒:', this.connectionReady);
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.connectionReady) {
this.showMessage('WebSocket 未連接,正在嘗試重連...', 'error'); // 收集回饋數據,等待連接就緒後提交
// 嘗試重新建立連接 const feedbackData = this.collectFeedbackData();
this.setupWebSocket(); if (feedbackData) {
this.pendingSubmission = feedbackData;
this.showMessage('WebSocket 連接中,回饋將在連接就緒後自動提交...', 'info');
// 確保 WebSocket 連接
this.ensureWebSocketReady(() => {
if (this.pendingSubmission) {
this.submitFeedbackInternal(this.pendingSubmission);
this.pendingSubmission = null;
}
});
}
} else { } else {
this.showMessage(`當前狀態不允許提交: ${this.feedbackState}`, 'warning'); this.showMessage(`當前狀態不允許提交: ${this.feedbackState}`, 'warning');
} }
return; return;
} }
// 收集回饋數據並提交
const feedbackData = this.collectFeedbackData();
if (!feedbackData) {
return;
}
this.submitFeedbackInternal(feedbackData);
}
/**
* 收集回饋數據
*/
collectFeedbackData() {
// 根據當前佈局模式獲取回饋內容 // 根據當前佈局模式獲取回饋內容
let feedback = ''; let feedback = '';
if (this.layoutMode.startsWith('combined')) { if (this.layoutMode.startsWith('combined')) {
@ -1712,9 +1831,25 @@ class FeedbackApp {
if (!feedback && this.images.length === 0) { if (!feedback && this.images.length === 0) {
this.showMessage('請提供回饋文字或上傳圖片', 'warning'); 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'); this.setFeedbackState('processing');
@ -1722,12 +1857,9 @@ class FeedbackApp {
// 發送回饋 // 發送回饋
this.websocket.send(JSON.stringify({ this.websocket.send(JSON.stringify({
type: 'submit_feedback', type: 'submit_feedback',
feedback: feedback, feedback: feedbackData.feedback,
images: this.images, images: feedbackData.images,
settings: { settings: feedbackData.settings
image_size_limit: this.imageSizeLimit,
enable_base64_detail: this.enableBase64Detail
}
})); }));
// 清空表單 // 清空表單