diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index d5f56e5..e7d4308 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -49,9 +49,45 @@ this.isInitialized = false; this.pendingSubmission = null; + // 初始化防抖函數 + this.initDebounceHandlers(); + console.log('🚀 FeedbackApp 建構函數初始化完成'); } + /** + * 初始化防抖處理器 + */ + FeedbackApp.prototype.initDebounceHandlers = function() { + // 為自動提交檢查添加防抖 + this._debouncedCheckAndStartAutoSubmit = window.MCPFeedback.Utils.DOM.debounce( + this._originalCheckAndStartAutoSubmit.bind(this), + 200, + false + ); + + // 為 WebSocket 訊息處理添加防抖 + this._debouncedHandleWebSocketMessage = window.MCPFeedback.Utils.DOM.debounce( + this._originalHandleWebSocketMessage.bind(this), + 50, + false + ); + + // 為會話更新處理添加防抖 + this._debouncedHandleSessionUpdated = window.MCPFeedback.Utils.DOM.debounce( + this._originalHandleSessionUpdated.bind(this), + 100, + false + ); + + // 為狀態更新處理添加防抖 + this._debouncedHandleStatusUpdate = window.MCPFeedback.Utils.DOM.debounce( + this._originalHandleStatusUpdate.bind(this), + 100, + false + ); + }; + /** * 初始化應用程式 */ @@ -222,7 +258,14 @@ self.checkAndStartAutoSubmit(); }, 500); // 延遲 500ms 確保所有初始化完成 - // 16. 建立 WebSocket 連接 + // 16. 播放啟動音效(如果音效已啟用) + setTimeout(function() { + if (self.audioManager) { + self.audioManager.playStartupNotification(); + } + }, 800); // 延遲 800ms 確保所有初始化完成且避免與其他音效衝突 + + // 17. 建立 WebSocket 連接 self.webSocketManager.connect(); resolve(); @@ -532,9 +575,9 @@ }; /** - * 處理 WebSocket 訊息 + * 處理 WebSocket 訊息(原始版本,供防抖使用) */ - FeedbackApp.prototype.handleWebSocketMessage = function(data) { + FeedbackApp.prototype._originalHandleWebSocketMessage = function(data) { console.log('📨 處理 WebSocket 訊息:', data); switch (data.type) { @@ -555,11 +598,11 @@ break; case 'status_update': console.log('狀態更新:', data.status_info); - this.handleStatusUpdate(data.status_info); + this._originalHandleStatusUpdate(data.status_info); break; case 'session_updated': console.log('🔄 收到會話更新訊息:', data.session_info); - this.handleSessionUpdated(data); + this._originalHandleSessionUpdated(data); break; case 'desktop_close_request': console.log('🖥️ 收到桌面關閉請求'); @@ -568,6 +611,18 @@ } }; + /** + * 處理 WebSocket 訊息(防抖版本) + */ + FeedbackApp.prototype.handleWebSocketMessage = function(data) { + if (this._debouncedHandleWebSocketMessage) { + this._debouncedHandleWebSocketMessage(data); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalHandleWebSocketMessage(data); + } + }; + /** * 處理 WebSocket 關閉 */ @@ -629,9 +684,9 @@ }; /** - * 處理會話更新 + * 處理會話更新(原始版本,供防抖使用) */ - FeedbackApp.prototype.handleSessionUpdated = function(data) { + FeedbackApp.prototype._originalHandleSessionUpdated = function(data) { console.log('🔄 處理會話更新:', data.session_info); // 播放音效通知 @@ -734,9 +789,21 @@ }; /** - * 處理狀態更新 + * 處理會話更新(防抖版本) */ - FeedbackApp.prototype.handleStatusUpdate = function(statusInfo) { + FeedbackApp.prototype.handleSessionUpdated = function(data) { + if (this._debouncedHandleSessionUpdated) { + this._debouncedHandleSessionUpdated(data); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalHandleSessionUpdated(data); + } + }; + + /** + * 處理狀態更新(原始版本,供防抖使用) + */ + FeedbackApp.prototype._originalHandleStatusUpdate = function(statusInfo) { console.log('處理狀態更新:', statusInfo); // 更新 SessionManager 的狀態資訊 @@ -784,6 +851,18 @@ } }; + /** + * 處理狀態更新(防抖版本) + */ + FeedbackApp.prototype.handleStatusUpdate = function(statusInfo) { + if (this._debouncedHandleStatusUpdate) { + this._debouncedHandleStatusUpdate(statusInfo); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalHandleStatusUpdate(statusInfo); + } + }; + /** * 提交回饋 */ @@ -1230,10 +1309,14 @@ }; /** - * 檢查並啟動自動提交(如果條件滿足) + * 檢查並啟動自動提交(原始版本,供防抖使用) */ - FeedbackApp.prototype.checkAndStartAutoSubmit = function() { - console.log('🔍 檢查自動提交條件...'); + FeedbackApp.prototype._originalCheckAndStartAutoSubmit = function() { + // 減少重複日誌:只在首次檢查或條件變化時記錄 + if (!this._lastAutoSubmitCheck || Date.now() - this._lastAutoSubmitCheck > 1000) { + console.log('🔍 檢查自動提交條件...'); + this._lastAutoSubmitCheck = Date.now(); + } if (!this.autoSubmitManager || !this.settingsManager || !this.promptManager) { console.log('⚠️ 自動提交管理器、設定管理器或提示詞管理器未初始化'); @@ -1288,6 +1371,18 @@ } }; + /** + * 檢查並啟動自動提交(防抖版本) + */ + FeedbackApp.prototype.checkAndStartAutoSubmit = function() { + if (this._debouncedCheckAndStartAutoSubmit) { + this._debouncedCheckAndStartAutoSubmit(); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalCheckAndStartAutoSubmit(); + } + }; + /** * 處理自動提交狀態變更 */ diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-manager.js index 7bf1840..dd6f988 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/audio/audio-manager.js @@ -58,10 +58,19 @@ // 當前播放的 Audio 物件 this.currentAudio = null; - + + // 用戶互動檢測 + this.userHasInteracted = false; + this.pendingNotifications = []; + this.autoplayBlocked = false; + this.interactionListenersAdded = false; + // 回調函數 this.onSettingsChange = options.onSettingsChange || null; - + + // 啟動音效播放標記 + this.startupNotificationPlayed = false; + console.log('🔊 AudioManager 初始化完成'); } @@ -70,6 +79,7 @@ */ AudioManager.prototype.initialize = function() { this.loadAudioSettings(); + this.setupUserInteractionDetection(); console.log('✅ AudioManager 初始化完成'); }; @@ -122,7 +132,7 @@ }; /** - * 播放通知音效 + * 播放通知音效(智能播放策略) */ AudioManager.prototype.playNotification = function() { if (!this.currentAudioSettings.enabled) { @@ -134,51 +144,114 @@ const audioData = this.getAudioById(this.currentAudioSettings.selectedAudioId); if (!audioData) { console.warn('⚠️ 找不到指定的音效,使用預設音效'); - this.playAudio(this.defaultAudios['default-beep']); + this.playAudioSmart(this.defaultAudios['default-beep']); return; } - this.playAudio(audioData); + this.playAudioSmart(audioData); } catch (error) { console.error('❌ 播放通知音效失敗:', error); } }; /** - * 播放指定的音效 + * 播放啟動音效通知(應用程式就緒時播放) + */ + AudioManager.prototype.playStartupNotification = function() { + if (!this.currentAudioSettings.enabled) { + console.log('🔇 音效通知已停用,跳過啟動音效'); + return; + } + + // 確保啟動音效只播放一次 + if (this.startupNotificationPlayed) { + console.log('🔇 啟動音效已播放過,跳過重複播放'); + return; + } + + this.startupNotificationPlayed = true; + console.log('🎵 播放應用程式啟動音效'); + + try { + const audioData = this.getAudioById(this.currentAudioSettings.selectedAudioId); + if (!audioData) { + console.warn('⚠️ 找不到指定的音效,使用預設啟動音效'); + this.playAudioSmart(this.defaultAudios['default-beep']); + return; + } + + this.playAudioSmart(audioData); + } catch (error) { + console.error('❌ 播放啟動音效失敗:', error); + } + }; + + /** + * 智能音效播放(處理自動播放限制) + */ + AudioManager.prototype.playAudioSmart = function(audioData) { + // 如果已知自動播放被阻止,直接加入待播放隊列 + if (this.autoplayBlocked && !this.userHasInteracted) { + this.addToPendingNotifications(audioData); + return; + } + + // 嘗試播放 + this.playAudio(audioData) + .then(() => { + // 播放成功,清空待播放隊列 + this.processPendingNotifications(); + }) + .catch((error) => { + if (error.name === 'NotAllowedError') { + // 自動播放被阻止 + this.autoplayBlocked = true; + this.addToPendingNotifications(audioData); + this.showAutoplayBlockedNotification(); + } + }); + }; + + /** + * 播放指定的音效(返回 Promise) */ AudioManager.prototype.playAudio = function(audioData) { - try { - // 停止當前播放的音效 - if (this.currentAudio) { - this.currentAudio.pause(); - this.currentAudio = null; - } + return new Promise((resolve, reject) => { + try { + // 停止當前播放的音效 + if (this.currentAudio) { + this.currentAudio.pause(); + this.currentAudio = null; + } - // 建立新的 Audio 物件 - this.currentAudio = new Audio(); - this.currentAudio.src = 'data:' + audioData.mimeType + ';base64,' + audioData.data; - this.currentAudio.volume = this.currentAudioSettings.volume / 100; + // 建立新的 Audio 物件 + this.currentAudio = new Audio(); + this.currentAudio.src = 'data:' + audioData.mimeType + ';base64,' + audioData.data; + this.currentAudio.volume = this.currentAudioSettings.volume / 100; - // 播放音效 - const playPromise = this.currentAudio.play(); - - if (playPromise !== undefined) { - playPromise - .then(() => { - console.log('🔊 音效播放成功:', audioData.name); - }) - .catch(error => { - console.error('❌ 音效播放失敗:', error); - // 可能是瀏覽器的自動播放政策限制 - if (error.name === 'NotAllowedError') { - console.warn('⚠️ 瀏覽器阻止自動播放,需要用戶互動'); - } - }); + // 播放音效 + const playPromise = this.currentAudio.play(); + + if (playPromise !== undefined) { + playPromise + .then(() => { + console.log('🔊 音效播放成功:', audioData.name); + resolve(); + }) + .catch(error => { + console.error('❌ 音效播放失敗:', error); + reject(error); + }); + } else { + // 舊版瀏覽器,假設播放成功 + console.log('🔊 音效播放(舊版瀏覽器):', audioData.name); + resolve(); + } + } catch (error) { + console.error('❌ 播放音效時發生錯誤:', error); + reject(error); } - } catch (error) { - console.error('❌ 播放音效時發生錯誤:', error); - } + }); }; /** @@ -433,6 +506,97 @@ return btoa(binary); }; + /** + * 設置用戶互動檢測 + */ + AudioManager.prototype.setupUserInteractionDetection = function() { + if (this.interactionListenersAdded) return; + + const self = this; + const interactionEvents = ['click', 'keydown', 'touchstart']; + + const handleUserInteraction = function() { + if (!self.userHasInteracted) { + self.userHasInteracted = true; + console.log('🎯 檢測到用戶互動,音效播放已解鎖'); + + // 播放待播放的通知 + self.processPendingNotifications(); + + // 移除事件監聽器 + interactionEvents.forEach(event => { + document.removeEventListener(event, handleUserInteraction, true); + }); + self.interactionListenersAdded = false; + } + }; + + // 添加事件監聽器 + interactionEvents.forEach(event => { + document.addEventListener(event, handleUserInteraction, true); + }); + + this.interactionListenersAdded = true; + console.log('🎯 用戶互動檢測已設置'); + }; + + /** + * 添加到待播放通知隊列 + */ + AudioManager.prototype.addToPendingNotifications = function(audioData) { + // 限制隊列長度,避免積累太多通知 + if (this.pendingNotifications.length >= 3) { + this.pendingNotifications.shift(); // 移除最舊的通知 + } + + this.pendingNotifications.push({ + audioData: audioData, + timestamp: Date.now() + }); + + console.log('📋 音效已加入待播放隊列:', audioData.name, '隊列長度:', this.pendingNotifications.length); + }; + + /** + * 處理待播放的通知 + */ + AudioManager.prototype.processPendingNotifications = function() { + if (this.pendingNotifications.length === 0) return; + + console.log('🔊 處理待播放通知,數量:', this.pendingNotifications.length); + + // 只播放最新的通知,避免音效重疊 + const latestNotification = this.pendingNotifications[this.pendingNotifications.length - 1]; + this.pendingNotifications = []; // 清空隊列 + + this.playAudio(latestNotification.audioData) + .then(() => { + console.log('🔊 待播放通知播放成功'); + }) + .catch(error => { + console.warn('⚠️ 待播放通知播放失敗:', error); + }); + }; + + /** + * 顯示自動播放被阻止的通知 + */ + AudioManager.prototype.showAutoplayBlockedNotification = function() { + // 只顯示一次通知 + if (this.autoplayNotificationShown) return; + this.autoplayNotificationShown = true; + + console.log('🔇 瀏覽器阻止音效自動播放,請點擊頁面任意位置以啟用音效通知'); + + // 可以在這裡添加 UI 通知邏輯 + if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) { + const message = window.i18nManager ? + window.i18nManager.t('audio.autoplayBlocked', '瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知') : + '瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知'; + window.MCPFeedback.Utils.showMessage(message, 'info'); + } + }; + /** * 獲取當前設定 */ diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/logger.js b/src/mcp_feedback_enhanced/web/static/js/modules/logger.js new file mode 100644 index 0000000..b532721 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/static/js/modules/logger.js @@ -0,0 +1,339 @@ +/** + * MCP Feedback Enhanced - 日誌管理模組 + * =================================== + * + * 統一的日誌管理系統,支援不同等級的日誌輸出 + * 生產環境可關閉詳細日誌以提升效能 + */ + +(function() { + 'use strict'; + + // 確保命名空間存在 + window.MCPFeedback = window.MCPFeedback || {}; + + /** + * 日誌等級枚舉 + */ + const LogLevel = { + ERROR: 0, // 錯誤:嚴重問題,必須記錄 + WARN: 1, // 警告:潛在問題,建議記錄 + INFO: 2, // 資訊:一般資訊,正常記錄 + DEBUG: 3, // 調試:詳細資訊,開發時記錄 + TRACE: 4 // 追蹤:最詳細資訊,深度調試時記錄 + }; + + /** + * 日誌等級名稱映射 + */ + const LogLevelNames = { + [LogLevel.ERROR]: 'ERROR', + [LogLevel.WARN]: 'WARN', + [LogLevel.INFO]: 'INFO', + [LogLevel.DEBUG]: 'DEBUG', + [LogLevel.TRACE]: 'TRACE' + }; + + /** + * 日誌管理器 + */ + function Logger(options) { + options = options || {}; + + // 當前日誌等級(預設為 INFO) + this.currentLevel = this.parseLogLevel(options.level) || LogLevel.INFO; + + // 模組名稱 + this.moduleName = options.moduleName || 'App'; + + // 是否啟用時間戳 + this.enableTimestamp = options.enableTimestamp !== false; + + // 是否啟用模組名稱 + this.enableModuleName = options.enableModuleName !== false; + + // 是否啟用顏色(僅在支援的環境中) + this.enableColors = options.enableColors !== false; + + // 自訂輸出函數 + this.customOutput = options.customOutput || null; + + // 日誌緩衝區(用於收集日誌) + this.logBuffer = []; + this.maxBufferSize = options.maxBufferSize || 1000; + + // 顏色映射 + this.colors = { + [LogLevel.ERROR]: '#f44336', // 紅色 + [LogLevel.WARN]: '#ff9800', // 橙色 + [LogLevel.INFO]: '#2196f3', // 藍色 + [LogLevel.DEBUG]: '#4caf50', // 綠色 + [LogLevel.TRACE]: '#9c27b0' // 紫色 + }; + } + + /** + * 解析日誌等級 + */ + Logger.prototype.parseLogLevel = function(level) { + if (typeof level === 'number') { + return level; + } + + if (typeof level === 'string') { + const upperLevel = level.toUpperCase(); + for (const [value, name] of Object.entries(LogLevelNames)) { + if (name === upperLevel) { + return parseInt(value); + } + } + } + + return null; + }; + + /** + * 設置日誌等級 + */ + Logger.prototype.setLevel = function(level) { + const parsedLevel = this.parseLogLevel(level); + if (parsedLevel !== null) { + this.currentLevel = parsedLevel; + this.info('日誌等級已設置為:', LogLevelNames[this.currentLevel]); + } else { + this.warn('無效的日誌等級:', level); + } + }; + + /** + * 獲取當前日誌等級 + */ + Logger.prototype.getLevel = function() { + return this.currentLevel; + }; + + /** + * 檢查是否應該記錄指定等級的日誌 + */ + Logger.prototype.shouldLog = function(level) { + return level <= this.currentLevel; + }; + + /** + * 格式化日誌訊息 + */ + Logger.prototype.formatMessage = function(level, args) { + const parts = []; + + // 添加時間戳 + if (this.enableTimestamp) { + const now = new Date(); + const timestamp = now.toISOString().substr(11, 12); // HH:mm:ss.SSS + parts.push(`[${timestamp}]`); + } + + // 添加等級 + parts.push(`[${LogLevelNames[level]}]`); + + // 添加模組名稱 + if (this.enableModuleName) { + parts.push(`[${this.moduleName}]`); + } + + // 組合前綴 + const prefix = parts.join(' '); + + // 轉換參數為字符串 + const messages = Array.from(args).map(arg => { + if (typeof arg === 'object') { + try { + return JSON.stringify(arg, null, 2); + } catch (e) { + return String(arg); + } + } + return String(arg); + }); + + return { + prefix: prefix, + message: messages.join(' '), + fullMessage: prefix + ' ' + messages.join(' ') + }; + }; + + /** + * 輸出日誌 + */ + Logger.prototype.output = function(level, formatted) { + // 添加到緩衝區 + this.addToBuffer(level, formatted); + + // 如果有自訂輸出函數,使用它 + if (this.customOutput) { + this.customOutput(level, formatted); + return; + } + + // 使用瀏覽器控制台 + const consoleMethods = { + [LogLevel.ERROR]: 'error', + [LogLevel.WARN]: 'warn', + [LogLevel.INFO]: 'info', + [LogLevel.DEBUG]: 'log', + [LogLevel.TRACE]: 'log' + }; + + const method = consoleMethods[level] || 'log'; + + // 如果支援顏色且啟用 + if (this.enableColors && console.log.toString().indexOf('native') === -1) { + const color = this.colors[level]; + console[method](`%c${formatted.fullMessage}`, `color: ${color}`); + } else { + console[method](formatted.fullMessage); + } + }; + + /** + * 添加到日誌緩衝區 + */ + Logger.prototype.addToBuffer = function(level, formatted) { + const logEntry = { + timestamp: Date.now(), + level: level, + levelName: LogLevelNames[level], + moduleName: this.moduleName, + message: formatted.message, + fullMessage: formatted.fullMessage + }; + + this.logBuffer.push(logEntry); + + // 限制緩衝區大小 + if (this.logBuffer.length > this.maxBufferSize) { + this.logBuffer.shift(); + } + }; + + /** + * 通用日誌方法 + */ + Logger.prototype.log = function(level) { + if (!this.shouldLog(level)) { + return; + } + + const args = Array.prototype.slice.call(arguments, 1); + const formatted = this.formatMessage(level, args); + this.output(level, formatted); + }; + + /** + * 錯誤日誌 + */ + Logger.prototype.error = function() { + this.log.apply(this, [LogLevel.ERROR].concat(Array.prototype.slice.call(arguments))); + }; + + /** + * 警告日誌 + */ + Logger.prototype.warn = function() { + this.log.apply(this, [LogLevel.WARN].concat(Array.prototype.slice.call(arguments))); + }; + + /** + * 資訊日誌 + */ + Logger.prototype.info = function() { + this.log.apply(this, [LogLevel.INFO].concat(Array.prototype.slice.call(arguments))); + }; + + /** + * 調試日誌 + */ + Logger.prototype.debug = function() { + this.log.apply(this, [LogLevel.DEBUG].concat(Array.prototype.slice.call(arguments))); + }; + + /** + * 追蹤日誌 + */ + Logger.prototype.trace = function() { + this.log.apply(this, [LogLevel.TRACE].concat(Array.prototype.slice.call(arguments))); + }; + + /** + * 獲取日誌緩衝區 + */ + Logger.prototype.getBuffer = function() { + return this.logBuffer.slice(); // 返回副本 + }; + + /** + * 清空日誌緩衝區 + */ + Logger.prototype.clearBuffer = function() { + this.logBuffer = []; + }; + + /** + * 導出日誌 + */ + Logger.prototype.exportLogs = function(options) { + options = options || {}; + const format = options.format || 'json'; + const minLevel = this.parseLogLevel(options.minLevel) || LogLevel.ERROR; + + const filteredLogs = this.logBuffer.filter(log => log.level <= minLevel); + + if (format === 'json') { + return JSON.stringify(filteredLogs, null, 2); + } else if (format === 'text') { + return filteredLogs.map(log => log.fullMessage).join('\n'); + } + + return filteredLogs; + }; + + // 全域日誌管理器 + const globalLogger = new Logger({ + moduleName: 'Global', + level: LogLevel.INFO + }); + + // 從環境變數或 URL 參數檢測日誌等級 + function detectLogLevel() { + // 檢查 URL 參數 + const urlParams = new URLSearchParams(window.location.search); + const urlLogLevel = urlParams.get('logLevel') || urlParams.get('log_level'); + if (urlLogLevel) { + return urlLogLevel; + } + + // 檢查 localStorage + const storedLevel = localStorage.getItem('mcp-log-level'); + if (storedLevel) { + return storedLevel; + } + + // 檢查是否為開發環境 + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + return LogLevel.DEBUG; + } + + return LogLevel.INFO; + } + + // 設置全域日誌等級 + globalLogger.setLevel(detectLogLevel()); + + // 匯出到全域命名空間 + window.MCPFeedback.Logger = Logger; + window.MCPFeedback.LogLevel = LogLevel; + window.MCPFeedback.logger = globalLogger; + + console.log('✅ Logger 模組載入完成,當前等級:', LogLevelNames[globalLogger.getLevel()]); + +})(); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js index 75ad7a4..1bca3f1 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js @@ -63,6 +63,9 @@ showFullSessionId: options.showFullSessionId || false }); + // 初始化防抖處理器 + this.initDebounceHandlers(); + // 最後初始化數據管理器(確保 UI 組件已準備好接收回調) this.dataManager = new window.MCPFeedback.Session.DataManager({ settingsManager: this.settingsManager, @@ -81,13 +84,49 @@ }); }; + /** + * 初始化防抖處理器 + */ + SessionManager.prototype.initDebounceHandlers = function() { + // 為會話變更處理添加防抖 + this._debouncedHandleSessionChange = window.MCPFeedback.Utils.DOM.debounce( + this._originalHandleSessionChange.bind(this), + 100, + false + ); + // 為歷史記錄變更處理添加防抖 + this._debouncedHandleHistoryChange = window.MCPFeedback.Utils.DOM.debounce( + this._originalHandleHistoryChange.bind(this), + 150, + false + ); + + // 為統計資訊變更處理添加防抖 + this._debouncedHandleStatsChange = window.MCPFeedback.Utils.DOM.debounce( + this._originalHandleStatsChange.bind(this), + 100, + false + ); + + // 為資料變更處理添加防抖 + this._debouncedHandleDataChanged = window.MCPFeedback.Utils.DOM.debounce( + this._originalHandleDataChanged.bind(this), + 200, + false + ); + }; /** - * 處理會話變更 + * 處理會話變更(原始版本,供防抖使用) */ - SessionManager.prototype.handleSessionChange = function(sessionData) { - console.log('📋 處理會話變更:', sessionData); + SessionManager.prototype._originalHandleSessionChange = function(sessionData) { + // 減少重複日誌:只在會話 ID 變化時記錄 + const sessionId = sessionData ? sessionData.session_id : null; + if (!this._lastSessionId || this._lastSessionId !== sessionId) { + console.log('📋 處理會話變更:', sessionData); + this._lastSessionId = sessionId; + } // 更新 UI 渲染 this.uiRenderer.renderCurrentSession(sessionData); @@ -99,29 +138,74 @@ }; /** - * 處理歷史記錄變更 + * 處理會話變更(防抖版本) */ - SessionManager.prototype.handleHistoryChange = function(history) { - console.log('📋 處理歷史記錄變更:', history.length, '個會話'); + SessionManager.prototype.handleSessionChange = function(sessionData) { + if (this._debouncedHandleSessionChange) { + this._debouncedHandleSessionChange(sessionData); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalHandleSessionChange(sessionData); + } + }; + + /** + * 處理歷史記錄變更(原始版本,供防抖使用) + */ + SessionManager.prototype._originalHandleHistoryChange = function(history) { + // 減少重複日誌:只在歷史記錄數量變化時記錄 + if (!this._lastHistoryCount || this._lastHistoryCount !== history.length) { + console.log('📋 處理歷史記錄變更:', history.length, '個會話'); + this._lastHistoryCount = history.length; + } // 更新 UI 渲染 this.uiRenderer.renderSessionHistory(history); }; /** - * 處理統計資訊變更 + * 處理歷史記錄變更(防抖版本) */ - SessionManager.prototype.handleStatsChange = function(stats) { - console.log('📋 處理統計資訊變更:', stats); + SessionManager.prototype.handleHistoryChange = function(history) { + if (this._debouncedHandleHistoryChange) { + this._debouncedHandleHistoryChange(history); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalHandleHistoryChange(history); + } + }; + + /** + * 處理統計資訊變更(原始版本,供防抖使用) + */ + SessionManager.prototype._originalHandleStatsChange = function(stats) { + // 減少重複日誌:只在統計資訊有意義變化時記錄 + const statsKey = stats ? JSON.stringify(stats) : null; + if (!this._lastStatsKey || this._lastStatsKey !== statsKey) { + console.log('📋 處理統計資訊變更:', stats); + this._lastStatsKey = statsKey; + } // 更新 UI 渲染 this.uiRenderer.renderStats(stats); }; /** - * 處理資料變更(用於異步載入完成後的更新) + * 處理統計資訊變更(防抖版本) */ - SessionManager.prototype.handleDataChanged = function() { + SessionManager.prototype.handleStatsChange = function(stats) { + if (this._debouncedHandleStatsChange) { + this._debouncedHandleStatsChange(stats); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalHandleStatsChange(stats); + } + }; + + /** + * 處理資料變更(原始版本,供防抖使用) + */ + SessionManager.prototype._originalHandleDataChanged = function() { console.log('📋 處理資料變更,重新渲染所有內容'); // 重新渲染所有內容 @@ -134,6 +218,18 @@ this.uiRenderer.renderStats(stats); }; + /** + * 處理資料變更(防抖版本) + */ + SessionManager.prototype.handleDataChanged = function() { + if (this._debouncedHandleDataChanged) { + this._debouncedHandleDataChanged(); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalHandleDataChanged(); + } + }; + /** * 設置事件監聽器 */ diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js index 4480119..abe0f72 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js @@ -14,6 +14,11 @@ const DOMUtils = window.MCPFeedback.Utils.DOM; const TimeUtils = window.MCPFeedback.Utils.Time; + + // 創建模組專用日誌器 + const logger = window.MCPFeedback.Logger ? + new window.MCPFeedback.Logger({ moduleName: 'SessionUIRenderer' }) : + console; const StatusUtils = window.MCPFeedback.Utils.Status; /** @@ -35,11 +40,26 @@ this.activeTimeTimer = null; this.currentSessionData = null; + // 渲染防抖機制 + this.renderDebounceTimers = { + stats: null, + history: null, + currentSession: null + }; + this.renderDebounceDelay = options.renderDebounceDelay || 100; // 預設 100ms 防抖延遲 + + // 快取上次渲染的數據,避免不必要的重渲染 + this.lastRenderedData = { + stats: null, + historyLength: 0, + currentSessionId: null + }; + this.initializeElements(); this.initializeProjectPathDisplay(); this.startActiveTimeTimer(); - console.log('🎨 SessionUIRenderer 初始化完成'); + logger.info('SessionUIRenderer 初始化完成,渲染防抖延遲:', this.renderDebounceDelay + 'ms'); } /** @@ -109,18 +129,49 @@ }; /** - * 渲染當前會話 + * 渲染當前會話(帶防抖機制) */ SessionUIRenderer.prototype.renderCurrentSession = function(sessionData) { if (!this.currentSessionCard || !sessionData) return; - console.log('🎨 渲染當前會話:', sessionData); + const self = this; // 檢查是否是新會話(會話 ID 變更) const isNewSession = !this.currentSessionData || this.currentSessionData.session_id !== sessionData.session_id; - // 更新當前會話數據 + // 檢查數據是否有變化 + if (!isNewSession && self.lastRenderedData.currentSessionId === sessionData.session_id && + self.currentSessionData && + self.currentSessionData.status === sessionData.status && + self.currentSessionData.summary === sessionData.summary) { + // 數據沒有重要變化,跳過渲染 + return; + } + + // 清除之前的防抖定時器 + if (self.renderDebounceTimers.currentSession) { + clearTimeout(self.renderDebounceTimers.currentSession); + } + + // 對於新會話,立即渲染;對於更新,使用防抖 + if (isNewSession) { + self._performCurrentSessionRender(sessionData, isNewSession); + } else { + self.renderDebounceTimers.currentSession = setTimeout(function() { + self._performCurrentSessionRender(sessionData, false); + }, self.renderDebounceDelay); + } + }; + + /** + * 執行實際的當前會話渲染 + */ + SessionUIRenderer.prototype._performCurrentSessionRender = function(sessionData, isNewSession) { + console.log('🎨 渲染當前會話:', sessionData); + + // 更新快取 + this.lastRenderedData.currentSessionId = sessionData.session_id; this.currentSessionData = sessionData; // 如果是新會話,重置活躍時間定時器 @@ -334,13 +385,39 @@ }; /** - * 渲染會話歷史列表 + * 渲染會話歷史列表(帶防抖機制) */ SessionUIRenderer.prototype.renderSessionHistory = function(sessionHistory) { - if (!this.historyList) return; + if (!this.historyList || !sessionHistory) return; + const self = this; + + // 檢查數據是否有變化(簡單比較長度) + if (self.lastRenderedData.historyLength === sessionHistory.length) { + // 長度沒有變化,跳過渲染(可以進一步優化為深度比較) + return; + } + + // 清除之前的防抖定時器 + if (self.renderDebounceTimers.history) { + clearTimeout(self.renderDebounceTimers.history); + } + + // 設置新的防抖定時器 + self.renderDebounceTimers.history = setTimeout(function() { + self._performHistoryRender(sessionHistory); + }, self.renderDebounceDelay); + }; + + /** + * 執行實際的會話歷史渲染 + */ + SessionUIRenderer.prototype._performHistoryRender = function(sessionHistory) { console.log('🎨 渲染會話歷史:', sessionHistory.length, '個會話'); + // 更新快取 + this.lastRenderedData.historyLength = sessionHistory.length; + // 清空現有內容 DOMUtils.clearElement(this.historyList); @@ -519,30 +596,59 @@ }; /** - * 渲染統計資訊 + * 渲染統計資訊(帶防抖機制) */ SessionUIRenderer.prototype.renderStats = function(stats) { - console.log('🎨 渲染統計資訊:', stats); - console.log('🎨 統計元素狀態:', { - todayCount: !!this.statsElements.todayCount, - averageDuration: !!this.statsElements.averageDuration - }); + if (!stats) return; + + const self = this; + + // 檢查數據是否有變化 + if (self.lastRenderedData.stats && + self.lastRenderedData.stats.todayCount === stats.todayCount && + self.lastRenderedData.stats.averageDuration === stats.averageDuration) { + // 數據沒有變化,跳過渲染 + return; + } + + // 清除之前的防抖定時器 + if (self.renderDebounceTimers.stats) { + clearTimeout(self.renderDebounceTimers.stats); + } + + // 設置新的防抖定時器 + self.renderDebounceTimers.stats = setTimeout(function() { + self._performStatsRender(stats); + }, self.renderDebounceDelay); + }; + + /** + * 執行實際的統計資訊渲染 + */ + SessionUIRenderer.prototype._performStatsRender = function(stats) { + logger.debug('渲染統計資訊:', stats); + + // 更新快取 + this.lastRenderedData.stats = { + todayCount: stats.todayCount, + averageDuration: stats.averageDuration + }; // 更新今日會話數 if (this.statsElements.todayCount) { DOMUtils.safeSetTextContent(this.statsElements.todayCount, stats.todayCount.toString()); - console.log('🎨 已更新今日會話數:', stats.todayCount); + logger.debug('已更新今日會話數:', stats.todayCount); } else { - console.warn('🎨 找不到今日會話數元素 (.stat-today-count)'); + logger.warn('找不到今日會話數元素 (.stat-today-count)'); } // 更新今日平均時長 if (this.statsElements.averageDuration) { const durationText = TimeUtils.formatDuration(stats.averageDuration); DOMUtils.safeSetTextContent(this.statsElements.averageDuration, durationText); - console.log('🎨 已更新今日平均時長:', durationText); + logger.debug('已更新今日平均時長:', durationText); } else { - console.warn('🎨 找不到平均時長元素 (.stat-average-duration)'); + logger.warn('找不到平均時長元素 (.stat-average-duration)'); } }; @@ -624,11 +730,24 @@ // 停止定時器 this.stopActiveTimeTimer(); + // 清理防抖定時器 + Object.keys(this.renderDebounceTimers).forEach(key => { + if (this.renderDebounceTimers[key]) { + clearTimeout(this.renderDebounceTimers[key]); + this.renderDebounceTimers[key] = null; + } + }); + // 清理引用 this.currentSessionCard = null; this.historyList = null; this.statsElements = {}; this.currentSessionData = null; + this.lastRenderedData = { + stats: null, + historyLength: 0, + currentSessionId: null + }; console.log('🎨 SessionUIRenderer 清理完成'); }; diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js index 1ad2549..f636e2b 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js @@ -12,6 +12,11 @@ window.MCPFeedback = window.MCPFeedback || {}; const Utils = window.MCPFeedback.Utils; + // 創建模組專用日誌器 + const logger = window.MCPFeedback.Logger ? + new window.MCPFeedback.Logger({ moduleName: 'SettingsManager' }) : + console; + /** * 設定管理器建構函數 */ @@ -52,6 +57,13 @@ this.onSettingsChange = options.onSettingsChange || null; this.onLanguageChange = options.onLanguageChange || null; this.onAutoSubmitStateChange = options.onAutoSubmitStateChange || null; + + // 防抖機制相關 + this.saveToServerDebounceTimer = null; + this.saveToServerDebounceDelay = options.saveDebounceDelay || 500; // 預設 500ms 防抖延遲 + this.pendingServerSave = false; + + console.log('✅ SettingsManager 建構函數初始化完成,防抖延遲:', this.saveToServerDebounceDelay + 'ms'); } /** @@ -61,14 +73,14 @@ const self = this; return new Promise(function(resolve, reject) { - console.log('開始載入設定...'); + logger.info('開始載入設定...'); // 優先從伺服器端載入設定 self.loadFromServer() .then(function(serverSettings) { if (serverSettings && Object.keys(serverSettings).length > 0) { self.currentSettings = self.mergeSettings(self.defaultSettings, serverSettings); - console.log('從伺服器端載入設定成功:', self.currentSettings); + logger.info('從伺服器端載入設定成功:', self.currentSettings); // 同步到 localStorage self.saveToLocalStorage(); @@ -143,7 +155,7 @@ this.currentSettings = this.mergeSettings(this.currentSettings, newSettings); } - console.log('保存設定:', this.currentSettings); + logger.debug('保存設定:', this.currentSettings); // 保存到 localStorage this.saveToLocalStorage(); @@ -175,15 +187,43 @@ }; /** - * 保存到伺服器 + * 保存到伺服器(帶防抖機制) */ SettingsManager.prototype.saveToServer = function() { + const self = this; + + // 清除之前的定時器 + if (self.saveToServerDebounceTimer) { + clearTimeout(self.saveToServerDebounceTimer); + } + + // 標記有待處理的保存操作 + self.pendingServerSave = true; + + // 設置新的防抖定時器 + self.saveToServerDebounceTimer = setTimeout(function() { + self._performServerSave(); + }, self.saveToServerDebounceDelay); + }; + + /** + * 執行實際的伺服器保存操作 + */ + SettingsManager.prototype._performServerSave = function() { + const self = this; + + if (!self.pendingServerSave) { + return; + } + + self.pendingServerSave = false; + fetch('/api/save-settings', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(this.currentSettings) + body: JSON.stringify(self.currentSettings) }) .then(function(response) { if (response.ok) { @@ -197,6 +237,21 @@ }); }; + /** + * 立即保存到伺服器(跳過防抖機制) + * 用於重要操作,如語言變更、重置設定等 + */ + SettingsManager.prototype.saveToServerImmediate = function() { + // 清除防抖定時器 + if (this.saveToServerDebounceTimer) { + clearTimeout(this.saveToServerDebounceTimer); + this.saveToServerDebounceTimer = null; + } + + // 立即執行保存 + this._performServerSave(); + }; + /** * 合併設定 */ @@ -232,9 +287,19 @@ // 特殊處理語言變更 if (key === 'language' && oldValue !== value) { this.handleLanguageChange(value); + // 語言變更是重要操作,立即保存 + this.saveToLocalStorage(); + this.saveToServerImmediate(); + + // 觸發回調 + if (this.onSettingsChange) { + this.onSettingsChange(this.currentSettings); + } + } else { + // 一般設定變更使用防抖保存 + this.saveSettings(); } - - this.saveSettings(); + return this; }; @@ -304,8 +369,14 @@ // 重置為預設值 this.currentSettings = Utils.deepClone(this.defaultSettings); - // 保存重置後的設定 - this.saveSettings(); + // 立即保存重置後的設定(重要操作) + this.saveToLocalStorage(); + this.saveToServerImmediate(); + + // 觸發回調 + if (this.onSettingsChange) { + this.onSettingsChange(this.currentSettings); + } return this.currentSettings; }; diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js index 09f73bc..ebd5bd4 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js @@ -35,10 +35,32 @@ // 回調函數 this.onTabChange = options.onTabChange || null; this.onLayoutModeChange = options.onLayoutModeChange || null; - + + // 初始化防抖函數 + this.initDebounceHandlers(); + this.initUIElements(); } + /** + * 初始化防抖處理器 + */ + UIManager.prototype.initDebounceHandlers = function() { + // 為狀態指示器更新添加防抖 + this._debouncedUpdateStatusIndicator = Utils.DOM.debounce( + this._originalUpdateStatusIndicator.bind(this), + 100, + false + ); + + // 為狀態指示器元素更新添加防抖 + this._debouncedUpdateStatusIndicatorElement = Utils.DOM.debounce( + this._originalUpdateStatusIndicatorElement.bind(this), + 50, + false + ); + }; + /** * 初始化 UI 元素 */ @@ -266,23 +288,39 @@ }; /** - * 更新狀態指示器 + * 更新狀態指示器(原始版本,供防抖使用) */ - UIManager.prototype.updateStatusIndicator = function() { + UIManager.prototype._originalUpdateStatusIndicator = function() { const feedbackStatusIndicator = Utils.safeQuerySelector('#feedbackStatusIndicator'); const combinedStatusIndicator = Utils.safeQuerySelector('#combinedFeedbackStatusIndicator'); const statusInfo = this.getStatusInfo(); - + if (feedbackStatusIndicator) { - this.updateStatusIndicatorElement(feedbackStatusIndicator, statusInfo); - } - - if (combinedStatusIndicator) { - this.updateStatusIndicatorElement(combinedStatusIndicator, statusInfo); + this._originalUpdateStatusIndicatorElement(feedbackStatusIndicator, statusInfo); } - console.log('✅ 狀態指示器已更新: ' + statusInfo.status + ' - ' + statusInfo.title); + if (combinedStatusIndicator) { + this._originalUpdateStatusIndicatorElement(combinedStatusIndicator, statusInfo); + } + + // 減少重複日誌:只在狀態真正改變時記錄 + if (!this._lastStatusInfo || this._lastStatusInfo.status !== statusInfo.status) { + console.log('✅ 狀態指示器已更新: ' + statusInfo.status + ' - ' + statusInfo.title); + this._lastStatusInfo = statusInfo; + } + }; + + /** + * 更新狀態指示器(防抖版本) + */ + UIManager.prototype.updateStatusIndicator = function() { + if (this._debouncedUpdateStatusIndicator) { + this._debouncedUpdateStatusIndicator(); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalUpdateStatusIndicator(); + } }; /** @@ -329,9 +367,9 @@ }; /** - * 更新單個狀態指示器元素 + * 更新單個狀態指示器元素(原始版本,供防抖使用) */ - UIManager.prototype.updateStatusIndicatorElement = function(element, statusInfo) { + UIManager.prototype._originalUpdateStatusIndicatorElement = function(element, statusInfo) { if (!element) return; // 更新狀態類別 @@ -350,7 +388,22 @@ messageElement.textContent = statusInfo.message; } - console.log('🔧 已更新狀態指示器: ' + element.id + ' -> ' + statusInfo.status); + // 減少重複日誌:只記錄元素 ID 變化 + if (element.id) { + console.log('🔧 已更新狀態指示器: ' + element.id + ' -> ' + statusInfo.status); + } + }; + + /** + * 更新單個狀態指示器元素(防抖版本) + */ + UIManager.prototype.updateStatusIndicatorElement = function(element, statusInfo) { + if (this._debouncedUpdateStatusIndicatorElement) { + this._debouncedUpdateStatusIndicatorElement(element, statusInfo); + } else { + // 回退到原始方法(防抖未初始化時) + this._originalUpdateStatusIndicatorElement(element, statusInfo); + } }; /** diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/utils.js b/src/mcp_feedback_enhanced/web/static/js/modules/utils.js index 1ff4376..7b3cc75 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/utils.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/utils.js @@ -353,12 +353,12 @@ FEEDBACK_SUBMITTED: 'feedback_submitted', FEEDBACK_PROCESSING: 'processing', - // 預設設定 - DEFAULT_HEARTBEAT_FREQUENCY: 30000, - DEFAULT_TAB_HEARTBEAT_FREQUENCY: 5000, + // 預設設定(優化後的值) + DEFAULT_HEARTBEAT_FREQUENCY: 60000, // 從 30 秒調整為 60 秒,減少網路負載 + DEFAULT_TAB_HEARTBEAT_FREQUENCY: 10000, // 從 5 秒調整為 10 秒,減少標籤頁檢查頻率 DEFAULT_RECONNECT_DELAY: 1000, MAX_RECONNECT_ATTEMPTS: 5, - TAB_EXPIRED_THRESHOLD: 30000, + TAB_EXPIRED_THRESHOLD: 60000, // 從 30 秒調整為 60 秒,與心跳頻率保持一致 // 訊息類型 MESSAGE_SUCCESS: 'success', diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/utils/dom-utils.js b/src/mcp_feedback_enhanced/web/static/js/modules/utils/dom-utils.js index 5f4c1d7..c745104 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/utils/dom-utils.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/utils/dom-utils.js @@ -291,11 +291,101 @@ return true; } return false; + }, + + /** + * 防抖函數 - 延遲執行,在指定時間內重複調用會重置計時器 + * @param {Function} func - 要防抖的函數 + * @param {number} delay - 延遲時間(毫秒) + * @param {boolean} immediate - 是否立即執行第一次調用 + * @returns {Function} 防抖後的函數 + */ + debounce: function(func, delay, immediate) { + let timeoutId; + return function() { + const context = this; + const args = arguments; + + const later = function() { + timeoutId = null; + if (!immediate) { + func.apply(context, args); + } + }; + + const callNow = immediate && !timeoutId; + clearTimeout(timeoutId); + timeoutId = setTimeout(later, delay); + + if (callNow) { + func.apply(context, args); + } + }; + }, + + /** + * 節流函數 - 限制函數執行頻率,在指定時間內最多執行一次 + * @param {Function} func - 要節流的函數 + * @param {number} limit - 時間間隔(毫秒) + * @returns {Function} 節流後的函數 + */ + throttle: function(func, limit) { + let inThrottle; + return function() { + const context = this; + const args = arguments; + + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(function() { + inThrottle = false; + }, limit); + } + }; + }, + + /** + * 創建帶有防抖的函數包裝器 + * @param {Object} target - 目標對象 + * @param {string} methodName - 方法名稱 + * @param {number} delay - 防抖延遲時間 + * @param {boolean} immediate - 是否立即執行 + * @returns {Function} 原始函數的引用 + */ + wrapWithDebounce: function(target, methodName, delay, immediate) { + if (!target || typeof target[methodName] !== 'function') { + console.warn('無法為不存在的方法添加防抖:', methodName); + return null; + } + + const originalMethod = target[methodName]; + target[methodName] = this.debounce(originalMethod.bind(target), delay, immediate); + return originalMethod; + }, + + /** + * 創建帶有節流的函數包裝器 + * @param {Object} target - 目標對象 + * @param {string} methodName - 方法名稱 + * @param {number} limit - 節流時間間隔 + * @returns {Function} 原始函數的引用 + */ + wrapWithThrottle: function(target, methodName, limit) { + if (!target || typeof target[methodName] !== 'function') { + console.warn('無法為不存在的方法添加節流:', methodName); + return null; + } + + const originalMethod = target[methodName]; + target[methodName] = this.throttle(originalMethod.bind(target), limit); + return originalMethod; } }; // 將 DOMUtils 加入命名空間 - window.MCPFeedback.Utils.DOM = DOMUtils; + window.MCPFeedback.DOMUtils = DOMUtils; + window.MCPFeedback.Utils.DOM = DOMUtils; // 保持向後相容 console.log('✅ DOMUtils 模組載入完成'); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/utils/status-utils.js b/src/mcp_feedback_enhanced/web/static/js/modules/utils/status-utils.js index e107d66..c547bd1 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/utils/status-utils.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/utils/status-utils.js @@ -395,7 +395,8 @@ }; // 將 StatusUtils 加入命名空間 - window.MCPFeedback.Utils.Status = StatusUtils; + window.MCPFeedback.StatusUtils = StatusUtils; + window.MCPFeedback.Utils.Status = StatusUtils; // 保持向後相容 console.log('✅ StatusUtils 模組載入完成'); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/utils/time-utils.js b/src/mcp_feedback_enhanced/web/static/js/modules/utils/time-utils.js index 195ada9..be174bf 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/utils/time-utils.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/utils/time-utils.js @@ -432,7 +432,8 @@ }; // 將 TimeUtils 加入命名空間 - window.MCPFeedback.Utils.Time = TimeUtils; + window.MCPFeedback.TimeUtils = TimeUtils; + window.MCPFeedback.Utils.Time = TimeUtils; // 保持向後相容 console.log('✅ TimeUtils 模組載入完成'); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/websocket-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/websocket-manager.js index e354af0..be2cdfc 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/websocket-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/websocket-manager.js @@ -43,6 +43,10 @@ // 待處理的提交 this.pendingSubmission = null; this.sessionUpdatePending = false; + + // 網路狀態檢測 + this.networkOnline = navigator.onLine; + this.setupNetworkStatusDetection(); } /** @@ -217,11 +221,17 @@ self.connect(); }, 200); } - // 只有在非正常關閉時才重連 - else if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) { + // 檢查是否應該重連 + else if (this.shouldAttemptReconnect(event)) { this.reconnectAttempts++; - this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 15000); - console.log(this.reconnectDelay / 1000 + '秒後嘗試重連... (第' + this.reconnectAttempts + '次)'); + + // 改進的指數退避算法:基礎延遲 * 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 + '次)'); // 更新狀態為重連中 const reconnectingTemplate = window.i18nManager ? window.i18nManager.t('connectionMonitor.reconnecting') : '重連中... (第{attempt}次)'; @@ -377,6 +387,64 @@ return this.isConnected && this.connectionReady; }; + /** + * 設置網路狀態檢測 + */ + 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; + }; + /** * 關閉連接 */ diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html index 28ed78f..7f53574 100644 --- a/src/mcp_feedback_enhanced/web/templates/feedback.html +++ b/src/mcp_feedback_enhanced/web/templates/feedback.html @@ -1098,6 +1098,9 @@ + + + @@ -1137,27 +1140,52 @@ async function initializeApp() { const sessionId = '{{ session_id }}'; - // 檢查所有必要的模組是否已載入 - if (!window.MCPFeedback || - !window.MCPFeedback.Utils || - !window.MCPFeedback.ConnectionMonitor || - !window.MCPFeedback.SessionManager || - !window.MCPFeedback.FeedbackApp) { - console.error('❌ 模組載入不完整,延遲初始化...'); + // 檢查核心依賴 + const requiredModules = [ + 'MCPFeedback', + 'MCPFeedback.Logger', + 'MCPFeedback.Utils', + 'MCPFeedback.DOMUtils', + 'MCPFeedback.TimeUtils', + 'MCPFeedback.StatusUtils', + 'MCPFeedback.ConnectionMonitor', + 'MCPFeedback.SessionManager', + 'MCPFeedback.FeedbackApp' + ]; + + const missingModules = requiredModules.filter(modulePath => { + const parts = modulePath.split('.'); + let current = window; + for (const part of parts) { + if (!current[part]) return true; + current = current[part]; + } + return false; + }); + + if (missingModules.length > 0) { + const logger = window.MCPFeedback?.logger || console; + logger.warn('模組載入不完整,缺少:', missingModules.join(', ')); setTimeout(initializeApp, 100); return; } try { + const logger = window.MCPFeedback.logger; + logger.info('開始初始化應用程式...'); + // 確保 I18nManager 已經初始化 if (window.i18nManager) { + logger.debug('初始化國際化管理器...'); await window.i18nManager.init(); } // 初始化 FeedbackApp(使用新的命名空間) + logger.debug('創建 FeedbackApp 實例...'); window.feedbackApp = new window.MCPFeedback.FeedbackApp(sessionId); // 初始化應用程式 + logger.debug('初始化 FeedbackApp...'); await window.feedbackApp.init(); // 設置全域引用,讓 SessionManager 可以被 HTML 中的 onclick 調用 @@ -1165,9 +1193,10 @@ window.MCPFeedback.app = window.feedbackApp; } - console.log('✅ 應用程式初始化完成'); + logger.info('應用程式初始化完成'); } catch (error) { - console.error('❌ 應用程式初始化失敗:', error); + const logger = window.MCPFeedback?.logger || console; + logger.error('應用程式初始化失敗:', error); } }