新增 desktop 模式

This commit is contained in:
Minidoracat 2025-06-10 09:55:34 +08:00
parent 3ed676cf29
commit a2d4180cf6
13 changed files with 1828 additions and 52 deletions

51
.gitignore vendored
View File

@ -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/
# Temporary test files
test_*.py
# User configuration files
ui_settings.json
.config/

154
electron-builder.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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 會自動處理圖標的打包
- 確保圖標文件的版權合規性

View File

@ -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 和回饋收集的概念
- 在小尺寸下仍然清晰可辨
- 符合各平台的設計規範
- 使用一致的色彩方案

View File

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

View File

@ -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 `
<!DOCTYPE html>
<html>
<head>
<title>連接錯誤 - ${APP_CONFIG.name}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
margin: 0;
padding: 40px;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.error-container {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
max-width: 500px;
}
h1 { color: #e74c3c; margin-bottom: 20px; }
p { color: #666; line-height: 1.6; }
.retry-btn {
background: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
margin-top: 20px;
}
.retry-btn:hover { background: #2980b9; }
</style>
</head>
<body>
<div class="error-container">
<h1>無法連接到 Web 服務器</h1>
<p>桌面應用無法連接到本地 Web 服務器端口 ${this.webServerPort}</p>
<p>請確保 MCP 服務器正在運行</p>
<p><strong>錯誤詳情</strong> ${error.message}</p>
<button class="retry-btn" onclick="location.reload()">重試</button>
</div>
</body>
</html>
`;
}
/**
* 獲取應用圖標
*/
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;

View File

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

View File

@ -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('預載腳本載入完成');

View File

@ -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,7 +536,7 @@ 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 收集回饋支援自訂超時時間
@ -515,10 +552,10 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in
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()

View File

@ -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):
"""設置壓縮和緩存中間件"""