Skip to content

fix(mobile): 通过热更恢复 SSO Canary 匿名 OTA - #3838

Merged
MagicLizi merged 2 commits into
mainfrom
fix/mobile-sso-canary-ota-hotfix
Sep 3, 2026
Merged

fix(mobile): 通过热更恢复 SSO Canary 匿名 OTA#3838
MagicLizi merged 2 commits into
mainfrom
fix/mobile-sso-canary-ota-hotfix

Conversation

@guyong-zapo

@guyong-zapo guyong-zapo commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

这次改了什么

摘要

本 PR 替代已关闭的 #3824,在不修改 Mobile 原生配置、不改变 runtime fingerprint 的前提下,为 #3359 增加一个纯 JS OTA bridge:

  • 自建 OTA 在每次 check → fetch → reload 事务前临时把 EAS-Client-ID 覆盖为所有设备共用的固定 UUID,release / canary / beta 继续保留各自路由语义。
  • 启动、回前台、手动检查统一经过同一个串行协调器,自建线不再复用 analytics consent;非自建 EAS / TestFlight 的 consent 闸门保持不变。
  • bridge 本身仍按旧 header 启动;没有新 update 时恢复旧基线。只有共享 UUID 下载到不同 update ID 后,才持久切换到共享基线,避免 Expo 因 URL / requestHeaders 精确不匹配而回落包内 JS。
  • 原生请求超过 JS 等待预算后仍在协调器队列中排空;晚到的成功 fetch 会先提交新基线,再释放下一次 OTA 事务。
  • TapDB consent 与企业 SSO 的协议豁免逻辑保持原样,不调用 acceptPrivacyConsent()
  • 增加 SSO + consent=false + Canary=true、超时晚到、并发串行、emergency launch 与原生配置守卫测试;补齐 U1→U2 跨冷启动、首次基线写入失败及排队期间账号 / channel 切换回归。

变更类型

  • feat 新功能
  • fix 缺陷修复
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护
  • 其他:

范围

UI 变化

不涉及:settings.tsx 只调整更新检查的能力注入,没有修改布局、样式或 UI 文案。

  • 引用的设计规范:不涉及:纯更新链路与测试改动

怎么验证的

自动验证

pnpm test:unit:related
结果:通过;因本地 main 基线关系保守升级为全仓单测,test runner 502 passed / 1 skipped,所有可运行 workspace 通过

pnpm --filter mobile run --if-present typecheck
结果:通过

pnpm --filter mobile exec vitest run src/update/otaRequestCoordinator.test.ts src/update/manualUpdateCheck.test.ts src/update/resumeUpdateCheck.test.ts src/__tests__/mobileSettings.test.ts src/__tests__/nativeAppConfig.test.ts
结果:5 files / 86 tests 全部通过

node apps/mobile/scripts/ci-fingerprint.mjs compute --output <before|after>.json
结果:before / after 报告逐字节一致
  iOS:     83d4428e730ac0d899dff43e75836097b7fbab14
  Android: 61e47edc892d28ed2067b9968f117114634042ef

pnpm check:dco
结果:通过,2 个真实提交已签名

git diff --check
结果:通过

手工验证

不涉及:未生成新的签名 APK / IPA;本方案要求只发布现有 runtime 的 OTA。

未执行的验证

  • 未在 Android / iOS 真机执行“旧客户端 → bridge → 后续不同 update ID”完整迁移演练。
  • 未在真机抓包确认 manifest 与资源请求的 EAS-Client-ID 均为共享 UUID。
  • 发布前应保持 OTA endpoint 与 bridge 下载通道稳定,并在两个平台各做一次迁移演练。

风险

风险分类

  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 存量插件兼容(批准状态 / 指纹 / manifest 校验 / 安装布局 / 包格式)
  • 原生层 / fingerprint / OTA
  • 跨平台差异
  • 其他:

影响与回滚

  • 影响范围:仅自建 Android / iOS 的启动、回前台、手动 OTA;EAS / TestFlight 与 TapDB consent 不变。
  • 冷更边界:没有修改任何 fingerprint 输入;本 PR 合并后只需向现有 runtime 发布 OTA。
  • Bootstrap 限制:已内置 fix(mobile): 同意隐私政策前禁止发起携带 eas-client-id 的 OTA 更新检查 #3359 且当前始终 consent=false 的客户端不会发出首个 OTA 请求,尚未下载的 JS 无法解除旧闸门。它必须先同意一次隐私协议取得 bridge;卸载重装同一个旧整包也不能绕过。纯热更无法覆盖这部分设备。
  • 两阶段迁移:首个 bridge 仍按旧 URL/header 启动,并已经可以匿名检查;下载到另一个不同 update ID 后才永久切到共享基线。晚到设备会把当时最新 OTA 当作 bridge,需要后续另一个 update ID 才完成永久迁移。
  • 进程终止窗口:Expo 的 runtime override 会原生持久化。协调器通过串行、超时恢复和原生请求排空缩短窗口,但进程恰好在临时 shared 配置期间被系统杀死的风险无法由纯 JS 完全消除。
  • 初次基线推断:Expo 不向 JS 暴露当前 update 实际保存的 URL/header,bridge 首次运行只能用当前 endpoint/channel 记录旧基线;发布迁移期间应保持 endpoint 与下载 bridge 时的通道稳定。
  • 回滚 / 降级:若需恢复 consent 限制,应发布一个仍保留 shared header 与基线协调器、只重新加 consent gate 的前向 OTA;已经迁移到 shared 基线后,不可直接回退到 fix(mobile): 同意隐私政策前禁止发起携带 eas-client-id 的 OTA 更新检查 #3359 的 legacy-header 实现,否则会再次造成启动筛选不匹配。

