diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5b243e9..e1c7529 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,44 +19,44 @@ jobs: permissions: contents: write id-token: write - + steps: - uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - + - name: Install uv uses: astral-sh/setup-uv@v4 with: version: "latest" - + - name: Set up Python run: uv python install - + - name: Install dependencies run: | uv sync --dev - + - name: Configure Git run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - + - name: Commit dependency changes if any run: | if [ -n "$(git status --porcelain)" ]; then git add . git commit -m "📦 Update dependencies" || true fi - + - name: Get current version id: current_version run: | CURRENT_VERSION=$(grep '^version =' pyproject.toml | cut -d'"' -f2) echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT echo "Current version: $CURRENT_VERSION" - + - name: Bump version id: bump_version run: | @@ -64,12 +64,12 @@ jobs: NEW_VERSION=$(grep '^version =' pyproject.toml | cut -d'"' -f2) echo "new=$NEW_VERSION" >> $GITHUB_OUTPUT echo "New version: $NEW_VERSION" - + - name: Update __init__.py version run: | NEW_VERSION="${{ steps.bump_version.outputs.new }}" sed -i "s/__version__ = \".*\"/__version__ = \"$NEW_VERSION\"/" src/mcp_feedback_enhanced/__init__.py - + - name: Extract Release Highlights id: extract_highlights run: | @@ -112,7 +112,7 @@ jobs: echo "- 🚀 New features and improvements" > highlights.txt echo "- 🐛 Bug fixes and optimizations" >> highlights.txt fi - + - name: Generate Release Body id: release_body run: | @@ -173,7 +173,7 @@ jobs: echo "**Release automatically generated from CHANGELOG system** 🤖" >> release_body.md echo "Release body generated successfully" - + - name: Verify CHANGELOG Files run: | NEW_VERSION="v${{ steps.bump_version.outputs.new }}" @@ -227,7 +227,7 @@ jobs: else echo "✅ All CHANGELOG files verified successfully" fi - + - name: Commit version bump run: | git add . @@ -236,24 +236,24 @@ jobs: - Updated version to ${{ steps.bump_version.outputs.new }} - Auto-generated release from simplified workflow" git tag "v${{ steps.bump_version.outputs.new }}" - + - name: Build package run: uv build - + - name: Check package run: uv run twine check dist/* - + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} - + - name: Push changes and tags run: | git push origin main git push origin "v${{ steps.bump_version.outputs.new }}" - + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: @@ -265,7 +265,7 @@ jobs: generate_release_notes: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + - name: Summary run: | echo "🎉 Release v${{ steps.bump_version.outputs.new }} completed successfully!" @@ -279,4 +279,4 @@ jobs: echo " - Verify the package on PyPI" echo " - Test installation with: uvx mcp-feedback-enhanced@v${{ steps.bump_version.outputs.new }}" echo "" - echo "📋 Note: Make sure CHANGELOG files are updated for future releases" \ No newline at end of file + echo "📋 Note: Make sure CHANGELOG files are updated for future releases" diff --git a/.gitignore b/.gitignore index e0226ea..94dab5f 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,4 @@ test_*.py # User configuration files ui_settings.json -.config/ \ No newline at end of file +.config/ diff --git a/LICENSE b/LICENSE index aa85224..18001f2 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ MIT License Copyright (c) 2024 Fábio Ferreira -Portions of this software are modifications and enhancements +Portions of this software are modifications and enhancements Copyright (c) 2024 Minidoracat Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/RELEASE_NOTES/CHANGELOG.en.md b/RELEASE_NOTES/CHANGELOG.en.md index e01efa1..e8f9c8a 100644 --- a/RELEASE_NOTES/CHANGELOG.en.md +++ b/RELEASE_NOTES/CHANGELOG.en.md @@ -182,4 +182,4 @@ This version focuses on improving system stability and user experience, particul --- -**Full Project Info:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced) \ No newline at end of file +**Full Project Info:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced) diff --git a/RELEASE_NOTES/CHANGELOG.zh-CN.md b/RELEASE_NOTES/CHANGELOG.zh-CN.md index 3381487..c0ae4a0 100644 --- a/RELEASE_NOTES/CHANGELOG.zh-CN.md +++ b/RELEASE_NOTES/CHANGELOG.zh-CN.md @@ -182,4 +182,4 @@ --- -**完整项目信息:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced) \ No newline at end of file +**完整项目信息:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced) diff --git a/RELEASE_NOTES/CHANGELOG.zh-TW.md b/RELEASE_NOTES/CHANGELOG.zh-TW.md index e196e39..fbbfff0 100644 --- a/RELEASE_NOTES/CHANGELOG.zh-TW.md +++ b/RELEASE_NOTES/CHANGELOG.zh-TW.md @@ -182,4 +182,4 @@ --- -**完整專案資訊:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced) \ No newline at end of file +**完整專案資訊:** [GitHub - mcp-feedback-enhanced](https://github.com/Minidoracat/mcp-feedback-enhanced) diff --git a/RELEASE_NOTES/README.md b/RELEASE_NOTES/README.md index 3b23d01..9fc50f9 100644 --- a/RELEASE_NOTES/README.md +++ b/RELEASE_NOTES/README.md @@ -63,4 +63,4 @@ GitHub Actions 工作流程會自動執行: - **簡潔描述**: 保持項目符號簡潔但具描述性 - **問題引用**: 在適當的地方包含問題引用(例如 `fixes #10`) - **平行結構**: 在所有語言中保持平行結構 -- **時間順序**: 在 CHANGELOG 檔案中將最新版本放在頂部 \ No newline at end of file +- **時間順序**: 在 CHANGELOG 檔案中將最新版本放在頂部 diff --git a/RELEASE_NOTES/SIMPLIFIED_WORKFLOW.md b/RELEASE_NOTES/SIMPLIFIED_WORKFLOW.md index bece0c7..b2c4be0 100644 --- a/RELEASE_NOTES/SIMPLIFIED_WORKFLOW.md +++ b/RELEASE_NOTES/SIMPLIFIED_WORKFLOW.md @@ -14,7 +14,7 @@ This project now uses a simplified release workflow that no longer requires crea Before releasing, manually update these three files: - `RELEASE_NOTES/CHANGELOG.en.md` -- `RELEASE_NOTES/CHANGELOG.zh-TW.md` +- `RELEASE_NOTES/CHANGELOG.zh-TW.md` - `RELEASE_NOTES/CHANGELOG.zh-CN.md` ### 2. CHANGELOG 格式要求 / CHANGELOG Format Requirements diff --git a/RELEASE_NOTES/template.md b/RELEASE_NOTES/template.md index 5500046..fc68480 100644 --- a/RELEASE_NOTES/template.md +++ b/RELEASE_NOTES/template.md @@ -21,4 +21,4 @@ --- **說明**: 此模板應該適應每種語言(CHANGELOG.en.md, CHANGELOG.zh-TW.md, CHANGELOG.zh-CN.md) -**注意**: 版本發佈文件不包含安裝與相關連結部分,這些內容已移至各語言的完整 CHANGELOG 文件中 \ No newline at end of file +**注意**: 版本發佈文件不包含安裝與相關連結部分,這些內容已移至各語言的完整 CHANGELOG 文件中 diff --git a/debug_websocket.html b/debug_websocket.html index 04f30ae..c20bb62 100644 --- a/debug_websocket.html +++ b/debug_websocket.html @@ -59,24 +59,24 @@

