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);
}
}