提交前检查

  • 已 review 完整 diff
  • 每个 commit 都带 DCO 签名(git commit -s,见 DCO
  • UI 改动已在「UI 变化」注明引用的设计规范(不涉及视觉 / 文案)
  • 未提交凭证、令牌或授权文件
  • 已补充必要测试
  • 已确认测试结果并说明未执行项

Signed-off-by: guyong <guyong@xd.com>
@guyong-zapo
guyong-zapo requested a review from a team as a code owner September 3, 2026 06:00
@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 为自建 Mobile OTA 增加共享匿名客户端 ID 的纯 JS 两阶段迁移,并让启动、回前台和手动检查共用串行协调器。

  • 在 OTA 事务期间临时应用共享请求头,并持久记录 legacy/shared 启动基线
  • 将三条自建 OTA 入口迁移到统一协调器,同时保留非自建渠道的 consent 闸门
  • 增加超时排空、并发串行、emergency recovery、手动检查与原生配置守卫测试
  • 当前实现遗漏普通 embedded launch 的 null update ID,导致该启动路径无法执行 OTA

Confidence Score: 4/5

该 PR 暂不适合合并,因为普通包内 bundle 冷启动时会在检查更新前被协调器拒绝,无法取得 OTA bridge。

新协调器要求 currentUpdateId 非空,但正常 embedded launch 的 updateId 可以为空;启动入口因此在执行既有 check、fetch 和 reload 流程前失败并静默放行旧版本。

Files Needing Attention: apps/mobile/src/update/otaRequestCoordinator.ts, apps/mobile/src/update/useStartupOtaGate.ts

Important Files Changed

Filename Overview
apps/mobile/src/update/otaRequestCoordinator.ts 新增 OTA 请求头迁移与串行协调器,但 null updateId 处理使普通包内启动无法进入 OTA operation。
apps/mobile/src/update/useStartupOtaGate.ts 启动与 emergency recovery 改由协调器包装;协调器前置拒绝会被此处静默 fail-open。
apps/mobile/src/update/resumeUpdateCheck.ts 将回前台的 check/fetch 组合为可注入的单一协调事务,未发现独立缺陷。
apps/mobile/src/update/manualUpdateCheck.ts 手动检查支持协调客户端,并避免 fetch 未下载新 bundle 时无意义 reload。
apps/mobile/app/settings.tsx 自建渠道注入共享协调器,非自建渠道继续使用动态 consent 闸门。

Sequence Diagram

sequenceDiagram
  participant Gate as 启动 OTA Gate
  participant Coord as OTA 协调器
  participant Expo as expo-updates
  participant Store as AsyncStorage
  Gate->>Coord: runSelfHostedOtaRequest(channel)
  Coord->>Expo: 读取 updateId / runtimeVersion
  alt "普通 embedded launch,updateId=null"
    Coord-->>Gate: 抛出 current-update-id-unavailable
    Gate-->>Gate: catch 后放行业务树
    Note over Gate,Expo: check/fetch/reload 未执行
  else updateId 可用或 emergency 合成 ID
    Coord->>Store: 解析或写入 legacy 基线
    Coord->>Expo: 应用 shared URL/headers
    Gate->>Expo: check → fetch → reload/recovery
    Coord->>Store: 成功下载不同 ID 后写 shared 基线
    Coord->>Expo: 恢复最终基线配置
  end
Loading
Prompt To Fix All With AI
### Issue 1
apps/mobile/src/update/otaRequestCoordinator.ts:376-377
**普通包内启动无法检查 OTA**

当自建客户端正常运行包内 bundle、`Updates.updateId``null` 且不处于 emergency launch 时,这里会把空 ID 传给协调器,导致 `resolveBaseline` 在执行 `runStartupOtaUpdate` 前抛错。新安装或仍运行包内 JS 的客户端因此会跳过启动 OTA,无法通过该入口取得后续更新。

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(mobile): 通过热更恢复 SSO Canary 匿名 OTA" | Re-trigger Greptile

Comment thread apps/mobile/src/update/otaRequestCoordinator.ts
@MagicLizi MagicLizi added touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) labels Sep 3, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor

命中 UI 路径(apps/mobile/app/settings.tsx / apps/mobile/src/update/manualUpdateCheck.ts / apps/mobile/src/update/otaRequestCoordinator.ts 等)但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范。

@MagicLizi

Copy link
Copy Markdown
Contributor

这条 PR 会改自建 OTA 的隐私闸门:未同意 analytics / 隐私政策时,也能用全设备共享 UUID 匿名检查热更,覆盖启动、回前台和设置页手动检查。

这是产品方向和更新链路架构的变化,需要维护者确认后再合。请在 PR 上 Approve;如果要改,请 Request Changes。

讨论 issue:#3839

Signed-off-by: guyong <guyong@xd.com>
@MagicLizi MagicLizi removed the awaiting-discussion 等待维护者讨论(review-pr) label Sep 3, 2026
@MagicLizi
MagicLizi merged commit e04546c into main Sep 3, 2026
20 checks passed
@MagicLizi
MagicLizi deleted the fix/mobile-sso-canary-ota-hotfix branch September 3, 2026 13:22
@MagicLizi

Copy link
Copy Markdown
Contributor

自建 OTA 用共享 UUID 把 SSO Canary 匿名热更救回来,还没绕开非自建渠道的同意门,谢谢。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants