按 CLAUDE.md ADR-006「禁 CocoaPods,SPM 优先 + Vendor xcframework」组织, 每个 SDK 章节给出:凭证清单 / 工程接入 / 代码触点 / BackGameData 清理钩子 / 验收 / 风险,便于项目方发凭证后开发者按表落地。 关键约束(写入指南): - 微信:不复用 msext AppID;AppSecret 客户端泄露风险,fallback 路径上线前必须切后台 - 高德:APIKey 与 Bundle ID 绑死,必须重新申请(msext 的 key 不可用); iOS 14+ 隐私合规三步(updatePrivacyShow → updatePrivacyAgree → apiKey) - opencore-amr:复用 msext 已 segalign 8 修复版 .a,免重编源码;MRC wrapper 加 -fno-objc-arc 文件级标志 - 七牛:SPM 拉官方 objc-sdk v8.9.x;token 缓存 + 失效前 30s 预刷新 - 每个 SDK 都标明在 BackGameDataHandler 必须加的清理钩子(WXApi.delegate=nil / LocationService.stop / AudioRecorder.cancel / QiniuUploader.cancelInFlight) CLAUDE.md 文档索引追加一行指向新文件。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
18 KiB
SDK 接入指南
本文档面向项目方 + 开发者,描述 Phase 4.E / Phase 5 / Phase 3.B-D 三组 SDK 的真实接入步骤。
使用方式:每接入一个 SDK 走一遍对应章节的"凭证清单 / 工程接入 / 代码触点 / 清理钩子 / 验收"五步,全程对照 CLAUDE.md「原则 A — H5 端零修改」与 ADR-006「禁止 CocoaPods」。
0. 总览
| SDK | 触发的契约项 | 选型 | 阻塞 |
|---|---|---|---|
| 微信 OpenSDK | accreditlogin / friendsSharetypeUrlToptitleDescript(微信路径)/ sharelogin 反向 callback / sharesuccess |
Vendor .xcframework 手动 |
AppID + Universal Links + 后台 /wechat/login(或客户端 fallback) |
| 高德定位(AMap) | startlocation / getlocationinfo 反向 callback(9 字段) |
Vendor .xcframework 手动 |
新 APIKey(绑死 Bundle ID,不可复用 msext) + 隐私合规调用 |
| opencore-amr | prepareaudio(录音上传)/ mediaTypeAudio(远端语音回放)AMR ↔ WAV 转码 |
从 msext 拷贝已修复版 .a(fat header segalign 8) |
无外部凭证;二进制已在 msext 仓库内 |
| 七牛云上传 | prepareaudio → getaudiourl({audiourl, time}) / recordSuccess 反向 callback |
SPM 官方 https://github.com/qiniu/objc-sdk v8.9.x |
后台 token 颁发接口 / 长 token 缓存策略 |
两条铁律(CLAUDE.md):
- 任何接入步骤都不允许触碰 H5 端任何文件(包括
app_*.js) - 不引入 CocoaPods;SPM 优先,闭源走 Vendor
.xcframework
1. 微信 OpenSDK(Phase 4.E)
1.1 凭证清单(项目方提供)
- AppID:微信开放平台「移动应用」面板新建一个应用(新 Bundle ID 不能复用 msext 的
wx586a9b321e56efb7,必须为本项目重新申请) - Universal Links:项目方拥有的域名 +
apple-app-site-association文件(部署到https://your-domain.com/.well-known/apple-app-site-association,HTTPS 必须) - 后台
/wechat/login中转接口(强烈推荐):客户端拿code调后台,后台用AppSecret换access_token+unionid+userinfo,回 7 字段给客户端- 若后台未就绪,客户端走 fallback:
api.weixin.qq.com/sns/oauth2/access_token+sns/userinfo,AppSecret客户端硬编码 = 安全风险,CLAUDE.md 父项目已明确警告。新项目尽量直接走后台中转
- 若后台未就绪,客户端走 fallback:
- 微信 OpenSDK XCFramework:从 微信开放平台官网下载,使用 1.9.x 或最新(避免 1.8.x 的 ITMS-90809 历史包袱)
1.2 工程接入
-
放置 SDK
Vendor/WechatSDK/ ├── WechatOpenSDK.xcframework └── README.md # 记录版本号 + 下载日期 + 来源按 Design §14.1 Vendor 接入标准流程:Target → General → Frameworks → Embed & Sign
-
Info.plist 改动(仅原生侧,不动 H5)
<!-- 1. URL Scheme:让微信 App 回调能打开本 App --> <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLName</key> <string>weixin</string> <key>CFBundleURLSchemes</key> <array><string>YOUR_WX_APPID</string></array> </dict> </array> <!-- 2. 查询微信是否安装 --> <key>LSApplicationQueriesSchemes</key> <array> <string>weixin</string> <string>weixinULAPI</string> <string>weixinURLParamsAPI</string> <!-- 已有的 mqq* / snssdk1128 别覆盖 --> </array> -
Entitlement 改动(Signing & Capabilities)
- Associated Domains 添加
applinks:your-domain.com(与apple-app-site-association的域名严格一致)
- Associated Domains 添加
1.3 代码触点(接入此 SDK 必须改的文件)
| 文件 | 改动 |
|---|---|
Source/SDK/WeChat/WeChatSDK.swift(新建) |
WXApi.registerApp(_:universalLink:) 启动注册;包含全部 12 个 MMAPP_SUPPORT_* flag(Design §4.2) |
AppDelegate.swift |
application(_:didFinishLaunchingWithOptions:) 内调 WeChatSDK.register() |
SceneDelegate.swift |
openURLContexts 内按 QQ → WXApi 顺序判(msext AppDelegate.m 顺序硬约束,QQ 必须先判) |
Source/SDK/WeChat/WeChatManager.swift(新建) |
持久 delegate;authorize() async throws -> WXAuthCode(state UUID 配对);share(_:scene:) async throws(FIFO 串行)。Design §8.5 |
Source/Login/WeChatAuth.swift(新建) |
authorize() 拿 code → 转后台 /wechat/login(或 fallback 拼 sns/oauth2/access_token + sns/userinfo)→ 拿到 7 字段 user。Fallback 文件头标 // FIXME: 待后台 /wechat/login 就绪后切换为后台中转 |
Source/Bridge/Handlers/AccreditLoginHandler.swift |
移除 stub 返回,调 WeChatAuth.authorize() → 反向 bridge.call("sharelogin", 7 字段)字段名严格 1:1(含历史包袱): openid / headimgurl / nickname / sex / city / Province(大写 P!)/ unionid |
Source/Share/WechatShare.swift |
移除 stub return .success,调 WeChatManager.share;isInstalled 改为 WXApi.isWXAppInstalled() |
Source/Bridge/Handlers/BackGameDataHandler.swift |
必须加 WXApi.delegate = nil(清理子游戏 push 期间的 delegate 指针) |
1.4 清理钩子(CLAUDE.md 第 6.0 节回响)
BackGameDataHandler.swift 的 MainActor.run 块加一行:
WXApi.delegate = nil // ← Phase 4.E 接入时取消注释
理由:msext gameController.m:699 在 backgameData 里清 delegate,防止 pop 后微信回调指向已销毁的 SubGameVC。
1.5 验收(H5 端零修改前提下跑)
- H5 调
bridge.callHandler('accreditlogin')→ 拉起微信 → 同意授权 → 大厅收sharelogin({openid,...,Province}),Province是大写 P - H5 调
bridge.callHandler('friendsSharetypeUrlToptitleDescript', {sharefriend:"2", ...})→ 直接调起微信朋友圈 - H5 调
sharefriend:"1"→ SharePanel 三按钮,点微信 → 拉起微信好友选择 → 分享成功后大厅收sharesuccess({success:"2",type:"1"}) - 关键回归:截图分享 / QQ 分享 / 抖音分享路径不受影响
1.6 风险
- AppSecret 客户端泄露:fallback 路径上线前必须切到后台中转,否则 secret 一旦泄露不可重置(CLAUDE.md 父项目「已识别为安全风险,明确接受不修」的同款问题,新项目从一开始避免)
- Universal Links 配置错误:授权回不来。验证方式:
xcrun simctl openurl booted "https://your-domain.com/test"看是否拉起本 App - 微信审核:审核期间 OpenSDK 行为差异(部分 API 限频),上线初期保持后台监控
2. 高德定位 SDK(Phase 5)
2.1 凭证清单
- 新 APIKey:高德控制台 用项目方账号新建应用,绑定本项目的 Bundle ID(msext 的
b0d4a8e3fcbbcc0dd96283b7df6a4494不能复用,APIKey 与 Bundle ID 一一绑死) - AMap SDK XCFramework:官方下载页拉取最新(截至 2026-06 推荐 v2.10.x+)
AMapLocationKit.xcframework— 定位主库AMapFoundationKit.xcframework— 基础库(必须配合 LocationKit)
2.2 工程接入
-
放置 SDK
Vendor/AMap/ ├── AMapLocationKit.xcframework ├── AMapFoundationKit.xcframework └── README.md # 记录 SDK 版本 + APIKey 创建日期 + 绑定 Bundle IDTarget → General → Frameworks → Embed & Sign(两个都要)
-
Info.plist(仅原生侧)
<key>NSLocationWhenInUseUsageDescription</key> <string>{gamehallname}需要访问您的位置以提供本地化服务</string>{gamehallname}占位由 ChannelConfig.plist 注入或硬编为渠道名。
2.3 代码触点
| 文件 | 改动 |
|---|---|
Source/SDK/AMap/AMapWrapper.swift(新建) |
启动期调用顺序:updatePrivacyShow(.didShow, privacyInfo: .didContain) → updatePrivacyAgree(.didAgree) → AMapServices.shared().apiKey = "..."(iOS 14+ 隐私合规必走的 3 步,少一步定位拿不到结果) |
AppDelegate.swift |
didFinishLaunchingWithOptions 内调 AMapWrapper.bootstrap() |
Source/Location/LocationService.swift(新建,actor) |
requestOnce() async throws -> LocationPayload(一次定位)startContinuous(onUpdate:) / stop()(持续定位)包 AMapLocationManager,回调转 async |
Source/Bridge/Handlers/StartLocationHandler.swift |
移除 stub,按 data == 1 持续 / 其它一次性反向 callback bridge.call("getlocationinfo", 9 字段)字段名严格 1:1: address / city / cityCode / country / district / latitude(string,String(format: "%f", lat))/ longitude(string)/ province(小写 p!与 sharelogin.Province 不同)/ street失败: getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"}) |
Source/Bridge/Handlers/BackGameDataHandler.swift |
必须加 LocationService.shared.stop() |
2.4 清理钩子
BackGameDataHandler.swift 的 MainActor.run 块加:
LocationService.shared.stop() // ← Phase 5 接入时取消注释
理由:msext gameController.m:2473-2478 cleanUpAction 在 backgameData 里调 [locationManager stopUpdatingLocation] + delegate nil;防止 pop 后后台定位继续耗电 + 回调悬空。
2.5 验收
- 首次启动 → 系统权限弹框(文案为 Info.plist 设置的中文)
- H5 调
bridge.callHandler('startlocation', "1")→ 收到getlocationinfo({...9 字段...}),latitude/longitude是 string 类型,province是小写 p - 拒绝权限 → H5 收
getlocationinfo({errorCode:12,errorMsg:"缺少定位权限"}) - 子游戏定位中 → 调
backgameData退出 → 大厅不再收到悬挂定位回调(验证 cleanUp 钩子)
2.6 风险
- APIKey 绑定:Bundle ID 改了 / 多 Target 用不同 ID 都要重新申请 key,否则定位失败但无明显报错(高德回 errorCode 7「KEY 校验失败」)
- 隐私合规调用顺序:iOS 14+ 必须先
updatePrivacyShow→updatePrivacyAgree再apiKey,否则定位接口直接拒绝 - 模拟器:高德定位在模拟器走 Apple 的 CoreLocation 模拟坐标,定位精度低,必须真机测
3. opencore-amr 转码库(Phase 3.B)
3.1 凭证清单
- 无凭证。二进制已在 msext 仓库内,且已经过 fat header
segalign 8修复(参 CLAUDE.md 父项目「已处理的定时炸弹」一节,2026-06-20 摘掉-ld_classic的同一组.a)
3.2 工程接入
-
拷贝二进制(已修复版)
mkdir -p ylgamehall/Vendor/opencore-amr/ cp ../daoqi/msext/msext/Class/Common/VoiceConvert/lib/libopencore-amrnb.a ylgamehall/Vendor/opencore-amr/ cp ../daoqi/msext/msext/Class/Common/VoiceConvert/lib/libopencore-amrwb.a ylgamehall/Vendor/opencore-amr/ # 拷头文件 cp -r ../daoqi/msext/msext/Class/Common/VoiceConvert/include ylgamehall/Vendor/opencore-amr/ -
拷 VoiceConverter(ObjC wrapper)
ylgamehall/Vendor/opencore-amr/VoiceConverter/ ├── VoiceConverter.h # ObjC interface(保留 ObjC,wrapper 简单不重写 Swift) ├── VoiceConverter.m # MRC 代码可保留,加 -fno-objc-arc 编译标志或转 ARC ├── amrFileCodec.h/m └── interf_*.h/c -
Xcode 工程配置
- Target → Build Phases → Link Binary With Libraries 加入两个
.a - Target → Build Settings → Library Search Paths 加
$(PROJECT_DIR)/ylgamehall/Vendor/opencore-amr - Target → Build Settings → Header Search Paths 加
$(PROJECT_DIR)/ylgamehall/Vendor/opencore-amr/include - 不需要
-ld_classic(已用lipo -segalign 8修复 fat header,CLAUDE.md 父项目记录的修复同样适用) - 若复用 msext MRC 的
VoiceConverter.m,加文件级-fno-objc-arc编译标志
- Target → Build Phases → Link Binary With Libraries 加入两个
-
Bridging Header(已有
ylgamehall-Bridging-Header.h则追加;无则新建)#import "VoiceConverter.h"
3.3 代码触点
| 文件 | 改动 |
|---|---|
Source/Audio/VoiceCoder.swift(新建) |
Swift wrapper:static func amrToWav(_ amrPath: URL) throws -> URL / static func wavToAmr(_ wavPath: URL) throws -> URL内部调 ObjC VoiceConverter |
Source/Audio/AudioRecorder.swift(新建,actor) |
record() async throws -> AudioFile:AVAudioRecorder 录 WAV + 麦克风权限录音参数与 msext ChatVoiceRecorderVC 等价:16kHz / 单声道 / 16-bit |
Source/Bridge/Handlers/RemoteAudioHandler.swift |
移除 stub,分别接入:prepareaudio(录音上传)→ AudioRecorder.record() → WAV→AMR VoiceCoder.wavToAmr → QiniuUploader.upload(见 §4) → bridge.call("getaudiourl", {audiourl, time}),子游戏页额外触发 bridge.call("recordSuccess", {fileUrl, fileName, fileKey})mediaTypeAudio(远端回放)→ URLSession 下载 AMR → AMR→WAV VoiceCoder.amrToWav → AVAudioPlayer 播放开始播放 bridge.call("gameui_play_voice", user) / 播放结束 bridge.call("gameui_stop_voice", user)受 VoicePlayingHandler 的开关短路(=1 才播) |
Info.plist |
加 NSMicrophoneUsageDescription = "{gamehallname}需要访问您的麦克风录制语音消息" |
Source/Bridge/Handlers/BackGameDataHandler.swift |
必须加 AudioRecorder.shared.cancel() |
3.4 清理钩子
AudioRecorder.shared.cancel() // ← Phase 3.C 接入时取消注释
QiniuUploader.shared.cancelInFlight() // ← Phase 3.C 接入时取消注释
理由:录音 / 上传任务跨 VC 生命周期容易遗留;msext 通过 ChatVoiceRecorderVC 生命周期间接清理,我们的 actor 实现不绑 VC 必须显式 cancel。
3.5 验收
- H5 调
prepareaudio→ 弹麦克风权限 → 同意后开始录音 → 录完上传七牛 → 大厅收getaudiourl({audiourl:"https://...", time:"3"}) - 子游戏同样路径,额外收到
recordSuccess({fileUrl, fileName, fileKey}) - H5 调
mediaTypeAudio({audiourl, user})→ 听到对方语音 + 收到gameui_play_voice/ 结束收gameui_stop_voice voicePlaying总开关设 0 → 调mediaTypeAudio不播放
4. 七牛云上传(Phase 3.C / 3.D)
4.1 凭证清单
- 后台 token 颁发接口:每次录音前向后台 GET
/api/qiniu/upload-token?scene=voice拿到上传凭证 + 域名 - 七牛 Bucket 名 / CDN 域名:项目方提供(可硬编到 BundleConfig,或随 ChannelConfig 注入)
4.2 工程接入(SPM,优于二进制)
- Xcode → File → Add Package Dependencies
- URL:
https://github.com/qiniu/objc-sdk - 版本:
8.9.x最新(avoid 7.x 历史 API) - 勾选 target → Add Package
注:七牛官方 SPM 包是 ObjC,但 SPM 直接支持 ObjC + Swift 混合,不需要 bridging header(SPM 自动处理)。
4.3 代码触点
| 文件 | 改动 |
|---|---|
Source/Network/QiniuTokenService.swift(新建,actor) |
fetchUploadToken() async throws -> QiniuToken(含 token / 上传域名 / Key 模板 / 过期时间)缓存策略:token 失效前 30 秒强制刷新;并发请求合并 |
Source/Network/QiniuUploader.swift(新建,actor) |
upload(_ file: URL, token: QiniuToken) async throws -> UploadedFile内部用 QNUploadManager 包成 async;UploadedFile 含 fileUrl / fileName / fileKey取消语义: cancelInFlight() 取消所有进行中任务(接清理钩子用) |
Source/Bridge/Handlers/RemoteAudioHandler.swift |
prepareaudio 完整链路第 3 步:QiniuUploader.upload(amrFile, token: await QiniuTokenService.shared.fetchUploadToken()) |
4.4 清理钩子
见 §3.4(与 opencore-amr 同一处加 QiniuUploader.shared.cancelInFlight())。
4.5 验收
- H5 调
prepareaudio→ 录音 3 秒 → 上传 → 大厅收getaudiourl({audiourl:"https://your-cdn.com/voice/xxx.amr", time:"3"}) - 录音中 / 上传中 → 退出子游戏 → 任务被 cancel,不再有悬空网络写
- token 过期场景:模拟后台返回过期 token → uploader 自动刷新重试一次
4.6 风险
- token 时效:通常 1 小时;高频录音场景需要 caching + 失效前预刷新
- 上传失败重试:默认 3 次,超时建议设 30s(语音文件通常 < 100KB)
- CDN 域名 HTTPS:H5 业务侧拿到
audiourl后会请求播放,必须保证域名 HTTPS(NSAppTransportSecurity 我们没收紧 ATS,但七牛默认走 HTTPS)
5. 接入完毕的清单检查
每接入完一个 SDK,跑一遍:
BuildProject成功Vendor/<SDK>/README.md已记录版本号 / 来源 / 凭证创建日期- Info.plist 改动只在原生侧,未触 H5 任何文件
BackGameDataHandler.swift对应清理钩子已加(每个 SDK 都要回到此处补 1 行)docs/Development-Plan.md §5 / §8进度勾选已同步(提交时附 commit 备注「Plan 进度已勾选」)docs/Verification-Checklist.md对应 Phase 验证项已补充- 真机跑过该 SDK 的验收项(模拟器对 SDK 行为不可靠:高德、微信、AVAudioRecorder 都受影响)
6. 与原项目 daoqi 的关系
接入过程中若行为与文档描述不一致,优先参考原 daoqi 工程实现(参 CLAUDE.md「原项目 daoqi:遇到问题时的参考来源」一节):
- 微信回调字段:
daoqi/msext/Class/RootVC/NewRootVC.m:2428(sharelogin7 字段,含Province大写) - 高德 9 字段:
daoqi/msext/Class/RootVC/gameController.m:2528 / 2557(getlocationinfo字段顺序与类型) - AMR 转码:
daoqi/msext/Class/Common/VoiceConvert/(VoiceConverter.h/m、amrFileCodec.h/m) - 七牛上传:
daoqi/msext/Class/Utils/QiniuManager.m(参数命名 + 回调结构)
遇到契约边界 / 历史包袱时:照搬 msext 字面 + 字段名 + 顺序,不要凭语义猜测。原项目稳定运行多年的写法是真理,文档 / 设计稿可能有传抄错误(已在 CLAUDE.md「典型案例」积累若干)。