react-native-update 10.49.0 → 10.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/BUNDLEHASH_MIGRATION.md +22 -2
  2. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  3. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  4. package/README-CN.md +14 -1
  5. package/README.md +14 -1
  6. package/android/jni/Android.mk +5 -1
  7. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  8. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  9. package/android/lib/x86/librnupdate.so +0 -0
  10. package/android/lib/x86_64/librnupdate.so +0 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +42 -9
  12. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -22
  13. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  16. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
  18. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
  19. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  20. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  21. package/cpp/patch_core/digest.cpp +53 -0
  22. package/cpp/patch_core/digest.h +18 -0
  23. package/cpp/patch_core/patch_core.cpp +17 -1
  24. package/cpp/patch_core/patch_core.h +8 -0
  25. package/cpp/patch_core/tests/patch_core_test.cpp +99 -0
  26. package/cpp/update_flow_core/flow_json.cpp +522 -0
  27. package/cpp/update_flow_core/flow_json.h +103 -0
  28. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  29. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  30. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  31. package/cpp/update_flow_core/update_flow_core.h +77 -0
  32. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  33. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  34. package/harmony/pushy/src/main/cpp/pushy.cpp +150 -1
  35. package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -9
  36. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  37. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  38. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  39. package/harmony/pushy/src/main/ets/NativePatchCore.ts +16 -0
  40. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  41. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  42. package/harmony/pushy/src/main/ets/UpdateContext.ts +152 -52
  43. package/harmony/pushy.har +0 -0
  44. package/ios/RCTPushy/RCTPushy.mm +840 -68
  45. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  46. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  47. package/package.json +3 -1
  48. package/react-native-update.podspec +2 -0
  49. package/src/NativePushy.ts +14 -0
  50. package/src/client.ts +439 -122
  51. package/src/endpoint.ts +105 -80
  52. package/src/error.ts +4 -0
  53. package/src/locales/en.ts +4 -0
  54. package/src/locales/zh.ts +4 -0
  55. package/src/provider.tsx +74 -9
  56. package/src/telemetry.ts +8 -0
  57. package/src/type.ts +31 -0
  58. package/src/updateFlowCore.ts +320 -0
  59. package/src/utils.ts +14 -12
  60. package/src/isInRollout.ts +0 -86
  61. package/src/resolveCheckResult.ts +0 -30
@@ -4,7 +4,9 @@
4
4
  > - DB:pushy 北京 RDS 与 cresc Cloud SQL 均已加 `packages.bundleHash` 列(幂等 runner 在 `scripts/migrate-add-package-bundle-hash.ts`;cresc 经一次性 Cloud Run Job 进 VPC 执行)
5
5
  > - 服务:pushy `p`/`s1` @ `2026.7.29-a2ecac26`,cresc api/worker/dailyjob @ `2026.7.29-3aabff91`,线上带 `bundleHash` 冒烟均通过
6
6
  > - **`bundleHashJudge.enabled` 仍为关**——待 CLI/客户端发版、覆盖率起来后再开
7
- > - 客户端与 CLI 已提交**未发版**(npm 发布是独立决策);Phase 2 遗留见 §4.5,Phase 3 待观察期后
7
+ > - **客户端与 CLI 已发版(2026-07-30)**:`react-native-update@10.49.0`、`react-native-update-cli@2.20.3`。全链路(客户端上报 → CLI 注册 → 服务端判定基建)就位,仅待覆盖率与开开关
8
+ > - **`bundleHashJudge.enabled` 已于 2026-08-03 双端开启**(pushy + cresc,redis `config_dynamic`,回退=写回 false)。同日:CLI 2.21.2 补全 copiesCrc(§4.2.1 第 2 条)并随两服务端部署;客户端三端 CRC 校验 + fallback 遥测(§4.2.1 第 3/4 条)已实现、待随下个客户端版本发布
9
+ > - Phase 2 遗留见 §4.5,Phase 3 待观察期后
8
10
  > 依据:`BUNDLEHASH_DESIGN.md`(设计定稿)。本文只讲**怎么落地**,不重复论证为什么。
9
11
  > 下游:`NATIVE_CHECKUPDATE_DESIGN.md` 的协议下沉必须等本迁移完成,否则协议下沉后再改协议要返工。
10
12
 
@@ -203,6 +205,12 @@ await addFileFromZipEntry(...); // 只有新文件才打进 patch 包
203
205
  3. 客户端拷贝时校验,不符 → fallback full。代价退化为一次 pdiff 下载,**永不装出坏包**
204
206
  4. 给该 fallback 加遥测
205
207
 
