代码层落地 AppDataWriter + NetworkMonitor + WebContainer 接入

按 Design §7.5 蓝图落地,让 H5 启动时能从沙盒读到实际渠道/启动/设备
值,闭合 Phase 1.17 联调"app_gameconfig 不正确"现象。

新增 Source/WebView/AppDataWriter.swift:
  - public struct AppDataWriter(@MainActor)
  - ContainerRole enum:.lobby / .subGame(name:, dir:)
  - writeInitial():写 app_data.js(12 项)+ app_gamesname.js(暂只写当前
    gameStart 一项,Phase 6 子游戏完整时扩为扫描已装列表)
  - writeBattery(level:):写 app_battery.js(var app_getbattery=N;)
  - writeNetwork(code:):写 app_network.js(var app_getnetwork=N;)
  - 字面严格对齐 msext:
    * 字符串单引号包裹 'value'
    * 数值无引号(version=1 / Launchtype=0 / getwifisignalLevel=1)
    * app_gamesname 用 "var  app_gamesname=new Array(...)"(var 后两空格)
    * 大小写硬约束:Launchtype L 大、getwifisignalLevel wifi 小 + signal/Level 区分
  - escape 转义反斜杠 / 单引号 / 换行(避免渠道字段含单引号导致 H5 JS 解析错)
  - Logger debug 输出文件路径 + 每个 key=value 一行,便于 §7.5.8 联调对账

新增 Source/Resource/NetworkMonitor.swift:
  - @MainActor public final class NetworkMonitor
  - NWPathMonitor 最简 wrap:currentCode 同步快照(默认 2 WiFi,启动后被
    首次 path 更新)+ onChange MainActor 回调
  - shared 单例 + 幂等 start()

WebContainer.runBootPipelineSteps 接入:
  - switch outcome 前提取 let resolved: ResolvedVersion,避免变量作用域
    限制 case 内
  - 新增 writeAppDataFiles(resolved:):启动 NetworkMonitor + 开 battery
    监控 + 写 4 个文件首次值 + 挂 addObserver(battery) + onChange(network)
  - 调用位置:step 5 LobbyZipUpgrader 之后、step 6 loadFileURL 之前
    (与 msext NewRootVC.initJSdata 等价时序)

BuildProject 通过

Plan §6.4 里程碑加一行。

