Merge pull request #19 from Minidoracat/13-image-limit

 新增圖片設定功能,包括圖片大小限制和 Base64 詳細模式選項,並更新相關文檔與界面。
This commit is contained in:
Minidoracat 2025-06-04 21:38:02 +08:00 committed by GitHub
commit f4d101a881
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 761 additions and 82 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -22,12 +22,22 @@ __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",

View File

@ -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)
# 添加到分割器

View File

@ -15,7 +15,8 @@ from pathlib import Path
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QScrollArea, QGridLayout, QFileDialog, QMessageBox, QApplication
QScrollArea, QGridLayout, QFileDialog, QMessageBox, QApplication,
QComboBox, QCheckBox, QGroupBox, QFrame
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QDragEnterEvent, QDropEvent
@ -31,9 +32,10 @@ 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)
# 啟動時清理舊的臨時文件
@ -51,6 +53,9 @@ class ImageUploadWidget(QWidget):
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;")
@ -59,6 +64,86 @@ class ImageUploadWidget(QWidget):
# 統一的圖片區域(整合按鈕、拖拽、預覽)
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:
"""創建統一的圖片區域"""
# 創建滾動區域
@ -328,11 +413,25 @@ 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}")
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))
t('images.sizeLimitExceeded', filename=os.path.basename(file_path), size=size_str, limit=limit_str) +
"\n\n" + t('images.sizeLimitExceededAdvice')
)
continue
@ -565,6 +678,28 @@ 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'))

View File

@ -146,6 +146,25 @@ 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:
"""獲取圖片大小限制bytes0 表示無限制"""
return self.get('image_size_limit', 0)
def set_image_size_limit(self, size_bytes: int) -> None:
"""設置圖片大小限制bytes0 表示無限制"""
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:

View File

@ -271,7 +271,8 @@ class TabManager:
result = {
"interactive_feedback": "",
"command_logs": "",
"images": []
"images": [],
"settings": {}
}
# 獲取回饋文字和圖片
@ -283,6 +284,13 @@ class TabManager:
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:

View File

@ -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",

View File

@ -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": "应用设置",

View File

@ -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": "應用設置",

View File

@ -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}")

View File

@ -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."
}
}

View File

@ -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": "建议使用图片编辑软件压缩后再上传,或调整图片大小限制设置。"
}
}

View File

@ -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": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。"
}
}

View File