🔧 WebSocket 診斷工具

- +
準備開始診斷...
- +
- +
- +
等待操作...
@@ -84,43 +84,43 @@ let websocket = null; let logElement = document.getElementById('log'); let statusElement = document.getElementById('status'); - + function log(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); logElement.textContent += `[${timestamp}] ${message}\n`; logElement.scrollTop = logElement.scrollHeight; - + // 更新狀態 statusElement.textContent = message; statusElement.className = `status ${type}`; } - + function clearLog() { logElement.textContent = ''; log('日誌已清除'); } - + function testConnection() { const url = document.getElementById('wsUrl').value; - + if (websocket) { log('關閉現有連接...', 'warning'); websocket.close(); websocket = null; } - + log(`嘗試連接到: ${url}`, 'info'); - + try { websocket = new WebSocket(url); - + websocket.onopen = function(event) { log('✅ WebSocket 連接成功!', 'success'); }; - + websocket.onmessage = function(event) { log(`📨 收到消息: ${event.data}`, 'success'); - + try { const data = JSON.parse(event.data); log(`📋 解析後的數據: ${JSON.stringify(data, null, 2)}`, 'info'); @@ -128,36 +128,36 @@ log(`⚠️ JSON 解析失敗: ${e.message}`, 'warning'); } }; - + websocket.onclose = function(event) { log(`🔌 連接已關閉 - Code: ${event.code}, Reason: ${event.reason}`, 'warning'); websocket = null; }; - + websocket.onerror = function(error) { log(`❌ WebSocket 錯誤: ${error}`, 'error'); console.error('WebSocket error:', error); }; - + } catch (error) { log(`❌ 連接失敗: ${error.message}`, 'error'); } } - + function sendMessage() { const messageInput = document.getElementById('messageInput'); const message = messageInput.value.trim(); - + if (!message) { log('⚠️ 請輸入要發送的消息', 'warning'); return; } - + if (!websocket || websocket.readyState !== WebSocket.OPEN) { log('❌ WebSocket 未連接', 'error'); return; } - + try { websocket.send(message); log(`📤 已發送: ${message}`, 'info'); @@ -166,13 +166,13 @@ log(`❌ 發送失敗: ${error.message}`, 'error'); } } - + // 頁面加載時自動測試 window.onload = function() { log('🚀 WebSocket 診斷工具已載入'); log('💡 點擊 "測試連接" 開始診斷'); }; - + // Enter 鍵發送消息 document.getElementById('messageInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') { diff --git a/docs/architecture/component-details.md b/docs/architecture/component-details.md index 94e10f8..f21c4e1 100644 --- a/docs/architecture/component-details.md +++ b/docs/architecture/component-details.md @@ -13,25 +13,25 @@ graph TB TOOL[interactive_feedback
核心工具] I18N[i18n.py
國際化支援] end - + subgraph "第二層:Web UI 管理層" MANAGER[WebUIManager
單例管理器] SESSION[WebFeedbackSession
會話模型] RESULT[FeedbackResult
結果模型] end - + subgraph "第三層:Web 服務層" MAIN[main.py
FastAPI 應用] ROUTES[main_routes.py
路由處理] WS[WebSocket
實時通信] end - + subgraph "第四層:前端交互層" HTML[feedback.html
主頁面] JS[app.js
交互邏輯] CSS[樣式文件] end - + subgraph "工具層" BROWSER[browser.py
瀏覽器控制] NETWORK[network.py
網路工具] @@ -39,14 +39,14 @@ graph TB CLEANUP[session_cleanup_manager.py
清理管理] COMPRESS[compression_*.py
壓縮工具] end - + SERVER --> MANAGER TOOL --> SESSION MANAGER --> MAIN SESSION --> ROUTES ROUTES --> HTML HTML --> JS - + BROWSER --> MANAGER NETWORK --> MAIN PORT --> MAIN @@ -63,7 +63,7 @@ class MCPServer: def __init__(self): self.app = FastMCP("mcp-feedback-enhanced") self.setup_tools() - + @self.app.tool() async def interactive_feedback( project_directory: str, @@ -128,12 +128,12 @@ stateDiagram-v2 FEEDBACK_PROCESSING --> FEEDBACK_SUBMITTED: 處理完成 FEEDBACK_SUBMITTED --> WAITING: 新會話更新 FEEDBACK_SUBMITTED --> [*]: 會話結束 - + note right of WAITING 等待用戶輸入 顯示 AI 摘要 end note - + note right of FEEDBACK_PROCESSING 處理回饋數據 圖片壓縮等 @@ -155,7 +155,7 @@ class FastAPIApp: self.setup_middleware() self.setup_routes() self.setup_websocket() - + def setup_middleware(self): # CORS 設定 # 靜態文件服務 @@ -176,13 +176,13 @@ graph LR FEEDBACK[GET /feedback] STATIC[靜態資源] end - + subgraph "WebSocket 路由" WS[/ws] MSG[訊息處理] BROADCAST[廣播機制] end - + GET --> FEEDBACK FEEDBACK --> STATIC WS --> MSG @@ -220,15 +220,15 @@ class FeedbackApp { this.currentSession = null; this.feedbackState = 'WAITING'; } - + // WebSocket 管理 initWebSocket() { /* ... */ } handleWebSocketMessage(data) { /* ... */ } - + // 用戶交互 submitFeedback() { /* ... */ } handleImageUpload() { /* ... */ } - + // UI 更新 updateSessionDisplay() { /* ... */ } updateFeedbackState() { /* ... */ } diff --git a/docs/architecture/deployment-guide.md b/docs/architecture/deployment-guide.md index ba4e11b..54e5098 100644 --- a/docs/architecture/deployment-guide.md +++ b/docs/architecture/deployment-guide.md @@ -13,7 +13,7 @@ graph TB LOCAL_BROWSER[本地瀏覽器] LOCAL --> LOCAL_BROWSER end - + subgraph "SSH 遠程環境" REMOTE[遠程服務器] SSH_TUNNEL[SSH 隧道] @@ -21,13 +21,13 @@ graph TB REMOTE --> SSH_TUNNEL SSH_TUNNEL --> LOCAL_CLIENT end - + subgraph "WSL 環境" WSL[WSL 子系統] WIN_BROWSER[Windows 瀏覽器] WSL --> WIN_BROWSER end - + subgraph "容器化部署" DOCKER[Docker 容器] PORT_MAP[埠映射] @@ -97,11 +97,11 @@ flowchart TD SSH -->|否| WSL{WSL 環境?} WSL -->|是| WSL_CONFIG[WSL 配置] WSL -->|否| LOCAL_CONFIG[本地配置] - + SSH_CONFIG --> TUNNEL[建立 SSH 隧道] WSL_CONFIG --> WSL_BROWSER[WSL 瀏覽器開啟] LOCAL_CONFIG --> LOCAL_BROWSER[本地瀏覽器開啟] - + TUNNEL --> SUCCESS[部署成功] WSL_BROWSER --> SUCCESS LOCAL_BROWSER --> SUCCESS diff --git a/docs/architecture/interaction-flows.md b/docs/architecture/interaction-flows.md index da9c8c3..69178d5 100644 --- a/docs/architecture/interaction-flows.md +++ b/docs/architecture/interaction-flows.md @@ -16,7 +16,7 @@ sequenceDiagram participant WS as WebSocket participant UI as Web UI participant User as 用戶 - + Note over AI,User: 第一次調用流程 AI->>MCP: interactive_feedback(summary, timeout) MCP->>WM: launch_web_feedback_ui() @@ -26,14 +26,14 @@ sequenceDiagram User->>UI: 訪問回饋頁面 UI->>WS: 建立 WebSocket 連接 WS->>UI: connection_established - + Note over AI,User: 用戶回饋流程 User->>UI: 填寫回饋內容 UI->>WS: submit_feedback WS->>WM: 處理回饋數據 WM->>MCP: 設置回饋完成 MCP->>AI: 返回回饋結果 - + Note over AI,User: 第二次調用流程 AI->>MCP: interactive_feedback(new_summary, timeout) MCP->>WM: 更新現有會話 @@ -98,7 +98,7 @@ async def create_session(self, summary: str, project_dir: str): old_websockets = [] if self.current_session: old_websockets = list(self.current_session.websockets) - + # 創建新會話 session_id = str(uuid.uuid4()) self.current_session = WebFeedbackSession( @@ -106,11 +106,11 @@ async def create_session(self, summary: str, project_dir: str): summary=summary, project_directory=project_dir ) - + # 繼承 WebSocket 連接 for ws in old_websockets: self.current_session.add_websocket(ws) - + # 標記需要發送會話更新 self._pending_session_update = True ``` @@ -123,13 +123,13 @@ sequenceDiagram participant UI as Web UI participant WS as WebSocket participant Session as 會話管理 - + Browser->>UI: 訪問 /feedback UI->>WS: 建立 WebSocket 連接 WS->>Session: 註冊連接 Session->>WS: connection_established WS->>UI: 發送連接確認 - + alt 有待處理的會話更新 Session->>WS: session_updated WS->>UI: 會話更新訊息 @@ -153,13 +153,13 @@ stateDiagram-v2 AIProcessing --> SecondCall: AI 再次調用 SecondCall --> SessionUpdated: 會話更新 SessionUpdated --> UserFeedback: 等待新回饋 - + note right of SessionActive Web 服務器持續運行 瀏覽器標籤頁保持開啟 WebSocket 連接維持 end note - + note right of SessionUpdated 無需重新開啟瀏覽器 局部更新頁面內容 @@ -199,19 +199,19 @@ flowchart TD function handleSessionUpdated(data) { // 顯示會話更新通知 showNotification('會話已更新', 'info'); - + // 重置回饋狀態 feedbackState = 'FEEDBACK_WAITING'; - + // 局部更新 AI 摘要 updateAISummary(data.summary); - + // 清空回饋表單 clearFeedbackForm(); - + // 更新會話 ID currentSessionId = data.session_id; - + // 保持 WebSocket 連接不變 // 無需重新建立連接 } @@ -229,7 +229,7 @@ graph LR FR[feedback_received
回饋確認] ST[status_update
狀態更新] end - + subgraph "客戶端 → 服務器" SF[submit_feedback
提交回饋] HB[heartbeat
心跳檢測] @@ -246,7 +246,7 @@ stateDiagram-v2 FEEDBACK_PROCESSING --> FEEDBACK_SUBMITTED: 處理完成 FEEDBACK_SUBMITTED --> WAITING: 新會話更新 FEEDBACK_SUBMITTED --> [*]: 會話結束 - + WAITING --> ERROR: 連接錯誤 FEEDBACK_PROCESSING --> ERROR: 處理錯誤 ERROR --> WAITING: 錯誤恢復 @@ -260,7 +260,7 @@ stateDiagram-v2 // WebSocket 重連機制 function handleWebSocketClose() { console.log('WebSocket 連接已關閉,嘗試重連...'); - + setTimeout(() => { initWebSocket(); }, 3000); // 3秒後重連 diff --git a/docs/architecture/system-overview.md b/docs/architecture/system-overview.md index 5f08d67..891a3e9 100644 --- a/docs/architecture/system-overview.md +++ b/docs/architecture/system-overview.md @@ -11,40 +11,40 @@ graph TB subgraph "AI 助手環境" AI[AI 助手
Claude/GPT等] end - + subgraph "MCP Feedback Enhanced" subgraph "MCP 服務層" MCP[MCP Server
server.py] TOOL[interactive_feedback
工具] end - + subgraph "Web UI 管理層" WM[WebUIManager
單例模式] SESSION[WebFeedbackSession
會話管理] end - + subgraph "Web 服務層" API[FastAPI
HTTP/WebSocket] ROUTES[路由處理
main_routes.py] end - + subgraph "前端交互層" UI[Web UI
HTML/JS] WS[WebSocket
實時通信] end - + subgraph "工具層" ENV[環境檢測] BROWSER[智能瀏覽器開啟] RESOURCE[資源管理] end end - + subgraph "用戶環境" USER[用戶瀏覽器] FILES[專案文件] end - + AI -->|調用 MCP 工具| MCP MCP --> TOOL TOOL --> WM @@ -54,11 +54,11 @@ graph TB ROUTES --> UI UI --> WS WS --> USER - + ENV --> MCP BROWSER --> USER RESOURCE --> SESSION - + USER -->|回饋提交| WS FILES -->|專案內容| TOOL ``` @@ -74,7 +74,7 @@ stateDiagram-v2 SessionUpdated --> ActiveSession: 會話切換完成 ActiveSession --> Cleanup: 超時或手動清理 Cleanup --> NoSession: 資源釋放 - + note right of ActiveSession 只維護一個活躍會話 提升性能和用戶體驗 @@ -97,7 +97,7 @@ flowchart TD REMOTE -->|否| WSL{WSL 環境?} WSL -->|是| WSLOPEN[WSL 瀏覽器開啟] WSL -->|否| FALLBACK[回退模式] - + DIRECT --> SUCCESS[成功啟動] TUNNEL --> SUCCESS WSLOPEN --> SUCCESS @@ -148,7 +148,7 @@ sequenceDiagram participant WM as WebUIManager participant UI as Web UI participant User as 用戶 - + AI->>MCP: interactive_feedback() MCP->>WM: 創建/更新會話 WM->>UI: 啟動 Web 服務 @@ -168,7 +168,7 @@ graph LR D --> E[會話無縫更新] E --> F[用戶再次回饋] F --> G[持續循環...] - + style D fill:#e1f5fe style E fill:#e8f5e8 ``` diff --git a/docs/en/cache-management.md b/docs/en/cache-management.md index aef9112..f6c6bcd 100644 --- a/docs/en/cache-management.md +++ b/docs/en/cache-management.md @@ -57,7 +57,7 @@ python scripts/cleanup_cache.py --force # Windows taskkill /f /im uvx.exe taskkill /f /im python.exe /fi "WINDOWTITLE eq *mcp-feedback-enhanced*" - + # Then execute cleanup uv cache clean ``` diff --git a/docs/zh-CN/cache-management.md b/docs/zh-CN/cache-management.md index c91b2f3..440f33c 100644 --- a/docs/zh-CN/cache-management.md +++ b/docs/zh-CN/cache-management.md @@ -57,7 +57,7 @@ python scripts/cleanup_cache.py --force # Windows taskkill /f /im uvx.exe taskkill /f /im python.exe /fi "WINDOWTITLE eq *mcp-feedback-enhanced*" - + # 然后执行清理 uv cache clean ``` diff --git a/docs/zh-TW/cache-management.md b/docs/zh-TW/cache-management.md index 79ee85a..a895b28 100644 --- a/docs/zh-TW/cache-management.md +++ b/docs/zh-TW/cache-management.md @@ -57,7 +57,7 @@ python scripts/cleanup_cache.py --force # Windows taskkill /f /im uvx.exe taskkill /f /im python.exe /fi "WINDOWTITLE eq *mcp-feedback-enhanced*" - + # 然後執行清理 uv cache clean ``` diff --git a/pyproject.toml b/pyproject.toml index 388e221..25eb28f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,6 +160,18 @@ ignore = [ "S110", # 允許 try-except-pass(暫時) "E712", # 允許布林比較(暫時) "E722", # 允許裸露 except(暫時) + "ARG001", # 允許未使用函數參數(暫時) + "ARG002", # 允許未使用方法參數(暫時) + "PLW0603", # 允許使用 global 語句(暫時) + "RUF012", # 允許可變類別屬性(暫時) + "RUF006", # 允許未儲存 asyncio.create_task 返回值(暫時) + "PLR0915", # 允許函數語句過多(暫時) + "SIM110", # 允許使用 for 迴圈而非 any()(暫時) + "A002", # 允許遮蔽內建函數名稱(暫時) + "S104", # 允許綁定所有介面(暫時) + "RUF013", # 允許隱式 Optional(暫時) + "SIM108", # 允許 if-else 而非三元運算子(暫時) + "S602", # 允許 subprocess shell=True(暫時) ] # 每個檔案的最大複雜度 diff --git a/pytest.ini b/pytest.ini index 9d093fd..26eb7c6 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,7 +8,7 @@ testpaths = tests minversion = 6.0 # 添加選項 -addopts = +addopts = --strict-markers --strict-config --disable-warnings diff --git a/scripts/cleanup_cache.py b/scripts/cleanup_cache.py index 5ed318a..8b91afb 100644 --- a/scripts/cleanup_cache.py +++ b/scripts/cleanup_cache.py @@ -19,27 +19,26 @@ UV Cache 清理腳本 - 支援 Windows/macOS/Linux 跨平台 """ -import subprocess -import sys import argparse -import shutil -from pathlib import Path import os +import subprocess +from pathlib import Path + def get_cache_dir(): """取得 uv cache 目錄""" # Windows 預設路徑 - if os.name == 'nt': + if os.name == "nt": return Path.home() / "AppData" / "Local" / "uv" # macOS/Linux 預設路徑 - else: - return Path.home() / ".cache" / "uv" + return Path.home() / ".cache" / "uv" + def get_cache_size(cache_dir): """計算 cache 目錄大小""" if not cache_dir.exists(): return 0 - + total_size = 0 for dirpath, dirnames, filenames in os.walk(cache_dir): for filename in filenames: @@ -50,25 +49,24 @@ def get_cache_size(cache_dir): pass return total_size + def format_size(size_bytes): """格式化檔案大小顯示""" if size_bytes == 0: return "0 B" - - for unit in ['B', 'KB', 'MB', 'GB']: + + for unit in ["B", "KB", "MB", "GB"]: if size_bytes < 1024.0: return f"{size_bytes:.1f} {unit}" size_bytes /= 1024.0 return f"{size_bytes:.1f} TB" + def run_uv_command(command, check=True): """執行 uv 命令""" try: result = subprocess.run( - ["uv"] + command, - capture_output=True, - text=True, - check=check + ["uv"] + command, capture_output=True, text=True, check=check ) return result except subprocess.CalledProcessError as e: @@ -79,25 +77,26 @@ def run_uv_command(command, check=True): print("❌ 找不到 uv 命令,請確認 uv 已正確安裝") return None + def show_cache_info(): """顯示 cache 資訊""" print("🔍 UV Cache 資訊") print("=" * 50) - + cache_dir = get_cache_dir() print(f"Cache 目錄: {cache_dir}") - + if cache_dir.exists(): cache_size = get_cache_size(cache_dir) print(f"Cache 大小: {format_size(cache_size)}") - + # 顯示子目錄大小 subdirs = [] for subdir in cache_dir.iterdir(): if subdir.is_dir(): subdir_size = get_cache_size(subdir) subdirs.append((subdir.name, subdir_size)) - + if subdirs: print("\n📁 子目錄大小:") subdirs.sort(key=lambda x: x[1], reverse=True) @@ -106,6 +105,7 @@ def show_cache_info(): else: print("Cache 目錄不存在") + def clean_cache_selective(cache_dir, dry_run=False): """選擇性清理 cache,跳過正在使用的檔案""" cleaned_count = 0 @@ -117,7 +117,7 @@ def clean_cache_selective(cache_dir, dry_run=False): # 遍歷 cache 目錄 for root, dirs, files in os.walk(cache_dir): # 跳過一些可能正在使用的目錄 - if any(skip_dir in root for skip_dir in ['Scripts', 'Lib', 'pyvenv.cfg']): + if any(skip_dir in root for skip_dir in ["Scripts", "Lib", "pyvenv.cfg"]): continue for file in files: @@ -128,19 +128,22 @@ def clean_cache_selective(cache_dir, dry_run=False): total_saved += file_size cleaned_count += 1 if cleaned_count <= 10: # 只顯示前10個 - print(f" 將清理: {file_path.relative_to(cache_dir)} ({format_size(file_size)})") + print( + f" 將清理: {file_path.relative_to(cache_dir)} ({format_size(file_size)})" + ) else: file_size = file_path.stat().st_size file_path.unlink() total_saved += file_size cleaned_count += 1 - except (OSError, PermissionError, FileNotFoundError) as e: + except (OSError, PermissionError, FileNotFoundError): skipped_count += 1 if not dry_run and skipped_count <= 5: # 只顯示前5個錯誤 print(f" ⚠️ 跳過: {file_path.name} (正在使用中)") return cleaned_count, skipped_count, total_saved + def clean_cache(dry_run=False): """清理 cache""" action = "預覽" if dry_run else "執行" @@ -164,8 +167,10 @@ def clean_cache(dry_run=False): print(result.stdout) else: print(" 使用自定義掃描...") - cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=True) - print(f"\n📊 預覽結果:") + cleaned_count, skipped_count, total_saved = clean_cache_selective( + cache_dir, dry_run=True + ) + print("\n📊 預覽結果:") print(f" 可清理檔案: {cleaned_count}") print(f" 預計節省: {format_size(total_saved)}") else: @@ -177,9 +182,11 @@ def clean_cache(dry_run=False): print("✅ 標準 Cache 清理完成") else: print("⚠️ 標準清理失敗,使用選擇性清理...") - cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=False) + cleaned_count, skipped_count, total_saved = clean_cache_selective( + cache_dir, dry_run=False + ) - print(f"\n📊 清理結果:") + print("\n📊 清理結果:") print(f" 已清理檔案: {cleaned_count}") print(f" 跳過檔案: {skipped_count}") print(f" 節省空間: {format_size(total_saved)}") @@ -192,13 +199,14 @@ def clean_cache(dry_run=False): if cache_dir.exists(): after_size = get_cache_size(cache_dir) saved_size = before_size - after_size - print(f"\n📈 總體效果:") + print("\n📈 總體效果:") print(f" 清理前: {format_size(before_size)}") print(f" 清理後: {format_size(after_size)}") print(f" 實際節省: {format_size(saved_size)}") else: print(f" 節省空間: {format_size(before_size)}") + def force_clean_cache(): """強制清理 cache(關閉相關程序後)""" print("🔥 強制清理模式") @@ -206,7 +214,7 @@ def force_clean_cache(): print("⚠️ 警告:此模式會嘗試關閉可能使用 cache 的程序") confirm = input("確定要繼續嗎?(y/N): ") - if confirm.lower() != 'y': + if confirm.lower() != "y": print("❌ 已取消") return @@ -222,22 +230,28 @@ def force_clean_cache(): print("\n🔍 檢查相關程序...") try: import psutil + killed_processes = [] - for proc in psutil.process_iter(['pid', 'name', 'cmdline']): + for proc in psutil.process_iter(["pid", "name", "cmdline"]): try: - if proc.info['name'] and any(name in proc.info['name'].lower() - for name in ['uvx', 'uv.exe', 'python.exe']): - cmdline = ' '.join(proc.info['cmdline'] or []) - if 'mcp-feedback-enhanced' in cmdline or 'uvx' in cmdline: - print(f" 終止程序: {proc.info['name']} (PID: {proc.info['pid']})") + if proc.info["name"] and any( + name in proc.info["name"].lower() + for name in ["uvx", "uv.exe", "python.exe"] + ): + cmdline = " ".join(proc.info["cmdline"] or []) + if "mcp-feedback-enhanced" in cmdline or "uvx" in cmdline: + print( + f" 終止程序: {proc.info['name']} (PID: {proc.info['pid']})" + ) proc.terminate() - killed_processes.append(proc.info['pid']) + killed_processes.append(proc.info["pid"]) except (psutil.NoSuchProcess, psutil.AccessDenied): pass if killed_processes: print(f" 已終止 {len(killed_processes)} 個程序") import time + time.sleep(2) # 等待程序完全關閉 else: print(" 未發現相關程序") @@ -252,22 +266,29 @@ def force_clean_cache(): print("✅ 強制清理成功") else: print("⚠️ 標準清理仍然失敗,使用檔案級清理...") - cleaned_count, skipped_count, total_saved = clean_cache_selective(cache_dir, dry_run=False) + cleaned_count, skipped_count, total_saved = clean_cache_selective( + cache_dir, dry_run=False + ) print(f" 清理檔案: {cleaned_count}, 跳過: {skipped_count}") # 顯示結果 after_size = get_cache_size(cache_dir) saved_size = before_size - after_size - print(f"\n📈 清理結果:") + print("\n📈 清理結果:") print(f" 節省空間: {format_size(saved_size)}") + def main(): parser = argparse.ArgumentParser(description="UV Cache 清理工具") group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--size", action="store_true", help="顯示 cache 大小資訊") - group.add_argument("--dry-run", action="store_true", help="預覽清理內容(不實際清理)") + group.add_argument( + "--dry-run", action="store_true", help="預覽清理內容(不實際清理)" + ) group.add_argument("--clean", action="store_true", help="執行 cache 清理") - group.add_argument("--force", action="store_true", help="強制清理(會嘗試關閉相關程序)") + group.add_argument( + "--force", action="store_true", help="強制清理(會嘗試關閉相關程序)" + ) args = parser.parse_args() @@ -280,5 +301,6 @@ def main(): elif args.force: force_clean_cache() + if __name__ == "__main__": main() diff --git a/scripts/release.py b/scripts/release.py index cdc4b11..a41dba8 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -3,24 +3,28 @@ 本地發布腳本 用法: python scripts/release.py patch # 2.0.0 -> 2.0.1 - python scripts/release.py minor # 2.0.0 -> 2.1.0 + python scripts/release.py minor # 2.0.0 -> 2.1.0 python scripts/release.py major # 2.0.0 -> 3.0.0 """ +import re import subprocess import sys -import re from pathlib import Path + def run_cmd(cmd, check=True): """執行命令並返回結果""" print(f"🔨 執行: {cmd}") - result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + result = subprocess.run( + cmd, shell=True, capture_output=True, text=True, check=False + ) if check and result.returncode != 0: print(f"❌ 錯誤: {result.stderr}") sys.exit(1) return result + def get_current_version(): """從 pyproject.toml 獲取當前版本""" pyproject_path = Path("pyproject.toml") @@ -30,73 +34,76 @@ def get_current_version(): return match.group(1) raise ValueError("無法找到版本號") + def bump_version(version_type): """更新版本號""" - if version_type not in ['patch', 'minor', 'major']: + if version_type not in ["patch", "minor", "major"]: print("❌ 版本類型必須是: patch, minor, major") sys.exit(1) - + current = get_current_version() print(f"📦 當前版本: {current}") - + # 使用 bump2version with allow-dirty run_cmd(f"uv run bump2version --allow-dirty {version_type}") - + new_version = get_current_version() print(f"🎉 新版本: {new_version}") - + return current, new_version + def main(): if len(sys.argv) != 2: print(__doc__) sys.exit(1) - + version_type = sys.argv[1] - + print("🚀 開始發布流程...") - + # 檢查 Git 狀態(僅提示,不阻止) result = run_cmd("git status --porcelain", check=False) if result.stdout.strip(): print("⚠️ 有未提交的變更:") print(result.stdout) print("💡 將繼續執行(使用 --allow-dirty 模式)") - + # 更新版本 old_version, new_version = bump_version(version_type) - + # 建置套件 print("📦 建置套件...") run_cmd("uv build") - + # 檢查套件 print("🔍 檢查套件...") run_cmd("uv run twine check dist/*") - + # 提交所有變更(包括版本更新) print("💾 提交版本更新...") run_cmd("git add .") run_cmd(f'git commit -m "🔖 Release v{new_version}"') run_cmd(f'git tag "v{new_version}"') - + # 詢問是否發布 print(f"\n✅ 準備發布版本 {old_version} -> {new_version}") choice = input("是否發布到 PyPI? (y/N): ") - - if choice.lower() == 'y': + + if choice.lower() == "y": print("🚀 發布到 PyPI...") run_cmd("uv run twine upload dist/*") - + print("📤 推送到 GitHub...") run_cmd("git push origin main") run_cmd(f'git push origin "v{new_version}"') - + print(f"🎉 發布完成!版本 v{new_version} 已上線") - print(f"📦 安裝命令: uvx mcp-feedback-enhanced") + print("📦 安裝命令: uvx mcp-feedback-enhanced") else: print("⏸️ 發布已取消,版本已更新但未發布") print("💡 您可以稍後手動發布: uv run twine upload dist/*") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/mcp_feedback_enhanced/__init__.py b/src/mcp_feedback_enhanced/__init__.py index 4a3f0eb..4a84c2e 100644 --- a/src/mcp_feedback_enhanced/__init__.py +++ b/src/mcp_feedback_enhanced/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ MCP Interactive Feedback Enhanced ================================== @@ -27,24 +26,27 @@ import os from .server import main as run_server # 導入新的 Web UI 模組 -from .web import WebUIManager, launch_web_feedback_ui, get_web_ui_manager, stop_web_ui +from .web import WebUIManager, get_web_ui_manager, launch_web_feedback_ui, stop_web_ui + # 保持向後兼容性 feedback_ui = None # 主要導出介面 __all__ = [ - "run_server", - "feedback_ui", "WebUIManager", - "launch_web_feedback_ui", - "get_web_ui_manager", - "stop_web_ui", - "__version__", "__author__", + "__version__", + "feedback_ui", + "get_web_ui_manager", + "launch_web_feedback_ui", + "run_server", + "stop_web_ui", ] + def main(): """主要入口點,用於 uvx 執行""" from .__main__ import main as cli_main - return cli_main() \ No newline at end of file + + return cli_main() diff --git a/src/mcp_feedback_enhanced/__main__.py b/src/mcp_feedback_enhanced/__main__.py index 37d3dbf..53aa636 100644 --- a/src/mcp_feedback_enhanced/__main__.py +++ b/src/mcp_feedback_enhanced/__main__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ MCP Interactive Feedback Enhanced - 主程式入口 ============================================== @@ -11,15 +10,18 @@ MCP Interactive Feedback Enhanced - 主程式入口 python -m mcp_feedback_enhanced test # 執行測試 """ -import sys import argparse -import os import asyncio +import os +import sys import warnings + # 抑制 Windows 上的 asyncio ResourceWarning -if sys.platform == 'win32': - warnings.filterwarnings("ignore", category=ResourceWarning, message=".*unclosed transport.*") +if sys.platform == "win32": + warnings.filterwarnings( + "ignore", category=ResourceWarning, message=".*unclosed transport.*" + ) warnings.filterwarnings("ignore", category=ResourceWarning, message=".*unclosed.*") # 設置 asyncio 事件循環策略以減少警告 @@ -28,55 +30,67 @@ if sys.platform == 'win32': except AttributeError: pass + def main(): """主程式入口點""" parser = argparse.ArgumentParser( description="MCP Feedback Enhanced Enhanced - 互動式回饋收集 MCP 伺服器" ) - - subparsers = parser.add_subparsers(dest='command', help='可用命令') - + + subparsers = parser.add_subparsers(dest="command", help="可用命令") + # 伺服器命令(預設) - server_parser = subparsers.add_parser('server', help='啟動 MCP 伺服器(預設)') - + server_parser = subparsers.add_parser("server", help="啟動 MCP 伺服器(預設)") + # 測試命令 - test_parser = subparsers.add_parser('test', help='執行測試') - test_parser.add_argument('--web', action='store_true', help='測試 Web UI (自動持續運行)') - test_parser.add_argument('--desktop', action='store_true', help='測試桌面應用 (啟動 Electron 應用)') - test_parser.add_argument('--full', action='store_true', help='完整整合測試 (Web + 桌面)') - test_parser.add_argument('--electron-only', action='store_true', help='僅測試 Electron 環境') - test_parser.add_argument('--timeout', type=int, default=60, help='測試超時時間 (秒)') - + test_parser = subparsers.add_parser("test", help="執行測試") + test_parser.add_argument( + "--web", action="store_true", help="測試 Web UI (自動持續運行)" + ) + test_parser.add_argument( + "--desktop", action="store_true", help="測試桌面應用 (啟動 Electron 應用)" + ) + test_parser.add_argument( + "--full", action="store_true", help="完整整合測試 (Web + 桌面)" + ) + test_parser.add_argument( + "--electron-only", action="store_true", help="僅測試 Electron 環境" + ) + test_parser.add_argument( + "--timeout", type=int, default=60, help="測試超時時間 (秒)" + ) + # 版本命令 - version_parser = subparsers.add_parser('version', help='顯示版本資訊') - + version_parser = subparsers.add_parser("version", help="顯示版本資訊") + args = parser.parse_args() - - if args.command == 'test': + + if args.command == "test": run_tests(args) - elif args.command == 'version': + elif args.command == "version": show_version() - elif args.command == 'server': - run_server() - elif args.command is None: + elif args.command == "server" or args.command is None: run_server() else: # 不應該到達這裡 parser.print_help() sys.exit(1) + def run_server(): """啟動 MCP 伺服器""" from .server import main as server_main + return server_main() + def run_tests(args): """執行測試""" # 啟用調試模式以顯示測試過程 os.environ["MCP_DEBUG"] = "true" # 在 Windows 上抑制 asyncio 警告 - if sys.platform == 'win32': + if sys.platform == "win32": os.environ["PYTHONWARNINGS"] = "ignore::ResourceWarning" if args.web: @@ -113,20 +127,18 @@ def run_tests(args): def test_web_ui_simple(): """簡單的 Web UI 測試""" try: - from .web.main import WebUIManager import tempfile import time import webbrowser + from .web.main import WebUIManager + print("🔧 創建 Web UI 管理器...") manager = WebUIManager(host="127.0.0.1", port=8765) # 使用固定端口 print("🔧 創建測試會話...") with tempfile.TemporaryDirectory() as temp_dir: - session_id = manager.create_session( - temp_dir, - "Web UI 測試 - 驗證基本功能" - ) + session_id = manager.create_session(temp_dir, "Web UI 測試 - 驗證基本功能") if session_id: print("✅ 會話創建成功") @@ -170,6 +182,7 @@ def test_web_ui_simple(): except Exception as e: print(f"❌ Web UI 測試失敗: {e}") import traceback + traceback.print_exc() return False @@ -181,6 +194,7 @@ def test_desktop_app(): # 檢查桌面環境可用性 from .desktop import is_desktop_available + if not is_desktop_available(): print("❌ 桌面環境不可用") print("💡 請確保 Node.js 已安裝且不在遠程環境中") @@ -189,10 +203,9 @@ def test_desktop_app(): print("✅ 桌面環境檢查通過") # 設置桌面模式 - os.environ['MCP_FEEDBACK_MODE'] = 'desktop' + os.environ["MCP_FEEDBACK_MODE"] = "desktop" print("🔧 創建 Electron 管理器...") - from .desktop.electron_manager import ElectronManager import asyncio async def run_desktop_test(): @@ -208,7 +221,7 @@ def test_desktop_app(): result = await launch_desktop_app( os.getcwd(), "桌面應用測試 - 驗證 Electron 整合功能", - 300 # 5分鐘超時 + 300, # 5分鐘超時 ) print("✅ 桌面應用測試完成") @@ -224,6 +237,7 @@ def test_desktop_app(): except Exception as e: print(f"❌ 桌面應用測試失敗: {e}") import traceback + traceback.print_exc() return False @@ -236,11 +250,11 @@ async def wait_for_process(process): # 確保管道正確關閉 try: - if hasattr(process, 'stdout') and process.stdout: + if hasattr(process, "stdout") and process.stdout: process.stdout.close() - if hasattr(process, 'stderr') and process.stderr: + if hasattr(process, "stderr") and process.stderr: process.stderr.close() - if hasattr(process, 'stdin') and process.stdin: + if hasattr(process, "stdin") and process.stdin: process.stdin.close() except Exception as close_error: print(f"關閉進程管道時出錯: {close_error}") @@ -256,9 +270,15 @@ def test_electron_environment(): # 檢查 Node.js import subprocess + try: - result = subprocess.run(['node', '--version'], - capture_output=True, text=True, timeout=10) + 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: @@ -270,6 +290,7 @@ def test_electron_environment(): # 檢查桌面模組 from .desktop import is_desktop_available + if is_desktop_available(): print("✅ 桌面環境可用") else: @@ -278,6 +299,7 @@ def test_electron_environment(): # 檢查 Electron 管理器 from .desktop.electron_manager import ElectronManager + manager = ElectronManager() if manager.is_electron_available(): @@ -288,7 +310,7 @@ def test_electron_environment(): # 檢查文件結構 desktop_dir = manager.desktop_dir - required_files = ['main.js', 'preload.js', 'package.json'] + required_files = ["main.js", "preload.js", "package.json"] for file_name in required_files: file_path = desktop_dir / file_name @@ -324,20 +346,24 @@ def test_full_integration(): test_cases = [("auto", "auto"), ("web", "web"), ("desktop", "desktop")] for env_value, expected in test_cases: - os.environ['MCP_FEEDBACK_MODE'] = env_value + os.environ["MCP_FEEDBACK_MODE"] = env_value # 重新導入以獲取新的環境變數值 import sys - if 'mcp_feedback_enhanced.server' in sys.modules: - del sys.modules['mcp_feedback_enhanced.server'] + + 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})") + print( + f" ❌ MCP_FEEDBACK_MODE='{env_value}' → {actual} (期望: {expected})" + ) return False # 2. Electron 環境測試 @@ -348,9 +374,10 @@ def test_full_integration(): # 3. Web UI 基本功能測試 print("\n📋 3. 測試 Web UI 基本功能...") - from .web.main import WebUIManager import tempfile + from .web.main import WebUIManager + with tempfile.TemporaryDirectory() as temp_dir: manager = WebUIManager(host="127.0.0.1", port=8766) # 使用不同端口避免衝突 session_id = manager.create_session(temp_dir, "整合測試會話") @@ -363,7 +390,7 @@ def test_full_integration(): # 4. 桌面模式檢測測試 print("\n📋 4. 測試桌面模式檢測...") - os.environ['MCP_FEEDBACK_MODE'] = 'desktop' + os.environ["MCP_FEEDBACK_MODE"] = "desktop" manager = WebUIManager() if manager.should_use_desktop_mode(): @@ -379,16 +406,19 @@ def test_full_integration(): except Exception as e: print(f"❌ 完整整合測試失敗: {e}") import traceback + traceback.print_exc() return False def show_version(): """顯示版本資訊""" - from . import __version__, __author__ + from . import __author__, __version__ + print(f"MCP Feedback Enhanced Enhanced v{__version__}") print(f"作者: {__author__}") print("GitHub: https://github.com/Minidoracat/mcp-feedback-enhanced") + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/mcp_feedback_enhanced/debug.py b/src/mcp_feedback_enhanced/debug.py index c120784..e390adb 100644 --- a/src/mcp_feedback_enhanced/debug.py +++ b/src/mcp_feedback_enhanced/debug.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ 統一調試日誌模組 ================ @@ -29,26 +28,26 @@ from typing import Any def debug_log(message: Any, prefix: str = "DEBUG") -> None: """ 輸出調試訊息到標準錯誤,避免污染標準輸出 - + Args: message: 要輸出的調試信息 prefix: 調試信息的前綴標識,默認為 "DEBUG" """ # 只在啟用調試模式時才輸出,避免干擾 MCP 通信 - if not os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on"): + if os.getenv("MCP_DEBUG", "").lower() not in ("true", "1", "yes", "on"): return - + try: # 確保消息是字符串類型 if not isinstance(message, str): message = str(message) - + # 安全地輸出到 stderr,處理編碼問題 try: print(f"[{prefix}] {message}", file=sys.stderr, flush=True) except UnicodeEncodeError: # 如果遇到編碼問題,使用 ASCII 安全模式 - safe_message = message.encode('ascii', errors='replace').decode('ascii') + safe_message = message.encode("ascii", errors="replace").decode("ascii") print(f"[{prefix}] {safe_message}", file=sys.stderr, flush=True) except Exception: # 最後的備用方案:靜默失敗,不影響主程序 @@ -77,4 +76,4 @@ def is_debug_enabled() -> bool: def set_debug_mode(enabled: bool) -> None: """設置調試模式(用於測試)""" - os.environ["MCP_DEBUG"] = "true" if enabled else "false" \ No newline at end of file + os.environ["MCP_DEBUG"] = "true" if enabled else "false" diff --git a/src/mcp_feedback_enhanced/desktop/__init__.py b/src/mcp_feedback_enhanced/desktop/__init__.py index 18a7b31..65b928c 100644 --- a/src/mcp_feedback_enhanced/desktop/__init__.py +++ b/src/mcp_feedback_enhanced/desktop/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ 桌面應用模組 =========== @@ -26,30 +25,35 @@ 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) + + 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 @@ -75,10 +79,12 @@ async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> di 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() # 創建會話 @@ -95,6 +101,7 @@ async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> di # 等待 Web 服務器完全啟動 import time + debug_log("等待 Web 服務器啟動...") time.sleep(5) # 增加等待時間 @@ -102,7 +109,7 @@ async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> di if web_manager.server_thread and web_manager.server_thread.is_alive(): debug_log(f"✅ Web 服務器成功啟動在端口: {web_manager.port}") else: - raise RuntimeError(f"Web 服務器啟動失敗") + raise RuntimeError("Web 服務器啟動失敗") # 設置 Web 服務器端口 manager.set_web_server_port(web_manager.port) @@ -117,52 +124,49 @@ async def launch_desktop_app(project_dir: str, summary: str, timeout: int) -> di 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) + 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" -] +__all__ = ["ElectronManager", "is_desktop_available", "launch_desktop_app"] diff --git a/src/mcp_feedback_enhanced/desktop/electron_manager.py b/src/mcp_feedback_enhanced/desktop/electron_manager.py index a03b30e..420ae8b 100644 --- a/src/mcp_feedback_enhanced/desktop/electron_manager.py +++ b/src/mcp_feedback_enhanced/desktop/electron_manager.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Electron 管理器 ============== @@ -16,11 +15,9 @@ Electron 管理器 版本: 2.3.0 """ -import subprocess import asyncio -import os +import subprocess from pathlib import Path -from typing import Optional from ..debug import web_debug_log as debug_log from ..utils.error_handler import ErrorHandler, ErrorType @@ -28,16 +25,16 @@ from ..utils.error_handler import ErrorHandler, ErrorType class ElectronManager: """Electron 進程管理器""" - + def __init__(self): """初始化 Electron 管理器""" - self.electron_process: Optional[subprocess.Popen] = None + self.electron_process: subprocess.Popen | None = None self.desktop_dir = Path(__file__).parent - self.web_server_port: Optional[int] = None - + self.web_server_port: int | None = None + debug_log("ElectronManager 初始化完成") debug_log(f"桌面模組目錄: {self.desktop_dir}") - + async def launch_desktop_app(self, summary: str, project_dir: str) -> bool: """ 啟動 Electron 桌面應用 @@ -64,50 +61,52 @@ class ElectronManager: if success: debug_log("Electron 桌面應用啟動成功") return True - else: - debug_log("Electron 桌面應用啟動失敗") - return False + 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 + 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) + 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 依賴...") @@ -137,13 +136,11 @@ class ElectronManager: except Exception as e: error_id = ErrorHandler.log_error_with_context( - e, - context={"operation": "依賴檢查"}, - error_type=ErrorType.DEPENDENCY + e, context={"operation": "依賴檢查"}, error_type=ErrorType.DEPENDENCY ) debug_log(f"依賴檢查失敗 [錯誤ID: {error_id}]: {e}") return False - + def cleanup(self): """清理資源""" if self.electron_process: @@ -166,21 +163,30 @@ class ElectronManager: # 關閉管道以避免 ResourceWarning try: # 對於 asyncio 子進程,需要特殊處理 - if hasattr(self.electron_process, 'stdout') and self.electron_process.stdout: - if hasattr(self.electron_process.stdout, 'close'): + 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'): + 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'): + 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 = { @@ -188,21 +194,15 @@ class ElectronManager: "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" - } + "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: + 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}") @@ -213,12 +213,12 @@ class ElectronManager: try: # 使用 npm install - install_cmd = ['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=asyncio.subprocess.PIPE, ) _, stderr = await process.communicate() @@ -226,9 +226,8 @@ class ElectronManager: if process.returncode == 0: debug_log("Node.js 依賴安裝成功") return True - else: - debug_log(f"依賴安裝失敗: {stderr.decode()}") - return False + debug_log(f"依賴安裝失敗: {stderr.decode()}") + return False except Exception as e: debug_log(f"依賴安裝過程中出錯: {e}") @@ -241,21 +240,29 @@ class ElectronManager: try: # 構建 Electron 命令 - 使用本地安裝的 electron import platform + if platform.system() == "Windows": - electron_path = self.desktop_dir / "node_modules" / ".bin" / "electron.cmd" + 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) + 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) + "npx", + "electron", + ".", + "--port", + str(self.web_server_port or 8765), ] debug_log(f"使用 Electron 命令: {' '.join(electron_cmd)}") @@ -265,7 +272,7 @@ class ElectronManager: *electron_cmd, cwd=self.desktop_dir, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) debug_log(f"Electron 進程已啟動,PID: {self.electron_process.pid}") @@ -277,16 +284,17 @@ class ElectronManager: 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 + 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}") @@ -301,9 +309,9 @@ class ElectronManager: async def create_electron_manager() -> ElectronManager: """創建 Electron 管理器實例""" manager = ElectronManager() - + # 檢查可用性 if not manager.is_electron_available(): debug_log("Electron 環境不可用,建議使用 Web 模式") - + return manager diff --git a/src/mcp_feedback_enhanced/desktop/main.js b/src/mcp_feedback_enhanced/desktop/main.js index d8e8847..6c42bd4 100644 --- a/src/mcp_feedback_enhanced/desktop/main.js +++ b/src/mcp_feedback_enhanced/desktop/main.js @@ -2,16 +2,16 @@ /** * Electron 主進程 * =============== - * + * * 此文件是 MCP Feedback Enhanced 桌面應用的主進程入口點。 * 負責創建和管理 BrowserWindow,以及與現有 Web UI 的整合。 - * + * * 主要功能: * - 創建和管理應用視窗 * - 載入本地 Web 服務器內容 * - 處理應用生命週期事件 * - 提供桌面應用特有的功能 - * + * * 作者: Augment Agent * 版本: 2.3.0 */ @@ -42,7 +42,7 @@ class ElectronApp { this.mainWindow = null; this.webServerPort = APP_CONFIG.defaultPort; this.isDevMode = process.argv.includes('--dev'); - + this.setupEventHandlers(); this.parseCommandLineArgs(); } @@ -53,7 +53,7 @@ class ElectronApp { 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) { @@ -197,7 +197,7 @@ class ElectronApp { 連接錯誤 - ${APP_CONFIG.name}