react-native-update 10.53.0 → 10.54.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 (106) hide show
  1. package/README-CN.md +31 -31
  2. package/README.md +31 -31
  3. package/android/jni/HDiffPatch/.travis.yml +33 -0
  4. package/android/jni/HDiffPatch/builds/xcode/bestParams.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  5. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  6. package/android/jni/HDiffPatch/builds/xcode/hdiffz.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  7. package/android/jni/HDiffPatch/builds/xcode/hpatchz.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  8. package/android/jni/HDiffPatch/builds/xcode/lz4.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  9. package/android/jni/HDiffPatch/builds/xcode/lzma.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  10. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  11. package/android/jni/HDiffPatch/builds/xcode/testHashClash.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  12. package/android/jni/HDiffPatch/builds/xcode/unitTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  13. package/android/jni/HDiffPatch/builds/xcode/zstd.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  14. package/android/jni/lzma/DOC/7zC.txt +187 -0
  15. package/android/jni/lzma/DOC/7zFormat.txt +469 -0
  16. package/android/jni/lzma/DOC/Methods.txt +173 -0
  17. package/android/jni/lzma/DOC/installer.txt +166 -0
  18. package/android/jni/lzma/DOC/lzma-history.txt +446 -0
  19. package/android/jni/lzma/DOC/lzma-sdk.txt +357 -0
  20. package/android/jni/lzma/DOC/lzma-specification.txt +1176 -0
  21. package/android/jni/lzma/DOC/lzma.txt +328 -0
  22. package/android/jni/lzma/bin/7zS2.sfx +0 -0
  23. package/android/jni/lzma/bin/7zS2con.sfx +0 -0
  24. package/android/jni/lzma/bin/7zSD.sfx +0 -0
  25. package/android/jni/lzma/bin/7zdec.exe +0 -0
  26. package/android/jni/lzma/bin/7zr.exe +0 -0
  27. package/android/jni/lzma/bin/installer/config.txt +5 -0
  28. package/android/jni/lzma/bin/installer/cr.bat +5 -0
  29. package/android/jni/lzma/bin/lzma.exe +0 -0
  30. package/android/jni/lzma/bin/x64/7zr.exe +0 -0
  31. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  32. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  33. package/android/lib/x86/librnupdate.so +0 -0
  34. package/android/lib/x86_64/librnupdate.so +0 -0
  35. package/android/src/main/java/cn/reactnative/modules/update/ApkInstaller.java +62 -30
  36. package/android/src/main/java/cn/reactnative/modules/update/ArchiveLimits.java +44 -0
  37. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +124 -40
  38. package/android/src/main/java/cn/reactnative/modules/update/InstallRecord.java +125 -0
  39. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +43 -11
  40. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +2 -0
  41. package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +90 -0
  42. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +79 -19
  43. package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +21 -0
  44. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -17
  45. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleSupport.java +1 -0
  46. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  47. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +5 -0
  48. package/cpp/patch_core/archive_limits.h +38 -0
  49. package/cpp/patch_core/install_record.h +36 -0
  50. package/cpp/update_flow_core/flow_json.h +1 -0
  51. package/cpp/update_flow_core/update_flow_core.cpp +41 -7
  52. package/cpp/update_flow_core/update_flow_core.h +9 -0
  53. package/cpp/update_flow_core/update_flow_jni.cpp +12 -0
  54. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +2 -0
  55. package/harmony/pushy/src/main/cpp/pushy.cpp +37 -0
  56. package/harmony/pushy/src/main/ets/ArchiveLimits.ts +83 -0
  57. package/harmony/pushy/src/main/ets/DownloadTask.ts +126 -30
  58. package/harmony/pushy/src/main/ets/InstallRecord.ts +129 -0
  59. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +24 -2
  60. package/harmony/pushy/src/main/ets/NativePatchCore.ts +5 -0
  61. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +12 -0
  62. package/harmony/pushy/src/main/ets/UpdateContext.ts +78 -15
  63. package/harmony/pushy.har +0 -0
  64. package/ios/RCTPushy/RCTPushy.mm +384 -25
  65. package/ios/RCTPushy/RCTPushyDownloader.mm +32 -0
  66. package/package.json +25 -2
  67. package/src/NativePushy.ts +13 -0
  68. package/src/client.ts +160 -43
  69. package/src/core.ts +17 -3
  70. package/src/error.ts +3 -0
  71. package/src/index.ts +8 -0
  72. package/src/locales/en.ts +3 -0
  73. package/src/locales/zh.ts +2 -0
  74. package/src/metadata.ts +166 -0
  75. package/src/provider.tsx +52 -17
  76. package/src/type.ts +22 -0
  77. package/src/updateFlowCore.ts +95 -4
  78. package/src/utils.ts +31 -19
  79. package/.gitmodules +0 -6
  80. package/BUNDLEHASH_MIGRATION.md +0 -355
  81. package/NATIVE_CHECKUPDATE_DESIGN.md +0 -539
  82. package/NATIVE_CHECK_FOLLOWUPS.md +0 -514
  83. package/android/jni/HDiffPatch/test/_private_searchBestParams.cpp +0 -301
  84. package/android/jni/HDiffPatch/test/hpatchz_test.cpp +0 -292
  85. package/android/jni/HDiffPatch/test/testHashClash.cpp +0 -436
  86. package/android/jni/HDiffPatch/test/unit_test.cpp +0 -417
  87. package/biome.json +0 -59
  88. package/cpp/patch_core/tests/fixtures/rawstored.lzma2label.patch.bin +0 -0
  89. package/cpp/patch_core/tests/fixtures/rawstored.new.bin +0 -0
  90. package/cpp/patch_core/tests/fixtures/rawstored.old.bin +0 -0
  91. package/cpp/patch_core/tests/fixtures/v96.hbc +0 -0
  92. package/cpp/patch_core/tests/fixtures/v96.meta.json +0 -1
  93. package/cpp/patch_core/tests/fixtures/v96.streampatch.bin +0 -0
  94. package/cpp/patch_core/tests/fixtures/v96.t.hbc +0 -0
  95. package/cpp/patch_core/tests/fixtures/v96.tpatch.bin +0 -0
  96. package/cpp/patch_core/tests/fixtures/v96.tstreampatch.bin +0 -0
  97. package/cpp/patch_core/tests/fixtures/v96b.hbc +0 -0
  98. package/cpp/patch_core/tests/fixtures/v98.hbc +0 -0
  99. package/cpp/patch_core/tests/fixtures/v98.t.hbc +0 -0
  100. package/cpp/patch_core/tests/fixtures/v98b.hbc +0 -0
  101. package/cpp/patch_core/tests/fixtures/v98b.t.hbc +0 -0
  102. package/cpp/patch_core/tests/hbc_transform_test.cpp +0 -384
  103. package/cpp/patch_core/tests/patch_core_test.cpp +0 -970
  104. package/cpp/update_flow_core/tests/flow_vectors.json +0 -1401
  105. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +0 -317
  106. package/scripts/check-expo-version.js +0 -92
