news 2026/9/18 17:23:20

Folo 桌面端与移动端 OTA 服务统一设计:从 mainHash 到 runtimeVersion 的更新架构演进

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Folo 桌面端与移动端 OTA 服务统一设计:从 mainHash 到 runtimeVersion 的更新架构演进

Folo 桌面端与移动端 OTA 服务统一设计:从 mainHash 到 runtimeVersion 的更新架构演进

Folo(AI RSS Reader)在apps/ota中维护着一个基于 Cloudflare Workers + KV + R2 的移动端 OTA 更新服务。本文以仓库设计文档 docs/superpowers/specs/2026-04-11-desktop-ota-unification-design.md 为骨架,讲解如何把该服务扩展为移动端与新桌面端共用的"单一更新事实源":引入文件驱动的release-plan.json/release.json发布流程、以显式runtimeVersion取代mainHash的兼容性判定、面向direct/mas/mss三种分发的二进制策略(/policy),以及同时承载 renderer OTA 与直连安装包的/manifest协议。读完本文,你将掌握这套跨端更新系统在路由契约、元数据模型、KV 存储键设计与发布编排上的完整设计思路,并可在仓库源码中找到对应的落地实现。

背景:从移动端专属 OTA 到多产品统一更新源

apps/ota最初是一个移动端专用的 OTA 服务。仓库中的早期设计文档 docs/superpowers/specs/2026-04-10-ota-design.md 明确了其定位:基于 Expo Updates 的自定义后端,运行在 Cloudflare Workers 之上,以 GitHub Releases 作为发布事实源、Cloudflare R2 作为交付层,仅通过x.y.z纯版本号驱动,并依赖runtimeVersion表达原生兼容边界。

桌面端 OTA 统一设计文档(即本文主题)正是在这一基础之上的第二阶段演进。它要解决的核心问题是:把apps/ota从一个"仅服务移动端"的服务,扩展为移动端与新版桌面端的唯一更新事实源,同时做到:

  • 桌面端 OTA 与直接二进制更新策略都由apps/ota承载;
  • follow-server中既有的桌面更新路由保持不动,继续服务旧版桌面客户端;
  • 把兼容性判定从mainHash迁移到显式runtimeVersion
  • 桌面端发布编排对齐移动端已经建立的release-plan.json+release.json文件驱动工作流。

从仓库当前文件状态看,这套设计并非停留在纸面:apps/desktop/下已存在 release-plan.json(当前默认mode: "build")与 release.json,apps/ota/src/lib/下已出现 request.ts、desktop.ts 等桌面端解析模块,apps/ota/src/__tests__/也配套了 manifest.test.ts、policy.test.ts、sync.test.ts 等测试。配套的实施计划文档见 docs/superpowers/plans/2026-04-11-desktop-ota-unification.md。

设计目标与非目标

Goals

  • apps/ota成为移动端与新版桌面客户端唯一的更新事实源
  • follow-server保持不变,旧版桌面客户端完全兼容;
  • 桌面端 OTA 兼容性判定中移除mainHash
  • 桌面发布编排与移动端新的release-plan.json/release.json工作流对齐;
  • 支持direct、Mac App Store(mas)、Microsoft Store(mss)三类分发,且三者的二进制策略生效时机可以不同(商店审核完成时间不同步);
  • 一次桌面 OTA 发布可以同时发布 renderer OTA 数据与直连安装包数据;
  • 保留一种简单、可审计、仓库原生的发布流程,由入库 JSON 配置驱动。

Non-Goals

  • 不迁移旧版桌面客户端离开follow-server
  • 不替换follow-server既有的桌面 YAML 路由;
  • 不通过外部 API 自动探测 App Store / Microsoft Store 审核完成状态;
  • 不通过 OTA payload 下发原生代码;
  • 首个版本不做分阶段灰度百分比(staged rollout)。

Constraints

  • follow-server不允许被修改;
  • 新版桌面客户端访问apps/ota时只使用X-App-*请求头;
  • 桌面分发渠道只能从既有的X-App-Platform取值推断,这些值定义在 packages/internal/utils/src/headers.ts 的DesktopPlatform枚举中;
  • 桌面 OTA 兼容性必须使用显式runtimeVersion
  • 若桌面客户端省略X-App-Runtime-Version,服务端须把X-App-Version当作 runtimeVersion 处理。

六个关键设计决策

1. 服务归属:apps/ota拥有更新事实源

