diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a16de1..cdefcf0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,14 +85,20 @@ jobs: - name: Build Tauri App (macOS) if: runner.os == 'macOS' + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} run: pnpm tauri build --target universal-apple-darwin - name: Build Tauri App (Windows) if: runner.os == 'Windows' + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} run: pnpm tauri build - name: Build Tauri App (Linux) if: runner.os == 'Linux' + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} run: pnpm tauri build - name: Prepare macOS Assets @@ -180,6 +186,16 @@ jobs: run: | ls -la release-assets || true + - name: Collect Signatures + shell: bash + run: | + # 查找并复制签名文件到 release-assets + find src-tauri/target -name "*.sig" -type f 2>/dev/null | while read sig; do + cp "$sig" release-assets/ || true + done + echo "Collected signatures:" + ls -la release-assets/*.sig || echo "No signatures found" + - name: Upload Release Assets uses: softprops/action-gh-release@v1 with: diff --git a/docs/updater-plan.md b/docs/updater-plan.md index b182d6f..cb3473e 100644 --- a/docs/updater-plan.md +++ b/docs/updater-plan.md @@ -1,209 +1,91 @@ # 更新功能开发计划(Tauri v2 Updater) -> 目标:为桌面应用(macOS `.app`、Windows)集成并验证基于 Tauri v2 官方 Updater 插件的自动更新能力,覆盖版本检测、下载、安装与重启的完整闭环,并建立可复用的发布与测试流程。 +> 目标:基于 Tauri v2 官方 Updater,完成“检查更新 → 下载 → 安装 → 重启”的完整闭环;提供清晰的前后端接口、配置与测试/发布流程。 -## 一、范围与目标 -- 核心能力: - - 检查更新 → 下载 → 安装 → 应用重启。 - - 支持静态 JSON 与动态接口两种更新源。 - - 版本通道(stable/beta)与运行时切换端点(可选)。 -- 平台覆盖: - - macOS `.app`(优先级高)。 - - Windows(推荐安装器路径 NSIS/MSI;便携版说明限制)。 -- 安全要求: - - Updater 更新签名(Ed25519)强制开启:客户端 `pubkey` 校验服务端签名。 - - 不强制平台代码签名(macOS/Windows),但建议上线前完善。 +## 范围与目标 +- 能力:静态 JSON 与动态接口两种更新源;可选稳定/测试通道;进度反馈与错误处理。 +- 平台:macOS `.app` 优先;Windows 使用安装器(NSIS/MSI)。 +- 安全:启用 Ed25519 更新签名校验;上线前建议平台代码签名与公证。 -## 二、方案概述 -- 插件:`tauri-plugin-updater`(Rust/JS 双端 API),前端重启依赖 `@tauri-apps/plugin-process`。 -- 核心配置: - - `src-tauri/tauri.conf.json` - - `bundle.createUpdaterArtifacts: true` - - `plugins.updater.pubkey: ""` - - `plugins.updater.endpoints: ["<更新源 URL 列表>"]` - - Windows 可选:`plugins.updater.windows.installMode: "passive|basicUi|quiet"` - - `src-tauri/capabilities/default.json` 增加:`"updater:default"` -- 构建与签名: - - 生成密钥:`tauri signer generate` → 注入构建环境变量 `TAURI_SIGNING_PRIVATE_KEY`(及可选密码)。 - - 构建产出带签名的更新制品(各平台包 + sig)。 +## 架构与依赖 +- 插件:`tauri-plugin-updater`(更新)、`@tauri-apps/plugin-updater`(JS);`tauri-plugin-process` 与 `@tauri-apps/plugin-process`(重启)。 +- 签名与构建:`tauri signer generate` 生成密钥;CI/本机注入 `TAURI_SIGNING_PRIVATE_KEY`;`bundle.createUpdaterArtifacts: true` 生成签名制品。 +- 权限:在 `src-tauri/capabilities/default.json` 启用 `updater:default` 与 `process:allow-restart`。 +- 配置(`src-tauri/tauri.conf.json`): + - `plugins.updater.pubkey: ""` + - `plugins.updater.endpoints: ["<更新源 URL 列表>"]` + - Windows(可选):`plugins.updater.windows.installMode: "passive|basicUi|quiet"` -## 三、交付物 -- 可运行的自动更新能力(含前端触发入口与进度反馈)。 -- 配置完善:`tauri.conf.json`、`capabilities/default.json`、插件初始化。 -- 本地更新测试用 `latest.json` 模板与脚本(或说明)。 -- 文档: - - 更新源格式说明(静态/动态)。 - - 发布与回滚操作说明。 - - 常见问题排查清单。 +## 前端接口设计(TypeScript) +- 类型 + - `type UpdateChannel = 'stable' | 'beta'` + - `type UpdaterPhase = 'idle' | 'checking' | 'available' | 'downloading' | 'installing' | 'restarting' | 'upToDate' | 'error'` + - `type UpdateInfo = { currentVersion: string; availableVersion: string; notes?: string; pubDate?: string }` + - `type UpdateProgressEvent = { event: 'Started' | 'Progress' | 'Finished'; total?: number; downloaded?: number }` + - `type UpdateError = { code: string; message: string; cause?: unknown }` + - `type CheckOptions = { timeout?: number; channel?: UpdateChannel }` +- API(`src/lib/updater.ts`) + - `getCurrentVersion(): Promise` 读取当前版本。 + - `checkForUpdate(opts?: CheckOptions)` → `up-to-date` 或 `{ status: 'available', info, update }`。 + - `downloadAndInstall(update, onProgress?)` 下载并安装,进度回调映射 Started/Progress/Finished。 + - `relaunchApp()` 调用 `@tauri-apps/plugin-process.relaunch()`。 + - `runUpdateFlow(opts?)` 编排:检查 → 下载安装 → 重启;错误统一抛出 `UpdateError`。 + - `setUpdateChannel(channel)` 前端记录偏好;实际端点切换见“端点动态化”。 +- Hook(可选 `useUpdater()`) + - 返回 `{ phase, info?, progress?, error?, actions: { check, startUpdate, relaunch } }`。 +- UI(组件建议) + - `UpdateBanner`:发现新版本时展示;`UpdaterDialog`:显示说明、进度与错误/重试。 -## 四、里程碑与任务拆解 -1) 准备与密钥 -- 生成更新签名密钥对(Ed25519)。 -- 将公钥填入 `plugins.updater.pubkey`,在构建环境配置 `TAURI_SIGNING_PRIVATE_KEY`。 +## Rust 集成与权限 +- 插件注册(`src-tauri/src/main.rs`): + - `app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;` + - `.plugin(tauri_plugin_process::init())` 用于重启。 +- Windows 清理钩子(可选):`UpdaterExt::on_before_exit(app.cleanup_before_exit)`,避免安装器启动前文件占用。 +- 端点动态化(可选):在 `setup` 根据配置/环境切换 `endpoints`、超时、代理或 headers。 -2) 插件集成(Rust 侧) -- 注册插件:`tauri_plugin_updater::Builder::new().build()`。 -- 可选:在 `setup` 中后台自动检查与安装;或暴露 `invoke` 命令由前端驱动。 -- 可选:`updater_builder()` 覆盖网络参数(超时/代理/headers)与动态端点。 +## 更新源与格式 +- 静态 JSON(latest.json):字段 `version`、`platforms[target].url`、`platforms[target].signature`(`.sig` 内容);可选 `notes`、`pub_date`。 +- 动态接口: + - 无更新:HTTP 204 + - 有更新:HTTP 200 → `{ version, url, signature, notes?, pub_date? }` +- 通道组织:`/stable/latest.json`、`/beta/latest.json`;CDN 缓存需可控,回滚可强制刷新。 -3) 前端接入(Renderer) -- 依赖:`@tauri-apps/plugin-updater`、`@tauri-apps/plugin-process`。 -- 提供“检查更新”入口:显示当前版本、可用版本、更新日志。 -- 进度反馈:`downloadAndInstall` 回调显示 Started/Progress/Finished。 -- 成功后调用 `relaunch()`(或 Rust 侧 `app.restart()`)。 +## 用户流程与 UX +- 流程:检查 → 展示版本/日志 → 下载进度(累计/百分比)→ 安装 → 提示并重启。 +- 错误:网络异常(超时/断网/证书)、签名不匹配、权限/文件占用(Win)。提供“重试/稍后更新”。 +- 平台提示: + - macOS:建议安装在 `~/Applications`,避免 `/Applications` 提权导致失败。 + - Windows:优先安装器分发,并选择合适 `installMode`。 -4) 配置与权限 -- `tauri.conf.json`:`createUpdaterArtifacts`、`pubkey`、`endpoints`、Windows `installMode`(如需)。 -- `capabilities/default.json`:加入 `"updater:default"`。 +## 测试计划 +- 功能:有更新/无更新(204)/下载中断/重试/安装后重启成功与版本号提升。 +- 安全:签名不匹配必须拒绝更新;端点不可用/被劫持有清晰提示。 +- 网络:超时/断网/代理场景提示与恢复。 +- 平台: + - macOS:`/Applications` 与 `~/Applications` 的权限差异。 + - Windows:`passive|basicUi|quiet` 行为差异与成功率。 +- 本地自测:以 v1.0.0 运行,构建 v1.0.1 制品+`.sig`,本地 HTTP 托管 `latest.json`,验证全链路。 -5) 更新源与产物 -- 静态 JSON(`latest.json`)模板: - - `version`、`notes`、`pub_date`(可选),`platforms[target].url` 与 `platforms[target].signature`(必填,signature 为 `.sig` 内容)。 -- 动态 API: - - 无更新返回 HTTP 204;有更新返回 200 + `{ version, url, signature, notes?, pub_date? }`。 -- 产物托管: - - 本地 HTTP 服务器(开发自测)或 GitHub Releases/CDN(准生产)。 +## 发布与回滚 +- 发布(CI 推荐):注入 `TAURI_SIGNING_PRIVATE_KEY` → 构建生成各平台制品+签名 → 上传产物与 `latest.json` 至 Releases/CDN。 +- 回滚:撤下问题版本或将 `latest.json` 指回上一个稳定版本;如需降级,Rust 侧可定制版本比较策略(可选)。 -6) 测试计划 -- 基线用例: - - 发现更新 → 下载 → 安装 → 重启 → 版本号提升。 - - 无更新(204)提示“已是最新”。 - - 签名不匹配拒绝更新(安全校验)。 - - 网络超时/失败的错误提示与恢复。 -- 平台专项: - - macOS:`~/Applications` 与 `/Applications` 两种放置;无苹果账号情况下的 Gatekeeper 行为提示。 - - Windows:安装器三种 `installMode`;便携版在用户可写目录的可行性验证与限制说明(文件锁/提权)。 -- 自测步骤(本地静态 JSON): - - 用 v1.0.0 作为“已安装版本”,构建 v1.0.1 更新产物与 `.sig`。 - - 生成 `latest.json`,启动本地 HTTP(如 `npx http-server`)。 - - 旧版应用中点击“检查更新”并验证完整流程。 - -7) 发布与回滚 -- 发布: - - 通过 CI 生成更新产物与签名,上传到 Release/CDN。 - - 产物与 `latest.json` 上传至 Releases/CDN(或刷新动态接口数据)。 -- 回滚: - - 撤回最新产物;或将 `latest.json` 指向上一个稳定版本。 - - 如允许降级,Rust 侧定制 `version_comparator`。 - -8) 文档与移交 -- 更新源格式说明、运维手册、常见问题排查(见下文附录)。 - -## 五、配置清单(示例) -- `src-tauri/tauri.conf.json` 关键片段: -```json -{ - "bundle": { "createUpdaterArtifacts": true }, - "plugins": { - "updater": { - "pubkey": "", - "endpoints": [ - "https://releases.example.com/{{target}}/{{arch}}/{{current_version}}", - "https://github.com/org/repo/releases/latest/download/latest.json" - ], - "windows": { "installMode": "passive" } - } - } -} -``` -- `src-tauri/capabilities/default.json`: -```json -{ - "permissions": [ - "updater:default" - ] -} -``` - -## 六、前端最小用例(伪代码) -```ts -import { check } from '@tauri-apps/plugin-updater' -import { relaunch } from '@tauri-apps/plugin-process' - -export async function runUpdateFlow() { - const update = await check({ timeout: 30000 }) - if (!update) return { status: 'up-to-date' } - - let downloaded = 0 - let total = 0 - await update.downloadAndInstall((e) => { - switch (e.event) { - case 'Started': total = e.data.contentLength ?? 0; break - case 'Progress': downloaded += e.data.chunkLength; break - } - }) - - await relaunch() -} -``` - -## 七、更新源样例 -- 静态 `latest.json`: -```json -{ - "version": "1.0.1", - "notes": "Bug fixes and performance improvements", - "pub_date": "2025-01-01T10:00:00Z", - "platforms": { - "darwin-aarch64": { "url": "https://cdn/app-1.0.1-darwin-aarch64.tar.gz", "signature": "" }, - "windows-x86_64": { "url": "https://cdn/app-1.0.1-x86_64.zip", "signature": "" } - } -} -``` -- 动态接口(有更新返回 200): -```json -{ "version": "1.0.1", "url": "https://cdn/app-1.0.1.zip", "signature": "", "notes": "..." } -``` -- 无更新返回:HTTP 204 No Content。 - -## 八、平台差异与限制 -- macOS `.app`: - - 支持直接替换更新;若位于 `/Applications` 且需要管理员权限可能失败(Updater 不主动提权)。建议用户安装到 `~/Applications`。 - - 无苹果开发者账号也可测试 Updater,但分发会触发 Gatekeeper 警告(建议正式版走代码签名+公证)。 -- Windows: - - 强烈建议安装器(NSIS/MSI),Updater 会下载并运行安装器,`installMode` 控制交互程度。 - - 便携版(绿色版)不稳定:运行中无法覆盖自身文件、缺乏提权与回滚;如必须使用,需将应用放到可写目录并设计辅助替换流程(本计划不默认实现)。 - -## 九、测试用例清单 -- 功能流转:有更新/无更新/下载中断/重试/安装后重启成功。 -- 安全校验:签名错误与端点被劫持时应拒绝更新。 -- 网络异常:超时、代理、断网时的提示与恢复路径。 -- 平台行为: - - macOS:不同安装路径权限导致的成功/失败覆盖。 - - Windows:`passive|basicUi|quiet` 下安装器行为;便携版在用户可写目录的替换验证(若执行)。 - -## 十、发布与运维 -- 发布流水线(建议 CI): - - 设置 `TAURI_SIGNING_PRIVATE_KEY`(机密变量)。 - - 构建生成各平台更新产物与签名。 - - 产物与 `latest.json` 上传至 Releases/CDN(或刷新动态接口数据)。 -- 回滚策略: - - 撤回最新产物;或将 `latest.json` 指向上一个稳定版本。 - - 如允许降级,Rust 侧定制 `version_comparator`。 - -## 十一、时间排期(参考) +## 里程碑与验收 - D1:密钥与基础集成(插件/配置/权限)。 -- D2:前端入口与进度 UI、静态 JSON 本地自测通过。 -- D3:GitHub Releases/CDN 端到端验证、平台专项测试。 -- D4:文档完善、回滚与异常流程演练、准备上线。 +- D2:前端入口与进度 UI,静态 JSON 自测通过。 +- D3:Releases/CDN 端到端验证,平台专项测试。 +- D4:文档完善、回滚与异常流程演练。 +- 验收:两平台完成“发现→下载→安装→重启→版本提升”;签名校验生效;异常有明确提示与可行恢复。 -## 十二、验收标准 -- 基线流转:在两平台完成“发现→下载→安装→重启→版本提升”。 -- 安全:签名校验生效,签名不匹配拒绝更新。 -- 文档:更新源规范、操作手册、排障清单齐备。 -- 稳定性:网络异常与常见权限问题有明确用户提示与可行恢复路径。 +## 待确认 +- 更新源托管(GitHub Releases 还是自有 CDN)。 +- 是否需要 beta 通道与运行时切换。 +- Windows 是否仅支持安装器分发;便携版兼容策略是否需要明确说明。 +- UI 文案与样式偏好。 ---- - -### 附录 A:常见问题排查 -- “未发现更新”:确认 `version` 更大、端点可达、HTTP 返回码(204/200)。 -- “签名校验失败”:`pubkey` 与私钥不匹配;`signature` 必须是 `.sig` 文件内容。 -- “下载/超时失败”:增加 `timeout`、检查代理/证书、重试策略与提示。 -- “macOS 更新失败”:检查安装路径是否需要管理员权限;建议 `~/Applications`。 -- “Windows 便携版覆盖失败”:可写权限/进程占用/缺少提权,优先改为安装器分发。 - -### 附录 B:后续可选优化 -- 渠道支持(stable/beta)与 UI 切换。 -- 增量发布策略与 CDN 缓存优化。 -- 远程开关与灰度比例控制(动态接口)。 -- 统一 Telemetry:下载失败率、平均时延、更新成功率统计。 +## 落地步骤(实施顺序) +1) 生成 Ed25519 密钥,将公钥写入 `plugins.updater.pubkey`,在构建环境配置 `TAURI_SIGNING_PRIVATE_KEY`。 +2) `src-tauri` 注册 `tauri-plugin-updater` 与 `tauri-plugin-process`,补齐 `capabilities/default.json` 与 `tauri.conf.json`。 +3) 前端新增 `src/lib/updater.ts` 封装与 `UpdateBanner`/`UpdaterDialog` 组件,接入入口按钮。 +4) 本地静态 `latest.json` 自测全链路;完善错误与进度提示。 +5) 配置 CI 发布产物与 `latest.json`;编写发布/回滚操作手册。 diff --git a/package.json b/package.json index 769e670..94493c5 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "@codemirror/view": "^6.38.2", "@tailwindcss/vite": "^4.1.13", "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-process": "^2.0.0", + "@tauri-apps/plugin-updater": "^2.0.0", "codemirror": "^6.0.2", "lucide-react": "^0.542.0", "react": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a64f207..e9b265b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: '@tauri-apps/api': specifier: ^2.8.0 version: 2.8.0 + '@tauri-apps/plugin-process': + specifier: ^2.0.0 + version: 2.3.0 + '@tauri-apps/plugin-updater': + specifier: ^2.0.0 + version: 2.9.0 codemirror: specifier: ^6.0.2 version: 6.0.2 @@ -626,6 +632,12 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-process@2.3.0': + resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==} + + '@tauri-apps/plugin-updater@2.9.0': + resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1427,6 +1439,14 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.8.1 '@tauri-apps/cli-win32-x64-msvc': 2.8.1 + '@tauri-apps/plugin-process@2.3.0': + dependencies: + '@tauri-apps/api': 2.8.0 + + '@tauri-apps/plugin-updater@2.9.0': + dependencies: + '@tauri-apps/api': 2.8.0 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.0 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3073a89..6fde274 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -90,6 +90,15 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -562,6 +571,8 @@ dependencies = [ "tauri-build", "tauri-plugin-log", "tauri-plugin-opener", + "tauri-plugin-process", + "tauri-plugin-updater", "toml 0.8.2", ] @@ -834,6 +845,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1123,6 +1145,18 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "flate2" version = "1.1.2" @@ -1412,8 +1446,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1423,9 +1459,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1694,6 +1732,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.16" @@ -2102,6 +2157,7 @@ checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ "bitflags 2.9.3", "libc", + "redox_syscall", ] [[package]] @@ -2135,6 +2191,12 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -2193,6 +2255,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2549,6 +2617,18 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" +dependencies = [ + "bitflags 2.9.3", + "objc2 0.6.2", + "objc2-app-kit 0.3.1", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -2655,6 +2735,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2 0.6.2", + "objc2-foundation 0.3.1", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.16", +] + [[package]] name = "pango" version = "0.18.3" @@ -3042,6 +3136,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.16", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.16", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.40" @@ -3088,6 +3237,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3108,6 +3267,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3126,6 +3295,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3253,16 +3431,21 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3272,6 +3455,21 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] @@ -3325,6 +3523,12 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3347,6 +3551,41 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3791,6 +4030,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3926,6 +4171,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4108,6 +4364,48 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.16", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.8.0" @@ -4347,6 +4645,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -4624,6 +4932,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -4850,6 +5164,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.1" @@ -4894,6 +5218,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.0" @@ -5563,6 +5896,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xdg-home" version = "1.3.0" @@ -5702,6 +6045,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerotrie" version = "0.2.2" @@ -5735,6 +6084,18 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.11.0", + "memchr", +] + [[package]] name = "zvariant" version = "4.0.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 017f78e..49b2021 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,6 +24,8 @@ log = "0.4" tauri = { version = "2.8.2", features = ["tray-icon"] } tauri-plugin-log = "2" tauri-plugin-opener = "2" +tauri-plugin-process = "2" +tauri-plugin-updater = "2" dirs = "5.0" toml = "0.8" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 639f455..2299e84 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,8 @@ ], "permissions": [ "core:default", - "opener:default" + "opener:default", + "updater:default", + "process:allow-restart" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c776796..1ab0a0e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -220,8 +220,20 @@ async fn update_tray_menu( #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { + // 注册 Updater 插件(桌面端) + #[cfg(desktop)] + { + if let Err(e) = app + .handle() + .plugin(tauri_plugin_updater::Builder::new().build()) + { + // 若配置不完整(如缺少 pubkey),跳过 Updater 而不中断应用 + log::warn!("初始化 Updater 插件失败,已跳过:{}", e); + } + } #[cfg(target_os = "macos")] { // 设置 macOS 标题栏背景色为主界面蓝色 diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 46f2ad0..f36a9cb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -30,6 +30,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -38,4 +39,13 @@ "icons/icon.ico" ] } + , + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDRERTRCNEUxQUE3MDA4QTYKUldTbUNIQ3E0YlRrVFF2cnFVVE1jczlNZFlmemxXd0h6cTdibXRJWjBDSytQODdZOTYvR3d3d2oK", + "endpoints": [ + "https://github.com/jasonyoung/cc-switch/releases/latest/download/latest.json" + ] + } + } } diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index c80cc95..2e7c746 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { X, Info, RefreshCw, FolderOpen } from "lucide-react"; import { getVersion } from "@tauri-apps/api/app"; import "../lib/tauri-api"; +import { runUpdateFlow } from "../lib/updater"; import type { Settings } from "../types"; interface SettingsModalProps { @@ -66,11 +67,13 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const handleCheckUpdate = async () => { setIsCheckingUpdate(true); try { - await window.api.checkForUpdates(); + // 优先使用 Tauri Updater 流程;失败时回退到打开 Releases 页面 + await runUpdateFlow({ timeout: 30000 }); } catch (error) { - console.error("检查更新失败:", error); + console.error("检查更新失败,回退到 Releases 页面:", error); + await window.api.checkForUpdates(); } finally { - setTimeout(() => setIsCheckingUpdate(false), 2000); + setIsCheckingUpdate(false); } }; diff --git a/src/lib/updater.ts b/src/lib/updater.ts new file mode 100644 index 0000000..017406b --- /dev/null +++ b/src/lib/updater.ts @@ -0,0 +1,146 @@ +import { getVersion } from "@tauri-apps/api/app"; + +// 可选导入:在未注册插件或非 Tauri 环境下,调用时会抛错,外层需做兜底 +// 我们按需加载并在运行时捕获错误,避免构建期类型问题 +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import type { Update } from "@tauri-apps/plugin-updater"; + +export type UpdateChannel = "stable" | "beta"; + +export type UpdaterPhase = + | "idle" + | "checking" + | "available" + | "downloading" + | "installing" + | "restarting" + | "upToDate" + | "error"; + +export interface UpdateInfo { + currentVersion: string; + availableVersion: string; + notes?: string; + pubDate?: string; +} + +export interface UpdateProgressEvent { + event: "Started" | "Progress" | "Finished"; + total?: number; + downloaded?: number; +} + +export interface UpdateHandle { + version: string; + notes?: string; + date?: string; + downloadAndInstall: ( + onProgress?: (e: UpdateProgressEvent) => void, + ) => Promise; + download?: () => Promise; + install?: () => Promise; +} + +export interface CheckOptions { + timeout?: number; + channel?: UpdateChannel; +} + +function mapUpdateHandle(raw: Update): UpdateHandle { + return { + version: (raw as any).version ?? "", + notes: (raw as any).notes, + date: (raw as any).date, + async downloadAndInstall(onProgress?: (e: UpdateProgressEvent) => void) { + await (raw as any).downloadAndInstall((evt: any) => { + if (!onProgress) return; + const mapped: UpdateProgressEvent = { + event: evt?.event, + }; + if (evt?.event === "Started") { + mapped.total = evt?.data?.contentLength ?? 0; + mapped.downloaded = 0; + } else if (evt?.event === "Progress") { + mapped.downloaded = evt?.data?.chunkLength ?? 0; // 累积由调用方完成 + } + onProgress(mapped); + }); + }, + // 透传可选 API(若插件版本支持) + download: (raw as any).download + ? async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + await (raw as any).download(); + } + : undefined, + install: (raw as any).install + ? async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + await (raw as any).install(); + } + : undefined, + }; +} + +export async function getCurrentVersion(): Promise { + try { + return await getVersion(); + } catch { + return ""; + } +} + +export async function checkForUpdate( + opts: CheckOptions = {}, +): Promise< + | { status: "up-to-date" } + | { status: "available"; info: UpdateInfo; update: UpdateHandle } +> { + // 动态引入,避免在未安装插件时导致打包期问题 + const { check } = await import("@tauri-apps/plugin-updater"); + + const currentVersion = await getCurrentVersion(); + const update = await check({ timeout: opts.timeout ?? 30000 } as any); + + if (!update) { + return { status: "up-to-date" }; + } + + const mapped = mapUpdateHandle(update); + const info: UpdateInfo = { + currentVersion, + availableVersion: mapped.version, + notes: mapped.notes, + pubDate: mapped.date, + }; + + return { status: "available", info, update: mapped }; +} + +export async function relaunchApp(): Promise { + const { relaunch } = await import("@tauri-apps/plugin-process"); + await relaunch(); +} + +export async function runUpdateFlow( + opts: CheckOptions = {}, +): Promise<{ status: "up-to-date" | "done" }> { + const result = await checkForUpdate(opts); + if (result.status === "up-to-date") return result; + + let downloaded = 0; + let total = 0; + await result.update.downloadAndInstall((e) => { + if (e.event === "Started") { + total = e.total ?? 0; + downloaded = 0; + } else if (e.event === "Progress") { + downloaded += e.downloaded ?? 0; + // 调用方可监听此处并更新 UI(目前设置页仅显示加载态) + console.debug("update progress", { downloaded, total }); + } + }); + + await relaunchApp(); + return { status: "done" }; +}