跳转至

产品规格

Problem Statement

当 Netflix / Amazon / Disney+ / Hulu 这类 VIP 平台改了接口或参数,StreamFab 的下载就会失败。用户点下载,等待,然后拿到一个失败提示——他为这个功能付了钱。

今天修复这种故障的唯一路径是:改 C++ 代码 → 重新编译 → 走完整版本发布流程 → 用户升级。这条路以周计。在 Windows 上用户至少还能通过 LiveUpdate 收到升级;在 macOS 上客户端甚至不会自己下载新版本——它只是弹个窗,把用户丢到浏览器的下载页,让他自己下 dmg 重装。

这段等待期里,用户不知道我们已经在修了。他看到的只是"这个软件不能用了"。于是退款,于是差评。VIP 平台的故障直接吃掉收入和用户信任,而我们的响应速度被版本发布节奏锁死。

Solution

建立一条独立于完整版本发布的热修通道:P0/P1 站点故障发生后,当天完成修复构建与签名,次日全量送达用户,全程不走版本发布流程。

用户视角的体验是这样的:他今天遇到 Netflix 下载失败;我们在后台把修好的程序静默下载、校验、暂存到他的机器上;因为这是紧急修复,客户端弹一条轻提示告诉他"Netflix 的下载问题已修复,重启即可生效"——如果他正在下别的东西,这条提示会等到任务跑完再出现。他点一下重启,问题就没了。他不需要去官网、不需要重新安装、不需要知道版本号。

如果他忽略了这条提示,也没关系——修复已经躺在他机器上了,下次他自己重启 StreamFab 时会自动生效。

而如果这次热修本身有问题,他也不会被卡住:程序起不来会自动换回上一个能用的版本;指标不对我们能在服务端一键撤回。

技术上,热修的单位是整个主程序 exe——不拆动态库、不做站点规则外置化。因为站点适配逻辑是硬编码进 C++ 并静态链接的,规则外置化是一场大重构;而"整件替换"恰恰是 Sparkle、Squirrel、Chrome 组件更新的通行做法。这条路还有个额外好处:回滚就是把旧文件换回来,可靠得多。

User Stories

终端用户

  1. As a StreamFab 用户, I want 站点故障能在一两天内修好而不是等一个完整版本, so that 我不必为一个买了却用不了的功能申请退款。
  2. As a StreamFab 用户, I want 修复包在后台静默下载和校验, so that 我不必中断手头的下载任务去做任何操作。
  3. As a StreamFab 用户, I want 紧急修复就绪时收到一条明确的提示, so that 我知道问题已经解决、重启一下就能继续用。
  4. As a 正在下载的用户, I want 重启提示等我的下载任务完成后再出现, so that 我不会因为一条提示而丢掉下了一半的文件。
  5. As a 忽略了提示的用户, I want 修复在我下次自然重启时自动生效, so that 我不必记住任何待办事项。
  6. As a macOS 用户, I want 修复能在客户端内完成而不是把我丢到浏览器下载页, so that 我不必手动下载 dmg 重装一遍。
  7. As a 遇到热修后反而变差的用户, I want 客服能帮我退回上一个版本, so that 我不会被一个坏修复卡住。
  8. As a 用户, I want 热修失败时程序仍然能启动, so that 我至少不会连软件都打不开。
  9. As a 注重隐私的用户, I want 热修通道只做故障修复、不夹带功能推送, so that 我信任这条通道不会改变我熟悉的软件行为。

值班与运营

  1. As a 值班负责人, I want 在灰度期间看到命中热修与未命中人群的崩溃率和下载成功率对比, so that 我能判断这次热修是真的修好了还是引入了新问题。
  2. As a 值班负责人, I want 一键把灰度比例归零, so that 发现异常时我能立刻截断扩散而不必等任何人。
  3. As a 值班负责人, I want 强制回滚能力, so that 已经装上问题热修的用户也能被修回去。
  4. As a 项目 DRI, I want 每次热修都有故障确认、发布、全量、恢复的时间记录, so that 我能量化"故障恢复时间"这个北极星指标。
  5. As a 运营, I want 在热修尚未就绪时把故障站点标记为"维护中", so that 用户看到的是一条说明而不是反复重试后的失败。
  6. As a 客服, I want 一个不暴露在界面上的开关来禁用热修, so that 我能在排查时确认问题是否由热修引起。
  7. As a 客服, I want 按设备排除个别用户的能力, so that 我能处理个例而不影响整体放量。