apps/ota拥有移动端与新版桌面客户端的更新事实源;follow-server退化为仅服务旧桌面客户端的兼容层。这意味着新功能的迭代不再牵动旧的follow-server部署面。

2. 桌面兼容模型:用 runtimeVersion 取代 mainHash

桌面端不再以mainHash作为 OTA 兼容性键,而改用三个明确语义的版本维度:

  • installedBinaryVersion:当前已安装的桌面应用版本;
  • runtimeVersion:renderer OTA 的兼容线;
  • rendererVersion:当前已安装的 renderer 版本。

默认规则是:

runtimeVersion = installedBinaryVersion

这一模型与移动端的心智完全一致,消除了mainHash隐藏的兼容语义。

3. 文件驱动的发布意图

桌面端采用与移动端完全相同的工作流形态:

  • apps/desktop/release-plan.json 表达下一次的发布意图;
  • apps/desktop/release.json 记录实际打 tag 发布时被"锁定"的解析配置。

发布自动化在决定发布内容时必须读取release.json,而不是读取临时手工输入的工作流参数。当前仓库中桌面 release.json 的实态为version: "1.12.0"mode: "build"

4. 桌面发布三种模式

桌面端支持buildotabinary-policy三种模式:

模式含义
build只发布直连安装包资源
ota同时发布 renderer OTA 资源与直连安装包资源
binary-policy只发布二进制升级策略元数据,不重新构建或上传安装包

5. 分发策略粒度:per-distribution 时机

由于 MAS 与 MSS 的商店审核存在延迟且完成时间不同,桌面二进制策略必须支持按分发渠道独立设置生效时机。受支持的分发渠道为:

direct mas mss

策略查找须优先匹配分发特定策略,找不到再回退到产品级策略(product-level policy)。

6. 发布类型命名:store → binary

OTA 元数据模型中的发布类型从ota/store迁移为ota/binary。兼容规则是:Worker 继续把遗留的store元数据当作binary的别名接受。这样既能保证旧移动端元数据继续工作,又给桌面端一个比store更准确的命名。

发布配置设计:release-plan.json 与 release.json

桌面release-plan.json

建议的默认形态:

{ "mode": "build", "runtimeVersion": null, "channel": null, "distributions": [], "required": false, "message": null }

规则约束:

  • mode只能是buildotabinary-policy三者之一;
  • ota模式必须提供runtimeVersion
  • buildbinary-policy模式下runtimeVersion必须为null
  • otabinary-policy必须提供channel
  • binary-policy必须提供distributions
  • requiredmessage只影响二进制策略的发布行为。

允许的桌面渠道为stablebetaalphadevelopment不是发布渠道。

桌面release.json

建议形态:

{ "version": "1.6.1", "mode": "ota", "runtimeVersion": "1.6.0", "channel": "stable", "distributions": ["direct"], "required": false, "message": null }

release.json是 CI 打 tag 发布时消费的最终事实源。

工作流解析

桌面端镜像移动端的工作流模式:

  1. 解析脚本读取 apps/desktop/release.json;
  2. 解析器决定要触发的 workflow 动作;
  3. tag.yml依据解析输出分派构建与 OTA 发布。

据此,日常发布执行不再需要手工输入发布参数。实施计划中对应新增的解析器为.github/scripts/resolve-desktop-release-config.mjs,其职责可概括为:校验release.json中的version与 tag 版本一致,并按模式输出triggerDirectBuild/triggerStoreBuilds/triggerMetadataPublish/releaseKind/runtimeVersion/channel等输出项(buildota模式触发构建、binary-policy只触发元数据发布)。

OTA 元数据模型:schemaVersion 2 的 ota-release.json

桌面发布必须产出单一、机器可读的元数据文件供apps/ota使用。文件继续命名为ota-release.json,使 Worker 的同步路径在所有产品上保持一致。

一份桌面ota-release.json需要能够描述三种内容:

  • renderer OTA payload;
  • direct 直连安装包 payload;
  • 仅 binary-policy 的发布。

设计文档给出的建议完整形态:

