🎨 重構多語系機制,讓通知也支持多語系

This commit is contained in:
Minidoracat 2025-06-28 06:22:52 +08:00
parent 0917214272
commit 60e64c90dc
24 changed files with 1419 additions and 180 deletions

View File

@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""
訊息代碼驗證腳本
驗證後端訊息代碼前端常量和翻譯文件的一致性
確保所有訊息代碼都有對應的定義和翻譯
使用方式
python scripts/validate_message_codes.py
"""
import json
import re
import sys
from pathlib import Path
def extract_backend_codes():
"""從後端 Python 文件中提取所有訊息代碼"""
codes = set()
# 讀取 MessageCodes 類別
message_codes_file = Path(
"src/mcp_feedback_enhanced/web/constants/message_codes.py"
)
if message_codes_file.exists():
content = message_codes_file.read_text(encoding="utf-8")
# 匹配形如 SESSION_FEEDBACK_SUBMITTED = "session.feedbackSubmitted"
pattern = r'([A-Z_]+)\s*=\s*"([^"]+)"'
matches = re.findall(pattern, content)
for constant_name, code in matches:
codes.add(code)
return codes
def extract_frontend_codes():
"""從前端 JavaScript 文件中提取所有訊息代碼"""
codes = set()
# 讀取 message-codes.js
message_codes_js = Path(
"src/mcp_feedback_enhanced/web/static/js/modules/constants/message-codes.js"
)
if message_codes_js.exists():
content = message_codes_js.read_text(encoding="utf-8")
# 匹配形如 FEEDBACK_SUBMITTED: 'session.feedbackSubmitted'
pattern = r'[A-Z_]+:\s*[\'"]([^\'"]+)[\'"]'
matches = re.findall(pattern, content)
codes.update(matches)
# 讀取 utils.js 中的 fallback 訊息
utils_js = Path("src/mcp_feedback_enhanced/web/static/js/modules/utils.js")
if utils_js.exists():
content = utils_js.read_text(encoding="utf-8")
# 匹配 fallbackMessages 物件中的 key
fallback_section = re.search(
r"fallbackMessages\s*=\s*\{([^}]+)\}", content, re.DOTALL
)
if fallback_section:
pattern = r'[\'"]([^\'"]+)[\'"]:\s*[\'"][^\'"]+[\'"]'
matches = re.findall(pattern, fallback_section.group(1))
codes.update(matches)
return codes
def extract_translation_keys(locale="zh-TW"):
"""從翻譯文件中提取所有 key"""
keys = set()
translation_file = Path(
f"src/mcp_feedback_enhanced/web/locales/{locale}/translation.json"
)
if translation_file.exists():
try:
data = json.loads(translation_file.read_text(encoding="utf-8"))
def extract_keys_recursive(obj, prefix=""):
"""遞迴提取所有 key"""
if isinstance(obj, dict):
for key, value in obj.items():
full_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, dict):
extract_keys_recursive(value, full_key)
else:
keys.add(full_key)
extract_keys_recursive(data)
except json.JSONDecodeError as e:
print(f"❌ 無法解析翻譯文件 {translation_file}: {e}")
return keys
def validate_message_codes():
"""執行驗證"""
print("🔍 開始驗證訊息代碼一致性...\n")
# 提取所有代碼
backend_codes = extract_backend_codes()
frontend_codes = extract_frontend_codes()
# 提取所有語言的翻譯 key
locales = ["zh-TW", "en", "zh-CN"]
translation_keys = {}
for locale in locales:
translation_keys[locale] = extract_translation_keys(locale)
# 統計資訊
print("📊 統計資訊:")
print(f" - 後端訊息代碼數量: {len(backend_codes)}")
print(f" - 前端訊息代碼數量: {len(frontend_codes)}")
for locale in locales:
print(f" - {locale} 翻譯 key 數量: {len(translation_keys[locale])}")
print()
# 驗證後端代碼是否都有前端定義
print("🔍 檢查後端代碼是否都有前端定義...")
missing_in_frontend = backend_codes - frontend_codes
if missing_in_frontend:
print("❌ 以下後端代碼在前端沒有定義:")
for code in sorted(missing_in_frontend):
print(f" - {code}")
else:
print("✅ 所有後端代碼都有前端定義")
print()
# 驗證前端代碼是否都有翻譯
print("🔍 檢查前端代碼是否都有翻譯...")
all_frontend_codes = backend_codes | frontend_codes
for locale in locales:
print(f"\n 檢查 {locale} 翻譯:")
missing_translations = set()
for code in all_frontend_codes:
if code not in translation_keys[locale]:
missing_translations.add(code)
if missing_translations:
print(" ❌ 缺少以下翻譯:")
for code in sorted(missing_translations):
print(f" - {code}")
else:
print(" ✅ 所有代碼都有翻譯")
# 檢查是否有多餘的翻譯
print("\n🔍 檢查是否有多餘的翻譯...")
for locale in locales:
# 過濾掉非訊息代碼的 key如 buttons, labels 等)
message_keys = {
k
for k in translation_keys[locale]
if any(
k.startswith(prefix)
for prefix in [
"system.",
"session.",
"settings.",
"error.",
"command.",
"file.",
"prompt.",
"notification.",
]
)
}
extra_translations = message_keys - all_frontend_codes
if extra_translations:
print(f"\n {locale} 有多餘的翻譯:")
for key in sorted(extra_translations):
print(f" - {key}")
print("\n✅ 驗證完成!")
# 返回是否有錯誤
return len(missing_in_frontend) == 0 and all(
len(
[
code
for code in all_frontend_codes
if code not in translation_keys[locale]
]
)
== 0
for locale in locales
)
if __name__ == "__main__":
# 切換到專案根目錄
script_dir = Path(__file__).parent
project_root = script_dir.parent
import os
os.chdir(project_root)
# 執行驗證
success = validate_message_codes()
sys.exit(0 if success else 1)

View File

@ -0,0 +1,8 @@
"""
Web 常量模組
"""
from .message_codes import MessageCodes, get_message_code
__all__ = ["MessageCodes", "get_message_code"]

View File

@ -0,0 +1,173 @@
"""
統一的訊息代碼定義
這個模組定義了所有後端使用的訊息代碼常量
前端會根據這些代碼顯示對應的本地化訊息
使用方式
from ..constants import MessageCodes, get_message_code
# 使用常量
code = MessageCodes.SESSION_FEEDBACK_SUBMITTED
# 或使用輔助函數
code = get_message_code("SESSION_FEEDBACK_SUBMITTED")
"""
class MessageCodes:
"""訊息代碼常量類"""
# ========== 系統相關 ==========
SYSTEM_CONNECTION_ESTABLISHED = "system.connectionEstablished"
SYSTEM_CONNECTION_LOST = "system.connectionLost"
SYSTEM_CONNECTION_RECONNECTING = "system.connectionReconnecting"
SYSTEM_CONNECTION_RECONNECTED = "system.connectionReconnected"
SYSTEM_CONNECTION_FAILED = "system.connectionFailed"
SYSTEM_WEBSOCKET_ERROR = "system.websocketError"
SYSTEM_WEBSOCKET_READY = "system.websocketReady"
SYSTEM_MEMORY_PRESSURE = "system.memoryPressure"
SYSTEM_SHUTDOWN = "system.shutdown"
SYSTEM_PROCESS_KILLED = "system.processKilled"
SYSTEM_HEARTBEAT_STOPPED = "system.heartbeatStopped"
# ========== 會話相關 ==========
SESSION_NO_ACTIVE = "session.noActiveSession"
SESSION_CREATED = "session.created"
SESSION_UPDATED = "session.updated"
SESSION_EXPIRED = "session.expired"
SESSION_TIMEOUT = "session.timeout"
SESSION_CLEANED = "session.cleaned"
SESSION_FEEDBACK_SUBMITTED = "session.feedbackSubmitted"
SESSION_USER_MESSAGE_RECORDED = "session.userMessageRecorded"
SESSION_HISTORY_SAVED = "session.historySaved"
SESSION_HISTORY_LOADED = "session.historyLoaded"
SESSION_MANUAL_CLEANUP = "session.manualCleanup"
SESSION_ERROR_CLEANUP = "session.errorCleanup"
# ========== 設定相關 ==========
SETTINGS_SAVED = "settingsAPI.saved"
SETTINGS_LOADED = "settingsAPI.loaded"
SETTINGS_CLEARED = "settingsAPI.cleared"
SETTINGS_SAVE_FAILED = "settingsAPI.saveFailed"
SETTINGS_LOAD_FAILED = "settingsAPI.loadFailed"
SETTINGS_CLEAR_FAILED = "settingsAPI.clearFailed"
SETTINGS_SET_FAILED = "settingsAPI.setFailed"
SETTINGS_INVALID_VALUE = "settingsAPI.invalidValue"
SETTINGS_LOG_LEVEL_UPDATED = "settingsAPI.logLevelUpdated"
SETTINGS_INVALID_LOG_LEVEL = "settingsAPI.invalidLogLevel"
# ========== 命令執行相關 ==========
COMMAND_EXECUTING = "commandStatus.executing"
COMMAND_COMPLETED = "commandStatus.completed"
COMMAND_FAILED = "commandStatus.failed"
COMMAND_INVALID = "commandStatus.invalid"
COMMAND_OUTPUT_RECEIVED = "commandStatus.outputReceived"
COMMAND_ERROR = "commandStatus.error"
# ========== 錯誤相關 ==========
ERROR_GENERIC = "error.generic"
ERROR_NETWORK = "error.network"
ERROR_SERVER = "error.server"
ERROR_TIMEOUT = "error.timeout"
ERROR_INVALID_INPUT = "error.invalidInput"
ERROR_OPERATION_FAILED = "error.operationFailed"
ERROR_USER_MESSAGE_FAILED = "error.userMessageFailed"
ERROR_GET_SESSIONS_FAILED = "error.getSessionsFailed"
ERROR_GET_LOG_LEVEL_FAILED = "error.getLogLevelFailed"
ERROR_RESOURCE_CLEANUP = "error.resourceCleanup"
ERROR_PROCESSING = "error.processing"
# ========== 檔案相關 ==========
FILE_UPLOAD_SUCCESS = "file.uploadSuccess"
FILE_UPLOAD_FAILED = "file.uploadFailed"
FILE_SIZE_TOO_LARGE = "file.sizeTooLarge"
FILE_TYPE_NOT_SUPPORTED = "file.typeNotSupported"
FILE_PROCESSING = "file.processing"
FILE_REMOVED = "file.removed"
# ========== 提示詞相關 ==========
PROMPT_SAVED = "prompt.saved"
PROMPT_DELETED = "prompt.deleted"
PROMPT_APPLIED = "prompt.applied"
PROMPT_IMPORT_SUCCESS = "prompt.importSuccess"
PROMPT_IMPORT_FAILED = "prompt.importFailed"
PROMPT_EXPORT_SUCCESS = "prompt.exportSuccess"
PROMPT_VALIDATION_FAILED = "prompt.validationFailed"
# 向後兼容的映射表(從舊的 key 到新的常量名稱)
LEGACY_KEY_MAPPING = {
# feedback_session.py 的舊 key
"FEEDBACK_SUBMITTED": "SESSION_FEEDBACK_SUBMITTED",
"SESSION_CLEANUP": "SESSION_CLEANED",
"TIMEOUT_CLEANUP": "SESSION_TIMEOUT",
"EXPIRED_CLEANUP": "SESSION_EXPIRED",
"MEMORY_PRESSURE_CLEANUP": "SYSTEM_MEMORY_PRESSURE",
"MANUAL_CLEANUP": "SESSION_MANUAL_CLEANUP",
"ERROR_CLEANUP": "SESSION_ERROR_CLEANUP",
"SHUTDOWN_CLEANUP": "SYSTEM_SHUTDOWN",
"COMMAND_EXECUTING": "COMMAND_EXECUTING",
"COMMAND_COMPLETED": "COMMAND_COMPLETED",
"COMMAND_FAILED": "COMMAND_FAILED",
"COMMAND_INVALID": "COMMAND_INVALID",
"COMMAND_ERROR": "COMMAND_ERROR",
"PROCESS_KILLED": "SYSTEM_PROCESS_KILLED",
"RESOURCE_CLEANUP_ERROR": "ERROR_RESOURCE_CLEANUP",
"HEARTBEAT_STOPPED": "SYSTEM_HEARTBEAT_STOPPED",
"PROCESSING_ERROR": "ERROR_PROCESSING",
"WEBSOCKET_READY": "SYSTEM_WEBSOCKET_READY",
# main_routes.py 的舊 key
"no_active_session": "SESSION_NO_ACTIVE",
"websocket_connected": "SYSTEM_CONNECTION_ESTABLISHED",
"new_session_created": "SESSION_CREATED",
"user_message_recorded": "SESSION_USER_MESSAGE_RECORDED",
"add_user_message_failed": "ERROR_USER_MESSAGE_FAILED",
"settings_saved": "SETTINGS_SAVED",
"save_failed": "SETTINGS_SAVE_FAILED",
"load_failed": "SETTINGS_LOAD_FAILED",
"settings_cleared": "SETTINGS_CLEARED",
"clear_failed": "SETTINGS_CLEAR_FAILED",
"session_history_saved": "SESSION_HISTORY_SAVED",
"get_sessions_failed": "ERROR_GET_SESSIONS_FAILED",
"get_log_level_failed": "ERROR_GET_LOG_LEVEL_FAILED",
"invalid_log_level": "SETTINGS_INVALID_LOG_LEVEL",
"log_level_updated": "SETTINGS_LOG_LEVEL_UPDATED",
"set_failed": "SETTINGS_SET_FAILED",
}
def get_message_code(key: str) -> str:
"""
獲取訊息代碼
支援三種輸入方式
1. 直接使用常量名稱get_message_code("SESSION_FEEDBACK_SUBMITTED")
2. 使用舊的 key向後兼容get_message_code("FEEDBACK_SUBMITTED")
3. 使用小寫的舊 key向後兼容get_message_code("feedback_submitted")
Args:
key: 訊息 key 或常量名稱
Returns:
訊息代碼字串例如"session.feedbackSubmitted"
"""
# 嘗試直接從 MessageCodes 獲取
if hasattr(MessageCodes, key):
return str(getattr(MessageCodes, key))
# 嘗試從映射表獲取(支援大寫和小寫)
upper_key = key.upper()
if upper_key in LEGACY_KEY_MAPPING:
constant_name = LEGACY_KEY_MAPPING[upper_key]
if hasattr(MessageCodes, constant_name):
return str(getattr(MessageCodes, constant_name))
# 如果是小寫的 key也嘗試映射
if key in LEGACY_KEY_MAPPING:
constant_name = LEGACY_KEY_MAPPING[key]
if hasattr(MessageCodes, constant_name):
return str(getattr(MessageCodes, constant_name))
# 如果都找不到,返回一個預設格式
return f"unknown.{key}"

View File

@ -50,7 +50,8 @@
"sendFailed": "Send failed, please retry",
"noContent": "No content to copy",
"copySuccess": "Content copied to clipboard",
"copyFailed": "Failed to copy"
"copyFailed": "Failed to copy",
"provideTextOrImage": "Please provide feedback text or upload an image"
},
"summary": {
"title": "📋 AI Work Summary",
@ -96,7 +97,7 @@
"summaryTitle": "📋 AI Work Summary",
"feedbackTitle": "💬 Provide Feedback"
},
"settings": {
"settingsUI": {
"title": "⚙️ Settings",
"language": "🌍 Language",
"currentLanguage": "Current Language",
@ -195,7 +196,7 @@
"upload": "Upload",
"download": "Download"
},
"session": {
"sessionTimeout": {
"timeout": "⏰ Session has timed out, interface will close automatically",
"timeoutWarning": "Session is about to timeout",
"timeoutDescription": "Due to prolonged inactivity, the session has timed out. The interface will automatically close in 3 seconds.",
@ -261,7 +262,10 @@
"clearSuccess": "Session history cleared successfully",
"clearTitle": "Clear all session history records",
"description": "Manage locally stored session history records, including retention period settings and data export functionality",
"exportDescription": "Export or clear locally stored session history records"
"exportDescription": "Export or clear locally stored session history records",
"exportFailed": "Export failed: {error}",
"clearFailed": "Clear failed: {error}",
"clearFailedGeneric": "Clear failed"
},
"retention": {
"24hours": "24 hours",
@ -296,6 +300,14 @@
"clearAllTitle": "Clear all user message records from all sessions",
"confirmClearAll": "Are you sure you want to clear all user message records from all sessions? This action cannot be undone.",
"clearSuccess": "User message records cleared successfully"
},
"noActiveSession": "No active session data",
"sessionNotFound": "Session data not found",
"currentSession": {
"noData": "No current session data",
"noUserMessages": "Current session has no user message records",
"copyFailed": "Copy failed, please try again",
"dataManagerNotInit": "Data manager not initialized"
}
},
"connectionMonitor": {
@ -440,7 +452,8 @@
"countdownLabel": "Submit Countdown",
"pauseCountdown": "Pause Countdown",
"resumeCountdown": "Resume Countdown",
"paused": "Auto submit paused"
"paused": "Auto submit paused",
"autoCommitNoPrompt": "Please select a prompt as auto-submit content first"
},
"audio": {
"notification": {
@ -514,5 +527,95 @@
"permissionRequired": "Please grant notification permission first",
"criticalTitle": "MCP Feedback - Warning"
}
},
"system": {
"connectionEstablished": "WebSocket connection established",
"connectionLost": "WebSocket connection lost",
"connectionReconnecting": "Reconnecting...",
"connectionReconnected": "Reconnected",
"connectionFailed": "Connection failed",
"websocketError": "WebSocket error",
"memoryPressure": "Memory pressure cleanup",
"shutdown": "System shutdown",
"processKilled": "Process killed",
"heartbeatStopped": "Heartbeat stopped",
"websocketReady": "WebSocket ready"
},
"session": {
"noActiveSession": "No active session",
"created": "New MCP session created, page will refresh automatically",
"updated": "Session updated",
"expired": "Session expired",
"timeout": "Session timed out",
"cleaned": "Session cleaned",
"feedbackSubmitted": "Feedback submitted successfully",
"userMessageRecorded": "User message recorded",
"historySaved": "Session history saved ({{count}} sessions)",
"historyLoaded": "Session history loaded"
},
"settingsAPI": {
"saved": "Settings saved",
"loaded": "Settings loaded",
"cleared": "Settings cleared",
"saveFailed": "Save failed",
"loadFailed": "Load failed",
"clearFailed": "Clear failed",
"setFailed": "Set failed",
"invalidValue": "Invalid setting value",
"logLevelUpdated": "Log level updated",
"invalidLogLevel": "Invalid log level, must be one of DEBUG, INFO, WARN, ERROR"
},
"file": {
"uploadSuccess": "File uploaded successfully",
"uploadFailed": "File upload failed",
"sizeTooLarge": "File size exceeds limit",
"typeNotSupported": "File type not supported",
"processing": "Processing file...",
"removed": "File removed"
},
"prompt": {
"saved": "Prompt saved",
"deleted": "Prompt deleted",
"applied": "Prompt applied: {{name}}",
"importSuccess": "Prompts imported successfully",
"importFailed": "Prompts import failed",
"exportSuccess": "Prompts exported successfully",
"validationFailed": "Prompt validation failed"
},
"error": {
"generic": "An error occurred: {{error}}",
"network": "Network error",
"server": "Server error",
"timeout": "Operation timed out",
"invalidInput": "Invalid input",
"operationFailed": "Operation failed",
"userMessageFailed": "Failed to add user message",
"getSessionsFailed": "Failed to get sessions",
"getLogLevelFailed": "Failed to get log level",
"command": "Command execution error",
"resourceCleanup": "Resource cleanup error",
"processing": "Processing error"
},
"commandStatus": {
"executing": "Executing command...",
"completed": "Command completed",
"failed": "Command failed",
"outputReceived": "Output received",
"invalid": "Invalid command",
"error": "Command execution error"
},
"utils": {
"copySuccess": "Copied to clipboard",
"copyError": "Copy failed"
},
"fileUpload": {
"fileSizeExceeded": "Image size exceeds limit ({limit}): {filename}",
"maxFilesExceeded": "Maximum {maxFiles} files can be uploaded",
"processingFailed": "File processing failed, please retry"
},
"aria": {
"toggleAutoSubmit": "Toggle auto submit",
"toggleNotification": "Toggle notification",
"toggleAudioNotification": "Toggle audio notification"
}
}

View File

@ -50,7 +50,8 @@
"sendFailed": "发送失败,请重试",
"noContent": "没有可复制的内容",
"copySuccess": "内容已复制到剪贴板",
"copyFailed": "复制失败"
"copyFailed": "复制失败",
"provideTextOrImage": "请提供反馈文字或上传图片"
},
"summary": {
"title": "📋 AI 工作摘要",
@ -96,7 +97,7 @@
"summaryTitle": "📋 AI 工作摘要",
"feedbackTitle": "💬 提供反馈"
},
"settings": {
"settingsUI": {
"title": "⚙️ 设定",
"language": "🌍 语言",
"currentLanguage": "当前语言",
@ -121,7 +122,8 @@
"resetError": "重置设定时发生错误",
"timeout": "连线逾时 (秒)",
"autorefresh": "自动重新整理",
"debug": "除错模式"
"debug": "除错模式",
"autoCommitNoPrompt": "请先选择一个提示词作为自动提交内容"
},
"languages": {
"zh-TW": "繁體中文",
@ -195,7 +197,7 @@
"upload": "上传",
"download": "下载"
},
"session": {
"sessionTimeout": {
"timeout": "⏰ 会话已超时,界面将自动关闭",
"timeoutWarning": "会话即将超时",
"timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。",
@ -261,7 +263,10 @@
"clearSuccess": "会话历史已清空",
"clearTitle": "清空所有会话历史记录",
"description": "管理本地存储的会话历史记录,包括保存期限设定和数据导出功能",
"exportDescription": "导出或清空本地存储的会话历史记录"
"exportDescription": "导出或清空本地存储的会话历史记录",
"exportFailed": "导出失败: {error}",
"clearFailed": "清空失败: {error}",
"clearFailedGeneric": "清空失败"
},
"retention": {
"24hours": "24 小时",
@ -296,6 +301,14 @@
"clearAllTitle": "清空所有会话的用户消息记录",
"confirmClearAll": "确定要清空所有会话的用户消息记录吗?此操作无法撤销。",
"clearSuccess": "用户消息记录已清空"
},
"noActiveSession": "目前没有活跃的会话数据",
"sessionNotFound": "找不到会话资料",
"currentSession": {
"noData": "没有当前会话数据",
"noUserMessages": "当前会话没有用户消息记录",
"copyFailed": "复制失败,请重试",
"dataManagerNotInit": "数据管理器未初始化"
}
},
"connectionMonitor": {
@ -513,5 +526,82 @@
"permissionRequired": "请先授权通知权限",
"criticalTitle": "MCP Feedback - 警告"
}
},
"system": {
"connectionEstablished": "WebSocket 连接已建立",
"connectionLost": "WebSocket 连接已断开",
"connectionReconnecting": "正在重新连接...",
"connectionReconnected": "已重新连接",
"connectionFailed": "连接失败",
"websocketError": "WebSocket 错误"
},
"session": {
"noActiveSession": "没有活跃会话",
"created": "新的 MCP 会话已创建,页面将自动刷新",
"updated": "会话已更新",
"expired": "会话已过期",
"timeout": "会话已超时",
"cleaned": "会话已清理",
"feedbackSubmitted": "反馈已成功提交",
"userMessageRecorded": "用户消息已记录",
"historySaved": "会话历史已保存({{count}} 个会话)",
"historyLoaded": "会话历史已载入"
},
"settingsAPI": {
"saved": "设置已保存",
"loaded": "设置已载入",
"cleared": "设置已清除",
"saveFailed": "保存失败",
"loadFailed": "加载失败",
"clearFailed": "清除失败",
"invalidValue": "无效的设置值",
"logLevelUpdated": "日志等级已更新",
"invalidLogLevel": "无效的日志等级,必须是 DEBUG, INFO, WARN, ERROR 之一"
},
"file": {
"uploadSuccess": "文件上传成功",
"uploadFailed": "文件上传失败",
"sizeTooLarge": "文件大小超过限制",
"typeNotSupported": "不支持的文件类型",
"processing": "正在处理文件...",
"removed": "文件已移除"
},
"prompt": {
"saved": "提示词已保存",
"deleted": "提示词已删除",
"applied": "已套用提示词:{{name}}",
"importSuccess": "提示词导入成功",
"importFailed": "提示词导入失败",
"exportSuccess": "提示词导出成功",
"validationFailed": "提示词验证失败"
},
"error": {
"generic": "发生错误:{{error}}",
"network": "网络错误",
"server": "服务器错误",
"timeout": "操作超时",
"invalidInput": "输入无效",
"operationFailed": "操作失败"
},
"commandStatus": {
"executing": "正在执行命令...",
"completed": "命令执行完成",
"failed": "命令执行失败",
"outputReceived": "已接收输出",
"invalid": "无效的命令"
},
"utils": {
"copySuccess": "已复制到剪贴板",
"copyError": "复制失败"
},
"fileUpload": {
"fileSizeExceeded": "图片大小超过限制 ({limit}): {filename}",
"maxFilesExceeded": "最多只能上传 {maxFiles} 个文件",
"processingFailed": "文件处理失败,请重试"
},
"aria": {
"toggleAutoSubmit": "切换自动提交",
"toggleNotification": "切换通知",
"toggleAudioNotification": "切换音效通知"
}
}

View File

@ -55,7 +55,8 @@
"sendFailed": "發送失敗,請重試",
"noContent": "沒有可複製的內容",
"copySuccess": "內容已複製到剪貼板",
"copyFailed": "複製失敗"
"copyFailed": "複製失敗",
"provideTextOrImage": "請提供回饋文字或上傳圖片"
},
"summary": {
"title": "📋 AI 工作摘要",
@ -101,7 +102,7 @@
"summaryTitle": "📋 AI 工作摘要",
"feedbackTitle": "💬 提供回饋"
},
"settings": {
"settingsUI": {
"title": "⚙️ 設定",
"language": "🌍 語言",
"currentLanguage": "當前語言",
@ -126,7 +127,8 @@
"resetError": "重置設定時發生錯誤",
"timeout": "連線逾時 (秒)",
"autorefresh": "自動重新整理",
"debug": "除錯模式"
"debug": "除錯模式",
"autoCommitNoPrompt": "請先選擇一個提示詞作為自動提交內容"
},
"languages": {
"zh-TW": "繁體中文",
@ -200,7 +202,7 @@
"upload": "上傳",
"download": "下載"
},
"session": {
"sessionTimeout": {
"timeout": "⏰ 會話已超時,介面將自動關閉",
"timeoutWarning": "會話即將超時",
"timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。",
@ -266,7 +268,10 @@
"clearSuccess": "會話歷史已清空",
"clearTitle": "清空所有會話歷史記錄",
"description": "管理本地儲存的會話歷史記錄,包括保存期限設定和資料匯出功能",
"exportDescription": "匯出或清空本地儲存的會話歷史記錄"
"exportDescription": "匯出或清空本地儲存的會話歷史記錄",
"exportFailed": "匯出失敗: {error}",
"clearFailed": "清空失敗: {error}",
"clearFailedGeneric": "清空失敗"
},
"retention": {
"24hours": "24 小時",
@ -301,6 +306,14 @@
"clearAllTitle": "清空所有會話的用戶訊息記錄",
"confirmClearAll": "確定要清空所有會話的用戶訊息記錄嗎?此操作無法復原。",
"clearSuccess": "用戶訊息記錄已清空"
},
"noActiveSession": "目前沒有活躍的會話數據",
"sessionNotFound": "找不到會話資料",
"currentSession": {
"noData": "沒有當前會話數據",
"noUserMessages": "當前會話沒有用戶消息記錄",
"copyFailed": "複製失敗,請重試",
"dataManagerNotInit": "數據管理器未初始化"
}
},
"connectionMonitor": {
@ -518,5 +531,95 @@
"permissionRequired": "請先授權通知權限",
"criticalTitle": "MCP Feedback - 警告"
}
},
"system": {
"connectionEstablished": "WebSocket 連接已建立",
"connectionLost": "WebSocket 連接已斷開",
"connectionReconnecting": "正在重新連接...",
"connectionReconnected": "已重新連接",
"connectionFailed": "連接失敗",
"websocketError": "WebSocket 錯誤",
"memoryPressure": "記憶體壓力清理",
"shutdown": "系統關閉",
"processKilled": "進程已終止",
"heartbeatStopped": "心跳已停止",
"websocketReady": "WebSocket 已就緒"
},
"session": {
"noActiveSession": "沒有活躍會話",
"created": "新的 MCP 會話已創建,頁面將自動刷新",
"updated": "會話已更新",
"expired": "會話已過期",
"timeout": "會話已超時",
"cleaned": "會話已清理",
"feedbackSubmitted": "反饋已成功提交",
"userMessageRecorded": "用戶消息已記錄",
"historySaved": "會話歷史已保存({{count}} 個會話)",
"historyLoaded": "會話歷史已載入"
},
"settingsAPI": {
"saved": "設定已保存",
"loaded": "設定已載入",
"cleared": "設定已清除",
"saveFailed": "保存失敗",
"loadFailed": "載入失敗",
"clearFailed": "清除失敗",
"setFailed": "設定失敗",
"invalidValue": "無效的設定值",
"logLevelUpdated": "日誌等級已更新",
"invalidLogLevel": "無效的日誌等級,必須是 DEBUG, INFO, WARN, ERROR 之一"
},
"file": {
"uploadSuccess": "檔案上傳成功",
"uploadFailed": "檔案上傳失敗",
"sizeTooLarge": "檔案大小超過限制",
"typeNotSupported": "不支援的檔案類型",
"processing": "正在處理檔案...",
"removed": "檔案已移除"
},
"prompt": {
"saved": "提示詞已保存",
"deleted": "提示詞已刪除",
"applied": "已套用提示詞:{{name}}",
"importSuccess": "提示詞匯入成功",
"importFailed": "提示詞匯入失敗",
"exportSuccess": "提示詞匯出成功",
"validationFailed": "提示詞驗證失敗"
},
"error": {
"generic": "發生錯誤:{{error}}",
"network": "網絡錯誤",
"server": "伺服器錯誤",
"timeout": "操作超時",
"invalidInput": "輸入無效",
"operationFailed": "操作失敗",
"userMessageFailed": "添加用戶消息失敗",
"getSessionsFailed": "獲取會話列表失敗",
"getLogLevelFailed": "獲取日誌等級失敗",
"command": "命令執行錯誤",
"resourceCleanup": "資源清理錯誤",
"processing": "處理過程錯誤"
},
"commandStatus": {
"executing": "正在執行命令...",
"completed": "命令執行完成",
"failed": "命令執行失敗",
"outputReceived": "已接收輸出",
"invalid": "無效的命令",
"error": "命令執行錯誤"
},
"utils": {
"copySuccess": "已複製到剪貼板",
"copyError": "複製失敗"
},
"fileUpload": {
"fileSizeExceeded": "圖片大小超過限制 ({limit}): {filename}",
"maxFilesExceeded": "最多只能上傳 {maxFiles} 個檔案",
"processingFailed": "檔案處理失敗,請重試"
},
"aria": {
"toggleAutoSubmit": "切換自動提交",
"toggleNotification": "切換通知",
"toggleAudioNotification": "切換音效通知"
}
}

View File

@ -23,7 +23,6 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from ..debug import web_debug_log as debug_log
from ..i18n import get_i18n_manager
from ..utils.error_handler import ErrorHandler, ErrorType
from ..utils.memory_monitor import get_memory_monitor
from .models import CleanupReason, SessionStatus, WebFeedbackSession
@ -137,7 +136,7 @@ class WebUIManager:
def _init_basic_components(self):
"""同步初始化基本組件"""
# 基本組件初始化(必須同步)
self.i18n = get_i18n_manager()
# 移除 i18n 管理器,因為翻譯已移至前端
# 設置靜態文件和模板(必須同步)
self._setup_static_files()
@ -181,9 +180,8 @@ class WebUIManager:
def preload_i18n():
try:
# 觸發翻譯載入(如果尚未載入)
self.i18n.get_supported_languages()
debug_log("I18N 資源預載入完成")
# I18N 在前端處理,這裡只記錄預載入完成
debug_log("I18N 資源預載入完成(前端處理)")
return True
except Exception as e:
debug_log(f"I18N 資源預載入失敗: {e}")
@ -733,7 +731,7 @@ class WebUIManager:
refresh_message = {
"type": "session_updated",
"action": "new_session_created",
"message": "新的 MCP 會話已創建,頁面將自動刷新",
"messageCode": "session.created",
"session_info": {
"session_id": self.current_session.session_id,
"project_directory": self.current_session.project_directory,

View File

@ -26,6 +26,7 @@ from fastapi import WebSocket
from ...debug import web_debug_log as debug_log
from ...utils.error_handler import ErrorHandler, ErrorType
from ...utils.resource_manager import get_resource_manager, register_process
from ..constants import get_message_code
class SessionStatus(Enum):
@ -63,6 +64,9 @@ SUPPORTED_IMAGE_TYPES = {
}
TEMP_DIR = Path.home() / ".cache" / "interactive-feedback-mcp-web"
# 訊息代碼現在從統一的常量文件導入
# 使用 get_message_code 函數來獲取訊息代碼
def _safe_parse_command(command: str) -> list[str]:
"""
@ -135,6 +139,7 @@ class WebFeedbackSession:
self.command_logs: list[str] = []
self.user_messages: list[dict] = [] # 用戶消息記錄
self._cleanup_done = False # 防止重複清理
# 移除語言設定,改由前端處理
# 新增:會話狀態管理
self.status = SessionStatus.WAITING
@ -176,6 +181,18 @@ class WebFeedbackSession:
f"會話 {self.session_id} 初始化完成,自動清理延遲: {auto_cleanup_delay}秒,最大空閒: {max_idle_time}"
)
def get_message_code(self, key: str) -> str:
"""
獲取訊息代碼
Args:
key: 訊息 key
Returns:
訊息代碼用於前端 i18n
"""
return get_message_code(key)
def next_step(self, message: str | None = None) -> bool:
"""進入下一個狀態 - 單向流轉,不可倒退"""
old_status = self.status
@ -484,8 +501,9 @@ class WebFeedbackSession:
try:
await self.websocket.send_json(
{
"type": "feedback_received",
"message": "反饋已成功提交",
"type": "notification",
"code": self.get_message_code("FEEDBACK_SUBMITTED"),
"severity": "success",
"status": self.status.value,
}
)
@ -738,21 +756,24 @@ class WebFeedbackSession:
# 2. 關閉 WebSocket 連接
if self.websocket:
try:
# 根據清理原因發送不同的通知消息
message_map = {
CleanupReason.TIMEOUT: "會話已超時,介面將自動關閉",
CleanupReason.EXPIRED: "會話已過期,介面將自動關閉",
CleanupReason.MEMORY_PRESSURE: "系統內存不足,會話將被清理",
CleanupReason.MANUAL: "會話已被手動清理",
CleanupReason.ERROR: "會話發生錯誤,將被清理",
CleanupReason.SHUTDOWN: "系統正在關閉,會話將被清理",
# 根據清理原因獲取訊息代碼
code_key_map = {
CleanupReason.TIMEOUT: "TIMEOUT_CLEANUP",
CleanupReason.EXPIRED: "EXPIRED_CLEANUP",
CleanupReason.MEMORY_PRESSURE: "MEMORY_PRESSURE_CLEANUP",
CleanupReason.MANUAL: "MANUAL_CLEANUP",
CleanupReason.ERROR: "ERROR_CLEANUP",
CleanupReason.SHUTDOWN: "SHUTDOWN_CLEANUP",
}
code_key = code_key_map.get(reason, "SESSION_CLEANUP")
await self.websocket.send_json(
{
"type": "session_cleanup",
"type": "notification",
"code": self.get_message_code(code_key),
"severity": "warning",
"reason": reason.value,
"message": message_map.get(reason, "會話將被清理"),
}
)
await asyncio.sleep(0.1) # 給前端一點時間處理消息

View File

@ -16,6 +16,7 @@ from fastapi.responses import HTMLResponse, JSONResponse
from ... import __version__
from ...debug import web_debug_log as debug_log
from ..constants import get_message_code as get_msg_code
if TYPE_CHECKING:
@ -44,6 +45,11 @@ def load_user_layout_settings() -> str:
return "combined-vertical"
# 使用統一的訊息代碼系統
# 從 ..constants 導入的 get_msg_code 函數會處理所有訊息代碼
# 舊的 key 會自動映射到新的常量
def setup_routes(manager: "WebUIManager"):
"""設置路由"""
@ -79,7 +85,6 @@ def setup_routes(manager: "WebUIManager"):
"version": __version__,
"has_session": True,
"layout_mode": layout_mode,
"i18n": manager.i18n,
},
)
@ -113,16 +118,23 @@ def setup_routes(manager: "WebUIManager"):
return JSONResponse(content=translations)
@manager.app.get("/api/session-status")
async def get_session_status():
async def get_session_status(request: Request):
"""獲取當前會話狀態"""
current_session = manager.get_current_session()
# 從請求頭獲取客戶端語言
lang = (
request.headers.get("Accept-Language", "zh-TW").split(",")[0].split("-")[0]
)
if lang == "zh":
lang = "zh-TW"
if not current_session:
return JSONResponse(
content={
"has_session": False,
"status": "no_session",
"message": "沒有活躍會話",
"messageCode": get_msg_code("no_active_session"),
}
)
@ -139,12 +151,20 @@ def setup_routes(manager: "WebUIManager"):
)
@manager.app.get("/api/current-session")
async def get_current_session():
async def get_current_session(request: Request):
"""獲取當前會話詳細信息"""
current_session = manager.get_current_session()
# 從查詢參數獲取語言,如果沒有則從會話獲取,最後使用默認值
if not current_session:
return JSONResponse(status_code=404, content={"error": "沒有活躍會話"})
return JSONResponse(
status_code=404,
content={
"error": "No active session",
"messageCode": get_msg_code("no_active_session"),
},
)
return JSONResponse(
content={
@ -158,8 +178,9 @@ def setup_routes(manager: "WebUIManager"):
)
@manager.app.get("/api/all-sessions")
async def get_all_sessions():
async def get_all_sessions(request: Request):
"""獲取所有會話的實時狀態"""
try:
sessions_data = []
@ -189,44 +210,65 @@ def setup_routes(manager: "WebUIManager"):
except Exception as e:
debug_log(f"獲取所有會話狀態失敗: {e}")
return JSONResponse(
status_code=500, content={"error": f"獲取會話狀態失敗: {e!s}"}
status_code=500,
content={
"error": f"Failed to get sessions: {e!s}",
"messageCode": get_msg_code("get_sessions_failed"),
},
)
@manager.app.post("/api/add-user-message")
async def add_user_message(request: Request):
"""添加用戶消息到當前會話"""
try:
data = await request.json()
current_session = manager.get_current_session()
if not current_session:
return JSONResponse(status_code=404, content={"error": "沒有活躍會話"})
return JSONResponse(
status_code=404,
content={
"error": "No active session",
"messageCode": get_msg_code("no_active_session"),
},
)
# 添加用戶消息到會話
current_session.add_user_message(data)
debug_log(f"用戶消息已添加到會話 {current_session.session_id}")
return JSONResponse(
content={"status": "success", "message": "用戶消息已記錄"}
content={
"status": "success",
"messageCode": get_msg_code("user_message_recorded"),
}
)
except Exception as e:
debug_log(f"添加用戶消息失敗: {e}")
return JSONResponse(
status_code=500, content={"error": f"添加用戶消息失敗: {e!s}"}
status_code=500,
content={
"error": f"Failed to add user message: {e!s}",
"messageCode": get_msg_code("add_user_message_failed"),
},
)
@manager.app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
async def websocket_endpoint(websocket: WebSocket, lang: str = "zh-TW"):
"""WebSocket 端點 - 重構後移除 session_id 依賴"""
# 獲取當前活躍會話
session = manager.get_current_session()
if not session:
await websocket.close(code=4004, reason="沒有活躍會話")
await websocket.close(code=4004, reason="No active session")
return
await websocket.accept()
# 語言由前端處理,不需要在後端設置
debug_log(f"WebSocket 連接建立,語言由前端處理: {lang}")
# 檢查會話是否已有 WebSocket 連接
if session.websocket and session.websocket != websocket:
debug_log("會話已有 WebSocket 連接,替換為新連接")
@ -237,7 +279,10 @@ def setup_routes(manager: "WebUIManager"):
# 發送連接成功消息
try:
await websocket.send_json(
{"type": "connection_established", "message": "WebSocket 連接已建立"}
{
"type": "connection_established",
"messageCode": get_msg_code("websocket_connected"),
}
)
# 檢查是否有待發送的會話更新
@ -247,7 +292,7 @@ def setup_routes(manager: "WebUIManager"):
{
"type": "session_updated",
"action": "new_session_created",
"message": "新會話已創建,正在更新頁面內容",
"messageCode": get_msg_code("new_session_created"),
"session_info": {
"project_directory": session.project_directory,
"summary": session.summary,
@ -296,6 +341,7 @@ def setup_routes(manager: "WebUIManager"):
@manager.app.post("/api/save-settings")
async def save_settings(request: Request):
"""保存設定到檔案"""
try:
data = await request.json()
@ -310,18 +356,28 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"設定已保存到: {settings_file}")
return JSONResponse(content={"status": "success", "message": "設定已保存"})
return JSONResponse(
content={
"status": "success",
"messageCode": get_msg_code("settings_saved"),
}
)
except Exception as e:
debug_log(f"保存設定失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"保存失敗: {e!s}"},
content={
"status": "error",
"message": f"Save failed: {e!s}",
"messageCode": get_msg_code("save_failed"),
},
)
@manager.app.get("/api/load-settings")
async def load_settings():
async def load_settings(request: Request):
"""從檔案載入設定"""
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
@ -340,12 +396,17 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"載入設定失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"載入失敗: {e!s}"},
content={
"status": "error",
"message": f"Load failed: {e!s}",
"messageCode": get_msg_code("load_failed"),
},
)
@manager.app.post("/api/clear-settings")
async def clear_settings():
async def clear_settings(request: Request):
"""清除設定檔案"""
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
@ -357,18 +418,28 @@ def setup_routes(manager: "WebUIManager"):
else:
debug_log("設定檔案不存在,無需刪除")
return JSONResponse(content={"status": "success", "message": "設定已清除"})
return JSONResponse(
content={
"status": "success",
"messageCode": get_msg_code("settings_cleared"),
}
)
except Exception as e:
debug_log(f"清除設定失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"清除失敗: {e!s}"},
content={
"status": "error",
"message": f"Clear failed: {e!s}",
"messageCode": get_msg_code("clear_failed"),
},
)
@manager.app.get("/api/load-session-history")
async def load_session_history():
async def load_session_history(request: Request):
"""從檔案載入會話歷史"""
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
@ -402,12 +473,17 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"載入會話歷史失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"載入失敗: {e!s}"},
content={
"status": "error",
"message": f"Load failed: {e!s}",
"messageCode": get_msg_code("load_failed"),
},
)
@manager.app.post("/api/save-session-history")
async def save_session_history(request: Request):
"""保存會話歷史到檔案"""
try:
data = await request.json()
@ -435,7 +511,8 @@ def setup_routes(manager: "WebUIManager"):
return JSONResponse(
content={
"status": "success",
"message": f"會話歷史已保存({session_count} 個會話)",
"messageCode": get_msg_code("session_history_saved"),
"params": {"count": session_count},
}
)
@ -443,12 +520,17 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"保存會話歷史失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"保存失敗: {e!s}"},
content={
"status": "error",
"message": f"Save failed: {e!s}",
"messageCode": get_msg_code("save_failed"),
},
)
@manager.app.get("/api/log-level")
async def get_log_level():
async def get_log_level(request: Request):
"""獲取日誌等級設定"""
try:
# 使用統一的設定檔案路徑
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
@ -470,12 +552,16 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"獲取日誌等級失敗: {e}")
return JSONResponse(
status_code=500,
content={"error": f"獲取日誌等級失敗: {e!s}"},
content={
"error": f"Failed to get log level: {e!s}",
"messageCode": get_msg_code("get_log_level_failed"),
},
)
@manager.app.post("/api/log-level")
async def set_log_level(request: Request):
"""設定日誌等級"""
try:
data = await request.json()
log_level = data.get("logLevel")
@ -484,7 +570,8 @@ def setup_routes(manager: "WebUIManager"):
return JSONResponse(
status_code=400,
content={
"error": "無效的日誌等級,必須是 DEBUG, INFO, WARN, ERROR 之一"
"error": "Invalid log level",
"messageCode": get_msg_code("invalid_log_level"),
},
)
@ -512,7 +599,7 @@ def setup_routes(manager: "WebUIManager"):
content={
"status": "success",
"logLevel": log_level,
"message": "日誌等級已更新",
"messageCode": get_msg_code("log_level_updated"),
}
)
@ -520,7 +607,11 @@ def setup_routes(manager: "WebUIManager"):
debug_log(f"設定日誌等級失敗: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"設定失敗: {e!s}"},
content={
"status": "error",
"message": f"Set failed: {e!s}",
"messageCode": get_msg_code("set_failed"),
},
)

