Merge pull request #16 from farion1231/feat/auto-update

feat: Add auto-updater support with GitHub releases
This commit is contained in:
Jason Young
2025-09-09 15:16:19 +08:00
committed by GitHub
11 changed files with 655 additions and 199 deletions

View File

@@ -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:

View File

@@ -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: "<PUBLICKEY.PEM 内容>"`
- `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: "<PUBLICKEY.PEM>"`
- `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<string>` 读取当前版本。
- `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与动态端点。
## 更新源与格式
- 静态 JSONlatest.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": "<PUBLICKEY.PEM 内容>",
"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": "<sig 内容>" },
"windows-x86_64": { "url": "https://cdn/app-1.0.1-x86_64.zip", "signature": "<sig 内容>" }
}
}
```
- 动态接口(有更新返回 200
```json
{ "version": "1.0.1", "url": "https://cdn/app-1.0.1.zip", "signature": "<sig>", "notes": "..." }
```
- 无更新返回HTTP 204 No Content。
## 八、平台差异与限制
- macOS `.app`
- 支持直接替换更新;若位于 `/Applications` 且需要管理员权限可能失败Updater 不主动提权)。建议用户安装到 `~/Applications`
- 无苹果开发者账号也可测试 Updater但分发会触发 Gatekeeper 警告(建议正式版走代码签名+公证)。
- Windows
- 强烈建议安装器NSIS/MSIUpdater 会下载并运行安装器,`installMode` 控制交互程度。
- 便携版(绿色版)不稳定:运行中无法覆盖自身文件、缺乏提权与回滚;如必须使用,需将应用放到可写目录并设计辅助替换流程(本计划不默认实现)。
## 九、测试用例清单
- 功能流转:有更新/无更新/下载中断/重试/安装后重启成功。
- 安全校验:签名错误与端点被劫持时应拒绝更新。
- 网络异常:超时、代理、断网时的提示与恢复路径。
- 平台行为:
- macOS不同安装路径权限导致的成功/失败覆盖。
- Windows`passive|basicUi|quiet` 下安装器行为;便携版在用户可写目录的替换验证(若执行)。
## 十、发布与运维
- 发布流水线(建议 CI
- 设置 `TAURI_SIGNING_PRIVATE_KEY`(机密变量)。
- 构建生成各平台更新产物与签名。
- 产物与 `latest.json` 上传至 Releases/CDN或刷新动态接口数据
- 回滚策略:
- 撤回最新产物;或将 `latest.json` 指向上一个稳定版本。
- 如允许降级Rust 侧定制 `version_comparator`
## 十一、时间排期(参考)
## 里程碑与验收
- D1密钥与基础集成插件/配置/权限)。
- D2前端入口与进度 UI静态 JSON 本地自测通过。
- D3GitHub Releases/CDN 端到端验证平台专项测试。
- D4文档完善、回滚与异常流程演练、准备上线
- D2前端入口与进度 UI静态 JSON 自测通过。
- D3Releases/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`;编写发布/回滚操作手册

View File

@@ -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",

20
pnpm-lock.yaml generated
View File

@@ -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

361
src-tauri/Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -7,6 +7,8 @@
],
"permissions": [
"core:default",
"opener:default"
"opener:default",
"updater:default",
"process:allow-restart"
]
}

View File

@@ -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 标题栏背景色为主界面蓝色

View File

@@ -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"
]
}
}
}

View File

@@ -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);
}
};

146
src/lib/updater.ts Normal file
View File

@@ -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<void>;
download?: () => Promise<void>;
install?: () => Promise<void>;
}
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<string> {
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<void> {
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" };
}