# SDK 接入指南 > 本文档面向项目方 + 开发者,描述 Phase 4.E / Phase 5 / Phase 3.B-D 三组 SDK 的真实接入步骤。 > > **核心前提**:本项目复用 daoqi(msext) 原项目的全部 SDK 凭证。**Bundle ID 必须保持 `com.skyapp.ylgamehall`**(已确认与 msext Release 配置一致)— 改 Bundle ID 会导致所有 key 失效。 > > **代码状态(2026-06-22)**:所有 Swift wrapper / handler 真实路径 / 生命周期清理钩子已落地,全部包在 `#if canImport()` 守卫里。**只需用户在 Xcode UI 完成 3 件事**(详见 §A 用户手动步骤),代码即激活真实路径。 --- ## A. 用户手动 Xcode 步骤(必做 3 件事) > **选型偏好**:官方 SPM 优先;闭源 / 无官方 SPM 的走 Vendor 二进制(社区 SPM wrapper 不计入官方,仓库一旦停更即为定时炸弹,按 daoqi `CLAUDE.md` ADR-006 否决)。 > 当前结果: > - **七牛走官方 SPM**(`https://github.com/qiniu/objc-sdk`,活跃维护) > - **微信走 Vendor**(Tencent 不维护官方 SPM;社区 wrapper 风险高,回 Vendor 与 AMap 统一处置) > - **高德走 Vendor**(官方未提供 SPM) > > 这 3 件事都是 Xcode UI 操作,无法 AI 自动完成。完成后 `BuildProject` 即激活所有真实路径。 ### A.1 添加微信 OpenSDK(Vendor 手动) > Tencent 官方至今未维护 SPM 仓库(`Tencent/WechatOpenSDK-XCFramework` 不存在,404);社区 wrapper(yanyin1986 / anotheren)非官方,被否决。改走 Vendor `.xcframework`,与 AMap 同款处置方式。 **预先准备**:从腾讯开放平台下载 `OpenSDK*_NoPay.zip`(推荐 2.0.5+),解压取出 `WechatOpenSDK.xcframework`,放到 `Vendor/WechatSDK/`。下载页与详细说明见 `Vendor/WechatSDK/README.md`。 1. 选中 `ylgamehall` Target → General → Frameworks, Libraries, and Embedded Content 2. 点 `+` → Add Other → Add Files 3. 选 `Vendor/WechatSDK/WechatOpenSDK.xcframework` 4. **Embed** 列选 **Embed & Sign**(xcframework 是动态库,与下面 AMap 的 `Do Not Embed` 不同,别错) 5. ⌘B 编译 ### A.2 添加高德定位 framework × 2(Vendor 手动,无 SPM 替代) > ⚠️ 高德官方至今未提供 SPM 包 / 也不提供 xcframework;只发布旧式 fat `.framework`,含 x86_64 + **device** arm64(**不含** simulator arm64)。 > > **结果**:真机 build OK;M 芯片 Mac iOS-simulator 链接失败。**这是项目方接受的现状**——开发期跑真机,与 daoqi msext 同款实践。详细决策原因 / 不要做哪些事,见 `Vendor/AMap/README.md` 「已知限制」章节。 1. 选中 `ylgamehall` Target → General → Frameworks, Libraries, and Embedded Content 2. 点 `+` → Add Other → Add Files 3. 一次性选两个: - `Vendor/AMap/AMapFoundationKit.framework` - `Vendor/AMap/AMapLocationKit.framework` 4. 两个的 **Embed** 列都选 **Do Not Embed**(关键,别错——静态 fat framework,Embed 会触发 `__OBJC` 段重复符号错) 5. ⌘B 编译 — **target 必须选真机**或 Generic iOS Device,模拟器 target 会因 simulator-arm64 slice 缺失而链接失败(按设计) ### A.3 添加七牛 SPM 依赖(远程) > 项目方下载的 `docs/res/objc-sdk-8.9.2/` 改 SPM 后不再使用,可保留作离线备份或直接删除(不影响工程)。 1. Xcode → File → **Add Package Dependencies...** 2. 右上搜索框粘贴:`https://github.com/qiniu/objc-sdk` 3. **Dependency Rule** 选 **Up to Next Major Version** → `8.9.0` 4. Add Package → 弹窗里 Library 选 `Qiniu` → Add Package 5. ⌘B 编译 --- ## B. 完成 A 三步后会发生什么 代码侧已经全部写好,并用 `#if canImport(...)` 守卫围着真实路径。一旦 Xcode UI 把 SDK 加进 Target,`canImport` 立刻返回 true,下面这些就会自动激活: | 文件 | 激活后行为 | |---|---| | `AppDelegate.swift` | `WeChatSDK.register()` → 真实调 `WXApi.registerApp("wx586a9b321e56efb7", universalLink: "")` | | `AppDelegate.swift` | `AMapWrapper.bootstrap()` → 真实调隐私合规 3 步 + apiKey | | `SceneDelegate.swift` | `openURLContexts` → 真实调 `WXApi.handleOpen(url, delegate:)` | | `AccreditLoginHandler` | `accreditlogin` → 真实拉起微信授权 → 反向 callback `sharelogin` 7 字段(Province 大写 P) | | `WechatShare` | `share()` → 真实调 `WXApi.send(SendMessageToWXReq)` | | `StartLocationHandler` | `startlocation` → 真实调 `AMapLocationManager.requestLocation` → 反向 callback `getlocationinfo` 9 字段(latitude/longitude string, province 小写 p) | | `BackGameDataHandler` | 子游戏 backgameData 退出时自动清 `WXApi.delegate = nil` + `LocationService.stop()` | **Qiniu 单独说明**:七牛 SDK 加进 SPM 后,`canImport(QiniuSDK)` 通过;但 `RemoteAudioHandler` 仍然是 stub(prepareaudio 不真实录音上传),原因是录音→AMR 转码链路的 opencore-amr ObjC 库未接入。`QiniuUploader.upload(...)` 单独已可用,可用于其它非录音场景。完整录音上传链路见 §C。 --- ## C. 仍需用户手动做的事(opencore-amr / AMR 录音上传) > 这一块代码侧没有自动完成,因为需要从 msext 拷贝 ObjC 源码(含 MRC)+ 修改 Build Settings(File-level `-fno-objc-arc` flag)+ Bridging Header。这些 pbxproj 改动对 Xcode 26 同步项目风险较高,留用户手动。 ### C.1 拷贝 opencore-amr 静态库 ```bash mkdir -p ylgamehall/Vendor/opencore-amr/ # 已修复版(fat header segalign 8)静态库 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(含 amrFileCodec / interf_*) cp -r ../daoqi/msext/msext/Class/Common/VoiceConvert \ ylgamehall/Vendor/opencore-amr/VoiceConverter ``` ### C.2 Xcode 工程配置 1. 选中 Target → Build Phases → Link Binary With Libraries → `+` → Add Other → Add Files → 选两个 `.a` 2. Target → Build Settings: - **Library Search Paths** 加 `$(PROJECT_DIR)/Vendor/opencore-amr` - **Header Search Paths** 加 `$(PROJECT_DIR)/Vendor/opencore-amr/include` 3. 把 `Vendor/opencore-amr/VoiceConverter/` 下的 `.m` 文件拖入项目(Add to Target ylgamehall) 4. 每个 `.m` 文件单独加 `-fno-objc-arc` 编译标志(msext 是 MRC): - Target → Build Phases → Compile Sources → 找到每个 `.m` → 双击 → 加 `-fno-objc-arc` 5. 创建 Bridging Header `ylgamehall-Bridging-Header.h` 内容: ```objc #import "VoiceConverter.h" ``` 6. Target → Build Settings → **Objective-C Bridging Header** 设为 `ylgamehall/ylgamehall-Bridging-Header.h` ### C.3 接入 Swift 录音 + 上传链路(代码侧) 完成 C.1-C.2 后回到代码侧补两个文件: **`Source/Audio/VoiceCoder.swift`**(Swift wrapper) ```swift import Foundation public enum VoiceCoder { public static func wavToAmr(_ wav: URL) throws -> URL { let amr = wav.deletingPathExtension().appendingPathExtension("amr") // VoiceConverter 是 msext ObjC 类(MRC),桥到 Swift 后直接调 let ok = VoiceConverter.convertWav(toAmr: wav.path, amrSavePath: amr.path) guard ok else { throw VoiceCodeError.transcodeFailed } return amr } public static func amrToWav(_ amr: URL) throws -> URL { let wav = amr.deletingPathExtension().appendingPathExtension("wav") let ok = VoiceConverter.convertAmr(toWav: amr.path, wavSavePath: wav.path) guard ok else { throw VoiceCodeError.transcodeFailed } return wav } public enum VoiceCodeError: Error { case transcodeFailed } } ``` **`Source/Audio/AudioRecorder.swift`**(actor) ```swift import AVFoundation public actor AudioRecorder { public static let shared = AudioRecorder() public func record() async throws -> (file: URL, duration: Int) { // AVAudioRecorder 16kHz / mono / 16-bit WAV // 等价 msext ChatVoiceRecorderVC 配置 // ... 实现略,约 60 行 } public func cancel() { /* 停录音 */ } } ``` **升级 `RemoteAudioHandler.swift` 的 prepareaudio 分支**: ```swift bridge.register("prepareaudio") { _, callback in callback?(.string("Response from prepareaudio")) Task { @MainActor in do { let rec = try await AudioRecorder.shared.record() let amr = try VoiceCoder.wavToAmr(rec.file) let uploaded = try await QiniuUploader.shared.upload(amr, timeSec: rec.duration) bridge.call("getaudiourl", data: .object([ "audiourl": .string(uploaded.fileUrl), "time": .string("\(uploaded.timeSec)") ]), callback: nil) // 子游戏页额外触发(仅 SubGameViewController 注册的 handler 时才发, // 需要在 SubGameViewController.registerBridgeHandlers 里包一层判别): // bridge.call("recordSuccess", data: ...) } catch { // 失败兜底(msext 同款乐观策略:不反向通知 H5) } } } ``` **升级 BackGameDataHandler 清理钩子**:取消那行 `AudioRecorder.shared.cancel()` 注释。 --- ## 0. 凭证总表(沿用 msext) > 全部来自 daoqi/msext 原项目,**直接复用,无需重新申请**。 | 项 | 值 | 来源 | |---|---|---| | **Bundle ID** | `com.skyapp.ylgamehall` | `daoqi/msext.xcodeproj` Release | | **App 显示名** | `进贤聚友棋牌` | `SGDefineInfo.h:103 gamehallname` | | 微信 AppID | `wx586a9b321e56efb7` | `SGDefineInfo.h:105 kAuthOpenID` | | 微信 AppSecret | `b2792724b9565be23e8f5ba548f117cf` | `SGDefineInfo.h:107 Appsecret` | | 微信 AuthScope | `snsapi_message,snsapi_userinfo,snsapi_friend,snsapi_contact` | `SGDefineInfo.h:104` | | 微信 AuthState | `wechat_sdk` | `SGDefineInfo.h:106 kAuthState` | | 高德定位 APIKey | `b0d4a8e3fcbbcc0dd96283b7df6a4494` | `daoqi/msext/Class/Common/APIKey.h:14` | | 七牛 AccessKey | `dQbQLUm1jIuL9PEq4jd6VKB-6pPxPEdg7le9KeBm` | `QiniuConfig.m:12` | | 七牛 SecretKey | `RCZpwLhAPoQ2sQQyWXzMJc7Po2MyZWfUJeW4Jmfq` | `QiniuConfig.m:13` | | 七牛 BucketName | `iosaudio` | `QiniuConfig.m:16` | | 七牛 CDN Domain | `iosaudio.daoqi88.cn` | `daoqi/msext/msext/qiniudomain/` 目录名 | ### 0.1 安全权衡(与父项目 CLAUDE.md 同步) 父项目 CLAUDE.md 第 6 节明确指出 `AppSecret` / `SecretKey` 客户端硬编码是已识别但**接受不修**的安全风险。msext IPA 已分发数年等价泄露,新外壳沿用同一 key + 沿用同款客户端直拼路径 → **同等安全等级**,**不引入新的攻击面**,**不需要后台中转接口**。 --- ## 1. 微信 OpenSDK(Phase 4.E)— 代码已就绪 ### 1.1 凭证(已具备,沿用 msext) 参 §0 总表。 ### 1.2 工程接入 参 §A.1(用户手动 Xcode 步骤)。 ### 1.3 代码已落地 | 文件 | 状态 | 说明 | |---|---|---| | `Vendor/WechatSDK/WechatOpenSDK.xcframework` | ⏳ 等用户放置 | 项目方从腾讯开放平台下 `OpenSDK2.0.5+_NoPay.zip` 解压 → 放进 `Vendor/WechatSDK/` → 按 §A.1 Embed & Sign | | `Source/SDK/WeChat/WeChatSDK.swift` | ✅ 已写 | `register()` + `handleOpenURL` 包 canImport | | `Source/SDK/WeChat/WeChatManager.swift` | ✅ 已写 | authorize / shareLink async wrapper + WXApiDelegate | | `Source/Login/WeChatAuth.swift` | ✅ 已写 | OAuth2 客户端直拼 sns/oauth2 + userinfo + 7 字段 | | `Source/Bridge/Handlers/AccreditLoginHandler.swift` | ✅ 已升级 | 真实拉起授权 + sharelogin 反向 callback(Province 大写 P) | | `Source/Share/WechatShare.swift` | ✅ 已升级 | 真实链接分享(type=2/3 截图待 Phase 4.F) | | `Source/Bridge/Handlers/BackGameDataHandler.swift` | ✅ 已加钩子 | `WXApi.delegate = nil` 子游戏 pop 时清理 | | `AppDelegate.swift` | ✅ 已加注册 | `WeChatSDK.register()` | | `SceneDelegate.swift` | ✅ 已加 openURL | `WeChatSDK.handleOpenURL(ctx.url)` | | `Info.plist` | ✅ 已加 | CFBundleURLTypes + LSApplicationQueriesSchemes 微信 3 项 | ### 1.4 验收(用户 Xcode 加完 framework 后跑) - [ ] H5 调 `bridge.callHandler('accreditlogin')` → 拉起微信 → 同意授权 → 大厅 H5 收 `sharelogin({openid,...,Province})`,**Province 大写 P** - [ ] H5 调 `bridge.callHandler('friendsSharetypeUrlToptitleDescript', {sharefriend:"2", ...})` → 拉起微信朋友圈 - [ ] H5 调 `sharefriend:"1"` → SharePanel 三按钮,点微信 → 拉起微信好友选择 - [ ] **关键回归**:截图分享 / QQ 分享 / 抖音分享路径不受影响 ### 1.5 风险 - AppSecret / SecretKey 已永久泄露(msext IPA 已分发数年)。复用同一 secret 安全等级不变,不引入新风险。 - 企业签 / TF 分发,不上 App Store,不需要 Universal Links / 审核合规。 --- ## 2. 高德定位 SDK(Phase 5)— 代码已就绪 ### 2.1 凭证(已具备) 参 §0 总表。APIKey 与 Bundle ID `com.skyapp.ylgamehall` 绑死,已复用。 ### 2.2 工程接入 参 §A.2(用户手动 Xcode 步骤)。 ### 2.3 代码已落地 | 文件 | 状态 | |---|---| | `Vendor/AMap/AMapFoundationKit.framework` + `AMapLocationKit.framework` | ✅ 已就位 | | `Source/SDK/AMap/AMapWrapper.swift` | ✅ 已写 | 隐私合规 3 步 + apiKey 注入 | | `Source/Location/LocationService.swift` | ✅ 已写 | actor,requestOnce 异步包装 + stop | | `Source/Bridge/Handlers/StartLocationHandler.swift` | ✅ 已升级 | 真实定位 + getlocationinfo 9 字段(latitude/longitude string, province 小写 p)/ 失败回 errorCode 12 | | `Source/Bridge/Handlers/BackGameDataHandler.swift` | ✅ 已加钩子 | `LocationService.shared.stop()` | | `AppDelegate.swift` | ✅ 已加注册 | `AMapWrapper.bootstrap()` | | `Info.plist` | ✅ 已加 | NSLocationWhenInUseUsageDescription 中文文案 | ### 2.4 验收 - [ ] 首次启动 → 系统权限弹框(中文文案) - [ ] H5 调 `startlocation` → 收 `getlocationinfo({...9 字段...})`,**latitude/longitude 是 string、province 小写 p** - [ ] 拒绝权限 → H5 收 `getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})` - [ ] 子游戏定位中 → 退出子游戏 → 大厅不再收悬挂定位回调 ### 2.5 风险 - **APIKey 绑定 Bundle ID**:本项目已对齐 `com.skyapp.ylgamehall`,可复用。**改 Bundle ID 必须重新申请 key** - **模拟器定位精度低**:必须真机测 --- ## 3. opencore-amr 转码库(Phase 3.B)— 用户手动 参 §C(用户手动步骤)。代码侧的 wrapper 文件待用户完成 C.1-C.2 后自行新增(§C.3 已给出代码骨架)。 --- ## 4. 七牛云上传(Phase 3.C / 3.D)— 代码已就绪(除录音链路) ### 4.1 凭证(已具备) 参 §0 总表。客户端用 AccessKey + SecretKey 本地 HMAC-SHA1 拼 token(与 msext 等价路径),**无后台依赖**。 ### 4.2 工程接入 参 §A.3(用户手动添加 SPM 依赖)。 ### 4.3 代码已落地 | 文件 | 状态 | |---|---| | `Source/Network/QiniuConfig.swift` | ✅ 已写 | 4 项常量(AccessKey / SecretKey / BucketName / cdnDomain)+ publicURL 拼接 | | `Source/Network/QiniuTokenSigner.swift` | ✅ 已写 | 纯 Swift 用 `CryptoKit.HMAC` + Base64URL 自签 token(与 msext QiniuManager.m:200-230 等价) | | `Source/Network/QiniuUploader.swift` | ✅ 已写 | actor,`upload(_ file: URL, timeSec: Int) async throws -> UploadedFile`,包 `QNUploadManager` 异步 | ### 4.4 上层调用待 §C 完成 `Source/Bridge/Handlers/RemoteAudioHandler.swift` 的 `prepareaudio` 真实链路依赖 §C 的 AMR 转码 + AudioRecorder。完成后取消 §C.3 的 TODO 注释即可激活全链路。 ### 4.5 验收(依赖 §C 完成) - [ ] H5 调 `prepareaudio` → 录音 3 秒 → 上传 → 大厅 H5 收 `getaudiourl({audiourl:"http://iosaudio.daoqi88.cn/xxx.amr", time:"3"})` - [ ] 浏览器粘贴 `audiourl` 能直接下载 amr 文件 - [ ] 退出子游戏 → 进行中上传任务被 cancel(Swift Task.cancel 传播) --- ## 5. 接入完毕的清单检查 每接入完一个 SDK 跑一遍: - [ ] `BuildProject` 成功 - [ ] `Vendor//README.md` 已记录版本号 / 来源 / 凭证沿用 msext 的备注 — 已就位 - [ ] Info.plist 改动只在原生侧,未触 H5 任何文件 — 已就位 - [ ] `BackGameDataHandler.swift` 对应清理钩子已加 — 微信 + 高德已加 canImport 守卫;录音待 §C - [ ] `docs/Development-Plan.md §5 / §8` 进度勾选已同步 - [ ] `docs/Verification-Checklist.md` 对应 Phase 验证项已补充 - [ ] 真机跑过验收项(模拟器对微信 / 高德 / AVAudioRecorder 都不可靠) - [ ] **Bundle ID 没改**(`com.skyapp.ylgamehall`),改了所有 key 失效 --- ## 6. 与原项目 daoqi 的关系 接入过程中若行为与文档描述不一致,**优先参考原 daoqi 工程实现**(参 `CLAUDE.md`「原项目 daoqi:遇到问题时的参考来源」一节): - 微信 OAuth + 7 字段:`daoqi/msext/Class/RootVC/NewRootVC.m:2428`(`sharelogin` 7 字段,含 `Province` 大写) - 微信 SDK 12 个 MMAPP flag:`daoqi/msext/AppDelegate.m` 搜 `registerApp` - 高德 9 字段:`daoqi/msext/Class/RootVC/gameController.m:2528 / 2557`(`getlocationinfo` 字段顺序与类型) - 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) - 七牛域名读取:`daoqi/msext/AppDelegate.m:142` 用 `FuncPublic filename:@"qiniudomain"` 读 `Resources/qiniudomain/` 下第一个子目录名作为实际 Domain