ADR-009 凭证集中化:AppSecrets.plist + 七牛运行参数走 RemoteConfig.audio_*(契约影响)

【数据流改造(三分制)】
- 微信 AppID  唯一源 = Info.plist CFBundleURLTypes (URLName=weixin first scheme)
- 应用级凭证 唯一源 = AppSecrets.plist (wxAppSecret/qiniuAccessKey/qiniuSecretKey)
- 七牛运行参数 唯一源 = RemoteConfig 顶层 audio_domain / audio_bucket(远端动态注入)

【契约影响】
- ChannelConfig.plist:11 key → 10 key,移除 qiniudomain(ADR-007 守护规则同步)
- BundleConfig:删除 qiniuDomain 属性
- RemoteConfig:顶层新增可选字段 audioDomain / audioBucket(JSON snake_case 自动归一化)
- 启动期:WebContainerViewController parsed 分支校验 audio_domain/audio_bucket 非空,
  缺失抛 BootError.audioConfigMissing 弹 modal 永停(与 showmessage 同等致命)
- 上线前置:测试 / 生产远端 .txt 配置必须先补 audio_domain / audio_bucket 两个顶层 key
- WeChatSDK.appID / WeChatAuth.appSecret / QiniuConfig.* 调用方零签名变化

【新增】
- ylgamehall/Resources/AppSecrets.plist(3 key)
- ylgamehall/Source/Resource/AppSecrets.swift(单例加载,对齐 BundleConfig 模式)
- QiniuConfig 改 actor:cdnDomain/bucketName 进 actor 状态 + update(...) async;
  accessKey/secretKey 仍 nonisolated(直接读 AppSecrets)
- QiniuTokenSigner.uploadToken() 改 async(bucketName 来自 actor)
- QiniuUploader 预取 cdnDomain 闭包外,SDK 同步 callback 内直接拼 URL

【删除】
- WeChatSDK.swift  static let appID  硬编码 → Info.plist 启动期解析
- WeChatAuth.swift static let appSecret 硬编码 → AppSecrets.shared.wxAppSecret
- QiniuConfig 中 accessKey / secretKey / bucketName / cdnDomain 四处硬编码
- ChannelConfig.plist 的 qiniudomain 字段(plist 与代码双源僵尸字段)

【文档同步】
- Plan:新增 ADR-009 + ADR-007 守护规则改 10 key + §236 BundleConfig 描述
- Design §3.4 多处 "11 项" → "10 项";§7.0.3 plist 示例 + BundleConfig 代码骨架
  + Scripts/inject_channel.sh 同步
- SDK-Integration-Guide §0 凭证位置改三分制 + §尾"七牛域名读取"加新外壳路径
- Verification-Checklist L69 "(11 项)" → "(10 项)"

