OpenProgram 正式版本更新设计
本文定义普通 Desktop、CLI/server 与源码开发安装如何发现和应用新版本。稳定安装只消费完整、已发布、已验证的 GitHub Release;GitHub 仓库出现新提交不等于可安装更新。
1. 已确定的设计约束
stable只解析 GitHub 上最新的非 draft、非 prerelease 正式 Release。普通用户永远不跟踪main、任意 commit 或未完成的 tag。- 更新单位是完整平台产物,不做 Python wheel、Web、Program、OCR、Browser 或模型的局部产品更新。新版本必须继续满足统一 capability manifest。
- 默认自动检查,首次启动延迟执行;成功检查后每 24 小时检查一次,失败后最早 6 小时重试。网络错误不影响应用启动,也不循环弹窗。
- Desktop 与 CLI/server 共用版本解析、Release 选择和 checksum 规则,但安装动作按产物不同实现。
- 当前 macOS Desktop 使用 unsigned DMG。应用内负责检查、架构选择、完整下载和 SHA-256 校验,并在校验成功后打开 DMG;不实现自定义后台进程替换正在运行的
.app。 - macOS/Linux CLI/server 复用现有 release installer:下载到新版本目录,验证完整 runtime 和 worker cold start,成功后原子切换
current,失败时保留原版本。 - 源码开发安装继续使用显式 Git ref 的
openprogram upgrade流程。设置页不能把源码 checkout 更新解释为普通稳定更新。 - 当前只提供一个
stable通道,不显示没有实际行为差异的通道选择器。 - 删除 worker 启动时的“检查并自动应用”行为。
OPENPROGRAM_IMMUTABLE_RUNTIME=1的 managed runtime 和 Desktop 不允许进入旧的 Git/PyPI updater;openprogram update只保留为openprogram upgrade的兼容入口。 - 统一安装类型只有
managed_release、source_checkout和unknown。检测时OPENPROGRAM_IMMUTABLE_RUNTIME=1优先于 site-packages 等路径启发式规则;正式产品不得显示或使用pip_wheel更新语义。
2. 实施前基线与当前实现
| 范围 | v0.6.6 基线 | 当前实现 |
|---|---|---|
| Release | 已包含 macOS arm64/x64 unsigned DMG/ZIP、macOS/Linux 完整 runtime、各 runtime checksum 与 release-manifest.json。 | 客户端解析 /repos/Fzkuji/OpenProgram/releases/latest,拒绝 draft、prerelease、重复/缺失资产、非 vX.Y.Z tag 和 manifest 不一致的 Release。 |
| Desktop | 没有更新 IPC、定期检查或下载校验。 | 使用 app.getVersion();主进程实现检查、持久化、调度、下载与 SHA-256 校验,preload 只暴露无 URL 参数的动作,设置页显示状态、发布日期、notes 和字节进度。 |
| CLI/server | 版本化 installer 已完成 checksum、capability manifest 与 cold start。 | managed release 的 openprogram upgrade 严格验证 Release manifest 后执行不可变 tag installer;launcher 准备完成后以 os.replace 原子替换已有 current。 |
| 旧后台 updater | worker 会自动进入 Git/PyPI 更新路径。 | 后台 product apply 与 PyPI/Git 更新实现已删除;immutable runtime 只允许正式 Release 路径,源码 checkout 只允许显式 upgrade。 |
| 升级文档 | Desktop 仅说明手动下载 DMG,CLI 说明重新运行公开安装命令。 | 双语文档说明 v0.6.6 的一次性过渡、自动检查、校验下载、Desktop 安装交接和 CLI 原子升级边界。 |
3. 官方规范与方案比较
| 参考 | 明确能力/约束 | 采用方式 |
|---|---|---|
| Electron autoUpdater | macOS 基于 Squirrel.Mac,原生自动更新要求应用已签名;Electron 本身不为 Linux 提供内置 updater。 | 当前不采用安装模块。保留其事件状态模型,但不调用无法满足当前产物约束的 quitAndInstall()。 |
| Electron Updating Applications | 正式更新依赖平台产物、更新 metadata、检查/下载/重启事件;官方 GitHub 服务也把 macOS 签名列为条件。 | 修改采用。GitHub Release 作为 metadata 和下载源;Desktop 停在经过校验的 DMG 打开步骤。 |
| electron-builder Auto Update | 可生成 latest-mac.yml 并由 electron-updater 管理下载,但 macOS 自动更新同样要求签名。 | 拒绝新增依赖。现有 Release API、manifest 和 Node/Electron API 足够完成本阶段,不引入未能执行安装的 updater 依赖。 |
| GitHub Releases API | latest endpoint 返回最新发布的正式 Release,并排除 draft/prerelease;响应包含 tag、正文和 assets。 | 采用。客户端仍再次校验字段和版本格式,并按确切文件名匹配资产。 |
| 自定义 Desktop 替换 helper | 可等待应用退出后移动 .app,但需自行处理权限、DMG/App Translocation、备份、恢复、并发与启动确认。 | 拒绝。新增高风险安装器不能达到现有 CLI 原子升级的验证水平。 |
4. 统一版本与 Release 模型
vX.Y.Z- 版本比较只接受三段数字 SemVer,去掉 tag 前缀
v后逐段数值比较;不使用字符串排序。 - 发现相同或更低版本时返回
up-to-date,不允许稳定通道降级。 - Release 必须同时包含
release-manifest.json和当前安装类型要求的资产。manifest 以basename(path)建立索引,与 GitHub 的扁平 asset name 匹配;重复 basename、缺项或同名 metadata 不一致都使 Release 无效。缺失时不退回源码包或 wheel。 - metadata 固定从
https://api.github.com/repos/Fzkuji/OpenProgram/releases/latest读取;manifest/DMG/runtime 固定从该响应中同仓库的github.com/Fzkuji/OpenProgram/releases/download/vX.Y.Z/...入口读取;版本化 CLI installer 固定从raw.githubusercontent.com/Fzkuji/OpenProgram/vX.Y.Z/scripts/install-release.sh读取。 - 所有请求只允许 HTTPS,最多跟随 5 次重定向,并逐跳校验 host。允许的 host 精确为
api.github.com、github.com、raw.githubusercontent.com和release-assets.githubusercontent.com;任意其他 host、协议降级或凭据型 URL 都拒绝。renderer 不接收或构造下载 URL。
5. macOS Desktop 更新
5.1 主进程职责
- 启动后读取
userData/update-state.json;默认automaticChecks=true。 - 持久化 schema 1 包含
automaticChecks、lastAttemptAt、lastSuccessAt和最近一次成功的、经过验证的 release metadata。使用同目录临时文件加 rename 写入;损坏文件、未知 schema 或无效字段回退默认值。 - packaged app 启动 30 秒后计算检查计划。成功后下一次自动检查安排在
lastSuccessAt + 24h;失败后安排在lastAttemptAt + 6h,避免密集重试。手动检查始终跳过成功缓存和失败退避。 - 主进程只保留一个定时器和一个进行中的检查 Promise。每次完成、系统 sleep/resume 和 automaticChecks 设置变化后重新计算下一次时间;resume 不补发多个过期检查,最多立即启动一个,然后重新排程。开发模式不执行定时检查,但保留手动测试入口。
- 使用 Electron 主进程网络 API 请求 GitHub latest,设置固定 User-Agent、超时和响应大小上限。
- 按
process.arch选择OpenProgram-X.Y.Z-mac-arm64-unsigned.dmg或OpenProgram-X.Y.Z-mac-x64-unsigned.dmg,从 manifest 取得字节数和 SHA-256。 - 用户点击下载后显示系统保存对话框;写入同目录临时文件,流式计算 SHA-256,校验字节数和 hash 后原子改名。
- 校验成功后打开 DMG;失败时删除临时文件并显示可重试错误。不得打开未校验文件。
5.2 IPC 边界
updates.getState() // 只读当前状态 updates.check() // 手动检查;主进程强制刷新 updates.setAutomaticChecks(enabled) // 保存布尔偏好 updates.download() // 只下载主进程已验证 metadata 对应的 DMG updates.openRelease() // 打开固定 Release 页面 updates.onState(callback) // 状态推送
renderer 不能传入仓库、URL、文件名、hash 或目标命令。主进程持有全部远端和文件系统权限,并只接受布尔偏好和无参数动作。
6. macOS/Linux CLI 与 server 更新
Release 安装
openprogram upgrade 解析 latest,若有新版本则下载该 tag 的版本化 installer,并把目标版本显式传入。
原子切换
installer 在新目录完成 archive checksum、manifest、功能和 cold-start 验证,再切换 current。
源码开发
检测到 Git checkout 后继续执行现有 preflight、checkout、依赖、build、probe 和 restart 流程;界面明确标为 development。
openprogram upgrade --check只报告当前/最新版本和是否可升级,不写文件、不重启。openprogram upgrade默认需要用户显式执行,不在后台替换 server runtime。- managed upgrade 启动版本化 installer 时显式固定
OPENPROGRAM_REPOSITORY=Fzkuji/OpenProgram和已验证的目标版本,并从子进程环境删除OPENPROGRAM_RUNTIME_ARCHIVE、OPENPROGRAM_RUNTIME_SHA256以及其他会改变正式产物来源的测试覆盖变量。installer 的本地 archive 注入只保留给直接执行脚本的 CI,不由产品命令继承。 - 运行中的 worker 不直接从旧进程内覆盖文件。升级命令只在隔离 cold start 成功后切换
current,不自动停止或重启现有 worker;旧 worker 继续运行当前代码,命令明确提示用户显式执行openprogram worker restart。因此事务成功边界是current原子切换,不宣称新 worker 已接管。 - Web 设置页第一阶段不请求远端版本,也不提供远程执行升级按钮;显示 host 当前版本、安装类型和
openprogram upgrade --check/openprogram upgrade指令。
7. 设置页信息结构
入口固定在 Settings → General → Application。Desktop 和普通浏览器显示不同的可执行动作,但不按“desktop user/client user”重复分类。非 Desktop 页面通过 owner-auth 保护的只读 GET /api/system/version 取得 host 信息,响应 schema 为 {currentVersion, installType};版本来自当前 Python distribution metadata,安装类型来自统一 detector。detector 先检查 immutable runtime,再区分源码 checkout,否则返回 unknown;该接口不访问 GitHub、不执行更新。
- 有更新时显示版本号、发布日期和 Release Notes 摘要;完整内容链接到该 Release。
- 下载进度显示已下载字节/总字节。保存完成前按钮不可重复触发。
- 普通 Web 页面没有 Desktop bridge 时,不显示 DMG 下载动作;加载
/api/system/version后显示实际 host 版本、安装类型和 CLI 升级命令。请求失败时显示 unknown,不使用硬编码版本。 - 设置文字区分“Automatically check for updates”和“Download and open DMG”,不使用“Install and restart”。
8. 安全、隐私与失败处理
| 情况 | 处理 | 保留状态 |
|---|---|---|
| 离线、超时、GitHub 限流 | 记录简短错误;自动检查静默,手动检查在设置页显示重试。 | 应用和旧版本继续运行。 |
| Release 字段或版本无效 | 拒绝该响应,不下载任何资产。 | 保留最近一次成功 metadata,但标明检查时间。 |
| 缺少架构资产/manifest 条目 | 显示“该版本没有当前架构的完整安装包”。 | 不退回错误架构、ZIP、wheel 或源码包。 |
| 下载中断/hash/size 不一致 | 删除临时文件;不得打开 DMG 或切换 CLI runtime。 | 已安装版本不变。 |
| 并发检查或下载 | 复用同一个进行中的 Promise;按钮进入 disabled 状态。 | 只有一个网络请求/下载任务。 |
| Release Notes 内容 | 设置页显示纯文本摘要;不把 GitHub Markdown 当 HTML 注入。 | 完整正文只在外部 GitHub 页面打开。 |
| 更新状态文件损坏/未知 schema/未来时间 | 恢复默认值并立即以临时文件加 rename 重写;失败时留在内存并在下次写入重试。 | 不阻止应用启动,也不把损坏时间戳用于节流。 |
自动检查会向 GitHub 暴露普通 HTTPS 请求所含的 IP、User-Agent 与请求时间。不开启遥测,不上传 OpenProgram 配置、会话或设备标识。
9. 实现顺序与验收
- 旧 updater 收敛:测试证明 worker 启动不再 apply 更新、immutable runtime 不进入 Git/PyPI 路径、
update兼容入口与upgrade使用同一正式语义。 - 安装类型:测试证明 Desktop/CLI 的 immutable runtime 均为
managed_release,源码仓库为source_checkout,不存在把正式产品显示为pip_wheel的路径。 - Desktop 检查与 UI:纯函数测试覆盖版本解析、latest 验证、真实 workflow 嵌套 manifest path 的 basename 唯一匹配、架构资产选择和状态转换;IPC 测试证明 renderer 不能指定 URL;Web 测试覆盖所有状态和非 Desktop 回退。
- Desktop 下载:自动化测试通过注入的
fetch/Response覆盖正常下载、允许的 GitHub 资产重定向、拒绝第三方/HTTP 重定向、重定向上限、body stall、大小不符、hash 不符、取消保存和并发请求;没有把该测试描述为真实 HTTP server 验收。只有校验成功才调用打开 DMG。 - 持久化与调度:自动化测试覆盖 24 小时成功缓存、失败后 6 小时内不重复、6 小时到期重试、损坏/未来/未知 schema、原子写入及写失败回滚;main 的 timer、manual IPC 重排和 resume wiring 由源码合同检查覆盖。应用连续运行与实际 sleep/resume 属于首个 updater release 的可见验收,不列为本地自动化已执行。
- CLI/Linux:fake GitHub/installer 测试覆盖
--check、无更新、缺资产、安装失败保留旧current、成功切换且不重启现有 worker、显式 restart 后使用新版本,以及注入 archive/checksum/repository 环境变量不能改变产品命令的正式更新来源。 - 发布门禁:release workflow 校验每个 Desktop 资产都存在 manifest 条目,每个 runtime 都有独立 checksum;Release 创建后使用 workflow token 查询 GitHub latest endpoint,确认目标 tag、稳定状态和唯一 manifest 资产。
- 可见验收:在
/Applications/OpenProgram.app的 default profile/18100 实例中验证设置页、版本号、手动检查、下载进度和失败提示;不启动第二个可见 Desktop 实例。
10. 实现与验证记录
- 已实现:Desktop 主进程更新服务、最小 IPC、Settings Application 状态;managed CLI Release 检查/升级;旧后台与 PyPI product updater 删除;installer old→new 原子替换;release 后 GitHub latest endpoint gate。
- 已验证:版本/manifest/架构匹配、URL 和逐跳重定向约束、损坏/未来缓存、短写、metadata/download stall、checksum/size 失败清理、并发 Promise、dry-run 和 JSON 输出合同、已有
current的真实替换。 - 待 release 验收:首个包含 updater 的正式版本发布后,在默认
/Applications/OpenProgram.app与隔离 CLI state 中验证 v0.6.6 一次性过渡及后续版本发现。发布前不把本地候选描述为已交付客户端。