mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
🔥 移除 Electron 桌面
This commit is contained in:
parent
c3a49695e7
commit
a9a3139f0e
23
.gitignore
vendored
23
.gitignore
vendored
@ -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
|
||||
|
@ -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"
|
||||
}
|
@ -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/",
|
||||
]
|
||||
|
||||
|
@ -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__
|
||||
|
@ -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"]
|
@ -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 會自動處理圖標的打包
|
||||
- 確保圖標文件的版權合規性
|
@ -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 和回饋收集的概念
|
||||
- 在小尺寸下仍然清晰可辨
|
||||
- 符合各平台的設計規範
|
||||
- 使用一致的色彩方案
|
@ -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
|
@ -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 `
|
||||
<!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;
|
@ -1,169 +0,0 @@
|
||||
{
|
||||
"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/"
|
||||
}
|
||||
}
|
@ -1,204 +0,0 @@
|
||||
/**
|
||||
* 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('預載腳本載入完成');
|
@ -28,7 +28,6 @@ import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastmcp import FastMCP
|
||||
@ -114,35 +113,6 @@ 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__
|
||||
|
||||
@ -500,18 +470,10 @@ async def interactive_feedback(
|
||||
project_directory = os.getcwd()
|
||||
project_directory = os.path.abspath(project_directory)
|
||||
|
||||
# 根據模式選擇啟動方式
|
||||
mode = get_feedback_mode()
|
||||
debug_log(f"回饋模式: {mode.value}")
|
||||
# 使用 Web 模式
|
||||
debug_log("回饋模式: web")
|
||||
|
||||
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)
|
||||
result = await launch_web_feedback_ui(project_directory, summary, timeout)
|
||||
|
||||
# 處理取消情況
|
||||
if not result:
|
||||
@ -603,70 +565,6 @@ async def launch_web_feedback_ui(project_dir: str, summary: str, timeout: int) -
|
||||
}
|
||||
|
||||
|
||||
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:
|
||||
debug_log(f"桌面應用啟動失敗,回退到 Web 模式: {e}")
|
||||
# 回退到 Web 模式
|
||||
return await launch_web_feedback_ui(project_dir, summary, timeout)
|
||||
|
||||
|
||||
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()
|
||||
def get_system_info() -> str:
|
||||
"""
|
||||
|
@ -124,14 +124,12 @@ class WebUIManager:
|
||||
self._init_basic_components()
|
||||
|
||||
debug_log(f"WebUIManager 基本初始化完成,將在 {self.host}:{self.port} 啟動")
|
||||
debug_log(f"回饋模式: {self.mode}")
|
||||
debug_log("回饋模式: web")
|
||||
|
||||
def _init_basic_components(self):
|
||||
"""同步初始化基本組件"""
|
||||
# 基本組件初始化(必須同步)
|
||||
self.i18n = get_i18n_manager()
|
||||
self.mode = self._detect_feedback_mode()
|
||||
self.desktop_manager: Any = None
|
||||
|
||||
# 設置靜態文件和模板(必須同步)
|
||||
self._setup_static_files()
|
||||
@ -152,11 +150,7 @@ class WebUIManager:
|
||||
# 創建並行任務
|
||||
tasks = []
|
||||
|
||||
# 任務1:桌面管理器初始化
|
||||
if self.mode == "desktop":
|
||||
tasks.append(self._init_desktop_manager_async())
|
||||
|
||||
# 任務2:I18N 預載入(如果需要)
|
||||
# 任務:I18N 預載入(如果需要)
|
||||
tasks.append(self._preload_i18n_async())
|
||||
|
||||
# 並行執行所有任務
|
||||
@ -174,51 +168,6 @@ class WebUIManager:
|
||||
elapsed = time.time() - start_time
|
||||
debug_log(f"並行初始化完成,耗時: {elapsed:.2f}秒")
|
||||
|
||||
def _detect_feedback_mode(self) -> str:
|
||||
"""檢測回饋模式"""
|
||||
mode = os.environ.get("MCP_FEEDBACK_MODE", "auto").lower()
|
||||
if mode in ["web", "desktop", "auto"]:
|
||||
return mode
|
||||
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
|
||||
|
||||
async def _init_desktop_manager_async(self):
|
||||
"""異步初始化桌面管理器"""
|
||||
|
||||
def init_desktop():
|
||||
try:
|
||||
from ..desktop import ElectronManager
|
||||
|
||||
manager = ElectronManager()
|
||||
debug_log("桌面管理器異步初始化成功")
|
||||
return manager
|
||||
except ImportError:
|
||||
debug_log("桌面模組不可用,將在需要時回退到 Web 模式")
|
||||
return None
|
||||
except Exception as e:
|
||||
debug_log(f"桌面管理器異步初始化失敗: {e}")
|
||||
return None
|
||||
|
||||
# 在線程池中執行同步初始化
|
||||
loop = asyncio.get_event_loop()
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
self.desktop_manager = await loop.run_in_executor(executor, init_desktop)
|
||||
|
||||
async def _preload_i18n_async(self):
|
||||
"""異步預載入 I18N 資源"""
|
||||
|
||||
@ -237,20 +186,6 @@ class WebUIManager:
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
await loop.run_in_executor(executor, preload_i18n)
|
||||
|
||||
def should_use_desktop_mode(self) -> bool:
|
||||
"""判斷是否應該使用桌面模式"""
|
||||
if self.mode == "web":
|
||||
return False
|
||||
if self.mode == "desktop":
|
||||
return self.desktop_manager is not None
|
||||
# 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):
|
||||
"""設置壓縮和緩存中間件"""
|
||||
# 獲取壓縮管理器
|
||||
|
Loading…
x
Reference in New Issue
Block a user