参考契约章节:docs/Development-Plan.md ADR-009、docs/H5-Native-Implementation-Design.md §7.0.3
BuildProject 通过,Xcode 即时诊断 0 警告。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-27 21:07:55 +08:00
co-authored by Claude Opus 4.7
parent 8125dd4d11
commit 83d9502dee
15 changed files with 260 additions and 67 deletions
+28 -17
View File
@@ -508,10 +508,10 @@ public enum OverlayBridge {
| getter | 返回类型 | 业务语义 | 数据源 |
|---|---|---|---|
| `getchannelName()` | string | 渠道 ID11 项渠道注入之一)| `BundleConfig.shared.channel` |
| `getmarketname()` | string | 市场 ID11 项渠道注入之一)| `BundleConfig.shared.market` |
| `getchannelName()` | string | 渠道 ID10 项渠道注入之一)| `BundleConfig.shared.channel` |
| `getmarketname()` | string | 市场 ID10 项渠道注入之一)| `BundleConfig.shared.market` |
| `getOther()` | string | 渠道 `other` 字段 | `BundleConfig.shared.other` |
| `getothername(name)` | string | 按 H5 传入 key 动态读 11 项渠道注入任意字段 | `BundleConfig.shared.value(forKey: name)` |
| `getothername(name)` | string | 按 H5 传入 key 动态读 10 项渠道注入任意字段 | `BundleConfig.shared.value(forKey: name)` |
| `getcompareCode()` | int | 业务校验码(msext `RootVC.m` 沿用 zip 版本号或固定值) | 待原 msext 取值确认(Phase 2 实施时查 `RootVC.m:1560` 附近 `getcompareCode` 真实返回值,并对齐) |
| `getbattery()` | double | 当前电池电量 0.01.0 | `UIDevice.current.batteryLevel`(启动期 snapshot 一次) |
| `getnetwork()` | int | 当前网络类型(0 无 / 1 WiFi / 2 蜂窝) | `NWPathMonitor` 当前 pathloadFileURL 前 snapshot |
@@ -521,7 +521,7 @@ public enum OverlayBridge {
**为什么不走 BridgeBus 异步 callback**H5 端代码形式是 `var ch = window.settings.getchannelName()``var b = window.settings.getbattery()` 等**同步表达式**(取值后立即用于业务判断),WKWebView 时代 native 无法同步返回 JS 值(异步 evaluateJavaScript 改不了 H5 端代码 → 违反契约原则 A)。唯一不破契约的实现路径:**WebView 加载前在 `documentStart` 注入完整数据快照 + 同步 JS getter polyfill**,本地查询零延迟。
**数据快照时机**
- 静态字段(渠道 / market / other / appVersion 等 11 项渠道注入):app 启动期读 `ChannelConfig.plist` 后即不变,全程一次即可
- 静态字段(渠道 / market / other / appVersion 等 10 项渠道注入):app 启动期读 `ChannelConfig.plist` 后即不变,全程一次即可
- 动态字段(getbattery / getnetwork):**每次 `loadFileURL` 前重新 snapshot 注入**(精度足够,原 msext 自身也只在 `viewDidLoad` 取一次,H5 业务里"启动时刻电量值"被复用整个会话)
- 已安装子游戏列表(getGameinstall):每次 loadFileURL 前扫描沙盒 + 注入 → SwitchOverGameData 解压新子游戏后自然在下次 loadFileURL 刷新
@@ -552,7 +552,7 @@ public enum SettingsBridgePolyfill {
window.__nativeSnapshot = \(json);
window.settings = window.settings || {};
// ── 11 项渠道注入(静态,启动期一次性快照)────────────
// ── 10 项渠道注入(静态,启动期一次性快照)────────────
window.settings.getchannelName = function() { return window.__nativeSnapshot.channel || ""; };
window.settings.getmarketname = function() { return window.__nativeSnapshot.market || ""; };
window.settings.getOther = function() { return window.__nativeSnapshot.other || ""; };
@@ -583,7 +583,7 @@ public enum SettingsBridgePolyfill {
}
public struct Snapshot: Sendable {
public let channelConfig: [String: String] // 11
public let channelConfig: [String: String] // 10
public let channel: String
public let market: String
public let other: String
@@ -1816,13 +1816,14 @@ Phase 6 子游戏(`SwitchOverGameData` 入参的 `Gamedirectory` / `gamedownlo
#### 7.0.3 渠道注入:ChannelConfig.plist 母包模式
**契约 §0.3 描述 msext 用"空目录名注入"机制存储 11 个渠道值,本项目按 ADR-007 改用 `ChannelConfig.plist` 等价实现** —— 契约边界(H5 通过 `app_data.js` 看到的 11 个 JS 全局变量)完全不变,实现内部更简洁。
**契约 §0.3 描述 msext 用"空目录名注入"机制存储 11 个渠道值,本项目按 ADR-007 改用 `ChannelConfig.plist` 等价实现** —— 契约边界(H5 通过 `app_data.js` 看到的 JS 全局变量)完全不变,实现内部更简洁。
**存储**:`ylgamehall/Resources/ChannelConfig.plist`,11 个 string key 一一对应渠道值:
> **2026-06-27 修订(ADR-009)**:原 11 个 key 之一的 `qiniudomain` 已移除 —— 七牛 CDN 域名改由 RemoteConfig 顶层 `audio_domain` 远端动态下发,跨渠道无差异化诉求。**当前 ChannelConfig.plist = 10 个 string key**。
**存储**:`ylgamehall/Resources/ChannelConfig.plist`,10 个 string key 一一对应渠道值:
```xml
<dict>
<key>qiniudomain</key> <string>iosaudio.daoqi88.cn</string>
<key>gameid</key> <string>G2hw0u...</string>
<key>channel</key> <string>FtJf07...</string>
<key>gamedir</key> <string>FtJf07...</string>
@@ -1836,6 +1837,18 @@ Phase 6 子游戏(`SwitchOverGameData` 入参的 `Gamedirectory` / `gamedownlo
</dict>
```
**伴生配置**:`ylgamehall/Resources/AppSecrets.plist`(ADR-009 引入,跨渠道相同的应用级凭证 3 项):
```xml
<dict>
<key>wxAppSecret</key> <string>b27927...</string>
<key>qiniuAccessKey</key> <string>dQbQLU...</string>
<key>qiniuSecretKey</key> <string>RCZpwL...</string>
</dict>
```
> 微信 AppID 不在 AppSecrets.plist —— 它的唯一权威源是 `Info.plist` 的 CFBundleURLTypes(URLName=weixin 首个 scheme),iOS 系统级 URL Scheme 必须在 Info.plist 静态声明,运行期无法注入。
**运行时读取路径**:`Bundle.main.bundleURL.appendingPathComponent("ChannelConfig.plist")`,由 `BundleConfig.swift`(§7.2)用 `PropertyListSerialization` 反序列化。
**多渠道分发**(IPA 后处理,不重新 Xcode build):
@@ -1892,12 +1905,11 @@ public enum SandboxPaths {
```swift
// ResourceKit/BundleConfig.swift
//
// Bundle ChannelConfig.plist 11
// §7.0.3 ChannelConfig.plist / ADR-007
// Bundle ChannelConfig.plist 10
// §7.0.3 ChannelConfig.plist / ADR-007 / ADR-009
public final class BundleConfig: @unchecked Sendable {
public static let shared = BundleConfig()
public let qiniuDomain: String
public let gameId: String
public let channel: String
public let gameDir: String
@@ -1911,7 +1923,6 @@ public final class BundleConfig: @unchecked Sendable {
public init(bundle: Bundle = .main) {
let dict = Self.loadPlist(bundle: bundle)
qiniuDomain = dict["qiniudomain"] ?? ""
gameId = dict["gameid"] ?? ""
channel = dict["channel"] ?? ""
gameDir = dict["gamedir"] ?? ""
@@ -1935,7 +1946,7 @@ public final class BundleConfig: @unchecked Sendable {
}
```
> 七牛 CDN 域名(用于录音上传后的公网 URL 组装)在新项目中统一从 `BundleConfig.shared.qiniuDomain` 读取,不暴露任何全局变量。所有 AudioKit / RecordUploader 等模块通过依赖注入获取 BundleConfig
> 七牛 CDN 域名(录音上传后的公网 URL 组装)从远端 RemoteConfig 顶层 `audio_domain` 注入(ADR-009),启动期由 `WebContainerViewController` 调 `await QiniuConfig.shared.update(cdnDomain:bucketName:)` 写入 `QiniuConfig` actor。所有 AudioKit / RecordUploader 等模块从 `QiniuConfig.shared` 异步读取
>
> **测试性**:`init(bundle:)` 接受任意 Bundle,单测可注入 mock bundle 验证不同 plist fixture。
@@ -1989,9 +2000,9 @@ inject "gameid" "$3"
inject "market" "$4"
inject "agent" "$5"
inject "appversion" "$6"
inject "qiniudomain" "$7"
inject "gamestart" "$8"
inject "gameconfig" "$9"
inject "gamestart" "$7"
inject "gameconfig" "$8"
# qiniudomain 已于 2026-06-27 移除(ADR-009),七牛 CDN 域名走 RemoteConfig.audio_domain
echo "Channel injected: channel=$1 gamedir=$2 market=$4"
```