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
+6 -3
View File
@@ -5,10 +5,13 @@
// OAuth2 msext 4 ViewController
// Code access_token userinfo 7 user
//
// AppSecret = CLAUDE.md
// AppSecret = CLAUDE.md
// msext IPA Appsecret 沿 secret
// /wechat/login docs/SDK-Integration-Guide.md §0.1
//
// AppSecrets.plist wxAppSecret
// AppSecrets.shared
//
// 1:1 msext NewRootVC.m:2428 sharelogin payload
// openid / headimgurl / nickname / sex / city / Province P/ unionid
//
@@ -40,8 +43,8 @@ public enum WeChatAuthError: Error, Sendable {
@MainActor
public enum WeChatAuth {
/// AppSecret沿 msext SGDefineInfo.h:107 sns/oauth2
static let appSecret = "b2792724b9565be23e8f5ba548f117cf"
/// AppSecret `AppSecrets.plist` `wxAppSecret`
static var appSecret: String { AppSecrets.shared.wxAppSecret }
/// OAuth2 code access_token userinfo 7 user
public static func authorize() async throws -> WeChatUser {
+41 -18
View File
@@ -2,36 +2,59 @@
// QiniuConfig.swift
// 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
/// nonisolated使 QiniuUploaderactor MainActor
/// immutable static await MainActor.run
public enum QiniuConfig {
/// AccessKey沿 msext QiniuConfig.m:12
public nonisolated static let accessKey = "dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm"
/// 访actor cdnDomain / bucketName
/// accessKey / secretKey nonisolated immutable actor
public actor QiniuConfig {
/// SecretKey沿 msext QiniuConfig.m:13 HMAC-SHA1 putPolicy
public nonisolated static let secretKey = "RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq"
public static let shared = QiniuConfig()
/// 沿 msext QiniuConfig.m:16 putPolicy scope
public nonisolated static let bucketName = "iosaudio"
// MARK: - AppSecrets.plist
/// CDN 沿 msext qiniudomain/
/// msext fallback `iosaudio.daoqi8888.cn` `daoqi88`
/// 沿
public nonisolated static let cdnDomain = "iosaudio.daoqi88.cn"
/// AccessKey `AppSecrets.plist.qiniuAccessKey`
public nonisolated static var accessKey: String { AppSecrets.shared.qiniuAccessKey }
/// SecretKey `AppSecrets.plist.qiniuSecretKey`HMAC-SHA1 putPolicy
public nonisolated static var secretKey: String { AppSecrets.shared.qiniuSecretKey }
/// key msext QiniuConfig.m:22
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
public nonisolated static func publicURL(forKey key: String) -> String {
"http://\(cdnDomain)/\(key)"
/// http:// msext CDN https
public func publicURL(forKey key: String) -> String {
"http://\(_cdnDomain)/\(key)"
}
}
@@ -17,7 +17,8 @@
import Foundation
import CryptoKit
/// token nonisolatedactor await MainActor.run
/// token bucketName actor `QiniuConfig.shared` await
/// accessKey / secretKey AppSecrets await
public enum QiniuTokenSigner {
/// token
@@ -27,11 +28,12 @@ public enum QiniuTokenSigner {
/// key key `:key`
/// key daoqi
/// - 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 // 使
// 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)
// 2. putPolicy JSONdaoqi options:0
@@ -56,7 +56,9 @@ public actor QiniuUploader {
let key = QiniuConfig.recordingDirectory + localFile.lastPathComponent
// 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`
// `initWithConfiguration:` defaultConfigurationV2v2 defaultConfiguration
@@ -75,7 +77,7 @@ public actor QiniuUploader {
token: token,
complete: { info, savedKey, _ in
if let info, info.isOK, let savedKey {
let url = QiniuConfig.publicURL(forKey: savedKey)
let url = "http://\(cdnDomain)/\(savedKey)"
cont.resume(returning: UploadedFile(
fileUrl: url,
fileName: fileName,
@@ -12,7 +12,27 @@ import Foundation
nonisolated public struct RemoteConfig: Codable, Sendable {
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 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 {
@@ -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
// ylgamehall
//
// Bundle ChannelConfig.plist 11 string
// Bundle ChannelConfig.plist 10 string
// docs/H5-Native-Implementation-Design.md §7 / ADR-007
//
// qiniudomain 2026-06-27 CDN
// RemoteConfig audio_domain
//
import Foundation
/// 11 `BundleConfig.shared` App
/// 10 `BundleConfig.shared` App
///
/// `plutil` `.app/ChannelConfig.plist` +
/// Xcode build ADR-007
@@ -17,7 +20,6 @@ nonisolated public final class BundleConfig: Sendable {
/// App main bundle `ChannelConfig.plist`访
public static let shared = BundleConfig()
public let qiniuDomain: String
public let gameId: String
public let channel: String
public let gameDir: String
@@ -32,7 +34,6 @@ nonisolated public final class BundleConfig: Sendable {
/// bundle plist fixture
public init(bundle: Bundle = .main) {
let dict = Self.loadPlist(bundle: bundle)
qiniuDomain = dict["qiniudomain"] ?? ""
gameId = dict["gameid"] ?? ""
channel = dict["channel"] ?? ""
gameDir = dict["gamedir"] ?? ""
+22 -2
View File
@@ -15,8 +15,28 @@ import UIKit
@MainActor
public enum WeChatSDK {
/// AppID沿 msext SGDefineInfo.h:105 kAuthOpenID
public static let appID = "wx586a9b321e56efb7"
/// AppID **** Info.plist CFBundleURLTypes
/// 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
/// 1.x registerApp Universal Link
@@ -41,6 +41,8 @@ public final class WebContainerViewController: UIViewController {
case operationalMessage(String)
/// IPA + Safari
case ipaUpgradeRequired(downloadURL: String)
/// RemoteConfig audio_domain / audio_bucket
case audioConfigMissing
}
// MARK: - Lifecycle
@@ -183,6 +185,10 @@ public final class WebContainerViewController: UIViewController {
} catch BootError.ipaUpgradeRequired(let dl) {
// IPA modal alert
showIPAUpgradeAlert(downloadURL: dl)
} catch BootError.audioConfigMissing {
// RemoteConfig audio_domain / audio_bucket
// / modal
showBlockingAlert(message: "音频服务暂不可用(配置缺失),请联系客服。")
} catch {
// / / splash
// snapshot NWPath """" kind
@@ -339,6 +345,15 @@ public final class WebContainerViewController: UIViewController {
throw BootError.operationalMessage(msg)
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 r = VersionResolver.resolve(
config: cfg,