♻️ 優化 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:
# 有舊連接,立即發送會話更新通知
# 有舊連接,立即發送會話更新通知並轉移連接
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"""

View File

@ -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")

View File

@ -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}`;
}
// 確保 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
}));
// 清空表單