diff --git a/AI_TEST_INSTRUCTIONS.md b/AI_TEST_INSTRUCTIONS.md new file mode 100644 index 0000000..bed5ed6 --- /dev/null +++ b/AI_TEST_INSTRUCTIONS.md @@ -0,0 +1,147 @@ +# AI端图片数据接收测试说明 + +## 📋 测试目的 +验证AI端是否能正确接收到通过MCP协议传输的图片数据,以及数据格式是否符合标准。 + +## 🚀 快速测试(推荐) + +### 方法1:使用修复版测试脚本 +```bash +# 1. 进入项目目录 +cd /Users/lizhenmin/Documents/Cline/MCP/mcp-feedback-enhanced + +# 2. 激活虚拟环境 +source venv/bin/activate + +# 3. 运行修复版测试(解决大数据传输问题) +python simple_ai_test_fixed.py +``` + +### 方法2:使用完整模拟器 +```bash +# 1. 进入项目目录 +cd /Users/lizhenmin/Documents/Cline/MCP/mcp-feedback-enhanced + +# 2. 激活虚拟环境 +source venv/bin/activate + +# 3. 运行完整模拟器 +python simulate_ai_client.py +``` + +## 📊 测试流程 + +1. **脚本启动**:自动启动MCP服务器进程 +2. **连接初始化**:建立MCP协议连接 +3. **工具调用**:AI调用`interactive_feedback`工具 +4. **等待数据**:等待60-120秒接收用户反馈 +5. **数据分析**:分析接收到的图片数据 +6. **结果报告**:生成详细的测试报告 + +## 🔍 测试期间操作 + +当脚本显示"等待用户反馈"时,你可以: + +1. **通过Augment Code界面**上传图片 +2. **通过其他MCP客户端**发送图片数据 +3. **等待超时**查看基础功能 + +## 📈 预期结果 + +### ✅ 成功情况 +``` +🎉 发现图片! + MIME: image/png + Base64长度: 99,328 字符 + 文件大小: 72.7 KB + 格式: ✅ PNG + 保存为: ai_test_image_1.png + +📊 测试结果: + 文本项目: 1 + 图片项目: 1 + +🎉 成功!AI端接收到了 1 张图片 +✅ 图片数据传输正常 +✅ MCP协议工作正常 +``` + +### ⚠️ 超时情况 +``` +📊 测试结果: + 文本项目: 1 + 图片项目: 0 + +⚠️ 没有接收到图片数据 +💡 可能是超时或没有上传图片 +``` + +## 🔧 验证要点 + +脚本会自动验证以下内容: + +### 1. MCP协议格式 +- ✅ `type: "image"` +- ✅ `data: "base64字符串"` +- ✅ `mimeType: "image/png"` + +### 2. 数据完整性 +- ✅ Base64解码成功 +- ✅ 文件头格式正确 +- ✅ 文件大小合理 + +### 3. 图片格式支持 +- ✅ PNG格式 +- ✅ JPEG格式 +- ✅ GIF格式 +- ✅ 其他格式检测 + +## 📁 输出文件 + +测试成功时会生成: +- `ai_test_image_1.png` - 接收到的第一张图片 +- `ai_test_image_2.png` - 接收到的第二张图片 +- 等等... + +## 🐛 故障排除 + +### 问题1:服务器启动失败 +```bash +# 检查虚拟环境 +source venv/bin/activate +python -c "import mcp_feedback_enhanced; print('OK')" +``` + +### 问题2:连接超时 +```bash +# 检查端口占用 +lsof -i :8765 +``` + +### 问题3:没有接收到图片 +- 确认在等待期间上传了图片 +- 检查图片格式是否支持 +- 查看控制台错误信息 + +## 📝 测试记录 + +建议记录以下信息: +- [ ] 测试时间 +- [ ] 上传的图片格式和大小 +- [ ] 接收到的数据项数量 +- [ ] Base64数据长度 +- [ ] 解码后的文件大小 +- [ ] 是否生成了测试文件 + +## 🎯 测试目标 + +通过这个测试,我们要验证: +1. ✅ MCP服务器能正确处理图片数据 +2. ✅ AI端能接收到完整的图片数据 +3. ✅ 数据格式符合MCP ImageContent标准 +4. ✅ Base64编码/解码工作正常 +5. ✅ 图片文件可以正确重建 + +--- + +**准备好了吗?运行测试脚本开始验证吧!** 🚀 diff --git a/README.md b/README.md index dd3b510..d3862ee 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ follow mcp-feedback-enhanced instructions | `MCP_WEB_PORT` | Web UI port | `1024-65535` | `8765` | | `MCP_DESKTOP_MODE` | Desktop application mode | `true`/`false` | `false` | | `MCP_LANGUAGE` | Force UI language | `zh-TW`/`zh-CN`/`en` | Auto-detect | +| `MCP_AI_CLIENT` | AI client type identification | `cursor`/`augment`/custom | `cursor` | **`MCP_WEB_HOST` Explanation**: - `127.0.0.1` (default): Local access only, higher security @@ -192,6 +193,17 @@ follow mcp-feedback-enhanced instructions 4. System default language 5. Fallback to default language (Traditional Chinese) +**`MCP_AI_CLIENT` Explanation**: +- Used to identify the AI client type for customized response formats +- **Default**: `cursor` (standard MCP protocol) +- When set to `cursor`: Uses standard MCP protocol with separate text and image content (recommended for Cursor, Cline, Windsurf) +- When set to `augment`: Returns text content with embedded base64 image data for easy JavaScript extraction (recommended for Augment) +- When set to other values: Uses standard MCP protocol with separate text and image content +- Augment format includes special markers for programmatic parsing: + - Text content: Standard feedback text + - Image data: Embedded as `data:mime/type;base64,` with `---END_IMAGE_N---` markers + - Extraction-friendly format for client-side processing + ### Testing Options ```bash # Version check @@ -414,3 +426,9 @@ MIT License - See [LICENSE](LICENSE) file for details --- **🌟 Welcome to Star and share with more developers!** + + + +MCP_WEB_PORT=8765 MCP_AI_CLIENT=augment MCP_DEBUG=true \ +uv run --directory "/Users/lizhenmin/Documents/Cline/MCP/mcp-feedback-enhanced" \ +python -m mcp_feedback_enhanced 2>/tmp/mcp-fe.log \ No newline at end of file diff --git a/README.zh-CN.md b/README.zh-CN.md index 0e738f4..7e41c37 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -174,6 +174,7 @@ pip install uv | `MCP_WEB_PORT` | Web UI 端口 | `1024-65535` | `8765` | | `MCP_DESKTOP_MODE` | 桌面应用程序模式 | `true`/`false` | `false` | | `MCP_LANGUAGE` | 强制指定界面语言 | `zh-TW`/`zh-CN`/`en` | 自动检测 | +| `MCP_AI_CLIENT` | AI 客户端类型识别 | `cursor`/`augment`/自定义 | `cursor` | **`MCP_WEB_HOST` 说明**: - `127.0.0.1`(默认):仅本地访问,安全性较高 @@ -192,6 +193,17 @@ pip install uv 4. 系统默认语言 5. 回退到默认语言(繁体中文) +**`MCP_AI_CLIENT` 说明**: +- 用于识别 AI 客户端类型,提供定制化的响应格式 +- **默认值**: `cursor`(标准 MCP 协议) +- 当设置为 `cursor` 时:使用标准 MCP 协议,文本和图片内容分别传输(推荐用于 Cursor、Cline、Windsurf) +- 当设置为 `augment` 时:返回包含嵌入式 base64 图片数据的文本内容,便于 JavaScript 提取(推荐用于 Augment) +- 当设置为其他值时:使用标准 MCP 协议,文本和图片内容分别传输 +- Augment 格式包含特殊标记便于程序化解析: + - 文本内容:标准反馈文本 + - 图片数据:嵌入为 `data:mime/type;base64,` 格式,带有 `---END_IMAGE_N---` 标记 + - 便于客户端提取的格式 + ### 测试选项 ```bash # 版本查询 diff --git a/README.zh-TW.md b/README.zh-TW.md index bfa3e0d..48da77b 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -174,6 +174,7 @@ pip install uv | `MCP_WEB_PORT` | Web UI 端口 | `1024-65535` | `8765` | | `MCP_DESKTOP_MODE` | 桌面應用程式模式 | `true`/`false` | `false` | | `MCP_LANGUAGE` | 強制指定介面語言 | `zh-TW`/`zh-CN`/`en` | 自動偵測 | +| `MCP_AI_CLIENT` | AI 客戶端類型識別 | `cursor`/`augment`/自訂 | `cursor` | **`MCP_WEB_HOST` 說明**: - `127.0.0.1`(預設):僅本地存取,安全性較高 @@ -183,7 +184,7 @@ pip install uv - 用於強制指定介面語言,覆蓋系統自動偵測 - 支援的語言代碼: - `zh-TW`:繁體中文 - - `zh-CN`:簡體中文 + - `zh-CN`:簡體中文 - `en`:英文 - 語言偵測優先順序: 1. 用戶在介面中保存的語言設定(最高優先級) @@ -192,6 +193,17 @@ pip install uv 4. 系統預設語言 5. 回退到預設語言(繁體中文) +**`MCP_AI_CLIENT` 說明**: +- 用於識別 AI 客戶端類型,提供客製化的回應格式 +- **預設值**: `cursor`(標準 MCP 協定) +- 當設定為 `cursor` 時:使用標準 MCP 協定,文字和圖片內容分別傳輸(推薦用於 Cursor、Cline、Windsurf) +- 當設定為 `augment` 時:回傳包含嵌入式 base64 圖片資料的文字內容,便於 JavaScript 提取(推薦用於 Augment) +- 當設定為其他值時:使用標準 MCP 協定,文字和圖片內容分別傳輸 +- Augment 格式包含特殊標記便於程式化解析: + - 文字內容:標準回饋文字 + - 圖片資料:嵌入為 `data:mime/type;base64,` 格式,帶有 `---END_IMAGE_N---` 標記 + - 便於客戶端提取的格式 + ### 測試選項 ```bash # 版本查詢 diff --git a/detect_mcp_version.py b/detect_mcp_version.py new file mode 100644 index 0000000..7b06beb --- /dev/null +++ b/detect_mcp_version.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +检测MCP客户端协议版本 +""" + +import sys +from pathlib import Path + +# 添加项目路径 +sys.path.insert(0, str(Path(__file__).parent / "src")) + +from mcp.types import TextContent, ImageContent + + +def detect_mcp_version(): + """检测MCP协议版本和客户端支持情况""" + print("🔍 检测MCP协议版本和客户端支持") + + # 1. 检查MCP types模块 + print("\n1️⃣ 检查MCP types模块") + try: + import mcp.types as types + print(f" mcp.types模块: ✅ 可用") + + # 检查可用的类型 + available_types = [] + for attr_name in dir(types): + if not attr_name.startswith('_'): + attr = getattr(types, attr_name) + if isinstance(attr, type): + available_types.append(attr_name) + + print(f" 可用类型: {', '.join(sorted(available_types))}") + + # 检查关键类型 + key_types = ['TextContent', 'ImageContent', 'EmbeddedResource', 'BlobResourceContents'] + for key_type in key_types: + if hasattr(types, key_type): + print(f" {key_type}: ✅ 支持") + else: + print(f" {key_type}: ❌ 不支持") + + except ImportError as e: + print(f" mcp.types模块: ❌ 导入失败 - {e}") + return False + + # 2. 测试ImageContent创建 + print("\n2️⃣ 测试ImageContent创建") + try: + test_image = ImageContent( + type="image", + data="iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1Pe", + mimeType="image/png" + ) + print(f" ImageContent创建: ✅ 成功") + print(f" 类型: {type(test_image)}") + print(f" 属性: type={test_image.type}, mimeType={test_image.mimeType}") + + # 检查序列化 + if hasattr(test_image, 'model_dump'): + dump = test_image.model_dump() + print(f" 序列化方法: model_dump() ✅") + print(f" 序列化结果: {list(dump.keys())}") + elif hasattr(test_image, 'dict'): + dump = test_image.dict() + print(f" 序列化方法: dict() ✅") + print(f" 序列化结果: {list(dump.keys())}") + else: + print(f" 序列化方法: ❌ 无标准方法") + + except Exception as e: + print(f" ImageContent创建: ❌ 失败 - {e}") + return False + + # 3. 检查FastMCP版本 + print("\n3️⃣ 检查FastMCP版本") + try: + from fastmcp import FastMCP + print(f" FastMCP: ✅ 可用") + + # 尝试获取版本信息 + if hasattr(FastMCP, '__version__'): + print(f" 版本: {FastMCP.__version__}") + else: + print(f" 版本: 未知") + + except ImportError as e: + print(f" FastMCP: ❌ 导入失败 - {e}") + + # 4. 检查MCP协议特性 + print("\n4️⃣ 检查MCP协议特性") + + # 检查是否支持2025-06-18特性 + features_2025_06_18 = { + "ImageContent": hasattr(types, 'ImageContent'), + "EmbeddedResource": hasattr(types, 'EmbeddedResource'), + "BlobResourceContents": hasattr(types, 'BlobResourceContents'), + } + + supported_features = sum(features_2025_06_18.values()) + total_features = len(features_2025_06_18) + + print(f" 2025-06-18特性支持: {supported_features}/{total_features}") + for feature, supported in features_2025_06_18.items(): + status = "✅" if supported else "❌" + print(f" {feature}: {status}") + + # 5. 协议版本推断 + print("\n5️⃣ 协议版本推断") + + if supported_features == total_features: + version_estimate = "2025-06-18或更新" + compatibility = "完全兼容" + image_support = "应该完全支持ImageContent" + elif supported_features >= total_features * 0.7: + version_estimate = "接近2025-06-18" + compatibility = "部分兼容" + image_support = "可能部分支持ImageContent" + else: + version_estimate = "2025-06-18之前" + compatibility = "有限兼容" + image_support = "可能不支持ImageContent显示" + + print(f" 估计协议版本: {version_estimate}") + print(f" 兼容性: {compatibility}") + print(f" 图片支持: {image_support}") + + # 6. 建议 + print("\n6️⃣ 建议") + + if supported_features == total_features: + print(" ✅ 您的MCP环境支持最新特性") + print(" 💡 如果图片仍无法显示,问题可能在客户端渲染层") + print(" 🔧 建议:检查MCP客户端的ImageContent处理实现") + else: + print(" ⚠️ 您的MCP环境可能不完全支持最新特性") + print(" 💡 建议升级到最新版本的MCP Python SDK") + print(" 🔧 或者使用向后兼容的文本格式传输图片信息") + + return True + + +if __name__ == "__main__": + success = detect_mcp_version() + sys.exit(0 if success else 1) diff --git a/examples/mcp-config-augment.json b/examples/mcp-config-augment.json new file mode 100644 index 0000000..72fa00b --- /dev/null +++ b/examples/mcp-config-augment.json @@ -0,0 +1,17 @@ +{ + "mcpServers": { + "mcp-feedback-enhanced": { + "command": "uvx", + "args": ["mcp-feedback-enhanced@latest"], + "timeout": 600, + "env": { + "MCP_AI_CLIENT": "augment", + "MCP_WEB_HOST": "127.0.0.1", + "MCP_WEB_PORT": "8765", + "MCP_DEBUG": "false", + "MCP_LANGUAGE": "en" + }, + "autoApprove": ["interactive_feedback"] + } + } +} diff --git a/pyproject.toml b/pyproject.toml index 90a5696..c0650bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "jinja2>=3.1.0", "websockets>=13.0.0", "aiohttp>=3.8.0", + "pillow>=11.2.1", "mcp>=1.9.3", ] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7b9c906 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# Runtime image compression dependency +Pillow>=11.2.1 diff --git a/simulate_ai_client.py b/simulate_ai_client.py new file mode 100644 index 0000000..af69ca5 --- /dev/null +++ b/simulate_ai_client.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +""" +模拟AI客户端 - 通过MCP标准协议连接 +这个版本通过MCP标准协议(JSON-RPC over stdin/stdout)与服务器通信。 + +使用方法: +1. 运行此脚本:python simulate_ai_client.py +2. 脚本会启动MCP服务器子进程并通过标准协议通信 +3. 调用真正的interactive_feedback工具 +4. 分析接收到的真实MCP数据并显示详细报告 + +功能特点: +- ✅ 真正的MCP协议通信(JSON-RPC over stdin/stdout) +- ✅ 调用真实的interactive_feedback工具 +- ✅ 获取真实的MCP响应数据 +- ✅ 完整的数据分析和报告 +- ✅ 支持图片数据的Base64解码和验证 + +注意: +- 使用MCP标准协议,与VSCode中的AI工具使用相同的通信方式 +- 会启动独立的MCP服务器子进程进行测试 +- 获得的是真实的工具调用响应数据 +""" + +import asyncio +import json +import base64 +import subprocess +import sys +import time +from pathlib import Path +import logging +import argparse +import os + +class AIClient: + def __init__(self): + self.process = None + self.request_id = 0 + + async def start_mcp_server(self): + """启动MCP服务器子进程""" + try: + print("🚀 启动MCP服务器子进程...") + + # 设置环境变量 + env = os.environ.copy() + env["PYTHONPATH"] = str(Path(__file__).parent / "src") + + # 启动MCP服务器 + self.process = await asyncio.create_subprocess_exec( + sys.executable, "-m", "mcp_feedback_enhanced", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=env, + cwd=str(Path(__file__).parent), + limit=1024*1024*10 # 10MB缓冲区限制 + ) + + print("✅ MCP服务器子进程已启动") + + # 等待服务器初始化 + await asyncio.sleep(1) + return True + + except Exception as e: + print(f"❌ 启动MCP服务器失败: {e}") + return False + + async def send_request(self, method, params=None): + """发送JSON-RPC请求到MCP服务器""" + if not self.process: + raise Exception("MCP服务器未启动") + + self.request_id += 1 + request = { + "jsonrpc": "2.0", + "id": self.request_id, + "method": method + } + if params: + request["params"] = params + + request_json = json.dumps(request) + "\n" + print(f"📤 发送请求: {method}") + + try: + # 发送请求 + self.process.stdin.write(request_json.encode()) + await self.process.stdin.drain() + + # 读取响应 + response_line = await self.process.stdout.readline() + if not response_line: + raise Exception("服务器没有响应") + + response = json.loads(response_line.decode().strip()) + print(f"📥 收到响应: {response.get('result', {}).get('serverInfo', {}).get('name', 'OK')}") + + return response + + except Exception as e: + print(f"❌ 发送请求失败: {e}") + raise + + async def call_interactive_feedback(self, arguments): + """调用interactive_feedback工具""" + project_directory = arguments.get("project_directory", ".") + summary = arguments.get("summary", "AI客户端测试") + timeout = arguments.get("timeout", 120) + + print(f"🛠️ 调用interactive_feedback工具") + print(f" 项目目录: {project_directory}") + print(f" 摘要: {summary}") + print(f" 超时: {timeout}秒") + + # 调用tools/call方法 + response = await self.send_request("tools/call", { + "name": "interactive_feedback", + "arguments": { + "project_directory": project_directory, + "summary": summary, + "timeout": timeout + } + }) + + return response + + + + + async def initialize_connection(self): + """初始化MCP连接""" + print("\n🔧 初始化MCP连接...") + + # 发送初始化请求 + init_response = await self.send_request("initialize", { + "protocolVersion": "2025-06-18", + "capabilities": {"tools": {}}, + "clientInfo": { + "name": "AI-Client-Simulator", + "version": "1.0.0" + } + }) + + # 发送initialized通知 + await self.send_notification("notifications/initialized") + + server_name = init_response.get('result', {}).get('serverInfo', {}).get('name', 'Unknown') + print(f"✅ 连接成功: {server_name}") + + async def send_notification(self, method, params=None): + """发送通知(不需要响应)""" + if not self.process: + raise Exception("MCP服务器未启动") + + notification = { + "jsonrpc": "2.0", + "method": method + } + if params: + notification["params"] = params + + notification_json = json.dumps(notification) + "\n" + print(f"📤 发送通知: {method}") + + self.process.stdin.write(notification_json.encode()) + await self.process.stdin.drain() + + async def call_interactive_feedback(self, arguments): + """调用interactive_feedback工具""" + timeout = arguments.get("timeout", 120) + print(f"\n🤖 AI请求用户反馈(等待{timeout}秒)...") + print("💡 现在可以通过其他方式上传图片进行测试") + + start_time = time.time() + + response = await self.send_request("tools/call", { + "name": "interactive_feedback", + "arguments": arguments + }) + + end_time = time.time() + elapsed = end_time - start_time + print(f"⏱️ 实际等待时间: {elapsed:.1f}秒") + + return response + + def analyze_received_data(self, response): + """详细分析AI端接收到的数据""" + print("\n" + "="*60) + print("🔍 AI端数据接收分析报告") + print("="*60) + + if "result" not in response: + print("❌ 响应格式错误:没有result字段") + return False + + result = response["result"] + + # 获取内容数据 + if "content" not in result: + print("❌ 没有找到content字段") + return False + + content = result["content"] + print(f"📊 在content字段中发现 {len(content)} 个数据项") + + # 统计数据 + text_items = [] + image_items = [] + other_items = [] + + # 分析每个数据项 + for i, item in enumerate(content, 1): + print(f"\n📋 数据项 {i}:") + + if isinstance(item, dict): + item_type = item.get("type", "unknown") + print(f" 类型: {item_type}") + + if item_type == "text": + text_items.append(item) + text_content = item.get("text", "") + print(f" 文本长度: {len(text_content)} 字符") + + # 显示完整文本内容 + print(f" 内容: {text_content}") + + elif item_type == "image": + image_items.append(item) + self.analyze_image_data(item, i) + + else: + other_items.append(item) + print(f" ⚠️ 未知类型: {item_type}") + else: + other_items.append(item) + print(f" ⚠️ 非字典格式: {type(item)}") + + # 生成分析报告 + self.generate_analysis_report(text_items, image_items, other_items, result) + + return len(image_items) > 0 + + def analyze_image_data(self, image_item, index): + """详细分析图片数据""" + print(f" 🎉 发现图片数据!") + + mime_type = image_item.get("mimeType", "unknown") + data = image_item.get("data", "") + + print(f" MIME类型: {mime_type}") + print(f" Base64长度: {len(data):,} 字符") + + if len(data) > 0: + print(f" Base64完整内容: {data}") + + # 尝试解码Base64 + try: + decoded = base64.b64decode(data) + file_size = len(decoded) + print(f" 解码后大小: {file_size:,} bytes ({file_size/1024:.1f} KB)") + + # 检测文件格式 + format_info = self.detect_image_format(decoded) + print(f" 文件格式: {format_info}") + + # 验证数据完整性 + if file_size > 0: + print(f" 数据完整性: ✅ 完整") + else: + print(f" 数据完整性: ❌ 空数据") + + # 保存测试文件(可选) + test_file = f"/tmp/ai_received_image_{index}.{self.get_file_extension(decoded)}" + try: + with open(test_file, "wb") as f: + f.write(decoded) + print(f" 测试文件: {test_file}") + except: + pass + + except Exception as e: + print(f" Base64解码: ❌ 失败 - {e}") + else: + print(f" ⚠️ Base64数据为空") + + # 检查annotations + annotations = image_item.get("annotations") + if annotations: + print(f" Annotations:") + if "audience" in annotations: + print(f" 受众: {annotations['audience']}") + if "priority" in annotations: + print(f" 优先级: {annotations['priority']}") + else: + print(f" Annotations: 无") + + def detect_image_format(self, data): + """检测图片格式""" + if len(data) < 8: + return "❌ 数据太短" + + # PNG + if data.startswith(b'\x89PNG\r\n\x1a\n'): + return "✅ PNG图片" + # JPEG + elif data.startswith(b'\xff\xd8\xff'): + return "✅ JPEG图片" + # GIF + elif data.startswith(b'GIF8'): + return "✅ GIF图片" + # WebP + elif data[8:12] == b'WEBP': + return "✅ WebP图片" + # BMP + elif data.startswith(b'BM'): + return "✅ BMP图片" + else: + hex_header = data[:8].hex().upper() + return f"⚠️ 未知格式 (头部: {hex_header})" + + def get_file_extension(self, data): + """根据文件头获取扩展名""" + if data.startswith(b'\x89PNG'): + return "png" + elif data.startswith(b'\xff\xd8\xff'): + return "jpg" + elif data.startswith(b'GIF8'): + return "gif" + elif len(data) > 12 and data[8:12] == b'WEBP': + return "webp" + elif data.startswith(b'BM'): + return "bmp" + else: + return "bin" + + def generate_analysis_report(self, text_items, image_items, other_items, result): + """生成最终分析报告""" + print(f"\n" + "="*60) + print("📊 AI端数据接收总结报告") + print("="*60) + + print(f"📈 数据统计:") + print(f" 文本项目: {len(text_items)}") + print(f" 图片项目: {len(image_items)}") + print(f" 其他项目: {len(other_items)}") + print(f" 总计: {len(text_items) + len(image_items) + len(other_items)}") + + print(f"\n🔍 MCP协议验证:") + + # 验证文本内容 + if text_items: + valid_text = all("text" in item for item in text_items) + print(f" 文本格式: {'✅ 符合标准' if valid_text else '❌ 格式错误'}") + else: + print(f" 文本格式: ⚠️ 无文本内容") + + # 验证图片内容 + if image_items: + valid_images = 0 + for item in image_items: + if (item.get("type") == "image" and + "data" in item and + "mimeType" in item and + item.get("mimeType", "").startswith("image/")): + valid_images += 1 + + print(f" 图片格式: ✅ {valid_images}/{len(image_items)} 符合MCP ImageContent标准") + + if valid_images == len(image_items): + print(f" 🎉 所有图片数据都符合MCP协议标准!") + else: + print(f" ⚠️ 部分图片数据格式有问题") + else: + print(f" 图片格式: ❌ 没有接收到图片数据") + + # 检查错误状态 + is_error = result.get("isError", False) + print(f" 错误状态: {'⚠️ 有错误' if is_error else '✅ 正常'}") + + # 最终结论 + print(f"\n🎯 测试结论:") + if image_items: + print(f" ✅ 图片数据传输测试成功!") + print(f" ✅ AI端成功接收到 {len(image_items)} 张图片") + print(f" ✅ 数据格式符合MCP ImageContent标准") + print(f" ✅ Base64编码/解码正常") + else: + print(f" ⚠️ 没有接收到图片数据") + print(f" 💡 可能原因:超时、用户未上传、或处理逻辑问题") + + async def cleanup(self): + """清理资源""" + if self.process: + self.process.terminate() + try: + await asyncio.wait_for(self.process.wait(), timeout=5) + except asyncio.TimeoutError: + self.process.kill() + await self.process.wait() + self.process = None + print("🔌 MCP服务器进程已关闭") + + +async def main(): + """主函数""" + parser = argparse.ArgumentParser(description="模拟AI客户端 - 通过MCP标准协议连接") + parser.add_argument("--timeout", type=int, default=120, help="等待用户反馈的超时时间(秒)") + args = parser.parse_args() + + print("🤖 AI客户端模拟器") + print("="*60) + print("这个脚本通过MCP标准协议连接服务器并接收图片数据") + print("测试步骤:") + print("1. 启动MCP服务器子进程") + print("2. 初始化MCP连接") + print("3. 调用interactive_feedback工具") + print("4. 等待并分析接收到的数据") + print("="*60) + + client = AIClient() + + try: + # 启动MCP服务器 + if not await client.start_mcp_server(): + return + + # 初始化连接 + await client.initialize_connection() + + # 调用interactive_feedback并等待数据 + print("\n⏳ 开始等待用户反馈...") + print("💡 提示:现在可以通过其他方式(如Augment Code界面)上传图片") + + response = await client.call_interactive_feedback({ + "project_directory": str(Path(__file__).parent), + "summary": "🧪 AI客户端测试:请上传图片测试数据传输功能", + "timeout": args.timeout + }) + + # 分析接收到的数据 + client.analyze_received_data(response) + + # 将完整的JSON响应写入文件 + output_file = "response_data.json" + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(response, f, indent=2, ensure_ascii=False) + print(f"\n✅ 完整响应数据已保存到: {output_file}") + + except KeyboardInterrupt: + print(f"\n⚠️ 用户中断测试") + except Exception as e: + print(f"\n❌ 测试过程中出错: {e}") + import traceback + traceback.print_exc() + + finally: + await client.cleanup() + print(f"\n👋 测试结束") + + +if __name__ == "__main__": + print("🚀 启动AI客户端模拟器...") + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n👋 再见!") diff --git a/src-tauri/python/mcp_feedback_enhanced_desktop/desktop_app.py b/src-tauri/python/mcp_feedback_enhanced_desktop/desktop_app.py index 4f67472..53ac54a 100644 --- a/src-tauri/python/mcp_feedback_enhanced_desktop/desktop_app.py +++ b/src-tauri/python/mcp_feedback_enhanced_desktop/desktop_app.py @@ -210,11 +210,22 @@ async def launch_tauri_app(self, server_url: str): debug_log(f"找到 Tauri 可執行檔案: {tauri_exe}") - # 設置環境變數 + # 設置環境變數,确保所有关键环境变量都被传递 env = os.environ.copy() env["MCP_DESKTOP_MODE"] = "true" env["MCP_WEB_URL"] = server_url + # 确保关键的 MCP 环境变量被传递给桌面应用 + mcp_env_vars = [ + "MCP_AI_CLIENT", "IS_AUGMENT_CLIENT", "is_augment_client", + "MCP_WEB_PORT", "MCP_WEB_HOST", + "MCP_DEBUG", "MCP_LANGUAGE" + ] + for var in mcp_env_vars: + if var in os.environ: + env[var] = os.environ[var] + debug_log(f"传递环境变量给桌面应用: {var} = {os.environ[var]!r}") + # 啟動 Tauri 應用程式 try: # Windows 下隱藏控制台視窗 diff --git a/src/mcp_feedback_enhanced/desktop_app/desktop_app.py b/src/mcp_feedback_enhanced/desktop_app/desktop_app.py index b58dbb0..a4f375a 100644 --- a/src/mcp_feedback_enhanced/desktop_app/desktop_app.py +++ b/src/mcp_feedback_enhanced/desktop_app/desktop_app.py @@ -211,11 +211,22 @@ async def launch_tauri_app(self, server_url: str): debug_log(f"找到 Tauri 可執行檔案: {tauri_exe}") - # 設置環境變數 + # 設置環境變數,确保所有关键环境变量都被传递 env = os.environ.copy() env["MCP_DESKTOP_MODE"] = "true" env["MCP_WEB_URL"] = server_url + # 确保关键的 MCP 环境变量被传递给桌面应用 + mcp_env_vars = [ + "MCP_AI_CLIENT", "IS_AUGMENT_CLIENT", "is_augment_client", + "MCP_WEB_PORT", "MCP_WEB_HOST", + "MCP_DEBUG", "MCP_LANGUAGE" + ] + for var in mcp_env_vars: + if var in os.environ: + env[var] = os.environ[var] + debug_log(f"传递环境变量给桌面应用: {var} = {os.environ[var]!r}") + # 啟動 Tauri 應用程式 try: # Windows 下隱藏控制台視窗 diff --git a/src/mcp_feedback_enhanced/server.py b/src/mcp_feedback_enhanced/server.py index 31a5bb8..f5ad736 100644 --- a/src/mcp_feedback_enhanced/server.py +++ b/src/mcp_feedback_enhanced/server.py @@ -32,12 +32,35 @@ from fastmcp import FastMCP from fastmcp.utilities.types import Image as MCPImage -from mcp.types import TextContent -from pydantic import Field +from mcp.types import TextContent, ImageContent +from pydantic import Field, BaseModel # 導入統一的調試功能 from .debug import server_debug_log as debug_log +# 模块级变量:在 main() 中设置,在 interactive_feedback 中使用 +_startup_ai_client_type: str = "" + + + + +# 定義符合標準協議的ImageContent類 +class StandardImageContent(BaseModel): + """符合MCP標準協議的ImageContent格式""" + type: str = "image" + image: dict # 包含 data, mimeType, name, description 字段 + + def dict(self, **kwargs): + """重写dict方法,确保返回正确的格式""" + return { + "type": self.type, + "image": self.image + } + + class Config: + # 允许任意字段,确保兼容性 + extra = "allow" + # 導入多語系支援 # 導入錯誤處理框架 from .utils.error_handler import ErrorHandler, ErrorType @@ -264,12 +287,13 @@ def save_feedback_to_file(feedback_data: dict, file_path: str | None = None) -> return file_path -def create_feedback_text(feedback_data: dict) -> str: +def create_feedback_text(feedback_data: dict, include_image_summary: bool = False) -> str: """ 建立格式化的回饋文字 Args: feedback_data: 回饋資料字典 + include_image_summary: 是否包含圖片概要(當使用標準協議格式時應設為False) Returns: str: 格式化後的回饋文字 @@ -284,8 +308,8 @@ def create_feedback_text(feedback_data: dict) -> str: if feedback_data.get("command_logs"): text_parts.append(f"=== 命令執行日誌 ===\n{feedback_data['command_logs']}") - # 圖片附件概要 - if feedback_data.get("images"): + # 圖片附件概要(僅在明確要求時包含) + if feedback_data.get("images") and include_image_summary: images = feedback_data["images"] text_parts.append(f"=== 圖片附件概要 ===\n用戶提供了 {len(images)} 張圖片:") @@ -360,17 +384,152 @@ def create_feedback_text(feedback_data: dict) -> str: return "\n\n".join(text_parts) if text_parts else "用戶未提供任何回饋內容。" -def process_images(images_data: list[dict]) -> list[MCPImage]: +def create_feedback_text_with_base64(feedback_data: dict) -> str: + """ + 為 Augment 客戶端建立簡潔的 JSON 格式 + + 當 is_augment_client 為 true 時,圖片將保存到臨時文件並返回絕對路徑, + 而不是 base64 數據,以便後續處理。 + + Args: + feedback_data: 回饋資料字典 + + Returns: + str: 簡潔的 JSON 字符串 + """ + debug_log(f"[AUGMENT_FORMAT] 開始創建簡潔 JSON 格式") + + # 構建簡潔的數據結構 + # 處理用戶回饋文本,添加適當的前綴以保持一致性 + feedback_text = feedback_data.get("interactive_feedback", "").strip() + if feedback_text: + formatted_text = f"用戶回饋:{feedback_text}" + else: + formatted_text = "用戶未提供回饋" + + # 添加命令日誌(如果有的話) + logs = feedback_data.get("logs", "") or feedback_data.get("command_logs", "") + if logs and logs.strip(): + formatted_text += f"\n\n執行日誌:{logs.strip()}" + + simple_data = { + "text": formatted_text, + "images": [] + } + + # 處理圖片數據 + images = feedback_data.get("images", []) + if images: + debug_log(f"[AUGMENT_FORMAT] 處理 {len(images)} 張圖片") + + for i, img in enumerate(images, 1): + try: + # 獲取圖片數據 + img_data = None + if img.get("data"): + if isinstance(img["data"], bytes): + img_data = img["data"] + debug_log(f"圖片 {i} 使用 bytes 數據,大小: {len(img_data)} bytes") + elif isinstance(img["data"], str): + # 如果是 base64 字符串,解碼為 bytes + img_data = base64.b64decode(img["data"]) + debug_log(f"圖片 {i} 從 base64 解碼,大小: {len(img_data)} bytes") + + if img_data: + # 检测实际图片格式(而不是仅基于文件名) + actual_format = _detect_image_format(img_data) + name = img.get("name", f"image_{i}") + + # 根据实际格式设置类型和扩展名 + if actual_format.upper() in ('JPEG', 'JPG'): + img_type = "jpeg" + file_ext = ".jpg" + elif actual_format.upper() == 'GIF': + img_type = "gif" + file_ext = ".gif" + elif actual_format.upper() == 'WEBP': + img_type = "webp" + file_ext = ".webp" + elif actual_format.upper() == 'PNG': + img_type = "png" + file_ext = ".png" + else: + # 如果无法检测格式,回退到基于文件名的推断 + if name.lower().endswith((".jpg", ".jpeg")): + img_type = "jpeg" + file_ext = ".jpg" + elif name.lower().endswith(".gif"): + img_type = "gif" + file_ext = ".gif" + elif name.lower().endswith(".webp"): + img_type = "webp" + file_ext = ".webp" + else: + img_type = "png" + file_ext = ".png" + + debug_log(f"圖片 {i} 格式檢測: 實際格式={actual_format}, 設定類型={img_type}, 擴展名={file_ext}") + + # 創建臨時文件保存圖片(二進制模式) + try: + temp_file_path = create_temp_file( + suffix=file_ext, + prefix=f"augment_image_{i}_", + text=False # 二進制模式,適用於圖片文件 + ) + + # 將圖片數據寫入臨時文件 + with open(temp_file_path, 'wb') as f: + f.write(img_data) + + debug_log(f"圖片 {i} 已保存到臨時文件: {temp_file_path}") + + # 創建圖片對象:包含文件路徑和類型 + img_obj = { + "path": temp_file_path, # 使用絕對路徑替代 base64 數據 + "type": img_type + } + + simple_data["images"].append(img_obj) + debug_log(f"圖片 {i} 已添加,類型: {img_type},路徑: {temp_file_path}") + + except Exception as file_error: + debug_log(f"圖片 {i} 保存到臨時文件失敗: {file_error}") + # 如果保存失敗,跳過這張圖片 + continue + + else: + debug_log(f"圖片 {i} 數據處理失敗,跳過") + + except Exception as e: + debug_log(f"圖片 {i} 處理失敗: {e}") + continue + + # 轉換為 JSON 字符串 + try: + json_result = json.dumps(simple_data, ensure_ascii=False, separators=(',', ':')) + debug_log(f"[AUGMENT_FORMAT] 簡潔 JSON 創建成功,長度: {len(json_result)} 字符") + return json_result + except Exception as e: + debug_log(f"[AUGMENT_FORMAT] JSON 序列化失敗: {e}") + # 回退到最簡格式 + return json.dumps({ + "text": simple_data["text"], + "images": [] + }, ensure_ascii=False) + + +def process_images(images_data: list[dict]) -> list[dict]: """ - 處理圖片資料,轉換為 MCP 圖片對象 + 處理圖片資料,轉換為標準 MCP ImageContent 對象 Args: images_data: 圖片資料列表 Returns: - List[MCPImage]: MCP 圖片對象列表 + List[dict]: 標準 MCP ImageContent 格式的字典列表 """ - mcp_images = [] + image_contents = [] for i, img in enumerate(images_data, 1): try: @@ -397,20 +556,51 @@ def process_images(images_data: list[dict]) -> list[MCPImage]: debug_log(f"圖片 {i} 數據為空,跳過") continue - # 根據文件名推斷格式 + # 根據文件名推斷 MIME 類型 file_name = img.get("name", "image.png") if file_name.lower().endswith((".jpg", ".jpeg")): - image_format = "jpeg" + mime_type = "image/jpeg" elif file_name.lower().endswith(".gif"): - image_format = "gif" + mime_type = "image/gif" + elif file_name.lower().endswith(".webp"): + mime_type = "image/webp" else: - image_format = "png" # 默認使用 PNG - - # 創建 MCPImage 對象 - mcp_image = MCPImage(data=image_bytes, format=image_format) - mcp_images.append(mcp_image) - - debug_log(f"圖片 {i} ({file_name}) 處理成功,格式: {image_format}") + mime_type = "image/png" # 默認使用 PNG + + # 將 bytes 轉換為 base64 字符串(MCP ImageContent 標準格式) + image_base64 = base64.b64encode(image_bytes).decode('utf-8') + + # 計算文件大小 + file_size_kb = len(image_bytes) / 1024 + image_name = img.get("name", f"image_{i}.png") + + # 使用標準MCP ImageContent類 + # 根據Context7文檔,ImageContent應該有data和mimeType字段 + try: + image_content = ImageContent( + type="image", + data=image_base64, # 使用base64字符串 + mimeType=mime_type + ) + debug_log(f"圖片 {i} 使用標準ImageContent創建成功") + except Exception as e: + debug_log(f"ImageContent創建失敗: {e}") + # 如果ImageContent不支持這種格式,回退到字典 + image_content = { + "type": "image", + "data": image_base64, + "mimeType": mime_type + } + + image_contents.append(image_content) + + debug_log(f"圖片 {i} ({image_name}) 處理成功,MIME類型: {mime_type},base64長度: {len(image_base64)}") + + # 根據image_content的類型記錄不同信息 + if hasattr(image_content, 'mimeType'): + debug_log(f"圖片 {i} ImageContent對象已創建: mimeType={image_content.mimeType}") + elif isinstance(image_content, dict): + debug_log(f"圖片 {i} 字典格式已創建: mimeType={image_content.get('mimeType')}") except Exception as e: # 使用統一錯誤處理(不影響 JSON RPC) @@ -421,8 +611,43 @@ def process_images(images_data: list[dict]) -> list[MCPImage]: ) debug_log(f"圖片 {i} 處理失敗 [錯誤ID: {error_id}]: {e}") - debug_log(f"共處理 {len(mcp_images)} 張圖片") - return mcp_images + debug_log(f"共處理 {len(image_contents)} 張圖片") + return image_contents + + +def _detect_image_format(image_data: bytes) -> str: + """ + 检测图片的实际格式 + + Args: + image_data: 图片字节数据 + + Returns: + str: 图片格式 (PNG, JPEG, GIF, WEBP, UNKNOWN) + """ + try: + # 检查文件头魔数 + if image_data.startswith(b'\x89PNG\r\n\x1a\n'): + return 'PNG' + elif image_data.startswith(b'\xff\xd8\xff'): + return 'JPEG' + elif image_data.startswith(b'GIF8'): + return 'GIF' + elif len(image_data) > 12 and image_data[8:12] == b'WEBP': + return 'WEBP' + elif image_data.startswith(b'BM'): + return 'BMP' + else: + # 尝试使用PIL检测 + try: + from PIL import Image + with Image.open(io.BytesIO(image_data)) as img: + return img.format or 'UNKNOWN' + except: + return 'UNKNOWN' + except Exception as e: + debug_log(f"图片格式检测失败: {e}") + return 'UNKNOWN' # ===== MCP 工具定義 ===== @@ -449,12 +674,21 @@ async def interactive_feedback( timeout: Timeout in seconds for waiting user feedback (default: 600 seconds) Returns: - list: List containing TextContent and MCPImage objects representing user feedback + list: List containing TextContent and ImageContent objects representing user feedback in standard MCP format """ # 環境偵測 is_remote = is_remote_environment() is_wsl = is_wsl_environment() + # 使用服务器启动时的固定配置(不再重新读取环境变量) + current_ai_client_type = _startup_ai_client_type + is_augment_client = current_ai_client_type == 'augment' + + print(f"[SERVER_CONFIG] 当前进程PID: {os.getpid()}", file=sys.stderr) + print(f"[SERVER_CONFIG] 使用服务器启动时的固定配置: {current_ai_client_type!r}", file=sys.stderr) + print(f"[SERVER_CONFIG] is_augment_client = {is_augment_client}", file=sys.stderr) + print(f"[SERVER_CONFIG] 配置来源: 服务器启动时环境变量", file=sys.stderr) + debug_log(f"環境偵測結果 - 遠端: {is_remote}, WSL: {is_wsl}") debug_log("使用介面: Web UI") @@ -467,6 +701,22 @@ async def interactive_feedback( # 使用 Web 模式 debug_log("回饋模式: web") + # 在啟動 Web UI 之前,確保 WebUIManager 能夠獲取到正確的 AI 客戶端類型 + from .web import get_web_ui_manager + from .web.main import _web_ui_manager + + # 如果 WebUIManager 還沒有創建,我們需要確保它能獲取到正確的 AI 客戶端類型 + manager = get_web_ui_manager() + + # 檢查 WebUIManager 是否正確讀取了 AI 客戶端類型 + if manager.ai_client_type != current_ai_client_type: + debug_log(f"WebUIManager AI 客戶端類型不匹配: manager={manager.ai_client_type}, expected={current_ai_client_type}") + debug_log(f"強制更新 WebUIManager 的 AI 客戶端類型") + manager.ai_client_type = current_ai_client_type + # 同時更新保存的環境變數 + manager.env_vars['MCP_AI_CLIENT'] = current_ai_client_type + + # 現在啟動 Web UI result = await launch_web_feedback_ui(project_directory, summary, timeout) # 處理取消情況 @@ -479,31 +729,85 @@ async def interactive_feedback( # 建立回饋項目列表 feedback_items = [] - # 添加文字回饋 - if ( - result.get("interactive_feedback") - or result.get("command_logs") - or result.get("images") - ): - feedback_text = create_feedback_text(result) - feedback_items.append(TextContent(type="text", text=feedback_text)) - debug_log("文字回饋已添加") - - # 添加圖片回饋 - if result.get("images"): - mcp_images = process_images(result["images"]) - # 修復 arg-type 錯誤 - 直接擴展列表 - feedback_items.extend(mcp_images) - debug_log(f"已添加 {len(mcp_images)} 張圖片") - - # 確保至少有一個回饋項目 - if not feedback_items: - feedback_items.append( - TextContent(type="text", text="用戶未提供任何回饋內容。") - ) + # 根據 AI 客戶端類型決定返回格式(使用直接读取的值确保可靠性) + print(f"[FINAL_CHECK] 最终判断:current_ai_client_type = '{current_ai_client_type}'", file=sys.stderr) + print(f"[FINAL_CHECK] 最终判断:is_augment_client = {is_augment_client}", file=sys.stderr) + if is_augment_client: + # Augment 客戶端:根據是否有圖片決定格式 + images = result.get("images", []) + has_images = bool(images and any(img.get("data") for img in images)) + + if has_images: + # 有圖片:使用 JSON 格式便於 JavaScript 提取 + debug_log("有圖片數據,使用 JSON 格式返回") + json_text = create_feedback_text_with_base64(result) + return [TextContent(type="text", text=json_text)] + else: + # 無圖片:使用普通文本格式便於閱讀 + debug_log("無圖片數據,使用文本格式返回") + text_parts = [] + + # 用戶回饋 + feedback = result.get("interactive_feedback", "").strip() + if feedback: + text_parts.append(f"用戶回饋:{feedback}") + else: + text_parts.append("用戶未提供回饋") + + # 命令日誌 + logs = result.get("logs", "") or result.get("command_logs", "") + if logs and logs.strip(): + text_parts.append(f"執行日誌:{logs.strip()}") + + combined_text = "\n\n".join(text_parts) if text_parts else "無回饋內容" + return [TextContent(type="text", text=combined_text)] + else: + # 標準客戶端:分別返回文字和圖片 + debug_log("使用標準格式:文字和圖片分別傳輸") + + # 添加文字回饋(不包含圖片概要,因為圖片將以標準協議格式單獨傳輸) + if result.get("interactive_feedback") or result.get("command_logs"): + feedback_text = create_feedback_text(result, include_image_summary=False) + feedback_items.append(TextContent(type="text", text=feedback_text)) + debug_log("文字回饋已添加") + + # 添加圖片回饋(採用cunzhi項目的成功策略:圖片優先,文本在後) + if result.get("images"): + image_contents = process_images(result["images"]) + # 🎯 關鍵策略:圖片優先添加(模仿cunzhi項目) + # 直接添加圖片字典對象,不嵌套在JSON字符串中 + feedback_items.extend(image_contents) + debug_log(f"已添加 {len(image_contents)} 張圖片(直接字典格式)") + + # 不再添加詳細圖片信息到文本中,因為圖片數據已經以標準協議格式單獨傳輸 + debug_log(f"圖片數據將以標準協議格式單獨傳輸,不添加到文本中") + + # 確保至少有一個回饋項目 + if not feedback_items: + feedback_items.append( + TextContent(type="text", text="用戶未提供任何回饋內容。") + ) + + debug_log(f"回饋收集完成,原始項目數: {len(feedback_items)}") - debug_log(f"回饋收集完成,共 {len(feedback_items)} 個項目") - return feedback_items + # 標準模式:處理並轉換所有項目 + # 將StandardImageContent對象轉換為字典 + final_items = [] + + for item in feedback_items: + if isinstance(item, StandardImageContent): + # 將StandardImageContent轉換為字典 + final_items.append({ + "type": "image", + "image": item.image + }) + debug_log(f"StandardImageContent已轉換為字典格式") + else: + # 其他項目直接添加 + final_items.append(item) + + debug_log(f"最終返回項目數: {len(final_items)}") + return final_items except Exception as e: # 使用統一錯誤處理,但不影響 JSON RPC 響應 @@ -514,10 +818,23 @@ async def interactive_feedback( ) # 生成用戶友好的錯誤信息 - user_error_msg = ErrorHandler.format_user_error(e, include_technical=False) + user_error_msg = ErrorHandler.format_user_error(e, include_technical=True) # 暂时显示技术细节 debug_log(f"回饋收集錯誤 [錯誤ID: {error_id}]: {e!s}") + debug_log(f"錯誤堆棧: {e.__class__.__name__}: {e}") - return [TextContent(type="text", text=user_error_msg)] + # 根據 AI 客戶端類型決定錯誤響應格式 + print(f"[ERROR_FORMAT] 錯誤處理:current_ai_client_type = '{current_ai_client_type}'", file=sys.stderr) + print(f"[ERROR_FORMAT] 錯誤處理:is_augment_client = {is_augment_client}", file=sys.stderr) + + if is_augment_client: + # Augment 客戶端:錯誤時使用簡單文本格式(因為沒有圖片) + debug_log("使用 Augment 文本格式返回錯誤信息") + error_text = f"❌ 操作超时\n技術細節:{user_error_msg}" + return [TextContent(type="text", text=error_text)] + else: + # 標準客戶端:直接返回錯誤信息 + debug_log("使用標準格式返回錯誤信息") + return [TextContent(type="text", text=user_error_msg)] async def launch_web_feedback_ui(project_dir: str, summary: str, timeout: int) -> dict: @@ -570,13 +887,24 @@ def get_system_info() -> str: is_remote = is_remote_environment() is_wsl = is_wsl_environment() + # 檢測 AI 客戶端類型 + ai_client = os.getenv("MCP_AI_CLIENT", "").lower().strip() + system_info = { "平台": sys.platform, "Python 版本": sys.version.split()[0], "WSL 環境": is_wsl, "遠端環境": is_remote, "介面類型": "Web UI", + "AI 客戶端": ai_client if ai_client else "未指定", + "Augment 模式": ai_client == "augment", "環境變數": { + "MCP_AI_CLIENT": os.getenv("MCP_AI_CLIENT"), + "MCP_DEBUG": os.getenv("MCP_DEBUG"), + "MCP_WEB_HOST": os.getenv("MCP_WEB_HOST"), + "MCP_WEB_PORT": os.getenv("MCP_WEB_PORT"), + "MCP_DESKTOP_MODE": os.getenv("MCP_DESKTOP_MODE"), + "MCP_LANGUAGE": os.getenv("MCP_LANGUAGE"), "SSH_CONNECTION": os.getenv("SSH_CONNECTION"), "SSH_CLIENT": os.getenv("SSH_CLIENT"), "DISPLAY": os.getenv("DISPLAY"), @@ -620,6 +948,8 @@ def main(): "on", ) + # AI 客戶端類型現在在 WebUIManager 初始化時讀取(與 MCP_WEB_PORT 處理方式完全一致) + if debug_enabled: debug_log("🚀 啟動互動式回饋收集 MCP 服務器") debug_log(f" 服務器名稱: {SERVER_NAME}") @@ -630,10 +960,21 @@ def main(): debug_log(f" WSL 環境: {is_wsl_environment()}") debug_log(f" 桌面模式: {'啟用' if desktop_mode else '禁用'}") debug_log(" 介面類型: Web UI") + debug_log(" AI 客戶端類型: 將在 WebUIManager 初始化時讀取") debug_log(" 等待來自 AI 助手的調用...") debug_log("準備啟動 MCP 伺服器...") debug_log("調用 mcp.run()...") + # 在 MCP 服务器启动前,读取并保存服务器配置 + global _startup_ai_client_type + _startup_ai_client_type = os.getenv('MCP_AI_CLIENT', 'augment').lower().strip() + + # 打印启动配置到 stderr(不干扰 MCP 协议) + print(f"[STARTUP_CHECK] MCP_AI_CLIENT = {os.getenv('MCP_AI_CLIENT')!r}", file=sys.stderr, flush=True) + print(f"[STARTUP_CHECK] MCP_WEB_PORT = {os.getenv('MCP_WEB_PORT')!r}", file=sys.stderr, flush=True) + print(f"[STARTUP_CHECK] 当前进程PID: {os.getpid()}", file=sys.stderr, flush=True) + print(f"[STARTUP_CHECK] 服务器固定处理方式: {_startup_ai_client_type}", file=sys.stderr, flush=True) + try: # 使用正確的 FastMCP API mcp.run() diff --git a/src/mcp_feedback_enhanced/utils/image_compressor.py b/src/mcp_feedback_enhanced/utils/image_compressor.py new file mode 100644 index 0000000..f1905ef --- /dev/null +++ b/src/mcp_feedback_enhanced/utils/image_compressor.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +""" +图片压缩工具 +============ + +基于 Pillow 的高质量图片压缩工具,专门针对 AI 识别优化。 +支持多种格式,智能压缩策略,确保压缩后图片保持清晰度。 +""" + +import io +import logging +from pathlib import Path +from typing import Optional, Tuple, Union + +try: + from PIL import Image, ImageOps + PILLOW_AVAILABLE = True +except ImportError: + PILLOW_AVAILABLE = False + Image = None + ImageOps = None + +from ..debug import web_debug_log as debug_log + + +class ImageCompressor: + """图片压缩器""" + + # 压缩配置常量 + TARGET_SIZE = 500 * 1024 # 目标大小 500KB + COMPRESSION_THRESHOLD = 500 * 1024 # 压缩阈值 500KB + MAX_DIMENSION = 2048 # 最大尺寸 + MIN_DIMENSION = 400 # 最小尺寸(确保 AI 识别) + + # 质量参数 + JPEG_QUALITY_HIGH = 85 + JPEG_QUALITY_MEDIUM = 75 + JPEG_QUALITY_LOW = 65 + JPEG_QUALITY_MIN = 50 + + # WebP 质量参数(通常比 JPEG 更高效) + WEBP_QUALITY_HIGH = 80 + WEBP_QUALITY_MEDIUM = 70 + WEBP_QUALITY_LOW = 60 + WEBP_QUALITY_MIN = 45 + + def __init__(self): + """初始化压缩器""" + if not PILLOW_AVAILABLE: + raise ImportError("Pillow 库未安装,无法使用图片压缩功能") + + self.logger = logging.getLogger(__name__) + + def compress_image_bytes( + self, + image_data: bytes, + target_size: Optional[int] = None, + format_hint: Optional[str] = None + ) -> Tuple[bytes, dict]: + """ + 压缩图片字节数据 + + Args: + image_data: 原始图片字节数据 + target_size: 目标大小(字节),默认使用 TARGET_SIZE + format_hint: 格式提示(如 'image/jpeg') + + Returns: + Tuple[bytes, dict]: (压缩后的字节数据, 压缩信息) + """ + if not image_data: + raise ValueError("图片数据为空") + + target_size = target_size or self.TARGET_SIZE + original_size = len(image_data) + + debug_log(f"开始压缩图片,原始大小: {self._format_size(original_size)}") + + # 如果图片已经小于目标大小,直接返回 + if original_size <= target_size: + debug_log(f"图片大小 {self._format_size(original_size)} 已小于目标 {self._format_size(target_size)},无需压缩") + return image_data, { + 'original_size': original_size, + 'compressed_size': original_size, + 'compression_ratio': 0.0, + 'compressed': False, + 'format': self._detect_format(image_data), + 'dimensions': self._get_image_dimensions(image_data) + } + + try: + # 打开图片 + with Image.open(io.BytesIO(image_data)) as img: + # 获取原始信息 + original_format = img.format or 'JPEG' + original_dimensions = img.size + + debug_log(f"原始图片信息: {original_dimensions[0]}x{original_dimensions[1]}, 格式: {original_format}") + + # 转换为 RGB(如果需要) + if img.mode in ('RGBA', 'LA', 'P'): + # 对于透明图片,使用白色背景 + background = Image.new('RGB', img.size, (255, 255, 255)) + if img.mode == 'P': + img = img.convert('RGBA') + background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None) + img = background + elif img.mode != 'RGB': + img = img.convert('RGB') + + # 应用 EXIF 旋转 + img = ImageOps.exif_transpose(img) + + # 多轮压缩尝试 + compressed_data, compression_info = self._compress_with_multiple_attempts( + img, target_size, original_format, original_dimensions + ) + + # 如果仍未达到目标大小,执行自适应降采样循环,直至 <= 目标或到达下限 + min_quality = self.JPEG_QUALITY_MIN + min_dimension = self.MIN_DIMENSION + current_img = img + current_format = compression_info.get('format', 'JPEG') + current_quality = compression_info.get('quality', self.JPEG_QUALITY_MEDIUM) + current_data = compressed_data + + def encode(image, fmt, quality): + out = io.BytesIO() + save_kwargs = {'format': fmt} + if fmt in ('JPEG','WEBP'): + save_kwargs['quality'] = int(max(min(quality, 95), 40)) + save_kwargs['optimize'] = True + if fmt == 'JPEG': + save_kwargs['progressive'] = True + image.save(out, **save_kwargs) + return out.getvalue() + + # 自适应循环 + attempt = 0 + while len(current_data) > target_size and \ + (max(current_img.size) > min_dimension or current_quality > min_quality): + attempt += 1 + # 先降低质量,再必要时按比例缩小 + if current_quality > min_quality: + current_quality = max(min_quality, int(current_quality - 5)) + else: + # 缩放到 90% 尺寸,保持不低于最小尺寸 + w, h = current_img.size + scale = 0.9 + new_w = max(int(w * scale), min_dimension) + new_h = max(int(h * scale), min_dimension) + if (new_w, new_h) == current_img.size: + # 无法继续缩放,跳出 + break + current_img = current_img.resize((new_w, new_h), Image.Resampling.LANCZOS) + + try: + current_data = encode(current_img, 'JPEG', current_quality) + except Exception: + # 回退为 WEBP 尝试 + try: + current_data = encode(current_img, 'WEBP', current_quality) + current_format = 'WEBP' + except Exception: + break + + # 计算最终信息 + final_size = len(current_data) + compression_info.update({ + 'original_size': original_size, + 'compressed_size': final_size, + 'compression_ratio': (1 - final_size / original_size) * 100.0, + 'compressed': True, + 'original_format': original_format, + 'original_dimensions': original_dimensions, + 'format': current_format, + 'quality': current_quality, + 'dimensions': current_img.size, + }) + + debug_log( + f"压缩完成: {self._format_size(original_size)} → {self._format_size(final_size)} " + f"(压缩率: {compression_info['compression_ratio']:.1f}%)" + ) + + return current_data, compression_info + + except Exception as e: + debug_log(f"图片压缩失败: {e}") + # 压缩失败时返回原始数据 + return image_data, { + 'original_size': original_size, + 'compressed_size': original_size, + 'compression_ratio': 0.0, + 'compressed': False, + 'error': str(e), + 'format': self._detect_format(image_data), + 'dimensions': self._get_image_dimensions(image_data) + } + + def _compress_with_multiple_attempts( + self, + img: Image.Image, + target_size: int, + original_format: str, + original_dimensions: Tuple[int, int] + ) -> Tuple[bytes, dict]: + """多轮压缩尝试""" + + best_result = None + best_size = float('inf') + + # 尝试不同的压缩策略 + strategies = [ + # 策略1: 保持原始尺寸,降低质量 + {'resize': False, 'format': 'JPEG', 'quality': self.JPEG_QUALITY_HIGH}, + {'resize': False, 'format': 'JPEG', 'quality': self.JPEG_QUALITY_MEDIUM}, + {'resize': False, 'format': 'WEBP', 'quality': self.WEBP_QUALITY_HIGH}, + + # 策略2: 适度缩放,中等质量 + {'resize': True, 'scale': 0.8, 'format': 'JPEG', 'quality': self.JPEG_QUALITY_MEDIUM}, + {'resize': True, 'scale': 0.8, 'format': 'WEBP', 'quality': self.WEBP_QUALITY_MEDIUM}, + + # 策略3: 更大缩放,保持质量 + {'resize': True, 'scale': 0.6, 'format': 'JPEG', 'quality': self.JPEG_QUALITY_HIGH}, + {'resize': True, 'scale': 0.6, 'format': 'WEBP', 'quality': self.WEBP_QUALITY_HIGH}, + + # 策略4: 激进压缩 + {'resize': True, 'scale': 0.5, 'format': 'JPEG', 'quality': self.JPEG_QUALITY_LOW}, + {'resize': True, 'scale': 0.5, 'format': 'WEBP', 'quality': self.WEBP_QUALITY_LOW}, + + # 策略5: 最后手段 + {'resize': True, 'scale': 0.4, 'format': 'JPEG', 'quality': self.JPEG_QUALITY_MIN}, + {'resize': True, 'scale': 0.4, 'format': 'WEBP', 'quality': self.WEBP_QUALITY_MIN}, + ] + + for i, strategy in enumerate(strategies, 1): + try: + result_data, info = self._apply_compression_strategy(img, strategy) + result_size = len(result_data) + + # 构造可选的缩放信息,避免在 f-string 表达式中使用反斜杠 + scale_info = f", 缩放: {strategy.get('scale', 1.0):.1f}" if strategy.get('resize') else "" + debug_log( + f"策略 {i}: {self._format_size(result_size)} " + f"({strategy['format']}, 质量: {strategy['quality']}{scale_info})" + ) + + # 如果达到目标大小,直接返回 + if result_size <= target_size: + info['strategy'] = i + info['compressed_size'] = result_size + return result_data, info + + # 记录最佳结果 + if result_size < best_size: + best_size = result_size + best_result = (result_data, info) + best_result[1]['strategy'] = i + best_result[1]['compressed_size'] = result_size + + except Exception as e: + debug_log(f"策略 {i} 失败: {e}") + continue + + # 如果没有策略达到目标,返回最佳结果 + if best_result: + debug_log(f"使用最佳策略 {best_result[1]['strategy']}: {self._format_size(best_size)}") + return best_result + + # 如果所有策略都失败,返回原始图片的 JPEG 版本 + debug_log("所有压缩策略失败,返回基础 JPEG 压缩") + return self._apply_compression_strategy(img, { + 'resize': False, + 'format': 'JPEG', + 'quality': self.JPEG_QUALITY_MEDIUM + }) + + def _apply_compression_strategy(self, img: Image.Image, strategy: dict) -> Tuple[bytes, dict]: + """应用压缩策略""" + working_img = img.copy() + + # 应用缩放 + if strategy.get('resize') and strategy.get('scale'): + scale = strategy['scale'] + new_width = max(int(working_img.width * scale), self.MIN_DIMENSION) + new_height = max(int(working_img.height * scale), self.MIN_DIMENSION) + + # 确保不超过最大尺寸 + if max(new_width, new_height) > self.MAX_DIMENSION: + ratio = self.MAX_DIMENSION / max(new_width, new_height) + new_width = int(new_width * ratio) + new_height = int(new_height * ratio) + + working_img = working_img.resize((new_width, new_height), Image.Resampling.LANCZOS) + + # 保存到字节流 + output = io.BytesIO() + save_format = strategy['format'] + save_kwargs = {'format': save_format} + + if save_format in ('JPEG', 'WEBP'): + save_kwargs['quality'] = strategy['quality'] + save_kwargs['optimize'] = True + + if save_format == 'JPEG': + save_kwargs['progressive'] = True + + working_img.save(output, **save_kwargs) + compressed_data = output.getvalue() + + return compressed_data, { + 'compressed_size': len(compressed_data), + 'format': save_format, + 'quality': strategy['quality'], + 'dimensions': working_img.size, + 'resized': strategy.get('resize', False), + 'scale': strategy.get('scale', 1.0) + } + + def _detect_format(self, image_data: bytes) -> str: + """检测图片格式""" + try: + with Image.open(io.BytesIO(image_data)) as img: + return img.format or 'UNKNOWN' + except: + return 'UNKNOWN' + + def _get_image_dimensions(self, image_data: bytes) -> Optional[Tuple[int, int]]: + """获取图片尺寸""" + try: + with Image.open(io.BytesIO(image_data)) as img: + return img.size + except: + return None + + def _format_size(self, size_bytes: int) -> str: + """格式化文件大小""" + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.1f} KB" + else: + return f"{size_bytes / (1024 * 1024):.1f} MB" + + +# 全局压缩器实例 +_compressor = None + +def get_image_compressor() -> ImageCompressor: + """获取图片压缩器实例(单例模式)""" + global _compressor + if _compressor is None: + _compressor = ImageCompressor() + return _compressor + +def compress_image_if_needed( + image_data: bytes, + target_size: Optional[int] = None, + format_hint: Optional[str] = None +) -> Tuple[bytes, dict]: + """ + 如果需要则压缩图片的便捷函数 + + Args: + image_data: 图片字节数据 + target_size: 目标大小,默认 500KB + format_hint: 格式提示 + + Returns: + Tuple[bytes, dict]: (压缩后数据, 压缩信息) + """ + if not PILLOW_AVAILABLE: + debug_log("Pillow 不可用,跳过图片压缩") + return image_data, { + 'original_size': len(image_data), + 'compressed_size': len(image_data), + 'compression_ratio': 0.0, + 'compressed': False, + 'error': 'Pillow not available' + } + + compressor = get_image_compressor() + return compressor.compress_image_bytes(image_data, target_size, format_hint) diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index f7c3a18..458ca4e 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -71,6 +71,41 @@ def __init__(self, host: str = "127.0.0.1", port: int | None = None): else: debug_log(f"未設定 MCP_WEB_PORT 環境變數,使用預設端口 {preferred_port}") + # 檢查環境變數 MCP_AI_CLIENT(完全參考 MCP_WEB_PORT 的處理方式) + # 在主线程中保存环境变量,避免子线程无法访问的问题 + env_ai_client = os.getenv("MCP_AI_CLIENT") + # 別名/兼容變量:允許 is_augment_client/IS_AUGMENT_CLIENT 布林型開關 + alias_bool = None + for key in ("IS_AUGMENT_CLIENT", "is_augment_client"): + v = os.getenv(key) + if v and v.lower().strip() in ("1", "true", "yes", "on"): + alias_bool = key + break + + if env_ai_client: + self.ai_client_type = env_ai_client.lower().strip() + debug_log(f"使用環境變數指定的 AI 客戶端類型: {self.ai_client_type}") + elif alias_bool: + self.ai_client_type = "augment" + debug_log(f"檢測到 {alias_bool}=true,AI 客戶端類型: augment") + else: + # 保持默認為 cursor(不臆測客戶端) + self.ai_client_type = "cursor" + debug_log("未設定 MCP_AI_CLIENT/別名,使用預設值(cursor 模式)") + + # 保存所有关键环境变量到实例中,确保子线程可以访问 + self.env_vars = { + "MCP_AI_CLIENT": env_ai_client, + "IS_AUGMENT_CLIENT": os.getenv("IS_AUGMENT_CLIENT"), + "is_augment_client": os.getenv("is_augment_client"), + "MCP_WEB_HOST": os.getenv("MCP_WEB_HOST"), + "MCP_WEB_PORT": os.getenv("MCP_WEB_PORT"), + "MCP_DEBUG": os.getenv("MCP_DEBUG"), + "MCP_DESKTOP_MODE": os.getenv("MCP_DESKTOP_MODE"), + "MCP_LANGUAGE": os.getenv("MCP_LANGUAGE"), + } + debug_log(f"保存环境变量到实例: {self.env_vars}") + # 使用增強的端口管理,測試模式下禁用自動清理避免權限問題 auto_cleanup = os.environ.get("MCP_TEST_MODE", "").lower() != "true" @@ -334,7 +369,7 @@ def create_session(self, project_directory: str, summary: str) -> str: if old_session and old_session.websocket: old_websocket = old_session.websocket debug_log("保存舊會話的 WebSocket 連接以發送更新通知") - + old_session.next_step("反饋處理完成") # 創建新會話 session_id = str(uuid.uuid4()) session = WebFeedbackSession(session_id, project_directory, summary) @@ -482,6 +517,12 @@ def start_server(self): """啟動 Web 伺服器(優化版本,支援並行初始化)""" def run_server_with_retry(): + # 在子线程中恢复环境变量 + for key, value in self.env_vars.items(): + if value is not None: + os.environ[key] = value + debug_log(f"子线程恢复环境变量: {key} = {value!r}") + max_retries = 5 retry_count = 0 original_port = self.port diff --git a/src/mcp_feedback_enhanced/web/models/feedback_session.py b/src/mcp_feedback_enhanced/web/models/feedback_session.py index bc3ba57..8105e55 100644 --- a/src/mcp_feedback_enhanced/web/models/feedback_session.py +++ b/src/mcp_feedback_enhanced/web/models/feedback_session.py @@ -26,6 +26,7 @@ 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 ...utils.image_compressor import compress_image_if_needed from ..constants import get_message_code @@ -54,6 +55,8 @@ class CleanupReason(Enum): # 常數定義 MAX_IMAGE_SIZE = 1 * 1024 * 1024 # 1MB 圖片大小限制 +COMPRESSION_THRESHOLD = 500 * 1024 # 500KB 壓縮閾值(修正为500KB) +COMPRESSION_TARGET_SIZE = 500 * 1024 # 500KB 壓縮目標大小 SUPPORTED_IMAGE_TYPES = { "image/png", "image/jpeg", @@ -632,24 +635,75 @@ def _process_images(self, images: list[dict]) -> list[dict]: debug_log(f"圖片 {img['name']} 數據為空,跳過") continue + # Python 后端智能压缩处理 + original_size = len(image_bytes) + + # 检查是否需要压缩(大于500KB) + if original_size > COMPRESSION_THRESHOLD: + debug_log(f"圖片 {img['name']} 大小 {self._format_size(original_size)} 超過閾值,開始 Python 壓縮") + + try: + # 使用 Python 压缩 + compressed_bytes, compression_info = compress_image_if_needed( + image_bytes, + target_size=COMPRESSION_TARGET_SIZE, + format_hint=img.get('type') + ) + + # 使用压缩后的数据 + final_bytes = compressed_bytes + final_size = len(compressed_bytes) + is_compressed = compression_info.get('compressed', False) + + if is_compressed: + compression_ratio = compression_info.get('compression_ratio', 0) + debug_log( + f"圖片 {img['name']} Python 壓縮成功: {self._format_size(original_size)} → " + f"{self._format_size(final_size)} (壓縮率: {compression_ratio:.1f}%)" + ) + else: + debug_log(f"圖片 {img['name']} 無需壓縮或壓縮失敗,使用原始數據") + + except Exception as e: + debug_log(f"圖片 {img['name']} Python 壓縮失敗: {e},使用原始數據") + final_bytes = image_bytes + final_size = original_size + is_compressed = False + compression_info = {} + else: + # 无需压缩 + final_bytes = image_bytes + final_size = original_size + is_compressed = False + compression_info = {} + debug_log(f"圖片 {img['name']} 大小 {self._format_size(original_size)} 無需壓縮") + processed_images.append( { "name": img["name"], - "data": image_bytes, # 保存原始 bytes 數據 - "size": len(image_bytes), + "data": final_bytes, # 保存处理后的 bytes 數據 + "size": final_size, + "compressed": is_compressed, + "original_size": original_size, + "compression_info": compression_info, # 保存压缩详细信息 } ) - debug_log( - f"圖片 {img['name']} 處理成功,大小: {len(image_bytes)} bytes" - ) - except Exception as e: debug_log(f"圖片處理錯誤: {e}") continue return processed_images + def _format_size(self, size_bytes: int) -> str: + """格式化文件大小""" + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.1f} KB" + else: + return f"{size_bytes / (1024 * 1024):.1f} MB" + def add_log(self, log_entry: str): """添加命令日誌""" self.command_logs.append(log_entry) @@ -684,6 +738,8 @@ async def run_command(self, command: str): return # 使用安全的方式執行命令(不使用 shell=True) + # 确保环境变量正确传递给子进程 + env = os.environ.copy() self.process = subprocess.Popen( parsed_command, shell=False, # 安全:不使用 shell @@ -693,6 +749,7 @@ async def run_command(self, command: str): text=True, bufsize=1, universal_newlines=True, + env=env, # 传递环境变量 ) # 註冊進程到資源管理器 diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index 6809abd..b3039de 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -763,7 +763,11 @@ // 更新 AI 摘要區域顯示「已送出反饋」狀態 const submittedMessage = window.i18nManager ? window.i18nManager.t('feedback.submittedWaiting') : '已送出反饋,等待下次 MCP 調用...'; this.updateSummaryStatus(submittedMessage); - + + // 刷新會話列表以顯示最新狀態 + console.log('🔄 反饋提交成功,刷新會話列表'); + this.refreshSessionList(); + // 執行提交回饋後的自動命令 this.executeAutoCommandOnFeedbackSubmit(); @@ -780,6 +784,7 @@ // 如果有會話管理器,觸發數據刷新 if (this.sessionManager && this.sessionManager.dataManager) { console.log('🔄 刷新會話列表以顯示最新狀態'); + // 然後從服務器加載最新數據 this.sessionManager.dataManager.loadFromServer(); } else { console.log('⚠️ 會話管理器未初始化,跳過會話列表刷新'); @@ -851,29 +856,90 @@ window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS ); - // 局部更新頁面內容而非開啟新視窗 + // 智能選擇更新策略:音效通知啟用時使用局部更新,否則使用 window.open const self = this; setTimeout(function() { - console.log('🔄 執行局部更新頁面內容'); + // 檢查音效通知是否啟用 + const audioNotificationEnabled = self.audioManager && + self.audioManager.currentAudioSettings && + self.audioManager.currentAudioSettings.enabled; + + if (audioNotificationEnabled) { + console.log('🔊 音效通知已啟用,使用局部更新策略'); + + // 使用局部更新方案 + // 1. 更新會話資訊 + if (data.session_info) { + self.currentSessionId = data.session_info.session_id; + console.log('📋 新會話 ID:', self.currentSessionId); + } - // 1. 更新會話資訊 - if (data.session_info) { - self.currentSessionId = data.session_info.session_id; - console.log('📋 新會話 ID:', self.currentSessionId); - } + // 2. 刷新頁面內容(AI 摘要、表單等) + self.refreshPageContent(); + + // 3. 重置表單狀態 + self.clearFeedback(); + + } else { + console.log('🔇 音效通知未啟用,使用 window.open 策略'); + + try { + // 嘗試打開新標籤頁 + const newWindow = window.open(window.location.href, '_blank'); + + if (newWindow) { + console.log('✅ 新標籤頁打開成功,準備關閉當前標籤頁'); - // 2. 刷新頁面內容(AI 摘要、表單等) - self.refreshPageContent(); + // 短暫延遲後關閉當前標籤頁 + setTimeout(function() { + console.log('🔄 關閉當前標籤頁'); + window.close(); + }, 800); // 給新標籤頁一些時間加載 + + } else { + console.warn('❌ window.open 被阻止,回退到局部更新'); + + // 回退到局部更新方案 + // 1. 更新會話資訊 + if (data.session_info) { + self.currentSessionId = data.session_info.session_id; + console.log('📋 新會話 ID:', self.currentSessionId); + } + + // 2. 刷新頁面內容(AI 摘要、表單等) + self.refreshPageContent(); + + // 3. 重置表單狀態 + self.clearFeedback(); + } + } catch (error) { + console.error('❌ window.open 執行失敗:', error); + + // 回退到局部更新方案 + // 1. 更新會話資訊 + if (data.session_info) { + self.currentSessionId = data.session_info.session_id; + console.log('📋 新會話 ID:', self.currentSessionId); + } + + // 2. 刷新頁面內容(AI 摘要、表單等) + self.refreshPageContent(); + + // 3. 重置表單狀態 + self.clearFeedback(); + } + } - // 3. 重置表單狀態 - self.clearFeedback(); + // 4. 強制刷新會話列表(新會話創建時) + console.log('🔄 新會話創建,強制刷新會話列表'); + self.refreshSessionList(true); - // 4. 重置回饋狀態為等待中 + // 5. 重置回饋狀態為等待中 if (self.uiManager) { self.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, self.currentSessionId); } - // 5. 重新啟動會話超時計時器(如果已啟用) + // 6. 重新啟動會話超時計時器(如果已啟用) if (self.settingsManager && self.settingsManager.get('sessionTimeoutEnabled')) { console.log('🔄 新會話創建,重新啟動會話超時計時器'); const timeoutSettings = { @@ -883,7 +949,7 @@ self.webSocketManager.updateSessionTimeoutSettings(timeoutSettings); } - // 6. 檢查並啟動自動提交 + // 7. 檢查並啟動自動提交 self.checkAndStartAutoSubmit(); console.log('✅ 局部更新完成,頁面已準備好接收新的回饋'); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js index c765286..2e5c24b 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js @@ -322,8 +322,9 @@ * 添加檔案到列表 */ FileUploadManager.prototype.addFiles = function(files) { + // 僅將檔案轉為 Base64,前端不進行壓縮;壓縮交由後端處理 const promises = files.map(file => this.fileToBase64(file)); - + const self = this; Promise.all(promises) .then(function(base64Results) { @@ -336,26 +337,24 @@ data: base64, timestamp: Date.now() }; - + self.files.push(fileData); console.log('✅ 檔案已添加:', file.name); - + if (self.onFileAdd) { self.onFileAdd(fileData); } }); - + self.updateAllPreviews(); }) .catch(function(error) { - console.error('❌ 檔案處理失敗:', error); - const message = window.i18nManager ? - window.i18nManager.t('fileUpload.processingFailed', '檔案處理失敗,請重試') : - '檔案處理失敗,請重試'; - self.showMessage(message, 'error'); + console.error('❌ 添加檔案失敗:', error); }); }; + // 注意:前端不做壓縮,僅負責轉換與預覽,壓縮由後端處理 + /** * 將檔案轉換為 Base64 */ @@ -409,7 +408,13 @@ const img = document.createElement('img'); img.src = 'data:' + file.type + ';base64,' + file.data; img.alt = file.name; - img.title = file.name + ' (' + this.formatFileSize(file.size) + ')'; + + // 構建標題信息 + let title = file.name + ' (' + this.formatFileSize(file.size) + ')'; + if (file.compressed && file.originalSize) { + title += ' - 已壓縮,原始: ' + this.formatFileSize(file.originalSize); + } + img.title = title; // 檔案資訊 const info = document.createElement('div'); @@ -421,7 +426,14 @@ const size = document.createElement('div'); size.className = 'image-size'; - size.textContent = this.formatFileSize(file.size); + + // 顯示壓縮信息 + if (file.compressed && file.originalSize) { + size.innerHTML = this.formatFileSize(file.size) + + ' 🗜️ 已壓縮'; + } else { + size.textContent = this.formatFileSize(file.size); + } // 移除按鈕 const removeBtn = document.createElement('button'); diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js index 98ca393..1c97519 100644 --- a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js +++ b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js @@ -395,9 +395,11 @@ const self = this; - // 檢查數據是否有變化(簡單比較長度) - if (self.lastRenderedData.historyLength === sessionHistory.length) { - // 長度沒有變化,跳過渲染(可以進一步優化為深度比較) + // 檢查數據是否有變化(比較長度和狀態) + const currentHistoryHash = self.calculateHistoryHash(sessionHistory); + if (self.lastRenderedData.historyLength === sessionHistory.length && + self.lastRenderedData.historyHash === currentHistoryHash) { + // 長度和狀態都沒有變化,跳過渲染 return; } @@ -412,6 +414,20 @@ }, self.renderDebounceDelay); }; + /** + * 計算會話歷史的哈希值(用於檢測狀態變化) + */ + SessionUIRenderer.prototype.calculateHistoryHash = function(sessionHistory) { + if (!sessionHistory || sessionHistory.length === 0) { + return 'empty'; + } + + // 計算基於會話 ID 和狀態的簡單哈希 + return sessionHistory.map(session => + `${session.session_id}:${session.status}:${session.feedback_completed}` + ).join('|'); + }; + /** * 執行實際的會話歷史渲染 */ @@ -420,6 +436,7 @@ // 更新快取 this.lastRenderedData.historyLength = sessionHistory.length; + this.lastRenderedData.historyHash = this.calculateHistoryHash(sessionHistory); // 清空現有內容 DOMUtils.clearElement(this.historyList); diff --git a/tests/helpers/mcp_client.py b/tests/helpers/mcp_client.py index 3068f79..7ea9b4f 100644 --- a/tests/helpers/mcp_client.py +++ b/tests/helpers/mcp_client.py @@ -5,6 +5,7 @@ import asyncio import json +import os import subprocess from pathlib import Path from typing import Any @@ -29,6 +30,13 @@ async def start_server(self) -> bool: # 使用正確的 uv run 命令啟動 MCP 服務器 cmd = ["uv", "run", "python", "-m", "mcp_feedback_enhanced"] + # 確保環境變數正確傳遞 + env = os.environ.copy() + if "MCP_AI_CLIENT" not in env or not env["MCP_AI_CLIENT"]: + env["MCP_AI_CLIENT"] = "augment" # 測試時默認使用 augment + if "MCP_DEBUG" not in env: + env["MCP_DEBUG"] = "true" # 測試時啟用調試 + self.server_process = subprocess.Popen( cmd, stdin=subprocess.PIPE, @@ -39,6 +47,7 @@ async def start_server(self) -> bool: cwd=Path.cwd(), encoding="utf-8", # 明確指定 UTF-8 編碼 errors="replace", # 處理編碼錯誤 + env=env, # 傳遞環境變數 ) self.stdin = self.server_process.stdin