新增 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

53
.gitignore vendored
View File

@ -10,13 +10,62 @@ wheels/
.venv*/ .venv*/
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 # Logs
*.log *.log
#Others # macOS
.DS_Store .DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Linux
*~
# IDE
.vscode/
.idea/
*.swp
*.swo
# Others
.cursor/rules/ .cursor/rules/
uv.lock uv.lock
.mcp_feedback_settings.json .mcp_feedback_settings.json
test_reports/ 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] [project]
name = "mcp-feedback-enhanced" name = "mcp-feedback-enhanced"
version = "2.3.0" 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" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
authors = [ authors = [
{ name = "Minidoracat", email = "minidora0702@gmail.com" } { 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 = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@ -34,6 +34,9 @@ dev = [
"pytest>=7.0.0", "pytest>=7.0.0",
"pytest-asyncio>=0.21.0", "pytest-asyncio>=0.21.0",
] ]
desktop = [
"nodejs>=16.0.0", # Node.js 環境(需要系統安裝)
]
[project.urls] [project.urls]
Homepage = "https://github.com/Minidoracat/mcp-feedback-enhanced" Homepage = "https://github.com/Minidoracat/mcp-feedback-enhanced"

View File

@ -14,6 +14,19 @@ MCP Interactive Feedback Enhanced - 主程式入口
import sys import sys
import argparse import argparse
import os 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(): def main():
"""主程式入口點""" """主程式入口點"""
@ -29,6 +42,9 @@ def main():
# 測試命令 # 測試命令
test_parser = subparsers.add_parser('test', help='執行測試') test_parser = subparsers.add_parser('test', help='執行測試')
test_parser.add_argument('--web', action='store_true', help='測試 Web UI (自動持續運行)') 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='測試超時時間 (秒)') test_parser.add_argument('--timeout', type=int, default=60, help='測試超時時間 (秒)')
# 版本命令 # 版本命令
@ -59,14 +75,37 @@ def run_tests(args):
# 啟用調試模式以顯示測試過程 # 啟用調試模式以顯示測試過程
os.environ["MCP_DEBUG"] = "true" os.environ["MCP_DEBUG"] = "true"
# 在 Windows 上抑制 asyncio 警告
if sys.platform == 'win32':
os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning"
if args.web: if args.web:
print("🧪 執行 Web UI 測試...") print("🧪 執行 Web UI 測試...")
success = test_web_ui_simple() success = test_web_ui_simple()
if not success: if not success:
sys.exit(1) 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: else:
print("❌ 測試功能已簡化") print("❌ 測試功能已簡化")
print("💡 對於用戶:使用 'test --web' 測試 Web UI") print("💡 可用的測試選項:")
print(" --web 測試 Web UI")
print(" --desktop 測試桌面應用")
print(" --full 完整整合測試")
print(" --electron-only 僅測試 Electron 環境")
print("💡 對於開發者:使用 'uv run pytest' 執行完整測試") print("💡 對於開發者:使用 'uv run pytest' 執行完整測試")
sys.exit(1) sys.exit(1)
@ -135,6 +174,215 @@ def test_web_ui_simple():
return False 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(): def show_version():
"""顯示版本資訊""" """顯示版本資訊"""
from . import __version__, __author__ 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 asyncio
import base64 import base64
from typing import Annotated, List from typing import Annotated, List
from enum import Enum
import io import io
from fastmcp import FastMCP from fastmcp import FastMCP
@ -108,6 +109,34 @@ SERVER_NAME = "互動式回饋收集 MCP"
SSH_ENV_VARS = ['SSH_CONNECTION', 'SSH_CLIENT', 'SSH_TTY'] SSH_ENV_VARS = ['SSH_CONNECTION', 'SSH_CLIENT', 'SSH_TTY']
REMOTE_ENV_VARS = ['REMOTE_CONTAINERS', 'CODESPACES'] 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 服務器 # 初始化 MCP 服務器
from . import __version__ from . import __version__
@ -452,8 +481,16 @@ async def interactive_feedback(
project_directory = os.getcwd() project_directory = os.getcwd()
project_directory = os.path.abspath(project_directory) 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: if not result:
@ -499,26 +536,26 @@ async def interactive_feedback(
return [TextContent(type="text", text=user_error_msg)] 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 收集回饋支援自訂超時時間 啟動 Web UI 收集回饋支援自訂超時時間
Args: Args:
project_dir: 專案目錄路徑 project_dir: 專案目錄路徑
summary: AI 工作摘要 summary: AI 工作摘要
timeout: 超時時間 timeout: 超時時間
Returns: Returns:
dict: 收集到的回饋資料 dict: 收集到的回饋資料
""" """
debug_log(f"啟動 Web UI 介面,超時時間: {timeout}") debug_log(f"啟動 Web UI 介面,超時時間: {timeout}")
try: try:
# 使用新的 web 模組 # 使用新的 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 # 傳遞 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: except ImportError as e:
# 使用統一錯誤處理 # 使用統一錯誤處理
error_id = ErrorHandler.log_error_with_context( 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, "interactive_feedback": user_error_msg,
"images": [] "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": "", async def launch_desktop_feedback_ui(project_dir: str, summary: str, timeout: int) -> dict:
"interactive_feedback": f"回饋收集超時({timeout}秒),介面已自動關閉。", """
"images": [] 啟動桌面應用收集回饋
}
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: except Exception as e:
# 使用統一錯誤處理 debug_log(f"桌面應用啟動失敗,回退到 Web 模式: {e}")
error_id = ErrorHandler.log_error_with_context( # 回退到 Web 模式
e, return await launch_web_feedback_ui(project_dir, summary, timeout)
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}")
# 發生錯誤時也要停止 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 { async def launch_auto_feedback_ui(project_dir: str, summary: str, timeout: int) -> dict:
"command_logs": "", """
"interactive_feedback": user_error_msg, 自動檢測環境並選擇合適的回饋介面
"images": []
} 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() @mcp.tool()

View File

@ -100,6 +100,14 @@ class WebUIManager:
self.server_process = None self.server_process = None
self.i18n = get_i18n_manager() 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_static_files()
self._setup_templates() self._setup_templates()
@ -108,6 +116,43 @@ class WebUIManager:
setup_routes(self) setup_routes(self)
debug_log(f"WebUIManager 初始化完成,將在 {self.host}:{self.port} 啟動") 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): def _setup_compression_middleware(self):
"""設置壓縮和緩存中間件""" """設置壓縮和緩存中間件"""