高德官方至今不提供 xcframework,只发布 fat .framework(含 x86_64 + device arm64)。M 芯片 Mac iOS-simulator target 链接失败是 fat 库的物理限制,与 daoqi msext 维护实践一致——开发期跑真机即可。 不动 Build Settings、不自造 xcframework、不切 CocoaPods 三条「不要做」也 一并明示,避免未来维护者花时间在这条死路上: - Vendor/AMap/README.md 新增「已知限制」章节,含决策原因 + 三条「不要做」 - docs/SDK-Integration-Guide.md §A.2 简明提示 + 链回 README Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
18 KiB
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(<ModuleName>)守卫里。只需用户在 Xcode UI 完成 3 件事(详见 §A 用户手动步骤),代码即激活真实路径。
A. 用户手动 Xcode 步骤(必做 3 件事)
选型偏好:官方 SPM 优先;闭源 / 无官方 SPM 的走 Vendor 二进制(社区 SPM wrapper 不计入官方,仓库一旦停更即为定时炸弹,按 daoqi
CLAUDE.mdADR-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。
- 选中
ylgamehallTarget → General → Frameworks, Libraries, and Embedded Content - 点
+→ Add Other → Add Files - 选
Vendor/WechatSDK/WechatOpenSDK.xcframework - Embed 列选 Embed & Sign(xcframework 是动态库,与下面 AMap 的
Do Not Embed不同,别错) - ⌘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「已知限制」章节。
- 选中
ylgamehallTarget → General → Frameworks, Libraries, and Embedded Content - 点
+→ Add Other → Add Files - 一次性选两个:
Vendor/AMap/AMapFoundationKit.frameworkVendor/AMap/AMapLocationKit.framework
- 两个的 Embed 列都选 Do Not Embed(关键,别错——静态 fat framework,Embed 会触发
__OBJC段重复符号错) - ⌘B 编译 — target 必须选真机或 Generic iOS Device,模拟器 target 会因 simulator-arm64 slice 缺失而链接失败(按设计)
A.3 添加七牛 SPM 依赖(远程)
项目方下载的
docs/res/objc-sdk-8.9.2/改 SPM 后不再使用,可保留作离线备份或直接删除(不影响工程)。
- Xcode → File → Add Package Dependencies...
- 右上搜索框粘贴:
https://github.com/qiniu/objc-sdk - Dependency Rule 选 Up to Next Major Version →
8.9.0 - Add Package → 弹窗里 Library 选
Qiniu→ Add Package - ⌘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-arcflag)+ Bridging Header。这些 pbxproj 改动对 Xcode 26 同步项目风险较高,留用户手动。
C.1 拷贝 opencore-amr 静态库
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 工程配置
- 选中 Target → Build Phases → Link Binary With Libraries →
+→ Add Other → Add Files → 选两个.a - Target → Build Settings:
- Library Search Paths 加
$(PROJECT_DIR)/Vendor/opencore-amr - Header Search Paths 加
$(PROJECT_DIR)/Vendor/opencore-amr/include
- Library Search Paths 加
- 把
Vendor/opencore-amr/VoiceConverter/下的.m文件拖入项目(Add to Target ylgamehall) - 每个
.m文件单独加-fno-objc-arc编译标志(msext 是 MRC):- Target → Build Phases → Compile Sources → 找到每个
.m→ 双击 → 加-fno-objc-arc
- Target → Build Phases → Compile Sources → 找到每个
- 创建 Bridging Header
ylgamehall-Bridging-Header.h内容:#import "VoiceConverter.h" - 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)
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)
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 分支:
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 |
✅ 已写 |
Source/Location/LocationService.swift |
✅ 已写 |
Source/Bridge/Handlers/StartLocationHandler.swift |
✅ 已升级 |
Source/Bridge/Handlers/BackGameDataHandler.swift |
✅ 已加钩子 |
AppDelegate.swift |
✅ 已加注册 |
Info.plist |
✅ 已加 |
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 |
✅ 已写 |
Source/Network/QiniuTokenSigner.swift |
✅ 已写 |
Source/Network/QiniuUploader.swift |
✅ 已写 |
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/<SDK>/README.md已记录版本号 / 来源 / 凭证沿用 msext 的备注 — 已就位- Info.plist 改动只在原生侧,未触 H5 任何文件 — 已就位
BackGameDataHandler.swift对应清理钩子已加 — 微信 + 高德已加 canImport 守卫;录音待 §Cdocs/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(sharelogin7 字段,含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