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:
joywayer
2026-06-27 20:04:19 +08:00
co-authored by Claude Opus 4.7
parent 90a523736e
commit 8125dd4d11
6 changed files with 249 additions and 325 deletions
@@ -8,7 +8,7 @@
import Foundation
// MARK: - Codable agent channel marketagent gamelist
// MARK: - Codable 4 agent game channel market
nonisolated public struct RemoteConfig: Codable, Sendable {
public let showmessage: String?
@@ -22,7 +22,6 @@ nonisolated public struct Agent: Codable, Sendable {
public let appDownload: String?
public let gameVersion: String?
public let gameZip: String?
public let channellist: [Channel]?
public let gamelist: [Game]?
public init(from decoder: any Decoder) throws {
@@ -33,12 +32,11 @@ nonisolated public struct Agent: Codable, Sendable {
appDownload = try c.decodeFlexibleStringIfPresent(forKey: .appDownload)
gameVersion = try c.decodeFlexibleStringIfPresent(forKey: .gameVersion)
gameZip = try c.decodeFlexibleStringIfPresent(forKey: .gameZip)
channellist = try c.decodeIfPresent([Channel].self, forKey: .channellist)
gamelist = try c.decodeIfPresent([Game].self, forKey: .gamelist)
}
private enum CodingKeys: String, CodingKey {
case agentid, showmessage, channellist, gamelist
case agentid, showmessage, gamelist
case appVersion, appDownload, gameVersion, gameZip
}
}
+84 -182
View File
@@ -3,15 +3,23 @@
// ylgamehall
//
// RemoteConfig 5 appVersion/appDownload/gameVersion/
// gameZip/showmessage msext chulishengji
// docs/Development-Plan.md ADR-008-D / Design §6.3.2 / msext NewRootVC.m:1372-1538
// gameZip/showmessage
//
// daoqi `NewRootVC.m:689-810`
// agentlist[agentid] gamelist[gameid] channellist[channelid] marketlist[marketid]
//
// market退
// msext "" if
//
// 5 `pickString` / `pickInt`
// " / chulishengji"
//
import Foundation
// MARK: -
/// chulishengji 5
/// 4 fallback 5
public struct ResolvedVersion: Sendable, Equatable {
/// IPA BundleConfig.appVersion 0
public let appVersion: Int
@@ -25,39 +33,32 @@ public struct ResolvedVersion: Sendable, Equatable {
public let showmessage: String?
}
// MARK: -
// MARK: - 4
/// agent game 4 5
private struct SubtreeExtract {
var appVersion: Int = 0
var appDownload: String?
var gameVersion: Int = 0
/// agent `game_download`game `game_zip`
/// key
var gameZip: String?
var showmessage: String?
/// `Agent` / `Game` / `Channel` / `Market`
/// VersionResolver fallback
private protocol RemoteConfigNode {
var appVersion: String? { get }
var appDownload: String? { get }
var gameVersion: String? { get }
var gameZip: String? { get }
var showmessage: String? { get }
}
extension Agent: RemoteConfigNode {}
extension Game: RemoteConfigNode {}
extension Channel: RemoteConfigNode {}
extension Market: RemoteConfigNode {}
// MARK: - VersionResolver
public enum VersionResolver {
/// chulishengji msext `NewRootVC.m:1372-1538`
///
/// 1. agent `agent channel market game`game market
/// 2. game `game game-self channel market`
/// 3. **IPA ** agent `gameConfig.appVersion > agentConfig.appVersion`
/// game NewRootVC.m:1416
/// 4. **zip ** game `agentConfig.gameVersion > gameConfig.gameVersion`
/// agent NewRootVC.m:1456
/// 5. **showmessage** nil退 `config.showmessage`
/// 4 fallback
///
/// - Parameters:
/// - config: RemoteConfigCodable
/// - agentId: BundleConfig.agent
/// - channelId: BundleConfig.channel
/// - marketId: BundleConfig.market
/// - gameId: BundleConfig.gameId
/// - config: RemoteConfig
/// - agentId/channelId/marketId/gameId: BundleConfig
/// - Returns: 5 0 / nil
public static func resolve(
config: RemoteConfig,
@@ -66,184 +67,85 @@ public enum VersionResolver {
marketId: String,
gameId: String
) -> ResolvedVersion {
// agent
guard let agent = config.agentlist?.first(where: { $0.agentid == agentId }) else {
// agent 0 / nil WebContainer ""
return ResolvedVersion(
appVersion: 0, appDownload: nil,
gameVersion: 0, gameZip: nil,
showmessage: config.showmessage
)
}
// agent game
let game = agent.gamelist?.first(where: { $0.gameid == gameId })
let agentExtract = extractAgentSubtree(
agent: agent,
let chain = buildChain(
config: config,
agentId: agentId,
gameId: gameId,
channelId: channelId,
marketId: marketId,
gameId: gameId
marketId: marketId
)
let gameExtract: SubtreeExtract = game.map { g in
extractGameSubtree(game: g, channelId: channelId, marketId: marketId)
} ?? SubtreeExtract()
let (appVer, appDL) = mergeIPA(agent: agentExtract, game: gameExtract)
let (gameVer, gameZip) = mergeZip(agent: agentExtract, game: gameExtract)
// showmessagegame > agent >
let msg = gameExtract.showmessage
?? agentExtract.showmessage
?? config.showmessage
return ResolvedVersion(
appVersion: appVer,
appDownload: appDL,
gameVersion: gameVer,
gameZip: gameZip,
showmessage: msg
appVersion: pickInt(chain, \.appVersion),
appDownload: pickString(chain, \.appDownload),
gameVersion: pickInt(chain, \.gameVersion),
gameZip: pickString(chain, \.gameZip),
// showmessage: 退 config.showmessage
showmessage: pickString(chain, \.showmessage) ?? config.showmessage
)
}
// MARK: - agent getagentversionNewRootVC.m:885-1013
// MARK: -
/// agent 4 agent channel market game
/// game market market.gamelist game
private static func extractAgentSubtree(
agent: Agent,
channelId: String,
marketId: String,
gameId: String
) -> SubtreeExtract {
var r = SubtreeExtract()
// 1agent
applyOnto(&r, from: agent)
// 2channel
guard let channel = agent.channellist?.first(where: { $0.channelid == channelId })
else { return r }
applyOnto(&r, from: channel)
// 3market
guard let market = channel.marketlist?.first(where: { $0.marketid == marketId })
else { return r }
applyOnto(&r, from: market)
// 4game market msext
// Market gamelist channel marketlist
// gamelist Market.gamelist
// RemoteConfig 线
return r
}
// MARK: - game getgameversionNewRootVC.m:1015-1180
/// game 4 game game-self channel market
/// 2 game infotwo channellistmsext
/// agentid game.channellist
private static func extractGameSubtree(
game: Game,
/// 沿 agent game channel market
///
/// 0..4
/// - `[]` agent
/// - `[agent]` agent game
/// - `[agent, game]`
/// - ...
/// - `[agent, game, channel, market]`
private static func buildChain(
config: RemoteConfig,
agentId: String,
gameId: String,
channelId: String,
marketId: String
) -> SubtreeExtract {
var r = SubtreeExtract()
) -> [RemoteConfigNode] {
var chain: [RemoteConfigNode] = []
// 1game
applyOnto(&r, from: game)
guard let agent = config.agentlist?.first(where: { $0.agentid == agentId })
else { return chain }
chain.append(agent)
// 2game-self game.channellist 3
guard let game = agent.gamelist?.first(where: { $0.gameid == gameId })
else { return chain }
chain.append(game)
// 3channel
guard let channel = game.channellist?.first(where: { $0.channelid == channelId })
else { return r }
applyOnto(&r, from: channel)
else { return chain }
chain.append(channel)
// 4market
guard let market = channel.marketlist?.first(where: { $0.marketid == marketId })
else { return r }
applyOnto(&r, from: market)
else { return chain }
chain.append(market)
return r
return chain
}
// MARK: -
// MARK: - fallback
/// IPA agent game game NewRootVC.m:1408-1447
private static func mergeIPA(
agent: SubtreeExtract,
game: SubtreeExtract
) -> (Int, String?) {
//
if agent.appDownload != nil && game.appDownload != nil {
if game.appVersion > agent.appVersion {
return (game.appVersion, game.appDownload)
}
return (agent.appVersion, agent.appDownload)
/// marketagent退
private static func pickString(
_ chain: [RemoteConfigNode],
_ keyPath: KeyPath<RemoteConfigNode, String?>
) -> String? {
for node in chain.reversed() {
if let s = node[keyPath: keyPath], !s.isEmpty { return s }
}
//
if let dl = agent.appDownload {
return (agent.appVersion, dl)
}
if let dl = game.appDownload {
return (game.appVersion, dl)
}
return (0, nil)
return nil
}
/// zip game agent agent NewRootVC.m:1450-1492
private static func mergeZip(
agent: SubtreeExtract,
game: SubtreeExtract
) -> (Int, String?) {
if agent.gameZip != nil && game.gameZip != nil {
if agent.gameVersion > game.gameVersion {
return (agent.gameVersion, agent.gameZip)
}
return (game.gameVersion, game.gameZip)
/// 退
/// JSON string / int / double `decodeFlexibleStringIfPresent`
/// / 0
private static func pickInt(
_ chain: [RemoteConfigNode],
_ keyPath: KeyPath<RemoteConfigNode, String?>
) -> Int {
for node in chain.reversed() {
if let s = node[keyPath: keyPath], let v = Int(s), v != 0 { return v }
}
if let zip = game.gameZip {
return (game.gameVersion, zip)
}
if let zip = agent.gameZip {
return (agent.gameVersion, zip)
}
return (0, nil)
}
// MARK: -
/// SubtreeExtract msext
/// `if (xxx != "" && xxx != nil) self.xxx = xxx`
private static func applyOnto(_ r: inout SubtreeExtract, from agent: Agent) {
if let v = Int(agent.appVersion ?? ""), v != 0 { r.appVersion = v }
if let s = agent.appDownload, !s.isEmpty { r.appDownload = s }
if let v = Int(agent.gameVersion ?? ""), v != 0 { r.gameVersion = v }
if let s = agent.gameZip, !s.isEmpty { r.gameZip = s }
if let s = agent.showmessage, !s.isEmpty { r.showmessage = s }
}
private static func applyOnto(_ r: inout SubtreeExtract, from channel: Channel) {
if let v = Int(channel.appVersion ?? ""), v != 0 { r.appVersion = v }
if let s = channel.appDownload, !s.isEmpty { r.appDownload = s }
if let v = Int(channel.gameVersion ?? ""), v != 0 { r.gameVersion = v }
if let s = channel.gameZip, !s.isEmpty { r.gameZip = s }
if let s = channel.showmessage, !s.isEmpty { r.showmessage = s }
}
private static func applyOnto(_ r: inout SubtreeExtract, from market: Market) {
if let v = Int(market.appVersion ?? ""), v != 0 { r.appVersion = v }
if let s = market.appDownload, !s.isEmpty { r.appDownload = s }
if let v = Int(market.gameVersion ?? ""), v != 0 { r.gameVersion = v }
if let s = market.gameZip, !s.isEmpty { r.gameZip = s }
if let s = market.showmessage, !s.isEmpty { r.showmessage = s }
}
private static func applyOnto(_ r: inout SubtreeExtract, from game: Game) {
if let v = Int(game.appVersion ?? ""), v != 0 { r.appVersion = v }
if let s = game.appDownload, !s.isEmpty { r.appDownload = s }
if let v = Int(game.gameVersion ?? ""), v != 0 { r.gameVersion = v }
if let s = game.gameZip, !s.isEmpty { r.gameZip = s }
if let s = game.showmessage, !s.isEmpty { r.showmessage = s }
return 0
}
}
@@ -195,8 +195,8 @@ public final class SubGameViewController: UIViewController {
private func runBootPipelineSteps() async throws {
// 0. H5 SwitchOverGameData.gamedownloadurl ** url gameid**
// msext gameController.m:544 "ID" zip
// gameid RemoteConfig `game_zip` msext uplevel:
// chulishengji VersionResolver
// gameid RemoteConfig `game_zip` VersionResolver
// 4 fallbackagent game channel market
//
// resolve
// - lobbyResolvedgameId = bc.gameId AppDataWriter app_appversion