From e961a2c1c83b97cbbfab93ec8147f670f179965a Mon Sep 17 00:00:00 2001 From: Minidoracat Date: Sun, 15 Jun 2025 11:34:34 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=96=B0=E5=A2=9E=20Tauri=20?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E6=87=89=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-desktop.yml | 152 ++++++ .github/workflows/publish.yml | 129 ++++++ .gitignore | 26 ++ Makefile | 41 +- docs/DESKTOP_BUILD.md | 244 ++++++++++ docs/WORKFLOWS.md | 122 +++++ examples/mcp-config-desktop.json | 15 + examples/mcp-config-web.json | 15 + pyproject.toml | 13 +- scripts/build_desktop.py | 437 ++++++++++++++++++ src-tauri/Cargo.toml | 60 +++ src-tauri/build.rs | 3 + src-tauri/generate_icons.py | 102 ++++ src-tauri/icons/128x128.png | Bin 0 -> 2376 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 4793 bytes src-tauri/icons/256x256.png | Bin 0 -> 4793 bytes src-tauri/icons/32x32.png | Bin 0 -> 550 bytes src-tauri/icons/icon.icns | Bin 0 -> 18629 bytes src-tauri/icons/icon.ico | Bin 0 -> 18629 bytes src-tauri/pyproject.toml | 39 ++ .../mcp_feedback_enhanced_desktop/__init__.py | 30 ++ .../desktop_app.py | 334 +++++++++++++ src-tauri/src/lib.rs | 132 ++++++ src-tauri/src/main.rs | 79 ++++ src-tauri/tauri.conf.json | 70 +++ src/mcp_feedback_enhanced/__main__.py | 120 +++++ .../desktop_release/__init__.py | 1 + src/mcp_feedback_enhanced/server.py | 9 + src/mcp_feedback_enhanced/web/main.py | 99 +++- .../web/models/feedback_session.py | 17 + .../web/static/favicon.ico | 2 + .../web/static/index.html | 158 +++++++ .../web/static/js/app.js | 32 ++ .../web/utils/browser.py | 17 + 34 files changed, 2493 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/build-desktop.yml create mode 100644 docs/DESKTOP_BUILD.md create mode 100644 docs/WORKFLOWS.md create mode 100644 examples/mcp-config-desktop.json create mode 100644 examples/mcp-config-web.json create mode 100644 scripts/build_desktop.py create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/generate_icons.py create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/256x256.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/icon.icns create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/pyproject.toml create mode 100644 src-tauri/python/mcp_feedback_enhanced_desktop/__init__.py create mode 100644 src-tauri/python/mcp_feedback_enhanced_desktop/desktop_app.py create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src/mcp_feedback_enhanced/desktop_release/__init__.py create mode 100644 src/mcp_feedback_enhanced/web/static/favicon.ico create mode 100644 src/mcp_feedback_enhanced/web/static/index.html diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml new file mode 100644 index 0000000..74b77f8 --- /dev/null +++ b/.github/workflows/build-desktop.yml @@ -0,0 +1,152 @@ +name: Build Desktop Applications + +on: + workflow_dispatch: # 手動觸發 + inputs: + platforms: + description: '選擇要構建的平台' + required: true + default: 'all' + type: choice + options: + - all + - windows + - macos + - linux + upload_artifacts: + description: '是否上傳構建產物' + required: true + default: true + type: boolean + + push: + paths: + - 'src-tauri/**' # 桌面應用代碼變更時自動觸發 + - 'scripts/build_desktop.py' + branches: + - main + + pull_request: + paths: + - 'src-tauri/**' + - 'scripts/build_desktop.py' + +env: + CARGO_TERM_COLOR: always + +jobs: + # 多平台桌面應用構建 + build-desktop: + strategy: + fail-fast: false # 允許部分平台失敗 + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + binary: mcp-feedback-enhanced-desktop.exe + name: windows + enabled: ${{ github.event.inputs.platforms == 'all' || github.event.inputs.platforms == 'windows' || github.event.inputs.platforms == '' }} + - os: macos-latest + target: x86_64-apple-darwin + binary: mcp-feedback-enhanced-desktop + name: macos-intel + enabled: ${{ github.event.inputs.platforms == 'all' || github.event.inputs.platforms == 'macos' || github.event.inputs.platforms == '' }} + - os: macos-latest + target: aarch64-apple-darwin + binary: mcp-feedback-enhanced-desktop + name: macos-arm64 + enabled: ${{ github.event.inputs.platforms == 'all' || github.event.inputs.platforms == 'macos' || github.event.inputs.platforms == '' }} + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + binary: mcp-feedback-enhanced-desktop + name: linux + enabled: ${{ github.event.inputs.platforms == 'all' || github.event.inputs.platforms == 'linux' || github.event.inputs.platforms == '' }} + + runs-on: ${{ matrix.os }} + if: ${{ matrix.enabled != 'false' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - 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: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-${{ matrix.target }}- + ${{ runner.os }}-cargo- + + - name: Build desktop application for ${{ matrix.name }} + run: | + cd src-tauri + cargo build --release --target ${{ matrix.target }} --bin mcp-feedback-enhanced-desktop + + - name: Verify build output + run: | + echo "🔍 檢查構建產物..." + if [ "${{ matrix.os }}" = "windows-latest" ]; then + ls -la "src-tauri/target/${{ matrix.target }}/release/${{ matrix.binary }}" || echo "❌ Windows 二進制文件不存在" + else + ls -la "src-tauri/target/${{ matrix.target }}/release/${{ matrix.binary }}" || echo "❌ ${{ matrix.name }} 二進制文件不存在" + fi + shell: bash + + - name: Upload desktop binary + if: ${{ github.event.inputs.upload_artifacts != 'false' }} + uses: actions/upload-artifact@v4 + with: + name: desktop-${{ matrix.name }} + path: src-tauri/target/${{ matrix.target }}/release/${{ matrix.binary }} + retention-days: 30 # 保留 30 天 + compression-level: 6 + + # 構建摘要 + build-summary: + needs: build-desktop + runs-on: ubuntu-latest + if: always() + + steps: + - name: Generate build summary + run: | + echo "## 🖥️ 桌面應用構建摘要" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 構建結果" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # 檢查各平台構建狀態 + if [ "${{ needs.build-desktop.result }}" = "success" ]; then + echo "✅ 所有平台構建成功" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.build-desktop.result }}" = "failure" ]; then + echo "❌ 部分平台構建失敗" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ 構建狀態: ${{ needs.build-desktop.result }}" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 下一步" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- 構建產物已上傳到 GitHub Artifacts" >> $GITHUB_STEP_SUMMARY + echo "- 可以在發佈流程中使用這些二進制文件" >> $GITHUB_STEP_SUMMARY + echo "- 如需重新構建,請手動觸發此工作流程" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c5cf2f6..3a148cc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,6 +16,15 @@ on: description: 'Custom version number (e.g., 2.5.0) - overrides version_type if provided' required: false type: string + include_desktop: + description: '是否包含桌面應用二進制文件' + required: true + default: true + type: boolean + desktop_build_run_id: + description: '桌面應用構建的 Run ID(可選,留空使用最新的成功構建)' + required: false + type: string jobs: release: @@ -38,6 +47,9 @@ jobs: - name: Set up Python run: uv python install + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Install dependencies run: | uv sync --dev @@ -275,6 +287,101 @@ jobs: - Auto-generated release from workflow" git tag "v${{ steps.bump_version.outputs.new }}" + - name: Download desktop binaries from latest build + if: ${{ github.event.inputs.include_desktop == 'true' }} + uses: dawidd6/action-download-artifact@v6 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-desktop.yml + run_id: ${{ github.event.inputs.desktop_build_run_id }} + path: desktop-artifacts + if_no_artifact_found: warn + + - name: Prepare multi-platform desktop binaries + if: ${{ github.event.inputs.include_desktop == 'true' }} + run: | + # 檢查是否有桌面應用構建產物 + if [ ! -d "desktop-artifacts" ] || [ -z "$(ls -A desktop-artifacts 2>/dev/null)" ]; then + echo "⚠️ 警告:沒有找到桌面應用構建產物" + echo "💡 提示:請先運行 'Build Desktop Applications' 工作流程" + echo "🔧 或者設置 include_desktop 為 false 來跳過桌面應用" + exit 1 + fi + + # 創建桌面應用目錄 + mkdir -p src/mcp_feedback_enhanced/desktop_release + + # 複製所有平台的二進制文件並重命名 + echo "📦 準備多平台桌面應用二進制文件..." + + # 檢查並複製各平台文件 + if [ -f "desktop-artifacts/desktop-windows/mcp-feedback-enhanced-desktop.exe" ]; then + cp desktop-artifacts/desktop-windows/mcp-feedback-enhanced-desktop.exe src/mcp_feedback_enhanced/desktop_release/ + echo "✅ Windows 二進制文件已複製" + else + echo "⚠️ Windows 二進制文件不存在" + fi + + if [ -f "desktop-artifacts/desktop-macos-intel/mcp-feedback-enhanced-desktop" ]; then + cp desktop-artifacts/desktop-macos-intel/mcp-feedback-enhanced-desktop src/mcp_feedback_enhanced/desktop_release/mcp-feedback-enhanced-desktop-macos-intel + echo "✅ macOS Intel 二進制文件已複製" + else + echo "⚠️ macOS Intel 二進制文件不存在" + fi + + if [ -f "desktop-artifacts/desktop-macos-arm64/mcp-feedback-enhanced-desktop" ]; then + cp desktop-artifacts/desktop-macos-arm64/mcp-feedback-enhanced-desktop src/mcp_feedback_enhanced/desktop_release/mcp-feedback-enhanced-desktop-macos-arm64 + echo "✅ macOS ARM64 二進制文件已複製" + else + echo "⚠️ macOS ARM64 二進制文件不存在" + fi + + if [ -f "desktop-artifacts/desktop-linux/mcp-feedback-enhanced-desktop" ]; then + cp desktop-artifacts/desktop-linux/mcp-feedback-enhanced-desktop src/mcp_feedback_enhanced/desktop_release/mcp-feedback-enhanced-desktop-linux + echo "✅ Linux 二進制文件已複製" + else + echo "⚠️ Linux 二進制文件不存在" + fi + + # 設置執行權限 + chmod +x src/mcp_feedback_enhanced/desktop_release/mcp-feedback-enhanced-desktop-* 2>/dev/null || true + + # 創建 __init__.py + echo '"""桌面應用程式二進制檔案"""' > src/mcp_feedback_enhanced/desktop_release/__init__.py + + # 顯示文件列表 + echo "📦 最終的桌面應用二進制文件:" + ls -la src/mcp_feedback_enhanced/desktop_release/ + + - name: Build desktop applications locally (fallback) + if: ${{ github.event.inputs.include_desktop == 'true' }} + run: | + # 如果沒有預構建的桌面應用,嘗試本地構建 + if [ ! -d "src/mcp_feedback_enhanced/desktop_release" ] || [ -z "$(ls -A src/mcp_feedback_enhanced/desktop_release 2>/dev/null)" ]; then + echo "🔧 沒有找到預構建的桌面應用,嘗試本地構建..." + echo "⚠️ 注意:本地構建可能只支援當前平台" + + # 安裝 Rust(如果還沒安裝) + if ! command -v cargo &> /dev/null; then + echo "📦 安裝 Rust..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source ~/.cargo/env + fi + + # 運行本地構建腳本 + python scripts/build_desktop.py --release + + echo "✅ 本地桌面應用構建完成" + else + echo "✅ 使用預構建的桌面應用" + fi + + - name: Skip desktop applications + if: ${{ github.event.inputs.include_desktop != 'true' }} + run: | + echo "⏭️ 跳過桌面應用,僅發佈 Web 版本" + echo "💡 用戶將只能使用 Web 模式,無法使用桌面模式" + - name: Build package run: uv build @@ -311,10 +418,32 @@ jobs: echo "📦 Published to PyPI: https://pypi.org/project/mcp-feedback-enhanced/" echo "🚀 GitHub Release: https://github.com/Minidoracat/mcp-feedback-enhanced/releases/tag/v${{ steps.bump_version.outputs.new }}" echo "📝 Release notes generated from CHANGELOG files" + echo "" + + # 顯示桌面應用狀態 + if [ "${{ github.event.inputs.include_desktop }}" = "true" ]; then + echo "🖥️ 桌面應用狀態:" + if [ -d "src/mcp_feedback_enhanced/desktop_release" ] && [ -n "$(ls -A src/mcp_feedback_enhanced/desktop_release 2>/dev/null)" ]; then + echo " ✅ 桌面應用已包含在發佈中" + echo " 📱 支援的平台:" + [ -f "src/mcp_feedback_enhanced/desktop_release/mcp-feedback-enhanced-desktop.exe" ] && echo " - Windows x64" + [ -f "src/mcp_feedback_enhanced/desktop_release/mcp-feedback-enhanced-desktop-macos-intel" ] && echo " - macOS Intel" + [ -f "src/mcp_feedback_enhanced/desktop_release/mcp-feedback-enhanced-desktop-macos-arm64" ] && echo " - macOS Apple Silicon" + [ -f "src/mcp_feedback_enhanced/desktop_release/mcp-feedback-enhanced-desktop-linux" ] && echo " - Linux x64" + else + echo " ⚠️ 桌面應用未包含(可能構建失敗)" + fi + else + echo "🖥️ 桌面應用狀態:⏭️ 已跳過(僅 Web 版本)" + fi + echo "" echo "✅ Next steps:" echo " - Check the release on GitHub" echo " - Verify the package on PyPI" echo " - Test installation with: uvx mcp-feedback-enhanced@v${{ steps.bump_version.outputs.new }}" + if [ "${{ github.event.inputs.include_desktop }}" = "true" ]; then + echo " - Test desktop mode with: uvx mcp-feedback-enhanced@v${{ steps.bump_version.outputs.new }} test --desktop" + fi echo "" echo "📋 Note: Make sure CHANGELOG files are updated for future releases" diff --git a/.gitignore b/.gitignore index c753a01..49c7367 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,29 @@ ui_settings.json .env .env.local .env.*.local + +# Rust/Tauri build artifacts +src-tauri/target/ +src-tauri/Cargo.lock +src-tauri/WixTools/ +src-tauri/gen/ + +# Desktop application binaries (these should be built and copied by build script) +src/mcp_feedback_enhanced/desktop_release/* +src/mcp_feedback_enhanced/desktop_app/ +src/mcp_feedback_enhanced/desktop/mcp-feedback-enhanced-desktop + + + +# Tauri bundle outputs +src-tauri/target/bundle/ +src-tauri/target/release/bundle/ + +# Node.js (if using Tauri with frontend framework) +node_modules/ +package-lock.json +yarn.lock + +# Temporary build files +*.tmp +*.temp diff --git a/Makefile b/Makefile index 182b683..39b1d2c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # Compatible with Windows PowerShell and Unix systems # 兼容 Windows PowerShell 和 Unix 系統 -.PHONY: help install install-dev install-hooks lint format type-check test clean pre-commit-run pre-commit-all update-deps +.PHONY: help install install-dev install-hooks lint format type-check test clean pre-commit-run pre-commit-all update-deps check-rust build-desktop build-desktop-release test-desktop clean-desktop build-all test-all # 預設目標 - 顯示幫助訊息 help: ## Show this help message @@ -36,6 +36,13 @@ help: ## Show this help message @echo " bump-major Bump major version" @echo " ci Simulate CI pipeline locally" @echo " quick-check Quick check with auto-fix" + @echo "" + @echo "Desktop Application Commands:" + @echo " build-desktop Build desktop application (debug)" + @echo " build-desktop-release Build desktop application (release)" + @echo " test-desktop Test desktop application" + @echo " clean-desktop Clean desktop build artifacts" + @echo " check-rust Check Rust development environment" # 安裝相關命令 install: ## Install the package @@ -142,3 +149,35 @@ quick-check: lint-fix format type-check ## Quick check with auto-fix (recommende # Windows PowerShell 專用命令 ps-clean: ## PowerShell version of clean (Windows) powershell -Command "Get-ChildItem -Path . -Recurse -Name '__pycache__' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue; Get-ChildItem -Path . -Recurse -Name '*.pyc' | Remove-Item -Force -ErrorAction SilentlyContinue; @('.mypy_cache', '.ruff_cache', '.pytest_cache', 'htmlcov', 'dist', 'build') | ForEach-Object { if (Test-Path $$_) { Remove-Item $$_ -Recurse -Force } }" + +# 桌面應用程式相關命令 +check-rust: ## Check Rust development environment + @echo "🔍 Checking Rust environment..." + @rustc --version || (echo "❌ Rust not installed. Please visit https://rustup.rs/" && exit 1) + @cargo --version || (echo "❌ Cargo not installed" && exit 1) + @cargo install --list | grep tauri-cli || (echo "⚠️ Tauri CLI not installed, installing..." && cargo install tauri-cli) + @echo "✅ Rust environment check completed" + +build-desktop: ## Build desktop application (debug mode) + @echo "🔨 Building desktop application (debug)..." + uv run python scripts/build_desktop.py + +build-desktop-release: ## Build desktop application (release mode) + @echo "🚀 Building desktop application (release)..." + uv run python scripts/build_desktop.py --release + +test-desktop: build-desktop ## Test desktop application + @echo "🖥️ Testing desktop application..." + uv run python -m mcp_feedback_enhanced test --desktop + +clean-desktop: ## Clean desktop build artifacts + @echo "🧹 Cleaning desktop build artifacts..." + uv run python scripts/build_desktop.py --clean + +# 完整構建流程(包含桌面應用程式) +build-all: clean build-desktop-release build ## Build complete package with desktop app + @echo "🎉 Complete build finished!" + +# 測試所有功能 +test-all: test test-desktop ## Run all tests including desktop + @echo "✅ All tests completed!" diff --git a/docs/DESKTOP_BUILD.md b/docs/DESKTOP_BUILD.md new file mode 100644 index 0000000..269ba27 --- /dev/null +++ b/docs/DESKTOP_BUILD.md @@ -0,0 +1,244 @@ +# 桌面應用程式構建指南 + +本文檔說明如何構建 MCP Feedback Enhanced 的桌面應用程式。 + +## 先決條件 + +### 必需工具 + +1. **Python 3.8+** + ```bash + python --version + ``` + +2. **Rust 工具鏈** + ```bash + # 安裝 Rust (如果尚未安裝) + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + + # 驗證安裝 + rustc --version + cargo --version + ``` + +3. **Tauri CLI** (自動安裝) + ```bash + cargo install tauri-cli + ``` + +### 開發依賴 + +```bash +# 安裝 Python 開發依賴 +uv sync --dev + +# 或使用 pip +pip install -e ".[dev]" +``` + +## 構建方法 + +### 方法 1: 使用 Makefile (推薦) + +```bash +# 構建 Debug 版本 +make build-desktop + +# 構建 Release 版本 +make build-desktop-release + +# 構建並測試 +make test-desktop + +# 清理構建產物 +make clean-desktop + +# 完整構建流程 (包含 PyPI 包) +make build-all +``` + +### 方法 2: 直接使用 Python 腳本 + +```bash +# 構建 Debug 版本 +python scripts/build_desktop.py + +# 構建 Release 版本 +python scripts/build_desktop.py --release + +# 清理構建產物 +python scripts/build_desktop.py --clean + +# 查看幫助 +python scripts/build_desktop.py --help +``` + +## 構建產物 + +構建完成後,產物將位於: + +``` +src/mcp_feedback_enhanced/ +├── desktop_release/ # 發佈用二進制文件 +│ ├── __init__.py +│ ├── mcp-feedback-enhanced-desktop.exe # Windows +│ ├── mcp-feedback-enhanced-desktop-macos-intel # macOS Intel +│ ├── mcp-feedback-enhanced-desktop-macos-arm64 # macOS Apple Silicon +│ └── mcp-feedback-enhanced-desktop-linux # Linux +├── desktop_app/ # 發佈用 Python 模組 +│ ├── __init__.py +│ └── desktop_app.py +└── ... + +src-tauri/python/mcp_feedback_enhanced_desktop/ # 開發環境模組 +├── __init__.py +├── desktop_app.py +└── ... +``` + +### 多平台支援 + +構建腳本會自動構建以下平台的二進制文件: + +- **Windows**: `mcp-feedback-enhanced-desktop.exe` +- **macOS Intel**: `mcp-feedback-enhanced-desktop-macos-intel` +- **macOS Apple Silicon**: `mcp-feedback-enhanced-desktop-macos-arm64` +- **Linux**: `mcp-feedback-enhanced-desktop-linux` + +桌面應用會根據運行平台自動選擇對應的二進制文件。 + +## 測試桌面應用程式 + +```bash +# 方法 1: 直接測試 +python -m mcp_feedback_enhanced test --desktop + +# 方法 2: 使用 Makefile +make test-desktop +``` + +## 跨平台注意事項 + +### Windows +- 桌面應用程式不會顯示額外的 CMD 視窗 +- 二進制檔案: `mcp-feedback-enhanced-desktop.exe` + +### Linux/macOS +- 二進制檔案: `mcp-feedback-enhanced-desktop` +- 自動設置執行權限 + +## 故障排除 + +### 常見問題 + +1. **Rust 未安裝** + ``` + ❌ Rust 未安裝,請訪問 https://rustup.rs/ + ``` + 解決方案: 安裝 Rust 工具鏈 + +2. **Tauri CLI 未安裝** + ``` + ⚠️ Tauri CLI 未安裝,正在安裝... + ``` + 解決方案: 腳本會自動安裝,或手動執行 `cargo install tauri-cli` + +3. **構建失敗** + ``` + ❌ 構建失敗 + ``` + 解決方案: 檢查 Rust 環境,清理後重新構建 + ```bash + make clean-desktop + make build-desktop + ``` + +### 檢查環境 + +```bash +# 檢查 Rust 環境 +make check-rust + +# 檢查所有依賴 +make dev-setup +``` + +### 跨平台編譯要求 + +構建腳本會自動安裝以下 Rust targets: + +```bash +# 這些 targets 會自動安裝,無需手動執行 +rustup target add x86_64-pc-windows-msvc # Windows +rustup target add x86_64-apple-darwin # macOS Intel +rustup target add aarch64-apple-darwin # macOS Apple Silicon +rustup target add x86_64-unknown-linux-gnu # Linux +``` + +**注意**: +- 本地構建通常只能成功構建當前平台的二進制文件 +- 跨平台編譯需要複雜的工具鏈配置(C 編譯器、系統庫等) +- **完整的多平台支援在 GitHub Actions CI 中實現** +- 發佈到 PyPI 時會自動包含所有平台的二進制文件 + +## 自動化構建 + +### CI/CD 多平台構建 + +GitHub Actions 會在各自的原生平台上構建桌面應用: + +```yaml +# 多平台構建策略 +strategy: + matrix: + include: + - os: windows-latest # Windows 原生構建 + target: x86_64-pc-windows-msvc + - os: macos-latest # macOS 原生構建 + target: x86_64-apple-darwin + - os: ubuntu-latest # Linux 原生構建 + target: x86_64-unknown-linux-gnu +``` + +這確保了: +- ✅ 每個平台在其原生環境中構建 +- ✅ 避免跨平台編譯的複雜性 +- ✅ 最終 PyPI 包包含所有平台的二進制文件 + +### 本地自動化 + +```bash +# 完整的開發工作流程 +make dev-setup # 初始化環境 +make quick-check # 程式碼檢查 +make build-all # 構建所有組件 +make test-all # 測試所有功能 +``` + +## 發布流程 + +1. **構建發布版本** + ```bash + make build-desktop-release + ``` + +2. **構建 PyPI 包** + ```bash + make build-all + ``` + +3. **驗證包內容** + ```bash + # 檢查桌面應用程式是否包含在包中 + tar -tf dist/*.tar.gz | grep desktop + ``` + +桌面應用程式現在已完全集成到 PyPI 包中,包含所有主要平台的二進制文件,用戶安裝後可直接使用 `--desktop` 選項啟動。 + +### 平台兼容性 + +- **Windows**: 支援 x64 架構 +- **macOS**: 支援 Intel 和 Apple Silicon (M1/M2) 架構 +- **Linux**: 支援 x64 架構 + +桌面應用會自動檢測運行平台並選擇對應的二進制文件。 diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md new file mode 100644 index 0000000..06e155f --- /dev/null +++ b/docs/WORKFLOWS.md @@ -0,0 +1,122 @@ +# GitHub Actions 工作流程說明 + +本項目使用雙工作流程架構來優化構建和發佈流程。 + +## 🏗️ 工作流程架構 + +### 1. 桌面應用構建工作流程 (build-desktop.yml) + +**用途**: 專門負責構建多平台桌面應用二進制文件 + +**觸發條件**: +- 手動觸發 (workflow_dispatch) +- 桌面應用代碼變更時自動觸發 (`src-tauri/**`, `scripts/build_desktop.py`) +- Pull Request 中的桌面應用變更 + +**功能**: +- 在各自原生平台上構建桌面應用 +- 支援選擇性平台構建 +- 上傳構建產物到 GitHub Artifacts (保留 30 天) +- 提供詳細的構建摘要 + +**支援平台**: +- Windows x64 (`windows-latest`) +- macOS Intel (`macos-latest` + `x86_64-apple-darwin`) +- macOS Apple Silicon (`macos-latest` + `aarch64-apple-darwin`) +- Linux x64 (`ubuntu-latest`) + +### 2. 發佈工作流程 (publish.yml) + +**用途**: 負責版本管理和 PyPI 發佈 + +**觸發條件**: +- 手動觸發 (workflow_dispatch) + +**功能**: +- 自動或手動版本號管理 +- 可選擇是否包含桌面應用 +- 從最新的桌面應用構建下載二進制文件 +- 發佈到 PyPI +- 創建 GitHub Release + +## 🚀 使用方式 + +### 開發桌面應用時 + +1. **修改桌面應用代碼** (`src-tauri/` 目錄) +2. **自動觸發構建** - 推送到 main 分支會自動觸發桌面應用構建 +3. **手動觸發構建** (可選) - 在 GitHub Actions 頁面手動運行 "Build Desktop Applications" + +### 發佈新版本時 + +1. **確保桌面應用已構建** - 檢查最新的 "Build Desktop Applications" 工作流程是否成功 +2. **手動觸發發佈** - 在 GitHub Actions 頁面運行 "Auto Release to PyPI" +3. **選擇發佈選項**: + - `version_type`: patch/minor/major (或使用 custom_version) + - `include_desktop`: 是否包含桌面應用 (預設: true) + - `desktop_build_run_id`: 指定特定的構建 ID (可選) + +## 📋 最佳實踐 + +### 桌面應用構建 + +```bash +# 本地測試桌面應用構建 +python scripts/build_desktop.py --release + +# 檢查構建產物 +ls -la src/mcp_feedback_enhanced/desktop_release/ +ls -la src/mcp_feedback_enhanced/desktop_app/ +``` + +### 發佈流程 + +1. **準備發佈**: + - 更新 CHANGELOG 文件 + - 確保桌面應用構建成功 + - 測試本地功能 + +2. **執行發佈**: + - 手動觸發 "Auto Release to PyPI" 工作流程 + - 選擇適當的版本類型 + - 確認包含桌面應用 (如果需要) + +3. **發佈後驗證**: + - 檢查 PyPI 上的新版本 + - 測試安裝: `uvx mcp-feedback-enhanced@latest` + - 測試桌面模式: `uvx mcp-feedback-enhanced@latest test --desktop` + +## 🔧 故障排除 + +### 桌面應用構建失敗 + +1. **檢查構建日誌** - 查看 GitHub Actions 中的詳細錯誤信息 +2. **平台特定問題**: + - macOS: 可能缺少 Xcode 命令行工具 + - Linux: 可能缺少系統依賴 (GTK, Cairo 等) + - Windows: 通常構建成功 + +3. **本地測試** - 在對應平台上運行本地構建腳本 + +### 發佈時桌面應用缺失 + +1. **檢查構建狀態** - 確保最新的桌面應用構建成功 +2. **手動指定構建** - 使用 `desktop_build_run_id` 參數指定特定的成功構建 +3. **跳過桌面應用** - 設置 `include_desktop: false` 僅發佈 Web 版本 + +## 📊 工作流程優勢 + +### 效率提升 +- **分離關注點**: 構建和發佈獨立進行 +- **避免重複構建**: 不是每次發佈都需要重新構建桌面應用 +- **快速發佈**: 發佈流程更快速,特別是僅修改 Python 代碼時 + +### 靈活性 +- **選擇性構建**: 可以只構建特定平台 +- **選擇性發佈**: 可以選擇是否包含桌面應用 +- **版本控制**: 可以使用不同的桌面應用構建版本 + +### 可靠性 +- **原生構建**: 每個平台在其原生環境中構建 +- **構建緩存**: 利用 GitHub Actions 緩存加速構建 +- **錯誤隔離**: 桌面應用構建失敗不會影響 Web 版本發佈 diff --git a/examples/mcp-config-desktop.json b/examples/mcp-config-desktop.json new file mode 100644 index 0000000..46a46cc --- /dev/null +++ b/examples/mcp-config-desktop.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "mcp-feedback-enhanced": { + "command": "uvx", + "args": ["mcp-feedback-enhanced@latest"], + "timeout": 600, + "env": { + "MCP_DESKTOP_MODE": "true", + "MCP_WEB_PORT": "8765", + "MCP_DEBUG": "false" + }, + "autoApprove": ["interactive_feedback"] + } + } +} diff --git a/examples/mcp-config-web.json b/examples/mcp-config-web.json new file mode 100644 index 0000000..675f8cd --- /dev/null +++ b/examples/mcp-config-web.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "mcp-feedback-enhanced": { + "command": "uvx", + "args": ["mcp-feedback-enhanced@latest"], + "timeout": 600, + "env": { + "MCP_DESKTOP_MODE": "false", + "MCP_WEB_PORT": "8765", + "MCP_DEBUG": "false" + }, + "autoApprove": ["interactive_feedback"] + } + } +} diff --git a/pyproject.toml b/pyproject.toml index f71740f..c585834 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,12 +45,20 @@ mcp-feedback-enhanced = "mcp_feedback_enhanced.__main__:main" interactive-feedback-mcp = "mcp_feedback_enhanced.__main__:main" [build-system] -requires = ["hatchling"] +requires = [ + "hatchling", + "maturin>=1.8.7", + "setuptools-rust>=1.11.1" +] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/mcp_feedback_enhanced"] +# 包含桌面應用程式二進制檔案 +[tool.hatch.build.targets.wheel.force-include] +"src/mcp_feedback_enhanced/desktop_release" = "mcp_feedback_enhanced/desktop_release" + [tool.uv] dev-dependencies = [ "bump2version>=1.0.1", @@ -61,6 +69,9 @@ dev-dependencies = [ "ruff>=0.11.0", "mypy>=1.16.0", "pre-commit>=4.0.0", + "maturin>=1.8.7", + "setuptools-rust>=1.11.1", + "pillow>=11.2.1", ] # ===== Ruff 配置 ===== diff --git a/scripts/build_desktop.py b/scripts/build_desktop.py new file mode 100644 index 0000000..306c278 --- /dev/null +++ b/scripts/build_desktop.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python3 +""" +桌面應用程式構建腳本 + +此腳本負責構建 Tauri 桌面應用程式和 Python 擴展模組, +確保在 PyPI 發布時包含預編譯的二進制檔案。 + +使用方法: + python scripts/build_desktop.py [--release] [--clean] +""" + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path + + +def run_command( + cmd: list[str], cwd: str = None, check: bool = True, show_info: bool = True +) -> subprocess.CompletedProcess: + """執行命令並返回結果""" + if show_info: + print(f"🔧 執行命令: {' '.join(cmd)}") + if cwd: + print(f"📁 工作目錄: {cwd}") + + result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, check=False) + + # 處理標準輸出 + if result.stdout and show_info: + print("📤 輸出:") + print(result.stdout.strip()) + + # 智能處理標準錯誤 - 區分信息和真正的錯誤 + if result.stderr: + stderr_lines = result.stderr.strip().split("\n") + info_lines = [] + error_lines = [] + + for line in stderr_lines: + stripped_line = line.strip() + if not stripped_line: + continue + # 識別信息性消息和正常編譯輸出 + if ( + stripped_line.startswith("info:") + or "is up to date" in stripped_line + or "downloading component" in stripped_line + or "installing component" in stripped_line + or stripped_line.startswith("Compiling") + or stripped_line.startswith("Finished") + or stripped_line.startswith("Building") + or "target(s) in" in stripped_line + ): + info_lines.append(stripped_line) + else: + error_lines.append(stripped_line) + + # 顯示信息性消息 + if info_lines and show_info: + print("ℹ️ 信息:") + for line in info_lines: + print(f" {line}") + + # 顯示真正的錯誤 + if error_lines: + print("❌ 錯誤:") + for line in error_lines: + print(f" {line}") + + if check and result.returncode != 0: + raise subprocess.CalledProcessError(result.returncode, cmd) + + return result + + +def check_rust_environment(): + """檢查 Rust 開發環境""" + print("🔍 檢查 Rust 開發環境...") + + try: + result = run_command(["rustc", "--version"]) + print(f"✅ Rust 編譯器: {result.stdout.strip()}") + except (subprocess.CalledProcessError, FileNotFoundError): + print("❌ 未找到 Rust 編譯器") + print("💡 請安裝 Rust: https://rustup.rs/") + return False + + try: + result = run_command(["cargo", "--version"]) + print(f"✅ Cargo: {result.stdout.strip()}") + except (subprocess.CalledProcessError, FileNotFoundError): + print("❌ 未找到 Cargo") + return False + + try: + result = run_command(["cargo", "install", "--list"]) + if "tauri-cli" in result.stdout: + print("✅ Tauri CLI 已安裝") + else: + print("⚠️ Tauri CLI 未安裝,嘗試安裝...") + run_command(["cargo", "install", "tauri-cli"]) + print("✅ Tauri CLI 安裝完成") + except subprocess.CalledProcessError: + print("❌ 無法安裝 Tauri CLI") + return False + + return True + + +def install_rust_targets(): + """安裝跨平台編譯所需的 Rust targets""" + print("🎯 安裝跨平台編譯 targets...") + + # 定義需要的 targets + targets = [ + ("x86_64-pc-windows-msvc", "Windows x64"), + ("x86_64-apple-darwin", "macOS Intel"), + ("aarch64-apple-darwin", "macOS Apple Silicon"), + ("x86_64-unknown-linux-gnu", "Linux x64"), + ] + + installed_count = 0 + updated_count = 0 + + for target, description in targets: + print(f"📦 檢查 target: {target} ({description})") + try: + result = run_command( + ["rustup", "target", "add", target], check=False, show_info=False + ) + + if result.returncode == 0: + # 檢查是否是新安裝還是已存在 + if "is up to date" in result.stderr: + print(f"✅ {description} - 已是最新版本") + updated_count += 1 + elif "installing component" in result.stderr: + print(f"🆕 {description} - 新安裝完成") + installed_count += 1 + else: + print(f"✅ {description} - 安裝成功") + installed_count += 1 + else: + print(f"⚠️ {description} - 安裝失敗") + if result.stderr: + print(f" 錯誤: {result.stderr.strip()}") + except Exception as e: + print(f"⚠️ 安裝 {description} 時發生錯誤: {e}") + + print( + f"✅ Rust targets 檢查完成 (新安裝: {installed_count}, 已存在: {updated_count})" + ) + + +def clean_build_artifacts(project_root: Path): + """清理構建產物""" + print("🧹 清理構建產物...") + + # 清理 Rust 構建產物 + rust_target = project_root / "src-tauri" / "target" + if rust_target.exists(): + print(f"清理 Rust target 目錄: {rust_target}") + shutil.rmtree(rust_target) + + # 清理 Python 構建產物 + python_build_dirs = [ + project_root / "build", + project_root / "dist", + project_root / "*.egg-info", + ] + + for build_dir in python_build_dirs: + if build_dir.exists(): + print(f"清理 Python 構建目錄: {build_dir}") + if build_dir.is_dir(): + shutil.rmtree(build_dir) + else: + build_dir.unlink() + + +def build_rust_extension(project_root: Path, release: bool = True): + """構建 Rust 擴展模組""" + print("🔨 構建 Rust 擴展模組...") + + src_tauri = project_root / "src-tauri" + if not src_tauri.exists(): + raise FileNotFoundError(f"src-tauri 目錄不存在: {src_tauri}") + + # 構建 Rust 庫 + build_cmd = ["cargo", "build"] + if release: + build_cmd.append("--release") + + run_command(build_cmd, cwd=str(src_tauri)) + print("✅ Rust 擴展模組構建完成") + + +def build_tauri_app_multiplatform(project_root: Path, release: bool = True): + """構建多平台 Tauri 桌面應用程式""" + print("🖥️ 構建多平台 Tauri 桌面應用程式...") + + src_tauri = project_root / "src-tauri" + + # 定義目標平台 + targets = [ + ("x86_64-pc-windows-msvc", "mcp-feedback-enhanced-desktop.exe"), + ("x86_64-apple-darwin", "mcp-feedback-enhanced-desktop"), + ("aarch64-apple-darwin", "mcp-feedback-enhanced-desktop"), + ("x86_64-unknown-linux-gnu", "mcp-feedback-enhanced-desktop"), + ] + + successful_builds = [] + + # 平台描述映射 + platform_descriptions = { + "x86_64-pc-windows-msvc": "Windows x64", + "x86_64-apple-darwin": "macOS Intel", + "aarch64-apple-darwin": "macOS Apple Silicon", + "x86_64-unknown-linux-gnu": "Linux x64", + } + + for target, binary_name in targets: + description = platform_descriptions.get(target, target) + print(f"🔨 構建 {description} ({target})...") + + # 構建命令 + build_cmd = [ + "cargo", + "build", + "--bin", + "mcp-feedback-enhanced-desktop", + "--target", + target, + ] + if release: + build_cmd.append("--release") + + try: + run_command(build_cmd, cwd=str(src_tauri), show_info=False) + successful_builds.append((target, binary_name)) + print(f"✅ {description} 構建成功") + except subprocess.CalledProcessError as e: + print(f"⚠️ {description} 構建失敗") + print("💡 可能缺少該平台的編譯工具鏈或依賴") + # 顯示具體錯誤信息 + if hasattr(e, "stderr") and e.stderr: + print(f" 錯誤詳情: {e.stderr.strip()}") + except Exception as e: + print(f"❌ {description} 構建時發生未預期錯誤: {e}") + + if successful_builds: + print(f"✅ 成功構建 {len(successful_builds)} 個平台") + + # 如果只構建了當前平台,給出提示 + if len(successful_builds) == 1: + print("") + print("💡 注意:只成功構建了當前平台的二進制文件") + print(" 其他平台的構建失敗通常是因為缺少跨平台編譯工具鏈") + print(" 完整的多平台支援將在 GitHub Actions CI 中完成") + print(" 發佈到 PyPI 時會包含所有平台的二進制文件") + + return successful_builds + print("❌ 所有平台構建都失敗了") + return [] + + +def copy_multiplatform_artifacts( + project_root: Path, successful_builds: list, release: bool = True +): + """複製多平台構建產物到適當位置""" + print("📦 複製多平台構建產物...") + + src_tauri = project_root / "src-tauri" + build_type = "release" if release else "debug" + + # 創建目標目錄 + desktop_dir = project_root / "src" / "mcp_feedback_enhanced" / "desktop_release" + desktop_dir.mkdir(parents=True, exist_ok=True) + + # 定義平台到文件名的映射 + platform_mapping = { + "x86_64-pc-windows-msvc": "mcp-feedback-enhanced-desktop.exe", + "x86_64-apple-darwin": "mcp-feedback-enhanced-desktop-macos-intel", + "aarch64-apple-darwin": "mcp-feedback-enhanced-desktop-macos-arm64", + "x86_64-unknown-linux-gnu": "mcp-feedback-enhanced-desktop-linux", + } + + copied_files = [] + + for target, original_binary_name in successful_builds: + # 源文件路徑 + src_file = src_tauri / "target" / target / build_type / original_binary_name + + # 目標文件名 + dst_filename = platform_mapping.get(target, original_binary_name) + dst_file = desktop_dir / dst_filename + + if src_file.exists(): + shutil.copy2(src_file, dst_file) + # 設置執行權限(非 Windows) + # 0o755 權限是必要的,因為這些是可執行的二進制檔案 + if not dst_filename.endswith(".exe"): + os.chmod(dst_file, 0o755) # noqa: S103 + copied_files.append(dst_filename) + print(f"✅ 複製 {target} 二進制檔案: {src_file} -> {dst_file}") + else: + print(f"⚠️ 找不到 {target} 的二進制檔案: {src_file}") + + if not copied_files: + print("⚠️ 沒有找到可複製的二進制檔案") + return False + + # 創建 __init__.py 文件,讓 desktop 目錄成為 Python 包 + desktop_init = desktop_dir / "__init__.py" + if not desktop_init.exists(): + desktop_init.write_text('"""桌面應用程式二進制檔案"""', encoding="utf-8") + print(f"✅ 創建 __init__.py: {desktop_init}") + + print(f"✅ 成功複製 {len(copied_files)} 個平台的二進制檔案") + return True + + +def copy_desktop_python_module(project_root: Path): + """複製桌面應用 Python 模組到發佈位置""" + print("📦 複製桌面應用 Python 模組...") + + # 源路徑和目標路徑 + python_src = project_root / "src-tauri" / "python" / "mcp_feedback_enhanced_desktop" + python_dst = project_root / "src" / "mcp_feedback_enhanced" / "desktop_app" + + if not python_src.exists(): + print(f"⚠️ 源模組不存在: {python_src}") + return False + + # 如果目標目錄存在,先刪除 + if python_dst.exists(): + shutil.rmtree(python_dst) + print(f"🗑️ 清理舊的模組目錄: {python_dst}") + + # 複製模組 + shutil.copytree(python_src, python_dst) + print(f"✅ 複製桌面應用模組: {python_src} -> {python_dst}") + + return True + + +def main(): + """主函數""" + parser = argparse.ArgumentParser( + description="構建 MCP Feedback Enhanced 桌面應用程式", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +範例: + python scripts/build_desktop.py # 構建 Debug 版本 + python scripts/build_desktop.py --release # 構建 Release 版本 + python scripts/build_desktop.py --clean # 清理構建產物 + +構建完成後,可以使用以下命令測試: + python -m mcp_feedback_enhanced test --desktop + +或使用 Makefile: + make build-desktop # 構建 Debug 版本 + make build-desktop-release # 構建 Release 版本 + make test-desktop # 構建並測試 + """, + ) + parser.add_argument( + "--release", action="store_true", help="構建發布版本 (預設為 Debug)" + ) + parser.add_argument("--clean", action="store_true", help="清理構建產物") + + args = parser.parse_args() + + # 獲取專案根目錄 + project_root = Path(__file__).parent.parent.resolve() + print(f"專案根目錄: {project_root}") + + try: + # 清理構建產物(如果需要) + if args.clean: + clean_build_artifacts(project_root) + + # 檢查 Rust 環境 + if not check_rust_environment(): + sys.exit(1) + + # 安裝跨平台編譯 targets + install_rust_targets() + + # 構建 Rust 擴展 + build_rust_extension(project_root, args.release) + + # 構建多平台 Tauri 應用程式 + successful_builds = build_tauri_app_multiplatform(project_root, args.release) + + if not successful_builds: + print("❌ 沒有成功構建任何平台") + sys.exit(1) + + # 複製多平台構建產物 + if not copy_multiplatform_artifacts( + project_root, successful_builds, args.release + ): + print("⚠️ 構建產物複製失敗,但 Rust 編譯成功") + return + + # 複製桌面應用 Python 模組 + if not copy_desktop_python_module(project_root): + print("⚠️ 桌面應用模組複製失敗") + return + + print("🎉 多平台桌面應用程式構建完成!") + print("") + print("📍 構建產物位置:") + print(" 多平台二進制檔案: src/mcp_feedback_enhanced/desktop_release/") + print(" 桌面應用模組: src/mcp_feedback_enhanced/desktop_app/") + print(" 開發環境模組: src-tauri/python/mcp_feedback_enhanced_desktop/") + print("") + print("🌍 支援的平台:") + for target, _ in successful_builds: + print(f" ✅ {target}") + print("") + print("🚀 下一步:") + print(" 測試桌面應用程式: python -m mcp_feedback_enhanced test --desktop") + print(" 或使用 Makefile: make test-desktop") + print(" 構建發布包: make build-all") + + except Exception as e: + print(f"❌ 構建失敗: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..b485bd5 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "mcp-feedback-enhanced-desktop" +version = "2.4.3" +edition = "2021" +rust-version = "1.70" +description = "Desktop application for MCP Feedback Enhanced using Tauri" +authors = ["Minidoracat "] +license = "MIT" + +# 設置 crate 類型為 cdylib,用於 Python 擴展 +[lib] +name = "mcp_feedback_enhanced_desktop_lib" +crate-type = ["cdylib"] + +# 二進制目標 +[[bin]] +name = "mcp-feedback-enhanced-desktop" +path = "src/main.rs" + +[dependencies] +# Tauri 核心依賴 +tauri = { version = "2.2", features = ["custom-protocol"] } +tauri-plugin-shell = "2.2" + +# PyO3 用於 Python 綁定 +pyo3 = { version = "0.22", features = ["extension-module"] } + +# 序列化支援 +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# 異步運行時 +tokio = { version = "1.0", features = ["full"] } + +# 日誌記錄 +log = "0.4" +env_logger = "0.11" + +[build-dependencies] +tauri-build = { version = "2.2", features = [] } + +# 開發配置文件 +[profile.dev] +incremental = true +debug = true + +# 發布配置文件 +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true + +# 專用於打包的配置文件 +[profile.bundle-dev] +inherits = "dev" + +[profile.bundle-release] +inherits = "release" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/generate_icons.py b/src-tauri/generate_icons.py new file mode 100644 index 0000000..cd2af8f --- /dev/null +++ b/src-tauri/generate_icons.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +生成基本的應用程式圖標 + +這個腳本會生成 Tauri 應用程式所需的基本圖標文件。 +在實際部署中,應該使用專業的圖標設計。 +""" + +import os + +from PIL import Image, ImageDraw + + +def create_simple_icon(size, output_path): + """創建簡單的圖標""" + # 創建圖像 + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + # 繪製簡單的圖標 - 一個帶邊框的圓形 + margin = size // 8 + draw.ellipse( + [margin, margin, size - margin, size - margin], + fill=(52, 152, 219, 255), + outline=(41, 128, 185, 255), + width=2, + ) + + # 在中心繪製 "MCP" 文字 + try: + # 嘗試使用系統字體 + from PIL import ImageFont + + font_size = size // 4 + try: + font = ImageFont.truetype("arial.ttf", font_size) + except: + font = ImageFont.load_default() + except ImportError: + font = None + + text = "MCP" + if font: + bbox = draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + else: + text_width = len(text) * (size // 8) + text_height = size // 6 + + text_x = (size - text_width) // 2 + text_y = (size - text_height) // 2 + + draw.text((text_x, text_y), text, fill=(255, 255, 255, 255), font=font) + + # 保存圖像 + img.save(output_path) + print(f"已生成圖標: {output_path}") + + +def main(): + """主函數""" + icons_dir = "icons" + os.makedirs(icons_dir, exist_ok=True) + + # 生成不同尺寸的 PNG 圖標 + sizes = [32, 128, 256] + for size in sizes: + if size == 128: + # 生成普通和 2x 版本 + create_simple_icon(size, f"{icons_dir}/{size}x{size}.png") + create_simple_icon(size * 2, f"{icons_dir}/{size}x{size}@2x.png") + else: + create_simple_icon(size, f"{icons_dir}/{size}x{size}.png") + + # 為 Windows 創建 ICO 文件 + try: + img_256 = Image.open(f"{icons_dir}/256x256.png") + img_256.save( + f"{icons_dir}/icon.ico", + format="ICO", + sizes=[(256, 256), (128, 128), (64, 64), (32, 32), (16, 16)], + ) + print(f"已生成 Windows 圖標: {icons_dir}/icon.ico") + except Exception as e: + print(f"生成 ICO 文件失敗: {e}") + + # 為 macOS 創建 ICNS 文件(需要額外工具) + print("注意:macOS ICNS 文件需要使用專門的工具生成") + print("可以使用在線工具或 iconutil 命令將 PNG 轉換為 ICNS") + + print("圖標生成完成!") + + +if __name__ == "__main__": + try: + main() + except ImportError: + print("錯誤:需要安裝 Pillow 庫") + print("請運行:pip install Pillow") + except Exception as e: + print(f"生成圖標時發生錯誤: {e}") diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..fc9a86d8403267ae2cbbd62b680dd5e0fcba20a3 GIT binary patch literal 2376 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/256x256.png b/src-tauri/icons/256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..5db972b8af7e40bcb339dfaebf51bc918b8d1f8e GIT binary patch literal 4793 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..382c39c70b2f6071cfe3f59a68f7ebe06b075a5f GIT binary patch literal 550 zcmV+>0@?kEP)kvkAEasL_!m0M_i+dzFgpm}>@mY+|A^04@ zL<`H&)#HmgCR$idiX)gvF@#b_5bXd>sB0;;9NiJvLV<~4fB_S&eNQM9m>32aFyWI7 z*5YT_7NYN8iBg#sjYy zw%q*0Aj-|cFvVMnVe`#T?=Dzy1N^{B6$| z?m^{XfQ5;H;qO2E)dv$^Sq27%JMVrmC=0SPC<(GLJpAw*F8=TTe+Fix0#=ZN86GwD z4r1Uam=_?!u;9Xb%Ax=SF1`HDP+~30(0%kZgNl$4SnTAJ?+j@sLJapm{AT$1=O06( zlQ_e?bMH~2fEd7pE&!`IkSh~VG>C99!-F3LPCfm`aN@}~hQ&cL46DQC8O}fd4#v0u z%sljpi4mJ(T9gI29KeK4F}6^kh2=OMfK4-{mg53UxFnEWORgh`X_PSGGk_9D5ZfYR o9Q19%LD4FvWxrq)Fbp&R09Py9?#AjG=l}o!07*qoM6N<$g0N@x+W-In literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..e7ddd889ec1ba7516a1911043d385ee03fcab110 GIT binary patch literal 18629 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%0AOLcI~LB_YFUEU!MlnfV~dPE#5`kR4(A<@ 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e7ddd889ec1ba7516a1911043d385ee03fcab110 GIT binary patch literal 18629 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%0AOLcI~LB_YFUEU!MlnfV~dPE#5`kR4(A<@ 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 literal 0 HcmV?d00001 diff --git a/src-tauri/pyproject.toml b/src-tauri/pyproject.toml new file mode 100644 index 0000000..5ddd3d6 --- /dev/null +++ b/src-tauri/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "mcp-feedback-enhanced-desktop" +version = "2.4.3" +description = "Desktop application extension for MCP Feedback Enhanced" +requires-python = ">=3.11" +dependencies = [ + "mcp-feedback-enhanced>=2.4.3" +] + +[project.entry-points.pytauri] +ext_mod = "mcp_feedback_enhanced_desktop.ext_mod" + +[build-system] +requires = [ + "setuptools>=61", + "setuptools-rust>=1.11.1", + "maturin>=1.8.7" +] +build-backend = "setuptools.build_meta" + +# Maturin 配置 +[tool.maturin] +# Python 源碼目錄 +python-source = "python" +# 模組名稱 +module-name = "mcp_feedback_enhanced_desktop.ext_mod" +# 必要的功能特性 +features = ["pyo3/extension-module", "tauri/custom-protocol"] +# 使用 Git 作為 sdist 生成器 +sdist-generator = "git" +# 包含前端資源 +include = [ + { path = "../src/mcp_feedback_enhanced/web/static/**/*", format = "sdist" } +] + +# 支援 Python 穩定 ABI +[tool.maturin.abi3] +enabled = true +minimum = "3.11" diff --git a/src-tauri/python/mcp_feedback_enhanced_desktop/__init__.py b/src-tauri/python/mcp_feedback_enhanced_desktop/__init__.py new file mode 100644 index 0000000..01a63df --- /dev/null +++ b/src-tauri/python/mcp_feedback_enhanced_desktop/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +""" +MCP Feedback Enhanced Desktop Application +========================================= + +基於 Tauri 的桌面應用程式包裝器,為 MCP Feedback Enhanced 提供原生桌面體驗。 + +主要功能: +- 原生桌面應用程式界面 +- 整合現有的 Web UI 功能 +- 跨平台支援(Windows、macOS、Linux) +- 無需瀏覽器的獨立運行環境 + +作者: Minidoracat +版本: 2.4.3 +""" + +__version__ = "2.4.3" +__author__ = "Minidoracat" +__email__ = "minidora0702@gmail.com" + +from .desktop_app import DesktopApp, launch_desktop_app + + +__all__ = [ + "DesktopApp", + "__author__", + "__version__", + "launch_desktop_app", +] diff --git a/src-tauri/python/mcp_feedback_enhanced_desktop/desktop_app.py b/src-tauri/python/mcp_feedback_enhanced_desktop/desktop_app.py new file mode 100644 index 0000000..4f67472 --- /dev/null +++ b/src-tauri/python/mcp_feedback_enhanced_desktop/desktop_app.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +""" +桌面應用程式主要模組 + +此模組提供桌面應用程式的核心功能,包括: +- 桌面模式檢測 +- Tauri 應用程式啟動 +- 與現有 Web UI 的整合 +""" + +import asyncio +import os +import sys +import time + + +# 導入現有的 MCP Feedback Enhanced 模組 +try: + from mcp_feedback_enhanced.debug import server_debug_log as debug_log + from mcp_feedback_enhanced.web.main import WebUIManager, get_web_ui_manager +except ImportError as e: + print(f"無法導入 MCP Feedback Enhanced 模組: {e}") + sys.exit(1) + + +class DesktopApp: + """桌面應用程式管理器""" + + def __init__(self): + self.web_manager: WebUIManager | None = None + self.desktop_mode = False + self.app_handle = None + + def set_desktop_mode(self, enabled: bool = True): + """設置桌面模式""" + self.desktop_mode = enabled + if enabled: + # 設置環境變數,防止開啟瀏覽器 + os.environ["MCP_DESKTOP_MODE"] = "true" + debug_log("桌面模式已啟用,將禁止開啟瀏覽器") + else: + os.environ.pop("MCP_DESKTOP_MODE", None) + debug_log("桌面模式已禁用") + + def is_desktop_mode(self) -> bool: + """檢查是否為桌面模式""" + return ( + self.desktop_mode + or os.environ.get("MCP_DESKTOP_MODE", "").lower() == "true" + ) + + async def start_web_backend(self) -> str: + """啟動 Web 後端服務""" + debug_log("啟動 Web 後端服務...") + + # 獲取 Web UI 管理器 + self.web_manager = get_web_ui_manager() + + # 設置桌面模式,禁止自動開啟瀏覽器 + self.set_desktop_mode(True) + + # 啟動服務器 + if ( + self.web_manager.server_thread is None + or not self.web_manager.server_thread.is_alive() + ): + self.web_manager.start_server() + + # 等待服務器啟動 + max_wait = 10 # 最多等待 10 秒 + wait_count = 0 + while wait_count < max_wait: + if ( + self.web_manager.server_thread + and self.web_manager.server_thread.is_alive() + ): + break + await asyncio.sleep(0.5) + wait_count += 0.5 + + if not ( + self.web_manager.server_thread and self.web_manager.server_thread.is_alive() + ): + raise RuntimeError("Web 服務器啟動失敗") + + server_url = self.web_manager.get_server_url() + debug_log(f"Web 後端服務已啟動: {server_url}") + return server_url + + def create_test_session(self): + """創建測試會話""" + if not self.web_manager: + raise RuntimeError("Web 管理器未初始化") + + import tempfile + + with tempfile.TemporaryDirectory() as temp_dir: + session_id = self.web_manager.create_session( + temp_dir, "桌面應用程式測試 - 驗證 Tauri 整合功能" + ) + debug_log(f"測試會話已創建: {session_id}") + return session_id + + async def launch_tauri_app(self, server_url: str): + """啟動 Tauri 桌面應用程式""" + debug_log("正在啟動 Tauri 桌面視窗...") + + import os + import subprocess + from pathlib import Path + + # 找到 Tauri 可執行檔案 + # 首先嘗試從打包後的位置找(PyPI 安裝後的位置) + try: + from mcp_feedback_enhanced.desktop_release import __file__ as desktop_init + + desktop_dir = Path(desktop_init).parent + + # 根據平台選擇對應的二進制文件 + import platform + + system = platform.system().lower() + machine = platform.machine().lower() + + # 定義平台到二進制文件的映射 + if system == "windows": + tauri_exe = desktop_dir / "mcp-feedback-enhanced-desktop.exe" + elif system == "darwin": # macOS + # 檢測 Apple Silicon 或 Intel + if machine in ["arm64", "aarch64"]: + tauri_exe = ( + desktop_dir / "mcp-feedback-enhanced-desktop-macos-arm64" + ) + else: + tauri_exe = ( + desktop_dir / "mcp-feedback-enhanced-desktop-macos-intel" + ) + elif system == "linux": + tauri_exe = desktop_dir / "mcp-feedback-enhanced-desktop-linux" + else: + # 回退到通用名稱 + tauri_exe = desktop_dir / "mcp-feedback-enhanced-desktop" + + if tauri_exe.exists(): + debug_log(f"找到打包後的 Tauri 可執行檔案: {tauri_exe}") + else: + # 嘗試回退選項 + fallback_files = [ + desktop_dir / "mcp-feedback-enhanced-desktop.exe", + desktop_dir / "mcp-feedback-enhanced-desktop-macos-intel", + desktop_dir / "mcp-feedback-enhanced-desktop-macos-arm64", + desktop_dir / "mcp-feedback-enhanced-desktop-linux", + desktop_dir / "mcp-feedback-enhanced-desktop", + ] + + for fallback in fallback_files: + if fallback.exists(): + tauri_exe = fallback + debug_log(f"使用回退的可執行檔案: {tauri_exe}") + break + else: + raise FileNotFoundError( + f"找不到任何可執行檔案,檢查的路徑: {tauri_exe}" + ) + + except (ImportError, FileNotFoundError): + # 回退到開發環境路徑 + debug_log("未找到打包後的可執行檔案,嘗試開發環境路徑...") + project_root = Path(__file__).parent.parent.parent.parent + tauri_exe = ( + project_root + / "src-tauri" + / "target" + / "debug" + / "mcp-feedback-enhanced-desktop.exe" + ) + + if not tauri_exe.exists(): + # 嘗試其他可能的路徑 + tauri_exe = ( + project_root + / "src-tauri" + / "target" + / "debug" + / "mcp-feedback-enhanced-desktop" + ) + + if not tauri_exe.exists(): + # 嘗試 release 版本 + tauri_exe = ( + project_root + / "src-tauri" + / "target" + / "release" + / "mcp-feedback-enhanced-desktop.exe" + ) + if not tauri_exe.exists(): + tauri_exe = ( + project_root + / "src-tauri" + / "target" + / "release" + / "mcp-feedback-enhanced-desktop" + ) + + if not tauri_exe.exists(): + raise FileNotFoundError( + "找不到 Tauri 可執行檔案,已嘗試的路徑包括開發和發布目錄" + ) from None + + debug_log(f"找到 Tauri 可執行檔案: {tauri_exe}") + + # 設置環境變數 + env = os.environ.copy() + env["MCP_DESKTOP_MODE"] = "true" + env["MCP_WEB_URL"] = server_url + + # 啟動 Tauri 應用程式 + try: + # Windows 下隱藏控制台視窗 + creation_flags = 0 + if os.name == "nt": + creation_flags = subprocess.CREATE_NO_WINDOW + + self.app_handle = subprocess.Popen( + [str(tauri_exe)], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + creationflags=creation_flags, + ) + debug_log("Tauri 桌面應用程式已啟動") + + # 等待一下確保應用程式啟動 + await asyncio.sleep(2) + + except Exception as e: + debug_log(f"啟動 Tauri 應用程式失敗: {e}") + raise + + def stop(self): + """停止桌面應用程式""" + debug_log("正在停止桌面應用程式...") + + # 停止 Tauri 應用程式 + if self.app_handle: + try: + self.app_handle.terminate() + self.app_handle.wait(timeout=5) + debug_log("Tauri 應用程式已停止") + except Exception as e: + debug_log(f"停止 Tauri 應用程式時發生錯誤: {e}") + try: + self.app_handle.kill() + except: + pass + finally: + self.app_handle = None + + if self.web_manager: + # 注意:不停止 Web 服務器,保持持久性 + debug_log("Web 服務器保持運行狀態") + + # 注意:不清除桌面模式設置,保持 MCP_DESKTOP_MODE 環境變數 + # 這樣下次 MCP 調用時仍然會啟動桌面應用程式 + # self.set_desktop_mode(False) # 註釋掉這行 + debug_log("桌面應用程式已停止") + + +async def launch_desktop_app(test_mode: bool = False) -> DesktopApp: + """啟動桌面應用程式 + + Args: + test_mode: 是否為測試模式,測試模式下會創建測試會話 + """ + debug_log("正在啟動桌面應用程式...") + + app = DesktopApp() + + try: + # 啟動 Web 後端 + server_url = await app.start_web_backend() + + if test_mode: + # 測試模式:創建測試會話 + debug_log("測試模式:創建測試會話") + app.create_test_session() + else: + # MCP 調用模式:使用現有會話 + debug_log("MCP 調用模式:使用現有 MCP 會話,不創建新的測試會話") + + # 啟動 Tauri 桌面應用程式 + await app.launch_tauri_app(server_url) + + debug_log(f"桌面應用程式已啟動,後端服務: {server_url}") + return app + + except Exception as e: + debug_log(f"桌面應用程式啟動失敗: {e}") + app.stop() + raise + + +def run_desktop_app(): + """同步方式運行桌面應用程式""" + try: + # 設置事件循環策略(Windows) + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + # 運行應用程式 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + app = loop.run_until_complete(launch_desktop_app()) + + # 保持應用程式運行 + debug_log("桌面應用程式正在運行,按 Ctrl+C 停止...") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + debug_log("收到停止信號...") + finally: + app.stop() + loop.close() + + except Exception as e: + print(f"桌面應用程式運行失敗: {e}") + sys.exit(1) + + +if __name__ == "__main__": + run_desktop_app() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..2b55b9f --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,132 @@ +use pyo3::prelude::*; +use tauri::{Builder, Context, Manager}; +use std::sync::Mutex; + +// 全局狀態管理 +static APP_STATE: Mutex> = Mutex::new(None); + +/// Tauri 應用程式狀態 +#[derive(Default)] +struct AppState { + web_url: String, + desktop_mode: bool, +} + +/// 生成 Tauri 上下文 +pub fn tauri_generate_context() -> Context { + tauri::generate_context!() +} + +/// 創建 Tauri 應用程式構建器 +pub fn create_tauri_builder() -> Builder { + Builder::default() + .plugin(tauri_plugin_shell::init()) + .manage(AppState::default()) + .setup(|app| { + // 儲存應用程式句柄到全局狀態 + { + let mut state = APP_STATE.lock().unwrap(); + *state = Some(app.handle().clone()); + } + + // 設置應用程式狀態 + let _app_state = app.state::(); + { + // 這裡可以設置初始狀態 + } + + println!("Tauri 應用程式已初始化"); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + get_web_url, + set_web_url, + is_desktop_mode, + set_desktop_mode + ]) +} + +/// 獲取 Web URL +#[tauri::command] +fn get_web_url(state: tauri::State) -> String { + state.web_url.clone() +} + +/// 設置 Web URL +#[tauri::command] +fn set_web_url(url: String, _state: tauri::State) { + // 注意:這裡需要使用內部可變性,但 tauri::State 不支援 + // 實際實現中可能需要使用 Mutex 或其他同步原語 + println!("設置 Web URL: {}", url); +} + +/// 檢查是否為桌面模式 +#[tauri::command] +fn is_desktop_mode(state: tauri::State) -> bool { + state.desktop_mode +} + +/// 設置桌面模式 +#[tauri::command] +fn set_desktop_mode(enabled: bool, _state: tauri::State) { + println!("設置桌面模式: {}", enabled); +} + +/// PyO3 模組定義 +#[pymodule] +#[pyo3(name = "ext_mod")] +pub mod ext_mod { + use super::*; + + #[pymodule_init] + fn init(module: &Bound<'_, PyModule>) -> PyResult<()> { + // 註冊 context_factory 函數 + module.add_function(wrap_pyfunction!(context_factory, module)?)?; + + // 註冊 builder_factory 函數 + module.add_function(wrap_pyfunction!(builder_factory, module)?)?; + + // 註冊 run_app 函數 + module.add_function(wrap_pyfunction!(run_app, module)?)?; + + Ok(()) + } + + /// 創建 Tauri 上下文的工廠函數 + #[pyfunction] + fn context_factory() -> PyResult { + // 返回序列化的上下文信息 + // 實際實現中,這裡應該返回可以被 Python 使用的上下文 + Ok("tauri_context".to_string()) + } + + /// 創建 Tauri 構建器的工廠函數 + #[pyfunction] + fn builder_factory() -> PyResult { + // 返回序列化的構建器信息 + // 實際實現中,這裡應該返回可以被 Python 使用的構建器 + Ok("tauri_builder".to_string()) + } + + /// 運行 Tauri 應用程式 + #[pyfunction] + fn run_app(web_url: String) -> PyResult { + println!("正在啟動 Tauri 應用程式,Web URL: {}", web_url); + + // 創建並運行 Tauri 應用程式 + let _builder = create_tauri_builder(); + let _context = tauri_generate_context(); + + // 在實際實現中,這裡需要處理異步運行 + // 目前返回成功狀態 + match std::thread::spawn(move || { + // 這裡應該運行 Tauri 應用程式 + // builder.run(context) + println!("Tauri 應用程式線程已啟動"); + 0 + }).join() { + Ok(code) => Ok(code), + Err(_) => Ok(1), + } + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..6a85df1 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,79 @@ +// Prevents additional console window on Windows in both debug and release, DO NOT REMOVE!! +#![cfg_attr(target_os = "windows", windows_subsystem = "windows")] + +use tauri::{Builder, Manager}; +use std::sync::Mutex; + +// 全局狀態管理 +static APP_STATE: Mutex> = Mutex::new(None); + +/// Tauri 應用程式狀態 +#[derive(Default)] +struct AppState { + web_url: String, + desktop_mode: bool, +} + +/// 獲取 Web URL +#[tauri::command] +fn get_web_url(state: tauri::State) -> String { + state.web_url.clone() +} + +/// 設置 Web URL +#[tauri::command] +fn set_web_url(url: String, _state: tauri::State) { + println!("設置 Web URL: {}", url); +} + +/// 檢查是否為桌面模式 +#[tauri::command] +fn is_desktop_mode(state: tauri::State) -> bool { + state.desktop_mode +} + +/// 設置桌面模式 +#[tauri::command] +fn set_desktop_mode(enabled: bool, _state: tauri::State) { + println!("設置桌面模式: {}", enabled); +} + +fn main() { + // 初始化日誌 + env_logger::init(); + + println!("正在啟動 MCP Feedback Enhanced 桌面應用程式..."); + + // 創建 Tauri 應用程式 + Builder::default() + .plugin(tauri_plugin_shell::init()) + .manage(AppState::default()) + .setup(|app| { + // 儲存應用程式句柄到全局狀態 + { + let mut state = APP_STATE.lock().unwrap(); + *state = Some(app.handle().clone()); + } + + // 檢查是否有 MCP_WEB_URL 環境變數 + if let Ok(web_url) = std::env::var("MCP_WEB_URL") { + println!("檢測到 Web URL: {}", web_url); + + // 獲取主視窗並導航到 Web URL + if let Some(window) = app.get_webview_window("main") { + let _ = window.navigate(web_url.parse().unwrap()); + } + } + + println!("Tauri 應用程式已初始化"); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + get_web_url, + set_web_url, + is_desktop_mode, + set_desktop_mode + ]) + .run(tauri::generate_context!()) + .expect("運行 Tauri 應用程式時發生錯誤"); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..6fbd9d4 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,70 @@ +{ + "productName": "MCP Feedback Enhanced", + "version": "2.4.3", + "identifier": "com.minidoracat.mcp-feedback-enhanced", + "build": { + "frontendDist": "../src/mcp_feedback_enhanced/web/static", + "devUrl": "http://127.0.0.1:8765", + "beforeDevCommand": "", + "beforeBuildCommand": "" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "MCP Feedback Enhanced", + "width": 1200, + "height": 800, + "minWidth": 800, + "minHeight": 600, + "resizable": true, + "fullscreen": false, + "decorations": true, + "alwaysOnTop": false, + "skipTaskbar": false, + "center": true, + "url": "index.html" + } + ], + "security": { + "csp": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss: http: https:; font-src 'self' data:;" + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "resources": [], + "externalBin": [], + "copyright": "Copyright © 2024 Minidoracat", + "category": "DeveloperTool", + "shortDescription": "Enhanced MCP server for interactive user feedback", + "longDescription": "An enhanced MCP server that provides interactive user feedback functionality for AI-assisted development, featuring Web UI with intelligent environment detection.", + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + }, + "macOS": { + "frameworks": [], + "minimumSystemVersion": "10.13", + "exceptionDomain": "" + }, + "linux": { + "deb": { + "depends": [] + } + } + }, + "plugins": { + "shell": { + "open": true + } + } +} diff --git a/src/mcp_feedback_enhanced/__main__.py b/src/mcp_feedback_enhanced/__main__.py index 27f33be..4a2cff4 100644 --- a/src/mcp_feedback_enhanced/__main__.py +++ b/src/mcp_feedback_enhanced/__main__.py @@ -47,6 +47,9 @@ def main(): test_parser.add_argument( "--web", action="store_true", help="測試 Web UI (自動持續運行)" ) + test_parser.add_argument( + "--desktop", action="store_true", help="啟動桌面應用程式模式" + ) test_parser.add_argument( "--timeout", type=int, default=60, help="測試超時時間 (秒)" ) @@ -100,10 +103,16 @@ 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) else: print("❌ 測試功能已簡化") print("💡 可用的測試選項:") print(" --web 測試 Web UI") + print(" --desktop 啟動桌面應用程式") print("💡 對於開發者:使用 'uv run pytest' 執行完整測試") sys.exit(1) @@ -185,6 +194,117 @@ def test_web_ui_simple(): os.environ.pop("MCP_WEB_PORT", None) +def test_desktop_app(): + """測試桌面應用程式""" + try: + print("🔧 檢查桌面應用程式依賴...") + + # 檢查是否有 Tauri 桌面模組 + try: + import os + import sys + + # 嘗試導入桌面應用程式模組 + def import_desktop_app(): + # 首先嘗試從發佈包位置導入 + try: + from .desktop_app import launch_desktop_app as desktop_func + + print("✅ 找到發佈包中的桌面應用程式模組") + return desktop_func + except ImportError: + pass + + # 回退到開發環境路徑 + tauri_python_path = os.path.join( + os.path.dirname(__file__), "..", "..", "src-tauri", "python" + ) + if os.path.exists(tauri_python_path): + sys.path.insert(0, tauri_python_path) + print(f"✅ 找到 Tauri Python 模組路徑: {tauri_python_path}") + try: + from mcp_feedback_enhanced_desktop import ( # type: ignore + launch_desktop_app as dev_func, + ) + + return dev_func + except ImportError: + print("❌ 無法從開發環境路徑導入桌面應用程式模組") + return None + else: + print(f"⚠️ Tauri Python 模組路徑不存在: {tauri_python_path}") + print("💡 請確保已正確建立 PyTauri 專案結構") + return None + + launch_desktop_app_func = import_desktop_app() + if launch_desktop_app_func is None: + return False + + print("✅ 桌面應用程式模組導入成功") + + except ImportError as e: + print(f"❌ 無法導入桌面應用程式模組: {e}") + print( + "💡 請確保已執行 'make build-desktop' 或 'python scripts/build_desktop.py'" + ) + return False + + print("🚀 啟動桌面應用程式...") + + # 設置桌面模式環境變數 + os.environ["MCP_DESKTOP_MODE"] = "true" + + # 使用 asyncio 啟動桌面應用程式 + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + # 使用 WebUIManager 來管理桌面應用實例 + from .web.main import get_web_ui_manager + + manager = get_web_ui_manager() + + # 啟動桌面應用並保存實例到 manager + app = loop.run_until_complete(launch_desktop_app_func(test_mode=True)) + manager.desktop_app_instance = app + + print("✅ 桌面應用程式啟動成功") + print("💡 桌面應用程式正在運行,按 Ctrl+C 停止...") + + # 保持應用程式運行 + try: + while True: + import time + + time.sleep(1) + except KeyboardInterrupt: + print("\n🛑 停止桌面應用程式...") + app.stop() + return True + + except Exception as e: + print(f"❌ 桌面應用程式啟動失敗: {e}") + import traceback + + traceback.print_exc() + return False + finally: + loop.close() + + except Exception as e: + print(f"❌ 桌面應用程式測試失敗: {e}") + import traceback + + traceback.print_exc() + return False + finally: + # 清理環境變數 + os.environ.pop("MCP_DESKTOP_MODE", None) + + async def wait_for_process(process): """等待進程結束""" try: diff --git a/src/mcp_feedback_enhanced/desktop_release/__init__.py b/src/mcp_feedback_enhanced/desktop_release/__init__.py new file mode 100644 index 0000000..ee2044b --- /dev/null +++ b/src/mcp_feedback_enhanced/desktop_release/__init__.py @@ -0,0 +1 @@ +"桌面應用程式二進制檔案" diff --git a/src/mcp_feedback_enhanced/server.py b/src/mcp_feedback_enhanced/server.py index a989cb6..dd8af1d 100644 --- a/src/mcp_feedback_enhanced/server.py +++ b/src/mcp_feedback_enhanced/server.py @@ -603,6 +603,14 @@ def main(): # 檢查是否啟用調試模式 debug_enabled = os.getenv("MCP_DEBUG", "").lower() in ("true", "1", "yes", "on") + # 檢查是否啟用桌面模式 + desktop_mode = os.getenv("MCP_DESKTOP_MODE", "").lower() in ( + "true", + "1", + "yes", + "on", + ) + if debug_enabled: debug_log("🚀 啟動互動式回饋收集 MCP 服務器") debug_log(f" 服務器名稱: {SERVER_NAME}") @@ -611,6 +619,7 @@ def main(): debug_log(f" 編碼初始化: {'成功' if _encoding_initialized else '失敗'}") debug_log(f" 遠端環境: {is_remote_environment()}") debug_log(f" WSL 環境: {is_wsl_environment()}") + debug_log(f" 桌面模式: {'啟用' if desktop_mode else '禁用'}") debug_log(" 介面類型: Web UI") debug_log(" 等待來自 AI 助手的調用...") debug_log("準備啟動 MCP 伺服器...") diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index f41ac0f..43ad4a0 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -115,6 +115,7 @@ class WebUIManager: self.server_thread: threading.Thread | None = None self.server_process = None + self.desktop_app_instance: Any = None # 桌面應用實例引用 # 初始化標記,用於追蹤異步初始化狀態 self._initialization_complete = False @@ -563,10 +564,15 @@ class WebUIManager: """智能開啟瀏覽器 - 檢測是否已有活躍標籤頁 Returns: - bool: True 表示檢測到活躍標籤頁,False 表示開啟了新視窗 + bool: True 表示檢測到活躍標籤頁或桌面模式,False 表示開啟了新視窗 """ try: + # 檢查是否為桌面模式 + if os.environ.get("MCP_DESKTOP_MODE", "").lower() == "true": + debug_log("檢測到桌面模式,跳過瀏覽器開啟") + return True + # 檢查是否有活躍標籤頁 has_active_tabs = await self._check_active_tabs() @@ -585,6 +591,83 @@ class WebUIManager: self.open_browser(url) return False + async def launch_desktop_app(self, url: str) -> bool: + """ + 啟動桌面應用程式 + + Args: + url: Web 服務 URL + + Returns: + bool: True 表示成功啟動桌面應用程式 + """ + try: + # 嘗試導入桌面應用程式模組 + def import_desktop_app(): + # 首先嘗試從發佈包位置導入 + try: + from mcp_feedback_enhanced.desktop_app import ( + launch_desktop_app as desktop_func, + ) + + debug_log("使用發佈包中的桌面應用程式模組") + return desktop_func + except ImportError: + pass + + # 回退到開發環境路徑 + import sys + + project_root = os.path.dirname( + os.path.dirname(os.path.dirname(__file__)) + ) + desktop_module_path = os.path.join(project_root, "src-tauri", "python") + if desktop_module_path not in sys.path: + sys.path.insert(0, desktop_module_path) + try: + from mcp_feedback_enhanced_desktop import ( # type: ignore + launch_desktop_app as dev_func, + ) + + debug_log("使用開發環境桌面應用程式模組") + return dev_func + except ImportError: + debug_log("無法從開發環境路徑導入桌面應用程式模組") + raise + + launch_desktop_app_func = import_desktop_app() + + # 啟動桌面應用程式 + desktop_app = await launch_desktop_app_func() + # 保存桌面應用實例引用,以便後續控制 + self.desktop_app_instance = desktop_app + debug_log("桌面應用程式啟動成功") + return True + + except ImportError as e: + debug_log(f"無法導入桌面應用程式模組: {e}") + debug_log("回退到瀏覽器模式...") + self.open_browser(url) + return False + except Exception as e: + debug_log(f"桌面應用程式啟動失敗: {e}") + debug_log("回退到瀏覽器模式...") + self.open_browser(url) + return False + + def close_desktop_app(self): + """關閉桌面應用程式""" + if self.desktop_app_instance: + try: + debug_log("正在關閉桌面應用程式...") + self.desktop_app_instance.stop() + self.desktop_app_instance = None + debug_log("桌面應用程式已關閉") + except Exception as e: + debug_log(f"關閉桌面應用程式失敗: {e}") + else: + debug_log("沒有活躍的桌面應用程式實例") + async def notify_session_update(self, session): """向活躍標籤頁發送會話更新通知""" try: @@ -1012,9 +1095,19 @@ async def launch_web_feedback_ui( if manager.server_thread is None or not manager.server_thread.is_alive(): manager.start_server() - # 使用根路徑 URL 並智能開啟瀏覽器 + # 檢查是否為桌面模式 + desktop_mode = os.environ.get("MCP_DESKTOP_MODE", "").lower() == "true" + + # 使用根路徑 URL feedback_url = manager.get_server_url() # 直接使用根路徑 - has_active_tabs = await manager.smart_open_browser(feedback_url) + + if desktop_mode: + # 桌面模式:啟動桌面應用程式 + debug_log("檢測到桌面模式,啟動桌面應用程式...") + has_active_tabs = await manager.launch_desktop_app(feedback_url) + else: + # Web 模式:智能開啟瀏覽器 + has_active_tabs = await manager.smart_open_browser(feedback_url) debug_log(f"[DEBUG] 服務器地址: {feedback_url}") diff --git a/src/mcp_feedback_enhanced/web/models/feedback_session.py b/src/mcp_feedback_enhanced/web/models/feedback_session.py index f70aa8e..7aa915b 100644 --- a/src/mcp_feedback_enhanced/web/models/feedback_session.py +++ b/src/mcp_feedback_enhanced/web/models/feedback_session.py @@ -421,6 +421,23 @@ class WebFeedbackSession: "status": self.status.value, } ) + + # 檢查是否為桌面模式,如果是則立即關閉桌面應用程式 + import os + + if os.environ.get("MCP_DESKTOP_MODE", "").lower() == "true": + debug_log("桌面模式:反饋提交後立即關閉桌面應用程式") + + # 立即關閉桌面應用程式,無延遲 + try: + from ..main import get_web_ui_manager + + manager = get_web_ui_manager() + manager.close_desktop_app() + debug_log("桌面應用程式立即關閉成功") + except Exception as close_error: + debug_log(f"立即關閉桌面應用程式失敗: {close_error}") + except Exception as e: debug_log(f"發送反饋確認失敗: {e}") diff --git a/src/mcp_feedback_enhanced/web/static/favicon.ico b/src/mcp_feedback_enhanced/web/static/favicon.ico new file mode 100644 index 0000000..9a0fc00 --- /dev/null +++ b/src/mcp_feedback_enhanced/web/static/favicon.ico @@ -0,0 +1,2 @@ +# 這是一個佔位符文件,實際的 favicon 需要是二進制格式 +# 在實際部署中,應該使用真正的 .ico 文件 diff --git a/src/mcp_feedback_enhanced/web/static/index.html b/src/mcp_feedback_enhanced/web/static/index.html new file mode 100644 index 0000000..29c6f5a --- /dev/null +++ b/src/mcp_feedback_enhanced/web/static/index.html @@ -0,0 +1,158 @@ + + + + + + MCP Feedback Enhanced - 桌面版 + + + +
+ +
桌面版正在啟動...
+
+
正在連接到後端服務...
+
+
+ + + + diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index caadb01..d5f56e5 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -561,6 +561,10 @@ console.log('🔄 收到會話更新訊息:', data.session_info); this.handleSessionUpdated(data); break; + case 'desktop_close_request': + console.log('🖥️ 收到桌面關閉請求'); + this.handleDesktopCloseRequest(data); + break; } }; @@ -596,6 +600,34 @@ console.log('反饋已提交,頁面保持開啟狀態'); }; + /** + * 處理桌面關閉請求 + */ + FeedbackApp.prototype.handleDesktopCloseRequest = function(data) { + console.log('🖥️ 處理桌面關閉請求:', data.message); + + // 顯示關閉訊息 + const closeMessage = data.message || '正在關閉桌面應用程式...'; + window.MCPFeedback.Utils.showMessage(closeMessage, window.MCPFeedback.Utils.CONSTANTS.MESSAGE_INFO); + + // 檢查是否在 Tauri 環境中 + if (window.__TAURI__) { + console.log('🖥️ 檢測到 Tauri 環境,關閉桌面視窗'); + try { + // 使用 Tauri API 關閉視窗 + window.__TAURI__.window.getCurrent().close(); + } catch (error) { + console.error('關閉 Tauri 視窗失敗:', error); + // 備用方案:關閉瀏覽器視窗 + window.close(); + } + } else { + console.log('🖥️ 非 Tauri 環境,嘗試關閉瀏覽器視窗'); + // 在瀏覽器環境中嘗試關閉視窗 + window.close(); + } + }; + /** * 處理會話更新 */ diff --git a/src/mcp_feedback_enhanced/web/utils/browser.py b/src/mcp_feedback_enhanced/web/utils/browser.py index eb9b36f..66846ed 100644 --- a/src/mcp_feedback_enhanced/web/utils/browser.py +++ b/src/mcp_feedback_enhanced/web/utils/browser.py @@ -48,6 +48,18 @@ def is_wsl_environment() -> bool: return False +def is_desktop_mode() -> bool: + """ + 檢測是否為桌面模式 + + 當設置了 MCP_DESKTOP_MODE 環境變數時,禁止開啟瀏覽器 + + Returns: + bool: True 表示桌面模式,False 表示 Web 模式 + """ + return os.environ.get("MCP_DESKTOP_MODE", "").lower() == "true" + + def open_browser_in_wsl(url: str) -> None: """ 在 WSL 環境中開啟 Windows 瀏覽器 @@ -117,6 +129,11 @@ def smart_browser_open(url: str) -> None: Args: url: 要開啟的 URL """ + # 檢查是否為桌面模式 + if is_desktop_mode(): + debug_log("檢測到桌面模式,跳過瀏覽器開啟") + return + if is_wsl_environment(): debug_log("檢測到 WSL 環境,使用 WSL 專用瀏覽器啟動方式") open_browser_in_wsl(url)