Design §7.5.3/.8:AppDataWriter 加 os.log 调试输出 + 联调可观察性

用户在 Phase 1.17 联调准备时反馈:app_*.js 写入时应该有控制台输出,
方便调试。本 commit 在 §7.5 蓝图层落实可观察性,Phase 2 实施按此骨架
就有完整 debug log。

§7.5.3 AppDataWriter 改动(同时修正若干与 msext 字面不一致的细节):
  - 引入 os.Logger(subsystem: ylgamehall, category: AppDataWriter)
  - writeAppData / writeGamesName / writeBattery / writeNetwork 全部打印
    {ContainerRole} + 文件路径 + 每个 key=value 一行,便于和 H5
    console.log(app_xxx) 逐项对账
  - ContainerRole 实现 CustomStringConvertible("lobby" / "subGame(name)")
  - Logger.debug 级别:Debug 出 Xcode 控制台,Release 自动过滤
  - privacy: .public 标注(确保字符串值不被 Logger 模糊化为 <private>)

**字面修正**(对照 daoqi/msext 实际代码二次校对):
  - 字符串包裹符 " → '(msext 用 "var x='%@';" 单引号)
  - escape 转义对象从 \" 改为 \'(与单引号包裹配套)
  - 数值字段(version / Launchtype / getwifisignalLevel / appversion)
    去掉引号,沿用 msext "var app_version=1;" / "var app_appversion='%d';"
  - app_gamesname 从 JS array literal [...] 改为 new Array(...),
    var 后两空格,严格 AppDelegate.m:259 字面