View File

@ -670,6 +670,11 @@
break;
case 'session_updated':
console.log('🔄 收到會話更新訊息:', data.session_info);
// 處理訊息代碼
if (data.messageCode && window.i18nManager) {
const message = window.i18nManager.t(data.messageCode);
window.MCPFeedback.Utils.showMessage(message, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
}
this._originalHandleSessionUpdated(data);
break;
case 'desktop_close_request':
@ -712,9 +717,20 @@
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED);
this.uiManager.setLastSubmissionTime(Date.now());
// 停止自動提交計時器(如果正在運行)
if (this.autoSubmitManager && this.autoSubmitManager.isEnabled) {
console.log('⏸️ 反饋已成功提交,停止自動提交倒數計時器');
this.autoSubmitManager.stop();
}
// 顯示成功訊息
const successMessage = window.i18nManager ? window.i18nManager.t('feedback.submitSuccess') : '回饋提交成功!';
window.MCPFeedback.Utils.showMessage(data.message || successMessage, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
if (data.messageCode && window.i18nManager) {
const message = window.i18nManager.t(data.messageCode, data.params);
window.MCPFeedback.Utils.showMessage(message, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
} else {
const successMessage = window.i18nManager ? window.i18nManager.t('feedback.submitSuccess') : '回饋提交成功!';
window.MCPFeedback.Utils.showMessage(data.message || successMessage, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS);
}
// 更新 AI 摘要區域顯示「已送出反饋」狀態
const submittedMessage = window.i18nManager ? window.i18nManager.t('feedback.submittedWaiting') : '已送出反饋,等待下次 MCP 調用...';
@ -793,8 +809,11 @@
}
// 顯示新會話通知
const defaultMessage = window.i18nManager ?
window.i18nManager.t('session.created') :
'New MCP session created, page will refresh automatically';
window.MCPFeedback.Utils.showMessage(
data.message || '新的 MCP 會話已創建,正在更新內容...',
data.message || defaultMessage,
window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS
);
@ -1101,7 +1120,10 @@
const images = this.imageHandler ? this.imageHandler.getImages() : [];
if (!feedback && images.length === 0) {
window.MCPFeedback.Utils.showMessage('請提供回饋文字或上傳圖片', window.MCPFeedback.Utils.CONSTANTS.MESSAGE_WARNING);
const message = window.i18nManager ?
window.i18nManager.t('feedback.provideTextOrImage', '請提供回饋文字或上傳圖片') :
'請提供回饋文字或上傳圖片';
window.MCPFeedback.Utils.showMessage(message, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_WARNING);
return null;
}
@ -1130,6 +1152,12 @@
this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_PROCESSING);
}
// 停止自動提交計時器(如果正在運行)
if (this.autoSubmitManager && this.autoSubmitManager.isEnabled) {
console.log('⏸️ 手動提交反饋,停止自動提交倒數計時器');
this.autoSubmitManager.stop();
}
// 3. 發送回饋到 AI 助手
const success = this.webSocketManager.send({
type: 'submit_feedback',

View File

@ -8,10 +8,40 @@
class I18nManager {
constructor() {
this.currentLanguage = 'zh-CN';
this.currentLanguage = this.getDefaultLanguage();
this.translations = {};
this.loadingPromise = null;
}
getDefaultLanguage() {
// 1. 先檢查本地儲存的設定
const savedLanguage = localStorage.getItem('language');
if (savedLanguage && ['zh-TW', 'zh-CN', 'en'].includes(savedLanguage)) {
console.log('🌐 使用儲存的語言設定:', savedLanguage);
return savedLanguage;
}
// 2. 檢查瀏覽器語言
const browserLang = navigator.language || navigator.userLanguage;
console.log('🌐 瀏覽器語言:', browserLang);
if (browserLang.startsWith('zh-TW') || browserLang.includes('Hant')) {
console.log('🌐 偵測到繁體中文環境');
return 'zh-TW';
}
if (browserLang.startsWith('zh') || browserLang.includes('Hans')) {
console.log('🌐 偵測到簡體中文環境');
return 'zh-CN';
}
if (browserLang.startsWith('en')) {
console.log('🌐 偵測到英文環境');
return 'en';
}
// 3. 預設使用繁體中文
console.log('🌐 使用預設語言: zh-TW');
return 'zh-TW';
}
async init() {
console.log(`i18nManager 使用預設語言: ${this.currentLanguage}`);
@ -163,6 +193,16 @@ class I18nManager {
}
});
// 翻譯有 data-i18n-aria-label 屬性的元素
const ariaLabelElements = document.querySelectorAll('[data-i18n-aria-label]');
ariaLabelElements.forEach(element => {
const key = element.getAttribute('data-i18n-aria-label');
const translation = this.t(key);
if (translation && translation !== key) {
element.setAttribute('aria-label', translation);
}
});
// 更新動態內容
this.updateDynamicContent();
@ -290,31 +330,18 @@ class I18nManager {
// 設定頁籤的下拉選擇器
const selector = document.getElementById('settingsLanguageSelect');
if (selector) {
// 設置當前值
// 設置當前值,不綁定事件(讓 SettingsManager 統一處理)
selector.value = this.currentLanguage;
console.log(`🔧 setupLanguageSelectors: 設置 select.value = ${this.currentLanguage}`);
// 移除舊的事件監聽器(如果存在)
if (selector._i18nChangeHandler) {
selector.removeEventListener('change', selector._i18nChangeHandler);
}
// 添加新的事件監聽器 - 通過 SettingsManager 統一管理
selector._i18nChangeHandler = (e) => {
console.log(`🔄 i18n select change event: ${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);
// 不再綁定事件監聽器,避免與 SettingsManager 衝突
// 事件處理完全交由 SettingsManager 負責
}
// 新版現代化語言選擇器
const languageOptions = document.querySelectorAll('.language-option');
if (languageOptions.length > 0) {
// 設置當前語言的活躍狀態和點擊事件
// 只設置當前語言的活躍狀態,不綁定事件
languageOptions.forEach(option => {
const lang = option.getAttribute('data-lang');
if (lang === this.currentLanguage) {
@ -322,20 +349,8 @@ class I18nManager {
} else {
option.classList.remove('active');
}
// 移除舊的事件監聽器(如果存在)
option.removeEventListener('click', option._languageClickHandler);
// 添加新的點擊事件監聽器 - 通過 SettingsManager 統一管理
option._languageClickHandler = () => {
if (window.feedbackApp && window.feedbackApp.settingsManager) {
window.feedbackApp.settingsManager.set('language', lang);
} else {
this.setLanguage(lang);
}
};
option.addEventListener('click', option._languageClickHandler);
});
// 事件監聽器由 SettingsManager 統一處理,避免重複綁定
}
}

View File

@ -591,7 +591,7 @@
// 可以在這裡添加 UI 通知邏輯
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
const message = window.i18nManager ?
window.i18nManager.t('audio.autoplayBlocked', '瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知') :
window.i18nManager.t('notification.autoplayBlocked', '瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知') :
'瀏覽器阻止音效自動播放,請點擊頁面以啟用音效通知';
window.MCPFeedback.Utils.showMessage(message, 'info');
}

View File

@ -85,13 +85,11 @@
<!-- 啟用開關 -->
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="audio.notification.enabled">啟用音效通知</div>
<div class="setting-description" data-i18n="audio.notification.enabledDesc">
啟用後將在有新會話更新時播放音效通知
</div>
<div class="setting-label" data-i18n="audio.notification.enabled"></div>
<div class="setting-description" data-i18n="audio.notification.enabledDesc"></div>
</div>
<div class="setting-control">
<button type="button" id="audioNotificationEnabled" class="toggle-btn" aria-label="切換音效通知">
<button type="button" id="audioNotificationEnabled" class="toggle-btn" data-i18n-aria-label="aria.toggleAudioNotification">
<span class="toggle-slider"></span>
</button>
</div>
@ -668,6 +666,16 @@
}
});
// 對有 data-i18n-aria-label 屬性的元素應用翻譯
const ariaLabelElements = this.container.querySelectorAll('[data-i18n-aria-label]');
ariaLabelElements.forEach(element => {
const key = element.getAttribute('data-i18n-aria-label');
const translation = this.t(key);
if (translation && translation !== key) {
element.setAttribute('aria-label', translation);
}
});
console.log('🌐 AudioSettingsUI 初始翻譯已應用');
};

View File

@ -0,0 +1,168 @@
/**
* MCP Feedback Enhanced - 訊息代碼常量
* ====================================
*
* 定義所有系統訊息的標準代碼用於國際化支援
*/
(function() {
'use strict';
// 確保命名空間存在
window.MCPFeedback = window.MCPFeedback || {};
window.MCPFeedback.Constants = window.MCPFeedback.Constants || {};
/**
* 訊息代碼枚舉
* 所有系統訊息都應該使用這些代碼而非硬編碼字串
*/
const MessageCodes = {
// 系統狀態訊息
SYSTEM: {
CONNECTION_ESTABLISHED: 'system.connectionEstablished',
CONNECTION_LOST: 'system.connectionLost',
CONNECTION_RECONNECTING: 'system.connectionReconnecting',
CONNECTION_RECONNECTED: 'system.connectionReconnected',
CONNECTION_FAILED: 'system.connectionFailed',
WEBSOCKET_ERROR: 'system.websocketError'
},
// 會話相關訊息
SESSION: {
NO_ACTIVE_SESSION: 'session.noActiveSession',
SESSION_CREATED: 'session.created',
SESSION_UPDATED: 'session.updated',
SESSION_EXPIRED: 'session.expired',
SESSION_TIMEOUT: 'session.timeout',
SESSION_CLEANED: 'session.cleaned',
FEEDBACK_SUBMITTED: 'session.feedbackSubmitted',
USER_MESSAGE_RECORDED: 'session.userMessageRecorded',
HISTORY_SAVED: 'session.historySaved',
HISTORY_LOADED: 'session.historyLoaded',
MANUAL_CLEANUP: 'session.manualCleanup',
ERROR_CLEANUP: 'session.errorCleanup'
},
// 設定相關訊息
SETTINGS: {
SAVED: 'settings.saved',
LOADED: 'settings.loaded',
CLEARED: 'settings.cleared',
SAVE_FAILED: 'settings.saveFailed',
LOAD_FAILED: 'settings.loadFailed',
CLEAR_FAILED: 'settings.clearFailed',
INVALID_VALUE: 'settings.invalidValue',
LOG_LEVEL_UPDATED: 'settings.logLevelUpdated',
INVALID_LOG_LEVEL: 'settings.invalidLogLevel'
},
// 通知相關訊息
NOTIFICATION: {
AUTOPLAY_BLOCKED: 'notification.autoplayBlocked',
PERMISSION_DENIED: 'notification.permissionDenied',
PERMISSION_GRANTED: 'notification.permissionGranted',
TEST_SENT: 'notification.testSent',
SOUND_ENABLED: 'notification.soundEnabled',
SOUND_DISABLED: 'notification.soundDisabled'
},
// 檔案上傳訊息
FILE: {
UPLOAD_SUCCESS: 'file.uploadSuccess',
UPLOAD_FAILED: 'file.uploadFailed',
SIZE_TOO_LARGE: 'file.sizeTooLarge',
TYPE_NOT_SUPPORTED: 'file.typeNotSupported',
PROCESSING: 'file.processing',
REMOVED: 'file.removed'
},
// 提示詞相關訊息
PROMPT: {
SAVED: 'prompt.saved',
DELETED: 'prompt.deleted',
APPLIED: 'prompt.applied',
IMPORT_SUCCESS: 'prompt.importSuccess',
IMPORT_FAILED: 'prompt.importFailed',
EXPORT_SUCCESS: 'prompt.exportSuccess',
VALIDATION_FAILED: 'prompt.validationFailed'
},
// 錯誤訊息
ERROR: {
GENERIC: 'error.generic',
NETWORK: 'error.network',
SERVER: 'error.server',
TIMEOUT: 'error.timeout',
INVALID_INPUT: 'error.invalidInput',
OPERATION_FAILED: 'error.operationFailed'
},
// 命令執行訊息
COMMAND: {
EXECUTING: 'commandStatus.executing',
COMPLETED: 'commandStatus.completed',
FAILED: 'commandStatus.failed',
OUTPUT_RECEIVED: 'commandStatus.outputReceived',
INVALID_COMMAND: 'commandStatus.invalid',
ERROR: 'commandStatus.error'
}
};
/**
* 訊息嚴重程度
*/
const MessageSeverity = {
INFO: 'info',
SUCCESS: 'success',
WARNING: 'warning',
ERROR: 'error'
};
/**
* 建立標準訊息物件
* @param {string} code - 訊息代碼
* @param {Object} params - 動態參數
* @param {string} severity - 嚴重程度
* @returns {Object} 標準訊息物件
*/
function createMessage(code, params = {}, severity = MessageSeverity.INFO) {
return {
type: 'notification',
code: code,
params: params,
severity: severity,
timestamp: Date.now()
};
}
/**
* 快捷方法建立成功訊息
*/
function createSuccessMessage(code, params = {}) {
return createMessage(code, params, MessageSeverity.SUCCESS);
}
/**
* 快捷方法建立錯誤訊息
*/
function createErrorMessage(code, params = {}) {
return createMessage(code, params, MessageSeverity.ERROR);
}
/**
* 快捷方法建立警告訊息
*/
function createWarningMessage(code, params = {}) {
return createMessage(code, params, MessageSeverity.WARNING);
}
// 匯出到全域命名空間
window.MCPFeedback.Constants.MessageCodes = MessageCodes;
window.MCPFeedback.Constants.MessageSeverity = MessageSeverity;
window.MCPFeedback.Constants.createMessage = createMessage;
window.MCPFeedback.Constants.createSuccessMessage = createSuccessMessage;
window.MCPFeedback.Constants.createErrorMessage = createErrorMessage;
window.MCPFeedback.Constants.createWarningMessage = createWarningMessage;
console.log('📋 訊息代碼常量載入完成');
})();

View File

@ -289,14 +289,23 @@
if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
const sizeLimit = this.formatFileSize(this.maxFileSize);
console.warn('⚠️ 檔案過大:', file.name, '超過限制', sizeLimit);
this.showMessage('圖片大小超過限制 (' + sizeLimit + '): ' + file.name, 'warning');
const message = window.i18nManager ?
window.i18nManager.t('fileUpload.fileSizeExceeded', {
limit: sizeLimit,
filename: file.name
}) :
'圖片大小超過限制 (' + sizeLimit + '): ' + file.name;
this.showMessage(message, 'warning');
continue;
}
// 檢查檔案數量限制
if (this.files.length + validFiles.length >= this.maxFiles) {
console.warn('⚠️ 檔案數量超過限制:', this.maxFiles);
this.showMessage('最多只能上傳 ' + this.maxFiles + ' 個檔案', 'warning');
const message = window.i18nManager ?
window.i18nManager.t('fileUpload.maxFilesExceeded', { maxFiles: this.maxFiles }) :
'最多只能上傳 ' + this.maxFiles + ' 個檔案';
this.showMessage(message, 'warning');
break;
}
@ -340,7 +349,10 @@
})
.catch(function(error) {
console.error('❌ 檔案處理失敗:', error);
self.showMessage('檔案處理失敗,請重試', 'error');
const message = window.i18nManager ?
window.i18nManager.t('fileUpload.processingFailed', '檔案處理失敗,請重試') :
'檔案處理失敗,請重試';
self.showMessage(message, 'error');
});
};

View File

@ -322,7 +322,8 @@
// 從 API 載入日誌等級
function loadLogLevelFromAPI() {
fetch('/api/log-level')
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/log-level?lang=' + lang)
.then(function(response) {
if (response.ok) {
return response.json();
@ -343,7 +344,8 @@
// 保存日誌等級到 API
function saveLogLevelToAPI(logLevel) {
fetch('/api/log-level', {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/log-level?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -360,6 +362,11 @@
})
.then(function(data) {
console.log('📋 日誌等級已保存:', data.logLevel);
// 處理訊息代碼
if (data.messageCode && window.i18nManager) {
const message = window.i18nManager.t(data.messageCode, data.params);
console.log('伺服器回應:', message);
}
})
.catch(function(error) {
console.warn('⚠️ 保存日誌等級失敗:', error);

View File

@ -55,6 +55,11 @@
this.setupEventListeners();
this.updateUI();
// 應用翻譯到動態生成的內容
if (window.i18nManager) {
window.i18nManager.applyTranslations();
}
console.log('✅ NotificationSettings 初始化完成');
};
@ -66,17 +71,15 @@
<!-- 啟用開關 -->
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="notification.settingLabel">瀏覽器通知</div>
<div class="setting-description" data-i18n="notification.description">
新會話建立時通知僅在背景執行時
</div>
<div class="setting-label" data-i18n="notification.settingLabel"></div>
<div class="setting-description" data-i18n="notification.description"></div>
<!-- 權限狀態 -->
<div id="permissionStatus" class="permission-status">
<!-- 動態更新 -->
</div>
</div>
<div class="setting-control">
<button type="button" id="notificationToggle" class="toggle-btn" aria-label="切換通知">
<button type="button" id="notificationToggle" class="toggle-btn" data-i18n-aria-label="aria.toggleNotification">
<span class="toggle-slider"></span>
</button>
</div>
@ -85,25 +88,25 @@
<!-- 通知觸發情境 -->
<div class="setting-item notification-trigger" style="display: none;">
<div class="setting-info">
<div class="setting-label" data-i18n="notification.triggerTitle">通知觸發情境</div>
<div class="setting-description" data-i18n="notification.triggerDescription">選擇何時接收通知</div>
<div class="setting-label" data-i18n="notification.triggerTitle"></div>
<div class="setting-description" data-i18n="notification.triggerDescription"></div>
</div>
<div class="trigger-options">
<label class="radio-option">
<input type="radio" name="notificationTrigger" value="focusLost" checked>
<span data-i18n="notification.trigger.focusLost">視窗失去焦點時切換到其他應用程式</span>
<span data-i18n="notification.trigger.focusLost"></span>
</label>
<label class="radio-option">
<input type="radio" name="notificationTrigger" value="tabSwitch">
<span data-i18n="notification.trigger.tabSwitch">切換到其他標籤頁時</span>
<span data-i18n="notification.trigger.tabSwitch"></span>
</label>
<label class="radio-option">
<input type="radio" name="notificationTrigger" value="background">
<span data-i18n="notification.trigger.background">視窗最小化或隱藏時</span>
<span data-i18n="notification.trigger.background"></span>
</label>
<label class="radio-option">
<input type="radio" name="notificationTrigger" value="always">
<span data-i18n="notification.trigger.always">總是通知包括前景</span>
<span data-i18n="notification.trigger.always"></span>
</label>
</div>
</div>
@ -111,12 +114,12 @@
<!-- 測試按鈕 -->
<div class="setting-item notification-actions" style="display: none;">
<div class="setting-info">
<div class="setting-label" data-i18n="notification.testTitle">測試通知</div>
<div class="setting-description" data-i18n="notification.testDescription">發送測試通知以確認功能正常</div>
<div class="setting-label" data-i18n="notification.testTitle"></div>
<div class="setting-description" data-i18n="notification.testDescription"></div>
</div>
<div class="setting-control">
<button type="button" id="testNotification" class="btn-primary">
<span data-i18n="notification.test">發送測試通知</span>
<span data-i18n="notification.test"></span>
</button>
</div>
</div>
@ -261,7 +264,7 @@
const settings = this.notificationManager.getSettings();
if (!settings.browserSupported) {
this.statusDiv.innerHTML = `<span data-i18n="notification.notSupported">⚠️ 您的瀏覽器不支援通知功能</span>`;
this.statusDiv.innerHTML = `<span data-i18n="notification.notSupported"></span>`;
this.statusDiv.className = 'permission-status status-unsupported';
this.toggle.disabled = true;
return;

View File

@ -370,7 +370,10 @@
const currentSession = this.dataManager.getCurrentSession();
if (!currentSession) {
this.showMessage('目前沒有活躍的會話數據', 'warning');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.noActiveSession', '目前沒有活躍的會話數據') :
'目前沒有活躍的會話數據';
this.showMessage(message, 'warning');
return;
}
@ -390,7 +393,10 @@
if (sessionData) {
this.detailsModal.showSessionDetails(sessionData);
} else {
this.showMessage('找不到會話資料', 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.sessionNotFound', '找不到會話資料') :
'找不到會話資料';
this.showMessage(message, 'error');
}
};
@ -595,7 +601,10 @@
} catch (error) {
console.error('📋 匯出會話歷史失敗:', error);
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
window.MCPFeedback.Utils.showMessage('匯出失敗: ' + error.message, 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.exportFailed', { error: error.message }) :
'匯出失敗: ' + error.message;
window.MCPFeedback.Utils.showMessage(message, 'error');
}
}
};
@ -623,7 +632,10 @@
} catch (error) {
console.error('📋 匯出單一會話失敗:', error);
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
window.MCPFeedback.Utils.showMessage('匯出失敗: ' + error.message, 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.exportFailed', { error: error.message }) :
'匯出失敗: ' + error.message;
window.MCPFeedback.Utils.showMessage(message, 'error');
}
}
};
@ -659,7 +671,10 @@
} catch (error) {
console.error('📋 清空會話歷史失敗:', error);
if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
window.MCPFeedback.Utils.showMessage('清空失敗: ' + error.message, 'error');
const errorMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.clearFailed', { error: error.message }) :
'清空失敗: ' + error.message;
window.MCPFeedback.Utils.showMessage(errorMessage, 'error');
}
}
};
@ -690,11 +705,17 @@
'用戶訊息記錄已清空';
this.showMessage(successMessage, 'success');
} else {
this.showMessage('清空失敗', 'error');
const errorMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.clearFailedGeneric', '清空失敗') :
'清空失敗';
this.showMessage(errorMessage, 'error');
}
} catch (error) {
console.error('📋 清空用戶訊息記錄失敗:', error);
this.showMessage('清空失敗: ' + error.message, 'error');
const errorMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.management.clearFailed', { error: error.message }) :
'清空失敗: ' + error.message;
this.showMessage(errorMessage, 'error');
}
};
@ -752,15 +773,24 @@
try {
const currentSession = this.dataManager.getCurrentSession();
if (!currentSession) {
this.showMessage('没有当前会话数据', 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.noData', '沒有當前會話數據') :
'沒有當前會話數據';
this.showMessage(message, 'error');
return;
}
const content = this.formatCurrentSessionContent(currentSession);
this.copyToClipboard(content, '当前会话内容已复制到剪贴板');
const successMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.copySuccess', '當前會話內容已複製到剪貼板') :
'當前會話內容已複製到剪貼板';
this.copyToClipboard(content, successMessage);
} catch (error) {
console.error('复制当前会话内容失败:', error);
this.showMessage('复制失败,请重试', 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.copyFailed', '複製失敗,請重試') :
'複製失敗,請重試';
this.showMessage(message, 'error');
}
};
@ -774,7 +804,10 @@
try {
if (!this.dataManager) {
console.log('📝 dataManager 不存在,尝试其他方式获取数据');
this.showMessage('数据管理器未初始化', 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.dataManagerNotInit', '數據管理器未初始化') :
'數據管理器未初始化';
this.showMessage(message, 'error');
return;
}
@ -783,7 +816,10 @@
if (!currentSession) {
console.log('📝 没有当前会话数据');
this.showMessage('当前会话没有数据', 'warning');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.noData', '當前會話沒有數據') :
'當前會話沒有數據';
this.showMessage(message, 'warning');
return;
}
@ -792,7 +828,10 @@
if (!currentSession.user_messages || currentSession.user_messages.length === 0) {
console.log('📝 没有用户消息记录');
this.showMessage('当前会话没有用户消息记录', 'warning');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.noUserMessages', '當前會話沒有用戶消息記錄') :
'當前會話沒有用戶消息記錄';
this.showMessage(message, 'warning');
return;
}
@ -804,11 +843,17 @@
console.log('📝 格式化后的内容长度:', content.length);
console.log('📝 格式化后的内容预览:', content.substring(0, 200));
this.copyToClipboard(content, '当前用户内容已复制到剪贴板');
const successMessage = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.userContentCopySuccess', '當前用戶內容已複製到剪貼板') :
'當前用戶內容已複製到剪貼板';
this.copyToClipboard(content, successMessage);
} catch (error) {
console.error('📝 复制当前用户内容失败:', error);
console.error('📝 错误堆栈:', error.stack);
this.showMessage('复制失败,请重试', 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.copyFailed', '複製失敗,請重試') :
'複製失敗,請重試';
this.showMessage(message, 'error');
}
};
@ -959,7 +1004,10 @@
this.showMessage(successMessage, 'success');
} catch (err) {
console.error('降级复制失败:', err);
this.showMessage('复制失败,请手动复制', 'error');
const message = window.i18nManager ?
window.i18nManager.t('sessionHistory.currentSession.copyFailedManual', '複製失敗,請手動複製') :
'複製失敗,請手動複製';
this.showMessage(message, 'error');
} finally {
document.body.removeChild(textArea);
}

View File

@ -327,7 +327,8 @@
* 發送用戶消息到服務器端
*/
SessionDataManager.prototype.sendUserMessageToServer = function(userMessage) {
fetch('/api/add-user-message', {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/add-user-message?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@ -633,7 +634,8 @@
const self = this;
// 首先嘗試獲取實時會話狀態
fetch('/api/all-sessions')
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/all-sessions?lang=' + lang)
.then(function(response) {
if (response.ok) {
return response.json();
@ -677,7 +679,8 @@
SessionDataManager.prototype.loadFromHistoryFile = function() {
const self = this;
fetch('/api/load-session-history')
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/load-session-history?lang=' + lang)
.then(function(response) {
if (response.ok) {
return response.json();
@ -775,7 +778,8 @@
lastCleanup: TimeUtils.getCurrentTimestamp()
};
fetch('/api/save-session-history', {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/save-session-history?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -791,7 +795,12 @@
}
})
.then(function(result) {
console.log('📊 會話快照保存回應:', result.message);
if (result.messageCode && window.i18nManager) {
const message = window.i18nManager.t(result.messageCode, result.params);
console.log('📊 會話快照保存回應:', message);
} else {
console.log('📊 會話快照保存回應:', result.message);
}
})
.catch(function(error) {
console.error('📊 保存會話快照到伺服器失敗:', error);
@ -807,7 +816,8 @@
lastCleanup: TimeUtils.getCurrentTimestamp()
};
fetch('/api/save-session-history', {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/save-session-history?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -823,7 +833,12 @@
}
})
.then(function(result) {
console.log('📊 伺服器保存回應:', result.message);
if (result.messageCode && window.i18nManager) {
const message = window.i18nManager.t(result.messageCode, result.params);
console.log('📊 伺服器保存回應:', message);
} else {
console.log('📊 伺服器保存回應:', result.message);
}
})
.catch(function(error) {
console.error('📊 保存會話歷史到伺服器失敗:', error);

View File

@ -23,11 +23,14 @@
function SettingsManager(options) {
options = options || {};
// 從 i18nManager 獲取當前語言作為預設值
const defaultLanguage = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
// 預設設定
this.defaultSettings = {
layoutMode: 'combined-vertical',
autoClose: false,
language: 'zh-TW',
language: defaultLanguage, // 使用 i18nManager 的當前語言
imageSizeLimit: 0,
enableBase64Detail: false,
// 移除 activeTab - 頁籤切換無需持久化
@ -80,6 +83,17 @@
console.log('沒有找到設定,使用預設值');
self.currentSettings = Utils.deepClone(self.defaultSettings);
}
// 同步語言設定到 i18nManager
if (self.currentSettings.language && window.i18nManager) {
const currentI18nLanguage = window.i18nManager.getCurrentLanguage();
if (self.currentSettings.language !== currentI18nLanguage) {
console.log('🔧 SettingsManager.loadSettings: 同步語言設定到 i18nManager');
console.log(' 從:', currentI18nLanguage, '到:', self.currentSettings.language);
window.i18nManager.setLanguage(self.currentSettings.language);
}
}
resolve(self.currentSettings);
})
.catch(function(error) {
@ -94,7 +108,8 @@
* 從伺服器載入設定
*/
SettingsManager.prototype.loadFromServer = function() {
return fetch('/api/load-settings')
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
return fetch('/api/load-settings?lang=' + lang)
.then(function(response) {
if (response.ok) {
return response.json();
@ -146,7 +161,8 @@
SettingsManager.prototype._performServerSave = function() {
const self = this;
fetch('/api/save-settings', {
const lang = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
fetch('/api/save-settings?lang=' + lang, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -154,10 +170,18 @@
body: JSON.stringify(self.currentSettings)
})
.then(function(response) {
if (response.ok) {
return response.json();
})
.then(function(data) {
if (data.status === 'success') {
console.log('設定已即時同步到伺服器端');
// 處理訊息代碼
if (data.messageCode && window.i18nManager) {
const message = window.i18nManager.t(data.messageCode, data.params);
console.log('伺服器回應:', message);
}
} else {
console.warn('同步設定到伺服器端失敗:', response.status);
console.warn('同步設定到伺服器端失敗:', data);
}
})
.catch(function(error) {
@ -239,15 +263,12 @@
* 處理語言變更
*/
SettingsManager.prototype.handleLanguageChange = function(newLanguage) {
console.log('語言設定變更: ' + newLanguage);
console.log('🔄 SettingsManager.handleLanguageChange: ' + newLanguage);
// 通知國際化系統(統一由 SettingsManager 管理)
if (window.i18nManager) {
// 直接設定語言,不觸發 i18nManager 的保存邏輯
window.i18nManager.currentLanguage = newLanguage;
window.i18nManager.applyTranslations();
window.i18nManager.setupLanguageSelectors();
document.documentElement.lang = newLanguage;
// 使用 setLanguage 方法確保正確更新
window.i18nManager.setLanguage(newLanguage);
}
// 延遲更新動態文字,確保 i18n 已經載入新語言
@ -688,7 +709,10 @@
try {
// 如果要啟用自動提交,檢查是否已選擇提示詞
if (newValue && (!currentPromptId || currentPromptId === '')) {
Utils.showMessage('請先選擇一個提示詞作為自動提交內容', Utils.CONSTANTS.MESSAGE_WARNING);
const message = window.i18nManager ?
window.i18nManager.t('settingsUI.autoCommitNoPrompt', '請先選擇一個提示詞作為自動提交內容') :
'請先選擇一個提示詞作為自動提交內容';
Utils.showMessage(message, Utils.CONSTANTS.MESSAGE_WARNING);
return;
}

View File

@ -494,7 +494,9 @@
feedbackInput.value = '';
console.log('📝 已清空文字內容');
}
feedbackInput.disabled = false;
// 只有在等待狀態才啟用輸入框
const canInput = this.feedbackState === Utils.CONSTANTS.FEEDBACK_WAITING;
feedbackInput.disabled = !canInput;
}
// 重新啟用提交按鈕

View File

@ -169,8 +169,12 @@
* @returns {Promise<boolean>} 複製是否成功
*/
copyToClipboard: function(text, successMessage, errorMessage) {
successMessage = successMessage || '已複製到剪貼板';
errorMessage = errorMessage || '複製失敗';
successMessage = successMessage || (window.i18nManager ?
window.i18nManager.t('utils.copySuccess', '已複製到剪貼板') :
'已複製到剪貼板');
errorMessage = errorMessage || (window.i18nManager ?
window.i18nManager.t('utils.copyError', '複製失敗') :
'複製失敗');
return new Promise(function(resolve) {
if (navigator.clipboard && navigator.clipboard.writeText) {
@ -262,7 +266,115 @@
* @param {string} type - 訊息類型 (success, error, warning, info)
* @param {number} duration - 顯示時間毫秒
*/
showMessage: function(message, type, duration) {
showMessage: function(messageOrCode, type, duration) {
// 處理訊息代碼物件
let actualMessage = messageOrCode;
let actualType = type || 'info';
if (typeof messageOrCode === 'object' && messageOrCode.code) {
// 使用 i18n 系統翻譯訊息代碼
if (window.i18nManager) {
actualMessage = window.i18nManager.t(messageOrCode.code, messageOrCode.params);
} else {
// 改善 fallback 機制:提供基本的英文訊息
actualMessage = this.getFallbackMessage(messageOrCode.code, messageOrCode.params);
}
// 使用訊息物件中的嚴重程度
actualType = messageOrCode.severity || type || 'info';
}
// 呼叫內部顯示方法
return this._displayMessage(actualMessage, actualType, duration);
},
/**
* 獲取 fallback 訊息
* i18n 系統尚未載入時使用
* @param {string} code - 訊息代碼
* @param {Object} params - 參數
* @returns {string} fallback 訊息
*/
getFallbackMessage: function(code, params) {
// 基本的 fallback 訊息對照表
const fallbackMessages = {
// 系統相關
'system.connectionEstablished': 'WebSocket connection established',
'system.connectionLost': 'WebSocket connection lost',
'system.connectionReconnecting': 'Reconnecting...',
'system.connectionReconnected': 'Reconnected',
'system.connectionFailed': 'Connection failed',
'system.websocketError': 'WebSocket error',
'system.websocketReady': 'WebSocket ready',
'system.memoryPressure': 'Memory pressure cleanup',
'system.shutdown': 'System shutdown',
'system.processKilled': 'Process killed',
'system.heartbeatStopped': 'Heartbeat stopped',
// 會話相關
'session.noActiveSession': 'No active session',
'session.created': 'New session created',
'session.updated': 'Session updated',
'session.expired': 'Session expired',
'session.timeout': 'Session timed out',
'session.cleaned': 'Session cleaned',
'session.feedbackSubmitted': 'Feedback submitted successfully',
'session.userMessageRecorded': 'User message recorded',
'session.historySaved': 'Session history saved',
'session.historyLoaded': 'Session history loaded',
// 設定相關
'settings.saved': 'Settings saved',
'settings.loaded': 'Settings loaded',
'settings.cleared': 'Settings cleared',
'settings.saveFailed': 'Save failed',
'settings.loadFailed': 'Load failed',
'settings.clearFailed': 'Clear failed',
'settings.setFailed': 'Set failed',
'settings.logLevelUpdated': 'Log level updated',
'settings.invalidLogLevel': 'Invalid log level',
// 錯誤相關
'error.generic': 'An error occurred',
'error.userMessageFailed': 'Failed to add user message',
'error.getSessionsFailed': 'Failed to get sessions',
'error.getLogLevelFailed': 'Failed to get log level',
'error.command': 'Command execution error',
'error.resourceCleanup': 'Resource cleanup error',
'error.processing': 'Processing error',
// 通知相關
'notification.autoplayBlocked': 'Browser blocked autoplay, click page to enable sound',
// 預設訊息
'default': 'System message'
};
// 嘗試獲取對應的 fallback 訊息
let message = fallbackMessages[code] || fallbackMessages['default'];
// 處理參數替換(簡單版本)
if (params && typeof params === 'object') {
for (const key in params) {
if (params.hasOwnProperty(key)) {
const placeholder = '{{' + key + '}}';
message = message.replace(placeholder, params[key]);
}
}
}
// 在開發模式下顯示警告
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
console.warn('[i18n] Fallback message used for:', code, '→', message);
}
return message;
},
/**
* 內部方法實際顯示訊息
* @private
*/
_displayMessage: function(message, type, duration) {
type = type || 'info';
duration = duration || 3000;

View File

@ -74,7 +74,10 @@
this.websocket = null;
}
this.websocket = new WebSocket(wsUrl);
// 添加語言參數到 WebSocket URL
const language = window.i18nManager ? window.i18nManager.getCurrentLanguage() : 'zh-TW';
const wsUrlWithLang = wsUrl + (wsUrl.includes('?') ? '&' : '?') + 'lang=' + language;
this.websocket = new WebSocket(wsUrlWithLang);
this.setupWebSocketEvents();
} catch (error) {
@ -261,6 +264,11 @@
console.log('WebSocket 連接確認');
this.connectionReady = true;
this.handleConnectionReady();
// 處理訊息代碼
if (data.messageCode && window.i18nManager) {
const message = window.i18nManager.t(data.messageCode);
Utils.showMessage(message, Utils.CONSTANTS.MESSAGE_SUCCESS);
}
break;
case 'heartbeat_response':
this.handleHeartbeatResponse();

View File

@ -693,13 +693,13 @@
<!-- 介面設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="settings.interface">🎨 介面設定</h3>
<h3 class="settings-card-title" data-i18n="settingsUI.interface">🎨 介面設定</h3>
</div>
<div class="settings-card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="settings.layoutMode">界面佈局模式</div>
<div class="setting-description" data-i18n="settings.layoutModeDesc">
<div class="setting-label" data-i18n="settingsUI.layoutMode">界面佈局模式</div>
<div class="setting-description" data-i18n="settingsUI.layoutModeDesc">
選擇 AI 摘要和回饋輸入的顯示方式
</div>
</div>
@ -707,15 +707,15 @@
<div class="layout-option">
<input type="radio" id="combinedVertical" name="layoutMode" value="combined-vertical" checked>
<label for="combinedVertical">
<div class="layout-option-title" data-i18n="settings.combinedVertical">垂直布局</div>
<div class="layout-option-desc" data-i18n="settings.combinedVerticalDesc">AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面</div>
<div class="layout-option-title" data-i18n="settingsUI.combinedVertical">垂直布局</div>
<div class="layout-option-desc" data-i18n="settingsUI.combinedVerticalDesc">AI 摘要在上,回饋輸入在下,摘要和回饋在同一頁面</div>
</label>
</div>
<div class="layout-option">
<input type="radio" id="combinedHorizontal" name="layoutMode" value="combined-horizontal">
<label for="combinedHorizontal">
<div class="layout-option-title" data-i18n="settings.combinedHorizontal">水平布局</div>
<div class="layout-option-desc" data-i18n="settings.combinedHorizontalDesc">AI 摘要在左,回饋輸入在右,增大摘要可視區域</div>
<div class="layout-option-title" data-i18n="settingsUI.combinedHorizontal">水平布局</div>
<div class="layout-option-desc" data-i18n="settingsUI.combinedHorizontalDesc">AI 摘要在左,回饋輸入在右,增大摘要可視區域</div>
</label>
</div>
</div>
@ -729,13 +729,13 @@
<!-- 語言設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="settings.language">🌍 語言設定</h3>
<h3 class="settings-card-title" data-i18n="settingsUI.language">🌍 語言設定</h3>
</div>
<div class="settings-card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="settings.currentLanguage">當前語言</div>
<div class="setting-description" data-i18n="settings.languageDesc">
<div class="setting-label" data-i18n="settingsUI.currentLanguage">當前語言</div>
<div class="setting-description" data-i18n="settingsUI.languageDesc">
選擇界面顯示語言
</div>
</div>
@ -804,7 +804,7 @@
</div>
</div>
<div class="setting-control">
<button type="button" id="autoSubmitToggle" class="toggle-btn" aria-label="切換自動提交">
<button type="button" id="autoSubmitToggle" class="toggle-btn" data-i18n-aria-label="aria.toggleAutoSubmit">
<span class="toggle-slider"></span>
</button>
</div>
@ -962,18 +962,18 @@
<!-- 重置設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="settings.advanced">🔧 進階設定</h3>
<h3 class="settings-card-title" data-i18n="settingsUI.advanced">🔧 進階設定</h3>
</div>
<div class="settings-card-body">
<div class="setting-item" style="border-bottom: none;">
<div class="setting-info">
<div class="setting-label" data-i18n="settings.reset">重置設定</div>
<div class="setting-description" data-i18n="settings.resetDesc">
<div class="setting-label" data-i18n="settingsUI.reset">重置設定</div>
<div class="setting-description" data-i18n="settingsUI.resetDesc">
清除所有已保存的設定,恢復到預設狀態
</div>
</div>
<button id="resetSettingsBtn" class="btn btn-secondary" style="font-size: 12px; padding: 6px 16px;">
<span data-i18n="settings.reset">重置設定</span>
<span data-i18n="settingsUI.reset">重置設定</span>
</button>
</div>
</div>