Phase 1.17 联调验证:启动后 H5 console 应读到实际 app_channel /
app_gameconfig 等值,不再是 H5 zip 包内默认占位。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 12:44:45 +08:00
co-authored by Claude Opus 4.7
parent 13cc24b463
commit 829cc8f825
4 changed files with 267 additions and 7 deletions
@@ -0,0 +1,171 @@
//
// AppDataWriter.swift
// ylgamehall
//
// H5 app_*.js loadFileURL 4 .js
// H5 <script src="app_*.js"> //
// msext NewRootVC.initJSdata / changebattery / changenetstate 1:1
//
// docs/H5-Native-Implementation-Design.md §7.5
//
import Foundation
import os.log
@MainActor
public struct AppDataWriter {
public enum ContainerRole: CustomStringConvertible, Sendable {
case lobby
case subGame(name: String, dir: String)
public var description: String {
switch self {
case .lobby: return "lobby"
case .subGame(let n, _): return "subGame(\(n))"
}
}
}
let bundleConfig: BundleConfig
let resolvedVersion: ResolvedVersion?
let containerRole: ContainerRole
private static let log = Logger(subsystem: "ylgamehall", category: "AppDataWriter")
public init(
bundleConfig: BundleConfig,
resolvedVersion: ResolvedVersion?,
containerRole: ContainerRole
) {
self.bundleConfig = bundleConfig
self.resolvedVersion = resolvedVersion
self.containerRole = containerRole
}
// MARK: - Public API
/// app_data.js #1-#12 12 var+ app_gamesname.js#13
/// loadFileURL
public func writeInitial() throws {
try writeAppData()
try writeGamesName()
}
/// #14 app_battery.jsloadFileURL + battery
public func writeBattery(_ level: Float) throws {
// batteryLevel -1 0.0
let normalized = max(level, 0)
let value = String(format: "%.2f", normalized)
let line = "var app_getbattery=\(value);\n"
let url = filePath("app_battery.js")
try line.write(to: url, atomically: true, encoding: .utf8)
Self.log.debug("[\(containerRole.description, privacy: .public)] write app_battery.js: app_getbattery=\(value, privacy: .public)\(url.path, privacy: .public)")
}
/// #15 app_network.jsloadFileURL + NWPathMonitor
public func writeNetwork(_ code: Int) throws {
let line = "var app_getnetwork=\(code);\n"
let url = filePath("app_network.js")
try line.write(to: url, atomically: true, encoding: .utf8)
Self.log.debug("[\(containerRole.description, privacy: .public)] write app_network.js: app_getnetwork=\(code, privacy: .public)\(url.path, privacy: .public)")
}
// MARK: - Private writers
private func writeAppData() throws {
let bc = bundleConfig
let launchtype: Int
let gameName: String
let gameDir: String
let gameStart: String
switch containerRole {
case .lobby:
launchtype = 0
gameName = bc.gameStart
gameDir = bc.gameDir
gameStart = bc.gameStart
case .subGame(let name, let dir):
launchtype = 1
gameName = name
gameDir = dir
gameStart = name
}
let appVersion = resolvedVersion?.appVersion.description ?? bc.appVersion
// docs/H5-Native-Contract.md §4.2 + msext NewRootVC.m:1204
// - msext "var x='%@';"
// - app_version=1 / app_Launchtype=0 / app_getwifisignalLevel=1
// - app_appversion msext %d NSString msext
// - app_Launchtype L app_getwifisignalLevel wifi + signal/Level
let lines =
"var app_version=1;" +
"var app_gameconfig='\(escape(bc.gameConfig))';" +
"var app_gamedir='\(escape(gameDir))';" +
"var app_gamestart='\(escape(gameStart))';" +
"var app_agent='\(escape(bc.agent))';" +
"var app_appversion='\(escape(appVersion))';" +
"var app_market='\(escape(bc.market))';" +
"var app_channel='\(escape(bc.channel))';" +
"var app_Launchtype=\(launchtype);" +
"var app_getwifisignalLevel=1;" +
"var app_gamename='\(escape(gameName))';" +
"var app_invitationcode='\(escape(bc.other))';"
let url = filePath("app_data.js")
try lines.write(to: url, atomically: true, encoding: .utf8)
Self.log.debug("""
[\(containerRole.description, privacy: .public)] write app_data.js → \(url.path, privacy: .public)
app_version = 1
app_gameconfig = '\(bc.gameConfig, privacy: .public)'
app_gamedir = '\(gameDir, privacy: .public)'
app_gamestart = '\(gameStart, privacy: .public)'
app_agent = '\(bc.agent, privacy: .public)'
app_appversion = '\(appVersion, privacy: .public)'
app_market = '\(bc.market, privacy: .public)'
app_channel = '\(bc.channel, privacy: .public)'
app_Launchtype = \(launchtype, privacy: .public)
app_getwifisignalLevel = 1
app_gamename = '\(gameName, privacy: .public)'
app_invitationcode = '\(bc.other, privacy: .public)'
""")
}
private func writeGamesName() throws {
// Phase 1: gameStart msext AppDelegate.m:259
// Phase 6 SandboxPaths.installedSubGames +
let names = [bundleConfig.gameStart]
let escaped = names.map { "'\(escape($0))'" }.joined(separator: ",")
// msext var + new Array(...)沿
let line = "var app_gamesname=new Array(\(escaped));\n"
let url = filePath("app_gamesname.js")
try line.write(to: url, atomically: true, encoding: .utf8)
Self.log.debug("[\(containerRole.description, privacy: .public)] write app_gamesname.js: \(names.count, privacy: .public) games → \(url.path, privacy: .public)\n games = \(names, privacy: .public)")
}
private func filePath(_ name: String) -> URL {
// msext NewRootVC {gamedir}/{gamestart}/app_*.js
let base: URL
switch containerRole {
case .lobby:
// {Caches}/{gameDir}/{gameStart}/ = SandboxPaths.lobbyIndex
base = SandboxPaths.lobbyIndex.deletingLastPathComponent()
case .subGame(let name, let dir):
// {Caches}/{dir}/{name}/ = SandboxPaths.subGameIndex(dir,name)
base = SandboxPaths.subGameIndex(dir, name).deletingLastPathComponent()
}
return base.appendingPathComponent(name)
}
/// JS string / / msext
///
private func escape(_ s: String) -> String {
s.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
.replacingOccurrences(of: "\n", with: "\\n")
.replacingOccurrences(of: "\r", with: "\\r")
}
}