diff --git a/.gitignore b/.gitignore index 74c57ec..c753a01 100644 --- a/.gitignore +++ b/.gitignore @@ -17,30 +17,7 @@ htmlcov/ .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 diff --git a/electron-builder.json b/electron-builder.json deleted file mode 100644 index 45c37f4..0000000 --- a/electron-builder.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "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 6de76d4..cd74f1f 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 and optional Electron desktop application." +description = "Enhanced MCP server for interactive user feedback and command execution in AI-assisted development, featuring Web UI with intelligent environment detection." readme = "README.md" requires-python = ">=3.11" authors = [ { name = "Minidoracat", email = "minidora0702@gmail.com" } ] -keywords = ["mcp", "ai", "feedback", "web-ui", "interactive", "development", "electron", "desktop", "cross-platform"] +keywords = ["mcp", "ai", "feedback", "web-ui", "interactive", "development"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -34,10 +34,6 @@ dev = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", ] -desktop = [ - # Node.js 環境需要系統安裝,不是 Python 套件 - # 請確保系統已安裝 Node.js >= 16.0.0 -] [project.urls] Homepage = "https://github.com/Minidoracat/mcp-feedback-enhanced" @@ -99,7 +95,7 @@ exclude = [ "buck-out", "build", "dist", - "node_modules", + "venv", "*.egg-info", ".trunk", @@ -268,7 +264,7 @@ exclude = [ ".venv/", "venv/", ".trunk/", - "node_modules/", + ".mypy_cache/", ] diff --git a/src/mcp_feedback_enhanced/__main__.py b/src/mcp_feedback_enhanced/__main__.py index 97a1baf..27f33be 100644 --- a/src/mcp_feedback_enhanced/__main__.py +++ b/src/mcp_feedback_enhanced/__main__.py @@ -47,15 +47,6 @@ def main(): 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="測試超時時間 (秒)" ) @@ -109,28 +100,10 @@ def run_tests(args): 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("💡 可用的測試選項:") print(" --web 測試 Web UI") - print(" --desktop 測試桌面應用") - print(" --full 完整整合測試") - print(" --electron-only 僅測試 Electron 環境") print("💡 對於開發者:使用 'uv run pytest' 執行完整測試") sys.exit(1) @@ -212,68 +185,6 @@ def test_web_ui_simple(): os.environ.pop("MCP_WEB_PORT", None) -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_TEST_MODE"] = "true" - os.environ["MCP_WEB_PORT"] = "9767" # 使用不同端口避免與 Web 測試衝突 - os.environ["MCP_FEEDBACK_MODE"] = "desktop" - - print("🔧 創建 Electron 管理器...") - 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 - finally: - # 清理測試環境變數 - os.environ.pop("MCP_TEST_MODE", None) - os.environ.pop("MCP_WEB_PORT", None) - os.environ.pop("MCP_FEEDBACK_MODE", None) - - async def wait_for_process(process): """等待進程結束""" try: @@ -295,162 +206,6 @@ async def wait_for_process(process): 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, - check=False, - ) - 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 基本功能...") - import tempfile - - from .web.main import WebUIManager - - # 設置測試模式 - os.environ["MCP_TEST_MODE"] = "true" - os.environ["MCP_WEB_PORT"] = "9766" - - with tempfile.TemporaryDirectory() as temp_dir: - manager = WebUIManager(host="127.0.0.1") # 使用動態端口分配避免衝突 - 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 - finally: - # 清理測試環境變數 - os.environ.pop("MCP_TEST_MODE", None) - os.environ.pop("MCP_WEB_PORT", None) - - def show_version(): """顯示版本資訊""" from . import __author__, __version__ diff --git a/src/mcp_feedback_enhanced/desktop/__init__.py b/src/mcp_feedback_enhanced/desktop/__init__.py deleted file mode 100644 index e1d979b..0000000 --- a/src/mcp_feedback_enhanced/desktop/__init__.py +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env python3 -""" -桌面應用模組 -=========== - -此模組提供 Electron 桌面應用整合功能,實現零前端改動的桌面化方案。 - -主要功能: -- Electron 進程管理 -- 與現有 Web UI 的無縫整合 -- 跨平台桌面應用支援 -- 環境變數控制 - -作者: Augment Agent -版本: 2.3.0 -""" - -import asyncio -import os -import sys -import time -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, - check=False, - ) - 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("啟動桌面應用(並行優化版本)...") - start_time = time.time() - - try: - # 並行任務1:創建 Electron 管理器和依賴檢查 - async def init_electron_manager(): - from .electron_manager import ElectronManager - - manager = ElectronManager() - # 預先檢查依賴(如果實現了異步版本) - if hasattr(manager, "ensure_dependencies_async"): - await manager.ensure_dependencies_async() - return manager - - # 並行任務2:初始化 Web 管理器和會話 - async def init_web_manager(): - from ..web import get_web_ui_manager - - web_manager = get_web_ui_manager() - - # 確保異步初始化完成 - if hasattr(web_manager, "_init_async_components"): - await web_manager._init_async_components() - - # 創建會話 - web_manager.create_session(project_dir, summary) - session = web_manager.get_current_session() - - if not session: - raise RuntimeError("無法創建回饋會話") - - return web_manager, session - - # 並行執行初始化任務 - debug_log("並行執行初始化任務...") - init_results = await asyncio.gather( - init_electron_manager(), init_web_manager(), return_exceptions=True - ) - - # 檢查初始化結果 - if isinstance(init_results[0], Exception): - raise init_results[0] - if isinstance(init_results[1], Exception): - raise init_results[1] - - manager = init_results[0] - web_result = init_results[1] - if isinstance(web_result, tuple) and len(web_result) == 2: - web_manager, session = web_result - else: - raise RuntimeError("Web 管理器初始化返回格式錯誤") - - init_time = time.time() - start_time - debug_log(f"並行初始化完成,耗時: {init_time:.2f}秒") - - # 並行任務3:啟動 Web 服務器 - async def start_web_server(): - if ( - not web_manager.server_thread - or not web_manager.server_thread.is_alive() - ): - debug_log("啟動 Web 服務器...") - web_manager.start_server() - - # 智能等待服務器啟動(減少固定等待時間) - await _wait_for_server_startup(web_manager) - return web_manager.port - - # 並行任務4:準備 Electron 環境 - async def prepare_electron(): - # 如果有預熱方法,在這裡調用 - if hasattr(manager, "preheat_async"): - await manager.preheat_async() - return True - - # 並行執行服務器啟動和 Electron 準備 - debug_log("並行啟動 Web 服務器和準備 Electron 環境...") - startup_results = await asyncio.gather( - start_web_server(), prepare_electron(), return_exceptions=True - ) - - if isinstance(startup_results[0], Exception): - raise startup_results[0] - if isinstance(startup_results[1], Exception): - debug_log(f"Electron 預熱失敗(非致命): {startup_results[1]}") - - server_port = startup_results[0] - - # 類型檢查:確保 manager 不是 Exception - if isinstance(manager, Exception): - raise manager - - # 設置 Web 服務器端口 - manager.set_web_server_port(server_port) # type: ignore[union-attr] - debug_log(f"桌面應用將連接到: http://localhost:{server_port}") - - # 啟動桌面應用 - desktop_success = await manager.launch_desktop_app(summary, project_dir) # type: ignore[union-attr] - - if desktop_success: - total_startup_time = time.time() - start_time - debug_log( - f"桌面應用啟動成功,總耗時: {total_startup_time:.2f}秒,等待用戶回饋..." - ) - try: - # 等待用戶回饋 - result = await session.wait_for_feedback(timeout) - debug_log("收到桌面應用用戶回饋") - return result # type: ignore[no-any-return] - finally: - # 確保 Electron 進程被正確清理 - debug_log("清理 Electron 進程...") - if not isinstance(manager, Exception): - await manager.cleanup_async() # type: ignore[union-attr] - debug_log("Electron 進程清理完成") - 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 模式") - # 確保清理 Electron 進程 - try: - if "manager" in locals() and not isinstance(manager, Exception): - await manager.cleanup_async() # type: ignore[union-attr] - except Exception as cleanup_error: - debug_log(f"清理 Electron 進程時出錯: {cleanup_error}") - - # 回退到 Web 模式 - from ..web import launch_web_feedback_ui - - return await launch_web_feedback_ui(project_dir, summary, timeout) - - -async def _wait_for_server_startup(web_manager, max_wait: int = 10) -> bool: - """智能等待服務器啟動""" - debug_log("智能等待 Web 服務器啟動...") - - for attempt in range(max_wait): - if web_manager.server_thread and web_manager.server_thread.is_alive(): - # 嘗試連接測試 - try: - import aiohttp - - timeout = aiohttp.ClientTimeout(total=1) - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get( - f"http://{web_manager.host}:{web_manager.port}/" - ) as response: - if response.status == 200: - debug_log( - f"✅ Web 服務器啟動驗證成功,耗時: {attempt + 1}秒" - ) - return True - except Exception: - pass - - await asyncio.sleep(1) - debug_log(f"等待 Web 服務器啟動... ({attempt + 1}/{max_wait})") - - # 回退到線程檢查 - if web_manager.server_thread and web_manager.server_thread.is_alive(): - debug_log("✅ Web 服務器線程運行正常(回退驗證)") - return True - - debug_log("❌ Web 服務器啟動失敗") - return False - - -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__ = ["ElectronManager", "is_desktop_available", "launch_desktop_app"] diff --git a/src/mcp_feedback_enhanced/desktop/assets/README.md b/src/mcp_feedback_enhanced/desktop/assets/README.md deleted file mode 100644 index 559c574..0000000 --- a/src/mcp_feedback_enhanced/desktop/assets/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# 應用圖標資源 - -此目錄包含桌面應用的圖標資源文件。 - -## 需要的圖標文件 - -### 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 deleted file mode 100644 index da8496f..0000000 --- a/src/mcp_feedback_enhanced/desktop/assets/icon-placeholder.txt +++ /dev/null @@ -1,28 +0,0 @@ -# 圖標佔位符 - -此文件標記圖標資源的位置。在實際部署中,需要替換為真實的圖標文件: - -## 需要的圖標文件: - -### 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 deleted file mode 100644 index a86c02f..0000000 --- a/src/mcp_feedback_enhanced/desktop/electron_manager.py +++ /dev/null @@ -1,514 +0,0 @@ -#!/usr/bin/env python3 -""" -Electron 管理器 -============== - -此模組負責管理 Electron 進程的生命週期,包括: -- Electron 應用啟動和停止 -- 與 Web 服務器的整合 -- 依賴檢測和自動安裝 -- 進程管理和錯誤處理 - -此文件為階段 1 的預留實現,完整功能將在階段 2 中實現。 - -作者: Augment Agent -版本: 2.3.0 -""" - -import asyncio -import os -import platform -import signal -import subprocess -import warnings -from pathlib import Path - -from ..debug import web_debug_log as debug_log -from ..utils.error_handler import ErrorHandler, ErrorType - - -def suppress_windows_asyncio_warnings(): - """抑制 Windows 上的 asyncio 相關警告""" - if platform.system().lower() == "windows": - # 抑制 ResourceWarning 和 asyncio 相關警告 - warnings.filterwarnings("ignore", category=ResourceWarning) - warnings.filterwarnings("ignore", message=".*unclosed transport.*") - warnings.filterwarnings("ignore", message=".*I/O operation on closed pipe.*") - # 設置環境變數抑制 asyncio 警告 - os.environ.setdefault("PYTHONWARNINGS", "ignore::ResourceWarning") - debug_log("已設置 Windows asyncio 警告抑制") - - -class ElectronManager: - """Electron 進程管理器 - 跨平台支持""" - - def __init__(self): - """初始化 Electron 管理器""" - self.electron_process: asyncio.subprocess.Process | None = None - self.desktop_dir = Path(__file__).parent - self.web_server_port: int | None = None - self.platform = platform.system().lower() - self._cleanup_in_progress = False - - # 設置平台特定的警告抑制 - suppress_windows_asyncio_warnings() - - debug_log("ElectronManager 初始化完成") - debug_log(f"桌面模組目錄: {self.desktop_dir}") - debug_log(f"檢測到平台: {self.platform}") - - 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 - 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, - check=False, - ) - 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 - - async def ensure_dependencies_async(self) -> bool: - """異步確保依賴已安裝(別名方法)""" - return await self.ensure_dependencies() - - def cleanup(self): - """同步清理資源(向後兼容)""" - try: - # 在事件循環中運行異步清理 - loop = asyncio.get_event_loop() - if loop.is_running(): - # 如果事件循環正在運行,創建任務 - asyncio.create_task(self.cleanup_async()) - else: - # 如果沒有事件循環,運行異步清理 - asyncio.run(self.cleanup_async()) - except Exception as e: - debug_log(f"同步清理失敗,嘗試基本清理: {e}") - self._basic_cleanup() - - async def cleanup_async(self): - """異步清理資源 - 跨平台支持""" - if self._cleanup_in_progress or not self.electron_process: - return - - self._cleanup_in_progress = True - debug_log(f"開始清理 Electron 進程 (平台: {self.platform})") - - try: - # 檢查進程是否還在運行 - if self.electron_process.returncode is None: - await self._terminate_process_cross_platform() - else: - debug_log("Electron 進程已自然結束") - - # 等待進程完全結束並清理管道 - await self._wait_and_cleanup_pipes() - - except Exception as e: - debug_log(f"清理 Electron 進程時出錯: {e}") - # 嘗試強制清理 - await self._force_cleanup() - finally: - self.electron_process = None - self._cleanup_in_progress = False - debug_log("Electron 進程清理完成") - - async def _terminate_process_cross_platform(self): - """跨平台進程終止""" - if self.platform == "windows": - await self._terminate_windows() - else: - await self._terminate_unix() - - async def _terminate_windows(self): - """Windows 平台進程終止""" - debug_log("使用 Windows 進程終止策略") - if not self.electron_process: - return - - try: - # Windows: 使用 terminate() 然後等待 - self.electron_process.terminate() - try: - await asyncio.wait_for(self.electron_process.wait(), timeout=5.0) - debug_log("Electron 進程已優雅終止") - except TimeoutError: - debug_log("優雅終止超時,強制終止") - self.electron_process.kill() - await asyncio.wait_for(self.electron_process.wait(), timeout=3.0) - debug_log("Electron 進程已強制終止") - except Exception as e: - debug_log(f"Windows 進程終止失敗: {e}") - raise - - async def _terminate_unix(self): - """Unix 系統進程終止""" - debug_log("使用 Unix 進程終止策略") - if not self.electron_process: - return - - try: - # Unix: 發送 SIGTERM 然後等待 - self.electron_process.send_signal(signal.SIGTERM) - try: - await asyncio.wait_for(self.electron_process.wait(), timeout=5.0) - debug_log("Electron 進程已優雅終止") - except TimeoutError: - debug_log("優雅終止超時,發送 SIGKILL") - # 使用 getattr 來處理可能不存在的 SIGKILL - sigkill = getattr(signal, "SIGKILL", signal.SIGTERM) - self.electron_process.send_signal(sigkill) - await asyncio.wait_for(self.electron_process.wait(), timeout=3.0) - debug_log("Electron 進程已強制終止") - except Exception as e: - debug_log(f"Unix 進程終止失敗: {e}") - raise - - async def _wait_and_cleanup_pipes(self): - """等待進程結束並清理管道""" - if not self.electron_process: - return - - try: - # 確保進程已經結束 - if self.electron_process.returncode is None: - await self.electron_process.wait() - - # 平台特定的管道清理 - if self.platform == "windows": - await self._cleanup_pipes_windows() - else: - await self._cleanup_pipes_unix() - - except Exception as e: - debug_log(f"管道清理失敗: {e}") - - async def _cleanup_pipes_windows(self): - """Windows 平台管道清理""" - debug_log("清理 Windows 管道") - if not self.electron_process: - return - - try: - # Windows: 需要特殊處理 asyncio 管道 - pipes = [ - ("stdout", self.electron_process.stdout), - ("stderr", self.electron_process.stderr), - ("stdin", self.electron_process.stdin), - ] - - for pipe_name, pipe in pipes: - if pipe and hasattr(pipe, "close"): - try: - # 檢查是否有 is_closing 方法(某些管道類型可能沒有) - if not hasattr(pipe, "is_closing") or not pipe.is_closing(): - pipe.close() - # 等待管道關閉 - if hasattr(pipe, "wait_closed"): - await pipe.wait_closed() - debug_log(f"已關閉 {pipe_name} 管道") - except Exception as e: - # Windows 上的管道關閉錯誤通常是無害的 - debug_log(f"關閉 {pipe_name} 管道時出現預期錯誤: {e}") - - except Exception as e: - debug_log(f"Windows 管道清理失敗: {e}") - - async def _cleanup_pipes_unix(self): - """Unix 系統管道清理""" - debug_log("清理 Unix 管道") - if not self.electron_process: - return - - try: - # Unix: 直接關閉管道 - pipes = [ - ("stdout", self.electron_process.stdout), - ("stderr", self.electron_process.stderr), - ("stdin", self.electron_process.stdin), - ] - - for pipe_name, pipe in pipes: - if pipe and hasattr(pipe, "close"): - try: - pipe.close() - if hasattr(pipe, "wait_closed"): - await pipe.wait_closed() - debug_log(f"已關閉 {pipe_name} 管道") - except Exception as e: - debug_log(f"關閉 {pipe_name} 管道失敗: {e}") - - except Exception as e: - debug_log(f"Unix 管道清理失敗: {e}") - - async def _force_cleanup(self): - """強制清理(最後手段)""" - debug_log("執行強制清理") - try: - if self.electron_process and self.electron_process.returncode is None: - # 嘗試使用 psutil 強制終止 - try: - import psutil - - process = psutil.Process(self.electron_process.pid) - process.kill() - debug_log("使用 psutil 強制終止進程") - except ImportError: - debug_log("psutil 不可用,使用系統調用") - if self.electron_process: - if self.platform == "windows": - self.electron_process.kill() - else: - # 使用 getattr 來處理可能不存在的 SIGKILL - sigkill = getattr(signal, "SIGKILL", signal.SIGTERM) - self.electron_process.send_signal(sigkill) - except Exception as e: - debug_log(f"強制終止失敗: {e}") - - # 基本清理 - self._basic_cleanup() - - except Exception as e: - debug_log(f"強制清理失敗: {e}") - - def _basic_cleanup(self): - """基本清理(同步)""" - debug_log("執行基本清理") - try: - if self.electron_process: - # 嘗試基本的管道關閉 - for pipe_name in ["stdout", "stderr", "stdin"]: - pipe = getattr(self.electron_process, pipe_name, None) - if pipe and hasattr(pipe, "close"): - try: - pipe.close() - except Exception: - pass # 忽略錯誤 - self.electron_process = None - except Exception as e: - debug_log(f"基本清理失敗: {e}") - - 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 - 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 - 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 deleted file mode 100644 index 6c42bd4..0000000 --- a/src/mcp_feedback_enhanced/desktop/main.js +++ /dev/null @@ -1,306 +0,0 @@ -#!/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 ` - - -
-桌面應用無法連接到本地 Web 服務器(端口 ${this.webServerPort})。
-請確保 MCP 服務器正在運行。
-錯誤詳情: ${error.message}
- -