diff --git a/.gitignore b/.gitignore index 5ce68c3..e0226ea 100644 --- a/.gitignore +++ b/.gitignore @@ -10,13 +10,62 @@ wheels/ .venv*/ venv*/ +# Node.js / Electron (Desktop App) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +yarn.lock +.npm +.yarn-integrity + +# Electron build outputs +dist-desktop/ +out/ +*.dmg +*.exe +*.deb +*.rpm +*.AppImage +*.zip +*.tar.gz + +# Electron cache +.electron/ +.electron-gyp/ + # Logs *.log -#Others +# macOS .DS_Store +.AppleDouble +.LSOverride +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# Linux +*~ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Others .cursor/rules/ uv.lock .mcp_feedback_settings.json -test_reports/ \ No newline at end of file +test_reports/ + +# Temporary test files +test_*.py + +# User configuration files +ui_settings.json +.config/ \ No newline at end of file diff --git a/electron-builder.json b/electron-builder.json new file mode 100644 index 0000000..45c37f4 --- /dev/null +++ b/electron-builder.json @@ -0,0 +1,154 @@ +{ + "appId": "com.minidoracat.mcp-feedback-enhanced", + "productName": "MCP Feedback Enhanced", + "copyright": "Copyright © 2024 Minidoracat", + "directories": { + "app": "src/mcp_feedback_enhanced/desktop", + "output": "dist-desktop", + "buildResources": "src/mcp_feedback_enhanced/desktop/assets" + }, + "files": [ + "main.js", + "preload.js", + "package.json", + "node_modules/**/*" + ], + "extraResources": [ + { + "from": "src/mcp_feedback_enhanced", + "to": "app", + "filter": [ + "**/*", + "!desktop/node_modules/**/*", + "!desktop/dist/**/*", + "!**/*.pyc", + "!**/__pycache__/**/*", + "!**/test_*.py" + ] + } + ], + "compression": "normal", + "removePackageScripts": true, + "win": { + "target": [ + { + "target": "nsis", + "arch": ["x64", "ia32"] + }, + { + "target": "portable", + "arch": ["x64"] + } + ], + "icon": "src/mcp_feedback_enhanced/desktop/assets/icon.ico", + "requestedExecutionLevel": "asInvoker", + "artifactName": "${productName}-${version}-${arch}.${ext}", + "publisherName": "Minidoracat" + }, + "mac": { + "target": [ + { + "target": "dmg", + "arch": ["x64", "arm64"] + }, + { + "target": "zip", + "arch": ["x64", "arm64"] + } + ], + "icon": "src/mcp_feedback_enhanced/desktop/assets/icon.icns", + "category": "public.app-category.developer-tools", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "artifactName": "${productName}-${version}-${arch}.${ext}" + }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": ["x64"] + }, + { + "target": "deb", + "arch": ["x64"] + }, + { + "target": "rpm", + "arch": ["x64"] + } + ], + "icon": "src/mcp_feedback_enhanced/desktop/assets/icon.png", + "category": "Development", + "artifactName": "${productName}-${version}-${arch}.${ext}", + "desktop": { + "StartupNotify": "true", + "Encoding": "UTF-8", + "MimeType": "x-scheme-handler/mcp-feedback", + "Keywords": "mcp;feedback;ai;development" + } + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "allowElevation": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "MCP Feedback Enhanced", + "include": "build/installer.nsh", + "artifactName": "${productName}-Setup-${version}.${ext}", + "deleteAppDataOnUninstall": false + }, + "dmg": { + "title": "${productName} ${version}", + "icon": "src/mcp_feedback_enhanced/desktop/assets/icon.icns", + "background": "src/mcp_feedback_enhanced/desktop/assets/dmg-background.png", + "contents": [ + { + "x": 130, + "y": 220, + "type": "file" + }, + { + "x": 410, + "y": 220, + "type": "link", + "path": "/Applications" + } + ], + "window": { + "width": 540, + "height": 380 + }, + "artifactName": "${productName}-${version}.${ext}" + }, + "appImage": { + "artifactName": "${productName}-${version}-${arch}.${ext}" + }, + "deb": { + "artifactName": "${productName}-${version}-${arch}.${ext}", + "priority": "optional", + "depends": [ + "gconf2", + "gconf-service", + "libnotify4", + "libappindicator1", + "libxtst6", + "libnss3" + ] + }, + "rpm": { + "artifactName": "${productName}-${version}-${arch}.${ext}", + "depends": [ + "libXScrnSaver", + "libnotify", + "libnss3" + ] + }, + "publish": { + "provider": "github", + "owner": "minidoracat", + "repo": "mcp-feedback-enhanced", + "releaseType": "draft" + }, + "buildVersion": "2.3.0" +} diff --git a/pyproject.toml b/pyproject.toml index 7655e56..a477ec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "mcp-feedback-enhanced" version = "2.3.0" -description = "Enhanced MCP server for interactive user feedback and command execution in AI-assisted development, featuring Web UI with intelligent environment detection." +description = "Enhanced MCP server for interactive user feedback and command execution in AI-assisted development, featuring Web UI with intelligent environment detection and optional Electron desktop application." readme = "README.md" requires-python = ">=3.11" authors = [ { name = "Minidoracat", email = "minidora0702@gmail.com" } ] -keywords = ["mcp", "ai", "feedback", "web-ui", "interactive", "development"] +keywords = ["mcp", "ai", "feedback", "web-ui", "interactive", "development", "electron", "desktop", "cross-platform"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -34,6 +34,9 @@ dev = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", ] +desktop = [ + "nodejs>=16.0.0", # Node.js 環境(需要系統安裝) +] [project.urls] Homepage = "https://github.com/Minidoracat/mcp-feedback-enhanced" diff --git a/src/mcp_feedback_enhanced/__main__.py b/src/mcp_feedback_enhanced/__main__.py index 6d7c066..37d3dbf 100644 --- a/src/mcp_feedback_enhanced/__main__.py +++ b/src/mcp_feedback_enhanced/__main__.py @@ -14,6 +14,19 @@ MCP Interactive Feedback Enhanced - 主程式入口 import sys import argparse import os +import asyncio +import warnings + +# 抑制 Windows 上的 asyncio ResourceWarning +if sys.platform == 'win32': + warnings.filterwarnings("ignore", category=ResourceWarning, message=".*unclosed transport.*") + warnings.filterwarnings("ignore", category=ResourceWarning, message=".*unclosed.*") + + # 設置 asyncio 事件循環策略以減少警告 + try: + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + except AttributeError: + pass def main(): """主程式入口點""" @@ -29,6 +42,9 @@ def main(): # 測試命令 test_parser = subparsers.add_parser('test', help='執行測試') test_parser.add_argument('--web', action='store_true', help='測試 Web UI (自動持續運行)') + test_parser.add_argument('--desktop', action='store_true', help='測試桌面應用 (啟動 Electron 應用)') + test_parser.add_argument('--full', action='store_true', help='完整整合測試 (Web + 桌面)') + test_parser.add_argument('--electron-only', action='store_true', help='僅測試 Electron 環境') test_parser.add_argument('--timeout', type=int, default=60, help='測試超時時間 (秒)') # 版本命令 @@ -59,14 +75,37 @@ def run_tests(args): # 啟用調試模式以顯示測試過程 os.environ["MCP_DEBUG"] = "true" + # 在 Windows 上抑制 asyncio 警告 + if sys.platform == 'win32': + os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning" + if args.web: print("🧪 執行 Web UI 測試...") success = test_web_ui_simple() if not success: sys.exit(1) + elif args.desktop: + print("🧪 執行桌面應用測試...") + success = test_desktop_app() + if not success: + sys.exit(1) + elif args.full: + print("🧪 執行完整整合測試...") + success = test_full_integration() + if not success: + sys.exit(1) + elif args.electron_only: + print("🧪 執行 Electron 環境測試...") + success = test_electron_environment() + if not success: + sys.exit(1) else: print("❌ 測試功能已簡化") - print("💡 對於用戶:使用 'test --web' 測試 Web UI") + print("💡 可用的測試選項:") + print(" --web 測試 Web UI") + print(" --desktop 測試桌面應用") + print(" --full 完整整合測試") + print(" --electron-only 僅測試 Electron 環境") print("💡 對於開發者:使用 'uv run pytest' 執行完整測試") sys.exit(1) @@ -135,6 +174,215 @@ def test_web_ui_simple(): return False +def test_desktop_app(): + """測試桌面應用""" + try: + print("🔧 檢查桌面環境...") + + # 檢查桌面環境可用性 + from .desktop import is_desktop_available + if not is_desktop_available(): + print("❌ 桌面環境不可用") + print("💡 請確保 Node.js 已安裝且不在遠程環境中") + return False + + print("✅ 桌面環境檢查通過") + + # 設置桌面模式 + os.environ['MCP_FEEDBACK_MODE'] = 'desktop' + + print("🔧 創建 Electron 管理器...") + from .desktop.electron_manager import ElectronManager + import asyncio + + async def run_desktop_test(): + print("🚀 啟動完整桌面應用測試...") + print("💡 這將啟動 Web 服務器和 Electron 應用視窗") + print("💡 請在應用中測試基本功能,然後關閉視窗") + + # 使用完整的桌面應用啟動函數 + from .desktop import launch_desktop_app + + try: + # 這會啟動 Web 服務器和 Electron 應用 + result = await launch_desktop_app( + os.getcwd(), + "桌面應用測試 - 驗證 Electron 整合功能", + 300 # 5分鐘超時 + ) + + print("✅ 桌面應用測試完成") + print(f"收到回饋: {result.get('interactive_feedback', '無回饋')}") + return True + + except Exception as e: + print(f"❌ 桌面應用測試失敗: {e}") + return False + + return asyncio.run(run_desktop_test()) + + except Exception as e: + print(f"❌ 桌面應用測試失敗: {e}") + import traceback + traceback.print_exc() + return False + + +async def wait_for_process(process): + """等待進程結束""" + try: + # 等待進程自然結束 + await process.wait() + + # 確保管道正確關閉 + try: + if hasattr(process, 'stdout') and process.stdout: + process.stdout.close() + if hasattr(process, 'stderr') and process.stderr: + process.stderr.close() + if hasattr(process, 'stdin') and process.stdin: + process.stdin.close() + except Exception as close_error: + print(f"關閉進程管道時出錯: {close_error}") + + except Exception as e: + print(f"等待進程時出錯: {e}") + + +def test_electron_environment(): + """測試 Electron 環境""" + try: + print("🔧 檢查 Electron 環境...") + + # 檢查 Node.js + import subprocess + try: + result = subprocess.run(['node', '--version'], + capture_output=True, text=True, timeout=10) + if result.returncode == 0: + print(f"✅ Node.js 版本: {result.stdout.strip()}") + else: + print("❌ Node.js 不可用") + return False + except (subprocess.TimeoutExpired, FileNotFoundError): + print("❌ Node.js 不可用") + return False + + # 檢查桌面模組 + from .desktop import is_desktop_available + if is_desktop_available(): + print("✅ 桌面環境可用") + else: + print("❌ 桌面環境不可用") + return False + + # 檢查 Electron 管理器 + from .desktop.electron_manager import ElectronManager + manager = ElectronManager() + + if manager.is_electron_available(): + print("✅ Electron 環境可用") + else: + print("❌ Electron 環境不可用") + return False + + # 檢查文件結構 + desktop_dir = manager.desktop_dir + required_files = ['main.js', 'preload.js', 'package.json'] + + for file_name in required_files: + file_path = desktop_dir / file_name + if file_path.exists(): + print(f"✅ {file_name} 存在") + else: + print(f"❌ {file_name} 不存在") + return False + + # 檢查 node_modules + node_modules = desktop_dir / "node_modules" + if node_modules.exists(): + print("✅ node_modules 存在") + else: + print("❌ node_modules 不存在") + return False + + print("🎉 Electron 環境測試完成,所有檢查通過") + return True + + except Exception as e: + print(f"❌ Electron 環境測試失敗: {e}") + return False + + +def test_full_integration(): + """完整整合測試""" + try: + print("🧪 執行完整整合測試...") + + # 1. 環境變數測試 + print("\n📋 1. 測試環境變數控制...") + test_cases = [("auto", "auto"), ("web", "web"), ("desktop", "desktop")] + + for env_value, expected in test_cases: + os.environ['MCP_FEEDBACK_MODE'] = env_value + + # 重新導入以獲取新的環境變數值 + import sys + if 'mcp_feedback_enhanced.server' in sys.modules: + del sys.modules['mcp_feedback_enhanced.server'] + + from .server import get_feedback_mode + actual = get_feedback_mode().value + + if actual == expected: + print(f" ✅ MCP_FEEDBACK_MODE='{env_value}' → {actual}") + else: + print(f" ❌ MCP_FEEDBACK_MODE='{env_value}' → {actual} (期望: {expected})") + return False + + # 2. Electron 環境測試 + print("\n📋 2. 測試 Electron 環境...") + if not test_electron_environment(): + print("❌ Electron 環境測試失敗") + return False + + # 3. Web UI 基本功能測試 + print("\n📋 3. 測試 Web UI 基本功能...") + from .web.main import WebUIManager + import tempfile + + with tempfile.TemporaryDirectory() as temp_dir: + manager = WebUIManager(host="127.0.0.1", port=8766) # 使用不同端口避免衝突 + session_id = manager.create_session(temp_dir, "整合測試會話") + + if session_id: + print(" ✅ Web UI 會話創建成功") + else: + print(" ❌ Web UI 會話創建失敗") + return False + + # 4. 桌面模式檢測測試 + print("\n📋 4. 測試桌面模式檢測...") + os.environ['MCP_FEEDBACK_MODE'] = 'desktop' + + manager = WebUIManager() + if manager.should_use_desktop_mode(): + print(" ✅ 桌面模式檢測正常") + else: + print(" ❌ 桌面模式檢測失敗") + return False + + print("\n🎉 完整整合測試通過!") + print("💡 所有核心功能正常運作") + return True + + except Exception as e: + print(f"❌ 完整整合測試失敗: {e}") + import traceback + traceback.print_exc() + return False + + def show_version(): """顯示版本資訊""" from . import __version__, __author__ diff --git a/src/mcp_feedback_enhanced/desktop/__init__.py b/src/mcp_feedback_enhanced/desktop/__init__.py new file mode 100644 index 0000000..18a7b31 --- /dev/null +++ b/src/mcp_feedback_enhanced/desktop/__init__.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +桌面應用模組 +=========== + +此模組提供 Electron 桌面應用整合功能,實現零前端改動的桌面化方案。 + +主要功能: +- Electron 進程管理 +- 與現有 Web UI 的無縫整合 +- 跨平台桌面應用支援 +- 環境變數控制 + +作者: Augment Agent +版本: 2.3.0 +""" + +import os +import sys +from typing import Optional + +from ..debug import web_debug_log as debug_log + + +def is_desktop_available() -> bool: + """ + 檢測桌面環境是否可用 + + Returns: + bool: True 表示桌面環境可用 + """ + try: + # 檢查是否有 Node.js 環境 + import subprocess + result = subprocess.run(['node', '--version'], + capture_output=True, + text=True, + timeout=5) + if result.returncode != 0: + debug_log("Node.js 不可用,桌面模式不可用") + return False + + # 檢查是否為遠程環境 + from ..server import is_remote_environment + if is_remote_environment(): + debug_log("檢測到遠程環境,桌面模式不適用") + return False + + debug_log("桌面環境檢測通過") + return True + + except (subprocess.TimeoutExpired, FileNotFoundError, ImportError) as e: + debug_log(f"桌面環境檢測失敗: {e}") + return False + except Exception as e: + debug_log(f"桌面環境檢測出現未預期錯誤: {e}") + return False + + +async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> dict: + """ + 啟動桌面應用收集回饋 + + Args: + project_dir: 專案目錄路徑 + summary: AI 工作摘要 + timeout: 超時時間(秒) + + Returns: + dict: 收集到的回饋資料 + """ + debug_log("啟動桌面應用...") + + try: + # 創建 Electron 管理器 + from .electron_manager import ElectronManager + manager = ElectronManager() + + # 首先啟動 Web 服務器(桌面應用需要載入 Web UI) + from ..web import get_web_ui_manager + web_manager = get_web_ui_manager() + + # 創建會話 + session_id = web_manager.create_session(project_dir, summary) + session = web_manager.get_current_session() + + if not session: + raise RuntimeError("無法創建回饋會話") + + # 啟動 Web 服務器(如果尚未啟動) + if not web_manager.server_thread or not web_manager.server_thread.is_alive(): + debug_log("啟動 Web 服務器...") + web_manager.start_server() + + # 等待 Web 服務器完全啟動 + import time + debug_log("等待 Web 服務器啟動...") + time.sleep(5) # 增加等待時間 + + # 驗證 Web 服務器是否正常運行 + if web_manager.server_thread and web_manager.server_thread.is_alive(): + debug_log(f"✅ Web 服務器成功啟動在端口: {web_manager.port}") + else: + raise RuntimeError(f"Web 服務器啟動失敗") + + # 設置 Web 服務器端口 + manager.set_web_server_port(web_manager.port) + debug_log(f"桌面應用將連接到: http://localhost:{web_manager.port}") + + # 啟動桌面應用 + desktop_success = await manager.launch_desktop_app(summary, project_dir) + + if desktop_success: + debug_log("桌面應用啟動成功,等待用戶回饋...") + # 等待用戶回饋 + result = await session.wait_for_feedback(timeout) + debug_log("收到桌面應用用戶回饋") + return result + else: + debug_log("桌面應用啟動失敗,回退到 Web 模式") + # 回退到 Web 模式 + from ..web import launch_web_feedback_ui + return await launch_web_feedback_ui(project_dir, summary, timeout) + + except Exception as e: + debug_log(f"桌面應用啟動過程中出錯: {e}") + debug_log("回退到 Web 模式") + # 回退到 Web 模式 + from ..web import launch_web_feedback_ui + return await launch_web_feedback_ui(project_dir, summary, timeout) + + +class ElectronManager: + """Electron 管理器 - 預留接口""" + + def __init__(self): + """初始化 Electron 管理器""" + self.electron_process = None + self.web_server_port = None + debug_log("ElectronManager 初始化(預留實現)") + + async def launch_desktop_app(self, summary: str, project_dir: str) -> bool: + """ + 啟動桌面應用 + + Args: + summary: AI 工作摘要 + project_dir: 專案目錄 + + Returns: + bool: 啟動是否成功 + """ + debug_log("桌面應用啟動功能尚未實現") + debug_log("此功能將在階段 2 中實現") + return False + + def is_available(self) -> bool: + """檢查桌面管理器是否可用""" + return is_desktop_available() + + +# 主要導出介面 +__all__ = [ + "is_desktop_available", + "launch_desktop_app", + "ElectronManager" +] diff --git a/src/mcp_feedback_enhanced/desktop/assets/README.md b/src/mcp_feedback_enhanced/desktop/assets/README.md new file mode 100644 index 0000000..559c574 --- /dev/null +++ b/src/mcp_feedback_enhanced/desktop/assets/README.md @@ -0,0 +1,39 @@ +# 應用圖標資源 + +此目錄包含桌面應用的圖標資源文件。 + +## 需要的圖標文件 + +### Windows +- `icon.ico` - Windows 圖標文件(多尺寸:16x16, 32x32, 48x48, 256x256) + +### macOS +- `icon.icns` - macOS 圖標文件(多尺寸:16x16 到 1024x1024) +- `entitlements.mac.plist` - macOS 權限配置文件 + +### Linux +- `icon.png` - Linux 圖標文件(建議 512x512) + +### 打包資源 +- `dmg-background.png` - macOS DMG 背景圖片(540x380) + +## 圖標設計建議 + +- 使用 MCP Feedback Enhanced 的品牌色彩 +- 簡潔明了的設計,在小尺寸下仍然清晰 +- 符合各平台的設計規範 +- 建議使用矢量圖形作為源文件 + +## 臨時解決方案 + +在開發階段,可以使用以下方式創建基本圖標: + +1. 使用線上圖標生成器 +2. 從現有的 Web UI favicon 轉換 +3. 使用系統預設圖標 + +## 注意事項 + +- 圖標文件應該放在此目錄中 +- electron-builder 會自動處理圖標的打包 +- 確保圖標文件的版權合規性 diff --git a/src/mcp_feedback_enhanced/desktop/assets/icon-placeholder.txt b/src/mcp_feedback_enhanced/desktop/assets/icon-placeholder.txt new file mode 100644 index 0000000..da8496f --- /dev/null +++ b/src/mcp_feedback_enhanced/desktop/assets/icon-placeholder.txt @@ -0,0 +1,28 @@ +# 圖標佔位符 + +此文件標記圖標資源的位置。在實際部署中,需要替換為真實的圖標文件: + +## 需要的圖標文件: + +### Windows +- icon.ico (多尺寸:16x16, 32x32, 48x48, 256x256) + +### macOS +- icon.icns (多尺寸:16x16 到 1024x1024) + +### Linux +- icon.png (建議 512x512) + +## 臨時解決方案 + +在開發階段,可以: +1. 使用 Electron 預設圖標 +2. 從現有 Web UI 的 favicon 轉換 +3. 使用線上圖標生成器創建基本圖標 + +## 圖標設計建議 + +- 簡潔明了,體現 MCP 和回饋收集的概念 +- 在小尺寸下仍然清晰可辨 +- 符合各平台的設計規範 +- 使用一致的色彩方案 diff --git a/src/mcp_feedback_enhanced/desktop/electron_manager.py b/src/mcp_feedback_enhanced/desktop/electron_manager.py new file mode 100644 index 0000000..a03b30e --- /dev/null +++ b/src/mcp_feedback_enhanced/desktop/electron_manager.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Electron 管理器 +============== + +此模組負責管理 Electron 進程的生命週期,包括: +- Electron 應用啟動和停止 +- 與 Web 服務器的整合 +- 依賴檢測和自動安裝 +- 進程管理和錯誤處理 + +此文件為階段 1 的預留實現,完整功能將在階段 2 中實現。 + +作者: Augment Agent +版本: 2.3.0 +""" + +import subprocess +import asyncio +import os +from pathlib import Path +from typing import Optional + +from ..debug import web_debug_log as debug_log +from ..utils.error_handler import ErrorHandler, ErrorType + + +class ElectronManager: + """Electron 進程管理器""" + + def __init__(self): + """初始化 Electron 管理器""" + self.electron_process: Optional[subprocess.Popen] = None + self.desktop_dir = Path(__file__).parent + self.web_server_port: Optional[int] = None + + debug_log("ElectronManager 初始化完成") + debug_log(f"桌面模組目錄: {self.desktop_dir}") + + async def launch_desktop_app(self, summary: str, project_dir: str) -> bool: + """ + 啟動 Electron 桌面應用 + + Args: + summary: AI 工作摘要 + project_dir: 專案目錄 + + Returns: + bool: 啟動是否成功 + """ + debug_log("=== 桌面應用啟動 ===") + debug_log(f"摘要: {summary}") + debug_log(f"專案目錄: {project_dir}") + + try: + # 確保依賴已安裝 + if not await self.ensure_dependencies(): + debug_log("依賴檢查失敗,無法啟動桌面應用") + return False + + # 啟動 Electron 應用 + success = await self._start_electron_process() + if success: + debug_log("Electron 桌面應用啟動成功") + return True + else: + debug_log("Electron 桌面應用啟動失敗") + return False + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "桌面應用啟動", "project_dir": project_dir}, + error_type=ErrorType.SYSTEM + ) + debug_log(f"桌面應用啟動異常 [錯誤ID: {error_id}]: {e}") + return False + + def set_web_server_port(self, port: int): + """設置 Web 服務器端口""" + self.web_server_port = port + debug_log(f"設置 Web 服務器端口: {port}") + + def is_electron_available(self) -> bool: + """檢查 Electron 是否可用""" + try: + # 檢查 Node.js + result = subprocess.run(['node', '--version'], + capture_output=True, + text=True, + timeout=5) + if result.returncode != 0: + debug_log("Node.js 不可用") + return False + + debug_log(f"Node.js 版本: {result.stdout.strip()}") + + # 檢查 package.json 是否存在 + package_json = self.desktop_dir / "package.json" + if not package_json.exists(): + debug_log("package.json 不存在,需要在階段 2 中創建") + return False + + return True + + except Exception as e: + debug_log(f"Electron 可用性檢查失敗: {e}") + return False + + async def ensure_dependencies(self) -> bool: + """確保依賴已安裝""" + debug_log("檢查 Electron 依賴...") + + try: + # 檢查 package.json 是否存在 + package_json = self.desktop_dir / "package.json" + if not package_json.exists(): + debug_log("package.json 不存在,創建中...") + await self._create_package_json() + + # 檢查 node_modules 是否存在 + node_modules = self.desktop_dir / "node_modules" + if not node_modules.exists(): + debug_log("node_modules 不存在,安裝依賴中...") + if not await self._install_dependencies(): + return False + + # 檢查 main.js 是否存在 + main_js = self.desktop_dir / "main.js" + if not main_js.exists(): + debug_log("main.js 不存在,將在後續步驟中創建") + return False + + debug_log("所有依賴檢查通過") + return True + + except Exception as e: + error_id = ErrorHandler.log_error_with_context( + e, + context={"operation": "依賴檢查"}, + error_type=ErrorType.DEPENDENCY + ) + debug_log(f"依賴檢查失敗 [錯誤ID: {error_id}]: {e}") + return False + + def cleanup(self): + """清理資源""" + if self.electron_process: + try: + # 檢查進程是否還在運行 + if self.electron_process.returncode is None: + self.electron_process.terminate() + debug_log("Electron 進程已終止") + else: + debug_log("Electron 進程已自然結束") + except Exception as e: + debug_log(f"終止 Electron 進程時出錯: {e}") + try: + if self.electron_process.returncode is None: + self.electron_process.kill() + debug_log("強制終止 Electron 進程") + except Exception as kill_error: + debug_log(f"強制終止 Electron 進程失敗: {kill_error}") + finally: + # 關閉管道以避免 ResourceWarning + try: + # 對於 asyncio 子進程,需要特殊處理 + if hasattr(self.electron_process, 'stdout') and self.electron_process.stdout: + if hasattr(self.electron_process.stdout, 'close'): + self.electron_process.stdout.close() + if hasattr(self.electron_process, 'stderr') and self.electron_process.stderr: + if hasattr(self.electron_process.stderr, 'close'): + self.electron_process.stderr.close() + if hasattr(self.electron_process, 'stdin') and self.electron_process.stdin: + if hasattr(self.electron_process.stdin, 'close'): + self.electron_process.stdin.close() + except Exception: + # 忽略管道關閉錯誤,這些通常是無害的 + pass + + self.electron_process = None + + async def _create_package_json(self): + """創建 package.json 文件""" + package_config = { + "name": "mcp-feedback-enhanced-desktop", + "version": "2.3.0", + "description": "MCP Feedback Enhanced Desktop Application", + "main": "main.js", + "scripts": { + "start": "electron .", + "dev": "electron . --dev" + }, + "dependencies": { + "electron": "^28.0.0" + }, + "devDependencies": { + "electron-builder": "^24.0.0" + } + } + + package_json_path = self.desktop_dir / "package.json" + with open(package_json_path, 'w', encoding='utf-8') as f: + import json + json.dump(package_config, f, indent=2, ensure_ascii=False) + + debug_log(f"已創建 package.json: {package_json_path}") + + async def _install_dependencies(self) -> bool: + """安裝 Node.js 依賴""" + debug_log("開始安裝 Node.js 依賴...") + + try: + # 使用 npm install + install_cmd = ['npm', 'install'] + process = await asyncio.create_subprocess_exec( + *install_cmd, + cwd=self.desktop_dir, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + _, stderr = await process.communicate() + + if process.returncode == 0: + debug_log("Node.js 依賴安裝成功") + return True + else: + debug_log(f"依賴安裝失敗: {stderr.decode()}") + return False + + except Exception as e: + debug_log(f"依賴安裝過程中出錯: {e}") + return False + + async def _start_electron_process(self) -> bool: + """啟動 Electron 進程""" + debug_log("啟動 Electron 進程...") + + try: + # 構建 Electron 命令 - 使用本地安裝的 electron + import platform + if platform.system() == "Windows": + electron_path = self.desktop_dir / "node_modules" / ".bin" / "electron.cmd" + else: + electron_path = self.desktop_dir / "node_modules" / ".bin" / "electron" + + if electron_path.exists(): + electron_cmd = [ + str(electron_path), '.', + '--port', str(self.web_server_port or 8765) + ] + else: + # 回退到 npx + electron_cmd = [ + 'npx', 'electron', '.', + '--port', str(self.web_server_port or 8765) + ] + + debug_log(f"使用 Electron 命令: {' '.join(electron_cmd)}") + + # 啟動 Electron 進程 + self.electron_process = await asyncio.create_subprocess_exec( + *electron_cmd, + cwd=self.desktop_dir, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + debug_log(f"Electron 進程已啟動,PID: {self.electron_process.pid}") + + # 等待一小段時間確保進程正常啟動 + await asyncio.sleep(2) + + # 檢查進程是否仍在運行 + if self.electron_process.returncode is None: + debug_log("Electron 進程運行正常") + return True + else: + debug_log(f"Electron 進程異常退出,返回碼: {self.electron_process.returncode}") + # 讀取錯誤輸出 + try: + _, stderr = await self.electron_process.communicate() + if stderr: + debug_log(f"Electron 錯誤輸出: {stderr.decode()}") + except Exception as e: + debug_log(f"讀取 Electron 錯誤輸出失敗: {e}") + return False + + except Exception as e: + debug_log(f"啟動 Electron 進程失敗: {e}") + return False + + def __del__(self): + """析構函數""" + self.cleanup() + + +# 便利函數 +async def create_electron_manager() -> ElectronManager: + """創建 Electron 管理器實例""" + manager = ElectronManager() + + # 檢查可用性 + if not manager.is_electron_available(): + debug_log("Electron 環境不可用,建議使用 Web 模式") + + return manager diff --git a/src/mcp_feedback_enhanced/desktop/main.js b/src/mcp_feedback_enhanced/desktop/main.js new file mode 100644 index 0000000..d8e8847 --- /dev/null +++ b/src/mcp_feedback_enhanced/desktop/main.js @@ -0,0 +1,306 @@ +#!/usr/bin/env node +/** + * Electron 主進程 + * =============== + * + * 此文件是 MCP Feedback Enhanced 桌面應用的主進程入口點。 + * 負責創建和管理 BrowserWindow,以及與現有 Web UI 的整合。 + * + * 主要功能: + * - 創建和管理應用視窗 + * - 載入本地 Web 服務器內容 + * - 處理應用生命週期事件 + * - 提供桌面應用特有的功能 + * + * 作者: Augment Agent + * 版本: 2.3.0 + */ + +const { app, BrowserWindow, ipcMain, shell } = require('electron'); +const path = require('path'); +const os = require('os'); + +// 應用配置 +const APP_CONFIG = { + name: 'MCP Feedback Enhanced', + width: 1200, + height: 800, + minWidth: 800, + minHeight: 600, + defaultPort: 8765 +}; + +// 全局變數 +let mainWindow = null; +let webServerPort = APP_CONFIG.defaultPort; + +/** + * Electron 應用類 + */ +class ElectronApp { + constructor() { + this.mainWindow = null; + this.webServerPort = APP_CONFIG.defaultPort; + this.isDevMode = process.argv.includes('--dev'); + + this.setupEventHandlers(); + this.parseCommandLineArgs(); + } + + /** + * 解析命令行參數 + */ + parseCommandLineArgs() { + const args = process.argv; + const portIndex = args.indexOf('--port'); + + if (portIndex !== -1 && portIndex + 1 < args.length) { + const port = parseInt(args[portIndex + 1]); + if (!isNaN(port) && port > 0 && port < 65536) { + this.webServerPort = port; + console.log(`使用指定端口: ${port}`); + } + } + } + + /** + * 設置事件處理器 + */ + setupEventHandlers() { + // 應用準備就緒 + app.whenReady().then(() => { + this.createWindow(); + this.setupIpcHandlers(); + }); + + // 所有視窗關閉 + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } + }); + + // 應用激活(macOS) + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + this.createWindow(); + } + }); + + // 處理證書錯誤(開發模式) + app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { + if (this.isDevMode && url.startsWith('https://localhost')) { + event.preventDefault(); + callback(true); + } else { + callback(false); + } + }); + } + + /** + * 創建主視窗 + */ + async createWindow() { + console.log('創建主視窗...'); + console.log(`視窗配置: ${APP_CONFIG.width}x${APP_CONFIG.height}`); + + this.mainWindow = new BrowserWindow({ + width: APP_CONFIG.width, + height: APP_CONFIG.height, + minWidth: APP_CONFIG.minWidth, + minHeight: APP_CONFIG.minHeight, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + enableRemoteModule: false, + preload: path.join(__dirname, 'preload.js'), + webSecurity: !this.isDevMode + }, + icon: this.getAppIcon(), + title: APP_CONFIG.name, + show: false, // 先隱藏,等載入完成後顯示 + titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', + alwaysOnTop: false, // 不總是置頂,但確保可見 + center: true, // 居中顯示 + resizable: true + }); + + // 載入 Web UI + await this.loadWebUI(); + + // 視窗準備顯示 + this.mainWindow.once('ready-to-show', () => { + console.log('視窗準備顯示,正在顯示視窗...'); + this.mainWindow.show(); + this.mainWindow.focus(); // 確保視窗獲得焦點 + + if (this.isDevMode) { + this.mainWindow.webContents.openDevTools(); + } + }); + + // 備用顯示機制:如果 ready-to-show 沒有觸發,強制顯示 + setTimeout(() => { + if (this.mainWindow && !this.mainWindow.isVisible()) { + console.log('備用顯示機制:強制顯示視窗'); + this.mainWindow.show(); + this.mainWindow.focus(); + } + }, 3000); // 3秒後強制顯示 + + // 處理視窗關閉 + this.mainWindow.on('closed', () => { + this.mainWindow = null; + }); + + // 處理外部連結 + this.mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: 'deny' }; + }); + + console.log('主視窗創建完成'); + } + + /** + * 載入 Web UI + */ + async loadWebUI() { + const webUrl = `http://localhost:${this.webServerPort}`; + console.log(`載入 Web UI: ${webUrl}`); + + try { + await this.mainWindow.loadURL(webUrl); + console.log('Web UI 載入成功'); + } catch (error) { + console.error('載入 Web UI 失敗:', error); + + // 載入錯誤頁面 + const errorHtml = this.createErrorPage(error); + await this.mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(errorHtml)}`); + + // 確保錯誤頁面也能顯示視窗 + console.log('載入錯誤頁面,強制顯示視窗'); + this.mainWindow.show(); + this.mainWindow.focus(); + } + } + + /** + * 創建錯誤頁面 + */ + createErrorPage(error) { + return ` + + + + 連接錯誤 - ${APP_CONFIG.name} + + + +
+