@@ -1,355 +0,0 @@
1
- # bundleHash 迁移执行方案
2
-
3
- > 状态:**Phase 0/1/2 已实现、已提交;两台服务端已完成生产迁移与部署(2026-07-29)**
4
- > - DB:pushy 北京 RDS 与 cresc Cloud SQL 均已加 `packages.bundleHash` 列(幂等 runner 在 `scripts/migrate-add-package-bundle-hash.ts`;cresc 经一次性 Cloud Run Job 进 VPC 执行)
5
- > - 服务:pushy `p`/`s1` @ `2026.7.29-a2ecac26`,cresc api/worker/dailyjob @ `2026.7.29-3aabff91`,线上带 `bundleHash` 冒烟均通过
6
- > - **`bundleHashJudge.enabled` 仍为关**——待 CLI/客户端发版、覆盖率起来后再开
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 待观察期后
10
- > 依据:`BUNDLEHASH_DESIGN.md`(设计定稿)。本文只讲**怎么落地**,不重复论证为什么。
11
- > 下游:`NATIVE_CHECKUPDATE_DESIGN.md` 的协议下沉必须等本迁移完成,否则协议下沉后再改协议要返工。
12
-
13
- ---
14
-
15
- ## 0. 一条贯穿全程的铁律
16
-
17
- **客户端哈希的文件,必须与 pdiff 实际打补丁的那个文件字节一致。**
18
-
19
- 不一致就是最坏结果:服务端确信 pdiff 可用、下发、下载完 hpatch 源校验失败,比现在的启发式还糟。
20
-
21
- **所以两侧都不要新写"找 bundle 文件"的逻辑,复用 pdiff 实际的源读取路径**:
22
-
23
- | | 客户端哈希的对象 | pdiff 的源读取(哈希必须与之逐字对齐) |
24
- |---|---|---|
25
- | iOS | `[RCTPushy binaryBundleURL]` 返回的文件(`mainBundle` 的 `main.jsbundle`) | 就是 pdiff 的 `fromBundle`(`RCTPushy.mm:727`) |
26
- | Android | **硬编码 `index.android.bundle`**,`context.getAssets().open()` 读 | `DownloadTask.copyBundledAssetToFile("index.android.bundle", …)`(`DownloadTask.java:322`)——pdiff 源本来就是硬编码的,**不走** `getBundleAssetName()` 反射;自定义 bundle 名的用户 pdiff 今天就打不了、CLI 也传不上(`getApkInfo` 的正则同样硬编码),三处一致地退化到 buildTime 启发式 |
27
- | Harmony | rawfile `bundle.harmony.js` | pdiff 的 `getRawFileContent('bundle.harmony.js')`(`DownloadTask.ts:559`) |
28
- | CLI | 解包后的同名条目 | `getIpaInfo` / `getApkInfo` / `getAppInfo` **已经把 bundleFile 拿在手里了** |
29
-
30
- 最后一行是这次调研最大的好消息,见 §3。
31
-
32
- ---
33
-
34
- ## 1. 实施前定稿(原设计 §11 的四个开放问题)
35
-
36
- | # | 问题 | 定稿 |
37
- |---|---|---|
38
- | 1 | 字段名 | `bundleHash` / `bundleStatus` / `digests`,维持原议 |
39
- | 2 | `digests` 用 sha256 还是复用 OSS md5 | **sha256**。OSS 的 etag 在分片上传下不是内容 md5,不能直接复用;且 §0 的 sha256 工具本来就要进 C++,复用同一套心智 |
40
- | 3 | 控制台聚合口径 | 按 **(app, packageVersion)**。因为对应的开发者动作就是"把这个包传上来",按 app 聚合无法定位到具体动作 |
41
- | 4 | `assetsHash` | **不做**。metro 把受管资源的内容 md5 嵌进 bundle,资源变则 bundle 字节必变,需求趋近于零。等真实反馈 |
42
-
43
- ---
44
-
45
- ## 2. Phase 0 — 客户端(react-native-update)
46
-
47
- ### 2.1 sha256 工具进 `cpp/patch_core`——但 Android 用 `MessageDigest`
48
-
49
- `cpp/patch_core/digest.{h,cpp}`,流式接口 + `Sha256File`,同时为 §8 的下载校验铺路:
50
-
51
- ```cpp
52
- namespace pushy::digest {
53
- class Sha256 { void Update(const uint8_t*, size_t); std::string HexDigest(); };
54
- std::string Sha256File(const std::string& path); // iOS 用
55
- }
56
- ```
57
-
58
- 各端消费方式(审查后修订,原稿要求三端全走 C++):
59
-
60
- | 平台 | 实现 | 理由 |
61
- |---|---|---|
62
- | iOS | `pushy::digest`(podspec 已从源码编译 patch_core,加一个文件即可) | 免费共享 |
63
- | Harmony | NAPI 包装 `pushy::digest`(CMakeLists 同样从源码编译) | 免费共享 |
64
- | **Android** | **`java.security.MessageDigest("SHA-256")`** | `librnupdate.so` 是预编译产物,为这一个函数重编 4 ABI 不值;SHA-256 是标准算法,C++ 侧的 NIST 测试向量同时锚定两个实现 |
65
-
66
- **收益:Phase 0 完全不需要重编 `.so`**,原方案的 R4(ABI/对齐回归)风险在本阶段消失。digest.cpp 照常加进 `.so` 的构建脚本,下次因其他原因重编时自动带上,Phase 3 的 C++ 侧校验直接可用。
67
-
68
- ### 2.2 缓存键:设计文档这里是错的
69
-
70
- 原设计写"缓存键 `(packageVersion, bundle 文件 size, mtime)`"。**Android 和 Harmony 的 bundle 不是磁盘文件**——一个在 APK 的 assets 里,一个在 rawfile 里,都没有独立的 size/mtime 可读。
71
-
72
- 改为按平台取各自的"安装身份":
73
-
74
- | 平台 | 缓存键 | 读取方式 |
75
- |---|---|---|
76
- | iOS | `(packageVersion, 文件 size, mtime)` | `main.jsbundle` 是真实文件,原设计可用 |
77
- | Android | `(packageVersion, PackageInfo.lastUpdateTime)` | 每次安装/覆盖安装都会变,含同版本重装 |
78
- | Harmony | `(packageVersion, BundleInfo.updateTime)` | 已核对 `BundleInfo.d.ts:390`,字段存在 |
79
-
80
- 键存 SharedPreferences / NSUserDefaults / preferences,与现有状态同一存储。
81
-
82
- ### 2.3 读取与计算
83
-
84
- - **Android**:`context.getAssets().open("index.android.bundle")` 流式喂给 `MessageDigest`——**与 pdiff 源读取(`copyBundledAssetToFile`)完全同一 API、同一硬编码名**(§0)。资源不存在(debug / 自定义名 / 无嵌入 bundle)→ 返回空串。
85
- - **iOS**:`Sha256File([[RCTPushy binaryBundleURL] path])`;文件不存在 → 空串。
86
- - **Harmony**:`resourceManager.getRawFileContent('bundle.harmony.js')` 拿到 buffer 后经 NAPI 喂给 sha256(与 pdiff 读法完全一致)。
87
-
88
- 线程:Android `StateSerialRunner`,iOS `_fileQueue`,Harmony taskpool。几 MB 约 10-50ms。
89
-
90
- ### 2.4 暴露给 JS
91
-
92
- **异步 getter,不进 `getConstants`**(AN-6/IO-4 的教训):
93
-
94
- ```ts
95
- // NativePushy.ts
96
- getBundleHash(): Promise<string>; // 计算失败 / debug 构建返回空串
97
- ```
98
-
99
- ### 2.5 JS 接入:预取 + 同步读,checkUpdate 零 await
100
-
101
- 初稿在 checkUpdate 里做 200ms 超时 race,评审否决——没必要的复杂度。定稿:
102
-
103
- ```ts
104
- // core.ts 模块加载时 fire-and-forget 预取,settle 后存模块变量
105
- if (typeof PushyModule.getBundleHash === 'function') {
106
- Promise.resolve(PushyModule.getBundleHash()).then((h) => { bundleHash = String(h || ''); });
107
- }
108
- export const getBundleHash = (): string => bundleHash;
109
-
110
- // client.ts checkUpdate 内:同步读,没有就省略
111
- const bundleHash = __DEV__ ? '' : getBundleHash();
112
- ```
113
-
114
- 拿不到就省略字段,服务端自动退回 buildTime 启发式(判定矩阵第 4 行),下一次检查自然带上。代价是每个进程的**第一次** checkUpdate 可能缺字段(原生缓存命中时预取通常几 ms 内 settle,实际很少缺)——一次退化完全可接受。
115
-
116
- **顺带消掉一个曾踩过的坑**:初稿的 race `await` 插进了"5s 去重检查 → `lastRespJson` 赋值"的同步窗口,导致并发 checkUpdate 双发请求(JS2-1 测试抓住)。同步读方案下 checkUpdate 完全没有新增 await,这类问题在结构上不可能出现。
117
-
118
- ### 2.6 `bundleStatus` 消费(字段先行)
119
-
120
- 服务端上线前不会有这个字段,先把消费侧写好:
121
- - `unknownBundle` → `warn()`(新 i18n key `warn_unknown_bundle`)+ `report({ type: 'bundleMismatch' })` 进 logger
122
- - `afterCheckUpdate` 的 `UpdateCheckState` 透传 `bundleStatus`(result 自带字段,无需额外代码)
123
-
124
- **修订原设计 §7 触达第 1 条**:\"`__DEV__` 下 console.warn 联调期拦截\"实际不可达——dev 构建不上报 bundleHash(metro bundle 无嵌入产物),服务端按判定矩阵第 4 行走 buildTime 启发式,永远不会返回 `unknownBundle`。warn 实际只出现在 release 日志里;开发者触达的主渠道是控制台聚合(触达第 3 条)与 CLI 前置拦截(第 4 条),不受影响。
125
-
126
- ### 2.7 验收
127
-
128
- - ✅ C++ sha256 NIST 向量测试(`bun test:patch-core`,26/26)
129
- - ✅ `bun test`(122,含 5 条新 bundleHash/bundleStatus 用例)/ `bun lint`(biome + tsc + harmony tsc)
130
- - ✅ `javac` 对 RN 0.85.2 AAR(main + oldarch)/ `clang -fsyntax-only` 对 e2etest Pods
131
- - ✅ **iOS + Android 模拟器 e2e**(`Example/e2etest/e2e/bundle-hash.test.ts`,已入 detox 套件、随 e2e_ios / e2e_android CI 跑):release 构建、真机路径全链路,原生算出的值与宿主侧对包内 bundle 算的 sha256 **逐字节一致**——
132
- - iOS:JS 预取 → `_fileQueue` 懒计算 → C++ digest → NSUserDefaults;宿主 `simctl get_app_container` + `plutil` 直读容器 plist 比对 `sha256(main.jsbundle)`;含缓存命中路径
133
- - Android:`AssetManager.open` 解压字节 == APK zip 条目字节(bundle 压缩存储,这是 iOS 覆盖不到的独立风险点)、`MessageDigest` == 标准 sha256;宿主 `adb root` 读 `shared_prefs/update.xml` 比对 `unzip -p` 提取值;gradle 构建顺带完成 newarch codegen 对 `getBundleHash` 的真实编译验证
134
- - ⬜ Harmony 真机比对未做:NAPI 直连已过 NIST 向量的 C++ digest,读法与 pdiff 完全同源(`getRawFileContent`),风险最低
135
- - **e2e 踩坑记录**:`simctl spawn defaults read` 看不到 app 容器 defaults(必须 `get_app_container` + `plutil -extract`);detox jest 环境把全局 `expect` 换成元素断言版(普通值断言用显式比较);staged 目录为空壳时 gradle autolinking 缓存会记住"无 android 平台",重 stage 后须清 `android/build/generated/autolinking`;Android release 下 detox `launchApp` 的会话握手在本机 arm64 模拟器挂起——本用例不需要 UI 交互,Android 分支用 `adb shell monkey` 拉起绕开
136
-
137
- ---
138
-
139
- ## 3. Phase 1 — CLI(react-native-update-cli)
140
-
141
- **成本远低于设计文档的预期。** `getIpaInfo` / `getApkInfo` / `getAppInfo`(`src/utils/index.ts:145/187/230`)为了校验"包里有没有 bundle",**已经把 bundle 的 Buffer 读出来了**,而且缺失时已经报错。
142
-
143
- 所以每个函数加一行:
144
-
145
- ```ts
146
- return { versionName, buildTime, bundleHash: sha256(bundleFile), ...appCredential };
147
- ```
148
-
149
- 再在 `uploadNativePackage`(`src/package.ts:170`)的 `package/create` 请求体里带上 `bundleHash`。
150
-
151
- 另外两件事:
152
- - AAB **不需要单独处理**:`uploadAab` 是先 `AabParser.extractApk` 转 APK 再委托 `uploadApk`(`package.ts:258`),自动经过 `getApkInfo`
153
- - 上传时若同 `versionName` 已存在**不同** `bundleHash` 的包记录 → 提示"重打包且 JS 有变"(设计 §7 第 4 条)。这需要服务端先提供查询,**可以延后到 Phase 2 之后**,不阻塞
154
-
155
- 老服务端会忽略未知字段——已验证:`pushy-server` 的 `dto.ts:280` 注释明确"未知字段由 Elysia 默认 normalize 剥离,保留前向兼容"。Phase 1 可以独立发布。
156
-
157
- ---
158
-
159
- ## 4. Phase 2 — 服务端(pushy-server)
160
-
161
- ### 4.1 最容易漏、漏了最严重的一条
162
-
163
- **`buildCheckUpdateCacheKey` 必须加 `bundleHash`。**(`code/src/utils/checkUpdateCapabilities.ts:51`)
164
-
165
- 现在的键是 `[appId, checkUpdateRevision, packageVersion, fromHash, buildTime, cacheBucket, toHash]`。一旦响应内容开始随 `bundleHash` 变化(给不给 pdiff、`bundleStatus` 是什么),不进键就是**跨设备串响应**:一个装了未注册二进制的设备可能拿到另一个设备的"给 pdiff"缓存,直接踩中 §0 那个最坏结果。
166
-
167
- 基数不会明显上升——`bundleHash` 与 `buildTime` 基本一一对应,Phase 3 删掉 buildTime 后反而回落。
168
-
169
- ### 4.2 数据与判定
170
-
171
- - `packages` 表加 `bundleHash VARCHAR(64)`(`prisma/schema.prisma:60`),`getCachedCheckUpdatePackage` 的 select 加该字段
172
- - 判定矩阵(设计 §6)落在 `handleCheckUpdate.ts` 的 `checkAndGenerateTaskAndCdnUrl` 之前:决定"这次允不允许下发 pdiff"
173
- - `digests` 下发 + `__diff.json` 的 copies 附摘要
174
-
175
- ### 4.2.1 不实现 `touchesPackageAssets`(推翻原设计 §6)
176
-
177
- 原设计要求 pdiff 生成时预计算 `touchesPackageAssets`(manifest 是否含"从包内拷非 bundle 文件"),用于门控"bundleHash 命中但 buildTime 不符"这一行。**该标记不应实现**:
178
-
179
- 查 CLI 的 pdiff 生成(`react-native-update-cli/src/diff.ts:695-710`):
180
-
181
- ```js
182
- // If same file.
183
- if (originEntries[entry.fileName] === entry.crc32) {
184
- copies[entry.fileName] = ''; // 同路径拷贝
185
- return;
186
- }
187
- const movedFrom = originMap[entry.crc32];
188
- if (movedFrom) {
189
- copies[entry.fileName] = movedFrom; // 按内容定位拷贝
190
- return;
191
- }
192
- await addFileFromZipEntry(...); // 只有新文件才打进 patch 包
193
- ```
194
-
195
- **ppk 里每一个与二进制包 CRC32 相同的非 bundle 文件都会进 `copies`**。任何带图片/字体的真实应用,该标记恒为 true → "重打包但 JS 未变"这一行永远拿不到 pdiff。
196
-
197
- 而这一行正是设计 §1 点名的最常见场景、整个方案的头号收益来源。**这个机制会把它自己要解决的主场景关掉。**
198
-
199
- 它防的场景设计文档自己也判定为 ≈ 不存在:metro 把受管资源的内容 hash 嵌进 bundle,资源变则 bundle 字节必变、bundleHash 不会命中,根本落不到这一行。
200
-
201
- **代之以运行时保证**(设计 §8 的 apply 校验,已建好一半):
202
-
203
- 1. 该行直接下发 pdiff
204
- 2. `copiesCrc`(`diff.ts:645`,目前只记 "moved" 条目,为 APK→AAB 路径变化而设)扩展到**全部** copies 条目
205
- 3. 客户端拷贝时校验,不符 → fallback full。代价退化为一次 pdiff 下载,**永不装出坏包**
206
- 4. 给该 fallback 加遥测
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
-
214
- 若遥测显示这一行的 pdiff 失败率显著,再引入**精确**判据(如"copies 含非 metro 资源"),而不是用恒为 true 的粗标记一刀切。
215
-
216
- ### 4.3 开关与灰度
217
-
218
- 判定逻辑挂 `dynamicConfig` 开关,默认关。开启后异常可一键退回 buildTime 启发式——**这是 Phase 2 唯一的回退手段,必须先有开关再有逻辑**。
219
-
220
- ### 4.4 实现状态与两处实施决策
221
-
222
- 已落地(`pushy-server` 工作区,未提交,`bun test` 841 全绿):
223
-
224
- - `packages.bundleHash VARCHAR(64) NULL`(schema + `migrations/add_package_bundle_hash.sql`);`package/create` 接收并存储(DTO 带 `^[0-9a-f]{64}$` 校验)
225
- - `checkUpdateBodyJson` 接收 `bundleHash`;`buildCheckUpdateCacheKey` 进键并 bump `cu:v3→v4`;`cu:manifest` 也 bump `v1→v2`(select 加了字段,避免旧缓存形状被误判为"老 CLI 上传")
226
- - 判定矩阵在 `handleCheckUpdate`,`dynamicConfig.bundleHashJudge.enabled` 门控(默认关);`unknownBundle` 的 pdiff 扣发复用 artifact 状态机的 `'empty'` 语义(天然做到"不下发、不入队")
227
- - 测试:矩阵五行 + 开关关死回退 + 缓存键分裂,7 条新用例
228
-
229
- 实施决策两处(文档此前未明确):
230
-
231
- 1. **`unknownBundle` 不再 block**。今天 buildTime 不符是 6 小时 block(`ApiErrorWithOp`);矩阵命中后同样场景变为"扣 pdiff、照发 full",终端用户无感知。这是行为放宽,也是设计本意(§7"所有分支自动降级到正确路径")。
232
- 2. **`ignoreBuildTime` 的 app 在开关开启后变严**:此前它无条件发 pdiff,现在内容不符会扣发。正确——内容不符的 pdiff 下载后 hpatch 源校验必败,本来就是白费流量。
233
-
234
- ### 4.4.1 cresc-server 同步(两态矩阵版)
235
-
236
- cresc-server 是独立仓库(结构同构、历史独立,`handleCheckUpdate` 已漂移约 700 行),Phase 2 已手工移植(`bun test` 760 全绿)。关键差异:
237
-
238
- - **cresc 没有 buildTime 启发式**(连 `ignoreBuildTime` 都已删除,见其 `drop_apps_ignore_build_time.sql`),今天 pdiff 是**无条件信任**的——正是设计 §1 "过松"失效模式的现役实例:不匹配的二进制会白下载 pdiff、hpatch 源校验失败再回落 full。bundleHash 判定对 cresc 是**第一道真正的门控**
239
- - 矩阵塌缩为**两态**:`matched` / `unknownBundle`(rebuiltSameJs 的定义依赖 buildTime 分歧,无 buildTime 则与 matched 不可区分,也无需区分——客户端对二者都静默)
240
- - 回退语义不同:开关关闭 = 回到"无条件信任 pdiff"(pushy 端是回到 buildTime 启发式)
241
- - 其余同构:`packages.bundleHash` 列 + migration、DTO、`cu:v3→v4`、`cu:manifest:v1→v2`、`'empty'` 语义扣发、`dynamicConfig.bundleHashJudge.enabled` 默认关
242
-
243
- 注:cresc-server 干净树上有两个**存量** TS 错误(`src/routers/user.ts:916/957`,email 变更通知的 audit extra 字段),与本次改动无关,未处理。
244
-
245
- ### 4.5 Phase 2 遗留(不阻塞开启开关)
246
-
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 是"下发了什么产物"维度,混入二进制身份维度不干净
248
- - `digests` 下发 + `__diff.json` copies 附摘要——与客户端 Phase 3 的消费侧同波次做
249
- - ~~CLI 上传冲突提示~~ **已实现(改判为无需动 CLI)**:`(appId, name)` 唯一约束下重复上传本来就 409,在两台服务器的冲突分支里用 bundleHash 增强语义——内容一致 →"无需重复上传"、内容不同 →"重打包且 JS 有变,请提升原生版本号"、任一侧无 hash → 通用文案不变。查询仅发生在冲突路径。cresc 3 条路由级用例覆盖;pushy 无路由级测试脚手架,靠同构代码 + cresc 用例 + tsc 兜底
250
-
251
- 另:cresc-server 顺手修了一个与本迁移无关的存量 bug——`/email/confirm` 的 `select` 漏了 `extra`/`name`(`user.ts:888`),导致改邮箱时 OAuth 凭据吊销实际不生效(安全相关)+ 通知邮件用户名恒为兜底文案。
252
-
253
- ### 4.6 推进门槛
254
-
255
- Phase 2 的收益与 Phase 0/1 的覆盖率成正比,但**不需要等覆盖率**:判定矩阵第 4/5 行本来就规定了"没上报 bundleHash → 原样走 buildTime 启发式"。所以 Phase 2 可以在覆盖率很低时就上线,收益随客户端升级自然增长。
256
-
257
- 真正要看的指标:`unknownBundle` 占比。它高说明教育有效(发现了真问题),持续不降说明 §7 的触达链路没起作用。
258
-
259
- ---
260
-
261
- ## 5. Phase 3 — 收尾(客户端)
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
-
275
- ### 5.1 `SyncBinaryVersion` 迁移有个状态危险,必须处理
276
-
277
- 设计文档写"`SyncBinaryVersion` 改 `(packageVersion, bundleHash)`"。直接改会出事:
278
-
279
- ```
280
- 现状:sp 里存的是 build_time
281
- 改后:代码拿 bundle_hash 去比 sp 里的 build_time
282
- 结果:必然不等 → 判定"二进制变了" → 清空全部热更状态
283
- → 所有设备的热更被抹掉一次,全网回内置包
284
- ```
285
-
286
- 这是一次性的全网退版,比本次要解决的问题严重得多。
287
-
288
- **解法**:给持久化状态加一个 schema 版本号,迁移而不是比较。
289
-
290
- ```
291
- 读状态时:
292
- 若 stateSchemaVersion < 2:
293
- 不做 binary 变更判定(这一次跳过)
294
- 把当前 bundleHash 写入,stateSchemaVersion = 2
295
- 保持 currentVersion / firstTime 等原样
296
- 否则:
297
- 正常比较 (packageVersion, bundleHash)
298
- ```
299
-
300
- 即"首次遇到旧格式状态时,认领它而不是否决它"。这是纯函数逻辑,落在 `cpp/patch_core/state_core.cpp`,可以直接单测覆盖。
301
-
302
- ### 5.2 其余收尾
303
-
304
- - 下载产物 `digests` 校验(IO-6 本体)
305
- - apply 时 copies 摘要校验 → 不符即 fallback full,**永不装出坏包**
306
- - 删除 `pushy_build_time` 构建注入:podspec 的 script_phase 写 node_modules(pnpm 下的老问题)、gradle 的 resValue、Harmony 的 meta.json 一并消失
307
-
308
- ### 5.3 buildTime 的降级顺序
309
-
310
- 不要一步到位。`bundleHash` 为空时(计算失败、老二进制)仍需要一个身份标识,所以:
311
-
312
- 1. 先让 `SyncBinaryVersion` 变成 `(packageVersion, bundleHash || buildTime)`
313
- 2. 观察一个版本周期,确认 `bundleHash` 为空的比例足够低
314
- 3. 再删构建期注入
315
-
316
- ---
317
-
318
- ## 6. 分阶段发布与回退
319
-
320
- | Phase | 仓库 | 可独立发布 | 回退方式 |
321
- |---|---|---|---|
322
- | 0 | react-native-update | ✅ | 老服务端忽略未知字段,天然无害 |
323
- | 1 | react-native-update-cli | ✅ | 同上 |
324
- | 2 | pushy-server | ✅ | `dynamicConfig` 开关(§4.3) |
325
- | 3 | react-native-update | ❌ 依赖 Phase 2 稳定 | 各项独立回退;§5.1 的 schema 版本号不可回退,需谨慎发布 |
326
-
327
- 任意新老组合的行为 = 两侧能力交集,最差退化为现状。
328
-
329
- ---
330
-
331
- ## 7. 风险
332
-
333
- | # | 风险 | 处置 |
334
- |---|---|---|
335
- | R1 | 客户端与 CLI 哈希了不同的文件 | §0:两侧都复用已有的 pdiff 源解析逻辑,不新写;验收里有手工比对 |
336
- | R2 | 服务端缓存串响应 | §4.1,必须先改缓存键再上判定逻辑 |
337
- | R3 | Phase 3 的状态迁移抹掉全网热更 | §5.1 的 schema 版本号;纯函数,单测覆盖 |
338
- | R4 | ~~`librnupdate.so` 重编引入对齐/ABI 回归~~ | **Phase 0 已消除**:Android 改用 MessageDigest,不动 `.so`(§2.1)。Phase 3 重编时 CI 的 `verify-android-so.js` 兜 16KB 对齐 |
339
- | R5 | 自定义 bundle 名 / Expo / use_frameworks 下客户端定位不到文件 | 定位不到就返回空串 → 退回 buildTime 启发式,不比现状差。自定义名三处(客户端哈希 / pdiff 源 / CLI 提取)一致硬编码,退化行为对齐(§0) |
340
- | R6 | 首次安装后第一次 checkUpdate 拿不到哈希 | §2.5 的 200ms 超时 + 省略字段,下次自然带上 |
341
- | R7 | "重打包但 JS 未变"行下发的 pdiff 因包内资源漂移而 apply 失败 | §4.2.1:apply 时校验 + fallback full + 遥测。这是本方案里唯一被下调保护强度的地方,用运行时保证换回主场景的收益 |
342
-
343
- ---
344
-
345
- ## 8. 建议的执行顺序
346
-
347
- Phase 0 与 Phase 1 无依赖,**可并行**,且 Phase 1 只有几行、应当先合(先让平台开始积累 `bundleHash` 记录,Phase 2 上线时就有数据可判)。
348
-
349
- ```
350
- Phase 1 (CLI, 数行) ─┐
351
- ├─> Phase 2 (服务端, 带开关) ─> 观察 ─> Phase 3 (收尾)
352
- Phase 0 (客户端) ─┘
353
- ```
354
-
355
- **Phase 3 之前不要动 `NATIVE_CHECKUPDATE_DESIGN.md` 的协议下沉**——协议此时还在变。