208
+ **已全部落地(2026-08-03)**:
209
+ - 第 2 条:CLI 2.21.2(同路径分支补记 CRC;两服务端已 bump 部署,worker 新生成的 pdiff 均带全量 copiesCrc)
210
+ - 第 3 条(三端):Android `BundledResourceCopier` 路径命中也比 CRC,不符 → 内容定位 → 仍无 → 抛错;iOS/共享 C++ `CopyOperation.expected_crc` 在 `ApplyPatchFromFileSource` 拷贝前校验(`digest::Crc32File`,NIST/check-value 测试锚定);Harmony pdiff 拷贝声明 CRC 的条目改走字节路径经 NAPI `crc32` 校验。三端不符均使整次 pdiff 失败,JS 策略链自动落 full
211
+ - 第 4 条:新增本地事件 `downloadFallback`(增量策略失败但后续策略救回),失败码为 PATCH_FAILED 时映射服务端 `patch_fail`(网络噪音不上报)。为此补齐了 Android(`PatchFailedException`:下载完成后的失败 reject 为 PATCH_FAILED,此前一律 DOWNLOAD_FAILED)与 Harmony(patch 阶段 `error.code='PATCH_FAILED'`)的稳定码
212
+ - 注意:存量已生成的 pdiff 只有 moved 条目的 CRC,客户端只校验有声明的条目——旧 pdiff 行为不变,随版本滚动自然收敛
213
+
206
214
  若遥测显示这一行的 pdiff 失败率显著,再引入**精确**判据(如"copies 含非 metro 资源"),而不是用恒为 true 的粗标记一刀切。
207
215
 
208
216
  ### 4.3 开关与灰度
