客户端更新切换到官方更新插件并按渠道分发

- 客户端接入 tauri-plugin-updater:原生侧注册插件,删除自研更新下载命令、下载进度事件与安装器启动逻辑
- 客户端更新服务与更新提示改走官方插件接口,删除自研清单解析、版本比较与下载实现
- 更新能力只授予客户端主窗口,移除只为自研清单放行的 OSS 白名单与 CSP 连接项
- 新增构建期更新检查开关:开发态默认关闭,agc 启动不请求更新清单、不显示更新入口
- 发布脚本按渠道生成官方更新插件清单与签名,universal macOS 产物同时挂两个平台键,缺签名失败关闭
- 发布脚本按渠道上传安装包、签名与渠道清单,并为 dev-win 生成旧协议 sha256 迁移清单
- 构建期按渠道注入更新端点配置,渠道与目标平台不匹配时发布失败关闭
- Jenkins 流水线新增渠道参数与签名凭据注入,归档补充签名与迁移清单
- 新增发布上传 dry-run 开关,只打印 ossutil 命令且不回显凭据
- 更新技术方案与开发运维文档,登记 macOS 渠道落地待办
This commit is contained in:
kdletters
2026-09-17 17:51:53 +08:00
parent 5db407c3dc
commit a5fd25f10a
31 changed files with 1411 additions and 567 deletions
@@ -1,68 +1,129 @@
# AGC 客户端更新检查与下载
## 交付范围
更新时间:`2026-09-17`
AGC 每次启动时由根窗口检查一次公开 OSS 更新清单。清单默认位于
`https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/latest.json`,构建时可用
`VITE_AGC_UPDATE_MANIFEST_URL` 覆盖为同一受信任 OSS 域名下的 HTTPS 地址。客户端版本取
`apps/ai-game-creator-shell/package.json`,通过 `version` 与清单版本比较;只有远端版本更高时显示更新提示。
本文件是 AGC 客户端自动更新的主规范:更新能力由 Tauri 官方插件 `tauri-plugin-updater` 承担,并按下文渠道分发。
清单格式:
## 目标
- 客户端自动更新改用 Tauri 官方 `tauri-plugin-updater`:清单请求、版本比较、更新包下载、签名校验、安装与退出全部在原生侧完成;前端只负责触发、展示和渠道选择。
- 更新按渠道分发。当前渠道集合为 `dev-win`Windows x64)与 `dev-mac`(macOS);构建管线按渠道产出并上传清单,客户端只读取自己渠道的清单。
- 更新链路的信任来源从「清单里的 sha256 + 受信域名」升级为「发布签名 + 受信域名」:清单里的 `signature` 由构建期私钥生成,客户端用内置公钥校验,校验不过就拒绝安装。
## 非目标
- 不做灰度放量、分批更新、强制更新和自动回滚;渠道只决定「取哪份清单」。
- 不做后台静默自动安装:是否下载安装始终由用户在更新提示里确认(仅「是否显示提示」受渠道与开发态开关影响)。
- 不支持应用商店分发(Microsoft Store / App Store)、移动端更新和企业内网自建更新服务。
- 不为自研 sha256 清单协议保留长期实现;迁移桥(见「契约与迁移」)只用于把已发布客户端带到新协议,随后整条删除。
## 入口与边界
- 用户入口:
- 客户端启动时在根窗口检查一次渠道清单,发现新版本时显示更新提示,用户可下载并安装。
- 运行时设置「关于」页提供手动检查更新(强制刷新)。
- 涉及模块:AGC 客户端(Rust `src-tauri`、前端 `src/`)、AGC 构建与发布脚本(`apps/ai-game-creator-shell/scripts/`)、Jenkins 发布流水线、OSS 对象布局。
- 正式状态来源:
- 客户端当前版本以 Tauri app version 为唯一权威来源(`tauri.conf.json`,由发布脚本与 `package.json``Cargo.toml``Cargo.lock` 同步递增)。
- 远端最新版本取自当前渠道的 `latest.json`
- 信任边界:清单地址在构建期确定并烘焙进产物;客户端不接受用户输入、后端响应或项目文件提供的更新地址,也不回退到其它渠道或旧协议地址。
## 必须成立的行为
### 正常路径
- 正式包启动时检查一次渠道清单;仅当清单版本高于当前版本时显示更新提示,提示包含目标版本与发布说明。
- 用户确认后下载更新包:下载期间显示进度与已下载字节数;下载完成后按平台安装。
- Windows 使用静默安装模式(NSIS `quiet`),安装启动成功后客户端退出并由安装程序重启新版本;macOS 由客户端在安装完成后重启进程接管新版本。
- 渠道在构建期确定并烘焙进产物:`dev-win` 产物只读 `dev-win` 清单,`dev-mac` 产物只读 `dev-mac` 清单,同一份二进制不会在运行期跨渠道切换。
- 开发态(`npm run agc` / `agc:serve` 由 Vite dev server 提供前端)不检查更新、不显示更新入口,也不下载任何更新包。
### 失败、重试与幂等
- 清单请求失败均静默忽略,不阻塞客户端启动:网络错误、TLS 错误、404(渠道尚未发布版本)、格式非法、渠道没有当前平台条目、远端版本不高于当前版本。
- 同一客户端生命周期内只自动检查一次;手动检查可强制刷新。
- 签名校验失败、下载中断或写入失败必须失败关闭:删除临时文件、不启动安装程序,并给出可读错误文案;不接受「校验失败但继续安装」。
- 重复点击下载或安装不产生并发安装;安装开始后客户端不再接受新的更新操作。
### 权限、归属与数据边界
- 更新能力通过 Tauri capability 显式授予客户端主窗口,其它窗口(调试窗口等)不得授予。
- 客户端只允许访问渠道清单声明的地址,只允许安装清单声明且签名校验通过的对象。
- 清单与安装包在 OSS 上保持公开可读;签名私钥与 OSS 凭据只存在于构建环境(Jenkins 凭据、本机发布配置),不写入仓库、日志、构建产物或客户端包。
- 客户端不记录更新地址以外的敏感信息;失败文案不回显凭据、绝对路径或响应正文。
## 契约与迁移
- 清单格式(Tauri updater v2):
```json
{
"version": "0.1.13",
"downloadUrl": "https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/0.1.13/Genarrative-AI-Game-Creator.exe",
"sha256": "<64位十六进制摘要>",
"size": 123456789,
"releaseNotes": "修复与改进"
"version": "0.1.48",
"notes": "发布说明,可为空",
"pub_date": "2026-09-17T00:00:00Z",
"platforms": {
"windows-x86_64": {
"signature": "<.sig 文件内容>",
"url": "https://<oss>/agc/dev-win/0.1.48/<安装包文件名>"
}
}
}
```
`downloadUrl` 必须是 HTTPS;如提供 `sha256` / `size`,Tauri 下载时会校验摘要和字节数。点击“下载更新”后,客户端将安装包流式写入系统临时目录并显示进度,校验成功后通过 Windows UAC 提权启动 NSIS 静默安装并退出旧客户端。
- 渠道与平台映射:
## 启动与失败策略
| 渠道 | 构建目标 | 清单平台键 | 更新包 | 清单地址 |
| --------- | ------------------------ | ---------------------------------------------- | ------------------------ | ------------------------------------ |
| `dev-win` | `x86_64-pc-windows-msvc` | `windows-x86_64` | NSIS `.exe` + `.exe.sig` | `<OSS base>/agc/dev-win/latest.json` |
| `dev-mac` | `universal-apple-darwin` | `darwin-aarch64` + `darwin-x86_64`(同一对象) | `*.app.tar.gz` + `.sig` | `<OSS base>/agc/dev-mac/latest.json` |
- 检查挂在 `WindowChrome` 根组件,覆盖首页、工作台和调试窗口;网络错误、格式错误或版本不高于当前版本均静默忽略,不阻塞客户端启动
- 更新请求使用单例 PromiseReact StrictMode 或同一窗口重复挂载不会重复请求
- Tauri HTTP capability 与 CSP 仅放行默认 OSS 域名;若更换域名,需同步更新 `capabilities/main.json``tauri.conf.json` 和发布环境配置
- 对象布局:清单固定写成 `agc/<channel>/latest.json`;安装包与签名写成 `agc/<channel>/<version>/<file>``<file>.sig`
- macOS 使用 universal 包:`dev-mac` 按 universal 目标构建(Intel 与 Apple Silicon 共用一个包),清单把同一个 `.app.tar.gz` 与同一个签名分别写入 `darwin-aarch64``darwin-x86_64`,升级后仍是 universal 包。这是 Tauri 官方发布工具对 universal 产物的既有写法
- 上一条的两个键不能合成单一 `darwin-universal` 键:更新插件按运行时实际架构解析清单键(Apple Silicon 命中 `darwin-aarch64`Intel 命中 `darwin-x86_64`),不存在自动命中 `darwin-universal` 的情形。将来真要单独发该键,必须在客户端同时设置自定义 target,否则清单里这一项永远不会被读取
- 构建期要求:打开 `bundle.createUpdaterArtifacts` 以生成 `.sig`;构建环境提供签名私钥与密码(私钥内容不得入库);公钥写入客户端配置。公钥在首个带更新能力的版本发布后不可更换,更换等于放弃自动更新(只能手动重装)。
- 版本递增按渠道独立进行:发布脚本读取该渠道远端 `latest.json``version`,与本地版本取较高者递增 patch;两个渠道的版本号互不影响。
- 迁移(旧协议 → 渠道清单):
- 迁移起点:已发布客户端(含当前线上版本)内置自研清单地址 `agc/latest.json`(sha256 格式),下载与安装由自研 Rust 命令完成。
- 迁移策略见「未决问题与决策」。迁移完成后,自研清单解析、下载命令、下载进度事件以及为此放行的 CSP / HTTP 白名单条目按「四不写」整条删除,不留兼容分支与墓碑说明。
## 发布约定
## 构建与发布
当前发布目标固定为 Windows x64 NSIS。执行 `npm run ai-game-creator-shell:build` 会先读取
`VITE_AGC_UPDATE_MANIFEST_URL`(默认 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/latest.json`)的
`latest.json`,取本地与 OSS 的较高版本并递增一个 patch,然后同步更新 package、Tauri 和 Cargo
版本后再向 Tauri 传入 `--target x86_64-pc-windows-msvc` 构建。OSS 清单首次不存在时按本地版本递增;
OSS 请求失败、清单格式错误或版本无效会终止发布,避免覆盖线上版本。构建完成后自动扫描 `.exe`
安装包,并在 `apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/latest.json`
生成包含版本、下载地址、大小和 SHA-256 的清单。可通过 `AGC_BUILD_TARGET` 显式覆盖目标(发布仍应使用
Windows x64),通过 `AGC_UPDATE_ARTIFACT` 指定要发布的安装包,通过 `AGC_UPDATE_OSS_BASE_URL` 指定
OSS 前缀,通过 `AGC_RELEASE_VERSION` 指定三段版本号(仅在明确需要复现指定版本时使用),通过
`AGC_UPDATE_RELEASE_NOTES` 写入发布说明,支持多行文本且保留内部换行;`--no-bundle` smoke 构建不会读取 OSS、修改版本或生成清单。
- 发布入口:`npm run ai-game-creator-shell:release:upload`(构建 + 按渠道上传);仅构建不发布的 smoke 使用 `--no-bundle` 分支,不读远端版本、不改版本、不生成清单。
- 渠道由构建参数显式指定,并按目标平台校验:Windows 目标只允许 `dev-win`macOS 目标只允许 `dev-mac`;未显式指定时按目标平台取默认渠道。
- 上传:安装包与 `.sig` 上传到 `agc/<channel>/<version>/`,清单以 `--force` 覆盖上传到 `agc/<channel>/latest.json`,保证 latest 指针与清单内 URL 指向已存在的对象。
- Jenkins 流水线需要新增渠道参数与签名凭据;签名私钥与密码只以受保护凭据注入当前进程,不写入 workspace、日志或归档产物。
- 归档证据:安装包、`.sig`、渠道清单与源码 commit。
每次发布安装包上传完成后,再使用 ossutil 的 `--force` 覆盖上传同一目录生成的 `latest.json`,确保固定的 latest 指针和 `downloadUrl` 指向已存在的 OSS 对象;未显式强制覆盖时,ossutil 在目标已存在时会交互询问并按默认值跳过,不能作为 Jenkins 非交互发布方式。清单和安装包均使用公开可读对象,不在清单中保存凭据、签名或本地路径。构建脚本本身不负责上传 OSS,发布流水线通过 `release:upload` 完成上传。
## 验收标准与证据
如需一键构建并上传,可执行 `npm run ai-game-creator-shell:release:upload`。该命令要求本机已安装并配置 `ossutil`
先按上述规则比较 OSS 版本、递增 patch、构建 Windows x64 NSIS,再上传安装包和 `latest.json`。默认上传到
`agc-dev` / `oss-rg-china-mainland.aliyuncs.com`,也可用 `AGC_OSS_BUCKET``AGC_OSS_ENDPOINT``OSSUTIL_BIN`
覆盖;本机执行时凭据由 ossutil 本机配置读取,不能写入仓库或命令行参数。
已获得的证据:
## Jenkins Windows 构建节点
| 条款 | 验收方式 | 证据 |
| ------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------------------- |
| 渠道与端点映射、渠道校验 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过(默认渠道、错配失败关闭、未知渠道失败关闭) |
| universal 包挂两个平台键 | 同上 + 本地发布烟测(伪造 bundle) | 通过(两键同 URL 同签名,不生成迁移清单) |
| 缺签名时失败关闭 | 同上 | 通过 |
| 开发态不检查更新 | `vitest run apps/ai-game-creator-shell/tests/appUpdate.test.ts` | 通过(开关关闭时不请求清单) |
| 旧自研链路整条删除 | 代码检索无残留命令、事件与白名单条目 | 通过(`download_agc_update` / 下载事件 / 清单常量均无残留) |
AGC 发布流水线使用 `jenkins/Jenkinsfile.ai-game-creator-shell-build`,当前节点标签为
`windows && win2022`。节点应为 Windows Server 2022 x64 虚拟机,预装 Node.js 22、npm
10.9.7、Rust 1.96.0、Visual Studio Build ToolsMSVC 与 Windows SDK)、Git 和 ossutil
Jenkins Agent 服务必须能在同一用户环境中找到这些命令。Tauri Windows bundler 使用
`tauri.windows.conf.json` 中的 `bundle.useLocalToolsDir: true`,把固定版本的 NSIS 工具缓存到
`src-tauri/target/.tauri/NSIS`,不依赖 Jenkins 服务账户的 `%LOCALAPPDATA%\tauri` 或 PATH 中的系统 NSIS。
Jenkins Checkout 的 `git clean -fdx` 会清理该构建目录,因此每次全新工作区可能重新下载 NSIS;这只影响构建耗时,不改变工具来源或执行权限要求。
流水线参数 `AGC_UPDATE_RELEASE_NOTES` 使用 Jenkins `text` 类型,可直接输入多行发布说明;执行根 workspace 的 `npm ci`,然后调用
`npm run ai-game-creator-shell:release:upload`,并归档 Windows 安装包、`latest.json` 与源码 commit。
流水线会将未导出的空参数按空字符串处理:`COMMIT_HASH` 留空时沿用 Jenkins SCM 当前提交,`OSSUTIL_BIN` 留空时使用节点 PATH 中的 `ossutil`,不会因 PowerShell 对空环境变量调用 `.Trim()` 而提前失败。
待执行证据(首次渠道发布后回填):
Jenkins Job 在“Build and upload”阶段通过受保护凭据 ID `AliyunAccessKeyId`
`AliyunaccessKeySecret` 注入 AccessKey,仅在当前进程运行时传给 ossutil,不写入仓库、workspace 或构建日志;
本机运行仍使用 ossutil 配置。凭据必须具备 `PutObject` 权限;OSS 对客户端保持公共读即可,公共读本身不授予
Jenkins 上传权限。由于版本号取决于 OSS 当前清单,Job 已关闭并发构建;若 Jenkins
上存在多个 AGC 发布 Job,还应使用同一个 Lockable Resource 串行化发布。Job 参数
`AGC_RELEASE_VERSION` 留空时自动递增,填写后会使用指定版本并更新对应的 `latest.json`,因此回滚或测试旧版本前应确认不会覆盖线上更新入口。
| 条款 | 验收方式 | 证据 |
| ---------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| 清单与对象布局符合渠道约定 | `ossutil ls oss://agc-dev/agc/dev-win/<version>/``ossutil cat .../dev-win/latest.json` | 待执行:URL 指向已存在安装包,签名与 `.sig` 内容一致 |
| 旧协议迁移桥 | `ossutil cat oss://agc-dev/agc/latest.json` | 待执行:`sha256` / `size` 与同一安装包匹配 |
| 真实更新闭环(含升级后重启) | 0.1.47 客户端升级到新版本,再启动不再提示;`npm run agc` 仍无更新入口 | 待执行 |
| 签名校验失败拒绝安装 | 渠道清单签名与实际安装包不匹配时的表现 | 待执行(需要真实渠道清单) |
## 未决问题与决策
已决策:
- macOS 采用 universal 包,同一产物同时挂 `darwin-aarch64``darwin-x86_64` 两个清单键(见「契约与迁移」)。
- 旧客户端迁移桥:保留一个版本周期。渠道清单上线后,发布管线同时把旧的 `agc/latest.json`sha256 格式)指向 `dev-win` 最新安装包,让已发布客户端自动升级到新协议;下个周期整条删除。
- 签名密钥:由本仓库维护者生成并保管,私钥保存在仓库外(`%USERPROFILE%\.tauri\genarrative-agc-updater.key`),只有公钥进入客户端配置;Jenkins 用受保护凭据 `AgcUpdaterSigningKey``AgcUpdaterSigningKeyPassword` 注入为 Tauri 打包器读取的 `TAURI_SIGNING_PRIVATE_KEY``TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,本机可用 `TAURI_SIGNING_PRIVATE_KEY_PATH` 指向同一私钥。当前密钥不带密码;首次发布前仍可重新生成,首次发布后不可更换。
- macOS 发布方式:`dev-mac` 产物在本机 mac 上执行发布入口上传,Jenkins 暂不新增 macOS 节点;macOS 代码签名与公证凭据未确认前,相关闭环记为未验证项,不静默通过。
待办:
- macOS `dev-mac` 渠道落地(macOS 构建机、签名与公证、安装后重启验证、是否接入 Jenkins macOS 节点)暂缓,由后续独立变更单独完成;在此之前 `dev-mac` 渠道只有构建与清单能力,不发布。