mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 02:22:26 +08:00
🔨 重構了保存機制,移除 localStorage 相關
This commit is contained in:
parent
81c6177b02
commit
be573a9a95
@ -330,7 +330,7 @@ def setup_routes(manager: "WebUIManager"):
|
|||||||
sessions = history_data if isinstance(history_data, list) else []
|
sessions = history_data if isinstance(history_data, list) else []
|
||||||
last_cleanup = 0
|
last_cleanup = 0
|
||||||
|
|
||||||
# 回傳與 localStorage 格式相容的資料
|
# 回傳會話歷史資料
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"sessions": sessions, "lastCleanup": last_cleanup}
|
content={"sessions": sessions, "lastCleanup": last_cleanup}
|
||||||
)
|
)
|
||||||
@ -364,11 +364,6 @@ def setup_routes(manager: "WebUIManager"):
|
|||||||
"savedAt": int(time.time() * 1000), # 當前時間戳
|
"savedAt": int(time.time() * 1000), # 當前時間戳
|
||||||
}
|
}
|
||||||
|
|
||||||
# 如果是首次儲存且有 localStorage 遷移標記
|
|
||||||
if not history_file.exists() and data.get("migratedFrom") == "localStorage":
|
|
||||||
history_data["migratedFrom"] = "localStorage"
|
|
||||||
history_data["migratedAt"] = int(time.time() * 1000)
|
|
||||||
|
|
||||||
# 保存會話歷史到檔案
|
# 保存會話歷史到檔案
|
||||||
with open(history_file, "w", encoding="utf-8") as f:
|
with open(history_file, "w", encoding="utf-8") as f:
|
||||||
json.dump(history_data, f, ensure_ascii=False, indent=2)
|
json.dump(history_data, f, ensure_ascii=False, indent=2)
|
||||||
@ -391,79 +386,82 @@ def setup_routes(manager: "WebUIManager"):
|
|||||||
content={"status": "error", "message": f"保存失敗: {e!s}"},
|
content={"status": "error", "message": f"保存失敗: {e!s}"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@manager.app.get("/api/active-tabs")
|
@manager.app.get("/api/log-level")
|
||||||
async def get_active_tabs():
|
async def get_log_level():
|
||||||
"""獲取活躍標籤頁信息 - 優先使用全局狀態"""
|
"""獲取日誌等級設定"""
|
||||||
current_time = time.time()
|
try:
|
||||||
expired_threshold = 60
|
# 使用統一的設定檔案路徑
|
||||||
|
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
|
||||||
|
settings_file = config_dir / "ui_settings.json"
|
||||||
|
|
||||||
# 清理過期的全局標籤頁
|
if settings_file.exists():
|
||||||
valid_global_tabs = {}
|
with open(settings_file, encoding="utf-8") as f:
|
||||||
for tab_id, tab_info in manager.global_active_tabs.items():
|
settings_data = json.load(f)
|
||||||
if current_time - tab_info.get("last_seen", 0) <= expired_threshold:
|
log_level = settings_data.get("logLevel", "INFO")
|
||||||
valid_global_tabs[tab_id] = tab_info
|
debug_log(f"從設定檔案載入日誌等級: {log_level}")
|
||||||
|
return JSONResponse(content={"logLevel": log_level})
|
||||||
|
else:
|
||||||
|
# 預設日誌等級
|
||||||
|
default_log_level = "INFO"
|
||||||
|
debug_log(f"使用預設日誌等級: {default_log_level}")
|
||||||
|
return JSONResponse(content={"logLevel": default_log_level})
|
||||||
|
|
||||||
manager.global_active_tabs = valid_global_tabs
|
except Exception as e:
|
||||||
|
debug_log(f"獲取日誌等級失敗: {e}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"error": f"獲取日誌等級失敗: {e!s}"},
|
||||||
|
)
|
||||||
|
|
||||||
# 如果有當前會話,也更新會話的標籤頁狀態
|
@manager.app.post("/api/log-level")
|
||||||
current_session = manager.get_current_session()
|
async def set_log_level(request: Request):
|
||||||
if current_session:
|
"""設定日誌等級"""
|
||||||
# 合併會話標籤頁到全局(如果有的話)
|
|
||||||
session_tabs = getattr(current_session, "active_tabs", {})
|
|
||||||
for tab_id, tab_info in session_tabs.items():
|
|
||||||
if current_time - tab_info.get("last_seen", 0) <= expired_threshold:
|
|
||||||
valid_global_tabs[tab_id] = tab_info
|
|
||||||
|
|
||||||
# 更新會話的活躍標籤頁
|
|
||||||
current_session.active_tabs = valid_global_tabs.copy()
|
|
||||||
manager.global_active_tabs = valid_global_tabs
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
content={
|
|
||||||
"has_session": current_session is not None,
|
|
||||||
"active_tabs": valid_global_tabs,
|
|
||||||
"count": len(valid_global_tabs),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@manager.app.post("/api/register-tab")
|
|
||||||
async def register_tab(request: Request):
|
|
||||||
"""註冊新標籤頁"""
|
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
tab_id = data.get("tabId")
|
log_level = data.get("logLevel")
|
||||||
|
|
||||||
if not tab_id:
|
if not log_level or log_level not in ["DEBUG", "INFO", "WARN", "ERROR"]:
|
||||||
return JSONResponse(status_code=400, content={"error": "缺少 tabId"})
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={
|
||||||
|
"error": "無效的日誌等級,必須是 DEBUG, INFO, WARN, ERROR 之一"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
current_session = manager.get_current_session()
|
# 使用統一的設定檔案路徑
|
||||||
if not current_session:
|
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
|
||||||
return JSONResponse(status_code=404, content={"error": "沒有活躍會話"})
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
settings_file = config_dir / "ui_settings.json"
|
||||||
|
|
||||||
# 註冊標籤頁
|
# 載入現有設定或創建新設定
|
||||||
tab_info = {
|
settings_data = {}
|
||||||
"timestamp": time.time() * 1000, # 毫秒時間戳
|
if settings_file.exists():
|
||||||
"last_seen": time.time(),
|
with open(settings_file, encoding="utf-8") as f:
|
||||||
"registered_at": time.time(),
|
settings_data = json.load(f)
|
||||||
}
|
|
||||||
|
|
||||||
if not hasattr(current_session, "active_tabs"):
|
# 更新日誌等級
|
||||||
current_session.active_tabs = {}
|
settings_data["logLevel"] = log_level
|
||||||
|
|
||||||
current_session.active_tabs[tab_id] = tab_info
|
# 保存設定到檔案
|
||||||
|
with open(settings_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(settings_data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
# 同時更新全局標籤頁狀態
|
debug_log(f"日誌等級已設定為: {log_level}")
|
||||||
manager.global_active_tabs[tab_id] = tab_info
|
|
||||||
|
|
||||||
debug_log(f"標籤頁已註冊: {tab_id}")
|
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"status": "success", "tabId": tab_id, "registered": True}
|
content={
|
||||||
|
"status": "success",
|
||||||
|
"logLevel": log_level,
|
||||||
|
"message": "日誌等級已更新",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_log(f"註冊標籤頁失敗: {e}")
|
debug_log(f"設定日誌等級失敗: {e}")
|
||||||
return JSONResponse(status_code=500, content={"error": f"註冊失敗: {e!s}"})
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={"status": "error", "message": f"設定失敗: {e!s}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handle_websocket_message(manager: "WebUIManager", session, data: dict):
|
async def handle_websocket_message(manager: "WebUIManager", session, data: dict):
|
||||||
@ -494,29 +492,14 @@ async def handle_websocket_message(manager: "WebUIManager", session, data: dict)
|
|||||||
debug_log(f"發送狀態更新失敗: {e}")
|
debug_log(f"發送狀態更新失敗: {e}")
|
||||||
|
|
||||||
elif message_type == "heartbeat":
|
elif message_type == "heartbeat":
|
||||||
# WebSocket 心跳處理
|
# WebSocket 心跳處理(簡化版)
|
||||||
tab_id = data.get("tabId", "unknown")
|
|
||||||
timestamp = data.get("timestamp", 0)
|
|
||||||
|
|
||||||
tab_info = {"timestamp": timestamp, "last_seen": time.time()}
|
|
||||||
|
|
||||||
# 更新會話的標籤頁信息
|
|
||||||
if hasattr(session, "active_tabs"):
|
|
||||||
session.active_tabs[tab_id] = tab_info
|
|
||||||
else:
|
|
||||||
session.active_tabs = {tab_id: tab_info}
|
|
||||||
|
|
||||||
# 同時更新全局標籤頁狀態
|
|
||||||
manager.global_active_tabs[tab_id] = tab_info
|
|
||||||
|
|
||||||
# 發送心跳回應
|
# 發送心跳回應
|
||||||
if session.websocket:
|
if session.websocket:
|
||||||
try:
|
try:
|
||||||
await session.websocket.send_json(
|
await session.websocket.send_json(
|
||||||
{
|
{
|
||||||
"type": "heartbeat_response",
|
"type": "heartbeat_response",
|
||||||
"tabId": tab_id,
|
"timestamp": data.get("timestamp", 0),
|
||||||
"timestamp": timestamp,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -168,7 +168,7 @@
|
|||||||
|
|
||||||
// 3. 初始化 UI 管理器
|
// 3. 初始化 UI 管理器
|
||||||
self.uiManager = new window.MCPFeedback.UIManager({
|
self.uiManager = new window.MCPFeedback.UIManager({
|
||||||
currentTab: settings.activeTab,
|
// 移除 activeTab - 頁籤切換無需持久化
|
||||||
layoutMode: settings.layoutMode,
|
layoutMode: settings.layoutMode,
|
||||||
onTabChange: function(tabName) {
|
onTabChange: function(tabName) {
|
||||||
self.handleTabChange(tabName);
|
self.handleTabChange(tabName);
|
||||||
@ -178,8 +178,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. 初始化標籤頁管理器
|
|
||||||
self.tabManager = new window.MCPFeedback.TabManager();
|
|
||||||
|
|
||||||
// 5. 初始化連線監控器
|
// 5. 初始化連線監控器
|
||||||
self.connectionMonitor = new window.MCPFeedback.ConnectionMonitor({
|
self.connectionMonitor = new window.MCPFeedback.ConnectionMonitor({
|
||||||
@ -401,8 +400,8 @@
|
|||||||
this.imageHandler.reinitialize(layoutMode);
|
this.imageHandler.reinitialize(layoutMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存當前頁籤設定
|
// 移除頁籤狀態保存 - 頁籤切換無需持久化
|
||||||
this.settingsManager.set('activeTab', tabName);
|
// this.settingsManager.set('activeTab', tabName);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,14 +14,7 @@ class I18nManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// 從 localStorage 載入語言偏好
|
console.log(`i18nManager 使用預設語言: ${this.currentLanguage}`);
|
||||||
const savedLanguage = localStorage.getItem('language');
|
|
||||||
if (savedLanguage) {
|
|
||||||
this.currentLanguage = savedLanguage;
|
|
||||||
console.log(`i18nManager 從 localStorage 載入語言: ${savedLanguage}`);
|
|
||||||
} else {
|
|
||||||
console.log(`i18nManager 使用默認語言: ${this.currentLanguage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 載入翻譯數據
|
// 載入翻譯數據
|
||||||
await this.loadTranslations();
|
await this.loadTranslations();
|
||||||
@ -125,7 +118,6 @@ class I18nManager {
|
|||||||
console.log(`🔄 i18nManager.setLanguage() 被調用: ${this.currentLanguage} -> ${language}`);
|
console.log(`🔄 i18nManager.setLanguage() 被調用: ${this.currentLanguage} -> ${language}`);
|
||||||
if (this.translations[language]) {
|
if (this.translations[language]) {
|
||||||
this.currentLanguage = language;
|
this.currentLanguage = language;
|
||||||
localStorage.setItem('language', language);
|
|
||||||
this.applyTranslations();
|
this.applyTranslations();
|
||||||
|
|
||||||
// 更新所有語言選擇器(包括現代化版本)
|
// 更新所有語言選擇器(包括現代化版本)
|
||||||
@ -302,10 +294,14 @@ class I18nManager {
|
|||||||
selector.removeEventListener('change', selector._i18nChangeHandler);
|
selector.removeEventListener('change', selector._i18nChangeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加新的事件監聽器
|
// 添加新的事件監聽器 - 通過 SettingsManager 統一管理
|
||||||
selector._i18nChangeHandler = (e) => {
|
selector._i18nChangeHandler = (e) => {
|
||||||
console.log(`🔄 i18n select change event: ${e.target.value}`);
|
console.log(`🔄 i18n select change event: ${e.target.value}`);
|
||||||
this.setLanguage(e.target.value);
|
if (window.feedbackApp && window.feedbackApp.settingsManager) {
|
||||||
|
window.feedbackApp.settingsManager.set('language', e.target.value);
|
||||||
|
} else {
|
||||||
|
this.setLanguage(e.target.value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
selector.addEventListener('change', selector._i18nChangeHandler);
|
selector.addEventListener('change', selector._i18nChangeHandler);
|
||||||
}
|
}
|
||||||
@ -325,9 +321,13 @@ class I18nManager {
|
|||||||
// 移除舊的事件監聽器(如果存在)
|
// 移除舊的事件監聽器(如果存在)
|
||||||
option.removeEventListener('click', option._languageClickHandler);
|
option.removeEventListener('click', option._languageClickHandler);
|
||||||
|
|
||||||
// 添加新的點擊事件監聽器
|
// 添加新的點擊事件監聽器 - 通過 SettingsManager 統一管理
|
||||||
option._languageClickHandler = () => {
|
option._languageClickHandler = () => {
|
||||||
this.setLanguage(lang);
|
if (window.feedbackApp && window.feedbackApp.settingsManager) {
|
||||||
|
window.feedbackApp.settingsManager.set('language', lang);
|
||||||
|
} else {
|
||||||
|
this.setLanguage(lang);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
option.addEventListener('click', option._languageClickHandler);
|
option.addEventListener('click', option._languageClickHandler);
|
||||||
});
|
});
|
||||||
|
@ -311,29 +311,87 @@
|
|||||||
if (urlLogLevel) {
|
if (urlLogLevel) {
|
||||||
return 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') {
|
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||||
return LogLevel.DEBUG;
|
return LogLevel.DEBUG;
|
||||||
}
|
}
|
||||||
|
|
||||||
return LogLevel.INFO;
|
return LogLevel.INFO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 從 API 載入日誌等級
|
||||||
|
function loadLogLevelFromAPI() {
|
||||||
|
fetch('/api/log-level')
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw new Error('載入日誌等級失敗: ' + response.status);
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
const apiLogLevel = data.logLevel;
|
||||||
|
if (apiLogLevel && Object.values(LogLevel).includes(apiLogLevel)) {
|
||||||
|
currentLogLevel = apiLogLevel;
|
||||||
|
console.log('📋 從 API 載入日誌等級:', apiLogLevel);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.warn('⚠️ 載入日誌等級失敗,使用預設值:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存日誌等級到 API
|
||||||
|
function saveLogLevelToAPI(logLevel) {
|
||||||
|
fetch('/api/log-level', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
logLevel: logLevel
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw new Error('保存日誌等級失敗: ' + response.status);
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
console.log('📋 日誌等級已保存:', data.logLevel);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.warn('⚠️ 保存日誌等級失敗:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 設置全域日誌等級
|
// 設置全域日誌等級
|
||||||
globalLogger.setLevel(detectLogLevel());
|
globalLogger.setLevel(detectLogLevel());
|
||||||
|
|
||||||
|
// 頁面載入後從 API 載入日誌等級
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', loadLogLevelFromAPI);
|
||||||
|
} else {
|
||||||
|
loadLogLevelFromAPI();
|
||||||
|
}
|
||||||
|
|
||||||
// 匯出到全域命名空間
|
// 匯出到全域命名空間
|
||||||
window.MCPFeedback.Logger = Logger;
|
window.MCPFeedback.Logger = Logger;
|
||||||
window.MCPFeedback.LogLevel = LogLevel;
|
window.MCPFeedback.LogLevel = LogLevel;
|
||||||
window.MCPFeedback.logger = globalLogger;
|
window.MCPFeedback.logger = globalLogger;
|
||||||
|
|
||||||
|
// 匯出設定方法
|
||||||
|
window.MCPFeedback.setLogLevel = function(logLevel) {
|
||||||
|
if (Object.values(LogLevel).includes(logLevel)) {
|
||||||
|
globalLogger.setLevel(logLevel);
|
||||||
|
saveLogLevelToAPI(logLevel);
|
||||||
|
console.log('📋 日誌等級已更新:', LogLevelNames[logLevel]);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ 無效的日誌等級:', logLevel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
console.log('✅ Logger 模組載入完成,當前等級:', LogLevelNames[globalLogger.getLevel()]);
|
console.log('✅ Logger 模組載入完成,當前等級:', LogLevelNames[globalLogger.getLevel()]);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
language: 'zh-TW',
|
language: 'zh-TW',
|
||||||
imageSizeLimit: 0,
|
imageSizeLimit: 0,
|
||||||
enableBase64Detail: false,
|
enableBase64Detail: false,
|
||||||
activeTab: 'combined',
|
// 移除 activeTab - 頁籤切換無需持久化
|
||||||
sessionPanelCollapsed: false,
|
sessionPanelCollapsed: false,
|
||||||
// 自動定時提交設定
|
// 自動定時提交設定
|
||||||
autoSubmitEnabled: false,
|
autoSubmitEnabled: false,
|
||||||
@ -58,12 +58,7 @@
|
|||||||
this.onLanguageChange = options.onLanguageChange || null;
|
this.onLanguageChange = options.onLanguageChange || null;
|
||||||
this.onAutoSubmitStateChange = options.onAutoSubmitStateChange || null;
|
this.onAutoSubmitStateChange = options.onAutoSubmitStateChange || null;
|
||||||
|
|
||||||
// 防抖機制相關
|
console.log('✅ SettingsManager 建構函數初始化完成 - 即時保存模式');
|
||||||
this.saveToServerDebounceTimer = null;
|
|
||||||
this.saveToServerDebounceDelay = options.saveDebounceDelay || 500; // 預設 500ms 防抖延遲
|
|
||||||
this.pendingServerSave = false;
|
|
||||||
|
|
||||||
console.log('✅ SettingsManager 建構函數初始化完成,防抖延遲:', this.saveToServerDebounceDelay + 'ms');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,28 +69,16 @@
|
|||||||
|
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
logger.info('開始載入設定...');
|
logger.info('開始載入設定...');
|
||||||
|
|
||||||
// 優先從伺服器端載入設定
|
// 只從伺服器端載入設定
|
||||||
self.loadFromServer()
|
self.loadFromServer()
|
||||||
.then(function(serverSettings) {
|
.then(function(serverSettings) {
|
||||||
if (serverSettings && Object.keys(serverSettings).length > 0) {
|
if (serverSettings && Object.keys(serverSettings).length > 0) {
|
||||||
self.currentSettings = self.mergeSettings(self.defaultSettings, serverSettings);
|
self.currentSettings = self.mergeSettings(self.defaultSettings, serverSettings);
|
||||||
logger.info('從伺服器端載入設定成功:', self.currentSettings);
|
logger.info('從伺服器端載入設定成功:', self.currentSettings);
|
||||||
|
|
||||||
// 同步到 localStorage
|
|
||||||
self.saveToLocalStorage();
|
|
||||||
resolve(self.currentSettings);
|
|
||||||
} else {
|
|
||||||
// 回退到 localStorage
|
|
||||||
return self.loadFromLocalStorage();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function(localSettings) {
|
|
||||||
if (localSettings) {
|
|
||||||
self.currentSettings = self.mergeSettings(self.defaultSettings, localSettings);
|
|
||||||
console.log('從 localStorage 載入設定:', self.currentSettings);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('沒有找到設定,使用預設值');
|
console.log('沒有找到設定,使用預設值');
|
||||||
|
self.currentSettings = Utils.deepClone(self.defaultSettings);
|
||||||
}
|
}
|
||||||
resolve(self.currentSettings);
|
resolve(self.currentSettings);
|
||||||
})
|
})
|
||||||
@ -125,27 +108,7 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 從 localStorage 載入設定
|
|
||||||
*/
|
|
||||||
SettingsManager.prototype.loadFromLocalStorage = function() {
|
|
||||||
if (!Utils.isLocalStorageSupported()) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const localSettings = localStorage.getItem('mcp-feedback-settings');
|
|
||||||
if (localSettings) {
|
|
||||||
const parsed = Utils.safeJsonParse(localSettings, null);
|
|
||||||
console.log('從 localStorage 載入設定:', parsed);
|
|
||||||
return Promise.resolve(parsed);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('從 localStorage 載入設定失敗:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存設定
|
* 保存設定
|
||||||
@ -157,10 +120,7 @@
|
|||||||
|
|
||||||
logger.debug('保存設定:', this.currentSettings);
|
logger.debug('保存設定:', this.currentSettings);
|
||||||
|
|
||||||
// 保存到 localStorage
|
// 只保存到伺服器端
|
||||||
this.saveToLocalStorage();
|
|
||||||
|
|
||||||
// 同步保存到伺服器端
|
|
||||||
this.saveToServer();
|
this.saveToServer();
|
||||||
|
|
||||||
// 觸發回調
|
// 觸發回調
|
||||||
@ -171,39 +131,13 @@
|
|||||||
return this.currentSettings;
|
return this.currentSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存到 localStorage
|
|
||||||
*/
|
|
||||||
SettingsManager.prototype.saveToLocalStorage = function() {
|
|
||||||
if (!Utils.isLocalStorageSupported()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
localStorage.setItem('mcp-feedback-settings', JSON.stringify(this.currentSettings));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存設定到 localStorage 失敗:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存到伺服器(帶防抖機制)
|
* 保存到伺服器(即時保存)
|
||||||
*/
|
*/
|
||||||
SettingsManager.prototype.saveToServer = function() {
|
SettingsManager.prototype.saveToServer = function() {
|
||||||
const self = this;
|
this._performServerSave();
|
||||||
|
|
||||||
// 清除之前的定時器
|
|
||||||
if (self.saveToServerDebounceTimer) {
|
|
||||||
clearTimeout(self.saveToServerDebounceTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 標記有待處理的保存操作
|
|
||||||
self.pendingServerSave = true;
|
|
||||||
|
|
||||||
// 設置新的防抖定時器
|
|
||||||
self.saveToServerDebounceTimer = setTimeout(function() {
|
|
||||||
self._performServerSave();
|
|
||||||
}, self.saveToServerDebounceDelay);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -212,12 +146,6 @@
|
|||||||
SettingsManager.prototype._performServerSave = function() {
|
SettingsManager.prototype._performServerSave = function() {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
if (!self.pendingServerSave) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pendingServerSave = false;
|
|
||||||
|
|
||||||
fetch('/api/save-settings', {
|
fetch('/api/save-settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -227,7 +155,7 @@
|
|||||||
})
|
})
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log('設定已同步到伺服器端');
|
console.log('設定已即時同步到伺服器端');
|
||||||
} else {
|
} else {
|
||||||
console.warn('同步設定到伺服器端失敗:', response.status);
|
console.warn('同步設定到伺服器端失敗:', response.status);
|
||||||
}
|
}
|
||||||
@ -237,20 +165,7 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 立即保存到伺服器(跳過防抖機制)
|
|
||||||
* 用於重要操作,如語言變更、重置設定等
|
|
||||||
*/
|
|
||||||
SettingsManager.prototype.saveToServerImmediate = function() {
|
|
||||||
// 清除防抖定時器
|
|
||||||
if (this.saveToServerDebounceTimer) {
|
|
||||||
clearTimeout(this.saveToServerDebounceTimer);
|
|
||||||
this.saveToServerDebounceTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 立即執行保存
|
|
||||||
this._performServerSave();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 合併設定
|
* 合併設定
|
||||||
@ -283,23 +198,15 @@
|
|||||||
SettingsManager.prototype.set = function(key, value) {
|
SettingsManager.prototype.set = function(key, value) {
|
||||||
const oldValue = this.currentSettings[key];
|
const oldValue = this.currentSettings[key];
|
||||||
this.currentSettings[key] = value;
|
this.currentSettings[key] = value;
|
||||||
|
|
||||||
// 特殊處理語言變更
|
// 特殊處理語言變更
|
||||||
if (key === 'language' && oldValue !== value) {
|
if (key === 'language' && oldValue !== value) {
|
||||||
this.handleLanguageChange(value);
|
this.handleLanguageChange(value);
|
||||||
// 語言變更是重要操作,立即保存
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.saveToServerImmediate();
|
|
||||||
|
|
||||||
// 觸發回調
|
|
||||||
if (this.onSettingsChange) {
|
|
||||||
this.onSettingsChange(this.currentSettings);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 一般設定變更使用防抖保存
|
|
||||||
this.saveSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有設定變更都即時保存
|
||||||
|
this.saveSettings();
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -334,14 +241,13 @@
|
|||||||
SettingsManager.prototype.handleLanguageChange = function(newLanguage) {
|
SettingsManager.prototype.handleLanguageChange = function(newLanguage) {
|
||||||
console.log('語言設定變更: ' + newLanguage);
|
console.log('語言設定變更: ' + newLanguage);
|
||||||
|
|
||||||
// 同步到 localStorage
|
// 通知國際化系統(統一由 SettingsManager 管理)
|
||||||
if (Utils.isLocalStorageSupported()) {
|
|
||||||
localStorage.setItem('language', newLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知國際化系統
|
|
||||||
if (window.i18nManager) {
|
if (window.i18nManager) {
|
||||||
window.i18nManager.setLanguage(newLanguage);
|
// 直接設定語言,不觸發 i18nManager 的保存邏輯
|
||||||
|
window.i18nManager.currentLanguage = newLanguage;
|
||||||
|
window.i18nManager.applyTranslations();
|
||||||
|
window.i18nManager.setupLanguageSelectors();
|
||||||
|
document.documentElement.lang = newLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 延遲更新動態文字,確保 i18n 已經載入新語言
|
// 延遲更新動態文字,確保 i18n 已經載入新語言
|
||||||
@ -361,17 +267,11 @@
|
|||||||
SettingsManager.prototype.resetSettings = function() {
|
SettingsManager.prototype.resetSettings = function() {
|
||||||
console.log('重置所有設定');
|
console.log('重置所有設定');
|
||||||
|
|
||||||
// 清除 localStorage
|
|
||||||
if (Utils.isLocalStorageSupported()) {
|
|
||||||
localStorage.removeItem('mcp-feedback-settings');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置為預設值
|
// 重置為預設值
|
||||||
this.currentSettings = Utils.deepClone(this.defaultSettings);
|
this.currentSettings = Utils.deepClone(this.defaultSettings);
|
||||||
|
|
||||||
// 立即保存重置後的設定(重要操作)
|
// 立即保存重置後的設定到伺服器
|
||||||
this.saveToLocalStorage();
|
this.saveToServer();
|
||||||
this.saveToServerImmediate();
|
|
||||||
|
|
||||||
// 觸發回調
|
// 觸發回調
|
||||||
if (this.onSettingsChange) {
|
if (this.onSettingsChange) {
|
||||||
|
@ -1,235 +0,0 @@
|
|||||||
/**
|
|
||||||
* MCP Feedback Enhanced - 標籤頁管理模組
|
|
||||||
* ====================================
|
|
||||||
*
|
|
||||||
* 處理多標籤頁狀態同步和智能瀏覽器管理
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// 確保命名空間和依賴存在
|
|
||||||
window.MCPFeedback = window.MCPFeedback || {};
|
|
||||||
const Utils = window.MCPFeedback.Utils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 標籤頁管理器建構函數
|
|
||||||
*/
|
|
||||||
function TabManager() {
|
|
||||||
this.tabId = Utils.generateId('tab');
|
|
||||||
this.heartbeatInterval = null;
|
|
||||||
this.heartbeatFrequency = Utils.CONSTANTS.DEFAULT_TAB_HEARTBEAT_FREQUENCY;
|
|
||||||
this.storageKey = 'mcp_feedback_tabs';
|
|
||||||
this.lastActivityKey = 'mcp_feedback_last_activity';
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化標籤頁管理器
|
|
||||||
*/
|
|
||||||
TabManager.prototype.init = function() {
|
|
||||||
// 註冊當前標籤頁
|
|
||||||
this.registerTab();
|
|
||||||
|
|
||||||
// 向服務器註冊標籤頁
|
|
||||||
this.registerTabToServer();
|
|
||||||
|
|
||||||
// 開始心跳
|
|
||||||
this.startHeartbeat();
|
|
||||||
|
|
||||||
// 監聽頁面關閉事件
|
|
||||||
const self = this;
|
|
||||||
window.addEventListener('beforeunload', function() {
|
|
||||||
self.unregisterTab();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 監聽 localStorage 變化(其他標籤頁的狀態變化)
|
|
||||||
window.addEventListener('storage', function(e) {
|
|
||||||
if (e.key === self.storageKey) {
|
|
||||||
self.handleTabsChange();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('📋 TabManager 初始化完成,標籤頁 ID: ' + this.tabId);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 註冊當前標籤頁
|
|
||||||
*/
|
|
||||||
TabManager.prototype.registerTab = function() {
|
|
||||||
const tabs = this.getActiveTabs();
|
|
||||||
tabs[this.tabId] = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
url: window.location.href,
|
|
||||||
active: true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Utils.isLocalStorageSupported()) {
|
|
||||||
localStorage.setItem(this.storageKey, JSON.stringify(tabs));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateLastActivity();
|
|
||||||
console.log('✅ 標籤頁已註冊: ' + this.tabId);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 註銷當前標籤頁
|
|
||||||
*/
|
|
||||||
TabManager.prototype.unregisterTab = function() {
|
|
||||||
const tabs = this.getActiveTabs();
|
|
||||||
delete tabs[this.tabId];
|
|
||||||
|
|
||||||
if (Utils.isLocalStorageSupported()) {
|
|
||||||
localStorage.setItem(this.storageKey, JSON.stringify(tabs));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('❌ 標籤頁已註銷: ' + this.tabId);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 開始心跳
|
|
||||||
*/
|
|
||||||
TabManager.prototype.startHeartbeat = function() {
|
|
||||||
const self = this;
|
|
||||||
this.heartbeatInterval = setInterval(function() {
|
|
||||||
self.sendHeartbeat();
|
|
||||||
}, this.heartbeatFrequency);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 發送心跳
|
|
||||||
*/
|
|
||||||
TabManager.prototype.sendHeartbeat = function() {
|
|
||||||
const tabs = this.getActiveTabs();
|
|
||||||
if (tabs[this.tabId]) {
|
|
||||||
tabs[this.tabId].timestamp = Date.now();
|
|
||||||
|
|
||||||
if (Utils.isLocalStorageSupported()) {
|
|
||||||
localStorage.setItem(this.storageKey, JSON.stringify(tabs));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateLastActivity();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新最後活動時間
|
|
||||||
*/
|
|
||||||
TabManager.prototype.updateLastActivity = function() {
|
|
||||||
if (Utils.isLocalStorageSupported()) {
|
|
||||||
localStorage.setItem(this.lastActivityKey, Date.now().toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 獲取活躍標籤頁
|
|
||||||
*/
|
|
||||||
TabManager.prototype.getActiveTabs = function() {
|
|
||||||
if (!Utils.isLocalStorageSupported()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(this.storageKey);
|
|
||||||
const tabs = stored ? Utils.safeJsonParse(stored, {}) : {};
|
|
||||||
|
|
||||||
// 清理過期的標籤頁
|
|
||||||
const now = Date.now();
|
|
||||||
const expiredThreshold = Utils.CONSTANTS.TAB_EXPIRED_THRESHOLD;
|
|
||||||
|
|
||||||
for (const tabId in tabs) {
|
|
||||||
if (tabs.hasOwnProperty(tabId)) {
|
|
||||||
if (now - tabs[tabId].timestamp > expiredThreshold) {
|
|
||||||
delete tabs[tabId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tabs;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('獲取活躍標籤頁失敗:', error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 檢查是否有活躍標籤頁
|
|
||||||
*/
|
|
||||||
TabManager.prototype.hasActiveTabs = function() {
|
|
||||||
const tabs = this.getActiveTabs();
|
|
||||||
return Object.keys(tabs).length > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 檢查是否為唯一活躍標籤頁
|
|
||||||
*/
|
|
||||||
TabManager.prototype.isOnlyActiveTab = function() {
|
|
||||||
const tabs = this.getActiveTabs();
|
|
||||||
return Object.keys(tabs).length === 1 && tabs[this.tabId];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 處理其他標籤頁狀態變化
|
|
||||||
*/
|
|
||||||
TabManager.prototype.handleTabsChange = function() {
|
|
||||||
console.log('🔄 檢測到其他標籤頁狀態變化');
|
|
||||||
// 可以在這裡添加更多邏輯
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 向服務器註冊標籤頁
|
|
||||||
*/
|
|
||||||
TabManager.prototype.registerTabToServer = function() {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
fetch('/api/register-tab', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
tabId: this.tabId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(function(response) {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ 標籤頁服務器註冊失敗: ' + response.status);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function(data) {
|
|
||||||
if (data) {
|
|
||||||
console.log('✅ 標籤頁已向服務器註冊: ' + self.tabId);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(error) {
|
|
||||||
console.warn('⚠️ 標籤頁服務器註冊錯誤: ' + error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理資源
|
|
||||||
*/
|
|
||||||
TabManager.prototype.cleanup = function() {
|
|
||||||
if (this.heartbeatInterval) {
|
|
||||||
clearInterval(this.heartbeatInterval);
|
|
||||||
this.heartbeatInterval = null;
|
|
||||||
}
|
|
||||||
this.unregisterTab();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 獲取當前標籤頁 ID
|
|
||||||
*/
|
|
||||||
TabManager.prototype.getTabId = function() {
|
|
||||||
return this.tabId;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 將 TabManager 加入命名空間
|
|
||||||
window.MCPFeedback.TabManager = TabManager;
|
|
||||||
|
|
||||||
console.log('✅ TabManager 模組載入完成');
|
|
||||||
|
|
||||||
})();
|
|
@ -308,20 +308,7 @@
|
|||||||
return 'WebSocket' in window;
|
return 'WebSocket' in window;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 檢查 localStorage 是否可用
|
|
||||||
* @returns {boolean} localStorage 是否可用
|
|
||||||
*/
|
|
||||||
isLocalStorageSupported: function() {
|
|
||||||
try {
|
|
||||||
const test = '__localStorage_test__';
|
|
||||||
localStorage.setItem(test, test);
|
|
||||||
localStorage.removeItem(test);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTML 轉義函數
|
* HTML 轉義函數
|
||||||
|
@ -319,7 +319,7 @@
|
|||||||
<script src="/static/js/i18n.js?v=2025010505"></script>
|
<script src="/static/js/i18n.js?v=2025010505"></script>
|
||||||
<!-- 載入所有模組 -->
|
<!-- 載入所有模組 -->
|
||||||
<script src="/static/js/modules/utils.js?v=2025010505"></script>
|
<script src="/static/js/modules/utils.js?v=2025010505"></script>
|
||||||
<script src="/static/js/modules/tab-manager.js?v=2025010505"></script>
|
|
||||||
<script src="/static/js/modules/websocket-manager.js?v=2025010505"></script>
|
<script src="/static/js/modules/websocket-manager.js?v=2025010505"></script>
|
||||||
<script src="/static/js/modules/image-handler.js?v=2025010505"></script>
|
<script src="/static/js/modules/image-handler.js?v=2025010505"></script>
|
||||||
<script src="/static/js/modules/settings-manager.js?v=2025010505"></script>
|
<script src="/static/js/modules/settings-manager.js?v=2025010505"></script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user