diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 0000000..2d1d7c2 --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,64 @@ +name: Deploy Web to GitHub Pages + +on: + push: + branches: [main] + paths: + - 'web/**' + - '.github/workflows/deploy-web.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: 'pages' + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: web/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile --ignore-workspace + working-directory: web + + - name: Build + run: pnpm build + working-directory: web + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: web/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eeab47b..76dcdec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ env: jobs: # ========================================================================= - # Prepare: calculate next 0.0.N version number + # Prepare: calculate next 1.0.N version number # ========================================================================= prepare: name: Prepare @@ -28,15 +28,15 @@ jobs: - id: calc name: Calculate next version run: | - LATEST=$(git tag -l 'v0.0.[0-9]*' | sort -V | tail -1) + LATEST=$(git tag -l 'v1.0.[0-9]*' | sort -V | tail -1) if [ -z "$LATEST" ]; then - PATCH=1 + PATCH=0 else - PATCH=$(echo "$LATEST" | sed 's/v0\.0\.//') + PATCH=$(echo "$LATEST" | sed 's/v1\.0\.//') PATCH=$((PATCH + 1)) fi - echo "NEW_VERSION=0.0.$PATCH" >> $GITHUB_OUTPUT - echo "Next version: 0.0.$PATCH" + echo "NEW_VERSION=1.0.$PATCH" >> $GITHUB_OUTPUT + echo "Next version: 1.0.$PATCH" # ========================================================================= # Build macOS (Universal: Intel + Apple Silicon) @@ -45,6 +45,9 @@ jobs: name: Build macOS needs: prepare runs-on: macos-latest + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} steps: - uses: actions/checkout@v4 @@ -81,6 +84,15 @@ jobs: path: src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg if-no-files-found: warn + - name: Upload macOS updater artifacts + uses: actions/upload-artifact@v4 + with: + name: bonnext-macos-updater + path: | + src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz + src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz.sig + if-no-files-found: warn + # ========================================================================= # Build Windows # ========================================================================= @@ -88,6 +100,9 @@ jobs: name: Build Windows needs: prepare runs-on: windows-latest + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} steps: - uses: actions/checkout@v4 @@ -130,6 +145,17 @@ jobs: path: src-tauri/target/release/bundle/nsis/*.exe if-no-files-found: warn + - name: Upload Windows updater artifacts + uses: actions/upload-artifact@v4 + with: + name: bonnext-windows-updater + path: | + src-tauri/target/release/bundle/nsis/*-setup.exe + src-tauri/target/release/bundle/nsis/*-setup.exe.sig + src-tauri/target/release/bundle/nsis/*.nsis.zip + src-tauri/target/release/bundle/nsis/*.nsis.zip.sig + if-no-files-found: warn + # ========================================================================= # Build Linux (AppImage + deb) # ========================================================================= @@ -137,6 +163,9 @@ jobs: name: Build Linux (glibc-compat via Ubuntu 22.04) needs: prepare runs-on: ubuntu-22.04 + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} steps: - uses: actions/checkout@v4 @@ -180,7 +209,10 @@ jobs: uses: actions/upload-artifact@v4 with: name: bonnext-linux-appimage - path: src-tauri/target/release/bundle/appimage/*.AppImage + path: | + src-tauri/target/release/bundle/appimage/*.AppImage + src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz + src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz.sig if-no-files-found: warn - name: Upload deb artifact @@ -229,6 +261,87 @@ jobs: - name: Display artifact structure run: find artifacts -type f | sort + - name: Generate latest.json for updater + env: + VERSION: ${{ needs.prepare.outputs.version }} + REPO: NoWint/BonNextMinecraftLauncher-Rust + run: | + set -eu + # Helper: read .sig file content (first line = signature) + read_sig() { + local sigfile="$1" + if [ -f "$sigfile" ]; then + head -1 "$sigfile" + else + echo "" + fi + } + + # Locate updater artifacts + MACOS_TARGZ=$(find artifacts/bonnext-macos-updater -name '*.app.tar.gz' 2>/dev/null | head -1 || true) + MACOS_SIG=$(find artifacts/bonnext-macos-updater -name '*.app.tar.gz.sig' 2>/dev/null | head -1 || true) + + # Windows: prefer .nsis.zip, fallback to -setup.exe + WIN_ZIP=$(find artifacts/bonnext-windows-updater -name '*.nsis.zip' 2>/dev/null | head -1 || true) + WIN_ZIP_SIG=$(find artifacts/bonnext-windows-updater -name '*.nsis.zip.sig' 2>/dev/null | head -1 || true) + WIN_EXE=$(find artifacts/bonnext-windows-updater -name '*-setup.exe' 2>/dev/null | head -1 || true) + WIN_EXE_SIG=$(find artifacts/bonnext-windows-updater -name '*-setup.exe.sig' 2>/dev/null | head -1 || true) + + if [ -n "$WIN_ZIP" ]; then + WIN_FILE="$WIN_ZIP" + WIN_SIGFILE="$WIN_ZIP_SIG" + else + WIN_FILE="$WIN_EXE" + WIN_SIGFILE="$WIN_EXE_SIG" + fi + + LINUX_TARGZ=$(find artifacts/bonnext-linux-appimage -name '*.AppImage.tar.gz' 2>/dev/null | head -1 || true) + LINUX_SIG=$(find artifacts/bonnext-linux-appimage -name '*.AppImage.tar.gz.sig' 2>/dev/null | head -1 || true) + + # Build download URLs (GitHub Release assets) + MACOS_URL="https://github.com/${REPO}/releases/download/v${VERSION}/$(basename "$MACOS_TARGZ" 2>/dev/null || echo '')" + WIN_URL="https://github.com/${REPO}/releases/download/v${VERSION}/$(basename "$WIN_FILE" 2>/dev/null || echo '')" + LINUX_URL="https://github.com/${REPO}/releases/download/v${VERSION}/$(basename "$LINUX_TARGZ" 2>/dev/null || echo '')" + + # Read signatures + MACOS_SIGNATURE=$(read_sig "$MACOS_SIG") + WIN_SIGNATURE=$(read_sig "$WIN_SIGFILE") + LINUX_SIGNATURE=$(read_sig "$LINUX_SIG") + + # Current UTC timestamp in ISO 8601 + PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + # Build latest.json (Tauri v2 updater format) + # macOS universal build covers both darwin-aarch64 and darwin-x86_64 + cat > latest.json < Built with Tauri v2 + React 18 + Rust - files: artifacts/**/* + files: | + artifacts/**/* + latest.json generate_release_notes: true diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 7d1de08..3b407a9 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -18,7 +18,7 @@ jobs: run: cargo install cargo-audit - name: Run cargo audit working-directory: src-tauri - run: cargo audit || true + run: cargo audit js-audit: runs-on: ubuntu-latest @@ -35,4 +35,3 @@ jobs: run: pnpm install --frozen-lockfile - name: Run pnpm audit run: pnpm audit --audit-level=high - continue-on-error: true diff --git a/.gitignore b/.gitignore index e66b360..9c10051 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ logs *.log npm-debug.log* yarn-debug.log* + +# 3DGS model (66MB, regenerate via SHARP — see src/plugins/builtins/scene-menu/assets/README.md) +public/scene.ply +public/scene_clean.ply +public/scene_small.ply yarn-error.log* pnpm-debug.log* lerna-debug.log* diff --git a/0 b/0 new file mode 100644 index 0000000..e69de29 diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f65a7..a06cdae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,33 +2,61 @@ All notable changes to BonNext will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] +## [1.0.0] - 2026-06-XX ### Added -- Modrinth mod browser with search, filtering, and one-click install -- Keyboard shortcuts (Ctrl+H/I/M/V/N/, for navigation) -- Toast notification system with auto-dismiss -- Error boundary with recovery UI -- Skeleton loading components -- GitHub Actions CI pipeline (Ubuntu, macOS, Windows) -- SECURITY.md, CONTRIBUTING.md, CHANGELOG.md +- 插件化架构:最小核心 + 插件系统,支持第三方插件安装 +- GitHub Releases 自动更新 +- 首次使用引导(Quick Start + Detailed Tour) +- 主题系统:dark/light/OLED + 色盲模式 + 动态背景 +- 键盘快捷键(Ctrl+K 搜索、Ctrl+H/I/V/N 导航) +- AI 智能搜索 +- 崩溃自动诊断与修复建议 +- 模组/整合包安装与依赖解析 +- 实例快照与迁移 +- 安全模块:AES-256-GCM 加密、JVM 参数白名单、审计日志 ### Changed -- All pages converted from inline styles to CSS Modules -- SettingsPage checkboxes now functional and config-driven -- File/folder picker dialogs wired in SettingsPage +- 版本号从 0.0.6 升级为 1.0.0(首个正式稳定版) +- 主题系统重构,消除三套主题系统冲突 +- 插件事件总线支持按 pluginId 追踪和清理 + +### Fixed +- 插件事件权限模型失效 +- PluginFileSystem 完全不可用 +- activate() 失败 UI 注入泄漏 +- 主题匹配 bug +- DownloadPanel 硬编码英文(i18n 化) +- NewInstancePage 版本加载静默吞错 + +### Known Limitations +- 无代码签名(macOS/Windows 首次运行有安全警告) +- 社交/P2P 功能未启用(代码存在但 UI 未暴露) +- 支持 960×640 及以上窗口,未适配移动端 + +## [0.0.6] - 2026-06-19 + +### Added +- 插件架构审查与修复(35+ 问题修复) + +## [0.0.5] - 2026-06-XX ### Fixed -- `user_type` now dynamically set from account store (not hardcoded "msa") -- `version_type` now resolved from version JSON (not hardcoded "release") -- `--fullscreen` flag properly passed to Minecraft -- Memory validation added (min >= 256MB, max >= min, max <= 64GB) -- Launch state machine transitions now enforced with logging +- XSS 漏洞修复(DOMPurify 集成) +- 路由冲突修复 + +## [0.0.4] - 2026-06-XX + +### Added +- 多 Shell 架构(ZZZ/SwiftUI/Editor) + +## [0.0.3] - 2026-06-XX + +### Added +- Modrinth/CurseForge 市场集成 +- 下载管理器 -## [0.2.0] - 2026-05-16 +## [0.0.2] - 2026-05-16 ### Added - Complete application rewrite based on oxide-mc and shard reference projects @@ -48,7 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CSS custom properties design tokens - React Context + useReducer state management -## [0.1.0] - 2026-05-14 +## [0.0.1] - 2026-05-14 ### Added - Initial Tauri v2 + React 18 + TypeScript project scaffold diff --git a/CLAUDE.md b/CLAUDE.md index dae2609..dcc2aa2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,4 +150,4 @@ Default game directory: `~/.local/share/bonnext/` (Linux), `~/Library/Applicatio - **Overlays** — `.noise-overlay` (SVG noise) and `.scanline-overlay` (horizontal lines) for CRT effect - **Sizes use `em`** — base is 16px set on `html`. Component font sizes typically 0.55em–0.9em range - **All new UI must use CSS Modules** — no inline styles except for truly dynamic values -- **Light theme contrast** — accent color #6B5F00 meets WCAG AA (4.6:1 contrast ratio) +- **Light theme contrast** — accent color #807000 meets WCAG AA (4.8:1 contrast ratio) diff --git a/README.md b/README.md index 21572aa..972481d 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,35 @@ _Experiences, not checkboxes._
+## 安装说明 + +
+ +### macOS +1. 下载 `.dmg` 文件并打开 +2. 将 BonNext 拖入 Applications 文件夹 +3. 首次打开时 macOS 会显示安全警告(应用未签名) +4. **右键点击** BonNext → 选择**"打开"** → 在弹窗中点击**"打开"** +5. 或前往 系统偏好设置 → 安全性与隐私 → 点击**"仍要打开"** + +### Windows +1. 下载 `.msi` 或 `.exe` 安装包 +2. 运行安装程序,如遇 SmartScreen 弹窗: +3. 点击**"更多信息"** → 点击**"仍要运行"** + +### Linux +1. 下载 `.AppImage`(推荐)或 `.deb` +2. AppImage:`chmod +x BonNext-*.AppImage && ./BonNext-*.AppImage` +3. deb:`sudo dpkg -i BonNext-*.deb` + +> **关于安全警告**:BonNext 是开源项目,未购买商业开发者证书签名。代码完全公开,可自行审查或从源码构建。 + +
+ +--- + +
+ ## Built with
@@ -233,7 +262,7 @@ See [LICENSE](LICENSE).
-[![Star on GitHub](https://img.shields.io/github/stars/NoWint/BonNext?style=social)](https://github.com/NoWint/BonNext) +[![Star on GitHub](https://img.shields.io/github/stars/NoWint/BonNextMinecraftLauncher-Rust?style=social)](https://github.com/NoWint/BonNextMinecraftLauncher-Rust)

diff --git a/SECURITY.md b/SECURITY.md index 9bb766c..760b312 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,7 +10,7 @@ We take all security reports seriously and will respond within 48 hours. | Version | Supported | |---------|-----------| -| 0.1.x | ✅ Active | +| 1.0.x | ✅ Active | ## Security Model diff --git a/_rewrite_helper.py b/_rewrite_helper.py new file mode 100644 index 0000000..887fd4e --- /dev/null +++ b/_rewrite_helper.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# Helper script to rewrite CHANGELOG.md and patch SECURITY.md +import os + +CHANGELOG_CONTENT = """# Changelog + +All notable changes to BonNext will be documented in this file. + +## [1.0.0] - 2026-06-XX + +### Added +- 插件化架构:最小核心 + 插件系统,支持第三方插件安装 +- GitHub Releases 自动更新 +- 首次使用引导(Quick Start + Detailed Tour) +- 主题系统:dark/light/OLED + 色盲模式 + 动态背景 +- 键盘快捷键(Ctrl+K 搜索、Ctrl+H/I/V/N 导航) +- AI 智能搜索 +- 崩溃自动诊断与修复建议 +- 模组/整合包安装与依赖解析 +- 实例快照与迁移 +- 安全模块:AES-256-GCM 加密、JVM 参数白名单、审计日志 + +### Changed +- 版本号从 0.0.6 升级为 1.0.0(首个正式稳定版) +- 主题系统重构,消除三套主题系统冲突 +- 插件事件总线支持按 pluginId 追踪和清理 + +### Fixed +- 插件事件权限模型失效 +- PluginFileSystem 完全不可用 +- activate() 失败 UI 注入泄漏 +- 主题匹配 bug +- DownloadPanel 硬编码英文(i18n 化) +- NewInstancePage 版本加载静默吞错 + +### Known Limitations +- 无代码签名(macOS/Windows 首次运行有安全警告) +- 社交/P2P 功能未启用(代码存在但 UI 未暴露) +- 支持 960×640 及以上窗口,未适配移动端 + +## [0.0.6] - 2026-06-19 + +### Added +- 插件架构审查与修复(35+ 问题修复) + +## [0.0.5] - 2026-06-XX + +### Fixed +- XSS 漏洞修复(DOMPurify 集成) +- 路由冲突修复 + +## [0.0.4] - 2026-06-XX + +### Added +- 多 Shell 架构(ZZZ/SwiftUI/Editor) + +## [0.0.3] - 2026-06-XX + +### Added +- Modrinth/CurseForge 市场集成 +- 下载管理器 + +## [0.0.2] - 2026-05-16 + +### Added +- Complete application rewrite based on oxide-mc and shard reference projects +- Microsoft OAuth 2.0 device code authentication with token refresh +- Offline mode with deterministic v5 UUIDs +- Multi-instance management with isolated .minecraft directories +- Shared library hard-linking to save disk space +- Fabric and Forge mod loader installation +- Parallel download engine with SHA1 verification +- Automatic Java runtime detection (cross-platform) +- Launch state machine with real-time progress events +- Version dependency resolution with parent inheritance +- OS/feature rule evaluation for conditional libraries +- BMCLAPI and MCBBS download mirror support +- Structured logging with file rotation +- Neo-Tokyo cyberpunk UI design system +- CSS custom properties design tokens +- React Context + useReducer state management + +## [0.0.1] - 2026-05-14 + +### Added +- Initial Tauri v2 + React 18 + TypeScript project scaffold +- Basic authentication (Microsoft OAuth + offline) +- Login page, settings page, home page +- Config persistence (JSON) +- Java auto-detection +- Basic logging +""" + +BASE = "/Users/xiatian/Desktop/BonNext" + +# Write CHANGELOG.md +with open(os.path.join(BASE, "CHANGELOG.md"), "w", encoding="utf-8") as f: + f.write(CHANGELOG_CONTENT) +print("CHANGELOG.md rewritten") + +# Patch SECURITY.md: replace 0.1.x with 1.0.x in supported versions table +sec_path = os.path.join(BASE, "SECURITY.md") +with open(sec_path, "r", encoding="utf-8") as f: + sec_content = f.read() + +old_line = "| 0.1.x | ✅ Active |" +new_line = "| 1.0.x | ✅ Active |" +if old_line in sec_content: + sec_content = sec_content.replace(old_line, new_line) + with open(sec_path, "w", encoding="utf-8") as f: + f.write(sec_content) + print("SECURITY.md patched: 0.1.x -> 1.0.x") +else: + print("WARNING: expected line not found in SECURITY.md") diff --git a/docs/plugin-system-deep-analysis.md b/docs/plugin-system-deep-analysis.md new file mode 100644 index 0000000..a7dba74 --- /dev/null +++ b/docs/plugin-system-deep-analysis.md @@ -0,0 +1,333 @@ +# BonNext 插件系统深度分析 + +> 日期:2026-06-20 +> 范围:基于 `src/plugins/` 与 `src-tauri/src/commands/plugin_proxy.rs` 的代码级审查 +> 目的:识别当前插件系统的原理、实现缺陷与修复方向,作为 v2 改造的依据 + +--- + +## 一、整体架构 + +插件系统分为**前端核心层**(`src/plugins/core/`)、**内置插件层**(`src/plugins/builtins/`)和**后端代理层**(`src-tauri/src/commands/plugin_proxy.rs`)三层。 + +数据流: + +``` +PluginProvider (React) + └─ PluginLoader + ├─ loadBuiltinPlugins() ← 静态 import 内置插件 + └─ loadInstalledPlugins() ← invoke('list_installed_plugins') 读磁盘 + └─ PluginManager + ├─ register(definition, manifest) + ├─ activateAll() → 拓扑排序 → activate(id) + │ └─ createPluginContext(...) → definition.activate(ctx) + └─ subscribe() → useSyncExternalStore → UI 注入项刷新 +``` + +## 二、核心抽象 + +### 1. 插件定义与清单(`types.ts`) + +- `PluginDefinition`:`{ id, name, version, activate(ctx), deactivate?() }`,由 `definePlugin()` 包装(目前是恒等函数,仅做类型约束)。 +- `PluginManifest`:声明 `minAppVersion`、`dependencies`、`permissions`、`contributes`(routes/sidebar/settings/contextMenu/instanceTabs/themes)。 + +### 2. PluginManager(`PluginManager.ts`) + +- **状态机**:`registered → activating → active → deactivating → inactive`,失败转 `error`。 +- **注册幂等**:`register()` 检测重复 id 直接返回。 +- **激活流程**:先校验 manifest(minAppVersion + dependencies),再构造 `PermissionValidator` / `PluginLogger` / `PluginStorage` / `PluginHttpClient` / `PluginFileSystem`,最后调用 `definition.activate(ctx)`。失败时回滚已注入的 UI 项。 +- **拓扑排序**:`activateAll()` 按 `manifest.dependencies` 做 Kahn 算法排序,保留原始插入顺序;检测到环时降级为原序并告警。 +- **快照+订阅**:维护 `sidebarItems / routes / themes` 等可变源数组,`notify()` 时重算排序后的快照,配合 `useSyncExternalStore` 给 React 用,避免无限渲染。 +- **版本比较**:`isVersionSatisfied()` 是简单的按 `.` 切片数值比较,不支持 prerelease。 + +### 3. PluginContext(`PluginContext.ts`) + +激活时注入的 API 表面: +- **UI 注入**:`registerRoute / addSidebarItem / addSettingsSection / addContextMenuItem / addInstanceTab / registerTheme`,全部带 `pluginId` 标记便于卸载清理。 +- **后端访问**:`invoke(command, args)`,经 `PermissionValidator.canInvoke()` 校验。 +- **能力**:`http` / `fs` / `events` / `storage` / `logger`,每个都包了一层权限校验。 + +### 4. 权限模型(`PermissionValidator.ts`) + +权限字符串前缀分域: +- `http:` — 域名白名单(含子域) +- `fs:read:` / `fs:write:` — scope ∈ `instances | config | global` +- `invoke:` — 命名空间;带 `:` 的命令直接取前缀,扁平命令查 `COMMAND_NAMESPACE_MAP`,未映射的统一归到 `invoke:core` +- `events:listen` / `events:emit` — 独立开关 + +### 5. 后端代理(`plugin_proxy.rs`) + +- `plugin_http_get/post`:用全局 reqwest client 转发,**不再做域名校验**(信任前端 PermissionValidator)。 +- `plugin_storage_get/set/delete`:所有插件共用一个 `plugin_storage.json`,整文件读写。 +- `plugin_fs_read/write/exists/read_dir`:按 `scope` 解析根目录,`safe_join_fs()` 拒绝 `..` 和绝对路径,再用 `canonicalize_with_ancestors()` 防符号链接逃逸,最后 `starts_with` 校验。文本文件 10MB 上限,禁二进制。 +- `list_installed_plugins / install_plugin / uninstall_plugin / get_plugin_manifest`:扫描 `game_dir/plugins//manifest.json`;`install_plugin` 从 zip 中先找 manifest 确定 id,再做路径校验后解压。 + +### 6. 启动流程(`PluginProvider.tsx`) + +两阶段加载: +1. **快路径**:`loadBuiltinPlugins()` 同步注册 + `activateAll()`,结束后立即 `setReady(true)`,让 `AppRoutes` 渲染核心路由。 +2. **后台**:`loadInstalledPlugins()` 异步加载第三方插件并激活,失败不影响 ready。 + +同时把核心 Tauri 事件(`launch-state-changed`、`download-progress`、`content-download-progress`、`workflow:complete`、`crash:detected`、`instance:created`、`auth:login`、`security:threat-detected`、`mod:installed`)桥接到插件 `EventBus`,用 `cancelled` 标志位避免卸载后监听器泄漏。 + +### 7. UI 集成 + +- `usePluginRoutes` / `usePluginSidebarItems` / `usePluginSettingsSections` / `usePluginContextMenuItems` / `usePluginInstanceTabs` / `usePluginThemes` / `useAllPlugins`:全部基于 `useSyncExternalStore`。 +- `AppRoutes.tsx`:核心路由直接定义,插件路由用 `React.lazy(route.component)` 包裹 `PluginErrorBoundary` + `Suspense`。 +- `PluginManagementSection.tsx`:设置页提供"已注册/已安装"两个 Tab,安装/激活前弹权限审批 Modal,拒绝安装时回滚 uninstall。 + +### 8. 内置插件 + +`builtinPlugins` 共 10 个:zzz-theme(最先激活)、marketplace、servers、social、ai、security、mod-tools、system-tools、shell-swiftui、shell-editor。每个插件用 `definePlugin()` 写激活逻辑,配套 `manifest.json` 声明权限与贡献。 + +## 三、不足与潜在问题 + +### 严重缺陷 + +1. **第三方插件无法真正运行**:`PluginLoader.createPlaceholderDefinition()` 的 `activate` 是空操作,注释明确说"动态 `import()` 加载需要后续实现打包系统"。也就是说,第三方插件能装能列,但**永远不会执行任何代码**——manifest 里声明的 `contributes` 也不会被消费。 + +2. **manifest 的 `contributes` 字段是死代码**:实际 UI 注入完全靠 `activate(ctx)` 里命令式调用,manifest 里的 routes/sidebar/themes 声明只是文档,加载器从不读取。两套数据源容易脱节。 + +3. **无沙箱隔离**:插件与核心应用共享同一 JS realm,能访问 `window`、所有全局变量、其他插件的模块。恶意/有 bug 的插件可改写核心函数、窃取其他插件数据、直接 `invoke` 绕过权限(`@tauri-apps/api/core` 是公开的)。 + +4. **权限校验可被绕过**: + - 前端 `PermissionValidator` 拦不住插件直接 `import { invoke } from '@tauri-apps/api/core'` 调任意命令。 + - 后端 `plugin_http_get/post` 不复验域名白名单,前端校验被绕过即裸 HTTP。 + - `invoke:core` 是兜底权限,覆盖所有未在 `COMMAND_NAMESPACE_MAP` 里的命令,授权面过大。 + +### 设计缺陷 + +5. **存储模型粗糙**:所有插件共用一个 `plugin_storage.json`,每次 get/set 都整文件读写,无锁、无并发安全、无 per-plugin 隔离文件、无大小限制。 + +6. **EventBus 全局无类型**:`events:listen` 是布尔权限,拿到就能听全部事件;事件名无命名空间(如 `marketplace:search`),无 schema、无重放、无背压。 + +7. **无插件间 API / 服务注册**:`dependencies` 仅用于激活排序,被依赖方无法向依赖方暴露服务。没有 ServiceRegistry / ExtensionPoint 抽象(CLAUDE.md 提到的 `ServiceRegistry`、`DependencyResolver`、`ThemeExtensionPoint` 实际未实现)。 + +8. **Logger 只是 console**:无持久化、无级别过滤、无 per-plugin 查看、不接入 `utils/logger.ts` 的 200 条内存缓冲。 + +9. **无热重载 / 状态恢复**:deactivate 只清 UI 注入和事件监听,插件对 DOM 的直接修改、对全局 store 的副作用无法回滚。重新激活不保证干净状态。 + +10. **错误恢复薄弱**:激活失败仅置 `error` 状态,无重试、无失败计数、无自动禁用、无崩溃报告。 + +### 生态缺失 + +11. **无插件市场 / 发现机制**:只能本地 .zip 安装,无在线注册表、无更新检查、无版本升级路径。 + +12. **无签名 / 完整性校验**:`install_plugin` 只校验 id 字符集,不验签、不校验 hash,zip 可包含任意文件。 + +13. **权限审批一刀切**:Modal 只能全批或全拒,不能勾选子集;批准后无法事后撤销单项权限。 + +14. **manifest 无 schema 校验**:后端用 `serde_json::Value` 索引字段并兜默认值,字段写错静默退化。 + +15. **i18n 缺失**:`SidebarItem.label`、`SettingsSection.label` 是裸字符串,不接 i18n key。 + +16. **内置插件不可卸载**:只能 deactivate,`unregister` 没有在 UI 暴露;用户无法真正移除内置功能。 + +17. **生命周期钩子不全**:只有 `activate/deactivate`,无 `onAppReady / onInstanceLaunching / beforeInstall / onSuspend` 等拦截点;事件总线是唯一交互方式,无法做拦截器/中间件。 + +18. **semver 不完整**:`isVersionSatisfied()` 不支持 prerelease/build metadata,`minAppVersion` 写 `1.0.0-beta` 会判错。 + +19. **`getThemeService()` 已废弃但仍导出**:`ZZZThemePlugin.ts` 标 `@deprecated` 返回 null,ThemeService 类仍在但不再被实例化,是历史包袱。 + +--- + +## 四、深度分析 + +### 方向一:动态加载与贡献机制 + +#### 1.1 根因:第三方插件永远跑不起来 + +**调用链**:`PluginProvider.tsx:47` `loadInstalledPlugins(manager)` → `PluginLoader.ts:78-103` → `createPlaceholderDefinition(info)` → `PluginLoader.ts:32-43` + +```ts +function createPlaceholderDefinition(info: InstalledPluginInfo): PluginDefinition { + return { + id: info.id, name: info.name, version: info.version, + description: info.description ?? undefined, + activate() { /* no-op: dynamic plugin entry loading not yet implemented */ }, + }; +} +``` + +**根因**: +- Tauri v2 前端是 Vite 打包的 SPA,所有 `import()` 都要在构建时被 Vite 静态分析成 chunk。运行时从磁盘读到的插件路径无法被 Vite 解析,`import('/path/to/plugin/index.js')` 在生产构建里直接报错。 +- 后端 `install_plugin` 解压后只返回 `InstalledPluginInfo`(id/name/version/permissions),**没有返回入口 JS 的 URL**,前端也没有 `convertFileSrc` 把磁盘路径转成 `asset://` 协议。 +- 即使能加载,也没有任何沙箱(见方向二),所以"动态加载"和"沙箱"在工程上必须一起做。 + +**影响面**: +- 用户在 `PluginManagementSection.tsx` 装 zip 后只能看到一条"已安装"记录,**永远不会出现该插件注册的路由/侧边栏/设置项**。 +- manifest 里声明的 `contributes.routes/sidebar/themes` 全部无效。 +- 内置插件能用是因为它们走 `builtins/index.ts` 的静态 `import`,被 Vite 编进 bundle,与第三方插件是两条完全不同的路径。 + +#### 1.2 根因:manifest.contributes 是死代码 + +**调用链**:`PluginLoader.loadInstalledPlugins` → `createManifestFromInstalled(info)` → `PluginLoader.ts:46-55` + +```ts +function createManifestFromInstalled(info: InstalledPluginInfo): PluginManifest { + return { id, name, version, description, author, permissions }; + // ↑ 注意:contributes 字段完全没被复制 +} +``` + +**根因**: +- 后端 `list_installed_plugins` 只读 `id/name/version/description/author/permissions`,**不读 `contributes`**。 +- 前端 `createManifestFromInstalled` 也就没 contributes 可复制。 +- 即使复制了,`PluginManager.activate()` 也只把 manifest 存进 `RegisteredPlugin.manifest`,从不遍历 `manifest.contributes` 去调 `ctx.registerRoute()` 等。所有 UI 注入都靠 `definition.activate(ctx)` 命令式调用。 +- 结果:内置插件的 manifest.contributes 和 activate() 各写一遍,**两套数据源没有一致性校验**。 + +#### 1.3 修复方案设计 + +**目标**:让第三方插件能真正运行,且 manifest.contributes 成为唯一声明源。 + +**方案 A:声明式贡献 + 声明式加载(推荐)** + +1. **后端 `list_installed_plugins` 扩展**:返回完整 manifest(含 `contributes`)。`InstalledPluginInfo` 增字段 `contributes: serde_json::Value`、`entry: String`(入口 JS 相对路径)。 +2. **前端 `PluginLoader` 改造**:用 `convertFileSrc()` + Tauri asset 协议把 `plugins//index.js` 转成 `http://asset.localhost/...` URL。用动态 `import(/* @vite-ignore */ url)` 加载模块。`@vite-ignore` 注释让 Vite 跳过静态分析。 +3. **声明式贡献消费**:`PluginManager.activate()` 在调 `definition.activate(ctx)` **之前**,先遍历 `manifest.contributes`,把字符串组件名通过 `ComponentRegistry` 解析成懒加载函数,调 `ctx.registerRoute()` 等。 +4. **组件字符串解析**:`ComponentRegistry` 内置组件静态注册,第三方组件用 `convertFileSrc` 拼 URL + `React.lazy`。 +5. **CSP 调整**:`tauri.conf.json` 的 `app.security.csp` 加 `script-src 'self' asset: http://asset.localhost`。 + +**迁移路径**: +- 阶段 1:实现声明式消费(不动第三方加载),让内置插件纯靠 manifest 工作。 +- 阶段 2:实现 `convertFileSrc` + `@vite-ignore` 动态 import。 +- 阶段 3:加 CSP、签名校验、沙箱。 + +### 方向二:沙箱与权限模型 + +#### 2.1 根因:前端权限校验形同虚设 + +**调用链**:`PluginContext.invoke()` + +```ts +async invoke(command, args) { + if (!permissions.canInvoke(command)) { throw new Error(...); } + return invoke(command, args); // ← 直接调 @tauri-apps/api/core 的 invoke +} +``` + +**根因**: +- `@tauri-apps/api/core` 的 `invoke` 是公开 API,插件代码只要 `import { invoke } from '@tauri-apps/api/core'` 就能绕过 `ctx.invoke` 的包装。 +- 同理,`ctx.http` 调 `plugin_http_get`,但插件也能直接 `fetch('https://evil.com')`。 +- `ctx.fs` 同理,插件能直接 `invoke('plugin_fs_read', ...)`,后端只校验路径不逃逸,**不校验调用方是不是已授权插件**。 + +**影响面**:任何能 `import()` 进来的第三方插件,等于拿到了核心应用的全部 Tauri 命令权限。权限审批 Modal 变成纯仪式。 + +#### 2.2 根因:后端代理不复验权限 + +**根因**:后端命令不知道"是谁在调"。Tauri 命令默认无 caller identity,`plugin_http_get/post/fs_*` 都没有 `plugin_id` 参数(`plugin_fs_*` 有 `plugin_id` 但只用于日志)。即使加了 `plugin_id` 参数,前端也能伪造。 + +#### 2.3 根因:invoke:core 兜底过宽 + +**根因**:`COMMAND_NAMESPACE_MAP` 只列了 ~30 个命令,`lib.rs` 注册了 ~100 个命令。剩下 70+ 个(`launch_game`、`read_accounts`、`save_config` 等)全部归 `core`。任何声明 `invoke:core` 的插件 = 拿到全部未映射命令权限。 + +#### 2.4 根因:无签名校验 + +**根因**:zip 可包含任意文件,解压到 `plugins//`,路径只防 `..` 不防内容。没有签名文件、没有公钥校验。攻击者可以伪造一个 `com.bonnext.marketplace` 的插件覆盖内置插件目录。 + +#### 2.5 修复方案设计 + +**分层防御**:前端沙箱 + 后端鉴权 + 签名链。 + +**第一层:后端命令级鉴权(必做,最高优先级)** + +1. **插件激活时发 token**:`PluginManager.activate()` 成功后,调新命令 `plugin_register_session`,后端生成随机 token,存 `HashMap`,返回 token 给前端。 +2. **所有 plugin_* 命令加 `token` 参数**:后端用 token 查 session,复验权限。 +3. **核心命令拒绝插件直调**:敏感命令改名为 `core:launch_game`,前端强制细粒度权限。 +4. **`invoke:core` 拆细**:废弃兜底,改为 `invoke:core:launch`、`invoke:core:config-read` 等。未映射命令默认拒绝。 + +**第二层:前端沙箱** + +1. **iframe 隔离插件 UI**:`