mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 02:22:26 +08:00
♻️ 優化 WebSocket 連接管理
This commit is contained in:
parent
da8128c5bb
commit
42dee74c89
@ -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"""
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}));
|
||||
|
||||
// 清空表單
|
||||
|
Loading…
x
Reference in New Issue
Block a user