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
+40 -8
View File
@@ -233,8 +233,8 @@ Contract Design Plan(本文档)
- [x] **1.1** 创建 `ylgamehall/Resources/ChannelConfig.plist` 含 11 个渠道键值对(沿用 msext 现网 demo 作为母包默认值;ADR-007 决策) - [x] **1.1** 创建 `ylgamehall/Resources/ChannelConfig.plist` 含 11 个渠道键值对(沿用 msext 现网 demo 作为母包默认值;ADR-007 决策)
- [x] **1.2** 实现 `ylgamehall/Source/Resource/BundleConfig.swift` - [x] **1.2** 实现 `ylgamehall/Source/Resource/BundleConfig.swift`
- `init(bundle: Bundle = .main)`:读 `bundle.url(forResource: "ChannelConfig", withExtension: "plist")`,用 `PropertyListSerialization` 反序列化为 `[String: String]` - `init(bundle: Bundle = .main)`:读 `bundle.url(forResource: "ChannelConfig", withExtension: "plist")`,用 `PropertyListSerialization` 反序列化为 `[String: String]`
- 公开 11 个只读属性:`qiniuDomain` / `gameId` / `channel` / `gameDir` / `gameStart` / `gameConfig` / `market` / `agent` / `appVersion` / `other` / `appleConfig` - 公开 10 个只读属性:`gameId` / `channel` / `gameDir` / `gameStart` / `gameConfig` / `market` / `agent` / `appVersion` / `other` / `appleConfig``qiniuDomain` 已于 2026-06-27 移除,七牛 CDN 改 RemoteConfig.audio_domain 远端注入,详见 ADR-009
- 单测 fixture:建立 mock bundle 含 fixture plist,验证 11 个 key 全部能读出且缺失 key 返回空串 - 单测 fixture:建立 mock bundle 含 fixture plist,验证 10 个 key 全部能读出且缺失 key 返回空串
- [x] **1.3** 实现 `ylgamehall/Source/Resource/SandboxPaths.swift` - [x] **1.3** 实现 `ylgamehall/Source/Resource/SandboxPaths.swift`
- 常量:`caches` / `documents` / `bundle` - 常量:`caches` / `documents` / `bundle`
- `lobbyIndex() -> URL` 拼出 `{Caches}/{gamedir}/{gamestart}/index.html` - `lobbyIndex() -> URL` 拼出 `{Caches}/{gamedir}/{gamestart}/index.html`
@@ -1018,7 +1018,8 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
- **修订决策**:废止"仓库根 `Resources/`"的固定结构假设,**项目内资源目录由各 Phase 实施时按需落地**: - **修订决策**:废止"仓库根 `Resources/`"的固定结构假设,**项目内资源目录由各 Phase 实施时按需落地**:
- 静态 Bundle 资源 → `ylgamehall/Resources/`synchronized group 自动收集,含 `gamehall.zip` / `ChannelConfig.plist` 等) - 静态 Bundle 资源 → `ylgamehall/Resources/`synchronized group 自动收集,含 `gamehall.zip` / `ChannelConfig.plist` 等)
- 原生 Asset Catalog → `ylgamehall/Assets.xcassets/` - 原生 Asset Catalog → `ylgamehall/Assets.xcassets/`
- 渠道注入值 → `ylgamehall/Resources/ChannelConfig.plist`11 个 string key,母包默认值;ADR-007 - 渠道注入值 → `ylgamehall/Resources/ChannelConfig.plist`10 个 string key,母包默认值;ADR-007、ADR-009
- 应用级凭证 → `ylgamehall/Resources/AppSecrets.plist`3 个 string keywxAppSecret / qiniuAccessKey / qiniuSecretKeyADR-009
- 闭源 SDK 二进制 → `Vendor/<SDK>/<SDK>.xcframework` - 闭源 SDK 二进制 → `Vendor/<SDK>/<SDK>.xcframework`
- 私人原始素材池 → `docs/res/`(项目不感知,需要时拷贝到上述工程内目录) - 私人原始素材池 → `docs/res/`(项目不感知,需要时拷贝到上述工程内目录)
- **理由** - **理由**
@@ -1079,12 +1080,12 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
1. 默认开启的 User Script Sandboxing 拒绝读 `Scripts/copy_channel_injection.sh``ChannelInjection/` 1. 默认开启的 User Script Sandboxing 拒绝读 `Scripts/copy_channel_injection.sh``ChannelInjection/`
2. 即使关掉 sandboxRun Script 在没有 Input/Output 声明 + 默认勾选"Based on dependency analysis"时,被 Xcode 视为"无依赖故无需运行"clean build 也不跑 2. 即使关掉 sandboxRun Script 在没有 Input/Output 声明 + 默认勾选"Based on dependency analysis"时,被 Xcode 视为"无依赖故无需运行"clean build 也不跑
- 综合工程成本估算:为保留"目录名编码"机制,需引入额外 Build Phase / 关闭 sandbox / 维护 Input/Output 列表,**全是 Xcode 行为兼容性维护,与项目目标无关** - 综合工程成本估算:为保留"目录名编码"机制,需引入额外 Build Phase / 关闭 sandbox / 维护 Input/Output 列表,**全是 Xcode 行为兼容性维护,与项目目标无关**
- **决策**:渠道注入存储改用**单一 `ylgamehall/Resources/ChannelConfig.plist`** 含 11 个 string key,与 msext 11 个目录一一对应 - **决策**:渠道注入存储改用**单一 `ylgamehall/Resources/ChannelConfig.plist`** 含 10 个 string key2026-06-22 初版 11 个,2026-06-27 移除 `qiniudomain`,详见 ADR-009
- 存储介质:plist(iOS 原生) - 存储介质:plist(iOS 原生)
- Bundle 加载:synchronized group 自动收集,零配置 - Bundle 加载:synchronized group 自动收集,零配置
- 运行时读取:`BundleConfig.init(bundle:)``PropertyListSerialization` 反序列化 - 运行时读取:`BundleConfig.init(bundle:)``PropertyListSerialization` 反序列化
- **理由** - **理由**
- **契约 100% 等价**H5 端通过 `app_data.js` 看到的 11 个 JS 全局变量行为完全不变(契约边界在 `BundleConfig.shared.xxx`,与底层存储无关) - **契约 100% 等价**H5 端通过 `app_data.js` 看到的 JS 全局变量行为完全不变(契约边界在 `BundleConfig.shared.xxx`,与底层存储无关)
- **CLAUDE.md 原则 B 落地**:原生内部自由重构,不要照搬旧项目;msext 那套是 iOS 9 / Xcode 14 时代的 hackXcode 26 + Swift 6 应当用更现代的存储 - **CLAUDE.md 原则 B 落地**:原生内部自由重构,不要照搬旧项目;msext 那套是 iOS 9 / Xcode 14 时代的 hackXcode 26 + Swift 6 应当用更现代的存储
- **IPA 后处理多渠道分发完全等价**: - **IPA 后处理多渠道分发完全等价**:
```bash ```bash
@@ -1099,9 +1100,11 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin
- Contract §0.3 描述的 msext 实现仅作为历史参考,新项目不照搬 - Contract §0.3 描述的 msext 实现仅作为历史参考,新项目不照搬
- Design §7.0 / §7.2 重写为 plist 方案 - Design §7.0 / §7.2 重写为 plist 方案
- Plan Phase 1.1 简化为"建 plist"单步任务(取代原 1.1.a~d 四步 inject_channel.sh 方案) - Plan Phase 1.1 简化为"建 plist"单步任务(取代原 1.1.a~d 四步 inject_channel.sh 方案)
- `BundleConfig.swift` 接口(11 个公开属性)保持不变,仅 init 实现切换 - `BundleConfig.swift` 接口(公开属性,当前 10 个)保持稳定,仅 init 实现切换
- **守护** - **守护**
- **`ChannelConfig.plist` 必须保持 11 个 key 完整且类型为 string**;新增 key 视同契约边界变更,需更新 Design / Plan / Contract(如该值被 H5 通过 app_data.js 暴露) - **`ChannelConfig.plist` 必须保持 10 个 key 完整且类型为 string**gameid/channel/gamedir/gamestart/gameconfig/market/agent/appversion/other/appleconfig);新增 / 移除 key 视同契约边界变更,需更新 Design / Plan / Contract(如该值被 H5 通过 app_data.js 暴露)
- 跨渠道相同的应用凭证(微信 AppSecret、七牛 AccessKey/SecretKey)放 `AppSecrets.plist`,不进 ChannelConfigADR-009
- 七牛 CDN 域名、bucket 名走 RemoteConfig 顶层 `audio_domain` / `audio_bucket` 远端注入,不在任何本地 plistADR-009
- 后处理工具必须**修改 plist 后立即重签**,否则 iOS 拒绝安装 - 后处理工具必须**修改 plist 后立即重签**,否则 iOS 拒绝安装
- 不允许把渠道值硬编码到 Swift 源码(违背"母包模式"的初衷:一份二进制 N 个渠道) - 不允许把渠道值硬编码到 Swift 源码(违背"母包模式"的初衷:一份二进制 N 个渠道)
- **回滚条件**:若未来 Xcode / iOS 改动让 plist 方案无法工作(极不可能)或后处理脚本失效,重新评估目录名方案或其它存储介质 - **回滚条件**:若未来 Xcode / iOS 改动让 plist 方案无法工作(极不可能)或后处理脚本失效,重新评估目录名方案或其它存储介质
@@ -1243,5 +1246,34 @@ ResolvedVersion(
--- ---
### ADR-009:凭证 / 七牛运行参数三分制(2026-06-27)
- **背景**:本次修订前,微信 AppID/Secret + 七牛 AccessKey/SecretKey/bucket/CDN 域名分散在 3 处:
- Swift 源码硬编码:`WeChatSDK.swift`(AppID) / `WeChatAuth.swift`(AppSecret) / `QiniuConfig.swift`(AccessKey/SecretKey/bucket/CDN)
- `Info.plist` 的 CFBundleURLTypes:微信 AppID 又出现一次(iOS 系统级 URL Scheme,必须在 Info.plist
- `ChannelConfig.plist` 的 `qiniudomain`:CDN 域名又一份,但 Swift 代码完全没读取(双源僵尸字段)
- **问题**:无单一真相源 → 多渠道分发 / 后台改后台时容易遗漏某处;CDN 域名出现"plist 改了但代码用硬编码"的潜在 bug
- **决策(三分制)**
| 数据维度 | 唯一权威源 | 理由 |
|---|---|---|
| 微信 AppID | `Info.plist` 的 CFBundleURLTypes (URLName=weixin 首个 scheme) | iOS 系统级 URL Scheme 注册,运行期不可注入,Info.plist 是事实唯一可写位置 |
| 应用级凭证(微信 AppSecret、七牛 AccessKey、七牛 SecretKey | `AppSecrets.plist` | 跨渠道相同的应用全局凭证;与渠道差异化字段语义分离 |
| 七牛 CDN 域名、bucket 名 | RemoteConfig 顶层 `audio_domain` / `audio_bucket` | 后台运维管理,无客户端直接出 ad-hoc 改值的诉求 |
- **远端注入时序**`WebContainerViewController` 在 `parsed` 分支(即 RemoteConfig 拉到、IPA 校验前)即调 `await QiniuConfig.shared.update(cdnDomain:bucketName:)` 注入;缺失 audio_domain / audio_bucket 抛 `BootError.audioConfigMissing`,与 showmessage 同等致命,弹 modal 永停(理由:缺这俩 → 录音上传必失败,业务不可用,启动期即报远比运行期某次上传时报更友好)
- **类型设计**
- `AppSecrets`:与 `BundleConfig` 同款 `nonisolated public final class Sendable`3 个不可变 String 属性
- `QiniuConfig`:从 `enum` 改为 `actor``accessKey`/`secretKey` 仍 nonisolated(直接读 `AppSecrets.shared`),`cdnDomain`/`bucketName` 进 actor 状态;`update(...)` / `publicURL(...)` async
- `QiniuTokenSigner.uploadToken()` 改 async(因 bucketName 来自 actor
- `QiniuUploader.upload()` 预取 cdnDomain 一次(避免 SDK 同步 callback 内再 await
- **删除清单**
- `WeChatSDK.swift` `static let appID = "..."` 硬编码 → 改读 Info.plist
- `WeChatAuth.swift` `static let appSecret = "..."` → 改读 `AppSecrets.shared.wxAppSecret`
- `QiniuConfig.swift` `static let accessKey/secretKey/bucketName/cdnDomain = "..."` 四处硬编码
- `ChannelConfig.plist` `<key>qiniudomain</key>` 字段
- `BundleConfig.swift` `qiniuDomain` 属性(10 key 守护规则同步)
- **回滚条件**:若未来 `Info.plist` 不再允许动态读取 CFBundleURLTypes(极不可能),或 RemoteConfig 接口被替换为按渠道差异化下发(需要进 4 层 fallback),重新评估
---
文档完成日期:2026-06-21 文档完成日期:2026-06-21
最后更新:2026-06-27ADR-008 第三轮修订:VersionResolver 单链 4 层 fallback + 5 字段共用同一查找接口;前置历史 2026-06-22 ADR-008 二次精确化 / ADR-007 渠道注入改 ChannelConfig.plist / ADR-006 纯 SPM + Vendor / ADR-005 极光降级 / Resources 目录记录) 最后更新:2026-06-27ADR-009 凭证集中化 + AppSecrets.plist + 七牛运行参数远端注入 + ChannelConfig 11→10 key;同日 ADR-008 第三轮修订:VersionResolver 单链 4 层 fallback;前置历史 2026-06-22 ADR-008 二次精确化 / ADR-007 渠道注入改 ChannelConfig.plist / ADR-006 纯 SPM + Vendor / ADR-005 极光降级 / Resources 目录记录)
+28 -17
View File
@@ -508,10 +508,10 @@ public enum OverlayBridge {
| getter | 返回类型 | 业务语义 | 数据源 | | getter | 返回类型 | 业务语义 | 数据源 |
|---|---|---|---| |---|---|---|---|
| `getchannelName()` | string | 渠道 ID11 项渠道注入之一)| `BundleConfig.shared.channel` | | `getchannelName()` | string | 渠道 ID10 项渠道注入之一)| `BundleConfig.shared.channel` |
| `getmarketname()` | string | 市场 ID11 项渠道注入之一)| `BundleConfig.shared.market` | | `getmarketname()` | string | 市场 ID10 项渠道注入之一)| `BundleConfig.shared.market` |
| `getOther()` | string | 渠道 `other` 字段 | `BundleConfig.shared.other` | | `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` 真实返回值,并对齐) | | `getcompareCode()` | int | 业务校验码(msext `RootVC.m` 沿用 zip 版本号或固定值) | 待原 msext 取值确认(Phase 2 实施时查 `RootVC.m:1560` 附近 `getcompareCode` 真实返回值,并对齐) |
| `getbattery()` | double | 当前电池电量 0.01.0 | `UIDevice.current.batteryLevel`(启动期 snapshot 一次) | | `getbattery()` | double | 当前电池电量 0.01.0 | `UIDevice.current.batteryLevel`(启动期 snapshot 一次) |
| `getnetwork()` | int | 当前网络类型(0 无 / 1 WiFi / 2 蜂窝) | `NWPathMonitor` 当前 pathloadFileURL 前 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**,本地查询零延迟。 **为什么不走 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 业务里"启动时刻电量值"被复用整个会话) - 动态字段(getbattery / getnetwork):**每次 `loadFileURL` 前重新 snapshot 注入**(精度足够,原 msext 自身也只在 `viewDidLoad` 取一次,H5 业务里"启动时刻电量值"被复用整个会话)
- 已安装子游戏列表(getGameinstall):每次 loadFileURL 前扫描沙盒 + 注入 → SwitchOverGameData 解压新子游戏后自然在下次 loadFileURL 刷新 - 已安装子游戏列表(getGameinstall):每次 loadFileURL 前扫描沙盒 + 注入 → SwitchOverGameData 解压新子游戏后自然在下次 loadFileURL 刷新
@@ -552,7 +552,7 @@ public enum SettingsBridgePolyfill {
window.__nativeSnapshot = \(json); window.__nativeSnapshot = \(json);
window.settings = window.settings || {}; window.settings = window.settings || {};
// ── 11 项渠道注入(静态,启动期一次性快照)──────────── // ── 10 项渠道注入(静态,启动期一次性快照)────────────
window.settings.getchannelName = function() { return window.__nativeSnapshot.channel || ""; }; window.settings.getchannelName = function() { return window.__nativeSnapshot.channel || ""; };
window.settings.getmarketname = function() { return window.__nativeSnapshot.market || ""; }; window.settings.getmarketname = function() { return window.__nativeSnapshot.market || ""; };
window.settings.getOther = function() { return window.__nativeSnapshot.other || ""; }; window.settings.getOther = function() { return window.__nativeSnapshot.other || ""; };
@@ -583,7 +583,7 @@ public enum SettingsBridgePolyfill {
} }
public struct Snapshot: Sendable { public struct Snapshot: Sendable {
public let channelConfig: [String: String] // 11 public let channelConfig: [String: String] // 10
public let channel: String public let channel: String
public let market: String public let market: String
public let other: String public let other: String
@@ -1816,13 +1816,14 @@ Phase 6 子游戏(`SwitchOverGameData` 入参的 `Gamedirectory` / `gamedownlo
#### 7.0.3 渠道注入:ChannelConfig.plist 母包模式 #### 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 ```xml
<dict> <dict>
<key>qiniudomain</key> <string>iosaudio.daoqi88.cn</string>
<key>gameid</key> <string>G2hw0u...</string> <key>gameid</key> <string>G2hw0u...</string>
<key>channel</key> <string>FtJf07...</string> <key>channel</key> <string>FtJf07...</string>
<key>gamedir</key> <string>FtJf07...</string> <key>gamedir</key> <string>FtJf07...</string>
@@ -1836,6 +1837,18 @@ Phase 6 子游戏(`SwitchOverGameData` 入参的 `Gamedirectory` / `gamedownlo
</dict> </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` 反序列化。 **运行时读取路径**:`Bundle.main.bundleURL.appendingPathComponent("ChannelConfig.plist")`,由 `BundleConfig.swift`(§7.2)用 `PropertyListSerialization` 反序列化。
**多渠道分发**(IPA 后处理,不重新 Xcode build): **多渠道分发**(IPA 后处理,不重新 Xcode build):
@@ -1892,12 +1905,11 @@ public enum SandboxPaths {
```swift ```swift
// ResourceKit/BundleConfig.swift // ResourceKit/BundleConfig.swift
// //
// Bundle ChannelConfig.plist 11 // Bundle ChannelConfig.plist 10
// §7.0.3 ChannelConfig.plist / ADR-007 // §7.0.3 ChannelConfig.plist / ADR-007 / ADR-009
public final class BundleConfig: @unchecked Sendable { public final class BundleConfig: @unchecked Sendable {
public static let shared = BundleConfig() public static let shared = BundleConfig()
public let qiniuDomain: String
public let gameId: String public let gameId: String
public let channel: String public let channel: String
public let gameDir: String public let gameDir: String
@@ -1911,7 +1923,6 @@ public final class BundleConfig: @unchecked Sendable {
public init(bundle: Bundle = .main) { public init(bundle: Bundle = .main) {
let dict = Self.loadPlist(bundle: bundle) let dict = Self.loadPlist(bundle: bundle)
qiniuDomain = dict["qiniudomain"] ?? ""
gameId = dict["gameid"] ?? "" gameId = dict["gameid"] ?? ""
channel = dict["channel"] ?? "" channel = dict["channel"] ?? ""
gameDir = dict["gamedir"] ?? "" 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。 > **测试性**:`init(bundle:)` 接受任意 Bundle,单测可注入 mock bundle 验证不同 plist fixture。
@@ -1989,9 +2000,9 @@ inject "gameid" "$3"
inject "market" "$4" inject "market" "$4"
inject "agent" "$5" inject "agent" "$5"
inject "appversion" "$6" inject "appversion" "$6"
inject "qiniudomain" "$7" inject "gamestart" "$7"
inject "gamestart" "$8" inject "gameconfig" "$8"
inject "gameconfig" "$9" # qiniudomain 已于 2026-06-27 移除(ADR-009),七牛 CDN 域名走 RemoteConfig.audio_domain
echo "Channel injected: channel=$1 gamedir=$2 market=$4" echo "Channel injected: channel=$1 gamedir=$2 market=$4"
``` ```
+12 -7
View File
@@ -199,15 +199,20 @@ bridge.register("prepareaudio") { _, callback in
|---|---|---| |---|---|---|
| **Bundle ID** | `com.skyapp.ylgamehall` | `daoqi/msext.xcodeproj` Release | | **Bundle ID** | `com.skyapp.ylgamehall` | `daoqi/msext.xcodeproj` Release |
| **App 显示名** | `进贤聚友棋牌` | `SGDefineInfo.h:103 gamehallname` | | **App 显示名** | `进贤聚友棋牌` | `SGDefineInfo.h:103 gamehallname` |
| 微信 AppID | `wx586a9b321e56efb7` | `SGDefineInfo.h:105 kAuthOpenID` | | 微信 AppID | `wx586a9b321e56efb7` | **新外壳**:`Info.plist` CFBundleURLTypes (URLName=weixin) 唯一权威源 / msext:`SGDefineInfo.h:105 kAuthOpenID` |
| 微信 AppSecret | `b2792724b9565be23e8f5ba548f117cf` | `SGDefineInfo.h:107 Appsecret` | | 微信 AppSecret | `b2792724b9565be23e8f5ba548f117cf` | **新外壳**:`AppSecrets.plist` 的 `wxAppSecret` / msext:`SGDefineInfo.h:107 Appsecret` |
| 微信 AuthScope | `snsapi_message,snsapi_userinfo,snsapi_friend,snsapi_contact` | `SGDefineInfo.h:104` | | 微信 AuthScope | `snsapi_message,snsapi_userinfo,snsapi_friend,snsapi_contact` | `SGDefineInfo.h:104` |
| 微信 AuthState | `wechat_sdk` | `SGDefineInfo.h:106 kAuthState` | | 微信 AuthState | `wechat_sdk` | `SGDefineInfo.h:106 kAuthState` |
| 高德定位 APIKey | `b0d4a8e3fcbbcc0dd96283b7df6a4494` | `daoqi/msext/Class/Common/APIKey.h:14` | | 高德定位 APIKey | `b0d4a8e3fcbbcc0dd96283b7df6a4494` | `daoqi/msext/Class/Common/APIKey.h:14` |
| 七牛 AccessKey | `dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm` | `QiniuConfig.m:12` | | 七牛 AccessKey | `dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm` | **新外壳**:`AppSecrets.plist` 的 `qiniuAccessKey` / msext:`QiniuConfig.m:12` |
| 七牛 SecretKey | `RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq` | `QiniuConfig.m:13` | | 七牛 SecretKey | `RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq` | **新外壳**:`AppSecrets.plist` 的 `qiniuSecretKey` / msext:`QiniuConfig.m:13` |
| 七牛 BucketName | `iosaudio` | `QiniuConfig.m:16` | | 七牛 BucketName | `iosaudio` | **新外壳**:RemoteConfig 顶层 `audio_bucket` 远端注入 / msext:`QiniuConfig.m:16` |
| 七牛 CDN Domain | `iosaudio.daoqi88.cn` | `daoqi/msext/msext/qiniudomain/` 目录名 | | 七牛 CDN Domain | `iosaudio.daoqi88.cn` | **新外壳**:RemoteConfig 顶层 `audio_domain` 远端注入 / msext:`qiniudomain/` 目录名 |
> **凭证位置三分制(新外壳 ADR-009)**:
> - **微信 AppID** 唯一源 = `Info.plist` 的 `CFBundleURLTypes`iOS 系统级 URL Scheme,运行期不可注入)
> - **应用级凭证**(微信 AppSecret、七牛 AccessKey、七牛 SecretKey)唯一源 = `Resources/AppSecrets.plist`
> - **七牛运行参数**CDN 域名、bucket 名)唯一源 = RemoteConfig 顶层 `audio_domain` / `audio_bucket`(启动期由 `WebContainerViewController` 注入 `QiniuConfig` actor;缺失即抛 `BootError.audioConfigMissing` 启动期致命)
### 0.1 安全权衡(与父项目 CLAUDE.md 同步) ### 0.1 安全权衡(与父项目 CLAUDE.md 同步)
@@ -350,4 +355,4 @@ bridge.register("prepareaudio") { _, callback in
- 高德 9 字段:`daoqi/msext/Class/RootVC/gameController.m:2528 / 2557``getlocationinfo` 字段顺序与类型) - 高德 9 字段:`daoqi/msext/Class/RootVC/gameController.m:2528 / 2557``getlocationinfo` 字段顺序与类型)
- AMR 转码:`daoqi/msext/Class/Common/VoiceConvert/``VoiceConverter.h/m`、`amrFileCodec.h/m` - AMR 转码:`daoqi/msext/Class/Common/VoiceConvert/``VoiceConverter.h/m`、`amrFileCodec.h/m`
- 七牛 token 自签算法:`daoqi/msext/Class/Utils/QiniuManager.m:200-230`putPolicy + HMAC-SHA1 + Base64URL - 七牛 token 自签算法:`daoqi/msext/Class/Utils/QiniuManager.m:200-230`putPolicy + HMAC-SHA1 + Base64URL
- 七牛域名读取:`daoqi/msext/AppDelegate.m:142` 用 `FuncPublic filename:@"qiniudomain"` 读 `Resources/qiniudomain/` 下第一个子目录名作为实际 Domain - 七牛域名读取msext 历史路径)`daoqi/msext/AppDelegate.m:142` 用 `FuncPublic filename:@"qiniudomain"` 读 `Resources/qiniudomain/` 下第一个子目录名作为实际 Domain。**新外壳不再沿用此路径**,CDN 域名 + bucket 名走 RemoteConfig 顶层 `audio_domain` / `audio_bucket` 远端注入到 `QiniuConfig` actorADR-009
+1 -1
View File
@@ -66,7 +66,7 @@ xcrun simctl get_app_container booted com.skyapp.ylgamehall data
预期 Xcode console 顺序: 预期 Xcode console 顺序:
``` ```
[BundleConfig] 渠道注入读取结果(11 项) [BundleConfig] 渠道注入读取结果(10 项)
[SandboxPaths] 路径计算结果 [SandboxPaths] 路径计算结果
[ResourceUnzipper] ensureReady 完成 [ResourceUnzipper] ensureReady 完成
[RemoteConfigClient] 拉取成功 .parsed [RemoteConfigClient] 拉取成功 .parsed
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>wxAppSecret</key>
<string>b2792724b9565be23e8f5ba548f117cf</string>
<key>qiniuAccessKey</key>
<string>dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm</string>
<key>qiniuSecretKey</key>
<string>RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq</string>
</dict>
</plist>
-2
View File
@@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>qiniudomain</key>
<string>iosaudio.daoqi88.cn</string>
<key>gameid</key> <key>gameid</key>
<string>G2hw0ubng0zcoI0r4mx3H2yr4GejidwO</string> <string>G2hw0ubng0zcoI0r4mx3H2yr4GejidwO</string>
<key>channel</key> <key>channel</key>
+6 -3
View File
@@ -5,10 +5,13 @@
// OAuth2 msext 4 ViewController // OAuth2 msext 4 ViewController
// Code access_token userinfo 7 user // Code access_token userinfo 7 user
// //
// AppSecret = CLAUDE.md // AppSecret = CLAUDE.md
// msext IPA Appsecret 沿 secret // msext IPA Appsecret 沿 secret
// /wechat/login docs/SDK-Integration-Guide.md §0.1 // /wechat/login docs/SDK-Integration-Guide.md §0.1
// //
// AppSecrets.plist wxAppSecret
// AppSecrets.shared
//
// 1:1 msext NewRootVC.m:2428 sharelogin payload // 1:1 msext NewRootVC.m:2428 sharelogin payload
// openid / headimgurl / nickname / sex / city / Province P/ unionid // openid / headimgurl / nickname / sex / city / Province P/ unionid
// //
@@ -40,8 +43,8 @@ public enum WeChatAuthError: Error, Sendable {
@MainActor @MainActor
public enum WeChatAuth { public enum WeChatAuth {
/// AppSecret沿 msext SGDefineInfo.h:107 sns/oauth2 /// AppSecret `AppSecrets.plist` `wxAppSecret`
static let appSecret = "b2792724b9565be23e8f5ba548f117cf" static var appSecret: String { AppSecrets.shared.wxAppSecret }
/// OAuth2 code access_token userinfo 7 user /// OAuth2 code access_token userinfo 7 user
public static func authorize() async throws -> WeChatUser { public static func authorize() async throws -> WeChatUser {
+41 -18
View File
@@ -2,36 +2,59 @@
// QiniuConfig.swift // QiniuConfig.swift
// ylgamehall // ylgamehall
// //
// 沿 msext QiniuConfig.m // 访
// //
// SecretKey = CLAUDE.md //
// AppSecret msext 沿 // - accessKey / secretKey AppSecrets.plist
// - bucketName / cdnDomain RemoteConfig audio_bucket / audio_domain
// WebContainerViewController
// update(domain:bucket:)
//
// secretKey = CLAUDE.md
// wxAppSecret msext 沿
// //
import Foundation import Foundation
/// nonisolated使 QiniuUploaderactor MainActor /// 访actor cdnDomain / bucketName
/// immutable static await MainActor.run /// accessKey / secretKey nonisolated immutable actor
public enum QiniuConfig { public actor QiniuConfig {
/// AccessKey沿 msext QiniuConfig.m:12
public nonisolated static let accessKey = "dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm"
/// SecretKey沿 msext QiniuConfig.m:13 HMAC-SHA1 putPolicy public static let shared = QiniuConfig()
public nonisolated static let secretKey = "RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq"
/// 沿 msext QiniuConfig.m:16 putPolicy scope // MARK: - AppSecrets.plist
public nonisolated static let bucketName = "iosaudio"
/// CDN 沿 msext qiniudomain/ /// AccessKey `AppSecrets.plist.qiniuAccessKey`
/// msext fallback `iosaudio.daoqi8888.cn` `daoqi88` public nonisolated static var accessKey: String { AppSecrets.shared.qiniuAccessKey }
/// 沿
public nonisolated static let cdnDomain = "iosaudio.daoqi88.cn" /// SecretKey `AppSecrets.plist.qiniuSecretKey`HMAC-SHA1 putPolicy
public nonisolated static var secretKey: String { AppSecrets.shared.qiniuSecretKey }
/// key msext QiniuConfig.m:22 /// key msext QiniuConfig.m:22
public nonisolated static let recordingDirectory = "" public nonisolated static let recordingDirectory = ""
// MARK: - RemoteConfig
private var _cdnDomain: String = ""
private var _bucketName: String = ""
/// WebContainerViewController
/// QiniuUploader / QiniuTokenSigner await
public func update(cdnDomain: String, bucketName: String) {
_cdnDomain = cdnDomain
_bucketName = bucketName
}
/// CDN http:// 访 `update(...)`
/// / URL 线 update
public var cdnDomain: String { _cdnDomain }
/// bucket putPolicy.scope访 `update(...)`
public var bucketName: String { _bucketName }
/// 访 URL`http://{cdnDomain}/{key}`msext QiniuManager.m:197 /// 访 URL`http://{cdnDomain}/{key}`msext QiniuManager.m:197
public nonisolated static func publicURL(forKey key: String) -> String { /// http:// msext CDN https
"http://\(cdnDomain)/\(key)" public func publicURL(forKey key: String) -> String {
"http://\(_cdnDomain)/\(key)"
} }
} }
@@ -17,7 +17,8 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
/// token nonisolatedactor await MainActor.run /// token bucketName actor `QiniuConfig.shared` await
/// accessKey / secretKey AppSecrets await
public enum QiniuTokenSigner { public enum QiniuTokenSigner {
/// token /// token
@@ -27,11 +28,12 @@ public enum QiniuTokenSigner {
/// key key `:key` /// key key `:key`
/// key daoqi /// key daoqi
/// - expiresIn: token 3600 /// - expiresIn: token 3600
public nonisolated static func uploadToken(key: String? = nil, expiresIn: TimeInterval = 3600) -> String { public static func uploadToken(key: String? = nil, expiresIn: TimeInterval = 3600) async -> String {
_ = key // 使 _ = key // 使
// 1. scope bucket daoqi QiniuManager.m:207 // 1. scope bucket daoqi QiniuManager.m:207
let scope = QiniuConfig.bucketName // bucketName audio_bucket
let scope = await QiniuConfig.shared.bucketName
let deadline = Int(Date().timeIntervalSince1970 + expiresIn) let deadline = Int(Date().timeIntervalSince1970 + expiresIn)
// 2. putPolicy JSONdaoqi options:0 // 2. putPolicy JSONdaoqi options:0
@@ -56,7 +56,9 @@ public actor QiniuUploader {
let key = QiniuConfig.recordingDirectory + localFile.lastPathComponent let key = QiniuConfig.recordingDirectory + localFile.lastPathComponent
// token QiniuTokenSigner / msext QiniuManager // token QiniuTokenSigner / msext QiniuManager
let token = QiniuTokenSigner.uploadToken(key: key) let token = await QiniuTokenSigner.uploadToken(key: key)
// CDN QiniuConfig actor URL await
let cdnDomain = await QiniuConfig.shared.cdnDomain
// SDK 8.x`QNUploadManager.init()` `kQNDeprecated` // SDK 8.x`QNUploadManager.init()` `kQNDeprecated`
// `initWithConfiguration:` defaultConfigurationV2v2 defaultConfiguration // `initWithConfiguration:` defaultConfigurationV2v2 defaultConfiguration
@@ -75,7 +77,7 @@ public actor QiniuUploader {
token: token, token: token,
complete: { info, savedKey, _ in complete: { info, savedKey, _ in
if let info, info.isOK, let savedKey { if let info, info.isOK, let savedKey {
let url = QiniuConfig.publicURL(forKey: savedKey) let url = "http://\(cdnDomain)/\(savedKey)"
cont.resume(returning: UploadedFile( cont.resume(returning: UploadedFile(
fileUrl: url, fileUrl: url,
fileName: fileName, fileName: fileName,
@@ -12,7 +12,27 @@ import Foundation
nonisolated public struct RemoteConfig: Codable, Sendable { nonisolated public struct RemoteConfig: Codable, Sendable {
public let showmessage: String? public let showmessage: String?
/// CDN ** http:// **访 URL
/// 4 fallback
/// WebContainerViewController BootError.audioConfigMissing
public let audioDomain: String?
/// bucket putPolicy.scope audioDomain
public let audioBucket: String?
public let agentlist: [Agent]? public let agentlist: [Agent]?
public init(from decoder: any Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
showmessage = try c.decodeFlexibleStringIfPresent(forKey: .showmessage)
audioDomain = try c.decodeFlexibleStringIfPresent(forKey: .audioDomain)
audioBucket = try c.decodeFlexibleStringIfPresent(forKey: .audioBucket)
agentlist = try c.decodeIfPresent([Agent].self, forKey: .agentlist)
}
private enum CodingKeys: String, CodingKey {
// case rawValue JSONDecoder.convertFromSnakeCase
// JSON audio_domain / audio_bucket audioDomain / audioBucket
case showmessage, agentlist, audioDomain, audioBucket
}
} }
nonisolated public struct Agent: Codable, Sendable { nonisolated public struct Agent: Codable, Sendable {
@@ -0,0 +1,49 @@
//
// AppSecrets.swift
// ylgamehall
//
// Bundle AppSecrets.plist 3 string
//
// = CLAUDE.md
// plist "" msext IPA
// 沿
//
// ChannelConfig.plist
// - ChannelConfig.plist
// - AppSecrets.plist
//
//
// - wxAppSecret : OAuth2 access_token msext SGDefineInfo.h:107
// - qiniuAccessKey : putPolicy + token
// - qiniuSecretKey : HMAC-SHA1 putPolicy
//
// AppID plist Info.plist CFBundleURLTypes
// URLName=weixin first scheme WeChatSDK.swift
//
import Foundation
nonisolated public final class AppSecrets: Sendable {
public static let shared = AppSecrets()
public let wxAppSecret: String
public let qiniuAccessKey: String
public let qiniuSecretKey: String
public init(bundle: Bundle = .main) {
let dict = Self.loadPlist(bundle: bundle)
wxAppSecret = dict["wxAppSecret"] ?? ""
qiniuAccessKey = dict["qiniuAccessKey"] ?? ""
qiniuSecretKey = dict["qiniuSecretKey"] ?? ""
}
private static func loadPlist(bundle: Bundle) -> [String: String] {
guard let url = bundle.url(forResource: "AppSecrets", withExtension: "plist"),
let data = try? Data(contentsOf: url),
let plist = try? PropertyListSerialization.propertyList(
from: data, format: nil) as? [String: String]
else { return [:] }
return plist
}
}
@@ -2,13 +2,16 @@
// BundleConfig.swift // BundleConfig.swift
// ylgamehall // ylgamehall
// //
// Bundle ChannelConfig.plist 11 string // Bundle ChannelConfig.plist 10 string
// docs/H5-Native-Implementation-Design.md §7 / ADR-007 // docs/H5-Native-Implementation-Design.md §7 / ADR-007
// //
// qiniudomain 2026-06-27 CDN
// RemoteConfig audio_domain
//
import Foundation import Foundation
/// 11 `BundleConfig.shared` App /// 10 `BundleConfig.shared` App
/// ///
/// `plutil` `.app/ChannelConfig.plist` + /// `plutil` `.app/ChannelConfig.plist` +
/// Xcode build ADR-007 /// Xcode build ADR-007
@@ -17,7 +20,6 @@ nonisolated public final class BundleConfig: Sendable {
/// App main bundle `ChannelConfig.plist`访 /// App main bundle `ChannelConfig.plist`访
public static let shared = BundleConfig() public static let shared = BundleConfig()
public let qiniuDomain: String
public let gameId: String public let gameId: String
public let channel: String public let channel: String
public let gameDir: String public let gameDir: String
@@ -32,7 +34,6 @@ nonisolated public final class BundleConfig: Sendable {
/// bundle plist fixture /// bundle plist fixture
public init(bundle: Bundle = .main) { public init(bundle: Bundle = .main) {
let dict = Self.loadPlist(bundle: bundle) let dict = Self.loadPlist(bundle: bundle)
qiniuDomain = dict["qiniudomain"] ?? ""
gameId = dict["gameid"] ?? "" gameId = dict["gameid"] ?? ""
channel = dict["channel"] ?? "" channel = dict["channel"] ?? ""
gameDir = dict["gamedir"] ?? "" gameDir = dict["gamedir"] ?? ""
+22 -2
View File
@@ -15,8 +15,28 @@ import UIKit
@MainActor @MainActor
public enum WeChatSDK { public enum WeChatSDK {
/// AppID沿 msext SGDefineInfo.h:105 kAuthOpenID /// AppID **** Info.plist CFBundleURLTypes
public static let appID = "wx586a9b321e56efb7" /// URLName=`weixin` CFBundleURLSchemes scheme iOS
/// URL Scheme Info.plist
///
/// AppID plutil Info.plist
/// Swift
///
/// preconditionFailurefail-fast
public static let appID: String = {
guard let urlTypes = Bundle.main.infoDictionary?["CFBundleURLTypes"] as? [[String: Any]],
let weixinType = urlTypes.first(where: {
($0["CFBundleURLName"] as? String) == "weixin"
}),
let schemes = weixinType["CFBundleURLSchemes"] as? [String],
let appID = schemes.first, !appID.isEmpty
else {
preconditionFailure(
"Info.plist 缺少 weixin URL SchemeCFBundleURLTypes 中 CFBundleURLName=weixin 的 CFBundleURLSchemes 首项即为微信 AppID"
)
}
return appID
}()
/// AppDelegate.didFinishLaunchingWithOptions /// AppDelegate.didFinishLaunchingWithOptions
/// 1.x registerApp Universal Link /// 1.x registerApp Universal Link
@@ -41,6 +41,8 @@ public final class WebContainerViewController: UIViewController {
case operationalMessage(String) case operationalMessage(String)
/// IPA + Safari /// IPA + Safari
case ipaUpgradeRequired(downloadURL: String) case ipaUpgradeRequired(downloadURL: String)
/// RemoteConfig audio_domain / audio_bucket
case audioConfigMissing
} }
// MARK: - Lifecycle // MARK: - Lifecycle
@@ -183,6 +185,10 @@ public final class WebContainerViewController: UIViewController {
} catch BootError.ipaUpgradeRequired(let dl) { } catch BootError.ipaUpgradeRequired(let dl) {
// IPA modal alert // IPA modal alert
showIPAUpgradeAlert(downloadURL: dl) showIPAUpgradeAlert(downloadURL: dl)
} catch BootError.audioConfigMissing {
// RemoteConfig audio_domain / audio_bucket
// / modal
showBlockingAlert(message: "音频服务暂不可用(配置缺失),请联系客服。")
} catch { } catch {
// / / splash // / / splash
// snapshot NWPath """" kind // snapshot NWPath """" kind
@@ -339,6 +345,15 @@ public final class WebContainerViewController: UIViewController {
throw BootError.operationalMessage(msg) throw BootError.operationalMessage(msg)
case .parsed(let cfg): case .parsed(let cfg):
// audio_domain / audio_bucket
// 使
guard let audioDomain = cfg.audioDomain, !audioDomain.isEmpty,
let audioBucket = cfg.audioBucket, !audioBucket.isEmpty
else {
throw BootError.audioConfigMissing
}
await QiniuConfig.shared.update(cdnDomain: audioDomain, bucketName: audioBucket)
let bc = BundleConfig.shared let bc = BundleConfig.shared
let r = VersionResolver.resolve( let r = VersionResolver.resolve(
config: cfg, config: cfg,