{ "schemaVersion": 2, "product": "desktop", "channel": "stable", "releaseVersion": "1.6.1", "releaseKind": "ota", "runtimeVersion": "1.6.0", "publishedAt": "2026-04-11T10:00:00Z", "git": { "tag": "desktop/v1.6.1", "commit": "abcdef123456" }, "policy": { "required": false, "minSupportedBinaryVersion": "1.6.0", "message": null, "distributions": { "direct": { "downloadUrl": "https://example.com/Folo-1.6.1.dmg" } } }, "desktop": { "renderer": { "version": "1.6.1", "commit": "abcdef123456", "launchAsset": { "path": "renderer/render-asset.tar.gz", "sha256": "0123...", "contentType": "application/gzip" }, "assets": [] }, "app": { "platforms": { "macos": { "platform": "macos-x64", "releaseDate": "2026-04-11T10:00:00Z", "manifest": { "name": "latest-mac.yml", "downloadUrl": "https://example.com/latest-mac.yml" }, "files": [ { "filename": "Folo-1.6.1-macos-x64.zip", "sha512": "base64sha512", "size": 123456789, "downloadUrl": "https://example.com/Folo-1.6.1-macos-x64.zip" } ] } } } } }

语义要点:

  • schemaVersion: 2用于与既有移动端专属形态区隔,避免歧义;
  • 移动端可以继续沿用现有 schema,也可以在未来选择迁移;
  • releaseKind: "ota"表示该发布可从/manifest下发 renderer OTA 数据;
  • releaseKind: "binary"表示/manifest不服务任何 OTA payload,但元数据仍可更新/policy
  • runtimeVersion仅在releaseKind: "ota"时必填,binary时应为null或省略;
  • direct二进制 payload 数据在buildota两种模式下都可以存在。

仓库中的落地实现位于 apps/ota/src/lib/schema.ts:desktopReleaseInputSchemaschemaVersion约束为字面量2product约束为字面量desktopreleaseKind使用z.enum(["ota", "binary", "store"])接受store遗留值,policy.distributions通过z.partialRecord(desktopDistributionSchema, ...)表达direct/mas/mss的分发级策略,runtimeVersion为可空 semver。

请求契约:X-App-* 请求头体系

新版桌面客户端请求apps/ota时只使用X-App-*请求头:

必填头:

  • X-App-Platform
  • X-App-Version
  • X-App-Channel

可选头:

  • X-App-Runtime-Version
  • X-App-Renderer-Version

X-App-Platform的实际取值可在 packages/internal/utils/src/headers.ts 的DesktopPlatform枚举中看到,包含desktopdesktop/webdesktop/macosdesktop/macos/dmgdesktop/macos/masdesktop/windows/exedesktop/windows/msdesktop/linux。其中分发相关的建构建函数createDesktopAPIHeaders会根据运行平台(darwin/win32/linux)以及是否为process.mas、Microsoft Store 构建,自动选择对应的 platform 头值。

X-App-Platform到路由维度的映射

头值platformdistribution
desktop/macos/dmgmacosdirect
desktop/macos/masmacosmas
desktop/windows/exewindowsdirect
desktop/windows/mswindowsmss
desktop/linuxlinuxdirect
desktop/web不参与桌面 OTA 与桌面二进制策略

仓库中的实际解析实现位于 apps/ota/src/lib/request.ts 的parseDesktopRequest:它用DESKTOP_PLATFORM_MAP把上述头值映射为platform + distribution,并实现X-App-Runtime-Version缺省时回退到X-App-Version的逻辑,与设计约束完全一致。

桌面版本语义

  • X-App-Version永远是已安装的二进制版本(installed binary version);
  • X-App-Runtime-Version是 OTA 兼容性键;
  • X-App-Runtime-Version缺失,使用X-App-Version
  • X-App-Renderer-Version仅用于判断某个 renderer payload 是否比已安装 renderer 更新。

GET /manifest:下发兼容 payload

/manifest只回答一个问题:当前客户端可用的兼容更新 payload 有哪些。桌面端manifest可以包含 renderer OTA payload 或 direct 渠道的完整应用 payload,但不能代替/policy去做商店升级拦截的最终 UX 决策

建议响应形态

{ "id": "uuid", "createdAt": "2026-04-11T10:00:00.000Z", "product": "desktop", "channel": "stable", "runtimeVersion": "1.6.0", "renderer": { "releaseVersion": "1.6.1", "version": "1.6.1", "commit": "abcdef1234", "launchAsset": { "key": "render-asset", "hash": "sha256-base64url", "fileExtension": ".tar.gz", "contentType": "application/gzip", "url": "https://ota.folo.is/assets/desktop/stable/1.6.0/1.6.1/windows/render-asset.tar.gz" }, "assets": [] }, "app": { "releaseVersion": "1.6.1", "version": "1.6.1", "platform": "windows-x64", "releaseDate": "2026-04-11T10:00:00.000Z", "manifest": { "name": "latest.yml", "downloadUrl": "https://example.com/latest.yml" }, "files": [ { "filename": "Folo-1.6.1-windows-x64.exe", "sha512": "base64sha512", "size": 123456789, "downloadUrl": "https://example.com/Folo-1.6.1-windows-x64.exe" } ] } }