@ -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,15 +89,18 @@ 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()
@ -117,14 +122,17 @@ class WebFeedbackSession:
"""
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 數據

View File

@ -113,8 +113,10 @@ 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:
@ -135,7 +137,9 @@ 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:
@ -158,7 +162,9 @@ 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()
@ -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":
# 執行命令

View File

@ -7,7 +7,7 @@
class PersistentSettings {
constructor() {
this.settingsFile = '.mcp_feedback_settings.json';
this.settingsFile = 'ui_settings.json';
this.storageKey = 'mcp_feedback_settings';
}
@ -92,6 +92,10 @@ class FeedbackApp {
this.websocket = null; // 初始化 WebSocket
this.isHandlingPaste = false; // 防止重複處理貼上事件的標記
// 圖片設定
this.imageSizeLimit = 0; // 0 表示無限制
this.enableBase64Detail = false;
// 立即檢查 DOM 狀態並初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
@ -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('回饋已提交');
@ -967,6 +1109,22 @@ class FeedbackApp {
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,6 +1312,8 @@ $ `;
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()
@ -1159,6 +1324,8 @@ $ `;
// 同時保存到 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) {

View File

@ -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) {

View File

@ -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;
}
</style>
</head>
<body>
@ -859,8 +975,44 @@
></textarea>
</div>
<!-- 圖片設定區域 -->
<div class="input-group" style="margin-bottom: 12px;">
<details class="image-settings-details">
<summary class="image-settings-summary" data-i18n="images.settings.title">⚙️ 圖片設定</summary>
<div class="image-settings-content">
<div class="image-setting-row">
<label class="image-setting-label" data-i18n="images.settings.sizeLimit">圖片大小限制:</label>
<select id="imageSizeLimit" class="image-setting-select">
<option value="0" data-i18n="images.settings.sizeLimitOptions.unlimited">無限制</option>
<option value="1048576" data-i18n="images.settings.sizeLimitOptions.1mb">1MB</option>
<option value="3145728" data-i18n="images.settings.sizeLimitOptions.3mb">3MB</option>
<option value="5242880" data-i18n="images.settings.sizeLimitOptions.5mb">5MB</option>
</select>
</div>
<div class="image-setting-row">
<label class="image-setting-checkbox-container">
<input type="checkbox" id="enableBase64Detail" class="image-setting-checkbox">
<span class="image-setting-checkmark"></span>
<span class="image-setting-label" data-i18n="images.settings.base64Detail">Base64 相容模式</span>
</label>
<small class="image-setting-help" data-i18n="images.settings.base64Warning">⚠️ 會增加傳輸量</small>
</div>
<div class="image-setting-help-text" data-i18n="images.settings.base64DetailHelp">
啟用後會在文字中包含完整的 Base64 圖片資料,提升與 Gemini 等 AI 模型的相容性
</div>
</div>
</details>
</div>
<div class="input-group">
<label class="input-label" data-i18n="feedback.imageLabel">圖片附件(可選)</label>
<!-- 相容性提示區域 -->
<div id="compatibilityHint" class="compatibility-hint" style="display: none;">
<span data-i18n="images.settings.compatibilityHint">💡 圖片無法正確識別?</span>
<button type="button" id="enableBase64Hint" class="compatibility-hint-btn" data-i18n="images.settings.enableBase64Hint">
嘗試啟用 Base64 相容模式
</button>
</div>
<div id="imageUploadArea" class="image-upload-area">
<div id="imageUploadText" data-i18n="feedback.imageUploadText">
📎 點擊選擇圖片或拖放圖片到此處<br>
@ -951,8 +1103,44 @@
></textarea>
</div>
<!-- 圖片設定區域 -->
<div class="input-group" style="margin-bottom: 12px;">
<details class="image-settings-details">
<summary class="image-settings-summary" data-i18n="images.settings.title">⚙️ 圖片設定</summary>
<div class="image-settings-content">
<div class="image-setting-row">
<label class="image-setting-label" data-i18n="images.settings.sizeLimit">圖片大小限制:</label>
<select id="combinedImageSizeLimit" class="image-setting-select">
<option value="0" data-i18n="images.settings.sizeLimitOptions.unlimited">無限制</option>
<option value="1048576" data-i18n="images.settings.sizeLimitOptions.1mb">1MB</option>
<option value="3145728" data-i18n="images.settings.sizeLimitOptions.3mb">3MB</option>
<option value="5242880" data-i18n="images.settings.sizeLimitOptions.5mb">5MB</option>
</select>
</div>
<div class="image-setting-row">
<label class="image-setting-checkbox-container">
<input type="checkbox" id="combinedEnableBase64Detail" class="image-setting-checkbox">
<span class="image-setting-checkmark"></span>
<span class="image-setting-label" data-i18n="images.settings.base64Detail">Base64 相容模式</span>
</label>
<small class="image-setting-help" data-i18n="images.settings.base64Warning">⚠️ 會增加傳輸量</small>
</div>
<div class="image-setting-help-text" data-i18n="images.settings.base64DetailHelp">
啟用後會在文字中包含完整的 Base64 圖片資料,提升與 Gemini 等 AI 模型的相容性
</div>
</div>
</details>
</div>
<div class="input-group">
<label class="input-label" data-i18n="feedback.imageLabel">圖片附件(可選)</label>
<!-- 相容性提示區域 -->
<div id="combinedCompatibilityHint" class="compatibility-hint" style="display: none;">
<span data-i18n="images.settings.compatibilityHint">💡 圖片無法正確識別?</span>
<button type="button" id="combinedEnableBase64Hint" class="compatibility-hint-btn" data-i18n="images.settings.enableBase64Hint">
嘗試啟用 Base64 相容模式
</button>
</div>
<div id="combinedImageUploadArea" class="image-upload-area" style="min-height: 100px;">
<div id="combinedImageUploadText" data-i18n="feedback.imageUploadText">
📎 點擊選擇圖片或拖放圖片到此處<br>