§7.5.8 新增「联调时的可观察性(Phase 1.17 调试手段)」:
  - 路径 1:Xcode 控制台 — 原生 os.log 输出格式样例
  - 路径 2:沙盒文件直接 cat — xcrun simctl get_app_container 命令 +
    4 个 .js 文件 cat 验证
  - 路径 3:H5 console — Safari Web Inspector 逐项 console.log(app_xxx)
    样例,标注 ⚠️ 大小写硬约束(Launchtype L 大写 / battery 带 get 前缀)
  - 路径 4:真机 Safari 调试启用方法
  - Release 注意事项:os.log .debug 自动过滤避免性能开销和敏感字段外泄

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 08:09:57 +08:00
co-authored by Claude Opus 4.7
parent 0cdd709e66
commit 9f4ccb762e
+129 -32
View File
@@ -2007,8 +2007,12 @@ done
#### 7.5.3 Swift 骨架
设计带可观察性:每次写文件都在 Debug 模式下把"写了哪个文件 + 内容预览 + 路径"打到控制台,便于联调时一眼对账 H5 端读到的值。Release 自动去 `.debug` 级别避免性能开销 + 防泄漏。
```swift
// Source/WebView/AppDataWriter.swift
import os.log
@MainActor
public struct AppDataWriter {
@@ -2016,13 +2020,22 @@ public struct AppDataWriter {
let resolvedVersion: ResolvedVersion? // fallback nil
let containerRole: ContainerRole // .lobby / .subGame(name:)
public enum ContainerRole {
public enum ContainerRole: CustomStringConvertible {
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))"
}
}
}
/// #1-#12 app_data.jsloadFileURL
/// app_gamesname.js#13
/// Debug Release
private static let log = Logger(subsystem: "ylgamehall", category: "AppDataWriter")
/// #1-#12 app_data.js + #13 app_gamesname.jsloadFileURL
public func writeInitial() throws {
try writeAppData()
try writeGamesName()
@@ -2030,16 +2043,19 @@ public struct AppDataWriter {
/// #14 app_battery.js / viewWillAppear
public func writeBattery(_ level: Float) throws {
let line = "var app_getbattery = \"\(String(format: "%.2f", level))\";\n"
try line.write(to: filePath("app_battery.js"),
atomically: true, encoding: .utf8)
let value = String(format: "%.2f", level)
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.jsNWPath / viewWillAppear
public func writeNetwork(_ code: Int) throws {
let line = "var app_getnetwork = \"\(code)\";\n"
try line.write(to: filePath("app_network.js"),
atomically: true, encoding: .utf8)
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)")
}
private func writeAppData() throws {
@@ -2064,33 +2080,53 @@ public struct AppDataWriter {
let appVersion = resolvedVersion?.appVersion.description ?? bc.appVersion
// var string JS ==
// msext 沿 string
// msext 沿
// app_version / app_Launchtype / app_getwifisignalLevel / app_appversion
// msext "var app_version=1;" / "var app_appversion='%d';"
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))";
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))';
"""
try lines.write(to: filePath("app_data.js"),
atomically: true, encoding: .utf8)
let url = filePath("app_data.js")
try lines.write(to: url, atomically: true, encoding: .utf8)
// Debug key=value 便 H5 console.log(app_xxx)
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 {
// JS Array literal: ["game1","game2",...]
// JS Array literal: new Array('game1','game2',...) msext AppDelegate.m:259
let installed = SandboxPaths.installedSubGameNames()
let escaped = installed.map { "\"\(escape($0))\"" }.joined(separator: ",")
let line = "var app_gamesname = [\(escaped)];\n"
try line.write(to: filePath("app_gamesname.js"),
atomically: true, encoding: .utf8)
let escaped = installed.map { "'\(escape($0))'" }.joined(separator: ",")
let line = "var app_gamesname=new Array(\(escaped));\n"
// ^ var msext 沿
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: \(installed.count, privacy: .public) games → \(url.path, privacy: .public)\n games = \(installed, privacy: .public)")
}
private func filePath(_ name: String) -> URL {
@@ -2105,10 +2141,11 @@ public struct AppDataWriter {
return base.appendingPathComponent(name)
}
/// JS string
/// JS string msext
///
private func escape(_ s: String) -> String {
s.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
.replacingOccurrences(of: "'", with: "\\'")
.replacingOccurrences(of: "\n", with: "\\n")
.replacingOccurrences(of: "\r", with: "\\r")
}
@@ -2185,6 +2222,66 @@ private func runBootPipelineSteps() async throws {
| §3.4 OverlayBridge polyfill | ✓ **保留** | 弹层 `window.settings.{backgameData/browser/finishweb}`,与系统版本无关 |
| §3.2 事件 callback `getBattery` / `getnetwork` / `appservice` | ✓ **保留** | 业务期 H5 主动收到的变化推送,与 §7.5 文件重写互为补充 |
#### 7.5.8 联调时的可观察性(Phase 1.17 调试手段)
H5 端读到 undefined / 值不对时,第一步是确认"原生写了什么、H5 读到什么"。三条对账路径:
**1. Xcode 控制台 — 原生写入 log(§7.5.3 已埋点)**
启动后 Xcode console 应出现:
```
[lobby] write app_data.js → /Users/.../Caches/<gamedir>/<gamestart>/app_data.js
app_version = 1
app_gameconfig = 'tsgames.daoqi88.cn-config_test-update_jsonv2_test'
app_gamedir = 'FtJf07...'
app_gamestart = 'gamehall'
...
app_Launchtype = 0
app_getwifisignalLevel = 1
app_gamename = 'gamehall'
app_invitationcode = ''
[lobby] write app_gamesname.js: 0 games → /Users/.../Caches/<gamedir>/<gamestart>/app_gamesname.js
games = []
[lobby] write app_battery.js: app_getbattery=0.85 → ...
[lobby] write app_network.js: app_getnetwork=2 → ...
```
**2. 沙盒文件直接读 — 等价 `cat`**
如果控制台 log 没问题但 H5 端仍读 undefined,去沙盒拿实际文件验证写入和"H5 看到的"是否一致:
```bash
# 模拟器路径
xcrun simctl get_app_container booted com.skyapp.ylgamehall data
# 拿到容器路径后
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_data.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_battery.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_network.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_gamesname.js
```
文件应包含 `var app_xxx=...;` 字面字符串。若文件不存在或为空 → AppDataWriter 没跑到 / 路径错;若文件正确但 H5 读 undefined → H5 端 `<script src>` 路径不对或加载时序问题。
**3. H5 console 对账 — Safari Web Inspector**
模拟器跑时 macOS Safari → 开发菜单 → Simulator → 当前 H5 页面 → Web Inspector → Console,逐项 console.log
```js
console.log("app_version =", app_version); // 期望 1
console.log("app_channel =", app_channel); // 期望 ChannelConfig.plist channel 值
console.log("app_Launchtype =", app_Launchtype); // 大厅期望 0,⚠️ L 大写
console.log("app_getbattery =", app_getbattery); // ⚠️ 带 get 前缀
console.log("app_getnetwork =", app_getnetwork);
console.log("app_gamesname =", app_gamesname); // 期望 Array
```
任一项为 `undefined` → 拼写错误(大小写 / 前缀)或文件没加载到。常见错误:写成 `app_launchtype`(小写 l/ `app_battery`(少 get/ `app_getNetwork`(驼峰错)。
**4. 真机调试**iPhone 上 Safari → 设置 → 高级 → 网页检查器开启,然后 macOS Safari 同样路径。
> ⚠️ 上面 H5 控制台和文件路径调试**仅在 Debug 配置生效**。Release 版本 os.log `.debug` 级别会被系统过滤掉,避免性能开销和敏感字段(agent / channel)外泄。Phase 9 Release 监控走 Sentry breadcrumb(§11.3),不依赖 console。
---
## 8. 能力模块详细设计