桌面 Manifest 规则

  • renderer仅在同时满足以下条件时返回:
    • 存在product + channel + runtimeVersion + platform匹配的兼容桌面 OTA 发布;
    • renderer payload 版本比X-App-Renderer-Version更新;
  • app仅在满足以下条件时返回:
    • 客户端分发渠道为direct
    • 存在请求平台下更新的兼容直连安装包;
  • masmss渠道绝不能/manifest拿到 direct 二进制 payload;
  • rendererapp都为空,返回204

客户端决策优先级

  1. renderer就优先应用 renderer;
  2. 否则有app就提供app(direct 全量更新);
  3. 二进制升级的引导与强制则单独走/policy

实施计划给出的对应测试期望包括:direct 客户端两者皆有时返回renderer + app、只有 renderer 时只返回 renderer、store 渠道请求永远拿不到app、无兼容 payload 时返回204

GET /policy:显式发布的二进制升级策略

/policy只回答三个问题:

  • 当前已安装二进制是否应继续可用;
  • 是否应提示用户升级;
  • 用户应去哪里拿到正确的二进制。

建议响应形态(无动作)

{ "action": "none", "targetVersion": null, "message": null, "distribution": "direct", "downloadUrl": null, "storeUrl": null, "publishedAt": null }

有直连升级可用时

{ "action": "prompt", "targetVersion": "1.6.1", "message": "A newer desktop version is available.", "distribution": "direct", "downloadUrl": "https://example.com/Folo-1.6.1.dmg", "storeUrl": null, "publishedAt": "2026-04-11T10:00:00.000Z" }

策略选择规则

对桌面端:

  1. X-App-Platform推断distribution
  2. 查询product + channel + distribution的策略;
  3. 若不存在,回退到product + channel
  4. 若仍无策略,返回none

动作语义:

  • none:不提示也不阻塞;
  • prompt:建议二进制升级,但允许继续使用;
  • block:继续使用前必须先完成二进制升级。

URL 语义:

  • direct返回downloadUrl
  • masmss返回storeUrl

为什么策略必须显式发布

服务端不能从 GitHub Release 的发布时间推断策略生效时机。原因在于 MAS / MSS 的审核时机是异步且不可知的,GitHub Release 可能早于商店二进制真正可安装就发布。因此二进制策略只有在针对相关分发渠道执行了一次显式binary-policy发布后才生效(例如 App Store 审核通过后发布 MAS 策略,Microsoft Store 审核完成后另行发布 MSS 策略)。

仓库的 KV 键实现(apps/ota/src/lib/constants.ts)已把 policy 键设计为可选分发维度:

policy: ( product, channel, distribution?, // "direct" | "mas" | "mss" ) => distribution ? `policy:${product}:${channel}:${distribution}` : `policy:${product}:${channel}`,

存储模型:KV 键设计

Release 记录

按 release-version 键存储解析后的桌面与移动发布元数据:

release:desktop:1.6.1 release:mobile:0.4.3

最新 OTA 指针

桌面 OTA 最新指针继续以 product、channel、runtimeVersion、platform 为维度:

latest:desktop:stable:1.6.0:windows

二进制策略键

新增分发感知键:

policy:desktop:stable:direct policy:desktop:stable:mas policy:desktop:stable:mss

移动端初期可继续使用 product 级键(policy:mobile:production等),未来再平滑迁移到分发感知键。

发布流程:三种模式的差异

Desktopbuild

  • 构建并上传直连安装包资源;
  • 发布桌面二进制元数据;
  • 不发布 renderer OTA payload。

Desktopota

  • 构建并上传 renderer OTA payload;
  • 构建并上传直连安装包资源;
  • 发布一份同时包含 renderer OTA 与 direct 二进制 payload 数据的元数据文件。

这样做保证了:既有用户有资格接收 renderer OTA,新用户也能立即下载最新直连安装包——"一次 OTA 发布仍要发布最新完整安装包"这一桌面端硬性需求由此得到满足。

Desktopbinary-policy

  • 只发布策略元数据;
  • 不重建安装包;
  • 不上传 renderer OTA payload;
  • 显式指定一个或多个目标分发渠道。

典型场景:App Store 审核通过后发布 MAS 策略,Microsoft Store 审核完成后再发布 MSS 策略。

迁移与验证方案

Migration Plan

  1. 引入桌面发布配置文件与解析逻辑;
  2. 让桌面发布流程产出新的桌面ota-release.json
  3. 扩展apps/ota的 sync、存储与选择逻辑以理解桌面元数据;
  4. apps/ota增加桌面感知的manifestpolicy路由;
  5. 让新版桌面客户端改用apps/otamanifest + policy
  6. 旧版桌面客户端继续使用follow-server,不做改动。

测试矩阵

单元测试覆盖:桌面 release config 校验与解析器行为、桌面元数据解析、X-App-Version到 runtimeVersion 的回退、X-App-Platform到 platform/distribution 的映射、策略选择与回退、renderer payload 选择、direct 二进制 payload 选择。仓库中的对应测试文件包括 schema.test.ts、sync.test.ts 等。

Worker 路由测试覆盖:direct 请求在两者可用时返回renderer + app;只有 renderer 时返回 renderer;MAS / MSS 请求永不从/manifest拿到app;policy 优先分发特定记录;direct返回downloadUrlmas/mss返回storeUrl;无兼容 payload 时manifest返回204。对应文件为 manifest.test.ts 与 policy.test.ts。

客户端验证:新版桌面客户端优先应用 renderer OTA;renderer 不可用时回退到 direct 全量更新;MAS / MSS 客户端尊重prompt并执行block

开放风险

  • 桌面元数据文件要同时为未来的移动端收敛预留向后兼容,需要谨慎处理 schema 版本演进;
  • 桌面客户端不得假定 store 分发渠道一定存在apppayload;
  • 发布自动化必须避免对商店渠道过早发布binary-policy

最终建议:一套可理解、可落地的更新模型

采纳apps/ota作为移动端与新版桌面客户端的统一更新事实源,follow-server保持原样为旧桌面客户端服务,从兼容模型中移除mainHash,并让桌面发布编排与移动端文件驱动发布流对齐。整套体系收敛为四层清晰的概念:

  • release-plan.json定义意图
  • release.json锁定打 tag 发布的配置
  • /manifest暴露兼容 payload 事实
  • /policy暴露二进制升级策略

同时保留桌面端直接分发的要求:一次 OTA 发布依然会发布最新完整安装包。对于正在阅读源码的开发者,建议按 request.ts → schema.ts → constants.ts →desktop.ts/sync.ts→ 路由与测试的顺序阅读,即可把本文的设计契约与代码逐一对上。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 16:23:57

四类AI Agent工作范式:技能调度、本地OS、IDE增强与CLI胶水

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 16:21:30

CMSIS-4不是版本号,而是嵌入式开发的隐性接口冻结契约

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 16:16:04

顺丰快递作业成本法实战:成本动因、分摊模型与数据链路

简介:围绕作业成本法在顺丰快递公司的应用,这份山东财经大学燕山学院本科毕业设计论文提供了完整的案例研究文本,适合会计、财务管理专业学生及物流企业成本管理从业者参考。论文先梳理国内外作业成本法在成本控制领域的理论文献,…

作者头像 李华
网站建设 2026/9/17 16:14:59

RK3588边缘ASR实测:Zipformer比Conformer快3倍省35%内存

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 16:14:24

讯飞Astron Agent掘金版Docker Compose私有化部署全攻略

1. 部署前的整体设计与思路拆解1.1 Astron Agent 掘金版到底是什么先说清楚这次部署的对象。讯飞 Astron Agent 是科大讯飞推出的一套智能体开发与编排平台,主打让开发者以低门槛方式把大模型能力、外部工具、知识库和业务流程串起来。所谓“掘金版”,可…

作者头像 李华
网站建设 2026/9/17 16:13:53

Vue3响应式解构:toRefs与storeToRefs详解

1. 为什么需要响应式解构?在Vue3的Composition API开发中,我们经常遇到一个典型问题:从reactive对象或Pinia store中解构出的属性会失去响应性。这个问题看似简单,却困扰着不少开发者。我接手过多个项目,发现团队成员经…

作者头像 李华