发布与研发

  1. As a 技术负责人, I want 热修发布走独立流水线, so that 紧急修复不被版本发布的排期和审批卡住。
  2. As a 技术负责人, I want 热修发布需要产品和技术双人审批, so that 这条快速通道不会被滥用于非紧急变更。
  3. As a 发布执行者, I want 热修构建复用现有的加壳与代码签名流程, so that 热修包不会因为签名缺失被杀软拦截而制造新故障。
  4. As a 发布执行者, I want 构建失败时立刻收到通知, so that 紧急修复不会因为无人察觉的失败而静静耽误几个小时。
  5. As a 客户端开发者, I want 决策逻辑(验签、版本校验、分桶、回退判定)是一份跨平台共享代码, so that Windows 和 macOS 的安全行为不会各写各的而产生漂移。
  6. As a 客户端开发者, I want 复用现有 LiveUpdate 的下载与替换机器, so that 我不必重写一套久经考验的代码并承担新的稳定性风险。
  7. As a CI 维护者, I want 热修包生成复用现有增量流水线的版本查询、防回滚和通知能力, so that 我只需新增热修特有的部分。
  8. As a 开发者, I want CI 产出的清单能被客户端的验签逻辑直接验证的契约测试, so that 协议不一致能在流水线里暴露而不是到线上才发现。

安全

  1. As a 安全负责人, I want 每个热修包和清单都有 Ed25519 签名且公钥硬编码在客户端里, so that 即使下发通道或 CDN 被攻破也无法投毒。
  2. As a 安全负责人, I want 客户端拒绝比本地已见更旧的清单, so that 攻击者无法通过重放旧清单把用户降级到有漏洞的版本。
  3. As a 安全负责人, I want 清单带过期时间, so that 攻击者无法通过冻结更新让用户永远停在旧状态。
  4. As a 安全负责人, I want 热修链路强制 HTTPS 且开启证书校验, so that 我们在签名之外还有一层传输保护。
  5. As a 安全负责人, I want 签名私钥保存在 Jenkins 凭据库并与代码签名证书同级管理, so that 密钥不散落在个人机器上。

灰度与回滚

  1. As a 发布负责人, I want 每次热修必须先经过内部白名单 canary 档, so that 我们不会重蹈 CrowdStrike 用"内容更新为求快跳过分级发布"造成全量事故的覆辙。
  2. As a 发布负责人, I want 10% 灰度覆盖一整个夜间使用高峰, so that 问题能在真实负载下暴露而不是在低谷期被掩盖。
  3. As a 发布负责人, I want 客户端自己完成灰度分桶, so that 服务端不需要维护用户名单也能精确控制放量比例。
  4. As a 客户端, I want 在替换前用子进程真实拉起一次新程序做自检, so that 那些在这台机器上根本起不来的热修根本不会被提交。
  5. As a 客户端, I want 新程序连续两次启动失败后自动换回上一个可用版本, so that 用户不会被一个崩溃的热修永久锁死。
  6. As a 客户端, I want 执行文件替换的组件本身永远不走热修通道更新, so that 负责救援的代码不会被它要救援的问题弄坏。
  7. As a 客户端, I want 文件替换中途失败时回滚已替换的文件, so that 用户不会停在一个半新半旧的损坏状态。

Implementation Decisions

范围

第一版受管文件三类:主程序 exe(覆盖全部 C++ 站点适配故障)、yt-dlp 可执行文件(收编其现有独立更新通道,统一签名与灰度)、配置开关文件(第一版仅实现站点维护开关)。

明确排除:原生动态库补丁下发、站点适配规则外置化、画质降级等其他配置开关、自动指标回滚、热修管理后台 UI。

基线定向

热修只针对发布时的最新正式版。清单按基线版本定向,客户端携带自身版本查询;旧版本用户的修复路径是升级到最新正式版。这与 CI 现有的版本对命名约定天然对齐。

决策引擎(跨平台共享)

热修的全部判定逻辑收敛为一个不依赖平台 API 的纯 C++ 模块,放在共享层,Windows 与 macOS 客户端同时链接。

接口形态:输入为清单字节 + 本地状态(当前版本、机器标识、已见清单版本、启动标记计数、逃生门标记),输出为决策(应用 / 跳过 / 拒绝)与原因码。它不做网络 IO、不碰文件系统、不拉起进程——这些由调用方注入。

它负责:Ed25519 验签、清单版本单调性检查、过期时间检查、基线版本匹配、灰度分桶、exclude 名单、urgent 判定、启动标记回退判定。

选择共享而非各平台各写一份,是因为安全逻辑写两遍必然漂移,且能让两个平台共用一套测试。

签名

双层:平台层复用现有的 VMProtect 加壳 + Authenticode / Developer ID 签名流程,覆盖 exe 本身;应用层新建 Ed25519 签名,覆盖清单与每个受管文件。

公钥硬编码进客户端二进制,另预埋一把备用公钥供应急轮换。不使用现有配置里的证书链字段——它来自主程序写出的配置文件,可被篡改,不能作信任根(且当前是死字段,无任何代码使用它)。

