LobeHub Desktop 本地更新测试指南:基于 stable/nightly/canary 三渠道的端到端验证方案
【免费下载链接】lobehub🤯 LobeHub is your Chief Agent Operator, organizing your agents into 7×24 operations by hiring, scheduling, and reporting on your entire AI team.项目地址: https://gitcode.com/GitHub_Trending/lo/lobehub
本指南完整讲解 LobeHub Desktop 应用(位于apps/desktop)如何在本机搭建一套"零成本"的更新链路测试环境:通过脚本在本地生成不同版本号的更新 manifest({channel}-mac.yml)并启动静态服务器模拟分发源,从而在接入真实发布渠道之前,端到端验证"发现新版本 → 切换渠道 → 降级回滚 → 下载失败"等核心更新场景。阅读并实操本文后,你将掌握scripts/update-test目录下全部脚本的用法、channel 切换的实现原理(基于electron-updater的 generic provider 与setFeedURL),以及本地未签名构建在 macOS 下的验证边界与签名注意事项。
背景:Desktop 应用的多渠道更新体系
LobeHub Desktop 的自动更新由主进程中的 UpdaterManager 统一管理,它基于electron-updater封装。整个体系围绕三个关键点设计:
- 渠道(Channel):
stable/nightly/canary三档,各自对应不同的 feed URL 与 manifest 文件名(如stable-mac.yml、nightly-mac.yml)。构建期的默认渠道由环境变量UPDATE_CHANNEL决定,运行时则可通过设置页切换并持久化到本地 store。 - 分发源(Feed):所有渠道统一走 generic HTTP provider,URL 规则为
{base}/{channel}/,其中 base 取自环境变量UPDATE_SERVER_URL(生产环境形如https://releases.lobehub.com)。 - 切换即重配:渠道一旦切换,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 / canary:
allowPrerelease会被置为true,应用以预发布身份去匹配带-nightly.x/-canary.x后缀的更高版本; - canary → stable:
allowPrerelease变回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[].url与path均指向{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.shsetup.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:local走electron-builder --dir(只输出目录结构,不产出可被更新的 DMG/ZIP 安装包),而package:mac:local走electron-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 / UpdaterManager 在isDev或FORCE_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 切换
- 进入设置 > Beta
- 在Update Channel下拉框中选择不同渠道
- 切换后应用会自动检查对应渠道的更新
- 查看日志确认 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.z或Update 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.ymlgenerate-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.apprun-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.log2. 更新检测不到
- 确认对应渠道的 manifest 存在:
curl http://localhost:8787/stable/stable-mac.yml- 确认 manifest 中的版本号大于当前应用版本;
- 结合 configs.ts 中
UPDATE_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 文件)。
注意事项
⚠️安全提醒:
- 测试完成后务必重新启用 Gatekeeper(
sudo spctl --master-enable),避免系统持续处于降低防护的状态; - 这些脚本仅用于本地开发测试,切勿在生产或共享环境沿用其中的占位 manifest 与未签名产物;
- 不要将未签名的包分发给其他用户——它既无法通过 Gatekeeper,也无法被 Squirrel.Mac 正常安装,只会制造困惑。
小结:测试链路与源码的对应关系
| 测试目标 | 操作入口 | 对应源码位置 |
|---|---|---|
| 初始 feed 指向本地 | UPDATE_SERVER_URL+dev-app-update.local.yml | configs.ts、UpdaterManager.configureUpdateProvider |
| 渠道升级检测 | 设置 > Beta 切换 +--all-channels高版本 manifest | switchChannel、allowPrerelease |
| 渠道降级回滚 | canary → stable,版本号回落 | allowDowngrade = true(切换后重设) |
| manifest 缺失/网络错误 | 删 manifest /stop-server.sh | isMissingUpdateManifestError容错分支 |
借助这套脚本,开发者可以像 CI 一样在提交前快速回归更新模块的核心行为,而无需触碰真实发布服务器;理解 manifest 与 provider 的绑定关系后,也可以将其平移到非 macOS 或私有对象存储的测试场景中复用。
【免费下载链接】lobehub🤯 LobeHub is your Chief Agent Operator, organizing your agents into 7×24 operations by hiring, scheduling, and reporting on your entire AI team.项目地址: https://gitcode.com/GitHub_Trending/lo/lobehub
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考