LobeHub Desktop 本地更新测试指南:基于 stable/nightly/canary 三渠道的端到端验证方案
本指南完整讲解 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.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: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.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.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 或私有对象存储的测试场景中复用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00