清单必须带单调递增版本号与过期时间,客户端拒绝比本地已见更旧的清单。这是防重放降级与冻结攻击的必要条件。

私钥存入 Jenkins 凭据库,与现有代码签名证书同级管理。签名动作发生在流水线的双人审批节点之后。

通道

服务端在现有 API 家族中新增热修清单接口,参照现有组件更新接口的模式:客户端携带客户端类型与版本查询,返回签名清单。热修包走现有多镜像 CDN 下发配置。

隔离体现在权限与流程而非物理机器:清单发布权限与版本发布系统权限分离,发布走独立的热修流水线。

热修链路强制 HTTPS 并开启证书校验——现有下载链路默认关闭证书校验、部分接口仍是明文 HTTP,热修通道不继承这个习惯。签名为主、传输为辅,两层都要。

灰度

客户端侧分桶:以机器标识与热修标识的哈希落桶,与清单中的百分比比较。服务端无需维护用户名单。

放量节奏为保守档:内部白名单 canary 观察半天 → 10% 过夜(覆盖晚间高峰)→ 全量。canary 不可跳过。百分比调整通过人工修改清单并经流水线发布,留审计记录。第一版不做自动放量、不做管理后台。

替换与回滚

替换事务的语义:给定受管文件集与目标目录,要么全部替换成功,要么回滚到原状。替换后需复核签名,通过才删除备份,并向调用方返回整体成败。

Windows 侧改造现有的文件复制助手(它当前的"备份"只是为绕开文件锁、下轮更新即删除、无回滚、无成败返回值)。macOS 侧新建,倾向整 bundle 替换。

三防线本地回滚,核心原则是验证与切换动作由尚未被替换的、已验证健康的代码执行

  1. 预提交自检——旧程序以子进程拉起新程序的自检模式(加载 + 核心模块初始化 + 正常退出),不通过则不提交切换。这在用户真实机器上真实拉起过一次。
  2. 启动标记计数——新程序在入口最早处(早于 UI 与浏览器内核初始化的纯 C 代码)写标记,健康检查通过后清除;连续两次未清除则自动换回备份版本并上报。
  3. 执行替换与回退的组件不在热修范围内,只随完整版本更新。

服务端两层回滚均为人工:百分比归零截断扩散;发布"内容为旧版本、清单版本号前进"的新清单实现强制回滚(兼容防降级校验)。

指标回滚第一版走人工判断——遥测看板给数据,值班负责人或项目 DRI 决策,流水线一键执行。自动阈值回滚留待后续,业界无公开成熟规范且误触发风险高。

热修的进程退出改为优雅退出,不沿用现有升级流程的强杀。

生效与用户控制

下载、验签、自检、暂存全程静默;重启生效。默认等待自然重启;清单标记 urgent 时弹轻提示引导重启,用户有活跃下载任务时延迟到任务完成后再弹,同一热修最多提示一到两次。

用户控制边界是:给用户控制何时生效的权利,不给是否修复的选择,留隐藏通道兜极端情况。界面不提供关闭热修的设置——一批用户关掉热修会造成在网版本碎片化,污染平台成功率指标,且排查故障时每次都要先确认对方是否关闭了热修。保留一个不暴露在界面上的标记文件开关供客服排查与极端兜底。

CI

在现有增量包流水线上扩展:配置生成脚本新增热修类型(它已有插件类型的分支可参照,并已连接版本数据库、具备版本对枚举与防回滚能力);打包脚本新增热修子类,只覆写补丁生成为单文件全量,复用打包、MD5、完整性校验;流水线新增热修参数、Ed25519 签名阶段、双人审批节点。

主 exe 不做二进制差分,走全量投递——现有配置已把主程序排除在差分之外(加壳签名的二进制做差分会触发杀软误报或破坏签名,且压缩率不如全量,客户端历史上有过差分回归问题)。热修沿用这一既定结论,包体积约等于主 exe 压缩后大小。

流水线必须补充失败通知——现有流水线只在成功时通知,而热修是紧急通道,失败必须响。

遥测

在现有事件通道上新增热修维度事件:拉取、验签、自检、切换、回滚,payload 携带热修版本、基线版本、灰度档位、失败原因。看板需支持命中与未命中人群的分组对比,崩溃数据需能按热修版本维度聚合。

Testing Decisions

好的测试只验证外部行为,不验证实现细节。对这个功能而言,这条原则的具体含义是:测试断言的是"给定这样一份清单和这样的本地状态,客户端做出了什么决定",而不是"它内部调用了哪个函数、用了什么数据结构"。这样即便将来重构验签实现或换掉分桶算法,测试依然有效。

