diff --git a/README.md b/README.md index 43aaad5..3a6fa6f 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,6 @@ For best results, add these rules to your AI assistant: |----------|---------|--------|---------| | `FORCE_WEB` | Force use Web UI | `true`/`false` | `false` | | `MCP_DEBUG` | Debug mode | `true`/`false` | `false` | -| `INCLUDE_BASE64_DETAIL` | Full Base64 for images | `true`/`false` | `false` | ### Testing Options ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index 7ea8cc8..872c157 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -128,7 +128,6 @@ uvx mcp-feedback-enhanced@latest test |------|------|-----|------| | `FORCE_WEB` | 强制使用 Web UI | `true`/`false` | `false` | | `MCP_DEBUG` | 调试模式 | `true`/`false` | `false` | -| `INCLUDE_BASE64_DETAIL` | 图片完整 Base64 | `true`/`false` | `false` | ### 测试选项 ```bash diff --git a/README.zh-TW.md b/README.zh-TW.md index 8b56b54..8a79244 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -128,7 +128,6 @@ uvx mcp-feedback-enhanced@latest test |------|------|-----|------| | `FORCE_WEB` | 強制使用 Web UI | `true`/`false` | `false` | | `MCP_DEBUG` | 調試模式 | `true`/`false` | `false` | -| `INCLUDE_BASE64_DETAIL` | 圖片完整 Base64 | `true`/`false` | `false` | ### 測試選項 ```bash diff --git a/src/mcp_feedback_enhanced/__init__.py b/src/mcp_feedback_enhanced/__init__.py index c32453f..ecdc1f2 100644 --- a/src/mcp_feedback_enhanced/__init__.py +++ b/src/mcp_feedback_enhanced/__init__.py @@ -22,19 +22,29 @@ __version__ = "2.2.2" __author__ = "Minidoracat" __email__ = "minidora0702@gmail.com" +import os + from .server import main as run_server -from .gui import feedback_ui # 導入新的 Web UI 模組 from .web import WebUIManager, launch_web_feedback_ui, get_web_ui_manager, stop_web_ui +# 條件性導入 GUI 模組(只有在不強制使用 Web 時才導入) +feedback_ui = None +if not os.getenv('FORCE_WEB', '').lower() in ('true', '1', 'yes'): + try: + from .gui import feedback_ui + except ImportError: + # 如果 GUI 依賴不可用,設為 None + feedback_ui = None + # 主要導出介面 __all__ = [ "run_server", - "feedback_ui", + "feedback_ui", "WebUIManager", "launch_web_feedback_ui", - "get_web_ui_manager", + "get_web_ui_manager", "stop_web_ui", "__version__", "__author__", diff --git a/src/mcp_feedback_enhanced/gui/tabs/feedback_tab.py b/src/mcp_feedback_enhanced/gui/tabs/feedback_tab.py index 0b09f85..7ec7ab6 100644 --- a/src/mcp_feedback_enhanced/gui/tabs/feedback_tab.py +++ b/src/mcp_feedback_enhanced/gui/tabs/feedback_tab.py @@ -106,7 +106,7 @@ class FeedbackTab(QWidget): image_upload_layout.setSpacing(8) image_upload_layout.setContentsMargins(0, 8, 0, 0) # 與回饋輸入區域保持一致的邊距 - self.image_upload = ImageUploadWidget() + self.image_upload = ImageUploadWidget(config_manager=self.config_manager) image_upload_layout.addWidget(self.image_upload, 1) # 添加到分割器 diff --git a/src/mcp_feedback_enhanced/gui/widgets/image_upload.py b/src/mcp_feedback_enhanced/gui/widgets/image_upload.py index 034a101..49aaf48 100644 --- a/src/mcp_feedback_enhanced/gui/widgets/image_upload.py +++ b/src/mcp_feedback_enhanced/gui/widgets/image_upload.py @@ -14,8 +14,9 @@ from typing import Dict, List from pathlib import Path from PySide6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, - QScrollArea, QGridLayout, QFileDialog, QMessageBox, QApplication + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QScrollArea, QGridLayout, QFileDialog, QMessageBox, QApplication, + QComboBox, QCheckBox, QGroupBox, QFrame ) from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QFont, QDragEnterEvent, QDropEvent @@ -30,10 +31,11 @@ from .image_preview import ImagePreviewWidget class ImageUploadWidget(QWidget): """圖片上傳元件""" images_changed = Signal() - - def __init__(self, parent=None): + + def __init__(self, parent=None, config_manager=None): super().__init__(parent) self.images: Dict[str, Dict[str, str]] = {} + self.config_manager = config_manager self._setup_ui() self.setAcceptDrops(True) # 啟動時清理舊的臨時文件 @@ -44,21 +46,104 @@ class ImageUploadWidget(QWidget): layout = QVBoxLayout(self) layout.setSpacing(6) layout.setContentsMargins(0, 8, 0, 8) # 調整邊距使其與其他區域一致 - + # 標題 self.title = QLabel(t('images.title')) self.title.setFont(QFont("", 10, QFont.Bold)) self.title.setStyleSheet("color: #007acc; margin: 1px 0;") layout.addWidget(self.title) - + + # 圖片設定區域 + self._create_settings_area(layout) + # 狀態標籤 self.status_label = QLabel(t('images.status', count=0)) self.status_label.setStyleSheet("color: #9e9e9e; font-size: 10px; margin: 5px 0;") layout.addWidget(self.status_label) - + # 統一的圖片區域(整合按鈕、拖拽、預覽) self._create_unified_image_area(layout) - + + def _create_settings_area(self, layout: QVBoxLayout) -> None: + """創建圖片設定區域""" + if not self.config_manager: + return # 如果沒有 config_manager,跳過設定區域 + + # 設定群組框 + settings_group = QGroupBox(t('images.settings.title')) + settings_group.setStyleSheet(""" + QGroupBox { + font-weight: bold; + font-size: 9px; + color: #9e9e9e; + border: 1px solid #464647; + border-radius: 4px; + margin-top: 6px; + padding-top: 4px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 8px; + padding: 0 4px 0 4px; + } + """) + + settings_layout = QHBoxLayout(settings_group) + settings_layout.setSpacing(12) + settings_layout.setContentsMargins(8, 8, 8, 8) + + # 圖片大小限制設定 + size_label = QLabel(t('images.settings.sizeLimit') + ":") + size_label.setStyleSheet("color: #cccccc; font-size: 9px;") + + self.size_limit_combo = QComboBox() + self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.unlimited'), 0) + self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.1mb'), 1024*1024) + self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.3mb'), 3*1024*1024) + self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.5mb'), 5*1024*1024) + + # 載入當前設定 + current_limit = self.config_manager.get_image_size_limit() + for i in range(self.size_limit_combo.count()): + if self.size_limit_combo.itemData(i) == current_limit: + self.size_limit_combo.setCurrentIndex(i) + break + + self.size_limit_combo.currentIndexChanged.connect(self._on_size_limit_changed) + + # Base64 詳細模式設定 + self.base64_checkbox = QCheckBox(t('images.settings.base64Detail')) + self.base64_checkbox.setChecked(self.config_manager.get_enable_base64_detail()) + self.base64_checkbox.stateChanged.connect(self._on_base64_detail_changed) + self.base64_checkbox.setToolTip(t('images.settings.base64DetailHelp')) + + # Base64 警告標籤 + base64_warning = QLabel(t('images.settings.base64Warning')) + base64_warning.setStyleSheet("color: #ff9800; font-size: 8px;") + + # 添加到佈局 + settings_layout.addWidget(size_label) + settings_layout.addWidget(self.size_limit_combo) + settings_layout.addWidget(self.base64_checkbox) + settings_layout.addWidget(base64_warning) + settings_layout.addStretch() + + layout.addWidget(settings_group) + + def _on_size_limit_changed(self, index: int) -> None: + """圖片大小限制變更處理""" + if self.config_manager: + size_bytes = self.size_limit_combo.itemData(index) + self.config_manager.set_image_size_limit(size_bytes) + debug_log(f"圖片大小限制已更新: {size_bytes} bytes") + + def _on_base64_detail_changed(self, state: int) -> None: + """Base64 詳細模式變更處理""" + if self.config_manager: + enabled = state == Qt.Checked + self.config_manager.set_enable_base64_detail(enabled) + debug_log(f"Base64 詳細模式已更新: {enabled}") + def _create_unified_image_area(self, layout: QVBoxLayout) -> None: """創建統一的圖片區域""" # 創建滾動區域 @@ -327,12 +412,26 @@ class ImageUploadWidget(QWidget): file_size = os.path.getsize(file_path) debug_log(f"文件大小: {file_size} bytes") - - # 更嚴格的大小限制(1MB) - if file_size > 1 * 1024 * 1024: + + # 動態圖片大小限制檢查 + size_limit = self.config_manager.get_image_size_limit() if self.config_manager else 1024*1024 + if size_limit > 0 and file_size > size_limit: + # 格式化限制大小顯示 + if size_limit >= 1024*1024: + limit_str = f"{size_limit/(1024*1024):.0f}MB" + else: + limit_str = f"{size_limit/1024:.0f}KB" + + # 格式化文件大小顯示 + if file_size >= 1024*1024: + size_str = f"{file_size/(1024*1024):.1f}MB" + else: + size_str = f"{file_size/1024:.1f}KB" + QMessageBox.warning( - self, t('errors.warning'), - t('errors.fileSizeExceeded', filename=os.path.basename(file_path), size=f"{file_size/1024/1024:.1f}") + self, t('errors.warning'), + t('images.sizeLimitExceeded', filename=os.path.basename(file_path), size=size_str, limit=limit_str) + + "\n\n" + t('images.sizeLimitExceededAdvice') ) continue @@ -349,11 +448,25 @@ class ImageUploadWidget(QWidget): debug_log(f"讀取的數據為空!") continue - # 再次檢查內存中的數據大小 - if len(raw_data) > 1 * 1024 * 1024: + # 再次檢查內存中的數據大小(使用配置的限制) + size_limit = self.config_manager.get_image_size_limit() if self.config_manager else 1024*1024 + if size_limit > 0 and len(raw_data) > size_limit: + # 格式化限制大小顯示 + if size_limit >= 1024*1024: + limit_str = f"{size_limit/(1024*1024):.0f}MB" + else: + limit_str = f"{size_limit/1024:.0f}KB" + + # 格式化文件大小顯示 + if len(raw_data) >= 1024*1024: + size_str = f"{len(raw_data)/(1024*1024):.1f}MB" + else: + size_str = f"{len(raw_data)/1024:.1f}KB" + QMessageBox.warning( - self, t('errors.warning'), - t('errors.dataSizeExceeded', filename=os.path.basename(file_path)) + self, t('errors.warning'), + t('images.sizeLimitExceeded', filename=os.path.basename(file_path), size=size_str, limit=limit_str) + + "\n\n" + t('images.sizeLimitExceededAdvice') ) continue @@ -506,7 +619,7 @@ class ImageUploadWidget(QWidget): font-size: 11px; } """) - return + return event.ignore() def dragLeaveEvent(self, event) -> None: @@ -564,7 +677,29 @@ class ImageUploadWidget(QWidget): # 更新標題 if hasattr(self, 'title'): self.title.setText(t('images.title')) - + + # 更新設定區域文字 + if hasattr(self, 'size_limit_combo'): + # 保存當前選擇 + current_data = self.size_limit_combo.currentData() + + # 清除並重新添加選項 + self.size_limit_combo.clear() + self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.unlimited'), 0) + self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.1mb'), 1024*1024) + self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.3mb'), 3*1024*1024) + self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.5mb'), 5*1024*1024) + + # 恢復選擇 + for i in range(self.size_limit_combo.count()): + if self.size_limit_combo.itemData(i) == current_data: + self.size_limit_combo.setCurrentIndex(i) + break + + if hasattr(self, 'base64_checkbox'): + self.base64_checkbox.setText(t('images.settings.base64Detail')) + self.base64_checkbox.setToolTip(t('images.settings.base64DetailHelp')) + # 更新按鈕文字 if hasattr(self, 'file_button'): self.file_button.setText(t('buttons.selectFiles')) @@ -572,10 +707,10 @@ class ImageUploadWidget(QWidget): self.paste_button.setText(t('buttons.pasteClipboard')) if hasattr(self, 'clear_button'): self.clear_button.setText(t('buttons.clearAll')) - + # 更新拖拽區域文字 if hasattr(self, 'drop_hint_label'): self.drop_hint_label.setText(t('images.dragHint')) - + # 更新狀態文字 - self._update_status() \ No newline at end of file + self._update_status() \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/gui/window/config_manager.py b/src/mcp_feedback_enhanced/gui/window/config_manager.py index 6ba3bfe..8da869c 100644 --- a/src/mcp_feedback_enhanced/gui/window/config_manager.py +++ b/src/mcp_feedback_enhanced/gui/window/config_manager.py @@ -146,19 +146,38 @@ class ConfigManager: self.update_partial_config({'always_center_window': always_center}) debug_log(f"視窗定位設置: {'總是中心顯示' if always_center else '智能定位'}") + def get_image_size_limit(self) -> int: + """獲取圖片大小限制(bytes),0 表示無限制""" + return self.get('image_size_limit', 0) + + def set_image_size_limit(self, size_bytes: int) -> None: + """設置圖片大小限制(bytes),0 表示無限制""" + self.update_partial_config({'image_size_limit': size_bytes}) + size_mb = size_bytes / (1024 * 1024) if size_bytes > 0 else 0 + debug_log(f"圖片大小限制設置: {'無限制' if size_bytes == 0 else f'{size_mb:.1f}MB'}") + + def get_enable_base64_detail(self) -> bool: + """獲取是否啟用 Base64 詳細模式""" + return self.get('enable_base64_detail', False) + + def set_enable_base64_detail(self, enabled: bool) -> None: + """設置是否啟用 Base64 詳細模式""" + self.update_partial_config({'enable_base64_detail': enabled}) + debug_log(f"Base64 詳細模式設置: {'啟用' if enabled else '停用'}") + def reset_settings(self) -> None: """重置所有設定到預設值""" try: # 清空配置緩存 self._config_cache = {} - + # 刪除配置文件 if self._config_file.exists(): self._config_file.unlink() debug_log("配置文件已刪除") - + debug_log("所有設定已重置到預設值") - + except Exception as e: debug_log(f"重置設定失敗: {e}") - raise \ No newline at end of file + raise \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/gui/window/tab_manager.py b/src/mcp_feedback_enhanced/gui/window/tab_manager.py index 56b03a0..3fa64a9 100644 --- a/src/mcp_feedback_enhanced/gui/window/tab_manager.py +++ b/src/mcp_feedback_enhanced/gui/window/tab_manager.py @@ -271,18 +271,26 @@ class TabManager: result = { "interactive_feedback": "", "command_logs": "", - "images": [] + "images": [], + "settings": {} } - + # 獲取回饋文字和圖片 if self.feedback_tab: result["interactive_feedback"] = self.feedback_tab.get_feedback_text() result["images"] = self.feedback_tab.get_images_data() - + # 獲取命令日誌 if self.command_tab: result["command_logs"] = self.command_tab.get_command_logs() - + + # 獲取圖片設定 + if self.config_manager: + result["settings"] = { + "image_size_limit": self.config_manager.get_image_size_limit(), + "enable_base64_detail": self.config_manager.get_enable_base64_detail() + } + return result def restore_content(self, feedback_text: str, command_logs: str, images_data: list) -> None: diff --git a/src/mcp_feedback_enhanced/locales/en/translations.json b/src/mcp_feedback_enhanced/locales/en/translations.json index 9d06ac3..e18d941 100644 --- a/src/mcp_feedback_enhanced/locales/en/translations.json +++ b/src/mcp_feedback_enhanced/locales/en/translations.json @@ -76,7 +76,24 @@ "paste_failed": "Paste failed, no image in clipboard", "paste_no_image": "No image in clipboard to paste", "paste_image_from_textarea": "Image intelligently pasted from text area to image area", - "images_clear": "Clear all images" + "images_clear": "Clear all images", + "settings": { + "title": "Image Settings", + "sizeLimit": "Image Size Limit", + "sizeLimitOptions": { + "unlimited": "Unlimited", + "1mb": "1MB", + "3mb": "3MB", + "5mb": "5MB" + }, + "base64Detail": "Base64 Compatibility Mode", + "base64DetailHelp": "When enabled, includes complete Base64 image data in text to improve compatibility with AI models like Gemini", + "base64Warning": "⚠️ Increases transmission size", + "compatibilityHint": "💡 Images not recognized correctly?", + "enableBase64Hint": "Try enabling Base64 compatibility mode" + }, + "sizeLimitExceeded": "Image {filename} size is {size}, exceeds {limit} limit!", + "sizeLimitExceededAdvice": "Please compress the image using image editing software, or adjust the image size limit setting." }, "language": { "settings": "Language Settings", diff --git a/src/mcp_feedback_enhanced/locales/zh-CN/translations.json b/src/mcp_feedback_enhanced/locales/zh-CN/translations.json index 17428de..8005fd5 100644 --- a/src/mcp_feedback_enhanced/locales/zh-CN/translations.json +++ b/src/mcp_feedback_enhanced/locales/zh-CN/translations.json @@ -61,7 +61,24 @@ "paste_failed": "粘贴失败,剪贴板中没有图片", "paste_no_image": "剪贴板中没有图片可粘贴", "paste_image_from_textarea": "已将图片从文本框智能贴到图片区域", - "images_clear": "清除所有图片" + "images_clear": "清除所有图片", + "settings": { + "title": "图片设置", + "sizeLimit": "图片大小限制", + "sizeLimitOptions": { + "unlimited": "无限制", + "1mb": "1MB", + "3mb": "3MB", + "5mb": "5MB" + }, + "base64Detail": "Base64 兼容模式", + "base64DetailHelp": "启用后会在文本中包含完整的 Base64 图片数据,提升与 Gemini 等 AI 模型的兼容性", + "base64Warning": "⚠️ 会增加传输量", + "compatibilityHint": "💡 图片无法正确识别?", + "enableBase64Hint": "尝试启用 Base64 兼容模式" + }, + "sizeLimitExceeded": "图片 {filename} 大小为 {size},超过 {limit} 限制!", + "sizeLimitExceededAdvice": "建议使用图片编辑软件压缩后再上传,或调整图片大小限制设置。" }, "settings": { "title": "应用设置", diff --git a/src/mcp_feedback_enhanced/locales/zh-TW/translations.json b/src/mcp_feedback_enhanced/locales/zh-TW/translations.json index 3c093d8..db451e0 100644 --- a/src/mcp_feedback_enhanced/locales/zh-TW/translations.json +++ b/src/mcp_feedback_enhanced/locales/zh-TW/translations.json @@ -77,7 +77,24 @@ "paste_failed": "貼上失敗,剪貼簿中沒有圖片", "paste_no_image": "剪貼簿中沒有圖片可貼上", "paste_image_from_textarea": "已將圖片從文字框智能貼到圖片區域", - "images_clear": "清除所有圖片" + "images_clear": "清除所有圖片", + "settings": { + "title": "圖片設定", + "sizeLimit": "圖片大小限制", + "sizeLimitOptions": { + "unlimited": "無限制", + "1mb": "1MB", + "3mb": "3MB", + "5mb": "5MB" + }, + "base64Detail": "Base64 相容模式", + "base64DetailHelp": "啟用後會在文字中包含完整的 Base64 圖片資料,提升與 Gemini 等 AI 模型的相容性", + "base64Warning": "⚠️ 會增加傳輸量", + "compatibilityHint": "💡 圖片無法正確識別?", + "enableBase64Hint": "嘗試啟用 Base64 相容模式" + }, + "sizeLimitExceeded": "圖片 {filename} 大小為 {size},超過 {limit} 限制!", + "sizeLimitExceededAdvice": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。" }, "settings": { "title": "應用設置", diff --git a/src/mcp_feedback_enhanced/server.py b/src/mcp_feedback_enhanced/server.py index 6cd19a4..466bf15 100644 --- a/src/mcp_feedback_enhanced/server.py +++ b/src/mcp_feedback_enhanced/server.py @@ -281,10 +281,22 @@ def create_feedback_text(feedback_data: dict) -> str: # 如果 AI 助手不支援 MCP 圖片,可以提供完整 base64 debug_log(f"圖片 {i} Base64 已準備,長度: {len(img_base64)}") - # 可選:根據環境變數決定是否包含完整 base64 - include_full_base64 = os.getenv("INCLUDE_BASE64_DETAIL", "").lower() in ("true", "1", "yes", "on") + # 檢查是否啟用 Base64 詳細模式(從 UI 設定中獲取) + include_full_base64 = feedback_data.get("settings", {}).get("enable_base64_detail", False) + if include_full_base64: - img_info += f"\n 完整 Base64: data:image/png;base64,{img_base64}" + # 根據檔案名推斷 MIME 類型 + file_name = img.get("name", "image.png") + if file_name.lower().endswith(('.jpg', '.jpeg')): + mime_type = 'image/jpeg' + elif file_name.lower().endswith('.gif'): + mime_type = 'image/gif' + elif file_name.lower().endswith('.webp'): + mime_type = 'image/webp' + else: + mime_type = 'image/png' + + img_info += f"\n 完整 Base64: data:{mime_type};base64,{img_base64}" except Exception as e: debug_log(f"圖片 {i} Base64 處理失敗: {e}") diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json index aaba1a4..4f23b82 100644 --- a/src/mcp_feedback_enhanced/web/locales/en/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json @@ -171,5 +171,24 @@ "contactDescription": "For technical support, issue reports, or feature suggestions, please contact us through Discord community or GitHub Issues.", "thanks": "Thanks & Contributions", "thanksText": "Thanks to the original author Fábio Ferreira (@fabiomlferreira) for creating the original interactive-feedback-mcp project.\n\nThis enhanced version is developed and maintained by Minidoracat, greatly expanding the project functionality with GUI interface, image support, multi-language capabilities, and many other improvements.\n\nSpecial thanks to sanshao85's mcp-feedback-collector project for UI design inspiration.\n\nOpen source collaboration makes technology better!" + }, + "images": { + "settings": { + "title": "Image Settings", + "sizeLimit": "Image Size Limit", + "sizeLimitOptions": { + "unlimited": "Unlimited", + "1mb": "1MB", + "3mb": "3MB", + "5mb": "5MB" + }, + "base64Detail": "Base64 Compatibility Mode", + "base64DetailHelp": "When enabled, includes full Base64 image data in text, improving compatibility with certain AI models", + "base64Warning": "⚠️ Increases transmission size", + "compatibilityHint": "💡 Images not recognized correctly?", + "enableBase64Hint": "Try enabling Base64 compatibility mode" + }, + "sizeLimitExceeded": "Image {filename} size is {size}, exceeds {limit} limit!", + "sizeLimitExceededAdvice": "Consider compressing the image with editing software before uploading, or adjust the image size limit settings." } } \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json index 7eba251..eebcde0 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json @@ -171,5 +171,24 @@ "contactDescription": "如需技术支持、问题反馈或功能建议,欢迎通过 Discord 社群或 GitHub Issues 与我们联系。", "thanks": "致谢与贡献", "thanksText": "感谢原作者 Fábio Ferreira (@fabiomlferreira) 创建了原始的 interactive-feedback-mcp 项目。\n\n本增强版本由 Minidoracat 开发和维护,大幅扩展了项目功能,新增了 GUI 界面、图片支持、多语言能力以及许多其他改进功能。\n\n同时感谢 sanshao85 的 mcp-feedback-collector 项目提供的 UI 设计灵感。\n\n开源协作让技术变得更美好!" + }, + "images": { + "settings": { + "title": "图片设置", + "sizeLimit": "图片大小限制", + "sizeLimitOptions": { + "unlimited": "无限制", + "1mb": "1MB", + "3mb": "3MB", + "5mb": "5MB" + }, + "base64Detail": "Base64 兼容模式", + "base64DetailHelp": "启用后会在文本中包含完整的 Base64 图片数据,提升与某些 AI 模型的兼容性", + "base64Warning": "⚠️ 会增加传输量", + "compatibilityHint": "💡 图片无法正确识别?", + "enableBase64Hint": "尝试启用 Base64 兼容模式" + }, + "sizeLimitExceeded": "图片 {filename} 大小为 {size},超过 {limit} 限制!", + "sizeLimitExceededAdvice": "建议使用图片编辑软件压缩后再上传,或调整图片大小限制设置。" } } \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json index 72ae971..35a2954 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json @@ -171,5 +171,24 @@ "contactDescription": "如需技術支援、問題回報或功能建議,歡迎透過 Discord 社群或 GitHub Issues 與我們聯繫。", "thanks": "致謝與貢獻", "thanksText": "感謝原作者 Fábio Ferreira (@fabiomlferreira) 創建了原始的 interactive-feedback-mcp 專案。\n\n本增強版本由 Minidoracat 開發和維護,大幅擴展了專案功能,新增了 GUI 介面、圖片支援、多語言能力以及許多其他改進功能。\n\n同時感謝 sanshao85 的 mcp-feedback-collector 專案提供的 UI 設計靈感。\n\n開源協作讓技術變得更美好!" + }, + "images": { + "settings": { + "title": "圖片設定", + "sizeLimit": "圖片大小限制", + "sizeLimitOptions": { + "unlimited": "無限制", + "1mb": "1MB", + "3mb": "3MB", + "5mb": "5MB" + }, + "base64Detail": "Base64 相容模式", + "base64DetailHelp": "啟用後會在文字中包含完整的 Base64 圖片資料,提升與某些 AI 模型的相容性", + "base64Warning": "⚠️ 會增加傳輸量", + "compatibilityHint": "💡 圖片無法正確識別?", + "enableBase64Hint": "嘗試啟用 Base64 相容模式" + }, + "sizeLimitExceeded": "圖片 {filename} 大小為 {size},超過 {limit} 限制!", + "sizeLimitExceededAdvice": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。" } } \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web/models/feedback_session.py b/src/mcp_feedback_enhanced/web/models/feedback_session.py index b83fafb..9294262 100644 --- a/src/mcp_feedback_enhanced/web/models/feedback_session.py +++ b/src/mcp_feedback_enhanced/web/models/feedback_session.py @@ -34,6 +34,7 @@ class WebFeedbackSession: self.websocket: Optional[WebSocket] = None self.feedback_result: Optional[str] = None self.images: List[dict] = [] + self.settings: dict = {} # 圖片設定 self.feedback_completed = threading.Event() self.process: Optional[subprocess.Popen] = None self.command_logs = [] @@ -73,7 +74,8 @@ class WebFeedbackSession: return { "logs": "\n".join(self.command_logs), "interactive_feedback": self.feedback_result or "", - "images": self.images + "images": self.images, + "settings": self.settings } else: # 超時了,立即清理資源 @@ -87,18 +89,21 @@ class WebFeedbackSession: await self._cleanup_resources_on_timeout() raise - async def submit_feedback(self, feedback: str, images: List[dict]): + async def submit_feedback(self, feedback: str, images: List[dict], settings: dict = None): """ 提交回饋和圖片 - + Args: feedback: 文字回饋 images: 圖片列表 + settings: 圖片設定(可選) """ self.feedback_result = feedback + # 先設置設定,再處理圖片(因為處理圖片時需要用到設定) + self.settings = settings or {} self.images = self._process_images(images) self.feedback_completed.set() - + if self.websocket: try: await self.websocket.close() @@ -108,23 +113,26 @@ class WebFeedbackSession: def _process_images(self, images: List[dict]) -> List[dict]: """ 處理圖片數據,轉換為統一格式 - + Args: images: 原始圖片數據列表 - + Returns: List[dict]: 處理後的圖片數據 """ processed_images = [] - + + # 從設定中獲取圖片大小限制,如果沒有設定則使用預設值 + size_limit = self.settings.get('image_size_limit', MAX_IMAGE_SIZE) + for img in images: try: if not all(key in img for key in ["name", "data", "size"]): continue - - # 檢查文件大小 - if img["size"] > MAX_IMAGE_SIZE: - debug_log(f"圖片 {img['name']} 超過大小限制,跳過") + + # 檢查文件大小(只有當限制大於0時才檢查) + if size_limit > 0 and img["size"] > size_limit: + debug_log(f"圖片 {img['name']} 超過大小限制 ({size_limit} bytes),跳過") continue # 解碼 base64 數據 diff --git a/src/mcp_feedback_enhanced/web/routes/main_routes.py b/src/mcp_feedback_enhanced/web/routes/main_routes.py index 910684b..365d7af 100644 --- a/src/mcp_feedback_enhanced/web/routes/main_routes.py +++ b/src/mcp_feedback_enhanced/web/routes/main_routes.py @@ -112,18 +112,20 @@ def setup_routes(manager: 'WebUIManager'): """保存設定到檔案""" try: data = await request.json() - - # 構建設定檔案路徑 - settings_file = Path.cwd() / ".mcp_feedback_settings.json" - + + # 使用與 GUI 版本相同的設定檔案路徑 + config_dir = Path.home() / ".config" / "mcp-feedback-enhanced" + config_dir.mkdir(parents=True, exist_ok=True) + settings_file = config_dir / "ui_settings.json" + # 保存設定到檔案 with open(settings_file, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) - + debug_log(f"設定已保存到: {settings_file}") - + return JSONResponse(content={"status": "success", "message": "設定已保存"}) - + except Exception as e: debug_log(f"保存設定失敗: {e}") return JSONResponse( @@ -135,18 +137,20 @@ def setup_routes(manager: 'WebUIManager'): async def load_settings(): """從檔案載入設定""" try: - settings_file = Path.cwd() / ".mcp_feedback_settings.json" - + # 使用與 GUI 版本相同的設定檔案路徑 + config_dir = Path.home() / ".config" / "mcp-feedback-enhanced" + settings_file = config_dir / "ui_settings.json" + if settings_file.exists(): with open(settings_file, 'r', encoding='utf-8') as f: settings = json.load(f) - + debug_log(f"設定已從檔案載入: {settings_file}") return JSONResponse(content=settings) else: debug_log("設定檔案不存在,返回空設定") return JSONResponse(content={}) - + except Exception as e: debug_log(f"載入設定失敗: {e}") return JSONResponse( @@ -158,16 +162,18 @@ def setup_routes(manager: 'WebUIManager'): async def clear_settings(): """清除設定檔案""" try: - settings_file = Path.cwd() / ".mcp_feedback_settings.json" - + # 使用與 GUI 版本相同的設定檔案路徑 + config_dir = Path.home() / ".config" / "mcp-feedback-enhanced" + settings_file = config_dir / "ui_settings.json" + if settings_file.exists(): settings_file.unlink() debug_log(f"設定檔案已刪除: {settings_file}") else: debug_log("設定檔案不存在,無需刪除") - + return JSONResponse(content={"status": "success", "message": "設定已清除"}) - + except Exception as e: debug_log(f"清除設定失敗: {e}") return JSONResponse( @@ -184,7 +190,8 @@ async def handle_websocket_message(manager: 'WebUIManager', session, data: dict) # 提交回饋 feedback = data.get("feedback", "") images = data.get("images", []) - await session.submit_feedback(feedback, images) + settings = data.get("settings", {}) + await session.submit_feedback(feedback, images, settings) elif message_type == "run_command": # 執行命令 diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index 9fdfa05..a5728d8 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -7,7 +7,7 @@ class PersistentSettings { constructor() { - this.settingsFile = '.mcp_feedback_settings.json'; + this.settingsFile = 'ui_settings.json'; this.storageKey = 'mcp_feedback_settings'; } @@ -91,6 +91,10 @@ class FeedbackApp { this.isConnected = false; // 初始化連接狀態 this.websocket = null; // 初始化 WebSocket this.isHandlingPaste = false; // 防止重複處理貼上事件的標記 + + // 圖片設定 + this.imageSizeLimit = 0; // 0 表示無限制 + this.enableBase64Detail = false; // 立即檢查 DOM 狀態並初始化 if (document.readyState === 'loading') { @@ -363,6 +367,9 @@ class FeedbackApp { if (resetSettingsBtn) { resetSettingsBtn.addEventListener('click', () => this.resetSettings()); } + + // 圖片設定監聽器 + this.setupImageSettingsListeners(); } setupSettingsListeners() { @@ -394,6 +401,118 @@ class FeedbackApp { }); } + setupImageSettingsListeners() { + // 圖片大小限制設定 - 原始分頁 + const imageSizeLimit = document.getElementById('imageSizeLimit'); + if (imageSizeLimit) { + imageSizeLimit.addEventListener('change', (e) => { + this.imageSizeLimit = parseInt(e.target.value); + this.saveSettings(); + this.syncImageSettings(); + }); + } + + // Base64 詳細模式設定 - 原始分頁 + const enableBase64Detail = document.getElementById('enableBase64Detail'); + if (enableBase64Detail) { + enableBase64Detail.addEventListener('change', (e) => { + this.enableBase64Detail = e.target.checked; + this.saveSettings(); + this.syncImageSettings(); + }); + } + + // 圖片大小限制設定 - 合併模式 + const combinedImageSizeLimit = document.getElementById('combinedImageSizeLimit'); + if (combinedImageSizeLimit) { + combinedImageSizeLimit.addEventListener('change', (e) => { + this.imageSizeLimit = parseInt(e.target.value); + this.saveSettings(); + this.syncImageSettings(); + }); + } + + // Base64 詳細模式設定 - 合併模式 + const combinedEnableBase64Detail = document.getElementById('combinedEnableBase64Detail'); + if (combinedEnableBase64Detail) { + combinedEnableBase64Detail.addEventListener('change', (e) => { + this.enableBase64Detail = e.target.checked; + this.saveSettings(); + this.syncImageSettings(); + }); + } + + // 相容性提示按鈕 - 原始分頁 + const enableBase64Hint = document.getElementById('enableBase64Hint'); + if (enableBase64Hint) { + enableBase64Hint.addEventListener('click', () => { + this.enableBase64Detail = true; + this.saveSettings(); + this.syncImageSettings(); + this.hideCompatibilityHint(); + }); + } + + // 相容性提示按鈕 - 合併模式 + const combinedEnableBase64Hint = document.getElementById('combinedEnableBase64Hint'); + if (combinedEnableBase64Hint) { + combinedEnableBase64Hint.addEventListener('click', () => { + this.enableBase64Detail = true; + this.saveSettings(); + this.syncImageSettings(); + this.hideCompatibilityHint(); + }); + } + } + + syncImageSettings() { + // 同步圖片大小限制設定 + const imageSizeLimit = document.getElementById('imageSizeLimit'); + const combinedImageSizeLimit = document.getElementById('combinedImageSizeLimit'); + + if (imageSizeLimit) { + imageSizeLimit.value = this.imageSizeLimit; + } + if (combinedImageSizeLimit) { + combinedImageSizeLimit.value = this.imageSizeLimit; + } + + // 同步 Base64 詳細模式設定 + const enableBase64Detail = document.getElementById('enableBase64Detail'); + const combinedEnableBase64Detail = document.getElementById('combinedEnableBase64Detail'); + + if (enableBase64Detail) { + enableBase64Detail.checked = this.enableBase64Detail; + } + if (combinedEnableBase64Detail) { + combinedEnableBase64Detail.checked = this.enableBase64Detail; + } + } + + showCompatibilityHint() { + const compatibilityHint = document.getElementById('compatibilityHint'); + const combinedCompatibilityHint = document.getElementById('combinedCompatibilityHint'); + + if (compatibilityHint) { + compatibilityHint.style.display = 'flex'; + } + if (combinedCompatibilityHint) { + combinedCompatibilityHint.style.display = 'flex'; + } + } + + hideCompatibilityHint() { + const compatibilityHint = document.getElementById('compatibilityHint'); + const combinedCompatibilityHint = document.getElementById('combinedCompatibilityHint'); + + if (compatibilityHint) { + compatibilityHint.style.display = 'none'; + } + if (combinedCompatibilityHint) { + combinedCompatibilityHint.style.display = 'none'; + } + } + setupTabs() { const tabButtons = document.querySelectorAll('.tab-button'); const tabContents = document.querySelectorAll('.tab-content'); @@ -674,8 +793,27 @@ class FeedbackApp { } addImage(file) { - if (file.size > 1024 * 1024) { // 1MB - alert('圖片大小不能超過 1MB'); + // 檢查圖片大小限制 + if (this.imageSizeLimit > 0 && file.size > this.imageSizeLimit) { + const limitMB = this.imageSizeLimit / (1024 * 1024); + const fileMB = file.size / (1024 * 1024); + + const message = window.i18nManager ? + window.i18nManager.t('images.sizeLimitExceeded', { + filename: file.name, + size: fileMB.toFixed(1) + 'MB', + limit: limitMB.toFixed(0) + 'MB' + }) : + `圖片 ${file.name} 大小為 ${fileMB.toFixed(1)}MB,超過 ${limitMB.toFixed(0)}MB 限制!`; + + const advice = window.i18nManager ? + window.i18nManager.t('images.sizeLimitExceededAdvice') : + '建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。'; + + alert(message + '\n\n' + advice); + + // 顯示相容性提示(如果圖片上傳失敗) + this.showCompatibilityHint(); return; } @@ -847,11 +985,15 @@ class FeedbackApp { type: img.type })); - // 發送回饋 + // 發送回饋(包含圖片設定) this.websocket.send(JSON.stringify({ type: 'submit_feedback', feedback: feedback, - images: imageData + images: imageData, + settings: { + image_size_limit: this.imageSizeLimit, + enable_base64_detail: this.enableBase64Detail + } })); console.log('回饋已提交'); @@ -960,13 +1102,29 @@ class FeedbackApp { this.autoClose = true; // 預設啟用 } } - + // 更新自動關閉開關狀態 const autoCloseToggle = document.getElementById('autoCloseToggle'); if (autoCloseToggle) { autoCloseToggle.classList.toggle('active', this.autoClose); } + // 載入圖片設定 + if (settings.imageSizeLimit !== undefined) { + this.imageSizeLimit = settings.imageSizeLimit; + } else { + this.imageSizeLimit = 0; // 預設無限制 + } + + if (settings.enableBase64Detail !== undefined) { + this.enableBase64Detail = settings.enableBase64Detail; + } else { + this.enableBase64Detail = false; // 預設關閉 + } + + // 同步圖片設定到 UI + this.syncImageSettings(); + // 確保語言選擇器與當前語言同步 this.syncLanguageSelector(); @@ -1053,6 +1211,8 @@ $ `; // 重置本地變數 this.layoutMode = 'separate'; this.autoClose = true; + this.imageSizeLimit = 0; + this.enableBase64Detail = false; // 更新佈局模式單選按鈕狀態 const layoutRadios = document.querySelectorAll('input[name="layoutMode"]'); @@ -1066,6 +1226,9 @@ $ `; autoCloseToggle.classList.toggle('active', this.autoClose); } + // 同步圖片設定到 UI + this.syncImageSettings(); + // 確保語言選擇器與當前語言同步 this.syncLanguageSelector(); @@ -1149,17 +1312,21 @@ $ `; const settings = { layoutMode: this.layoutMode, autoClose: this.autoClose, + imageSizeLimit: this.imageSizeLimit, + enableBase64Detail: this.enableBase64Detail, language: window.i18nManager?.currentLanguage || 'zh-TW', activeTab: localStorage.getItem('activeTab'), lastSaved: new Date().toISOString() }; - + await this.persistentSettings.saveSettings(settings); - + // 同時保存到 localStorage 作為備用(向後兼容) localStorage.setItem('layoutMode', this.layoutMode); localStorage.setItem('autoClose', this.autoClose.toString()); - + localStorage.setItem('imageSizeLimit', this.imageSizeLimit.toString()); + localStorage.setItem('enableBase64Detail', this.enableBase64Detail.toString()); + console.log('設定已保存:', settings); } catch (error) { console.warn('保存設定時發生錯誤:', error); diff --git a/src/mcp_feedback_enhanced/web/static/js/i18n.js b/src/mcp_feedback_enhanced/web/static/js/i18n.js index b27281f..1ae9dce 100644 --- a/src/mcp_feedback_enhanced/web/static/js/i18n.js +++ b/src/mcp_feedback_enhanced/web/static/js/i18n.js @@ -86,10 +86,30 @@ class I18nManager { }; } - // 支援巢狀鍵值的翻譯函數 - t(key, defaultValue = '') { + // 支援巢狀鍵值的翻譯函數,支援參數替換 + t(key, params = {}) { const langData = this.translations[this.currentLanguage] || {}; - return this.getNestedValue(langData, key) || defaultValue || key; + let translation = this.getNestedValue(langData, key); + + // 如果沒有找到翻譯,返回預設值或鍵名 + if (!translation) { + return typeof params === 'string' ? params : key; + } + + // 如果 params 是字串,當作預設值處理(向後相容) + if (typeof params === 'string') { + return translation; + } + + // 參數替換:將 {key} 替換為對應的值 + if (typeof params === 'object' && params !== null) { + Object.keys(params).forEach(paramKey => { + const placeholder = `{${paramKey}}`; + translation = translation.replace(new RegExp(placeholder, 'g'), params[paramKey]); + }); + } + + return translation; } getNestedValue(obj, path) { diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html index 151005b..53af572 100644 --- a/src/mcp_feedback_enhanced/web/templates/feedback.html +++ b/src/mcp_feedback_enhanced/web/templates/feedback.html @@ -798,6 +798,122 @@ gap: 16px; flex: 1; } + + /* 圖片設定樣式 */ + .image-settings-details { + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-tertiary); + margin-bottom: 8px; + } + + .image-settings-summary { + padding: 8px 12px; + cursor: pointer; + font-weight: 500; + color: var(--text-secondary); + font-size: 13px; + user-select: none; + transition: color 0.3s ease; + } + + .image-settings-summary:hover { + color: var(--text-primary); + } + + .image-settings-content { + padding: 12px; + border-top: 1px solid var(--border-color); + background: var(--bg-secondary); + } + + .image-setting-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + gap: 12px; + } + + .image-setting-row:last-of-type { + margin-bottom: 8px; + } + + .image-setting-label { + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + } + + .image-setting-select { + background: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + min-width: 80px; + } + + .image-setting-checkbox-container { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 13px; + } + + .image-setting-checkbox { + width: 16px; + height: 16px; + accent-color: var(--accent-color); + } + + .image-setting-help { + color: var(--warning-color); + font-size: 11px; + margin-left: auto; + } + + .image-setting-help-text { + color: var(--text-secondary); + font-size: 11px; + line-height: 1.4; + margin-top: 4px; + padding: 8px; + background: var(--bg-primary); + border-radius: 4px; + border: 1px solid var(--border-color); + } + + /* 相容性提示樣式 */ + .compatibility-hint { + background: rgba(33, 150, 243, 0.1); + border: 1px solid var(--info-color); + border-radius: 6px; + padding: 8px 12px; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 12px; + font-size: 13px; + color: var(--info-color); + } + + .compatibility-hint-btn { + background: var(--info-color); + color: white; + border: none; + border-radius: 4px; + padding: 4px 8px; + font-size: 11px; + cursor: pointer; + transition: background 0.3s ease; + } + + .compatibility-hint-btn:hover { + background: #1976d2; + }
@@ -859,8 +975,44 @@ > + +