無法連接到 Web 服務器

+

桌面應用無法連接到本地 Web 服務器(端口 ${this.webServerPort})。

+

請確保 MCP 服務器正在運行。

+

錯誤詳情: ${error.message}

+ +
+ + + `; + } + + /** + * 獲取應用圖標 + */ + getAppIcon() { + const iconPath = path.join(__dirname, 'assets'); + + if (process.platform === 'win32') { + return path.join(iconPath, 'icon.ico'); + } else if (process.platform === 'darwin') { + return path.join(iconPath, 'icon.icns'); + } else { + return path.join(iconPath, 'icon.png'); + } + } + + /** + * 設置 IPC 處理器 + */ + setupIpcHandlers() { + // 視窗控制 + ipcMain.handle('window-minimize', () => { + if (this.mainWindow) { + this.mainWindow.minimize(); + } + }); + + ipcMain.handle('window-maximize', () => { + if (this.mainWindow) { + if (this.mainWindow.isMaximized()) { + this.mainWindow.unmaximize(); + } else { + this.mainWindow.maximize(); + } + } + }); + + ipcMain.handle('window-close', () => { + if (this.mainWindow) { + this.mainWindow.close(); + } + }); + + // 獲取系統資訊 + ipcMain.handle('get-system-info', () => { + return { + platform: process.platform, + arch: process.arch, + nodeVersion: process.version, + electronVersion: process.versions.electron, + chromeVersion: process.versions.chrome + }; + }); + + console.log('IPC 處理器設置完成'); + } +} + +// 創建應用實例 +const electronApp = new ElectronApp(); + +// 導出供外部使用 +module.exports = electronApp; diff --git a/src/mcp_feedback_enhanced/desktop/package.json b/src/mcp_feedback_enhanced/desktop/package.json new file mode 100644 index 0000000..601e7fd --- /dev/null +++ b/src/mcp_feedback_enhanced/desktop/package.json @@ -0,0 +1,169 @@ +{ + "name": "mcp-feedback-enhanced-desktop", + "version": "2.3.0", + "description": "MCP Feedback Enhanced Desktop Application - Electron-based desktop interface for AI feedback collection", + "main": "main.js", + "author": { + "name": "Augment Agent", + "email": "minidora0702@gmail.com" + }, + "license": "MIT", + "homepage": "https://github.com/minidoracat/mcp-feedback-enhanced", + "repository": { + "type": "git", + "url": "https://github.com/minidoracat/mcp-feedback-enhanced.git" + }, + "keywords": [ + "electron", + "mcp", + "feedback", + "ai", + "desktop", + "cross-platform" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "scripts": { + "start": "electron .", + "dev": "electron . --dev", + "build": "electron-builder", + "build:win": "electron-builder --windows", + "build:mac": "electron-builder --mac", + "build:linux": "electron-builder --linux", + "dist": "npm run build", + "pack": "electron-builder --dir", + "postinstall": "electron-builder install-app-deps", + "clean": "rimraf dist node_modules", + "lint": "eslint *.js", + "test": "echo \"桌面應用測試功能待實現\" && exit 0" + }, + "dependencies": { + "electron": "^28.0.0" + }, + "devDependencies": { + "electron-builder": "^24.0.0", + "rimraf": "^5.0.0", + "eslint": "^8.0.0" + }, + "build": { + "appId": "com.minidoracat.mcp-feedback-enhanced", + "productName": "MCP Feedback Enhanced", + "directories": { + "output": "dist", + "buildResources": "assets" + }, + "files": [ + "main.js", + "preload.js", + "assets/**/*", + "node_modules/**/*" + ], + "extraResources": [ + { + "from": "../", + "to": "app", + "filter": [ + "**/*", + "!desktop/node_modules", + "!desktop/dist", + "!**/*.pyc", + "!**/__pycache__" + ] + } + ], + "win": { + "target": [ + { + "target": "nsis", + "arch": ["x64", "ia32"] + }, + { + "target": "portable", + "arch": ["x64"] + } + ], + "icon": "assets/icon.ico", + "requestedExecutionLevel": "asInvoker" + }, + "mac": { + "target": [ + { + "target": "dmg", + "arch": ["x64", "arm64"] + }, + { + "target": "zip", + "arch": ["x64", "arm64"] + } + ], + "icon": "assets/icon.icns", + "category": "public.app-category.developer-tools", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "assets/entitlements.mac.plist", + "entitlementsInherit": "assets/entitlements.mac.plist" + }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": ["x64"] + }, + { + "target": "deb", + "arch": ["x64"] + }, + { + "target": "rpm", + "arch": ["x64"] + } + ], + "icon": "assets/icon.png", + "category": "Development", + "desktop": { + "StartupNotify": "true", + "Encoding": "UTF-8", + "MimeType": "x-scheme-handler/mcp-feedback" + } + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "MCP Feedback Enhanced" + }, + "dmg": { + "title": "MCP Feedback Enhanced", + "icon": "assets/icon.icns", + "background": "assets/dmg-background.png", + "contents": [ + { + "x": 130, + "y": 220, + "type": "file" + }, + { + "x": 410, + "y": 220, + "type": "link", + "path": "/Applications" + } + ], + "window": { + "width": 540, + "height": 380 + } + }, + "publish": { + "provider": "github", + "owner": "minidoracat", + "repo": "mcp-feedback-enhanced" + } + }, + "electronDownload": { + "mirror": "https://npmmirror.com/mirrors/electron/" + } +} diff --git a/src/mcp_feedback_enhanced/desktop/preload.js b/src/mcp_feedback_enhanced/desktop/preload.js new file mode 100644 index 0000000..da37d63 --- /dev/null +++ b/src/mcp_feedback_enhanced/desktop/preload.js @@ -0,0 +1,204 @@ +/** + * Electron 預載腳本 + * ================== + * + * 此腳本在渲染進程中運行,但在網頁內容載入之前執行。 + * 它提供了安全的方式讓網頁與主進程通信,同時保持安全性。 + * + * 主要功能: + * - 提供安全的 IPC 通信接口 + * - 擴展現有的 WebSocket 管理器 + * - 添加桌面應用特有的 API + * - 標記桌面環境 + * + * 作者: Augment Agent + * 版本: 2.3.0 + */ + +const { contextBridge, ipcRenderer } = require('electron'); + +/** + * 桌面 API 接口 + * 通過 contextBridge 安全地暴露給渲染進程 + */ +const desktopAPI = { + // 環境標識 + isDesktop: true, + platform: process.platform, + + // 視窗控制 + window: { + minimize: () => ipcRenderer.invoke('window-minimize'), + maximize: () => ipcRenderer.invoke('window-maximize'), + close: () => ipcRenderer.invoke('window-close') + }, + + // 系統資訊 + system: { + getInfo: () => ipcRenderer.invoke('get-system-info'), + platform: process.platform, + arch: process.arch + }, + + // 事件監聽 + events: { + onSessionUpdate: (callback) => { + const wrappedCallback = (event, ...args) => callback(...args); + ipcRenderer.on('session-updated', wrappedCallback); + + // 返回清理函數 + return () => { + ipcRenderer.removeListener('session-updated', wrappedCallback); + }; + }, + + onFeedbackRequest: (callback) => { + const wrappedCallback = (event, ...args) => callback(...args); + ipcRenderer.on('feedback-request', wrappedCallback); + + return () => { + ipcRenderer.removeListener('feedback-request', wrappedCallback); + }; + } + }, + + // 回饋處理 + feedback: { + send: (data) => ipcRenderer.invoke('send-feedback', data), + cancel: () => ipcRenderer.invoke('cancel-feedback') + }, + + // 開發者工具 + dev: { + openDevTools: () => ipcRenderer.invoke('open-dev-tools'), + reload: () => ipcRenderer.invoke('reload-window') + } +}; + +/** + * 擴展現有的 Web UI 功能 + */ +const webUIExtensions = { + // 檢測桌面環境 + isDesktopMode: () => true, + + // 獲取桌面特有的配置 + getDesktopConfig: () => ({ + windowControls: true, + nativeMenus: process.platform === 'darwin', + customTitleBar: process.platform !== 'darwin' + }), + + // 桌面通知 + showNotification: (title, body, options = {}) => { + if ('Notification' in window && Notification.permission === 'granted') { + return new Notification(title, { body, ...options }); + } + return null; + }, + + // 請求通知權限 + requestNotificationPermission: async () => { + if ('Notification' in window) { + return await Notification.requestPermission(); + } + return 'denied'; + } +}; + +/** + * 日誌工具 + */ +const logger = { + debug: (...args) => { + if (process.env.NODE_ENV === 'development') { + console.log('[Desktop Debug]', ...args); + } + }, + info: (...args) => console.log('[Desktop Info]', ...args), + warn: (...args) => console.warn('[Desktop Warn]', ...args), + error: (...args) => console.error('[Desktop Error]', ...args) +}; + +// 暴露 API 到渲染進程 +try { + // 主要的桌面 API + contextBridge.exposeInMainWorld('electronAPI', desktopAPI); + + // Web UI 擴展 + contextBridge.exposeInMainWorld('desktopExtensions', webUIExtensions); + + // 日誌工具 + contextBridge.exposeInMainWorld('desktopLogger', logger); + + // 標記桌面環境(向後兼容) + contextBridge.exposeInMainWorld('MCP_DESKTOP_MODE', true); + + logger.info('桌面 API 已成功暴露到渲染進程'); + +} catch (error) { + console.error('暴露桌面 API 失敗:', error); +} + +/** + * DOM 載入完成後的初始化 + */ +window.addEventListener('DOMContentLoaded', () => { + logger.debug('DOM 載入完成,開始桌面環境初始化'); + + // 添加桌面環境樣式類 + document.body.classList.add('desktop-mode'); + document.body.classList.add(`platform-${process.platform}`); + + // 設置桌面環境變數 + document.documentElement.style.setProperty('--is-desktop', '1'); + + // 如果是 macOS,添加特殊樣式 + if (process.platform === 'darwin') { + document.body.classList.add('macos-titlebar'); + } + + // 監聽鍵盤快捷鍵 + document.addEventListener('keydown', (event) => { + // Ctrl/Cmd + R: 重新載入 + if ((event.ctrlKey || event.metaKey) && event.key === 'r') { + if (process.env.NODE_ENV === 'development') { + event.preventDefault(); + location.reload(); + } + } + + // F12: 開發者工具 + if (event.key === 'F12' && process.env.NODE_ENV === 'development') { + event.preventDefault(); + desktopAPI.dev.openDevTools(); + } + + // Escape: 最小化視窗 + if (event.key === 'Escape' && event.ctrlKey) { + event.preventDefault(); + desktopAPI.window.minimize(); + } + }); + + logger.debug('桌面環境初始化完成'); +}); + +/** + * 錯誤處理 + */ +window.addEventListener('error', (event) => { + logger.error('渲染進程錯誤:', { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: event.error + }); +}); + +window.addEventListener('unhandledrejection', (event) => { + logger.error('未處理的 Promise 拒絕:', event.reason); +}); + +logger.info('預載腳本載入完成'); diff --git a/src/mcp_feedback_enhanced/server.py b/src/mcp_feedback_enhanced/server.py index 78b39ce..9e301b8 100644 --- a/src/mcp_feedback_enhanced/server.py +++ b/src/mcp_feedback_enhanced/server.py @@ -31,6 +31,7 @@ import tempfile import asyncio import base64 from typing import Annotated, List +from enum import Enum import io from fastmcp import FastMCP @@ -108,6 +109,34 @@ SERVER_NAME = "互動式回饋收集 MCP" SSH_ENV_VARS = ['SSH_CONNECTION', 'SSH_CLIENT', 'SSH_TTY'] REMOTE_ENV_VARS = ['REMOTE_CONTAINERS', 'CODESPACES'] + +# ===== 回饋模式枚舉 ===== +class FeedbackMode(Enum): + """回饋模式枚舉""" + WEB = "web" + DESKTOP = "desktop" + AUTO = "auto" + + +def get_feedback_mode() -> FeedbackMode: + """ + 從環境變數獲取回饋模式 + + 環境變數 MCP_FEEDBACK_MODE 可設置為: + - 'web': 強制使用 Web 模式 + - 'desktop': 強制使用桌面模式 + - 'auto': 自動檢測(預設) + + Returns: + FeedbackMode: 回饋模式 + """ + mode = os.environ.get('MCP_FEEDBACK_MODE', 'auto').lower() + try: + return FeedbackMode(mode) + except ValueError: + debug_log(f"無效的 MCP_FEEDBACK_MODE 值: {mode},使用預設值 'auto'") + return FeedbackMode.AUTO + # 初始化 MCP 服務器 from . import __version__ @@ -452,8 +481,16 @@ async def interactive_feedback( project_directory = os.getcwd() project_directory = os.path.abspath(project_directory) - # 使用 Web UI - result = await launch_web_ui_with_timeout(project_directory, summary, timeout) + # 根據模式選擇啟動方式 + mode = get_feedback_mode() + debug_log(f"回饋模式: {mode.value}") + + if mode == FeedbackMode.DESKTOP: + result = await launch_desktop_feedback_ui(project_directory, summary, timeout) + elif mode == FeedbackMode.WEB: + result = await launch_web_feedback_ui(project_directory, summary, timeout) + else: # AUTO + result = await launch_auto_feedback_ui(project_directory, summary, timeout) # 處理取消情況 if not result: @@ -499,26 +536,26 @@ async def interactive_feedback( return [TextContent(type="text", text=user_error_msg)] -async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: int) -> dict: +async def launch_web_feedback_ui(project_dir: str, summary: str, timeout: int) -> dict: """ 啟動 Web UI 收集回饋,支援自訂超時時間 - + Args: project_dir: 專案目錄路徑 summary: AI 工作摘要 timeout: 超時時間(秒) - + Returns: dict: 收集到的回饋資料 """ debug_log(f"啟動 Web UI 介面,超時時間: {timeout} 秒") - + try: # 使用新的 web 模組 - from .web import launch_web_feedback_ui, stop_web_ui + from .web import launch_web_feedback_ui as web_launch, stop_web_ui # 傳遞 timeout 參數給 Web UI - return await launch_web_feedback_ui(project_dir, summary, timeout) + return await web_launch(project_dir, summary, timeout) except ImportError as e: # 使用統一錯誤處理 error_id = ErrorHandler.log_error_with_context( @@ -534,49 +571,66 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in "interactive_feedback": user_error_msg, "images": [] } - except TimeoutError as e: - debug_log(f"Web UI 超時: {e}") - # 超時時確保停止 Web 服務器 - try: - from .web import stop_web_ui - stop_web_ui() - debug_log("Web UI 服務器已因超時而停止") - except Exception as stop_error: - debug_log(f"停止 Web UI 服務器時發生錯誤: {stop_error}") - return { - "command_logs": "", - "interactive_feedback": f"回饋收集超時({timeout}秒),介面已自動關閉。", - "images": [] - } + +async def launch_desktop_feedback_ui(project_dir: str, summary: str, timeout: int) -> dict: + """ + 啟動桌面應用收集回饋 + + Args: + project_dir: 專案目錄路徑 + summary: AI 工作摘要 + timeout: 超時時間(秒) + + Returns: + dict: 收集到的回饋資料 + """ + debug_log(f"啟動桌面應用介面,超時時間: {timeout} 秒") + + try: + # 嘗試導入桌面模組 + from .desktop import launch_desktop_app + return await launch_desktop_app(project_dir, summary, timeout) + except ImportError as e: + debug_log(f"桌面模組未安裝或不可用,回退到 Web 模式: {e}") + # 回退到 Web 模式 + return await launch_web_feedback_ui(project_dir, summary, timeout) except Exception as e: - # 使用統一錯誤處理 - error_id = ErrorHandler.log_error_with_context( - e, - context={"operation": "Web UI 啟動", "timeout": timeout}, - error_type=ErrorType.SYSTEM - ) - user_error_msg = ErrorHandler.format_user_error(e, include_technical=False) - debug_log(f"❌ Web UI 錯誤 [錯誤ID: {error_id}]: {e}") + debug_log(f"桌面應用啟動失敗,回退到 Web 模式: {e}") + # 回退到 Web 模式 + return await launch_web_feedback_ui(project_dir, summary, timeout) - # 發生錯誤時也要停止 Web 服務器 - try: - from .web import stop_web_ui - stop_web_ui() - debug_log("Web UI 服務器已因錯誤而停止") - except Exception as stop_error: - ErrorHandler.log_error_with_context( - stop_error, - context={"operation": "Web UI 服務器停止"}, - error_type=ErrorType.SYSTEM - ) - debug_log(f"停止 Web UI 服務器時發生錯誤: {stop_error}") - return { - "command_logs": "", - "interactive_feedback": user_error_msg, - "images": [] - } +async def launch_auto_feedback_ui(project_dir: str, summary: str, timeout: int) -> dict: + """ + 自動檢測環境並選擇合適的回饋介面 + + Args: + project_dir: 專案目錄路徑 + summary: AI 工作摘要 + timeout: 超時時間(秒) + + Returns: + dict: 收集到的回饋資料 + """ + debug_log("自動檢測環境以選擇回饋介面") + + # 檢測是否為遠程環境 + if is_remote_environment(): + debug_log("檢測到遠程環境,使用 Web 模式") + return await launch_web_feedback_ui(project_dir, summary, timeout) + + # 本地環境:嘗試桌面模式,失敗則回退到 Web 模式 + try: + from .desktop import is_desktop_available + if is_desktop_available(): + debug_log("檢測到桌面環境可用,使用桌面模式") + return await launch_desktop_feedback_ui(project_dir, summary, timeout) + except ImportError: + debug_log("桌面模組不可用") + + debug_log("使用 Web 模式作為預設選擇") + return await launch_web_feedback_ui(project_dir, summary, timeout) @mcp.tool() diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index 6cdf085..4c60c63 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -100,6 +100,14 @@ class WebUIManager: self.server_process = None self.i18n = get_i18n_manager() + # 添加模式檢測支援 + self.mode = self._detect_feedback_mode() + self.desktop_manager = None + + # 如果是桌面模式,嘗試初始化桌面管理器 + if self.mode == "desktop": + self._init_desktop_manager() + # 設置靜態文件和模板 self._setup_static_files() self._setup_templates() @@ -108,6 +116,43 @@ class WebUIManager: setup_routes(self) debug_log(f"WebUIManager 初始化完成,將在 {self.host}:{self.port} 啟動") + debug_log(f"回饋模式: {self.mode}") + + def _detect_feedback_mode(self) -> str: + """檢測回饋模式""" + mode = os.environ.get('MCP_FEEDBACK_MODE', 'auto').lower() + if mode in ['web', 'desktop', 'auto']: + return mode + else: + debug_log(f"無效的 MCP_FEEDBACK_MODE 值: {mode},使用預設值 'auto'") + return 'auto' + + def _init_desktop_manager(self): + """初始化桌面管理器(如果可用)""" + try: + # 嘗試導入桌面模組 + from ..desktop import ElectronManager + self.desktop_manager = ElectronManager() + debug_log("桌面管理器初始化成功") + except ImportError: + debug_log("桌面模組不可用,將在需要時回退到 Web 模式") + self.desktop_manager = None + except Exception as e: + debug_log(f"桌面管理器初始化失敗: {e}") + self.desktop_manager = None + + def should_use_desktop_mode(self) -> bool: + """判斷是否應該使用桌面模式""" + if self.mode == "web": + return False + elif self.mode == "desktop": + return self.desktop_manager is not None + else: # auto + # 自動模式:檢測環境 + from ..server import is_remote_environment + if is_remote_environment(): + return False + return self.desktop_manager is not None def _setup_compression_middleware(self): """設置壓縮和緩存中間件"""