本页目录(8 节)

自动更新

Spark Work 桌面端自动更新:electron-builder + GitHub Release + 官网版本中心(www.yiqibyte.com)、UpdateService 检查/下载/安装状态、应用内更新入口、stable/beta 通道、Windows / macOS 签名构建。

阅读约 8 分钟最后核对 2026-06-29

SparkWork 桌面端的更新链路有三个真实参与方:GitHub Actions 负责构建与发布, 对象存储(MinIO / S3 兼容)承载安装包,官网版本中心(edu-server 的 /api/v1/desktop/releases/*)提供应用内更新所需的元数据;GitHub Releases 是回退源。 应用内更新由主进程的 UpdateService 实现,不走 electron-updater: 它自己拉元数据、自己下载、自己用 SHA 校验、最后调用系统方式打开安装包。

1. 整体链路一览

环节真实实现产物/接口
CI 构建与发布.github/workflows/publish-desktop-release.ymlGitHub Release v<version> + 平台安装包(同时上传为 5 天有效的 workflow artifacts)
安装包归档workflow 的 publish-to-official-website job(需要 official-website-production 环境审批)aws s3 cp 到 s3://<bucket>/stable/<version>/(dmg/exe/AppImage/zip/deb/rpm/blockmap)
元数据登记apps/desktop/scripts/register-release.mjsPOST {RELEASE_API_BASE}/api/v1/ci/desktop/releases/register(头 X-Release-Token)
应用内检查apps/desktop/src/main/services/UpdateService.tsGET /api/v1/desktop/releases/latest?channel=&platform=&arch=,失败回退 GitHub REST API
官网下载按钮apps/website/src/lib/releases.ts + apps/website/scripts/fetch-downloads.mjs运行时读同一个 /latest 接口,构建期烘焙快照到 src/content/downloads.generated.json

2. 发布流程:workflow 的真实触发与结构

触发条件(比「只有 apps/desktop/package.json 变更才触发」更宽):

  • push 到 master,且改动命中这些路径之一:apps/desktop/package.json、 根 package.json、.nvmrc、.node-version、.github/workflows/publish-desktop-release.yml 自身。
  • 手动 workflow_dispatch,输入:platforms(all / mac / win / mac-arm64 / mac-x64 / win-x64)、publish_to_release(默认 false,即只构建+签名验证)、optional_capabilities_only(只构建深度可选能力包)、ref。

prepare job(判断是否真的要发布):

  1. 版本号取自 apps/desktop/package.json 的 version,tag 为 v<version>。
  2. push 触发时会比对 github.event.before 那个提交里的版本号;版本号没变就直接结束(should_publish=false)。
  3. 若同名 tag 已存在且指向别的提交,直接报错退出:「请提升版本号,禁止把新产物发布到旧标签」。
  4. 更新说明由 scripts/release-notes.mjs 从 CHANGELOG.md 生成; 缺少该版本条目时允许为空,流程继续。
  5. GitHub Release 由这一个 job 预先创建/更新(--latest);不能让多个 matrix job 并发创建, 否则会撞 422 already_exists。

构建矩阵(push 默认全平台):

targetrunner产物
mac-arm64macos-latestarm64 dmg
mac-x64macos-26-intelx64 dmg(Intel runner + Xcode 26)
win-x64windows-2022x64 exe(NSIS 安装器)

构建步骤里的硬性要求:

  • pnpm 固定 11.13.0,Node 用 .nvmrc,安装用 --frozen-lockfile。
  • 先跑 pnpm run check:codex-protocol 校验 Codex App Server 协议兼容性。
  • macOS runner 需要 Python 3.11(node-gyp 9.x 依赖 distutils)。
  • macOS 先用 apps/desktop/scripts/import-cert-ci.sh 把 Developer ID Application 证书导入临时钥匙串, 并立即用 security find-identity -v -p codesigning | grep "Developer ID Application" 校验, 证书误配会在构建早期直接失败。
  • Windows 构建带 REQUIRE_WINDOWS_SIGNING=1 与 ALLOW_UNSIGNED_WINDOWS_RELEASE=0, 正式发布必须有证书。
  • renderer 打包内存:NODE_OPTIONS=--max-old-space-size=8192。
  • 实际打包分别走 apps/desktop/scripts/build-mac-release.sh 与 apps/desktop/scripts/build-win-release.sh;macOS 构建只对两类瞬时故障重试一次 (dmg 卸载 Resource busy、Apple 侧网络错误)。

Release 发布后(或手动触发且勾选 publish_to_release)才进入 publish-to-official-website job:它挂在 official-website-production 环境上,一次人工审批同时控制 MinIO 上传与版本中心登记。该 job 会先检查 6 个 secrets 是否齐备(RELEASE_API_BASE、RELEASE_CI_TOKEN、RELEASE_MINIO_ENDPOINT、RELEASE_MINIO_BUCKET、RELEASE_MINIO_ACCESS_KEY、RELEASE_MINIO_SECRET_KEY),缺一个就失败。

3. 官网版本中心:登记什么、读取什么

登记(CI → 官网):register-release.mjs 的行为可以逐条核对:

  • 环境变量:VERSION、PLATFORM、ARCH、RELEASE_API_BASE、RELEASE_CI_TOKEN、可选 CHANNEL(默认 stable)、DIST_DIR(默认 apps/desktop/dist)、RELEASE_OBJECT_PREFIX(默认 <channel>/<version>)、RELEASE_AUTO_PUBLISH(设为 false 时发布为未自动上线)。
  • 安装包匹配规则:文件名同时满足「含 -<platform>-<arch>.」与扩展名属于 [.dmg, .exe, .appimage, .zip, .deb, .rpm];匹配不到直接报错并打印 dist 目录内容。
  • 每个文件登记 platform、arch、fileName、fileSize、sha512(Base64)、objectKey、blockmapKey。
  • 请求:POST {apiBase}/api/v1/ci/desktop/releases/register,头X-Release-Token,body {version, channel, files, releaseNotes, autoPublish}; 失败最多重试 3 次(2s、4s 退避),响应 code !== 0 视为失败。
  • workflow 会按矩阵逐个平台/架构调用这个脚本,所以一次发布会登记多条文件记录。

读取(官网页面):

  • 运行时:apps/website/src/lib/releases.ts 请求 /api/v1/desktop/releases/latest?channel=stable;基地址优先取 VITE_RELEASES_API_BASE,没有配置时用同源 window.location.origin。 返回结构是 {code, message, data: LatestRelease | LatestRelease[] | null}, 字段为 version/channel/platform/arch/fileName/fileSize/publicUrl/releaseNotes/publishedAt。
  • 构建期:pnpm --filter @spark/website prebuild 会跑 scripts/fetch-downloads.mjs 把同一个接口的结果写进 src/content/downloads.generated.json;页面首屏用这份快照渲染(无闪烁), 后台再拉一次实时数据替换,接口失败则继续用快照。

4. 应用内更新:状态机、节奏与缓存

状态枚举(UpdateStatusState):idle → checking → available → downloading → downloaded, 以及 not-available 与 error。UpdateStatus 里还会带 currentVersion、updateInfo、progress、lastCheckedAt、updateSource(version-center / github)与 downloadSource。

检查节奏(真实常量):

行为取值说明
启动检查延迟5 秒避免影响首屏
常规检查间隔30 分钟静默执行(自动检查失败不弹提示)
抖动±5 分钟避免客户端集中请求
失败退避30 分钟 × 2ⁿ,上限 2 小时连续失败时指数退避
聚焦/唤醒补查距上次检查超过 30 分钟才补browser-window-focus 与 powerMonitor 的 resume 触发
手动检查失败仍会显示错误只有自动检查静默

下载与安装:

  • 默认 autoDownload: false:发现新版本先进入 available,由用户点「下载更新」。 打开「自动下载」后才会在检查时自动下载。
  • 下载落在 {userData}/spark-agent-updater/<version>/<assetName>; 应用启动时会清理缓存,只保留最近 2 个版本目录(按目录修改时间)。命中缓存则直接进入 downloaded,不重复下载。
  • 校验:元数据带 digest(版本中心映射成 sha512:<base64>, GitHub 资产可能是 sha256:<hex>)时端到端校验,不匹配报「更新包 SHA…完整性校验失败」; 旧版本资产没有 digest 时只记录警告并继续(兼容行为)。
  • 下载完成 → 系统通知 + 对话框。macOS 按钮是「打开安装镜像」,说明是「打开 dmg 后请将应用拖到 Applications 替换现有版本」, 随后应用延迟 1.2 秒退出;Windows 按钮是「安装更新」,直接启动 exe 安装器。
  • Windows 独有「自动安装」:autoInstall: true 且状态为 downloaded 时, 退出应用会自动启动安装器(其他平台该设置被强制为 false)。
  • 没有增量更新:blockmap 会被上传与登记,但更新服务不消费它—— 每次都是下载完整 dmg/exe(资产筛选时还会显式排除 *.dmg.blockmap / *.exe.blockmap)。

IPC 与界面:

  • 通道:update:check、update:download、update:install-restart、update:get-status、update:settings(可改 autoCheck / autoDownload / autoInstall / channel)。
  • 推送:stream:update:status 为主,另有 stream:update:available、stream:update:progress、stream:update:downloaded。
  • 设置 → 更新:状态卡(含下载进度条与操作按钮)、更新内容卡(超过 1200 字符可折叠)、 「打开 Release 页」、「更新策略」四项(自动检查更新 / 自动下载 / 自动安装 / 更新来源)、版本卡。 「更新来源」会写明检查顺序「官网版本中心 → GitHub Releases」并显示当前实际来源 (界面文案分别是 官网 / GitHub / 待检查)。
  • 侧边栏头部图标按钮:只在非 idle 状态出现——available 点击即下载、downloading 点击跳到设置页、downloaded 点击安装、error/checking 有对应图标;tooltip 会显示「来源:官网版本中心 / GitHub Releases」。
  • 偏好与上次检查时间持久化在 app_settings 的 updates/data 与 updates/lastChecked。

5. 更新源选择、回退与错误文案

  1. 先请求版本中心:GET {releasesApiBase}/api/v1/desktop/releases/latest?channel=&platform=&arch=。releasesApiBase 默认 https://www.yiqibyte.com, 可被 dev-app-update.yml / app-update.yml 覆盖。请求头 user-agent: Spark-Agent-Updater。
  2. 版本中心不可用(网络失败、code !== 0、非 2xx)时记录警告并回退 GitHub:stable 通道读 /releases/latest;beta 通道读 /releases?per_page=20 并取第一个非 draft。
  3. 版本比较用 tag 规范化后的版本号:不大于当前版本 → not-available。 平台资产筛选规则:macOS 只接受 .dmg,按当前架构优先(arm64 优先含 arm64 的包, x64 优先含 x64 的包,fallback universal);Windows 只接受 .exe,含 setup 加权、含 portable 降权。找不到合适资产时报「当前 Release 缺少适用于 macOS/Windows 的安装包」。
  4. GitHub 未认证请求会撞 rate limit:服务会解析 retry-after / x-ratelimit-reset 并在提示里给出重置时间;频繁点「检查更新」时请等待或改用带 token 的配置。

配置来源与优先级(用于排查「为什么读到的是另一份配置」):

  • 打包版读 {resourcesPath}/app-update.yml(由 electron-builder 依据 electron-builder.yml 的 publish 段生成:github / owner alexanderizh / repo spark-agent / releaseType release)。
  • 开发版按顺序找:当前工作目录的 dev-app-update.yml、apps/desktop/dev-app-update.yml 等候选路径;仓库里的这份内容是 provider: github、updaterCacheDirName: spark-agent-dev-updater、releasesApiBase: https://www.yiqibyte.com。
  • 可识别字段:owner、repo、updaterCacheDirName、token(用于提高 GitHub 限额,仅本地开发用)、releasesApiBase。都找不到时用内置默认值。

6. 签名、公证与原生模块要求

  • electron-builder.yml:appId: com.spark-agent.desktop、productName: Spark Agent、artifactName: ${productName}-${version}-${os}-${arch}.${ext}、electronVersion: 43.2.0、mac.target: dmg、hardenedRuntime: true、identity: 'Developer ID Application'、公证交给 afterSign: scripts/notarize.js。 协议 scheme 注册为 spark-agent。
  • macOS 还需要 APPLE_ID、APPLE_APP_SPECIFIC_PASSWORD、APPLE_TEAM_ID;build-mac-release.sh 会在启动前检查这三个变量、 keychain 里的 Developer ID 证书以及 xcrun notarytool 可用性。
  • Windows:signtoolOptions 里是 SHA-256 + RFC3161 时间戳 (http://timestamp.digicert.com),signExts: [.exe]。 CI 传 WIN_CSC_LINK / WIN_CSC_KEY_PASSWORD;build-win-release.sh 支持本地 .pfx 路径、http(s)、data URL 或 base64 内容, 并把证书写到临时目录、构建结束自动清理。
  • 签名校验:有证书时脚本要求最终 .exe 的 Authenticode 状态是 Valid; 正式发布路径(REQUIRE_WINDOWS_SIGNING=1)缺少证书会直接失败; 只传了一半凭据也会失败(除非显式允许未签名)。
  • 原生模块:better-sqlite3、keytar、node-pty 必须按目标架构重建到 Electron ABI。 打包脚本调用 pnpm run rebuild:native -- <arch>,宿主架构与目标一致时会继续跑 native:verify(真实用 Electron 加载一次);不一致时跳过并提示「在目标架构 runner 上构建」。
  • electron-builder.yml 关闭了自带 native rebuild(npmRebuild: false), 原因写在配置注释里:node-pty@1.1.0 的 install 脚本会跑 tsc 编译测试文件, 类型检查失败会拖垮整个 rebuild。

7. 本地构建与调试

常用命令(都在 apps/desktop 下,或直接用脚本路径):

# 开发模式(更新检查/下载链路同样可用)
pnpm --filter @spark/desktop dev

# macOS 单架构发布构建(内部会先 rebuild:native 再 electron-builder)
pnpm --filter @spark/desktop build:mac:arm64
pnpm --filter @spark/desktop build:mac:x64

# Windows 发布构建(不发布到 Release)
pnpm --filter @spark/desktop build:win:release -- --publish never
# 或者从仓库根目录:
bash apps/desktop/scripts/build-win-release.sh x64 --publish never

# 带本地证书的 Windows 构建
WIN_CSC_LINK=/path/to/cert.pfx \
WIN_CSC_KEY_PASSWORD=your-pfx-password \
pnpm --filter @spark/desktop build:win:release -- --publish never

调试要点:

  • dev-app-update.yml 会覆盖仓库/缓存目录/版本中心地址;如果要绕过 GitHub 限额, 可以在里面加一个仅供开发用的 token。
  • 本地想验证「版本中心 → 官网下载按钮」链路时,设置 VITE_RELEASES_API_BASE(运行时) 与 RELEASES_API_BASE(prebuild 拉快照)指向你的 edu-server 实例。
  • 可观测点:设置 → 更新 的状态卡与「更新来源」、会话/主窗口里的 stream:update:status 事件、侧边栏更新按钮 tooltip、以及 {userData}/spark-agent-updater/ 下的缓存目录。
  • 远端 Release 缺少对应平台安装包时,本地也会收到同一错误文案(例如「当前 Release 缺少适用于 macOS 的安装包」)。

8. 通道、设置与常见坑

通道类型是 UpdateChannel = 'stable' | 'beta',默认 stable,可通过 update:settings 修改并持久化在 updates/data。需要说清楚的是:当前设置 → 更新 页面没有通道选择器,只有「自动检查更新 / 自动下载 / 自动安装 / 更新来源」四项, 以及检查、下载、安装三个动作按钮;也就是说 beta 在协议与检查逻辑上可用,但普通用户界面上切不了。

常见坑:

  • 「自动安装」在 macOS 上打开又自己关掉:这是设计——保存偏好时会强制把非 Windows 平台的 autoInstall 置回 false(界面提示「当前平台不支持自动安装,下载后需手动打开安装包」)。
  • 以为发的是增量包:blockmap 只被登记,不会被消费;用户每次都下完整安装包。
  • 改了 CHANGELOG.md 却没触发发布:触发路径里没有 CHANGELOG, 它只影响 release notes 内容;另外版本号不变时 workflow 会判定不发布。
  • tag 已存在但指向旧提交:workflow 会直接失败并要求提升版本号,这是防止「新产物挂到旧标签」的保护。
  • Release 里能看到安装包,但应用内检查不到:应用内优先读版本中心; 如果版本中心登记失败(缺 secrets 或审批未通过),要么回退 GitHub 成功、要么提示缺少平台资产。 先看 GitHub Release 里资产名是否包含 -<platform>-<arch>.(例如 -mac-arm64.dmg),这是登记与筛选共用的命名约定。
  • 担心缓存越滚越大:更新器启动时只保留最近 2 个版本目录,其余自动回收; 想手动清理可以删掉 {userData}/spark-agent-updater/。
  • Windows 未签名包:本地构建允许产出未签名 exe(会有 SmartScreen 警告), 但 CI 正式发布路径要求签名;判据就是脚本对 Authenticode 状态是否为 Valid 的校验。

速查表

发布源
GitHub Release + MinIO 对象存储 + 官网版本中心(GitHub 为回退源)
登记接口
POST /api/v1/ci/desktop/releases/register(头 X-Release-Token)
检查接口
GET /api/v1/desktop/releases/latest?channel=&platform=&arch=
版本中心默认地址
https://www.yiqibyte.com
对象存储路径
s3://<bucket>/stable/<version>/(dmg/exe/AppImage/zip/deb/rpm/blockmap)
构建矩阵
mac-arm64(macos-latest)· mac-x64(macos-26-intel)· win-x64(windows-2022)
检查节奏
启动延迟 5 秒 · 常规 30 分钟 ± 5 分钟抖动 · 失败退避上限 2 小时
更新缓存
{userData}/spark-agent-updater/<version>/,保留最近 2 个版本
IPC
update:check / download / install-restart / get-status / settings
推送
stream:update:status(另有 available / progress / downloaded)
原生模块
better-sqlite3 · keytar · node-pty(pnpm run rebuild:native -- <arch> + native:verify)
macOS 签名
Developer ID Application + hardenedRuntime + afterSign 公证(notarize.js)
Windows 签名
WIN_CSC_LINK / WIN_CSC_KEY_PASSWORD,SHA-256 + RFC3161 时间戳

常见问题

展开 6 条常见问题
  • GitHub Release 是唯一发布源吗?

    不是。CI 会同时把安装包上传到对象存储(MinIO/S3 兼容桶的 stable/<version>/)并把元数据登记到官网版本中心;应用内更新优先读版本中心,失败才回退 GitHub Releases API。

  • 为什么我只改了版本号却要先人工审批?

    electron-builder 推完 GitHub Release 后,publish-to-official-website job 挂在 official-website-production 环境上,required reviewer 会拦一次;这次审批同时控制 MinIO 上传与版本中心登记,所以正式分发路径需要人工放行。

  • 应用内更新是增量下载吗?

    不是。blockmap 会被上传与登记,但 UpdateService 不消费它,资产筛选中还会显式排除 *.blockmap,因此每次都是完整安装包。

  • 自动检查更新会打扰我吗?

    不会弹提示。启动后延迟 5 秒检查一次,之后约每 30 分钟静默检查(±5 分钟抖动),失败按 30 分钟 × 2ⁿ 退避、上限 2 小时;只有手动检查失败才会显示错误。

  • 能切到 beta 通道吗?

    协议与检查逻辑支持 stable/beta(beta 会遍历 /releases?per_page=20 取第一个非 draft),也能通过 update:settings 修改并持久化;但当前设置 → 更新 页面没有通道选择器,界面上切不了。

  • Windows 一定要证书吗?

    本地构建可以不带证书(产出未签名 exe,会有 SmartScreen 警告);CI 正式发布路径设置了 REQUIRE_WINDOWS_SIGNING=1 且不允许未签名,脚本还会要求最终 exe 的 Authenticode 状态为 Valid。