@@ -236,7 +244,7 @@ cresc-server 是独立仓库(结构同构、历史独立,`handleCheckUpdate`
236
244
 
237
245
  ### 4.5 Phase 2 遗留(不阻塞开启开关)
238
246
 
239
- - 控制台 unknownBundle 聚合面板(§7 触达第 3 条,主渠道)——需 admin 侧配合。曾考虑挂在 `classifyCheckUpdateResult` 的 hitType 上,否决:hitType 是"下发了什么产物"维度,混入二进制身份维度不干净,应独立埋点
247
+ - ~~控制台 unknownBundle 聚合面板~~ **已实现(2026-08-01,四端部署)**:最终形态不是独立的 bundleStatus"内容判定"维度(首版上线后即废弃——它依赖开关开启+双侧 hash+恰好有更新流动,观测窗口太窄、零数据),而是**请求侧合并进原生包面板**:客户端报了 bundleHash 就记 `packageVersion_bundleHash`(admin 图例把 64 位 hex 截 8 位展示),否则回退 `packageVersion_buildTime`,同一 attribute。全量流量可见、与开关/判定结果解耦;未注册人群在面板上直接带 packageVersion 上下文。代价是丢失 matched/rebuiltSameJs 区分——那只有教育用途,unknownBundle 占比(§4.6 的推进指标)不受影响。曾考虑挂在 `classifyCheckUpdateResult` 的 hitType 上,否决:hitType 是"下发了什么产物"维度,混入二进制身份维度不干净
240
248
  - `digests` 下发 + `__diff.json` copies 附摘要——与客户端 Phase 3 的消费侧同波次做
241
249
  - ~~CLI 上传冲突提示~~ **已实现(改判为无需动 CLI)**:`(appId, name)` 唯一约束下重复上传本来就 409,在两台服务器的冲突分支里用 bundleHash 增强语义——内容一致 →"无需重复上传"、内容不同 →"重打包且 JS 有变,请提升原生版本号"、任一侧无 hash → 通用文案不变。查询仅发生在冲突路径。cresc 3 条路由级用例覆盖;pushy 无路由级测试脚手架,靠同构代码 + cresc 用例 + tsc 兜底
242
250
 
@@ -252,6 +260,18 @@ Phase 2 的收益与 Phase 0/1 的覆盖率成正比,但**不需要等覆盖
252
260
 
253
261
  ## 5. Phase 3 — 收尾(客户端)
254
262
 
263
+ > **定位修订(2026-08-03):buildTime 不删,永久保留作 fallback。**
264
+ > 理由:服务端分支与客户端双上报的保留成本≈0,而删除的伤害面(bundleHash
265
+ > 空串设备失去二进制身份 → 重打包检测失明 / 双缺请求拿不到 full)明确存在
266
+ > 且只能靠空串率数据压低,收益(构建注入清理、协议更干净)不急迫。注入的
267
+ > pnpm 老毛病若再发作,修注入本身而不是删身份。
268
+ > Phase 3 因此收缩为一件事:**§5.1 的 SyncBinaryVersion 迁移**——改为
269
+ > `bundleHash || buildTime`(bundleHash 优先、buildTime 兜底,schema 认领式
270
+ > 迁移照做)。不做它,rebuiltSameJs 的收益只兑现一半:服务端已不 block、照发
271
+ > pdiff,但客户端 buildTime 一变仍清状态回内置包再白下载一遍;hash 优先后
272
+ > 同 JS 重打包做到真正无感。§5.2 的 digests/copies 摘要照旧待做;§5.3 的
273
+ > 降级序列与"删构建注入"**取消**。
274
+
255
275
  ### 5.1 `SyncBinaryVersion` 迁移有个状态危险,必须处理
256
276
 
257
277
  设计文档写"`SyncBinaryVersion` 改 `(packageVersion, bundleHash)`"。直接改会出事:
@@ -1,8 +1,15 @@
1
1
  # 原生检测更新设计:让砖机也能被修好
2
2
 
3
- > 状态:设计草案,待评审
3
+ > 状态:**实现完成(2026-08-07,e34f390..24f3adf)**——三端编排全部落地:
4
+ > 标定层 syncNativeConfig、iOS RCTPushyOrchestrator、Android
5
+ > NativeCheckOrchestrator(librnupdate.so 已重编 4 ABI 并过符号/对齐校验)、
6
+ > Harmony NativeCheckOrchestrator.ts + NAPI,§10.3 的 JS 响应缓存复用
7
+ > (getNativeCheckCache,2 分钟 TTL)也已闭环。剩余:e2e 用例(沿用
8
+ > Example/e2etest 基建,验证"坏 bundle 下原生仍拉到修复版"端到端场景)、
9
+ > README/CHANGELOG、发版。
4
10
  > 取代:`REMOTE_RESET_DESIGN.md`(本地启动熔断方案,已放弃,理由见 §1.2)
5
- > 前置:`BUNDLEHASH_DESIGN.md`(设计定稿、待实施)——协议下沉前必须先定稿协议
11
+ > 前置:bundleHash 迁移 Phase 0/1/2 已上线且 buildTime 永久保留作 fallback 已定稿
12
+ > (2026-08-03)——checkUpdate 的 wire protocol 已稳定,协议前置解除
6
13
  > 关联:[[native-update-reset-design]] 的 Phase 3
7
14
 
8
15
  ---
@@ -82,9 +89,35 @@ A 直接淘汰。B 是 [[native-update-reset-design]] 里 Phase 3 的原计划
82
89
 
83
90
  **B 与 C 的差别只有一个:检测逻辑本身出 bug 时,能不能不发新 binary 就修好。**
84
91
 
92
+ 补充两点评审结论(2026-08-06):其一,B 并不消灭 TS 实现——app JS 侧的交互
93
+ 流程仍要用同一套决策逻辑,所以 B 实际是 **TS + C++ 双实现长期同步**(每次协
94
+ 议演进都要重编预编译 `.so`),而 C 是同一份 TS 源两处复用,这是 C 相对 B 的
95
+ 另一日常优势。其二,C 的独占收益比上表暗示的窄:只要 JS 侧 `checkUpdate` 保
96
+ 留为全功能回退(§6 是复用结果、不是拆除),原生检测逻辑出 bug 的最坏情形是
97
+ 正常设备仍被 JS 路径救起、只有砖机在窗口期救不了——即退回现状,而非"全网
98
+ 瘫"。真正集中风险的是原生**编排**代码(R2),而那部分 B / C 完全相同,
99
+ guardian 救不了它。
100
+
101
+ > **最终裁决(2026-08-07):选 B,三端统一 C++,金标向量作为双实现的强制契
102
+ > 约(已实现)。** 推理链:引擎方案的全部价值 = 单一事实来源;Android 宿主
103
+ > Hermes 被符号侦察排除、javascriptengine 因可用性是运行时属性只能当可选优
104
+ > 化——一个必定可用的 C++ 实现无论如何要存在;一旦如此,混合形态(iOS/
105
+ > Harmony 求值 + Android C++)要养两套机制,统一 C++ 的改动次数相同却删掉了
106
+ > 求值器机制整个类别;QuickJS 统一保单源要付 200KB×4 + 三端引擎集成,只换
107
+ > 来"少改一处代码",而那处改动有向量护栏,是机械劳动。
108
+ > "单源"要保的性质是**语义唯一且被机械强制**,由金标向量提供:
109
+ > `src/updateFlowCore.ts` 是参照实现(oracle),
110
+ > `scripts/generate-flow-vectors.ts` 产出
111
+ > `cpp/update_flow_core/tests/flow_vectors.json`(金标向量集),
112
+ > `src/__tests__/flowVectors.test.ts` 钉住 TS 与向量文件一致,
113
+ > `scripts/test-update-flow-core.sh`(CI cpp-test job,带 ASan/UBSan)钉住
114
+ > C++ 移植与向量一致。**纪律:语义改动先落 TS → 重新生成向量 → 移植 C++,
115
+ > 两侧不绿不发版。** C++ 侧的 `flow_json` 按 JS 语义实现(插入序对象、
116
+ > undefined ≠ null、JS truthiness / 严格相等),移植才能逐字节对齐。
117
+
85
118
  ---
86
119
 
87
- ## 5. Guardian bundle
120
+ ## 5. Guardian bundle(已否决路线,留档防止重新发明;裁决见 §4)
88
121
 
89
122
  ### 5.1 关键设计约束:它必须是纯函数,不做 IO
90
123
 
@@ -93,31 +126,93 @@ A 直接淘汰。B 是 [[native-update-reset-design]] 里 Phase 3 的原计划
93
126
  正确的边界是:**guardian 只做决策,IO 全部由原生执行。**
94
127
 
95
128
  ```
96
- 原生 ──> guardian.buildCheckRequest(state) ──> { url, headers, body }
97
- 原生 <── (发 HTTP,各端现成客户端)
129
+ 原生 ──> guardian.buildCheckRequest(state) ──> { endpoints, queryUrls, path, headers, body, timeoutMs }
130
+ 原生 <── (按 endpoints 顺序逐个请求,单个超时即换下一个;
131
+ 首个失败后拉 queryUrls 合并远程候选、排除已失败的,继续顺序尝试)
98
132
  原生 ──> guardian.decide(state, responseText) ──> { action, hash, url, type } | { action: 'none' }
99
133
  原生 <── (下载 + patch_core 应用 + state_core switchVersion,全是现成的)
100
134
  原生 ──> guardian.onOutcome(state, result) ──> { nextState }
101
135
  ```
102
136
 
137
+ **endpoint 计划是声明式的**(初稿此处只画了"一问一答",漏掉了 JS 侧真实存在的
138
+ 多 endpoint 回退与远程 endpoint 发现):候选排序(随机首选分摊负载 + 配置序回
139
+ 退 + 失败排除)是纯策略,由 `orderEndpointCandidates` 给出,随机数由原生作为
140
+ 输入注入(guardian 不可自取随机)。**原生侧刻意不实现 JS 交互路径的 hedged
141
+ race**(`src/endpoint.ts` 的 250ms 错峰竞速):原生检测跑在冷启动后台、结果下
142
+ 次启动才生效,延迟不敏感(§7 R5 本来就要求延迟数秒),顺序回退 + 单请求超时
143
+ 就够了——换来的是三端各自的执行引擎退化为一个 for 循环,无定时器、无 abort
144
+ 协调。两条路径共享同一份候选排序策略,只在并发形态上分叉。
145
+
103
146
  这个边界带来的简化是决定性的:
104
147
 
105
- - **不需要事件循环、不需要 Promise、不需要注入 HTTP** —— 只是同步求值一个小 HBC 再调几个函数
148
+ - **不需要事件循环、不需要 Promise、不需要注入 HTTP** —— 只是同步求值一段小 JS 再调几个函数
106
149
  - **隔离性天然成立** —— guardian 与 app bundle 是两个独立的 parse 单元、独立求值;app bundle 的语法错误跟它毫无关系
107
- - 灰度分桶(`isInRollout`)、diff→pdiff→full 选择、`expVersion` 解析、URL 拼接这些**已经是纯函数了**(`src/isInRollout.ts`、`src/resolveCheckResult.ts`),可以几乎原样搬过去
150
+ - 灰度分桶、diff→pdiff→full 选择、`expVersion` 解析、URL 拼接、请求体构造、
151
+ endpoint 排序**已全部抽为纯函数并在 JS 侧原地使用**(`src/updateFlowCore.ts`:
152
+ `buildCheckRequestBody` / `resolveCheckResult` / `decideDownload` /
153
+ `isInRollout` / `joinUrls` / `orderEndpointCandidates`,import 闭包为纯,
154
+ 可在裸引擎中求值),guardian 直接复用同一份源码
108
155
 
109
156
  ### 5.2 运行时
110
157
 
111
- 在后台线程创建一个 `hermes::makeHermesRuntime()`,求值 guardian 的 HBC,调用导出的函数,用完销毁。全同步,毫秒级。
112
-
113
- 放在 `cpp/` 里与 `patch_core` / `state_core` 同级,三端共享同一份 C++;各端只提供 HTTP 客户端(Android OkHttp、iOS NSURLSession、Harmony `@ohos.net.http` —— 都已在用)和文件路径。
114
-
115
- **主要风险**:直接链接 libhermes 的 C++ API 在 RN 各版本间会变,可能出现符号/ABI 兼容问题。Android 的 `librnupdate.so` 是预编译产物(4 个 ABI),链上 Hermes 后与宿主 RN 的 Hermes 版本耦合。**这是 C 相对 B 的主要代价,需要先做一个链接可行性验证再决定**。
116
-
117
- 备选:不用 Hermes,用一个极小的嵌入式 JS 引擎(QuickJS 约 200KB)。彻底解耦宿主 RN,代价是包体增加与另一套字节码工具链。
158
+ 在后台线程创建一个 JS 运行时,求值 guardian 源码,调用导出的函数,用完销毁。全同步,毫秒级。
159
+
160
+ **已定稿(2026-08-06):guardian 以纯文本 JS 源码分发与求值,不用 HBC。**
161
+ Hermes 字节码格式不跨版本稳定——若分发 HBC,服务端覆盖包必须按宿主 Hermes
162
+ 版本分桶,基线包也与构建期 hermesc 版本绑死,复杂度远超收益。源码求值走的
163
+ 是慢速路径,但 guardian 每次冷启动只求值一次、代码量千行级,毫秒级完全可接
164
+ 受。代价:宿主 libhermes 若编译时裁掉了源码编译器则不可用——这归入下面的链
165
+ 接可行性验证。
166
+
167
+ **源码可移植性已验证(2026-08-07)**:`src/updateFlowCore.ts` 经
168
+ `bun build --format=cjs` 打成 6.4KB 单文件纯文本 JS(零依赖、零 require),
169
+ 在三个裸引擎下对同一驱动脚本的输出**逐字节一致**且全部正确——macOS 系统
170
+ `jsc`(与 iOS JavaScriptCore 同源)、Hermes VM(RN 0.73 配套版本,源码直接
171
+ 求值无需 HBC)、Node V8。函数调用边界(JSON 进出)即 §5.1 的三步接口,工作
172
+ 正常。进一步用 ObjC 写了最小原生宿主实测通过:`JSContext` 求值源码 →
173
+ `callWithArguments` 调 `decideDownload` → 拿回决策 JSON——40 行代码、仅链
174
+ 系统 Foundation + JavaScriptCore 两个框架,就是 iOS 编排器的最终形态。语法下限:产物含 `?.`/`??`/对象展开 → Hermes ≥0.7(RN 0.64+)、iOS
175
+ JSC ≥13.4;如需更老目标,guardian 构建加 es2015 降级即可。
176
+
177
+ **纯文本定稿的推论——三端不必用同一个引擎**:源码是共享物,引擎只是求值
178
+ 器,选型可以按端就地取材:
179
+
180
+ | 端 | 引擎 | 风险 |
181
+ |---|---|---|
182
+ | iOS | 系统 JavaScriptCore 框架 | **零**——系统框架,零链接、零包体、零版本耦合,上面已实测同源引擎 |
183
+ | Harmony | 系统 JSVM-API(V8,API 11+,RNOH 本就要求 5.0+) | 低——系统能力,待落地时确认 API 细节 |
184
+ | Android | 开放问题:androidx.javascriptengine(系统 WebView 引擎、沙箱进程、异步)/ QuickJS(~200KB×4 ABI)/ ~~链接宿主 Hermes~~ | **剩余的全部 spike 范围**(Hermes 路线已被侦察排除,见下) |
185
+
186
+ **Android 链接宿主 Hermes 已排除(2026-08-07 对 hermes-android
187
+ 250829098.0.10 / RN 0.85 预编译产物的符号侦察)**:prefab 里虽然带了
188
+ `hermes_abi/hermes_abi.h`(稳定 C ABI 头),但其入口 `get_hermes_abi_vtable`
189
+ **并未从 `libhermesvm.so` 导出**——只导出了 C++ 的
190
+ `facebook::hermes::makeHermesRuntime(RuntimeConfig const&)`,而走 C++ 路线
191
+ 要求调用方与宿主版本的 `RuntimeConfig` 布局、libc++ ABI 精确匹配,且 JSI 符
192
+ 号导出为零(消费方须自行编译与宿主一致版本的 jsi.cpp)。对跨 RN 版本分发的
193
+ 预编译 `librnupdate.so`,这是不可控的版本矩阵;老 RN 的 `libhermes.so` 连库
194
+ 名和导出面都不同。若未来官方开始导出稳定 C ABI,此路线可复活。当前 Android
195
+ 实际候选就两个:javascriptengine(零包体、依赖 WebView provider、沙箱进程异
196
+ 步——对冷启动后台任务可接受,需真机验证)与 QuickJS(确定可行、包体代价)。
197
+
198
+ 原生编排层(HTTP、下载、状态)仍放 `cpp/` 与 `patch_core` / `state_core` 同级三端共享;求值器作为注入接口由各端实现,与 HTTP 客户端同一地位。
199
+
200
+ **剩余风险收窄为 Android 单点**:直接链接 libhermes 的 C++ API 在 RN 各版本间会变,且 `librnupdate.so` 是预编译产物(4 个 ABI)。若三个 Android 候选都不干净,**仅 Android 一端退回方案 B(决策逻辑 C++ 重写)也是可接受的混合形态**——三端源码一致性只在 iOS/Harmony 保持,Android 以 updateFlowCore 为参照实现并靠共享测试向量对齐。
118
201
 
119
202
  ### 5.3 分发与覆盖
120
203
 
204
+ > **裁决(2026-08-07):远程覆盖通道不做,guardian 只带随 binary 打包的基线。**
205
+ > 覆盖通道买的保险是"决策纯函数出 bug 时不发 binary 就能修",但决策层是全
206
+ > 链路最可测的部分(纯函数、单测、与 JS 侧同一份源码),且已有两条兜底:
207
+ > JS 侧 checkUpdate 全功能回退(决策 bug 最坏退回现状,不是新增灾难)、服
208
+ > 务端塑造响应/重绑版本本身就是一条远程修复通道(决策层消费的是服务端数
209
+ > 据)。代价却是全系统最敏感的安全面——启动最早期执行、有权决定装什么版本
210
+ > 的服务端下发代码——加上 §5.4/§5.5 的全部工程量,而覆盖机制自身是原生代
211
+ > 码,它出 bug 同样无法远程修。保费高于风险敞口。
212
+ > 未来若决策层 bug 真在现场咬人,优先评估**搭现有热更通道便车**(ppk 附带
213
+ > guardian.js,复用既有 hash 校验与下发权限),不自建通道。§5.4/§5.5 保留
214
+ > 作为那时的设计输入;"救砖"能力来自 §8 第 4 步的原生编排,不受本裁决影响。
215
+
121
216
  | | 来源 | 作用 |
122
217
  |---|---|---|
123
218
  | 基线 | 随 binary 打包(asset / rawfile) | 永远存在,已随发版验证过 |
@@ -174,13 +269,28 @@ app 侧的 `client.ts` / `UpdateProvider` 仍然负责**交互**:更新提示
174
269
 
175
270
  ## 8. 分期
176
271
 
177
- 1. **bundleHash 迁移**(`BUNDLEHASH_DESIGN.md`,已定稿)—— 协议下沉的前置,否则返工
178
- 2. **纯函数抽取** —— 把 `buildCheckRequest` / `decide` 从 `client.ts` 剥成无 IO 的纯函数,先在 JS 侧原地使用并补测试。这一步 B / C 都需要,且不依赖运行时选型,**可以立刻开始**
179
- 3. **运行时选型** —— Hermes 链接可行性验证;失败则退回方案 B(C++ 纯函数)
180
- 4. **原生编排** —— 三端 HTTP + 调用纯函数 + 复用现有下载/patch/state
181
- 5. **guardian 分发通道**(仅方案 C)—— 打包、下发、§5.4 回滚保护
182
-
183
- 第 2 步是关键:**它是 B 和 C 的公共前置**,做完之后再决定选型也不迟,而且它本身就能让现有 JS 实现更可测。
272
+ 1. ~~**bundleHash 迁移**~~ **已解除**(2026-08-03)—— Phase 0/1/2 已上线、
273
+ 判定开关双端开启、buildTime 永久保留作 fallback 定稿,checkUpdate 的
274
+ wire protocol 已稳定;剩余的 Phase 3(SyncBinaryVersion 迁移)是客户端
275
+ 本地状态变更,不动协议,与本方案只需在落地顺序上错开(都动原生启动路
276
+ 径与状态 schema),不再构成前置
277
+ 2. ~~**纯函数抽取**~~ **已完成**(2026-08-07)—— `src/updateFlowCore.ts`:
278
+ `buildCheckRequestBody` / `resolveCheckResult` / `decideDownload` /
279
+ `isInRollout` / `joinUrls` / `orderEndpointCandidates`,无 IO、无
280
+ react-native 依赖、无模块级状态(身份/随机数均参数注入),import 闭包
281
+ 为纯;client.ts / provider.tsx / endpoint.ts 已原地改用,单测覆盖
282
+ 3. ~~**运行时选型**~~ **已关闭:改判方案 B**(2026-08-07,裁决与推理链见
283
+ §4)。`cpp/update_flow_core` 已实现(flow_json + 七个决策函数的 1:1 移
284
+ 植),金标向量集在本机与 CI(ASan/UBSan)全过
285
+ 4. **原生编排** —— 三端 HTTP + 调用 `update_flow_core` + 复用现有下载/
286
+ patch/state;endpoint 执行引擎为顺序回退(§5.1),不移植 hedged race。
287
+ Android 侧 `update_flow_core` 进 `librnupdate.so`(协议演进从此绑定
288
+ `.so` 重编,走 build-android-so.sh + CI + verify-android-so.js 的 16KB
289
+ 对齐断言)
290
+ 5. ~~**guardian 分发通道**~~ **已裁决不做**(2026-08-07,见 §5.3)——随
291
+ §4 改判 B,整个 guardian 路线关闭;砖机救援能力在第 4 步,不受影响
292
+
293
+ 剩余工作只有第 4 步:原生编排。决策层已就位(TS 参照 + C++ 移植 + 向量契约),编排层是纯 IO 胶水。
184
294
 
185
295
  ## 9. 已写代码的处置
186
296
 
@@ -190,3 +300,123 @@ app 侧的 `client.ts` / `UpdateProvider` 仍然负责**交互**:更新提示
190
300
  - `autoReset` 遥测事件 —— 无 reset 动作后失去意义,撤回
191
301
 
192
302
  `unconfirmedBoots` / 前台闸门 / `markBootHealthy` / `PROVIDER_REQUIRED` / `CONTENT_APPEARED` 全部撤回。
303
+
304
+ ---
305
+
306
+ ## 10. 原生编排设计(§8 第 4 步)
307
+
308
+ ### 10.1 标定(provisioning)——设计到此才暴露的缺口
309
+
310
+ appKey、server endpoints、更新策略今天只活在 JS 的 `ClientOptions` 里,而原
311
+ 生检测跑在冷启动、任何 JS 之前。解决:**JS 是唯一配置源,原生只消费落盘副
312
+ 本。** 每次 `setOptions`(含构造)后 JS 调新的原生方法 `syncNativeConfig`,
313
+ 持久化:
314
+
315
+ ```text
316
+ { appKey, packageVersion, endpoints: server.main, queryUrls,
317
+ afterDownload: 'none' | 'setNeedUpdate',
318
+ disabled?: boolean, rnu, rn }
319
+ ```
320
+
321
+ - `afterDownload` 由 updateStrategy 折算:`silentAndLater` / `silentAndNow`
322
+ → `setNeedUpdate`(原生的作用面本来就是"下次启动");alert 类策略 → 只下
323
+ 载不激活,弹窗与确认永远归 JS(§6)
324
+ - **无配置 → 原生静默不跑**。首次安装首启、或从未升级到新 JS 的老接入,天
325
+ 然回到现状,零行为变化——这就是灰度开关,不需要另设开关
326
+ - 刻意不做 Info.plist / AndroidManifest 注入:配置双源必然漂移
327
+ - 砖机场景自洽:设备能被坏热更砖掉,说明它至少健康跑过一次 JS,配置早已落盘
328
+
329
+ ### 10.2 流程
330
+
331
+ 冷启动 + 延迟 5s(R5),后台线程,每次冷启动至多一轮:
332
+
333
+ ```text
334
+ 读 config(无则退出)+ 原生 state(currentVersion / rolledBackVersion /
335
+ packageVersion / buildTime / uuid / supportedDiffVersion / bundleHash 缓存)
336
+ → BuildCheckRequestBody(bundleHash 同步读缓存,缺省省略字段)
337
+ → OrderEndpointCandidates(endpoints, 原生随机数)
338
+ → 顺序请求,单个 connect/read 超时 10s、whole-call 15s、整轮 HTTP 最多
339
+ 8 次;全失败 → 拉 queryUrls
340
+ (任一成功即用)合并新候选,排除已失败的,再顺序一轮;仍失败 → 本轮放弃
341
+ → HandleCheckResponse(响应原文, identity, isDev=false)(已实现,含 info 透出)
342
+ → action=download:按 attempts 顺序走现有下载器(diff→pdiff→full,
343
+ testUrls 语义由原生逐个尝试实现);diff/pdiff 共享 600s 绝对 deadline,
344
+ full 另有 600s 救砖预算;同 hash 完整版本在任务真正开跑时再次跳过,失败
345
+ 清理不得删除已有 `.pushy-complete` 安装;成功 → setLocalHashInfo(info 的
346
+ name/description/metaInfo) → 按 afterDownload 决定是否 setNeedUpdate
347
+ → 原生处理完成后,将响应原文 + **响应到达时刻** + 请求/配置指纹落盘
348
+ (§10.3;下载耗时不得让旧响应获得新的时间戳)
349
+ ```
350
+
351
+ ### 10.3 与 JS 的去重(§6 的落地形态)
352
+
353
+ 首版**原生只写缓存**:响应原文 + 响应到达时间戳 + 请求/配置指纹落到固定文件。紧
354
+ 随其后的 JS 小改动:`checkUpdate` 先读该缓存,时间戳新鲜(暂定 2 分钟)
355
+ 则直接复用不发请求。改造前的过渡期是双检查——多一次网络请求,服务端有
356
+ 缓存,无害。
357
+
358
+ ### 10.4 失败策略
359
+
360
+ - 整轮静默失败:无重试风暴、无退避状态机,下次冷启动天然重试
361
+ - 下载/patch 失败不拉黑版本、不计数(本地熔断的教训:多记会毁好版本);
362
+ 每次启动至多重试一轮,行为有界
363
+ - 不引入任何新的回退/降级路径;apk 过期(expired)响应原生不处理,留给 JS UI
364
+
365
+ ### 10.5 安全面
366
+
367
+ 与 JS 路径同一协议、同一 HTTPS endpoints、同一下载器 hash 校验,无新增
368
+ 面。`flow_json` 解析网络数据已做深度上限 + 畸形输入回归(ASan/UBSan)。
369
+
370
+ ### 10.6 分平台落地顺序
371
+
372
+ iOS(NSURLSession,下载/patch/state 全现成,纯增量)→ Android(OkHttp +
373
+ librnupdate.so 进 update_flow_core,绑一次 .so 重编)→ Harmony。e2e 用例
374
+ 沿用 Example/e2etest 既有基建,验证"坏 bundle 下原生仍能拉到修复版"的端到
375
+ 端场景。
376
+
377
+ ### 10.7 forceBoot——策略的按版本远程覆盖(救砖的最后一环,2026-08-08 实现)
378
+
379
+ §10.1 的 `afterDownload` 折算暴露了一个洞:alert 类策略(默认策略)下原生只
380
+ 下载不激活,而砖机的 JS 永远不会跑——修复版躺在磁盘上永不生效,救援在它存
381
+ 在的理由上失效。
382
+
383
+ 解法是把激活决策做成**客户端默认 + 服务端按版本覆盖**:版本 `config` 增加
384
+ `forceBoot: true`(控制台按版本勾选,语义是"强制以该版本启动",不是 UX 层
385
+ 面的"静默")。激活谓词收敛为纯层的 `shouldActivateAfterDownload(info,
386
+ afterDownload)`:本地 silent 策略 或 响应标记 forceBoot 即激活。
387
+ `HandleCheckResponse` 增参 `afterDownload` 并在 download 决策中直接给出
388
+ `activate` 布尔——三端编排器各自只读这一个字段,零判断逻辑。
389
+
390
+ 刻意的语义边界:
391
+ - **仅作用于原生**。JS 侧交互策略不感知不受影响——健康设备该弹窗还弹窗,
392
+ 用户点"取消"只是"这次不切",下次冷启动仍会进入标记版本(原生分不出砖机
393
+ 与健康设备,这正是显式标记版本想要的触达)。
394
+ - **本机 `rolledBack` 黑名单赢过 forceBoot**(守卫在谓词之前):本机有崩溃
395
+ 证据的版本不会被重装,开发者应重绑到别的版本。
396
+ - **first_time 崩溃保护对强制版本依然生效**:强制启动的版本若也是坏的,
397
+ 下次启动照常回滚,不存在"强制进入坏版本且无法回头"。
398
+
399
+ **服务端存储位置的裁决(2026-08-09,推翻初稿):forceBoot 存 `bindings.config`,
400
+ 不存 `versions.config`。** 两个 config 的意图沿革必须记清,防止再犯:
401
+
402
+ - **`versions.config` 属旧灰度设计,已弃用且在被主动清洗**。旧设计把
403
+ rollout 存在版本上(`config.rollout[packageVersion]`);新设计把 rollout
404
+ 搬到 `bindings.rollout` 列,客户端协议里的 `config.rollout` 形状由服务端
405
+ **从绑定数据合成**。绑定事务里的 `removePackageRolloutConfig` 每次重绑都
406
+ 会从 versions.config 清掉对应的 legacy rollout 键——决策层对
407
+ versions.config 刻意不读,响应里的 config 一律合成,这是现行铁律。
408
+ - **初稿曾把 forceBoot 放进 versions.config 并让绑定路径透传它,已否决**:
409
+ 透传会把未被清洗的 legacy rollout 连带泄漏回响应、让双 config 源复活、
410
+ 与清洗机制逆行。相应改动在四个仓库均已回退。
411
+ - **`bindings.config` 是新设计预留的"这次投放"配置位**(upsert API 全线
412
+ 打通、快照本就 select 它),forceBoot 正是投放属性:救砖 = 把包重绑到正
413
+ 常版本这一动作。存绑定还带来正确的生命周期——重绑即替换绑定,救援结束
414
+ 后标记自动消失,不会像挂在版本上那样永久残留;粒度也收敛到单个
415
+ packageVersion。
416
+ - **客户端协议不变**:客户端仍读响应里 `info.config.forceBoot`,它从哪合成
417
+ 客户端不感知。
418
+
419
+ 已实施(四仓库,本地提交待推送):pushy-server / cresc-server 决策层从
420
+ `binding.config.forceBoot` 合成进下发 config(灰度与全量两分支),绑定列表
421
+ 接口补 select config;pushy-admin / cresc-admin 发布菜单加"全量+强制启动
422
+ (救砖)",已绑定行显示⚡标记 + 切换项(重发同绑定翻转标记)。