2025-06-10 07:19:47 +08:00
|
|
|
/**
|
|
|
|
* MCP Feedback Enhanced - WebSocket 管理模組
|
|
|
|
* =========================================
|
|
|
|
*
|
|
|
|
* 處理 WebSocket 連接、訊息傳遞和重連邏輯
|
|
|
|
*/
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// 確保命名空間和依賴存在
|
|
|
|
window.MCPFeedback = window.MCPFeedback || {};
|
|
|
|
const Utils = window.MCPFeedback.Utils;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* WebSocket 管理器建構函數
|
|
|
|
*/
|
|
|
|
function WebSocketManager(options) {
|
|
|
|
options = options || {};
|
2025-06-13 05:48:08 +08:00
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
this.websocket = null;
|
|
|
|
this.isConnected = false;
|
|
|
|
this.connectionReady = false;
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
this.maxReconnectAttempts = options.maxReconnectAttempts || Utils.CONSTANTS.MAX_RECONNECT_ATTEMPTS;
|
|
|
|
this.reconnectDelay = options.reconnectDelay || Utils.CONSTANTS.DEFAULT_RECONNECT_DELAY;
|
|
|
|
this.heartbeatInterval = null;
|
|
|
|
this.heartbeatFrequency = options.heartbeatFrequency || Utils.CONSTANTS.DEFAULT_HEARTBEAT_FREQUENCY;
|
2025-06-13 05:48:08 +08:00
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
// 事件回調
|
|
|
|
this.onOpen = options.onOpen || null;
|
|
|
|
this.onMessage = options.onMessage || null;
|
|
|
|
this.onClose = options.onClose || null;
|
|
|
|
this.onError = options.onError || null;
|
|
|
|
this.onConnectionStatusChange = options.onConnectionStatusChange || null;
|
2025-06-13 05:48:08 +08:00
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
// 標籤頁管理器引用
|
|
|
|
this.tabManager = options.tabManager || null;
|
2025-06-13 05:48:08 +08:00
|
|
|
|
|
|
|
// 連線監控器引用
|
|
|
|
this.connectionMonitor = options.connectionMonitor || null;
|
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
// 待處理的提交
|
|
|
|
this.pendingSubmission = null;
|
|
|
|
this.sessionUpdatePending = false;
|
2025-06-15 14:51:36 +08:00
|
|
|
|
|
|
|
// 網路狀態檢測
|
|
|
|
this.networkOnline = navigator.onLine;
|
|
|
|
this.setupNetworkStatusDetection();
|
2025-06-10 07:19:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 建立 WebSocket 連接
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.connect = function() {
|
|
|
|
if (!Utils.isWebSocketSupported()) {
|
|
|
|
console.error('❌ 瀏覽器不支援 WebSocket');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 確保 WebSocket URL 格式正確
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
|
const host = window.location.host;
|
|
|
|
const wsUrl = protocol + '//' + host + '/ws';
|
|
|
|
|
|
|
|
console.log('嘗試連接 WebSocket:', wsUrl);
|
2025-06-13 10:33:24 +08:00
|
|
|
const connectingMessage = window.i18nManager ? window.i18nManager.t('connectionMonitor.connecting') : '連接中...';
|
|
|
|
this.updateConnectionStatus('connecting', connectingMessage);
|
2025-06-10 07:19:47 +08:00
|
|
|
|
|
|
|
try {
|
|
|
|
// 如果已有連接,先關閉
|
|
|
|
if (this.websocket) {
|
|
|
|
this.websocket.close();
|
|
|
|
this.websocket = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.websocket = new WebSocket(wsUrl);
|
|
|
|
this.setupWebSocketEvents();
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
console.error('WebSocket 連接失敗:', error);
|
2025-06-13 10:33:24 +08:00
|
|
|
const connectionFailedMessage = window.i18nManager ? window.i18nManager.t('connectionMonitor.connectionFailed') : '連接失敗';
|
|
|
|
this.updateConnectionStatus('error', connectionFailedMessage);
|
2025-06-10 07:19:47 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 設置 WebSocket 事件監聽器
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.setupWebSocketEvents = function() {
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
this.websocket.onopen = function() {
|
|
|
|
self.handleOpen();
|
|
|
|
};
|
|
|
|
|
|
|
|
this.websocket.onmessage = function(event) {
|
|
|
|
self.handleMessage(event);
|
|
|
|
};
|
|
|
|
|
|
|
|
this.websocket.onclose = function(event) {
|
|
|
|
self.handleClose(event);
|
|
|
|
};
|
|
|
|
|
|
|
|
this.websocket.onerror = function(error) {
|
|
|
|
self.handleError(error);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 處理連接開啟
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.handleOpen = function() {
|
|
|
|
this.isConnected = true;
|
|
|
|
this.connectionReady = false; // 等待連接確認
|
2025-06-13 10:33:24 +08:00
|
|
|
const connectedMessage = window.i18nManager ? window.i18nManager.t('connectionMonitor.connected') : '已連接';
|
|
|
|
this.updateConnectionStatus('connected', connectedMessage);
|
2025-06-10 07:19:47 +08:00
|
|
|
console.log('WebSocket 連接已建立');
|
|
|
|
|
|
|
|
// 重置重連計數器和延遲
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
this.reconnectDelay = Utils.CONSTANTS.DEFAULT_RECONNECT_DELAY;
|
|
|
|
|
2025-06-13 05:48:08 +08:00
|
|
|
// 通知連線監控器
|
|
|
|
if (this.connectionMonitor) {
|
|
|
|
this.connectionMonitor.startMonitoring();
|
|
|
|
}
|
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
// 開始心跳
|
|
|
|
this.startHeartbeat();
|
|
|
|
|
|
|
|
// 請求會話狀態
|
|
|
|
this.requestSessionStatus();
|
|
|
|
|
|
|
|
// 調用外部回調
|
|
|
|
if (this.onOpen) {
|
|
|
|
this.onOpen();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 處理訊息接收
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.handleMessage = function(event) {
|
|
|
|
try {
|
|
|
|
const data = Utils.safeJsonParse(event.data, null);
|
|
|
|
if (data) {
|
2025-06-13 05:48:08 +08:00
|
|
|
// 記錄訊息到監控器
|
|
|
|
if (this.connectionMonitor) {
|
|
|
|
this.connectionMonitor.recordMessage();
|
|
|
|
}
|
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
this.processMessage(data);
|
2025-06-13 05:48:08 +08:00
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
// 調用外部回調
|
|
|
|
if (this.onMessage) {
|
|
|
|
this.onMessage(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('解析 WebSocket 訊息失敗:', error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 處理連接關閉
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.handleClose = function(event) {
|
|
|
|
this.isConnected = false;
|
|
|
|
this.connectionReady = false;
|
|
|
|
console.log('WebSocket 連接已關閉, code:', event.code, 'reason:', event.reason);
|
|
|
|
|
|
|
|
// 停止心跳
|
|
|
|
this.stopHeartbeat();
|
|
|
|
|
2025-06-13 05:48:08 +08:00
|
|
|
// 通知連線監控器
|
|
|
|
if (this.connectionMonitor) {
|
|
|
|
this.connectionMonitor.stopMonitoring();
|
|
|
|
}
|
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
// 處理不同的關閉原因
|
|
|
|
if (event.code === 4004) {
|
2025-06-13 10:33:24 +08:00
|
|
|
const noActiveSessionMessage = window.i18nManager ? window.i18nManager.t('connectionMonitor.noActiveSession') : '沒有活躍會話';
|
|
|
|
this.updateConnectionStatus('disconnected', noActiveSessionMessage);
|
2025-06-10 07:19:47 +08:00
|
|
|
} else {
|
2025-06-13 10:33:24 +08:00
|
|
|
const disconnectedMessage = window.i18nManager ? window.i18nManager.t('connectionMonitor.disconnected') : '已斷開';
|
|
|
|
this.updateConnectionStatus('disconnected', disconnectedMessage);
|
2025-06-10 07:19:47 +08:00
|
|
|
this.handleReconnection(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 調用外部回調
|
|
|
|
if (this.onClose) {
|
|
|
|
this.onClose(event);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 處理連接錯誤
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.handleError = function(error) {
|
|
|
|
console.error('WebSocket 錯誤:', error);
|
2025-06-13 10:33:24 +08:00
|
|
|
const connectionErrorMessage = window.i18nManager ? window.i18nManager.t('connectionMonitor.connectionError') : '連接錯誤';
|
|
|
|
this.updateConnectionStatus('error', connectionErrorMessage);
|
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
// 調用外部回調
|
|
|
|
if (this.onError) {
|
|
|
|
this.onError(error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 處理重連邏輯
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.handleReconnection = function(event) {
|
|
|
|
// 會話更新導致的正常關閉,立即重連
|
|
|
|
if (event.code === 1000 && event.reason === '會話更新') {
|
|
|
|
console.log('🔄 會話更新導致的連接關閉,立即重連...');
|
|
|
|
this.sessionUpdatePending = true;
|
|
|
|
const self = this;
|
|
|
|
setTimeout(function() {
|
|
|
|
self.connect();
|
|
|
|
}, 200);
|
|
|
|
}
|
2025-06-15 14:51:36 +08:00
|
|
|
// 檢查是否應該重連
|
|
|
|
else if (this.shouldAttemptReconnect(event)) {
|
2025-06-10 07:19:47 +08:00
|
|
|
this.reconnectAttempts++;
|
2025-06-15 14:51:36 +08:00
|
|
|
|
|
|
|
// 改進的指數退避算法:基礎延遲 * 2^重試次數,加上隨機抖動
|
|
|
|
const baseDelay = Utils.CONSTANTS.DEFAULT_RECONNECT_DELAY;
|
|
|
|
const exponentialDelay = baseDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
|
|
const jitter = Math.random() * 1000; // 0-1秒的隨機抖動
|
|
|
|
this.reconnectDelay = Math.min(exponentialDelay + jitter, 30000); // 最大 30 秒
|
|
|
|
|
|
|
|
console.log(Math.round(this.reconnectDelay / 1000) + '秒後嘗試重連... (第' + this.reconnectAttempts + '次)');
|
2025-06-13 05:48:08 +08:00
|
|
|
|
|
|
|
// 更新狀態為重連中
|
2025-06-13 10:33:24 +08:00
|
|
|
const reconnectingTemplate = window.i18nManager ? window.i18nManager.t('connectionMonitor.reconnecting') : '重連中... (第{attempt}次)';
|
|
|
|
const reconnectingMessage = reconnectingTemplate.replace('{attempt}', this.reconnectAttempts);
|
|
|
|
this.updateConnectionStatus('reconnecting', reconnectingMessage);
|
2025-06-13 05:48:08 +08:00
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
const self = this;
|
|
|
|
setTimeout(function() {
|
|
|
|
console.log('🔄 開始重連 WebSocket... (第' + self.reconnectAttempts + '次)');
|
|
|
|
self.connect();
|
|
|
|
}, this.reconnectDelay);
|
|
|
|
} else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
|
|
console.log('❌ 達到最大重連次數,停止重連');
|
2025-06-13 10:33:24 +08:00
|
|
|
const maxReconnectMessage = window.i18nManager ? window.i18nManager.t('connectionMonitor.maxReconnectReached') : 'WebSocket 連接失敗,請刷新頁面重試';
|
|
|
|
Utils.showMessage(maxReconnectMessage, Utils.CONSTANTS.MESSAGE_ERROR);
|
2025-06-10 07:19:47 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 處理訊息
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.processMessage = function(data) {
|
|
|
|
console.log('收到 WebSocket 訊息:', data);
|
|
|
|
|
|
|
|
switch (data.type) {
|
|
|
|
case 'connection_established':
|
|
|
|
console.log('WebSocket 連接確認');
|
|
|
|
this.connectionReady = true;
|
|
|
|
this.handleConnectionReady();
|
|
|
|
break;
|
|
|
|
case 'heartbeat_response':
|
|
|
|
this.handleHeartbeatResponse();
|
2025-06-13 05:48:08 +08:00
|
|
|
// 記錄 pong 時間到監控器
|
|
|
|
if (this.connectionMonitor) {
|
|
|
|
this.connectionMonitor.recordPong();
|
|
|
|
}
|
2025-06-10 07:19:47 +08:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// 其他訊息類型由外部處理
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 處理連接就緒
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.handleConnectionReady = function() {
|
|
|
|
// 如果有待提交的內容,現在可以提交了
|
|
|
|
if (this.pendingSubmission) {
|
|
|
|
console.log('🔄 連接就緒,提交待處理的內容');
|
|
|
|
const self = this;
|
|
|
|
setTimeout(function() {
|
|
|
|
if (self.pendingSubmission) {
|
|
|
|
self.send(self.pendingSubmission);
|
|
|
|
self.pendingSubmission = null;
|
|
|
|
}
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 處理心跳回應
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.handleHeartbeatResponse = function() {
|
|
|
|
if (this.tabManager) {
|
|
|
|
this.tabManager.updateLastActivity();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 發送訊息
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.send = function(data) {
|
|
|
|
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
|
|
try {
|
|
|
|
this.websocket.send(JSON.stringify(data));
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
|
|
console.error('發送 WebSocket 訊息失敗:', error);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn('WebSocket 未連接,無法發送訊息');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 請求會話狀態
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.requestSessionStatus = function() {
|
|
|
|
this.send({
|
|
|
|
type: 'get_status'
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 開始心跳
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.startHeartbeat = function() {
|
|
|
|
this.stopHeartbeat();
|
|
|
|
|
|
|
|
const self = this;
|
|
|
|
this.heartbeatInterval = setInterval(function() {
|
|
|
|
if (self.websocket && self.websocket.readyState === WebSocket.OPEN) {
|
2025-06-13 05:48:08 +08:00
|
|
|
// 記錄 ping 時間到監控器
|
|
|
|
if (self.connectionMonitor) {
|
|
|
|
self.connectionMonitor.recordPing();
|
|
|
|
}
|
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
self.send({
|
|
|
|
type: 'heartbeat',
|
|
|
|
tabId: self.tabManager ? self.tabManager.getTabId() : null,
|
|
|
|
timestamp: Date.now()
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, this.heartbeatFrequency);
|
|
|
|
|
|
|
|
console.log('💓 WebSocket 心跳已啟動,頻率: ' + this.heartbeatFrequency + 'ms');
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 停止心跳
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.stopHeartbeat = function() {
|
|
|
|
if (this.heartbeatInterval) {
|
|
|
|
clearInterval(this.heartbeatInterval);
|
|
|
|
this.heartbeatInterval = null;
|
|
|
|
console.log('💔 WebSocket 心跳已停止');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 更新連接狀態
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.updateConnectionStatus = function(status, text) {
|
|
|
|
if (this.onConnectionStatusChange) {
|
|
|
|
this.onConnectionStatusChange(status, text);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 設置待處理的提交
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.setPendingSubmission = function(data) {
|
|
|
|
this.pendingSubmission = data;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 檢查是否已連接且就緒
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.isReady = function() {
|
|
|
|
return this.isConnected && this.connectionReady;
|
|
|
|
};
|
|
|
|
|
2025-06-15 14:51:36 +08:00
|
|
|
/**
|
|
|
|
* 設置網路狀態檢測
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.setupNetworkStatusDetection = function() {
|
|
|
|
const self = this;
|
|
|
|
|
|
|
|
// 監聽網路狀態變化
|
|
|
|
window.addEventListener('online', function() {
|
|
|
|
console.log('🌐 網路已恢復,嘗試重新連接...');
|
|
|
|
self.networkOnline = true;
|
|
|
|
|
|
|
|
// 如果 WebSocket 未連接且不在重連過程中,立即嘗試連接
|
|
|
|
if (!self.isConnected && self.reconnectAttempts < self.maxReconnectAttempts) {
|
|
|
|
// 重置重連計數器,因為網路問題已解決
|
|
|
|
self.reconnectAttempts = 0;
|
|
|
|
self.reconnectDelay = Utils.CONSTANTS.DEFAULT_RECONNECT_DELAY;
|
|
|
|
|
|
|
|
setTimeout(function() {
|
|
|
|
self.connect();
|
|
|
|
}, 1000); // 延遲 1 秒確保網路穩定
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
window.addEventListener('offline', function() {
|
|
|
|
console.log('🌐 網路已斷開');
|
|
|
|
self.networkOnline = false;
|
|
|
|
|
|
|
|
// 更新連接狀態
|
|
|
|
const offlineMessage = window.i18nManager ?
|
|
|
|
window.i18nManager.t('connectionMonitor.offline', '網路已斷開') :
|
|
|
|
'網路已斷開';
|
|
|
|
self.updateConnectionStatus('offline', offlineMessage);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 檢查是否應該嘗試重連
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.shouldAttemptReconnect = function(event) {
|
|
|
|
// 如果網路離線,不嘗試重連
|
|
|
|
if (!this.networkOnline) {
|
|
|
|
console.log('🌐 網路離線,跳過重連');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果是正常關閉,不重連
|
|
|
|
if (event.code === 1000) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果達到最大重連次數,不重連
|
|
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
2025-06-10 07:19:47 +08:00
|
|
|
/**
|
|
|
|
* 關閉連接
|
|
|
|
*/
|
|
|
|
WebSocketManager.prototype.close = function() {
|
|
|
|
this.stopHeartbeat();
|
|
|
|
if (this.websocket) {
|
|
|
|
this.websocket.close();
|
|
|
|
this.websocket = null;
|
|
|
|
}
|
|
|
|
this.isConnected = false;
|
|
|
|
this.connectionReady = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
// 將 WebSocketManager 加入命名空間
|
|
|
|
window.MCPFeedback.WebSocketManager = WebSocketManager;
|
|
|
|
|
|
|
|
console.log('✅ WebSocketManager 模組載入完成');
|
|
|
|
|
|
|
|
})();
|