1.11 修订:VersionResolver 改单链 4 层 fallback,删除双子树合并算法(契约影响)
【契约影响】 - RemoteConfig 模型:Agent 节点移除 channellist 字段(线上 JSON 实际只挂 gamelist;此前误读 chulishengji 引入的 channellist 是空 schema) - VersionResolver 解析算法:从"双子树 + 字段级合并"改为"agentlist → gamelist → channellist → marketlist 单链 4 层 fallback",行为按 daoqi NewRootVC.m:689-810 主路径 - 对外 API ResolvedVersion / resolve(config:agentId:channelId:marketId:gameId:) 签名零变化,WebContainerViewController / SubGameViewController 调用点未动 【设计要点】 - 4 节点类型 conform 同一 private protocol RemoteConfigNode - 5 字段共用 pickString / pickInt 两个倒序 fallback 工具,不允许为某字段单独写 if 链 - buildChain 集中处理"任一层 id 不匹配立刻截断"语义 【文档同步】 - Design §6.3.2 整段重写 - Plan ADR-008 顶部追加"第三轮修订(2026-06-27)"小节 + 第二轮误读复盘 - Plan ADR-008-D 替换为单链 4 层算法说明 + 保留修订史 - Plan §1.11 / ADR-008-I / 最后更新日期 / Verification-Checklist L73 同步 参考契约章节:docs/H5-Native-Implementation-Design.md §6.3.2、docs/Development-Plan.md ADR-008-D BuildProject 通过。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
90a523736e
commit
8125dd4d11
+56
-66
@@ -883,7 +883,7 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
|
||||
- [x] 1.8 WVJB JS 协议
|
||||
- [x] 1.9 BridgedWebView
|
||||
- [x] 1.10 RemoteConfigClient(actor,URLSession async + 重试 + cache-busting + 短文本 + FlexibleString)
|
||||
- [x] 1.11 VersionResolver(chulishengji 双子树合并算法,纯函数)
|
||||
- [x] 1.11 VersionResolver(单链 4 层 fallback:agent→game→channel→market,纯函数)
|
||||
- [x] 1.12 LocalVersionReader(version.xml 解析 + ATS 全局放行)
|
||||
- [x] 1.13 LobbyZipUpgrader(actor,原子 rename 升级;端到端实测 260→261)
|
||||
- [x] 1.14.a AppIcon 替换 Xcode 默认空模板(iPad / 1024 缺失留 Phase 10 polish)
|
||||
@@ -1106,16 +1106,21 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
|
||||
- 不允许把渠道值硬编码到 Swift 源码(违背"母包模式"的初衷:一份二进制 N 个渠道)
|
||||
- **回滚条件**:若未来 Xcode / iOS 改动让 plist 方案无法工作(极不可能)或后处理脚本失效,重新评估目录名方案或其它存储介质
|
||||
|
||||
### ADR-008:Phase 1 补全远程配置 + 版本对比 + zip 升级(2026-06-22,含 2 次调研 revise)
|
||||
- **背景**:Plan 初版 Phase 1 任务清单(13 项)只覆盖"启动 → 解压 Bundle 内 zip → 加载 H5",**漏掉了 msext 启动流程里最关键的一环**:从 `gameconfig` 拼接出的远端 `.txt` 拉真实配置 → chulishengji 双子树算法 → 与本地版本比较 → 必要时下载并替换 H5 zip
|
||||
- **触发事件**:2026-06-22 两轮 subagent 调研 `daoqi/msext` 原项目,逐行核对核心方法:
|
||||
- 第一轮(粗粒度):发现远端配置流程整体形态、IPA / H5 zip 两条升级链
|
||||
- 第二轮(精确算法):核对 `NewRootVC.m` 全文,识别出**线上热路径是 chulishengji 双子树合并**,非 onnet 简单 4 层
|
||||
- **核心修正:线上热路径算法**
|
||||
- **不是** `onnet` 的 `if(self.gamelist==nil)` 分支(`NewRootVC.m:689-810`)的"简单线性 4 层覆盖"
|
||||
- **是** `onnet` 的 `else` 分支(`NewRootVC.m:811-877`)→ `gonetconfig1` / `gonetconfigone1` → **`chulishengji`**(`NewRootVC.m:1372-1538`)双子树合并
|
||||
- 触发条件:`viewWillAppear`(`NewRootVC.m:1609-1616`)与 `gonet`(`NewRootVC.m:1252-1258`)拉完 JSON 都强制把当前 agent 节点下 `gamelist` 提到 `self.gamelist`;只要服务器 JSON 里 agent 节点带 `gamelist`(线上 100% 都有),`self.gamelist` 就非 nil → 走 chulishengji 路径
|
||||
- **决策**:在 Phase 1 视图层之前插入 4 个新子项 `1.10 RemoteConfigClient / 1.11 VersionResolver / 1.12 LocalVersionReader / 1.13 LobbyZipUpgrader`,原 `1.10-1.13` 后移为 `1.14-1.17`。**`VersionResolver` 必须按 chulishengji 双子树算法实现,不照搬 onnet 简单 4 层**
|
||||
### ADR-008:Phase 1 补全远程配置 + 版本对比 + zip 升级(2026-06-22 起,含 3 次调研 revise)
|
||||
- **背景**:Plan 初版 Phase 1 任务清单(13 项)只覆盖"启动 → 解压 Bundle 内 zip → 加载 H5",**漏掉了 msext 启动流程里最关键的一环**:从 `gameconfig` 拼接出的远端 `.txt` 拉真实配置 → 单链 4 层 fallback 解析 → 与本地版本比较 → 必要时下载并替换 H5 zip
|
||||
- **触发事件**:2026-06-22 起多轮调研 `daoqi/msext` 原项目,逐行核对核心方法:
|
||||
- 第一轮(粗粒度,2026-06-22):发现远端配置流程整体形态、IPA / H5 zip 两条升级链
|
||||
- 第二轮(2026-06-22,**已废弃**):误读 `onnet` 的 `else` 分支 + `chulishengji`(`NewRootVC.m:1372-1538`),错误地把双子树合并当作线上热路径
|
||||
- **第三轮(2026-06-27,最终采用)**:项目维护者直接指出契约真实结构是单链 4 层 `agentlist → gamelist → channellist → marketlist`。重新核对 `NewRootVC.m`,确认 `onnet` 的 `if(self.gamelist==nil)` **顶层分支**(`NewRootVC.m:689-810`)= 线上热路径就是这条单链;`else` 分支的 chulishengji 是历史遗留代码、主路径不走
|
||||
- **核心算法(第三轮最终版)**:
|
||||
- 单链 4 层节点匹配:`agentlist[agentid] → gamelist[gameid] → channellist[channelid] → marketlist[marketid]`
|
||||
- 任一层 id 匹配失败立刻截断(短链);最长 0..4 节点
|
||||
- 字段语义:每个字段从最深层向根回退取第一个非空值(等价 msext `if(xxx!="" && xxx!=nil) self.xxx=xxx` 逐层覆盖的最终态)
|
||||
- 5 字段(appVersion / appDownload / gameVersion / gameZip / showmessage)**共用同一套** `pickString` / `pickInt` 倒序查找接口,不再有"双子树 / 多分支合并"
|
||||
- **第二轮误读复盘(保留以防再被翻出来)**:
|
||||
- 误读点:`viewWillAppear`(`NewRootVC.m:1609-1616`)与 `gonet`(`NewRootVC.m:1252-1258`)的 `self.gamelist` 同样取自 agent 节点下 `gamelist`,**但流程结构上先走 `onnet` 的 `if(self.gamelist==nil)` 分支命中后立即 return**,不会再进 else 的 chulishengji
|
||||
- 教训:原项目同名变量、不同入口的代码不能凭"看似都赋值了"判定走哪条;要顺着实际调用栈走一遍
|
||||
- **决策**:在 Phase 1 视图层之前插入 4 个新子项 `1.10 RemoteConfigClient / 1.11 VersionResolver / 1.12 LocalVersionReader / 1.13 LobbyZipUpgrader`,原 `1.10-1.13` 后移为 `1.14-1.17`。**`VersionResolver` 按单链 4 层 fallback 实现(第三轮)**
|
||||
|
||||
#### ADR-008-A 远端 URL 构造(`RemoteConfigClient`)
|
||||
|
||||
@@ -1139,71 +1144,56 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
|
||||
- **字段类型混乱(必须兼容)**:`marketid` / `game_version` / `app_version` 等本应为 string 的字段,服务端会发 number。msext 用 NSNumber 静默吞下;**新外壳用 `decodeFlexibleStringIfPresent` 扩展(String / Int / Double / Bool 全兼容)**——已在 1.10 实现
|
||||
- **解析失败行为**:msext 静默 nil → 后续 for 循环 0 次跑空 → App 静默卡死(缺陷)。新外壳应当**显式抛 `ConfigParseFailed` + UI 弹错误页 + 提供重试按钮**
|
||||
|
||||
#### ADR-008-D chulishengji 双子树合并算法(`VersionResolver`)
|
||||
#### ADR-008-D 单链 4 层 fallback 算法(`VersionResolver`)
|
||||
|
||||
新外壳 `VersionResolver.resolve()` 必须 1:1 复刻 `NewRootVC.m:1372-1538` 的算法,输出 5 个目标字段:`(appVersion, appDownload, gameVersion, gameZip, showmessage)`。
|
||||
> **修订史**:2026-06-22 初版误指为 chulishengji 双子树合并;2026-06-27 第三轮按项目维护者澄清推翻,改为本节描述的单链 4 层 fallback。下文以最终版本为准;第二轮内容已删除,复盘说明见 ADR-008 顶部"第二轮误读复盘"。
|
||||
|
||||
新外壳 `VersionResolver.resolve()` 1:1 复刻 `NewRootVC.m:689-810` 主路径,输出 5 个目标字段:`(appVersion, appDownload, gameVersion, gameZip, showmessage)`。
|
||||
|
||||
**契约结构**:
|
||||
|
||||
```
|
||||
agentlist[agentid]
|
||||
└─ gamelist[gameid]
|
||||
└─ channellist[channelid]
|
||||
└─ marketlist[marketid]
|
||||
```
|
||||
|
||||
每一层节点都可独立携带 `app_version / app_download / game_version / game_zip / showmessage` 字段(缺省视为空);越深的层覆盖越浅的层;越深的层缺省时回退到上一层。
|
||||
|
||||
**步骤**:
|
||||
|
||||
```swift
|
||||
// 1. 提取 agent 子树(getagentversion,NewRootVC.m:885-1013)
|
||||
// agent → channel → market → game(game 嵌在 market 命中后才遍历)
|
||||
agentConfig = extractAgentSubtree(agent)
|
||||
// → (app_version, app_download, game_version, game_download, showmessage)
|
||||
// 1. 顺链匹配,返回 0..4 个有序节点
|
||||
// 任一层匹配失败立刻截断(之后不再尝试更深层)
|
||||
let chain: [RemoteConfigNode] = buildChain(
|
||||
config, agentId, gameId, channelId, marketId
|
||||
)
|
||||
// 例:agent 命中、game 未命中 → chain = [agent]
|
||||
|
||||
// 2. 提取 game 子树(getgameversion,NewRootVC.m:1015-1180)
|
||||
// game → game-self → channel → market
|
||||
// (第二层 "game-self" 是把 gamedata 自身当 infotwo 再嵌一遍 channellist;
|
||||
// msext 那段曾经的 agentid 判断被注释掉了)
|
||||
gameConfig = extractGameSubtree(game)
|
||||
// → (app_version, app_download, game_version, game_download, showmessage)
|
||||
|
||||
// 3. 合并 IPA 字段(NewRootVC.m:1408-1447)
|
||||
if both have app_download:
|
||||
result.appVersion = agentConfig.appVersion // 默认 agent 赢
|
||||
result.appDownload = agentConfig.appDownload
|
||||
if gameConfig.appVersion > agentConfig.appVersion: // 版本号大的赢(line 1416)
|
||||
result.appVersion = gameConfig.appVersion
|
||||
result.appDownload = gameConfig.appDownload
|
||||
elif only one has: 用那一份
|
||||
else: nil
|
||||
|
||||
// 4. 合并 zip 字段(NewRootVC.m:1450-1492)
|
||||
if both have game_download:
|
||||
result.gameVersion = gameConfig.gameVersion // 默认 game 赢
|
||||
result.gameZip = gameConfig.gameZip
|
||||
if agentConfig.gameVersion > gameConfig.gameVersion: // 版本号大的赢(line 1456)
|
||||
result.gameVersion = agentConfig.gameVersion
|
||||
result.gameZip = agentConfig.gameZip
|
||||
elif only one has: 用那一份
|
||||
else: nil
|
||||
|
||||
// 5. showmessage:在两条子树 extract 内部多次覆盖(agent 子树 :897/:910/:924/:948;
|
||||
// game 子树 :1027/:1058/:1087/:1114),加上 onnet else 的两次 for 循环 (:826/:852)
|
||||
// 与 gonetconfigone1 :1319。算法上等同于"任意一处非空就覆盖",最后写赢。
|
||||
result.showmessage = mergeShowmessage(top, agent, game, channel, market)
|
||||
// 2. 5 字段共用同一套倒序 fallback 接口
|
||||
// 从最深层向根回退取第一个非空(等价 msext "下层非空即覆盖上层"的最终态)
|
||||
ResolvedVersion(
|
||||
appVersion: pickInt(chain, \.appVersion),
|
||||
appDownload: pickString(chain, \.appDownload),
|
||||
gameVersion: pickInt(chain, \.gameVersion),
|
||||
gameZip: pickString(chain, \.gameZip),
|
||||
showmessage: pickString(chain, \.showmessage) ?? config.showmessage
|
||||
)
|
||||
```
|
||||
|
||||
**agent 子树 4 层结构**(`getagentversion:`):
|
||||
- 第 1 层 agent 自己:`NewRootVC.m:889-915` 提取顶层 4 字段 + showmessage
|
||||
- 第 2 层 channel:`NewRootVC.m:903-940` 嵌 channellist 找匹配
|
||||
- 第 3 层 market:`NewRootVC.m:921-997` 嵌 marketlist 找匹配
|
||||
- 第 4 层 game:`NewRootVC.m:944-996` 嵌在 market 命中后才遍历 game(注意是 market.gamelist,不是 channel.gamelist)
|
||||
**统一接口要求(必须保持)**:
|
||||
- 4 层节点(Agent / Game / Channel / Market)conform 同一 `RemoteConfigNode` 协议,提供相同字段 getter
|
||||
- 字段查找只有 `pickString` / `pickInt` 两个工具,5 字段全走它们 —— 不允许出现"为某个字段写专门 if 链"的多处实现
|
||||
- `pickString` 跳过 nil / 空字符串;`pickInt` 跳过 nil / 解析失败 / 0(msext `[nil intValue] = 0` 行为兼容)
|
||||
|
||||
**game 子树 4 层结构**(`getgameversion:`):
|
||||
- 第 1 层 game 自己:`NewRootVC.m:1019-1045`
|
||||
- 第 2 层 game-self(`infotwo=gamedata` 自身再嵌 channellist):`NewRootVC.m:1048-1073`
|
||||
- 第 3 层 channel:`NewRootVC.m:1075-1100`
|
||||
- 第 4 层 market:`NewRootVC.m:1100-1163`
|
||||
|
||||
**字段名差异(必须留意)**:
|
||||
- agent 子树用 `game_download`(`NewRootVC.m:952`)
|
||||
- game 子树用 `game_zip`(`NewRootVC.m:1037`)
|
||||
- 两者本质同一字段,msext 历史命名不一致。新外壳 Codable 模型用 `gameZip`,但需要在两个 extract 函数里都查这两个 key(择一非空)
|
||||
**与 msext 实际代码的关系**:
|
||||
- msext 顺嵌套 for 循环里"非空即覆盖"`self.xxx = xxx` 的写法 —— **最终态等价**于从根向叶遍历后取最深一处非空值,再等价于从叶向根 fallback 取第一个非空值。三种写法语义一致,新外壳选第三种因为:(1) 5 字段共用同一接口最简洁;(2) `buildChain` 集中处理"任一层失败截断"语义;(3) 单测易写
|
||||
- 不复刻 msext 那段把 `showmessage` 散在 5 处覆盖的写法 —— 用 `pickString(\.showmessage)` 一处搞定,最终态相同
|
||||
|
||||
#### ADR-008-E 决策顺序(`WebContainer` 编排)
|
||||
|
||||
5 字段拿到后**固定顺序**判断(chulishengji 路径在 `NewRootVC.m:1501-1527`):
|
||||
5 字段拿到后**固定顺序**判断(msext 单链主路径在 `NewRootVC.m:768-793`):
|
||||
|
||||
| 优先级 | 条件 | 动作 |
|
||||
|---|---|---|
|
||||
@@ -1241,7 +1231,7 @@ result.showmessage = mergeShowmessage(top, agent, game, channel, market)
|
||||
| URL | HTTP | HTTPS(后台已切) |
|
||||
| cache-busting | `?vXXXXXXXX_YYYYYYYY` | **保留** |
|
||||
| JSON 解析 | SBJSON | Codable + JSONDecoder + 自定义 `decodeFlexibleStringIfPresent`(兼容 number-as-string) |
|
||||
| 算法 | chulishengji 双子树合并 | **复刻 chulishengji**,纯函数 + 单测 |
|
||||
| 算法 | onnet 单链 4 层(`NewRootVC.m:689-810`) | 单链 4 层 + 协议化 fallback,5 字段共用同一查找接口,纯函数 + 单测 |
|
||||
| version.xml | GDataXML | Foundation XMLParser |
|
||||
| 升级解压 | `removeItemAtPath` + ZipArchive | 临时目录 + 原子 rename |
|
||||
| 失败 UI | 静默 / NSLog | 弹错误页 + 重试按钮(不留卡死黑洞) |
|
||||
@@ -1254,4 +1244,4 @@ result.showmessage = mergeShowmessage(top, agent, game, channel, market)
|
||||
---
|
||||
|
||||
文档完成日期:2026-06-21
|
||||
最后更新:2026-06-22(ADR-008 二次精确化 chulishengji 双子树算法 + ADR-007 渠道注入改 ChannelConfig.plist + ADR-006 纯 SPM + Vendor + ADR-005 极光降级 + Resources/ 目录记录)
|
||||
最后更新:2026-06-27(ADR-008 第三轮修订:VersionResolver 改单链 4 层 fallback + 5 字段共用同一查找接口;前置历史 2026-06-22 ADR-008 二次精确化 / ADR-007 渠道注入改 ChannelConfig.plist / ADR-006 纯 SPM + Vendor / ADR-005 极光降级 / Resources 目录记录)
|
||||
|
||||
Reference in New Issue
Block a user