🔨 重構了保存機制,移除 localStorage 相關

This commit is contained in:
Minidoracat 2025-06-22 02:49:20 +08:00
parent 81c6177b02
commit be573a9a95
8 changed files with 169 additions and 477 deletions

View File

@ -330,7 +330,7 @@ def setup_routes(manager: "WebUIManager"):
sessions = history_data if isinstance(history_data, list) else []
last_cleanup = 0
# 回傳與 localStorage 格式相容的資料
# 回傳會話歷史資料
return JSONResponse(
content={"sessions": sessions, "lastCleanup": last_cleanup}
)
@ -364,11 +364,6 @@ def setup_routes(manager: "WebUIManager"):
"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:
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}"},
)
@manager.app.get("/api/active-tabs")
async def get_active_tabs():
"""獲取活躍標籤頁信息 - 優先使用全局狀態"""
current_time = time.time()
expired_threshold = 60
@manager.app.get("/api/log-level")
async def get_log_level():
"""獲取日誌等級設定"""
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
settings_file = config_dir / "ui_settings.json"
# 清理過期的全局標籤頁
valid_global_tabs = {}
for tab_id, tab_info in manager.global_active_tabs.items():
if current_time - tab_info.get("last_seen", 0) <= expired_threshold:
valid_global_tabs[tab_id] = tab_info
if settings_file.exists():
with open(settings_file, encoding="utf-8") as f:
settings_data = json.load(f)
log_level = settings_data.get("logLevel", "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}"},
)
# 如果有當前會話,也更新會話的標籤頁狀態
current_session = manager.get_current_session()
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):
"""註冊新標籤頁"""
@manager.app.post("/api/log-level")
async def set_log_level(request: Request):
"""設定日誌等級"""
try:
data = await request.json()
tab_id = data.get("tabId")
log_level = data.get("logLevel")
if not tab_id:
return JSONResponse(status_code=400, content={"error": "缺少 tabId"})
if not log_level or log_level not in ["DEBUG", "INFO", "WARN", "ERROR"]:
return JSONResponse(
status_code=400,
content={
"error": "無效的日誌等級,必須是 DEBUG, INFO, WARN, ERROR 之一"
},
)
current_session = manager.get_current_session()
if not current_session:
return JSONResponse(status_code=404, content={"error": "沒有活躍會話"})
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
config_dir.mkdir(parents=True, exist_ok=True)
settings_file = config_dir / "ui_settings.json"
# 註冊標籤頁
tab_info = {
"timestamp": time.time() * 1000, # 毫秒時間戳
"last_seen": time.time(),
"registered_at": time.time(),
}
# 載入現有設定或創建新設定
settings_data = {}
if settings_file.exists():
with open(settings_file, encoding="utf-8") as f:
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)
# 同時更新全局標籤頁狀態
manager.global_active_tabs[tab_id] = tab_info
debug_log(f"標籤頁已註冊: {tab_id}")
debug_log(f"日誌等級已設定為: {log_level}")
return JSONResponse(
content={"status": "success", "tabId": tab_id, "registered": True}
content={
"status": "success",
"logLevel": log_level,
"message": "日誌等級已更新",
}
)
except Exception as e:
debug_log(f"註冊標籤頁失敗: {e}")
return JSONResponse(status_code=500, content={"error": f"註冊失敗: {e!s}"})
debug_log(f"設定日誌等級失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"設定失敗: {e!s}"},
)
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}")
elif message_type == "heartbeat":
# 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
# WebSocket 心跳處理(簡化版)
# 發送心跳回應
if session.websocket:
try:
await session.websocket.send_json(
{
"type": "heartbeat_response",
"tabId": tab_id,
"timestamp": timestamp,
"timestamp": data.get("timestamp", 0),
}
)
except Exception as e:

View File

@ -168,7 +168,7 @@
// 3. 初始化 UI 管理器
self.uiManager = new window.MCPFeedback.UIManager({
currentTab: settings.activeTab,
// 移除 activeTab - 頁籤切換無需持久化
layoutMode: settings.layoutMode,
onTabChange: function(tabName) {
self.handleTabChange(tabName);
@ -178,8 +178,7 @@
}
});
// 4. 初始化標籤頁管理器
self.tabManager = new window.MCPFeedback.TabManager();
// 5. 初始化連線監控器
self.connectionMonitor = new window.MCPFeedback.ConnectionMonitor({
@ -401,8 +400,8 @@
this.imageHandler.reinitialize(layoutMode);
}
// 保存當前頁籤設定
this.settingsManager.set('activeTab', tabName);
// 移除頁籤狀態保存 - 頁籤切換無需持久化
// this.settingsManager.set('activeTab', tabName);
};
/**

View File

@ -14,14 +14,7 @@ class I18nManager {
}
async init() {
// 從 localStorage 載入語言偏好
const savedLanguage = localStorage.getItem('language');
if (savedLanguage) {
this.currentLanguage = savedLanguage;
console.log(`i18nManager 從 localStorage 載入語言: ${savedLanguage}`);
} else {
console.log(`i18nManager 使用默認語言: ${this.currentLanguage}`);
}
console.log(`i18nManager 使用預設語言: ${this.currentLanguage}`);
// 載入翻譯數據
await this.loadTranslations();
@ -125,7 +118,6 @@ class I18nManager {
console.log(`🔄 i18nManager.setLanguage() 被調用: ${this.currentLanguage} -> ${language}`);
if (this.translations[language]) {
this.currentLanguage = language;
localStorage.setItem('language', language);
this.applyTranslations();
// 更新所有語言選擇器(包括現代化版本)
@ -302,10 +294,14 @@ class I18nManager {
selector.removeEventListener('change', selector._i18nChangeHandler);
}
// 添加新的事件監聽器
// 添加新的事件監聽器 - 通過 SettingsManager 統一管理
selector._i18nChangeHandler = (e) => {
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);
}
@ -325,9 +321,13 @@ class I18nManager {
// 移除舊的事件監聽器(如果存在)
option.removeEventListener('click', option._languageClickHandler);
// 添加新的點擊事件監聽器
// 添加新的點擊事件監聽器 - 通過 SettingsManager 統一管理
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);
});

View File

@ -312,12 +312,6 @@
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;
@ -326,14 +320,78 @@
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());
// 頁面載入後從 API 載入日誌等級
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadLogLevelFromAPI);
} else {
loadLogLevelFromAPI();
}
// 匯出到全域命名空間
window.MCPFeedback.Logger = Logger;
window.MCPFeedback.LogLevel = LogLevel;
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()]);
})();

View File

@ -30,7 +30,7 @@
language: 'zh-TW',
imageSizeLimit: 0,
enableBase64Detail: false,
activeTab: 'combined',
// 移除 activeTab - 頁籤切換無需持久化
sessionPanelCollapsed: false,
// 自動定時提交設定
autoSubmitEnabled: false,
@ -58,12 +58,7 @@
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');
console.log('✅ SettingsManager 建構函數初始化完成 - 即時保存模式');
}
/**
@ -75,27 +70,15 @@
return new Promise(function(resolve, reject) {
logger.info('開始載入設定...');
// 優先從伺服器端載入設定
// 從伺服器端載入設定
self.loadFromServer()
.then(function(serverSettings) {
if (serverSettings && Object.keys(serverSettings).length > 0) {
self.currentSettings = self.mergeSettings(self.defaultSettings, serverSettings);
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 {
console.log('沒有找到設定,使用預設值');
self.currentSettings = Utils.deepClone(self.defaultSettings);
}
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);
// 保存到 localStorage
this.saveToLocalStorage();
// 同步保存到伺服器端
// 只保存到伺服器端
this.saveToServer();
// 觸發回調
@ -171,39 +131,13 @@
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() {
const self = this;
// 清除之前的定時器
if (self.saveToServerDebounceTimer) {
clearTimeout(self.saveToServerDebounceTimer);
}
// 標記有待處理的保存操作
self.pendingServerSave = true;
// 設置新的防抖定時器
self.saveToServerDebounceTimer = setTimeout(function() {
self._performServerSave();
}, self.saveToServerDebounceDelay);
this._performServerSave();
};
/**
@ -212,12 +146,6 @@
SettingsManager.prototype._performServerSave = function() {
const self = this;
if (!self.pendingServerSave) {
return;
}
self.pendingServerSave = false;
fetch('/api/save-settings', {
method: 'POST',
headers: {
@ -227,7 +155,7 @@
})
.then(function(response) {
if (response.ok) {
console.log('設定已同步到伺服器端');
console.log('設定已即時同步到伺服器端');
} else {
console.warn('同步設定到伺服器端失敗:', response.status);
}
@ -237,20 +165,7 @@
});
};
/**
* 立即保存到伺服器跳過防抖機制
* 用於重要操作如語言變更重置設定等
*/
SettingsManager.prototype.saveToServerImmediate = function() {
// 清除防抖定時器
if (this.saveToServerDebounceTimer) {
clearTimeout(this.saveToServerDebounceTimer);
this.saveToServerDebounceTimer = null;
}
// 立即執行保存
this._performServerSave();
};
/**
* 合併設定
@ -287,19 +202,11 @@
// 特殊處理語言變更
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;
};
@ -334,14 +241,13 @@
SettingsManager.prototype.handleLanguageChange = function(newLanguage) {
console.log('語言設定變更: ' + newLanguage);
// 同步到 localStorage
if (Utils.isLocalStorageSupported()) {
localStorage.setItem('language', newLanguage);
}
// 通知國際化系統
// 通知國際化系統(統一由 SettingsManager 管理)
if (window.i18nManager) {
window.i18nManager.setLanguage(newLanguage);
// 直接設定語言,不觸發 i18nManager 的保存邏輯
window.i18nManager.currentLanguage = newLanguage;
window.i18nManager.applyTranslations();
window.i18nManager.setupLanguageSelectors();
document.documentElement.lang = newLanguage;
}
// 延遲更新動態文字,確保 i18n 已經載入新語言
@ -361,17 +267,11 @@
SettingsManager.prototype.resetSettings = function() {
console.log('重置所有設定');
// 清除 localStorage
if (Utils.isLocalStorageSupported()) {
localStorage.removeItem('mcp-feedback-settings');
}
// 重置為預設值
this.currentSettings = Utils.deepClone(this.defaultSettings);
// 立即保存重置後的設定(重要操作)
this.saveToLocalStorage();
this.saveToServerImmediate();
// 立即保存重置後的設定到伺服器
this.saveToServer();
// 觸發回調
if (this.onSettingsChange) {

View File

@ -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 模組載入完成');
})();

View File

@ -308,20 +308,7 @@
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 轉義函數

View File

@ -319,7 +319,7 @@
<script src="/static/js/i18n.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/image-handler.js?v=2025010505"></script>
<script src="/static/js/modules/settings-manager.js?v=2025010505"></script>