# 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 为什么这件事值得做(而非"自用工具不需要") 现状的隐性成本: 1. **用户无法得知自己用的是哪版** —— 设置页无版本号展示,issue 反馈时只能靠猜("你那个缓存修复装上了吗?"——本次对话里就真实发生过:凌晨 02:50 装了包,13:02 又改了码,靠猜时间戳判断哪个生效)。 2. **升级不可达** —— 未来若分发给他人(u-desk 已有对外分发先例),对方拿到 v0.1.1 后永远停在 v0.1.1,只能等新包重装。 3. **升版本号是手工雷区** —— `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 — 换装脚本硬编码版本号** ```powershell # 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.go` - `CheckUpdate()` / `GetCurrentVersion()` / `GetUpdateConfig()` / `SetUpdateConfig()` - `DownloadUpdate()` 异步 + `download-progress` 事件流 - `InstallUpdateWithHash()` 哈希校验后安装 - 自动检查定时器(`startAutoUpdateCheck` 可重入,配置变更 `resetAutoUpdateCheck` 免重启生效) - 前端:`update-available` 事件驱动弹窗 **关键差异**:u-desk 是自研(无签名体系);Tauri 2 官方 updater **内建 ed25519 签名校验**,安全性更好,应优先采用官方插件而非照搬自研。 **另有可复用的健壮性经验**:`swap-install.ps1` 里已验证有效的三件套—— 1. `Wait-HandleReleased()`:独占打开目标 exe 探测句柄释放(治 Windows 文件锁延迟释放致 NSIS 静默伪成功) 2. SHA256 三级判据(等磁盘快照 / 两次确定性输出 / 实变) 3. 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` 为**唯一权威源**(打包器实际使用它)。 **动作** 1. `src-tauri/Cargo.toml` 的 `version` 对齐同名值(人工同步,或加校验脚本);**§2.2-D1 双源问题在此关闭**。 2. 新增 IPC `app_get_version`:返回 `env!("CARGO_PKG_VERSION")` + 构建时间 + git short hash(option_env! 注入,未注入显示 `dev`)。 3. `swap-install.ps1:8` 改为 glob 探测(**§2.2-D2 在此关闭**): ```powershell $installer = (Get-ChildItem 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_*_x64-setup.exe' | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName ``` 4. 设置页「关于」展示版本号(L0 即可交付的可见价值)。 **扩张性**:未来 `df-im-gateway` 等独立服务走各自版本线(在其自身发布 skill 内治理),本设计不扩范围,但 `app_get_version` 的返回结构预留 `components: []` 数组位。 ### L1:后端接入(插件注册 + 检查) **Cargo.toml** ```toml tauri-plugin-updater = "2" ``` **lib.rs**(`Builder` 链,插在 notification 之后) ```rust .plugin(tauri_plugin_updater::Builder::new().build()) ``` **capabilities/default.json** 补 ```json "updater:default" ``` **tauri.conf.json** ```json "bundle": { "active": true, "targets": ["nsis"], "createUpdaterArtifacts": true }, "plugins": { "updater": { "endpoints": ["https://app.1216.top/download/devflow/latest.json"], "pubkey": "", "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` 通道): 1. 前置:校验 `tauri.conf.json.version` 与 `Cargo.toml.version` 一致(防 D1 复发) 2. tauri build(含 createUpdaterArtifacts → .nsis.zip + .sig) 3. 计算 sha256 + 读取 .sig 内容 4. 生成 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" } } } 5. 上传 latest.json + .nsis.zip + .sig + 裸 exe 到 **app.1216.top** `download/devflow/`(走 publish-app-site 通道:suke-dev / `/var/www/app-site/`) 6. `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=` 兜底 | | 更新包下载中断 | 中 | 插件内建重试;失败保留旧版不破坏现状 | | 装完起不来(坏包) | 中 | `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__x64-setup.exe` | 手动下载的用户 | ✅ 站点「裸 exe」规范 | | `devflow__x64-setup.nsis.zip` | updater 拉取 | ✅ Tauri 格式强制 | | `devflow__x64-setup.nsis.zip.sig` | updater 验签 | ✅ | | `latest.json` | updater 清单 | ✅ | ### C4 补充决策:体验设计「环境式更新」 **核心原则**:检查频率 ≠ 体验。体验由「提醒方式 / 去重 / 过程可见 / 时机可控」四件事决定。 | 触点 | ✅ 定稿 | |---|---| | 平时 | 完全安静,零存在感 | | 发现新版 | **顶栏非模态徽章**(不弹窗打断) | | 查看详情 | 点徽章展开:版本号 + 更新说明 + 包大小 | | 决定 | 「立即升级」/「稍后」双选(**不自动下载安装**) | | 下载 | 进度条 + 可取消 | | 安装 | `passive` 进度窗口,无需点击 | | 完成 | 重启后 toast「已升级到 vX.Y.Z」 | | 反复 | **同版本只提示一次**(KV `df-update-notified-version`) | **为什么必须是「环境式」而非「隐形型(全自动)」**:DevFlow 是**有状态工作工具**(可能正跑长任务/开着会话),任何自动重启都会打断工作。时机控制权必须交给用户。