首页
/ LobeHub Desktop 本地更新测试指南:基于 stable/nightly/canary 三渠道的端到端验证方案

LobeHub Desktop 本地更新测试指南:基于 stable/nightly/canary 三渠道的端到端验证方案

2026-09-06 19:22:57作者:滑思眉Philip

本指南完整讲解 LobeHub Desktop 应用(位于 apps/desktop)如何在本机搭建一套"零成本"的更新链路测试环境:通过脚本在本地生成不同版本号的更新 manifest({channel}-mac.yml)并启动静态服务器模拟分发源,从而在接入真实发布渠道之前,端到端验证"发现新版本 → 切换渠道 → 降级回滚 → 下载失败"等核心更新场景。阅读并实操本文后,你将掌握 scripts/update-test 目录下全部脚本的用法、channel 切换的实现原理(基于 electron-updater 的 generic provider 与 setFeedURL),以及本地未签名构建在 macOS 下的验证边界与签名注意事项。

背景:Desktop 应用的多渠道更新体系

LobeHub Desktop 的自动更新由主进程中的 UpdaterManager 统一管理,它基于 electron-updater 封装。整个体系围绕三个关键点设计:

  1. 渠道(Channel)stable / nightly / canary 三档,各自对应不同的 feed URL 与 manifest 文件名(如 stable-mac.ymlnightly-mac.yml)。构建期的默认渠道由环境变量 UPDATE_CHANNEL 决定,运行时则可通过设置页切换并持久化到本地 store。
  2. 分发源(Feed):所有渠道统一走 generic HTTP provider,URL 规则为 {base}/{channel}/,其中 base 取自环境变量 UPDATE_SERVER_URL(生产环境形如 https://releases.lobehub.com)。
  3. 切换即重配:渠道一旦切换,UpdaterManager 会调用 configureUpdateProvider() 重新 setFeedURL(),并在下一次检查时读取对应渠道的 manifest。

apps/desktop/scripts/update-test 目录正是为这条更新链路量身打造的本机测试工具箱,其 README 即 apps/desktop/scripts/update-test/README.md。目录内文件与职责如下:

文件 职责
setup.sh 一键初始化:创建三渠道目录、示例 manifest 与本地测试配置模板
run-test.sh 一键启动测试(推荐),自动完成生成 manifest → 启动服务器 → 配置应用 → 启动应用
start-server.sh 在指定端口(默认 8787)后台启动本地静态服务器
stop-server.sh 停止本地服务器并清理 PID 文件
generate-manifest.sh 基于构建产物生成各渠道 manifest(含 SHA512、releaseNotes)
dev-app-update.local.yml 本地测试用的更新配置模板(generic provider 指向 http://localhost:8787/stable
server/ 服务器文件目录(自动生成,含 stable/nightly/canary/ 三个子目录及对应 manifest)

核心原理:channel 切换在源码中如何落地

要理解测试脚本为什么这样写,先看 UpdaterManager.ts 中与渠道强相关的两段实现。

feed URL 与渠道的绑定

configureUpdateProvider() 每次被调用时都会做三件事:将 base URL 中可能残留的渠道后缀剥掉、把当前渠道拼回 feed URL、再通过 setFeedURL 以 generic provider 形式写入:

private getBaseUpdateUrl(): string | undefined {
  if (!UPDATE_SERVER_URL) return undefined;
  return UPDATE_SERVER_URL.replace(/\/(stable|nightly|canary|beta)\/?$/, '');
}

private configureUpdateProvider() {
  const baseUrl = this.getBaseUpdateUrl();
  if (baseUrl) {
    const feedUrl = `${baseUrl}/${this.currentChannel}`;
    autoUpdater.channel = this.currentChannel;
    autoUpdater.setFeedURL({ provider: 'generic', url: feedUrl });
  } else {
    // 未配置 UPDATE_SERVER_URL 时回退到 GitHub provider(本地开发默认路径)
    autoUpdater.setFeedURL({ owner: 'lobehub', provider: 'github', repo: 'lobehub' });
  }
}

这正是文档中反复强调"必须设置 UPDATE_SERVER_URL 环境变量"的根本原因:一旦缺失,configureUpdateProvider() 会带着当前渠道走 GitHub 分支,本地测试就会去请求真实 GitHub Release 而非本地服务器,导致验证结果失真。同时,UPDATE_SERVER_URL 作为 base,与 manifest 文件名({channel}-mac.yml)共同决定了 feed 地址语义,例如 canary 渠道会去取 http://localhost:8787/canary/canary-mac.yml

升级与降级的自动判定

switchChannel() 是"设置 > Beta"页切换渠道时触发的逻辑。除了重配 provider,它还处理两个关键状态:

public switchChannel = (channel: UpdateChannel) => {
  this.currentChannel = channel;
  autoUpdater.allowPrerelease = channel !== 'stable';
  this.configureUpdateProvider();
  // configureUpdateProvider 内部的 channel setter 会副作用修改 allowDowngrade,
  // 因此这里必须重新置回 true,保证"从 canary 降回 stable"场景可被检测到
  autoUpdater.allowDowngrade = true;
  ...
};

从源码可以总结出渠道语义:

  • stable → nightly / canaryallowPrerelease 会被置为 true,应用以预发布身份去匹配带 -nightly.x / -canary.x 后缀的更高版本;
  • canary → stableallowPrerelease 变回 false,同时 allowDowngrade=true 让低版本号的 stable 包也能触发"降级更新"。

其余基础配置定义在 configs.ts

export const UPDATE_SERVER_URL = getDesktopEnv().UPDATE_SERVER_URL;

export const updaterConfig = {
  app: {
    autoCheckUpdate: true,
    autoDownloadUpdate: true,
    checkUpdateInterval: 60 * 60 * 1000, // 每小时自动检查一次
  },
  enableAppUpdate: !isDev, // 开发模式(isDev=true)下更新功能不初始化
};

由此可以推断两个测试约束:一是启动后约 60 秒会触发首次自动检查(setTimeout(() => this.checkForUpdates(), 60 * 1000));二是 enableAppUpdate: !isDev 意味着bun run dev 开发态下 updater 不会初始化,只能测 UI 与 IPC——这也是为什么下文区分"开发模式"与"打包模式"两种测试路径。

目录结构

脚本运行后会在 server/ 下自动生成以下结构(以仓库实际布局为准,完整路径为 apps/desktop/scripts/update-test/):

apps/desktop/scripts/update-test/
├── README.md                    # 本文对应的原始指南
├── setup.sh                     # 一键设置脚本
├── run-test.sh                  # 一键启动测试(推荐)
├── start-server.sh              # 启动本地更新服务器
├── stop-server.sh               # 停止本地更新服务器
├── generate-manifest.sh         # 生成 manifest 和目录结构
├── dev-app-update.local.yml     # 本地测试用的更新配置模板
└── server/                      # 本地服务器文件目录 (自动生成)
    ├── stable/                  # stable 渠道
    │   ├── stable-mac.yml
    │   └── {version}/
    │       ├── xxx.dmg
    │       └── xxx.zip
    ├── nightly/                 # nightly 渠道
    │   ├── nightly-mac.yml
    │   └── {version}/
    └── canary/                  # canary 渠道
        ├── canary-mac.yml
        └── {version}/

三个渠道目录内 {version}/ 存放的是 DMG/ZIP 安装包与 manifest 引用文件的宿主;manifest 中 files[].urlpath 均指向 {version}/xxx.dmg{version}/xxx.zip 这样的相对地址,因此把 server/ 整体交给任意静态文件服务器即可完成分发。

快速开始

以下命令均在仓库根目录执行。若目录结构未初始化,先进入目标目录并赋予脚本可执行权限:

cd apps/desktop/scripts/update-test
chmod +x *.sh

一键测试(推荐)

cd apps/desktop/scripts/update-test
./run-test.sh

阅读 run-test.sh 源码可见,它按固定顺序自动完成:为三渠道生成不同版本号的 manifest → 启动本地服务器 → 把 dev-app-update.local.yml 复制为 apps/desktop/dev-app-update.yml 完成应用配置 → 检查 macOS Gatekeeper 状态 → 询问是否启动打包后的应用。整个过程中无需手工干预生成与配置环节。

手动步骤(适合按需拆分执行)

1. 首次设置

cd apps/desktop/scripts/update-test
./setup.sh

setup.sh 会创建三渠道目录、写入三个 99.0.0 占位 manifest、并基于模板生成 dev-app-update.local.yml。该模板是 electron-updater 开发态配置:

provider: generic
url: http://localhost:8787/stable
updaterCacheDirName: lobehub-desktop-local-test
channel: stable

注意模板内的注释点明了它的边界:此文件只负责应用初始启动时的 provider 配置;运行时的 channel 切换走的是 UPDATE_SERVER_URL 环境变量 + setFeedURL() 这条链路,并不依赖此文件。

2. 构建测试包

cd apps/desktop
# 构建 DMG + ZIP (macOS 自动更新需要 ZIP)
bun run package:mac:local

注意: 不要使用 package:local。从 apps/desktop/package.json 的脚本定义可以看出区别:package:localelectron-builder --dir(只输出目录结构,不产出可被更新的 DMG/ZIP 安装包),而 package:mac:localelectron-builder --mac 真正产出 DMG。此外,package:mac:local 内部注入了 UPDATE_CHANNEL=nightly 并关闭公证与签名(--c.mac.notarize=false -c.mac.identity=null),这一点与后文 macOS 签名验证的边界直接相关。

3. 生成更新文件

cd apps/desktop/scripts/update-test

# 为所有渠道生成(推荐,会自动分配不同版本号)
./generate-manifest.sh --from-release --all-channels

# 或指定单个渠道
./generate-manifest.sh --from-release -c nightly -v 2.1.0-nightly.1

--from-release 模式下脚本会从 apps/desktop/release/ 自动探测第一个 *.dmg*-mac.zip(兼容 *.zip 命名),并尝试从 DMG 文件名中正则提取版本号(先匹配 x.y.z-(alpha|beta|rc|nightly|canary).n 形式,再退而匹配纯 x.y.z)。

4. 启动本地服务器

./start-server.sh
# 服务器默认在 http://localhost:8787 启动

start-server.sh 的实质是后台拉起静态服务器:

cd "$SERVER_DIR"
nohup npx serve -p "$PORT" --cors -n > "$LOG_FILE" 2>&1 &

其中 --cors 保证渲染进程/更新模块的跨源请求可用,-n 关闭自动列出目录;启动成功后 PID 记录在 .server.pid,日志在 .server.log。端口可通过 PORT 环境变量覆盖。

5. 启动应用(开发模式)

cd apps/desktop
UPDATE_SERVER_URL=http://localhost:8787 bun run dev

重要: 必须设置 UPDATE_SERVER_URL 环境变量,否则 channel 切换时 configureUpdateProvider() 会回退到 GitHub(原因见上文源码分析)。UpdaterCtr / UpdaterManagerisDevFORCE_DEV_UPDATE_CONFIG 为真时还会开启 autoUpdater.forceDevUpdateConfig = true,从而强制加载仓库根目录的 dev-app-update.yml(本地测试场景中它由脚本复制自 dev-app-update.local.yml)。

需要说明的是:dev 模式下 enableAppUpdate = !isDev = false,updater 不会真正初始化,因此这一路径主要用于验证设置页 UI、IPC 通信与日志输出;完整的"检查→下载"链路要在打包模式下验证。若想在打包产物中强制读取本地配置,可参考 run-test.sh 给出的启动方式:FORCE_DEV_UPDATE_CONFIG=true UPDATE_SERVER_URL=http://localhost:8787 open ".../LobeHub.app"

6. 测试 Channel 切换

  1. 进入 设置 > Beta
  2. Update Channel 下拉框中选择不同渠道
  3. 切换后应用会自动检查对应渠道的更新
  4. 查看日志确认 feed URL 切换正确:
tail -f ~/Library/Logs/lobehub-desktop-dev/main.log

(打包模式日志路径为 ~/Library/Logs/lobehub-desktop/main.log。)用 grep 过滤可关注的关键日志包括:

tail -f ~/Library/Logs/lobehub-desktop-dev/main.log | grep -E 'Switching|Configuring|channel|checking'
  • Channel 切换: Switching update channel: stable -> canary
  • Feed URL 切换: Configuring generic provider for canary channel
  • Manifest 匹配: Channel set to: canary (will look for canary-mac.yml)
  • 更新检测: Update available: x.y.zUpdate not available

切换的即时生效逻辑可回到源码印证:switchChannel() 通过自增的 checkGeneration 使在途检查失效(isStaleCheck() 会丢弃旧代结果),若当前无检查在跑则立即 checkForUpdates() 触发一次新检查。

7. 测试完成后

cd apps/desktop/scripts/update-test
./stop-server.sh

# 恢复默认的 dev-app-update.yml(可选)
cd apps/desktop
git checkout dev-app-update.yml

generate-manifest.sh 用法详解

generate-manifest.sh 负责产出形如 stable-mac.yml 的更新清单。完整参数如下:

用法: ./generate-manifest.sh [选项]

选项:
  -v, --version VERSION    指定版本号 (例如: 2.0.1)
  -c, --channel CHANNEL    指定渠道 (stable|nightly|canary, 默认: stable)
  -a, --all-channels       为所有渠道生成 manifest (stable/nightly/canary)
  -d, --dmg FILE           指定 DMG 文件名
  -z, --zip FILE           指定 ZIP 文件名
  -n, --notes TEXT         指定 release notes
  -f, --from-release       从 release 目录自动复制文件
  -h, --help               显示帮助信息

示例:
  ./generate-manifest.sh --from-release --all-channels
  ./generate-manifest.sh -v 2.0.1 -c stable --from-release
  ./generate-manifest.sh -v 2.1.0-nightly.1 -c nightly --from-release

生成逻辑与补充细节:

  • SHA512 计算:对真实文件执行 shasum -a 512 ... | xxd -r -p | base64(即 electron-updater 期望的 base64 编码哈希);文件不存在时写入 placeholder 占位,便于在无构建产物时先行验证 manifest 结构。
  • releaseDate:自动取当前 UTC 时间,格式化为 %Y-%m-%dT%H:%M:%S.000Z
  • --all-channels 版本编排:以基础版本为锚点——stable 用基础版本;nightly 取基础版本 patch+1 并追加 -nightly.<yyyyMMdd>;canary 取 patch+1 并追加 -canary.1。三者随附的 releaseNotes 会内置"测试要点"提示(如切回 stable 应触发降级、allowDowngrade 自动置 true 等),方便对照结果。
  • 生成时机建议:先构建(package:mac:local)再 --from-release,能拿到带真实哈希与文件大小的 manifest;只做 UI/IPC 冒烟时可先跑 setup.sh 生成 99.0.0 占位版本(均高于任何本地真实版本,保证"有新版本可用"场景成立)。

生成的 manifest 结构(以 stable 为例)形如:

version: 2.0.1
files:
  - url: 2.0.1/LobeHub-2.0.1-arm64.dmg
    sha512: <base64 sha512>
    size: 123456789
path: 2.0.1/LobeHub-2.0.1-arm64.dmg
sha512: <base64 sha512>
releaseDate: '2026-01-15T10:00:00.000Z'
releaseNotes: |
  ## v2.0.1 (Stable)
  ...

覆盖的测试场景矩阵

场景 操作
有新版本可用 manifest 中 version 大于当前应用版本
无新版本 version 小于或等于当前版本
Channel 切换(升级) 从 Stable 切到 Nightly/Canary,应检测到更高版本
Channel 切换(降级) 从 Canary 切到 Stable,allowDowngrade 应自动设为 true
下载失败 删除 server/{channel}/{version}/ 中的 DMG 文件
网络错误 停止本地服务器
Manifest 不存在 删除对应的 {channel}-mac.yml

从源码层面,"manifest 不存在"这类异常其实有专门的容错路径:UpdaterManager 的 isMissingUpdateManifestError() 会识别 cannot find ... 404 ... {channel}.yml 形态的错误,并将其按"暂无更新"处理(setStage('latest')),而不是弹错误框——本地测试时可以先删除某个渠道的 manifest 观察这种"优雅降级"行为。删除 DMG 文件的"下载失败"场景则可验证 error 事件分支:日志会输出错误上下文(channel、currentChannel、UPDATE_SERVER_URL 等),5 秒后 stage 回落到 idle。

关于 macOS 签名验证

Gatekeeper

本地测试的包未经签名和公证,macOS 会阻止运行。解决方法:

# 临时禁用 Gatekeeper(推荐,测试完成后务必重新启用)
sudo spctl --master-disable

# 测试完成后
sudo spctl --master-enable

或手动移除隔离属性:

xattr -cr /path/to/YourApp.app

run-test.sh 在打包模式启动前会自动探测 Gatekeeper 状态(spctl --status),若为 enabled 会给出警告并交互式确认,避免用户被"无法打开"卡住。

Squirrel.Mac 更新安装限制

本地未签名构建无法完成更新的安装步骤。 这是由 macOS 更新组件 Squirrel.Mac 的校验机制决定的:它要求更新包的签名与当前运行 app 的 designated requirement(DR)匹配;而 ad-hoc 签名的 DR 中包含 cdhash(二进制哈希),不同构建的哈希必然不同,因此校验必定失败。

由此可以明确本地测试的边界:

  • 能验证到"下载完成"为止:检测更新、切换 feed、下载包体(含下载进度广播)都可以完整走通;
  • 无法验证安装与重启:这一步依赖真实 Apple Developer 证书,仅在 CI 或具备证书的机器上存在有效签名,不存在此问题。

可验证的部分(通过日志)在上文"测试 Channel 切换"一节已列出,覆盖 channel 切换、feed URL 切换、manifest 匹配与更新可用性判定。

故障排除

1. Channel 切换后仍请求旧渠道

  • 确认启动应用时设置了 UPDATE_SERVER_URL=http://localhost:8787(未设置会回退 GitHub provider);
  • 查看日志确认 configureUpdateProvider 被调用:
grep 'Configuring generic' ~/Library/Logs/lobehub-desktop-dev/main.log

2. 更新检测不到

  • 确认对应渠道的 manifest 存在:
curl http://localhost:8787/stable/stable-mac.yml
  • 确认 manifest 中的版本号大于当前应用版本;
  • 结合 configs.tsUPDATE_CHANNEL 的归一化规则(只有 canary / beta 会被判定为 canary,其余归为 stable)核对当前渠道是否与 manifest 一致。

3. 服务器启动失败

# 检查端口是否被占用
lsof -i :8787

# 使用其他端口(start-server.sh 与 run-test.sh 均读取 PORT 环境变量)
PORT=9000 ./start-server.sh

若残留旧进程,可先 ./stop-server.sh(其实现会读 .server.pid,先 kill 再兜底 kill -9,最后清理 PID 文件)。

注意事项

⚠️ 安全提醒

  1. 测试完成后务必重新启用 Gatekeeper(sudo spctl --master-enable),避免系统持续处于降低防护的状态;
  2. 这些脚本仅用于本地开发测试,切勿在生产或共享环境沿用其中的占位 manifest 与未签名产物;
  3. 不要将未签名的包分发给其他用户——它既无法通过 Gatekeeper,也无法被 Squirrel.Mac 正常安装,只会制造困惑。

小结:测试链路与源码的对应关系

测试目标 操作入口 对应源码位置
初始 feed 指向本地 UPDATE_SERVER_URL + dev-app-update.local.yml configs.tsUpdaterManager.configureUpdateProvider
渠道升级检测 设置 > Beta 切换 + --all-channels 高版本 manifest switchChannelallowPrerelease
渠道降级回滚 canary → stable,版本号回落 allowDowngrade = true(切换后重设)
manifest 缺失/网络错误 删 manifest / stop-server.sh isMissingUpdateManifestError 容错分支

借助这套脚本,开发者可以像 CI 一样在提交前快速回归更新模块的核心行为,而无需触碰真实发布服务器;理解 manifest 与 provider 的绑定关系后,也可以将其平移到非 macOS 或私有对象存储的测试场景中复用。

登录后查看全文
热门项目推荐
相关项目推荐