后端 update.rs: app_check_update(拉 latest.json+ed25519 签名校验) / app_download_install_update / prefs 读写 / mark_checked / mark_notified 五命令, lib.rs 注册;tauri.conf updater 段(endpoints=app.1216.top/download/devflow/latest.json + pubkey + createUpdaterArtifacts) + tauri-plugin-updater 依赖 + capabilities 权限。 前端 UpdateBadge.vue(顶栏徽章+下拉) / useUpdateCheck.ts(检查/下载/安装状态机) / AboutPanel.vue(设置页关于面板) + AppLayout 接线 + SettingsNav/searchIndex 索引 + i18n zh/en settings 键。 运维 scripts/publish-release.ps1(发版) + swap-install.ps1(换装校验) + 设计文档 版本更新与自动升级-设计-2026-10-09.md。 验收: cargo check -p devflow EXIT 0 + vue-tsc EXIT 0 + build EXIT 0。
20 KiB
DevFlow 版本更新与自动升级 — 专项设计(2026-10-09)
对象:应用内「版本可见 → 检查更新 → 下载 → 安装重启」全链路。 标准:合理 · 兼容 · 优雅 · 可扩张。 现状勘察:
src-tauri/tauri.conf.json/src-tauri/Cargo.toml/src-tauri/capabilities/default.json/src-tauri/src/lib.rs/src/views/Settings.vue/src/components/settings/SettingsNav.vue/scripts/package-devflow.ps1/swap-install.ps1/ u-desk 先例。
一、本质定性
1.1 两条"升级"通道必须分开命名
现状只有一个动作叫「换装」,但它其实是两个语义完全不同的事情,混称导致讨论失真:
| 通道 | 触发源 | 使用者 | 目标 | 现载体 |
|---|---|---|---|---|
| 本地换装 | 本机源码改动 | 开发者(你) | 把刚编译的产物装到自己机器 | scripts/package-devflow.ps1 -Mode full / swap-install.ps1 |
| 应用内升级 | 远端新版本 | 任何使用者 | 把手上的旧版升到新版 | 不存在 |
本设计只做第二条。第一条保持不动(且是本设计的发布端依赖)。
1.2 为什么这件事值得做(而非"自用工具不需要")
现状的隐性成本:
- 用户无法得知自己用的是哪版 —— 设置页无版本号展示,issue 反馈时只能靠猜("你那个缓存修复装上了吗?"——本次对话里就真实发生过:凌晨 02:50 装了包,13:02 又改了码,靠猜时间戳判断哪个生效)。
- 升级不可达 —— 未来若分发给他人(u-desk 已有对外分发先例),对方拿到 v0.1.1 后永远停在 v0.1.1,只能等新包重装。
- 升版本号是手工雷区 ——
swap-install.ps1硬编码devflow_0.1.1_x64-setup.exe,改版本号忘改脚本 = 静默装回旧包(静默伪成功,最危险的一类故障)。
结论:先做 L0(版本单一源 + 可见),再做 L1(检查→提示),最后 L2(下载安装)。L0 交付即消除 1、3 两项,成本极低。
二、现状勘察(全部实证)
2.1 三元缺口
| 层 | 现状 | 证据 |
|---|---|---|
| 插件层 | 无 tauri-plugin-updater |
src-tauri/Cargo.toml:18-25 仅 dialog/opener/window-state/notification/single-instance |
| 配置层 | 无 plugins.updater 段 |
tauri.conf.json 仅 bundle.targets: ["nsis"] |
| 逻辑层 | 无版本比对 IPC | grep check_update|latest_version|auto_update 于 src-tauri/src → 0 命中 |
| UI 层 | 无版本号/检查更新入口 | grep 检查更新|getVersion|appVersion 于 src/ → 0 命中;SettingsNav 仅 7 分类 |
2.2 现存缺陷(本设计顺带治理)
D1 — 版本双源不一致
src-tauri/tauri.conf.json:3 "version": "0.1.1" ← 打包器实际用这个
src-tauri/Cargo.toml:3 version = "0.1.0" ← Rust crate 用这个
Tauri 2 中 bundle 版本取 tauri.conf.json,CARGO_PKG_VERSION 取 Cargo.toml。当前两者已经分叉(0.1.1 vs 0.1.0),未来必然出「设置页显示 0.1.0,实际装的是 0.1.1」这类灵异现象。
D2 — 换装脚本硬编码版本号
# swap-install.ps1:8
$installer = 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_0.1.1_x64-setup.exe'
版本号进入文件名,升版忘改 → 找不到文件 → Start-Process 抛错(尚好);若旧版包残留同目录,则装回旧版(伪成功)。必须改为 glob 探测(package-devflow.ps1:44 已用 Sort-Object LastWriteTime -Descending | Select-Object -First 1 的正确姿势,swap-install 未对齐)。
2.3 可复用的成熟资产
u-desk(同作者的 Wails 应用)已完成全套更新链路,虽技术栈不同(Go+Wails vs Rust+Tauri),但方案骨架可直接借鉴:
- 清单托管:
https://img.1216.top/u-desk/last-version.json(七牛 CDN) - 后端:
internal/api/update_api.go+internal/service/update_service.goCheckUpdate()/GetCurrentVersion()/GetUpdateConfig()/SetUpdateConfig()DownloadUpdate()异步 +download-progress事件流InstallUpdateWithHash()哈希校验后安装- 自动检查定时器(
startAutoUpdateCheck可重入,配置变更resetAutoUpdateCheck免重启生效)
- 前端:
update-available事件驱动弹窗
关键差异:u-desk 是自研(无签名体系);Tauri 2 官方 updater 内建 ed25519 签名校验,安全性更好,应优先采用官方插件而非照搬自研。
另有可复用的健壮性经验:swap-install.ps1 里已验证有效的三件套——
Wait-HandleReleased():独占打开目标 exe 探测句柄释放(治 Windows 文件锁延迟释放致 NSIS 静默伪成功)- SHA256 三级判据(等磁盘快照 / 两次确定性输出 / 实变)
- SYSTEM 上下文转投交互用户会话(GUI 才可见)
这三条应在应用内升级的「安装前等待」阶段复刻(Rust 侧实现等价逻辑)。
三、方案选型
方案 A:Tauri 官方 tauri-plugin-updater(推荐)
能力:check() → Update 对象(version/notes/date)→ download_and_install(on_chunk) → 自动重启。
优点
- 内建 ed25519 签名校验(防篡改,安全基线)
- 跨平台统一(NSIS / MSI / AppImage / dmg)
- 与 Tauri 窗口生命周期原生协同(安装前自动关窗)
- 官方维护,随 Tauri 2 演进
代价
- 需引入签名密钥(
tauri signer generate,公钥入配置,私钥入打包环境) - 打包需开
bundle.createUpdaterArtifacts: true(产出.nsis.zip+.nsis.zip.sig) - 端点必须 HTTPS + 清单格式固定(
latest.json)
方案 B:自研(照搬 u-desk 模式)
优点:零新依赖,可完全复用 swap-install 的静默安装动作。
缺点:签名/校验/回滚/进度全自研;latest.json 契约要自己定;安全基线靠自己守。
选型结论:A 为基座,B 的本地动作经验作为"安装前等待"的健壮性补丁喂给 A
理由:
- 官方插件把「签名校验」这件最容易做错的事做对了,没有理由重造。
- devflow 是联网分发场景(未来对外),签名是刚需;u-desk 的裸下载在自用场景够,对外不够。
- 本地换装通道(脚本)不废弃,继续服务开发期高频迭代(它比走远端快得多)。
四、分层设计
┌─ L3 发布侧(脚本增强)────────────────────────────┐
│ tauri build → 产出 .nsis.zip + .sig │
│ → 生成 latest.json(version/notes/pub_date/urls/sig)│
│ → 上传 app.1216.top/download/devflow/ │
└────────────────────────────────────────────────────┘
↑ 拉取
┌─ L2 前端 ──────────────────────────────────────────┐
│ SettingsNav 新增「关于」分类(第 8 类) │
│ AboutPanel.vue:版本号 / 检查更新 / 更新弹窗 / 进度 │
└────────────────────────────────────────────────────┘
↑ IPC
┌─ L1 后端(Rust)───────────────────────────────────┐
│ tauri-plugin-updater 注册 + capabilities 授权 │
│ IPC: app_get_version / app_check_update / │
│ app_download_install_update │
│ 本地 KV: df-update-auto-check / -interval-minutes / │
│ -last-check / -notified-version │
└────────────────────────────────────────────────────┘
↑ 单一源
┌─ L0 版本治理 ──────────────────────────────────────┐
│ 单一版本源 + 设置页展示版本号(零风险,先落) │
└────────────────────────────────────────────────────┘
L0:版本单一源治理
决策:以 tauri.conf.json 的 version 为唯一权威源(打包器实际使用它)。
动作
src-tauri/Cargo.toml的version对齐同名值(人工同步,或加校验脚本);§2.2-D1 双源问题在此关闭。- 新增 IPC
app_get_version:返回env!("CARGO_PKG_VERSION")+ 构建时间 + git short hash(option_env! 注入,未注入显示dev)。 swap-install.ps1:8改为 glob 探测(§2.2-D2 在此关闭):$installer = (Get-ChildItem 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_*_x64-setup.exe' | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName- 设置页「关于」展示版本号(L0 即可交付的可见价值)。
扩张性:未来 df-im-gateway 等独立服务走各自版本线(在其自身发布 skill 内治理),本设计不扩范围,但 app_get_version 的返回结构预留 components: [] 数组位。
L1:后端接入(插件注册 + 检查)
Cargo.toml
tauri-plugin-updater = "2"
lib.rs(Builder 链,插在 notification 之后)
.plugin(tauri_plugin_updater::Builder::new().build())
capabilities/default.json 补
"updater:default"
tauri.conf.json
"bundle": {
"active": true,
"targets": ["nsis"],
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"endpoints": ["https://app.1216.top/download/devflow/latest.json"],
"pubkey": "<tauri signer generate 产出的公钥>",
"windows": { "installMode": "passive" }
}
}
```
> `installMode: passive` —— **不是静默**:弹进度条窗口,用户看得见进度,但无需点击「下一步」,装完自动往下走。
> 三档对比:`basic`(完整向导,需点下一步)/ `passive`(进度条,无需点击)/ `quiet`(完全无 UI,**失败不可见**,不推荐)。
> 注:`swap-install.ps1` 用的 NSIS `/S` 是真静默(等价 `quiet`),应用内升级刻意不采用——面向用户自己机器的升级需要失败可见性。
**IPC 契约**(新增 3 个,命名沿用 `app_*` 前缀)
| 命令 | 入参 | 返回 | 风险档 |
|---|---|---|---|
| `app_get_version` | — | `{version, build_time, git_hash}` | 只读 |
| `app_check_update` | — | `{has_update, version?, notes?, date?}` | 只读(外发网络) |
| `app_download_install_update` | — | 流式事件 + 成功即重启 | **高风险(需审批)** |
**配置 KV**(复用既有 appSettings 体系,零新表)
- `df-update-auto-check`:bool,默认 `true`
- `df-update-check-interval-minutes`:int,默认 `15`
- `df-update-last-check`:ISO 时间戳(节流用,防每次启动都拉)
- `df-update-notified-version`:string(**同版本只提示一次**,防骚扰)
**自动检查策略**:启动后延迟 **3s** → 拉清单 → 有更新则发事件 `app-update-available` 到主窗;此后**每 15 分钟**检查一次,但**同一版本号只提示一次**(用户点「稍后」后不再重复弹)。**只提示不自动装**(安装是高风险动作,必须人点头)。
### L2:前端(设置页「关于」分类)
**SettingsNav** 加第 8 类:
```ts
{ value: 'about', icon: 'ℹ️', labelKey: 'settings.navAbout' },
searchIndex.ts 补对应条目(SettingsCategory 联合类型同步扩 'about')。
新增 src/components/settings/AboutPanel.vue(对齐既有 panel 视觉语言)
┌─ 关于 DevFlow ─────────────────────────┐
│ 当前版本 v0.1.2 (a1b2c3d) [检查更新] │
│ 构建时间 2026-10-09 14:30 │
│ ─────────────────────────────────────── │
│ ☑ 启动时自动检查更新(启动后 3 秒) │
│ 检查间隔 [15 分钟 ▾] │
│ ─────────────────────────────────────── │
│ 上次检查 2026-10-09 14:35 │
└─────────────────────────────────────────┘
更新弹窗(复用既有 ConfirmDialog 视觉,含:新版本号 / 更新说明 markdown / 下载进度条 / 「立即更新并重启」「稍后」)
事件订阅(对齐既有 useAiEvents 模式)
app-update-available→ 顶栏小红点 + 弹窗app-update-progress→ 进度条(downloaded/total/ 百分比)
i18n:zh-CN / en 双语同步补 settings.navAbout / settings.about* / update.* 系列键。
L3:发布侧脚本增强
新增 scripts/publish-release.ps1(或扩展 package-devflow.ps1 加 -Mode release 通道):
- 前置:校验
tauri.conf.json.version与Cargo.toml.version一致(防 D1 复发) - tauri build(含 createUpdaterArtifacts → .nsis.zip + .sig)
- 计算 sha256 + 读取 .sig 内容
- 生成 latest.json: { "version": "0.1.2", "notes": "<从 docs/CHANGELOG.md 提取本节>", "pub_date": "2026-10-09T14:30:00Z", "platforms": { "windows-x86_64": { "signature": "<.sig 内容>", "url": "https://app.1216.top/download/devflow/devflow_0.1.2_x64-setup.nsis.zip" } } }
- 上传 latest.json + .nsis.zip + .sig + 裸 exe 到 app.1216.top
download/devflow/(走 publish-app-site 通道:suke-dev //var/www/app-site/) app.1216.top的 JSON 已配Cache-Control: no-cache→ 无需刷 CDN,发布即生效
**密钥管理**
- 一次性:`tauri signer generate -w ~/.tauri/devflow.key` → 公钥入 `tauri.conf.json` 的 `pubkey`
- 打包时设环境变量 `TAURI_SIGNING_PRIVATE_KEY`(路径)+ `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`
- **私钥绝不入仓**(`.gitignore` 补 `*.key`)
---
## 五、兼容性核验
| 既有资产 | 影响 | 结论 |
|---|---|---|
| `scripts/package-devflow.ps1` | 不动 verify/full 两通道,L3 新增独立通道 | ✔ 零回归 |
| `swap-install.ps1` | 仅改第 8 行硬编码 → glob(D2 修复) | ✔ 行为等价,更健壮 |
| `lib.rs` Builder 链 | 追加一个 `.plugin()`,不触碰既有 5 个插件与 68 IPC | ✔ 零侵入 |
| `capabilities/default.json` | 追加一行 `updater:default` | ✔ 纯增量 |
| SettingsNav 7 类 | 追加第 8 类,搜索/防抖/持久化逻辑自动覆盖(`SETTINGS_INDEX` 驱动) | ✔ 架构自洽 |
| appSettings KV 体系 | 新增 4 个 key,复用既有读写 | ✔ 零新表零迁移 |
| `tauri.conf.json` targets | 保持 `["nsis"]`,只加 `createUpdaterArtifacts` | ✔ 不扩大构建目标(对齐知识库「targets 收窄需还原」的教训:本项是新增字段非收窄,安全) |
| devflow.db / devflow-dev.db | L1/L2 只读 KV,不涉 schema | ✔ 零库变更 |
**dex 依赖**:无。**P0 依赖**:无(可立即开工)。
---
## 六、实施批次(全部可独立交付)
| 批次 | 规模 | 内容 | 验收 |
|---|---|---|---|
| **B1** | S | L0 版本治理:Cargo.toml 对齐 0.1.1 / `app_get_version` IPC / swap-install glob 修复 / 设置页版本号展示 | `cargo check -p devflow` 0 error;设置页可见 `v0.1.1`;换装脚本升版后仍能正确取包 |
| **B2** | M | L1 插件接入 + `app_check_update` IPC + 自动检查节流 + `app-update-available` 事件 | 手动改远端 `latest.json` 版本号 → 应用内提示可用 |
| **B3** | M | L2 下载安装:进度事件 + 更新弹窗 + `app_download_install_update` + 重启 | 端到端:旧版 → 提示 → 下载 → 装 → 重启后版本号变新 |
| **B4** | S | L3 发布脚本 + 密钥生成 + latest.json 生成上传 | 一条命令产出并上传可分发的更新清单(app.1216.top 无需刷缓存) |
**建议序**:B1 立即做(零风险、当天可交付、消除 D1/D2 两个真实缺陷);B2→B3 一起做(拆开无独立价值);B4 在首次真实发版前完成即可。
**总估算**:B1 0.5 天 / B2+B3 1.5-2 天 / B4 0.5 天 ≈ **2.5-3 天**。
---
## 七、风险与边界
| 风险 | 等级 | 对策 |
|---|---|---|
| 私钥泄露 | **高** | `.gitignore` 拦 `*.key`;仅打包机存私钥;公钥入仓无妨 |
| ~~CDN 缓存陈旧致拉了旧清单~~ | ~~中~~ **已消解** | C1 改走 `app.1216.top`(已配 `Cache-Control: no-cache`),无缓存陈旧问题;可选 `?t=<timestamp>` 兜底 |
| 更新包下载中断 | 中 | 插件内建重试;失败保留旧版不破坏现状 |
| 装完起不来(坏包) | 中 | `installMode: passive` 失败可见;保留上一版安装包以备手回滚 |
| 自动检查打扰 | 低 | 15 分钟周期 + **同版本只提示一次** + 只提示不自动装 + 可关 |
| 版本双源复发 | 低 | B1 加校验(发布脚本第 1 步)+ 可选 `build.rs` 断言 |
**明确不做(本设计范围外)**
- 增量更新(delta update)——体量收益不值复杂度
- 灰度/分渠道发布
- 非主应用组件(`df-im-gateway` 等)的版本线
- 回滚 UI(保留手动重装路径即可)
---
## 八、决策点(✅ 2026-10-09 拍板定稿)
| # | 议题 | ✅ 定稿 | 依据 |
|---|---|---|---|
| **C1** | 清单托管位 | **`app.1216.top/download/devflow/`** | 产品门户语义正确;该站已配 `Cache-Control: no-cache`,发布即生效,无需刷 CDN;通道(suke-dev / `/var/www/app-site/`)现成 |
| **C2** | 安装模式 | **`passive`**(进度条窗口,无需点击) | 非静默;`quiet` 完全无感但失败不可见(静默伪成功风险) |
| **C3** | B1 是否先行 | **立即开工** | 零风险、当天交付,消 D1/D2 两个真实缺陷 |
| **C4** | 自动检查节奏 | **启动后 3s → 周期 15 分钟** + 同版本单次提示 + 关于页手动按钮 | 用户定:3s / 15min |
### C1 补充决策:双产物并存(站点规范 vs Tauri 强制格式)
app-site 站规为「不打 zip,直接裸 exe」,而 Tauri updater **强制** `.nsis.zip` + `.sig`。故 `download/devflow/` 并存四项:
| 文件 | 给谁 | 合规 |
|---|---|---|
| `devflow_<ver>_x64-setup.exe` | 手动下载的用户 | ✅ 站点「裸 exe」规范 |
| `devflow_<ver>_x64-setup.nsis.zip` | updater 拉取 | ✅ Tauri 格式强制 |
| `devflow_<ver>_x64-setup.nsis.zip.sig` | updater 验签 | ✅ |
| `latest.json` | updater 清单 | ✅ |
### C4 补充决策:体验设计「环境式更新」
**核心原则**:检查频率 ≠ 体验。体验由「提醒方式 / 去重 / 过程可见 / 时机可控」四件事决定。
| 触点 | ✅ 定稿 |
|---|---|
| 平时 | 完全安静,零存在感 |
| 发现新版 | **顶栏非模态徽章**(不弹窗打断) |
| 查看详情 | 点徽章展开:版本号 + 更新说明 + 包大小 |
| 决定 | 「立即升级」/「稍后」双选(**不自动下载安装**) |
| 下载 | 进度条 + 可取消 |
| 安装 | `passive` 进度窗口,无需点击 |
| 完成 | 重启后 toast「已升级到 vX.Y.Z」 |
| 反复 | **同版本只提示一次**(KV `df-update-notified-version`) |
**为什么必须是「环境式」而非「隐形型(全自动)」**:DevFlow 是**有状态工作工具**(可能正跑长任务/开着会话),任何自动重启都会打断工作。时机控制权必须交给用户。