现有测试基础:C++ 侧共享库仓库已有 gtest 工程与若干工具类测试可作样板,但升级库本身目前无任何测试;Python 侧补丁生成工具已有一套完整的 pytest 套件(扫描、XML 生成、补丁生成、往返、端到端),含配置、基准与回归目录,是质量很好的现成范式。

三个测试接缝:

接缝一:决策引擎(gtest,跨平台共享)

这是最高价值的接缝——全部安全与灰度判定都在这里,且完全无副作用,可以穷举测试。因为它不做 IO,攻击场景可以直接构造:篡改签名的清单被拒绝、比本地已见更旧的清单被拒绝、过期清单被拒绝、基线不匹配的清单被跳过、分桶边界值(0% 无人命中 / 100% 全员命中 / 指定百分比下同一机器标识结果稳定)、exclude 名单命中、逃生门标记生效、启动标记计数达到阈值触发回退、未达阈值不触发。

接缝二:替换事务(gtest,平台各自实现)

需要文件系统,测试针对临时目录中的假文件,注入失败来验证事务性:全部成功后目标文件为新内容且备份被清理;中途失败后目标目录回滚到原状且不留半新半旧;替换后签名复核失败同样触发回滚;整体成败正确返回给调用方。

接缝三:热修包与清单生成(pytest,复用现有套件的组织方式)

给定基线与热修两个可执行文件加灰度参数,产出包与签名清单,断言包结构正确、清单字段完整、灰度参数正确落入。

契约测试(闭合三者)

接缝三产出的清单,必须能通过接缝一的验签与解析逻辑。用一份共享的测试向量(公钥、样例清单、期望决策)同时驱动 CI 侧与客户端侧的测试,使协议不一致在流水线中暴露,而不是等到线上才发现清单格式对不上。这是整套测试策略里最关键的一环——跨语言、跨仓库的协议最容易在两端各自演进时悄悄裂开。

不做的测试:不为"下载器能否下载"这类现有能力补测试(复用的是久经考验的既有代码);不做端到端真机灰度自动化测试(灰度本身就是渐进验证机制,canary 档由真人验证)。

Out of Scope

  • 站点适配规则的外置化改造(把硬编码的 C++ 适配逻辑抽成可下发的数据或脚本)——这是独立的大型重构,需单独立项评估
  • 原生动态库形式的代码补丁下发
  • 主程序 exe 的二进制差分下发——受限于加壳签名二进制的差分会触发杀软误报,需先解决根因,属独立课题
  • 基于指标阈值的自动回滚
  • 热修管理后台 UI(第一版通过人工修改清单加流水线发布完成全部运营操作)
  • 覆盖历史版本的多基线热修
  • 站点维护开关之外的其他配置开关
  • 现有下载链路整体的 TLS 安全加固(热修链路强制严格校验,但不在本次范围内修复其他链路的历史问题)

Further Notes

下发服务是唯一的高阻塞未知。灰度分流与清单选择的服务端逻辑位于更新数据库与下发服务中,其源码不在已探索的任何一个仓库内。CI 侧只负责生成产物并落地到存储,下载配置由外部工具生成。热修的灰度参数最终必须落到这一侧——开发启动前需要该服务的负责人确认接口形态或提供源码

PRD 需要同步修订。原 PRD 将"站点适配规则更新"列为热修内容第一类,但代码事实是四个 Tier 0 平台的适配逻辑全部硬编码于 C++ 并静态链接进主程序,规则数据下发在现状下没有实现基础,实际形态是整 exe 替换。建议产品侧同步更新表述,避免评审与后续沟通中的理解错位。

安全能力是从零建,不是锦上添花。现有升级链路无任何非对称验签,唯一的内容校验是补丁重建后的 MD5,补丁包本身不校验,传输层证书校验被关闭,配置中的证书字段是死字段。热修通道的全部安全性由新引入的 Ed25519 层承担,这部分工作量不应被"复用现有机器"的乐观情绪掩盖。

一个有价值的印证:CI 配置早已把主程序排除在二进制差分之外,说明"整 exe 全量投递"这条路团队实际一直在走。本方案的核心选择是走在已有轨道上,而非另起炉灶。

其余待办(不阻塞开发启动):双平台主 exe 实际体积实测;macOS 替换粒度(整 bundle 与仅主二进制)及公证细节验证;清单 JSON 格式与热修版本命名语义的最终定义;补丁 XML 中目录节点 MD5 为空的既有缺陷(若签名清单需覆盖其语义完整性则宜先修);Ed25519 密钥的生成、备份与轮换操作规程。

完整的技术方案文档(含代码证据、现状分析、改造点清单)见仓库 docs/hotfix-channel-tech-spec.md;决策记录见 docs/adr/;业界调研见 docs/research/hotfix-channel-industry-survey.md