 joywayerandClaude Opus 4.7
|
b54803966a
|
IPA 升级跳转后等 didEnterBackground 自动 exit(0),让 iOS 顺利替换 IPA
【问题】
当前点"确定升级"后跳 Safari,但旧版本 App 仍在前台/后台 active —— iOS 安装
新 IPA 时会卡住或要求用户手动 kill 旧 App,体验不顺。
【方案】
跳 Safari 后挂一次性 didEnterBackground 监听 → 0.3s 后 exit(0):
- 等系统完成 active → inactive → background 切换再 exit,不会被 Springboard 视为 crash
- 比直接 exit(0) 优雅,避免前台闪退动画
- 用户从 Safari 取消回来不退(下次切后台才触发)
【UX 同步】
alert message 提示"确认后将跳转下载并自动退出当前 App",让用户预期对齐。
不影响 H5 契约(纯原生外壳交互)。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-27 21:11:35 +08:00 |
|
 joywayerandClaude Opus 4.7
|
83d9502dee
|
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>
|
2026-06-27 21:07:55 +08:00 |
|
 joywayerandClaude Opus 4.7
|
8125dd4d11
|
1.11 修订:VersionResolver 改单链 4 层 fallback,删除双子树合并算法(契约影响)
【契约影响】
- RemoteConfig 模型:Agent 节点移除 channellist 字段(线上 JSON 实际只挂 gamelist;此前误读 chulishengji 引入的 channellist 是空 schema)
- VersionResolver 解析算法:从"双子树 + 字段级合并"改为"agentlist → gamelist → channellist → marketlist 单链 4 层 fallback",行为按 daoqi NewRootVC.m:689-810 主路径
- 对外 API ResolvedVersion / resolve(config:agentId:channelId:marketId:gameId:) 签名零变化,WebContainerViewController / SubGameViewController 调用点未动
【设计要点】
- 4 节点类型 conform 同一 private protocol RemoteConfigNode
- 5 字段共用 pickString / pickInt 两个倒序 fallback 工具,不允许为某字段单独写 if 链
- buildChain 集中处理"任一层 id 不匹配立刻截断"语义
【文档同步】
- Design §6.3.2 整段重写
- Plan ADR-008 顶部追加"第三轮修订(2026-06-27)"小节 + 第二轮误读复盘
- Plan ADR-008-D 替换为单链 4 层算法说明 + 保留修订史
- Plan §1.11 / ADR-008-I / 最后更新日期 / Verification-Checklist L73 同步
参考契约章节:docs/H5-Native-Implementation-Design.md §6.3.2、docs/Development-Plan.md ADR-008-D
BuildProject 通过。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-27 20:04:19 +08:00 |
|
joywayer
|
90a523736e
|
3.A/3.F 修复:voicePlaying 入参类型兼容 + mediaTypeAudio canPlay 诊断
## 发现的根因
实测日志 mediaTypeAudio 进入了 native(amrFileCodec frame=107 打印两次确认
转码运行),但缺 gameui_play_voice 触发 log → canPlay=false 被短路。
原因是 voicePlaying handler 解析 H5 入参时类型不容忍。
之前实现:
let on = (data?.asInt ?? 0) == 1
asInt 只对 BridgeData.number case 返回值。若 H5 传 string "1"(实测确实
会传)→ asInt 返回 nil → `?? 0 = 0` → `0 == 1 = false` → 开关被关闭。
daoqi 用 ObjC `[data intValue]`(NSString/NSNumber 都能转),永远能
正确处理两种类型。
## 修复
VoicePlayingHandler:改用 asLooseString 比较 "1" 字符串,兼容 H5 传:
- string "1" → asLooseString → "1" → on=true ✓
- number 1 → asLooseString → "1" → on=true ✓
- string "0" → asLooseString → "0" → on=false ✓
- number 0 → asLooseString → "0" → on=false ✓
- 其它 → on=false(保守拒绝)
同时加 log.debug 记录每次 voicePlaying 调用的 raw + 解析后值,便于后续
诊断 H5 业务行为。
## 诊断辅助
RemoteAudioHandler.handleMediaTypeAudio 增加 log.debug:
mediaTypeAudio user=X transcoded=Y canPlay=Z
让真机测试能直接看到 canPlay 实际值,避免再次盲猜。
## 验证
BuildProject 通过 0 错误。真机重试 mediaTypeAudio 应能看到:
voicePlaying H5 → raw=1 → isEnabled=true
mediaTypeAudio user=0 transcoded=true canPlay=true
gameui_play_voice user=0
|
2026-06-27 12:07:20 +08:00 |
|
 joywayerandClaude Opus 4.7
|
522087f398
|
修复下载进度瞬间跳 100%:换手动 downloadTask + 续作
async 版 await session.download(from:) 内部走 completion-handler 路径,
session 级 delegate 的 didWriteData 全部被跳过,结果只有兜底 onProgress(1.0)
在下载完成后才触发——用户看到的"瞬间 100%"就是这个唯一一次回调。
换成手动 session.downloadTask(with:).resume() + CheckedContinuation:
- didWriteData 正常按 URLSession 节奏在 opQueue 上派发,进度真实推进
- didFinishDownloadingTo 内同步把临时文件 move 到沙盒 Caches 的已知位置
(否则 delegate 返回后系统立即清掉那个临时位置)
- didCompleteWithError 里 resume 续作
- continuation/moveError 跨线程(actor → opQueue)用 NSLock 保护
LobbyZipUpgrader / SubGameDownloader 同款修复(大厅 zip 下载 + 子游戏 zip 下载)。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-24 22:40:59 +08:00 |
|
joywayer
|
06b7686e17
|
启动文字提示,h5调试加入
|
2026-06-24 21:20:05 +08:00 |
|
joywayer
|
1d231a52ef
|
4.B QQ 链接分享改 previewimagedata 内联 base64:修缩略图破图
实测控制台 `unable to make sandbox extension: [1: Operation not permitted]`
证实 previewimageUrl=file://... 跨 app 沙盒无效 —— QQ 进程读不到调用方 app
tmp 目录里的文件,渲染破图占位。
msext 这段实现一直是装饰性代码(线上同样破图但没人改)。新外壳按原则 B
现代化修正:
- 字段名改用 QQ Open SDK 实际约定的 `previewimagedata`(值=图像字节
base64,而非 file path base64)
- 内容来源改用 app icon JPEG 0.6 编码后 base64 内联到 URL
- 删 writeAppIconToTemp(不再写临时文件)
App icon 60-180px JPEG 0.6 后 base64 约 5-15KB,iOS openURL ~64KB 限额下
余量充足。这条修正违反"原则 A 等价 msext"吗?不——契约 §3.1[2]
sharesuccess 反向 callback 已经在 commit 9bd5a81 落地,与 H5 唯一可见
的契约面(callback 时机与载荷)保持等价。msext 这处缩略图字段错写属于
原项目内部 bug,新外壳的"原则 B 内部自由重构"覆盖范围,按现代标准修正
不破契约。
|
2026-06-24 18:45:05 +08:00 |
|
joywayer
|
2f3a35892b
|
4.B QQ 链接分享补 previewimageUrl:用 app icon 当缩略图
QQ 会话里链接分享的缩略图位空白,因为 News 类型缺 previewimageUrl 字段。
对齐 msext QQShareManager.m:517-522 + 1271-1278 的写法:
1. app icon JPEG 0.7 写到 NSTemporaryDirectory()/qq_share_<UUID>.jpg
2. 拼 file://<path> 字符串
3. base64 后再 URL-encode
4. append &previewimageUrl=<那串>
writeAppIconToTemp() 与 appIconImage() 直接抄 WechatShare 同源策略(读
Info.plist CFBundleIcons → CFBundlePrimaryIcon → CFBundleIconFiles 最后
一个)。两处复制可接受 —— Share 模块统一缩略图工具可后续抽到
ImageProvider,本次保持改动范围最小。
|
2026-06-24 18:03:58 +08:00 |
|
joywayer
|
34a336d859
|
4.B QQ 链接分享补齐 URL Scheme 全量参数:可弹好友选择面板
【契约影响】§3.1[2]仅 native 实现细节,H5 无感
先前 QQShare.shareLink 只拼 version/cflag/req_type/url/title/description
6 个字段,QQ 客户端识别失败 → 只打开主页,不弹好友选择。对齐 msext
QQShareManager.shareToQQFriend:QQShareTypeNews URL Scheme fallback 路径
补齐:
- 公共参数:src_type=app / app_id / share_id / file_type=news /
callback_type=scheme / callback_name=QQ<hex(appID,8)> /
thirdAppDisplayName / app_name(后两者 base64+urlenc)
- 内容参数:News 类型 title/desc/url 必须 base64 后再 URL-encode(与
msext line 513-515 一致)
新增常量(沿用 daoqi 注册值):
appID = 102793577 (daoqi kQQAppID)
appIDHex = 06208169 (callback_name 后缀,QQ 协议要求)
appName = 进贤聚友棋牌 (msext kQQAppName 同源)
Info.plist 增加 CFBundleURLSchemes tencent102793577 —— QQ 客户端识别
"调用者已在 Info.plist 注册 AppID"的前置条件,没这一项 QQ 直接拒绝弹
分享面板。
⚠️ AppID 102793577 在 QQ 互联开放平台绑给 com.chao.msext。新外壳 bundle
id com.skyapp.ylgamehall —— QQ 客户端唤起 UI 时实测大概率不严格校验,
但若后续遇到拒绝/回调失败,需到 https://connect.qq.com/ 重新注册 AppID
绑给新 bundle id,只改 QQShare 顶部 3 个常量 + Info.plist 即可。
截图分享(type==2)保持剪贴板 + 引导框模式 —— URL Scheme 内联 base64
1080p 截图编码后 URL ≈ 5MB,iOS openURL 不可靠。
|
2026-06-24 17:49:23 +08:00 |
|
joywayer
|
0636aadbec
|
4.B 修复:friendsShare 路由跑偏(sharefriend/type 字段兼容 number)
【契约影响】docs/H5-Native-Contract.md §3.1[2]
H5 实际传:{"sharefriend":1, "sharetype":0, "type":2, ...}(number 而非
契约描述的 string "1"/"2")。先前用 obj["sharefriend"]?.asString 只匹配
.string case,遇到 number 全部 fallback 到 "",导致:
- sharefriend = "" → ShareCenter.dispatchSubGame default 分支 →
WechatShare.share(scene: .timeline) → **错走朋友圈**
- type = "" → WechatShare.share else 分支 → **错走链接分享**
实际 H5 意图:sharefriend=1 (好友/弹 SharePanel) + type=2 (截图分享)。
看到的现象:H5 想截图分享给好友,实际成了链接分享到朋友圈。
修复:sharefriend / sharetype / type 改用 asLooseString(number → 整数
字符串,与 commit b058990 修 mediaTypeAudio.user 同款)。对齐原 msext
ObjC 行为:[NSNumber intValue] 与 [NSString intValue] 都得到整数,所以
不论 H5 传 string 还是 number 都能识别。
webpageUrl / title / description 三项 H5 始终传 string,保持 asString
不变。
|
2026-06-24 09:16:27 +08:00 |
|
joywayer
|
a1332ed942
|
4.B FriendsShareHandler 加调试日志:原始 JSON + 解析字段 + 分发 mode
H5 调用分享接口时,按以下顺序输出到 Console(subsystem=ylgamehall,
category=FriendsShare):
← H5 friendsShare mode=lobby raw={"sharefriend":"1",...}
friendsShare parsed:
sharefriend = "1" (1=好友/SharePanel, 2/其他=朋友圈)
sharetype = "1" (1/2=微信, 3=闲聊;新外壳忽略)
type = "1" (1=链接, 2=截图, 其他=远端图)
webpageUrl = "https://..."
title = "..."
description = "..."
→ H5 sharesuccess fired: success="2", type="1"
dispatch → lobby (直发微信 3 type 分支)
raw 字段直接 JSONSerialization 序列化整个 BridgeData,含 H5 传入的所有
键(不限于上面解析的 6 项),便于排查 H5 端偷偷加字段或类型漂移。
|
2026-06-24 09:07:36 +08:00 |
|
joywayer
|
5d4bdc2e29
|
4.B 分享按页面 mode 拆分:大厅直发微信 3 分支 / 子游戏 SharePanel
【契约影响】docs/H5-Native-Contract.md §3.1[2]
原 msext 把 friendsSharetypeUrlToptitleDescript 同名 handler 在两个 VC
各注册一份且**行为完全不同**:
- NewRootVC.m(大厅):直发微信,3 type 分支
type==1 sendLinkURL(sharelogo 缩略图)
type==2 sendImageData(截图 40% 缩放为 thumb)
其他 NSData fromURL → sendImageData(公众号二维码场景)
- gameController.m(子游戏,活路径,早 return):
sharefriend==1 SharePanel 三选一
其他 WechatShareManager 朋友圈兜底(2 分支:截图/链接)
先前新外壳两个 VC 都注册同一份 handler 走"子游戏"逻辑,导致大厅页:
1. 强行弹 SharePanel 多一步操作
2. 丢失"远端图"分支(公众号二维码被错当链接发)
按页面 mode 拆开:
- FriendsShareHandler 加 Mode { lobby, subGame } 参数
- ShareCenter 拆 dispatchLobby(直发微信)/ dispatchSubGame(SharePanel)
- WechatShare 加 shareLobby() 实现完整 3 type 分支:
type==1 → shareLink + mediaTagName "游戏下载链接" + appIcon thumb
(原 msext 用 sharelogo.png,按用户决定 fallback 到 app icon)
type==2 → shareImage + mediaTagName "游戏截图分享" + 截图 0.4x thumb
+ messageExt = H5 description
其他 → URLSession 下载 NSData 直发 + 远端图 0.4x thumb
+ mediaTagName/messageExt 固定字面"公众号二维码分享"
- WebContainerViewController 注册 .lobby;SubGameViewController .subGame
附带修 sharefriend 默认值:之前 ?? "1"(弹面板)与原 msext nil→[intValue]
==0→朋友圈微信不一致。改成 ?? "",由 ShareCenter default 分支兜底走
朋友圈微信,与原项目完全等价。
|
2026-06-24 08:19:21 +08:00 |
|
joywayer
|
68c668a06c
|
4.B 截图分享补齐 msext 字段:WeChat description / QQ 引导框
【契约影响】docs/H5-Native-Contract.md §3.1[2]type==2 分支
WeChat 截图分享之前丢了 H5 description(msext 通过
sendImageData:MessageExt: 写到 WXMediaMessage.description,接收方在
微信聊天预览里能看到)。链接分享之前完全没传 thumb(msext 用 app
icon)。统一补齐:
- WeChatManager.ShareImage 加 messageExt / mediaTagName 字段;
shareImage 内 message.description = messageExt + message.messageExt
= messageExt + setThumbImage(thumb) + mediaTagName
- WeChatManager.ShareLink 把 thumbnailURL: String? 替换为 thumb:
UIImage?;shareLink 内 setThumbImage(thumb) + mediaTagName。drop
Sendable 因 UIImage 不可 Sendable(调用都在 MainActor 上无风险)
- WechatShare 双分支统一传 appIconImage() 做 thumb;截图分支 messageExt
= content.desc + mediaTagName "进贤聚友棋牌游戏截图分享";链接分支
mediaTagName "进贤聚友棋牌游戏分享"(字面对齐 msext WechatShareManager.m)
QQ 截图分享原本 4-fallback URL 链不靠谱(file_type=news 给截图场景实际
无意义)。简化为 Douyin 同款模式:截图 → UIPasteboard.image → 弹"立即
打开 QQ / 稍后分享"二选一 Alert。承认 ADR-006 无 QQ SDK 退化的现实,
但 UX 不再让用户摸不着北。
|
2026-06-24 07:53:14 +08:00 |
|
joywayer
|
9bd5a81e6c
|
4.B 分享逻辑对齐 daoqi msext:调用即触发 sharesuccess
【契约影响】docs/H5-Native-Contract.md §3.1[2]/§3.2[11]
H5 端 sharesuccess 时机改为"调用 friendsShare handler 即立刻发",与
原 msext 行为等价(live path 完全不等 SDK / URL Scheme 回包)。用户
明示有意为之:URL Scheme + 剪贴板路径本就没有可靠回包,乐观策略避免
H5 业务卡住。
3 平台分支严格 1:1 对齐 daoqi msext:
WechatShare(对齐 WechatShareManager shareWithContent)
- type=="2" → 截图 JPEG 0.6 + sendImageData + app icon thumb
- else → 链接分享 sendLinkURL + app icon thumb
- 移除原"远端图"分支(原项目无此分支)
- 新增 appIconImage helper(读 Info.plist CFBundleIcons,对齐
[self getAppIconImage])
QQShare(对齐 QQShareManager shareWithContent + simpleShareToQQFriend)
- type=="2" → 截图 → UIPasteboard.image → 4 URL Scheme fallback
- else → mqqapi://share/to_{fri,qzone}?req_type=1&url=...
&title=...&description=...(webpageUrl 空时 req_type=0)
- 不依赖 QQ SDK(CLAUDE.md ADR-006)
DouyinShare(对齐 DouyinShareManager shareWithContent)
- type=="2" → 截图 → UIPasteboard.image → "立即打开抖音"引导框
- else → title+"\n\n"+desc → UIPasteboard.string → 引导框
双侧空时兜底 "来自进贤聚友棋牌的精彩内容分享"
- 放弃之前的"存相册 + 自动拉起"激进路径,回到原项目"用户主动点"路径
架构调整:
- SharePlatform.share 改 sync fire-and-forget(去 ShareResult 返回)
- ShareCenter.dispatch / SharePanel.show 去 completion 参数
- FriendsShareHandler:解析后先发 responseCallback("sharefriend")
与 sharesuccess({success:"2", type:<sharefriend>}),再异步 dispatch
|
2026-06-24 07:34:03 +08:00 |
|
joywayer
|
d471393806
|
4.B SharePanel: 替换字符占位为 imageset 图标
从 docs/res/Images.xcassets 拷贝 shareWechat / shareQQ / shareDouyin
三套 imageset(@1x/@2x/@3x)到 ylgamehall/Assets.xcassets/。SharePanel
按钮从"彩色圆 + 首字"改为 setImage + clipsToBounds + 等比缩放,未安装
平台改 alpha 灰显(与原 daoqi msext SharePanel.m:147 createButtonWithImage
同款写法)。
ylgamehall/ 是 PBXFileSystemSynchronizedRootGroup,无需手改 pbxproj。
|
2026-06-24 07:33:26 +08:00 |
|
joywayer
|
b058990862
|
3.F 修复:mediaTypeAudio.user 字段兼容 number 与 string
## 问题
实测 H5 调 mediaTypeAudio 传入:
{audiourl: "http://...amr", user: 0, type: 1}
user 是 number(0),不是 string。我之前用 obj["user"]?.asString 严格
要求 string,导致解析失败 → silent return,远端语音无法播放。
## 根因
daoqi 用 `[data objectForKey:@"user"]` 后 `[NSString stringWithFormat:
@"%@", userid]` 容忍 NSNumber(ObjC `%@` 对 NSNumber 调 description
自动转 string)。新外壳走 Swift 强类型解析,丢了这个隐式兼容。
## 修复
给 BridgeData 加 `asLooseString` 扩展:
- string → 原样
- number → 整数样 double 转 Int 字符串(0.0 → "0"),其它走 String(d)
- bool → "true"/"false"
- null/array/object → nil
mediaTypeAudio handler 把 `obj["user"]?.asString` 改为
`obj["user"]?.asLooseString`。`audiourl` 保持 asString(URL 永远是
string)。
`asLooseString` 也可供其它 H5 字段类型不严格的 handler 复用。
## 验证
BuildProject 通过 0 错误。真机重试 mediaTypeAudio 应能正确解析
user=0 / user=12345 / user="abc" 等各种 H5 传入形态。
|
2026-06-24 02:12:21 +08:00 |
|
joywayer
|
c3a0ddbf8b
|
3.B 修复:七牛 token 401 bad token(base64URL padding + scope 字段)
## 问题
录音 + 转码正常,但七牛上传报:
status: 401 / error: bad token / QiniuUploadError error 1
## 根因
逐字段对照 daoqi QiniuManager.m:202-236 generateUploadToken,发现两处
关键不一致:
### 1. base64URL padding:新外壳删了 `=`,daoqi 保留
daoqi urlsafeBase64EncodeData:240-245 只替换 `+→-` 和 `/→_`,**保留
末尾 `=` padding**。
新外壳额外做了 `=→空` 替换,本意符合 RFC 4648 §5 base64URL 的可选
padding 删除,但七牛后端 decode token 时**依赖 padding 还原 base64**,
删 `=` 会让 HMAC 验签数据流不一致 → 401 bad token。
### 2. scope 字段:新外壳带了 `:key` 后缀,daoqi 只用 bucket
daoqi QiniuManager.m:207 putPolicy.scope = bucketName(限定 bucket,
允许任意 key)。
新外壳之前传 key 参数后构造 `bucket:key`(限定 key 精确匹配),与
daoqi 行为不一致。带 `:key` 后缀也是七牛合法 scope,但七牛后端
校验更严,且无必要——key 已经在 putFile API 单独传递,token 内不必
重复声明。
## 修复
- base64URLEncode:移除 `=→空` 替换
- uploadToken(key:):key 参数标记为废弃但保留(避免破坏调用方),
putPolicy.scope 改为只用 bucket
- 顺带:JSONSerialization 不再强制 sortedKeys(daoqi 用 options:0)
## 验证
BuildProject 通过 0 错误。真机重试录音上传应不再 401。
|
2026-06-24 02:09:28 +08:00 |
|
joywayer
|
7d89a9d3ee
|
3.E 修复:换用 iOS 17+ AVAudioApplication.requestRecordPermission 避免 EXC_BREAKPOINT
## 问题
第一次调用 prepareaudio 弹出系统权限对话框,用户点允许/拒绝都触发
Thread 10 EXC_BREAKPOINT 崩溃。
## 根因猜测
deprecated 的 AVAudioSession.requestRecordPermission(_:) 在
iOS 26.5 SDK 上内部桥接到 AVAudioApplication 时,与
withCheckedContinuation 的 Swift Concurrency 状态机交互异常。
具体表现:completion 在 Thread 10 触发 cont.resume(returning:),
runtime 检测到某种 actor / continuation 状态违规,触发 trap。
## 修复
新增 `requestMicrophonePermission()` 私有助手:
```swift
if #available(iOS 17.0, *) {
return await AVAudioApplication.requestRecordPermission() // 原生 async
} else {
return await withCheckedContinuation { ... } // 旧 SDK 兼容
}
```
iOS 17+ 走原生 async API,不经 Continuation 状态机,避开 corner case;
iOS 15.6 / 16 走 deprecated API 兼容路径。
## 验证
BuildProject 通过 0 错误。真机首次启动 → 按录音按钮 → 系统权限对话框
出现 → 点允许/拒绝 → 应无崩溃。
|
2026-06-24 02:05:25 +08:00 |
|
joywayer
|
6f10d231ad
|
3.E 优化:权限被拒绝时的 Alert 增加"去授权"跳转按钮
iOS 行为约束:用户在系统权限对话框选择"拒绝"后,再次调
requestRecordPermission 不会再弹对话框、直接返回 false。唯一恢复路径
是用户手动去「设置 → app → 麦克风」开启。
原 Alert 只有"确定"按钮(对齐 daoqi),用户被拒绝后需要自己摸索路径
去设置。优化为两按钮:
- "取消":关闭 Alert
- "去授权":UIApplication.open(openSettingsURLString) 直接跳本 app
的设置页
文案保留 daoqi NewRootVC.m:316 的引导语("需要访问您的麦克风,请启用
麦克风-设置/隐私/麦克风!")。
属于 UX 改进,对 H5 不可见,不影响契约(按钮属于 native 内部 UI,
原则 B 范畴)。
|
2026-06-24 01:58:02 +08:00 |
|
joywayer
|
530309c258
|
3.E 修复:notDetermined 路径触发 cb 让 H5 释放业务锁
## 问题
用户首次点击录音 → 弹权限对话框 → 用户允许 → H5 整体无法再点击。
## 根因
我上次修复让 .notDetermined 分支 return 时不触发 cb,违反 daoqi 行为:
daoqi NewRootVC.m:322 在 ifauth==1(NotDetermined)路径下走录音流程并
**立即触发 responseCallback**。
H5 业务在 callHandler('prepareaudio') 调用后通常会上"录音中"业务锁
(加触摸遮罩、禁用按钮等),等收到 cb 才解锁。系统权限对话框已经把
H5 的 in-flight touch 打断为 touchcancel,H5 业务的 touchcancel 处理
可能不完善 → 业务锁不释放 → 整个 H5 后续无法点击。
## 修复
.notDetermined 分支权限请求完成后**仍然触发 cb**:
```swift
case .notDetermined:
_ = await requestRecordPermission()
callback?(.string("Response from prepareaudio")) // ← 触发 cb
return
```
H5 收到 cb 释放业务锁,整个 H5 恢复可点击。本次实际未录音 → 无后续
getaudiourl,H5 按"无 getaudiourl 的失败路径"走(与 daoqi 转码失败
silent 路径等价)。用户第二次按录音按钮走 .authorized 分支正常录音。
## 验证
BuildProject 通过 0 错误。
|
2026-06-24 01:56:52 +08:00 |
|
joywayer
|
97749ac177
|
3.E 修复:首次启动权限请求与浮层 touchObserver 死锁
## 问题
首次启动 app,H5 按住麦克风按钮触发 prepareaudio:
1. 主动调 AVAudioSession.requestRecordPermission 弹系统权限对话框
2. 用户为了点"允许"必须松手
3. 松手被 RecordingAwareWindow.touchObserver 立刻捕获 → recorder.stop()
4. 浮层瞬间关闭,录音失败
5. 用户感受:"按一次没反应,再点也没用"
## 根因
我之前用 `requestRecordPermission` 主动请求权限(会同步弹系统对话框),
与 daoqi `FuncPublic.ifauth`(只读 `AVCaptureDevice.authorizationStatus`,
不主动请求)行为不一致。主动请求 + 按住状态 + touchObserver 全局监听
三者形成死锁。
## 修复
改用只读状态 + 三分支处理:
- `.denied / .restricted`:弹 Alert + return(对齐 daoqi)
- `.notDetermined`(首次):**主动弹权限对话框但不启动录音浮层**。
用户做完选择后 return,不触发 cb。第二次按录音按钮时走 .authorized
分支正常录音
- `.authorized`:直接启动录音浮层
## 与 daoqi 行为对比
daoqi 在 .notDetermined 时 ifauth 返回 1(继续启动录音),但实测此路径
也有体验问题——浮层弹出 + 系统对话框遮挡 + 用户松手立刻触发 touchEnded
→ 录音空文件 → 转码失败 → silent return。最终用户体验也是"按一次没
反应"。
新外壳的修复让首次行为更明确(不启动录音 = 不会有空文件上传),与 daoqi
真实终态(不触发 getaudiourl)等价,UX 等价(首次启动需按两次)。
## H5 契约影响
无。"不触发 cb" 与 daoqi 转码失败 silent return 等价;H5 收不到
getaudiourl 反向 callback 时本来就要兼容(daoqi 各类失败路径都不触发 cb)。
## 验证
BuildProject 通过 0 错误。
|
2026-06-24 01:50:04 +08:00 |
|
joywayer
|
df2a099fb5
|
3.F:mediaTypeAudio 完整实现(远端语音下载→转码→播放→反向 callback)
替换 mediaTypeAudio handler 的 stub 为完整链路。至此 Phase 3 录音 + 远端
语音播放整套流程接通;prepareaudio / mediaTypeAudio / VoicePlaying / 反向
callback 全部对齐 daoqi msext 行为。
## 完整链路
1. H5 调 mediaTypeAudio({audiourl, user})
2. 解析入参;参数不全 → log + return(不触发 cb,对齐 daoqi 入参空时
走到 file 不存在 return 的语义)
3. `VoicePlayer.shared.recordIncomingUser(user)` 更新 lastUserId
⚠️ 无论后续是否实际播放都更新(对齐 daoqi gameController.m:438
`self.User_id=userid;` 总是执行的行为)
4. 注册 VoicePlayer.onFinish 绑定当前 bridge([weak bridge])→ 播完触发
`gameui_stop_voice(lastUserId)` 反向 callback
5. `URLSession.shared.data(from:)` 异步下载 AMR
- 下载失败 → log + return,**不触发 cb**
(对齐 daoqi NewRootVC.m:335-337 file 不存在 return)
6. 写入沙盒 .amr 临时文件
7. `AMRCodec.amrToWav` 转码到 `{tempName}_AmrToWav.wav`
(文件名约定对齐 daoqi NewRootVC.m:341)
- 转码失败 → log,**但 cb 仍触发**(对齐 daoqi NewRootVC.m:355-357
amr转wav失败 NSLog 后仍触发 responseCallback)
8. `await VoiceCenter.shared.isEnabled`(canbofang 等价)+ 实际播放
- 仅转码成功 + 开关 enabled 时 play + 触发 `gameui_play_voice(user)`
对齐 daoqi NewRootVC.m:343-353 if (canbofang) { play + callHandler }
- `gameui_play_voice` data 是 user 字符串(非 dict)
9. 触发 `cb("Response from mediaTypeAudio")` —— **总是**触发
(除下载失败 return 路径外)
## VoicePlayer.onFinish 绑定时机
每次 mediaTypeAudio 调用时更新 onFinish 闭包,绑定当前 bridge。
[weak bridge] 防止 bridge 释放后 onFinish 触发野指针。
## 关键契约点(已验证字面字符串)
- handler 名:"mediaTypeAudio"
- 入参字段:"audiourl" / "user"
- 回包字面:"Response from mediaTypeAudio"
- 反向 callback:
- "gameui_play_voice" data = user String
- "gameui_stop_voice" data = lastUserId String
## 验证
BuildProject 通过 0 错误。至此 Phase 3 全部子任务(3.A / 3.B / 3.C /
3.D / 3.E / 3.F)落地,语音功能完整对齐原项目。
|
2026-06-24 01:38:17 +08:00 |
|
joywayer
|
f2b456cf00
|
3.E:prepareaudio 完整实现 + 大厅/子游戏分流接入
替换 RemoteAudioHandler 中 prepareaudio 的 stub 为完整链路:权限检查 →
录音浮层 → 转码 → 七牛上传 → 反向 callback getaudiourl + 子游戏额外
recordSuccess。mediaTypeAudio 仍为 stub(Phase 3.F 处理)。
## 完整链路
1. H5 调 prepareaudio
2. `AVAudioSession.requestRecordPermission`
- 拒绝:弹 Alert("需要访问您的麦克风..."),**不触发 cb**
(对齐 daoqi NewRootVC.m:314-319)
3. 同意 → `RecordingPresenter.start(on: hostVC, fileName:)`
- 启动失败:log + return,**不触发 cb**
- 启动成功:立即触发 `cb("Response from prepareaudio")`,对齐 daoqi
NewRootVC.m:322 `[recorderVC beginRecordByFileName:..]` 之后 cb 行为
4. 用户松手 / 60s 自动停 → `processCompletedRecording` 后台 Task
- 用 AVAudioPlayer 读 wav `duration` → `round` 取整秒(对齐 daoqi
NewRootVC.m:1891-1893)
- 生成 amr 文件名:
- 大厅 `yyyyMMddHHmmss%08X.amr`(无下划线,NewRootVC.m:1908)
- 子游戏 `yyyyMMddHHmmss_%08X.amr`(有下划线,gameController.m:2269)
- `AMRCodec.wavToAmr`
- `QiniuUploader.upload`(key = 本地文件名,对齐 daoqi 上传命名)
5. 反向 callback(MainActor):
- `getaudiourl({audiourl: String, time: String})`
⚠️ time 是字符串(%ld 整秒),不是 number — 对齐 daoqi
NewRootVC.m:1923 / gameController.m:2305
- **仅子游戏**额外 `recordSuccess({fileUrl, fileName, fileKey})`
对齐 daoqi gameController.m:2310;大厅 NewRootVC 无此 callHandler
## register 签名扩展
新增参数:
- `hostVC: @escaping @MainActor @Sendable () -> UIViewController?`
—— 每次录音重新求值,避免捕获过时引用
- `isSubGame: Bool` —— 决定是否触发 recordSuccess
WebContainerViewController(大厅)传 `isSubGame: false`;
SubGameViewController(子游戏)传 `isSubGame: true`。
## QiniuUploader 改动
key 生成方式:UUID → 本地文件名(lastPathComponent)。让调用方完全控制
key 命名,对齐 daoqi 录音文件命名规则。H5 通过 audiourl 转发给对端,对端
按文件名规律假设,必须严格对齐。
## 错误处理
启动失败 / 转码失败 / 上传失败:log + return,不触发反向 callback。
对齐 daoqi 同款 silent return 行为(mediaTypeAudio 下载失败、转码失败
也是同样 silent return 模式)。
## 验证
BuildProject 通过 0 错误。
|
2026-06-24 01:36:25 +08:00 |
|
joywayer
|
173bf79a74
|
3.D:VoicePlayer 远端语音播放器 + gameui_stop_voice 触发点
支撑 Phase 3.F mediaTypeAudio handler 的播放子链路。
## 行为对齐原项目
- `lastUserId`:mediaTypeAudio 入参 user 字段,每次进来都更新(无论
voicePlaying=1/0、无论是否实际播放),对齐 daoqi gameController.m:438
`self.User_id=userid;` 总是执行的行为
- `gameui_stop_voice` payload 用 `lastUserId`,**不**是 didFinishPlaying
传入的 player 关联值(多条消息连发时 stop 发最新 user,与 daoqi 行为
一致——这是原项目的"特征",不是 bug)
- 覆盖式 play:前一条还在播时收到第二条直接替换 player(对齐 daoqi
`voicePlayer = [[AVAudioPlayer alloc]...]` ARC 覆盖行为)
- 主动 stop 不触发 onFinish(daoqi 没有主动 stop 路径)
## Swift 6 并发处理
AVAudioPlayerDelegate 方法签名 nonisolated;AVAudioPlayer 是
non-Sendable 不能跨 actor 传递。用 ObjectIdentifier(Sendable)传递
身份,handleFinish 在 MainActor 内重新查 self.player 比对身份。
防御覆盖式 play 时旧 player stop 触发延迟 delegate:仅当 didFinish
的 player 与当前 self.player 同身份时才清理 + 触发 onFinish。
## 验证
BuildProject 通过 0 错误。
|
2026-06-24 01:31:52 +08:00 |
|
joywayer
|
c398a8dd58
|
3.C:录音组件落地(VoiceRecorder + Overlay + Presenter + Window 子类)
完整支撑 Phase 3.E prepareaudio handler 的录音子链路:用户按住 H5 麦克风按钮
→ Native 录音 → 松手停止 → 触发完成回调。
## 新增模块
- `RecordingAwareWindow`:UIWindow 子类,提供 touch 旁路监听通道
(static touchObserver closure)。默认行为与 UIWindow 完全一致,仅
RecordingPresenter 注册 observer 后才触发额外行为。daoqi msext
CustomWindow.m + nScreenTouch 通知同款思路,改用 closure 注入
- `VoiceRecorder`:AVAudioRecorder + AudioSession + 0.1s 计时器封装。
ObservableObject + @Published(暴露 averagePower / elapsed 给 SwiftUI)。
录音参数 8kHz/16bit/mono PCM 严格对齐 daoqi(AMR-NB 编码硬要求)。
60s 上限对齐 daoqi kDefaultMaxRecordTime
- `RecordingOverlay`:SwiftUI 浮层(UI 内部实现,按 CLAUDE.md「原则 B 原生
内部自由」与 daoqi 不一致)。半透明黑底胶囊 + SF Symbols 麦克风 + 音量
光晕动画 + 倒计时(剩 10s 显示秒数,对齐 daoqi UI 逻辑)
- `RecordingPresenter`:组装入口。挂载 overlay 到 hostVC 全屏覆盖 +
注册 touchObserver。提供 `start(on:fileName:onFinish:)` 单一 API +
`cancelActive()` 强制取消
## SceneDelegate 改动
UIWindow → RecordingAwareWindow 实例化。变量类型仍为 UIWindow?
(多态),sendEvent 走子类版本。零行为变化(observer 未注册时)。
## 为何必须走 UIWindow 子类
H5 麦克风按钮按下触发 prepareaudio → 录音浮层在按住状态下中途弹出。
iOS touch 事件流是 view hit-test 链,**in-flight touch 不传给中途加入
的 view**,浮层 view 自身收不到 touchesEnded,用户松手时检测不到。
唯一可靠路径是 UIWindow.sendEvent 旁路截获(UIWindow 接收所有 touch
event 无论 in-flight 还是新 touch)。daoqi msext 也是这条路。
## Combine 引入
VoiceRecorder 用 ObservableObject + @Published 给 SwiftUI 数据绑定,
需要 import Combine。CLAUDE.md「避免 Combine」精神是禁用 Publisher 链
替代 async/await,不是禁止 ObservableObject(iOS 15.6 无 @Observable
宏可替代)。已在 VoiceRecorder.swift 注释说明。
## 验证
BuildProject 通过 0 错误,全部新文件 XcodeRefreshCodeIssuesInFile 0 诊断。
|
2026-06-24 01:29:04 +08:00 |
|
joywayer
|
5b38456c66
|
3.B:vendor opencore-amr + AMRCodec Swift wrapper,打通 AMR↔WAV 编解码
为支撑 H5 的 prepareaudio / mediaTypeAudio 录音上传 + 远端语音播放
(docs/H5-Native-Contract.md §3.1 [4][5] + §3.2 [9])接入 AMR-NB
编解码能力。AMR-NB 是跨端(iOS / Android / 旧版本客户端)契约硬约束,
不可换 AAC / Opus。
## 接入路径
直接 vendor daoqi msext 现成的 fat .a(已上线多年),不打 xcframework /
不重编源码:
- Vendor/OpenCoreAMR/v0.1.x/libopencore-amrnb.a:daoqi 同款 5-slice fat
库(含 2026-06-20 lipo -segalign 8 修过的对齐版本)
- Vendor/OpenCoreAMR/v0.1.x/{interf_dec,interf_enc}.h:amrnb 公开头
- Vendor/OpenCoreAMR/README.md:来源、版本、修复历史、为何不打 xcframework
- ylgamehall/Source/Audio/AMRFileCodec/{amrFileCodec.h,amrFileCodec.mm}:
daoqi 同款 wrapper(文件级 WAV↔AMR 处理)
- ylgamehall/Source/Audio/AMRCodec.swift:Swift 薄 wrapper(wavToAmr /
amrToWav 两个纯函数 + AMRCodec.Error)
理由见 Vendor/OpenCoreAMR/README.md「为何不打 xcframework / 不重编源码」
一节,本质是 opencore-amr 整个 iOS 生态没有"现代主流"分发形态,社区无
官方 SPM / xcframework 维护,强行打反而引入新风险。
## 关键技术点:extern "C" 修复 ObjC++ name mangling
amrFileCodec.h 在原项目里没有 extern "C" 包裹函数声明,daoqi 能跑是
因为调用方 VoiceConverter.mm 也是 ObjC++,双方按相同 C++ name mangling
链接能对上。
新外壳从 Swift 通过 bridging-header 调用,Swift 按 C 函数解析,期望符号
_EncodeWAVEFileToAMRFile / _DecodeWAVEFileToWAVEFile(C 链接);但
.mm 编译时默认 C++ name mangling,符号变成 __Z25Encode...,导致链接器
报 Undefined symbol。
修复:在 amrFileCodec.h 的函数声明外加 #ifdef __cplusplus / extern "C"
包裹。这是标准 C/C++ 头文件兼容写法,对 daoqi 老路径无影响(ObjC++
调用方仍能链接 C-linkage 函数)。
## .gitignore:白名单 Vendor 下的 .a
发现历史遗留问题:原 .gitignore *.a 通配规则把 libWeChatSDK.a 也排除
了,意味着别人 clone 此仓库后链接失败。本次顺带修复:
!Vendor/**/*.a
让 Vendor 下的 .a 显式纳入跟踪。本提交同时把 libWeChatSDK.a(历史
缺失)+ libopencore-amrnb.a(本次新增)一并加入 git 跟踪。
## 工程改动
pbxproj 改动由用户在 Xcode UI 完成(4 项 Build Settings:Add .a 到
Frameworks group / Header Search Paths +2 / Library Search Paths +1;
EXCLUDED_ARCHS arm64-simulator 因真机调试场景暂未配,需要时再加)。
## 验证
BuildProject 通过,0 错误,符号链接正确。XcodeRefreshCodeIssuesInFile
对 AMRCodec.swift 0 诊断。
|
2026-06-24 01:20:30 +08:00 |
|
joywayer
|
5463b07fb6
|
voicePlaying 回包对齐原项目:从 "Response from voicePlaying" 改为 "voicePlaying"
契约影响:H5-Native-Contract.md §3.1 [6] voicePlaying responseCallback。
新外壳此前按其它 handler 的 "Response from XXX" 模式套写,与原项目
gameController.m:573 / NewRootVC.m:451 不一致(原项目这里就是 handler 名本身
作回包,是历史包袱)。虽然 H5 通常不校验 cb 内容,但字面保持一致是契约硬约束。
|
2026-06-24 00:07:25 +08:00 |
|
 joywayerandClaude Opus 4.7
|
ce3fbb7106
|
getWebdata 反向调用:大厅 observer 改永久挂钩,对齐 msext 双发行为
之前大厅 / 子游戏 observer 都 lifecycle-tied(viewWillAppear setup /
viewWillDisappear teardown),sub-game 内 overlay backgameData pop 时
只有 sub-game H5 收到 getWebdata(大厅 observer 已 teardown)。这与 msext
"NewRootVC observer 永久 + gameController observer 永久" 的双发行为不一致。
大厅 H5 可能依赖"sub-game 内 overlay 操作触发自身刷新",破坏契约。
修复:
- WebContainerViewController.subGameReturnObserver 改"永久挂钩"
- 加 subGameReturnObserverInstalled 标志,首次 setupExternalSubscriptions
时 install 一次,后续永不解绑(msext NewRootVC.m:593 等价)
- teardownExternalSubscriptions 不再触碰它
- SubGameViewController.subGameReturnObserver 保持 lifecycle-tied
- 子游戏自身 backgameData pop:sub-game 已 teardown → 仅大厅触发(单发,msext 等价)
- 子游戏内 overlay backgameData pop:sub-game.viewWillAppear 重新 setup
→ 大厅 + sub-game 双触发(双发,msext 等价)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 23:17:39 +08:00 |
|
 joywayerandClaude Opus 4.7
|
6ef90f9c3f
|
SubGameDirectoryStore.keyPrefix 也加 nonisolated
之前只把 3 个 method 标 nonisolated,漏了 keyPrefix 静态常量。actor 内的
ensureReady 访问 method 时跨 actor 拿到 keyPrefix 会触发 MainActor 隔离检查
失败(SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 项目设置下,未标注则默认
MainActor)。补 nonisolated。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 23:07:23 +08:00 |
|
 joywayerandClaude Opus 4.7
|
7be7a2abbc
|
子游戏 zip 升级链路完整对齐 msext(持久化目录 + advance + 版本比对)
之前新外壳的 SubGameDownloader 只看 index.html 是否存在,缺这两个关键行为:
- gamefilepath 持久化(msext NSUserDefaults["game_name"] → gamefilepath)
- 远端 game_version > 本地 version.xml 时升级 + 目录末尾追加 "1"
(msext gameController.m:1414 等价 [NSString stringWithFormat:@"%@1", gamefilepath])
本次完整补齐:
1. 新增 SubGameDirectoryStore:UserDefaults 持久化 directory → actualDir 映射
- current(forKey:) / advance(forKey:) / saveInitialIfNeeded(forKey:)
- nonisolated 让 actor / MainActor 都能直接调
2. SubGameDownloader 三分支决策(msext gameController.m:1239-1255 + uplevel: 等价):
- 目录不存在(首装)→ 下载到 currentDir,不 advance
- 目录存在 + 远端 > 本地 → advance + 下载到新目录
- 远端 ≤ 本地 → 复用 currentDir
API 改为按入参(directoryKey / gameStart / remoteVersion / remoteZipURL)调用,
返回 Outcome.actualDirectory 暴露实际生效目录名。
3. LocalAudioHandler.register 的 assetsRoot 改 @MainActor closure
srcIsloop 每次调用时取最新值,让子游戏升级(effectiveGameDir 末尾追加 "1")
后下一次 srcIsloop 自动用新目录路径。
4. SubGameViewController 引入 effectiveGameDir:
- init 时同步从 SubGameDirectoryStore 读初始值(首次=request.gameDir)
- boot pipeline 内调 ensureReady,从 outcome 同步升级后的新目录名
- loadFileURL / AppDataWriter / srcIsloop closure 全部走 effectiveGameDir
- resolveBoot 返回 (lobbyResolved, subGameResolved) 完整 ResolvedVersion,
给 ensureReady 用 gameVersion 做版本比对
5. WebContainerViewController 大厅侧 LocalAudioHandler 改 closure 形式(路径不变)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 23:04:57 +08:00 |
|
 joywayerandClaude Opus 4.7
|
18fd782156
|
弹层 Cookie / localStorage 改回与大厅共享,对齐 msext
OverlayViewController 之前用 .nonPersistent() 主动隔离弹层 cookie,避免
第三方外链污染游戏会话。但 msext threeView 用 UIWebView 默认与 app 内
其它 WebView 共享 cookie,H5 业务(运营公告 / 充值 / 客服等外链页)依赖
读到大厅登录态。隔离破坏契约。
改回 .default(),与 BridgedWebView(默认即 default)一致,全局共享。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 21:54:37 +08:00 |
|
 joywayerandClaude Opus 4.7
|
fd9976c4b9
|
下载进度回调改 session-level delegate(ad-hoc 不触发 didWriteData)
LobbyZipUpgrader 和 SubGameDownloader 之前用 `session.download(from:delegate:)`
的 iOS 15+ async API,把 DownloadProgressDelegate 作为 ad-hoc delegate 传入。
该 API 的 delegate 参数实际只收 URLSessionTaskDelegate 回调,**不会**触发
URLSessionDownloadDelegate.didWriteData,所以 0…1 进度从未上报,splash 进度条
一直停在 0,只有末尾兜底的 onProgress(1.0) 跑一下,看起来"一出现就 100%"。
改为每次下载临时新建一个 URLSession,把 progressDelegate 在 init 时绑到
session 级别(带独立 OperationQueue),下载结束 defer invalidateAndCancel
释放。delegate 现在能收到 didWriteData,真实字节比例上报,进度条 0→1 平滑。
副作用:删除 actor 字段 `session` 与可注入的 `init(session:)`,改用静态
`defaultConfig: URLSessionConfiguration` 复用超时配置(无外部调用方使用过自定义
session init)。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 21:34:38 +08:00 |
|
 joywayerandClaude Opus 4.7
|
57d4d93781
|
SplashOverlay 抽公共组件 + 进度条配色统一
大厅 / 子游戏之前各有一份 private SplashOverlay:lobby 版含错误态 UI(拉远端
配置失败 splash 上提示重试 / 前往设置);sub-game 版只有 image + label +
progressView,配色规则不同(width 0.45 vs 0.7,progressTintColor 不设)。
抽到 Source/WebView/SplashOverlay.swift 共用组件:
- 含完整加载 UI + 错误态 UI(sub-game 不调 showError,但保留兜底)
- 进度条 progressTintColor = .black,trackTintColor = .systemGray5(实色浅灰)
之前 lobby 用 .systemBlue + label.withAlphaComponent(0.15) 半透明轨道,
黑色填充推过半透明轨道时视觉上像"颜色逐渐加深";改实色轨道后是干脆的
"黑滑过灰",无加深错觉
- progressView.setProgress(animated: true) 保留补间,让快下载也能看到平滑增长
WebContainer / SubGame 删除各自的 private SplashOverlay 类定义,引用同一份。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 21:34:25 +08:00 |
|
 joywayerandClaude Opus 4.7
|
e50dfb75af
|
子游戏跳转链路修复 + H5 音频统一可用
修复后子游戏 SwitchOverGameData 跳转完全可用,行为对齐 daoqi msext:
【zip URL 解析】H5 字段 gamedownloadurl 实为 gameid(msext gameController.m:544
注释"游戏ID")。RemoteConfigClient 新增 lastParsed 缓存 + current() getter;
SubGameViewController.resolveBoot 做两次 VersionResolver.resolve:
- lobby 视角(gameId=bc.gameId)→ ResolvedVersion 传 AppDataWriter 算
app_appversion 审核标志(msext result_state 等价)
- sub-game 视角(gameId=H5 传的 gameid)→ 真实 game_zip 下载 URL
之前 SubGameDownloader 直接把 token 当 URL 用,URLSession 报 -1002
unsupported URL;AppDataWriter resolvedVersion 始终 nil 导致子游戏 app_appversion
永远 0、丢失审核切换能力。
【push 前停大厅背景音】AppCoordinator.showSubGame 节流通过后、push 前调
AudioPlayer.stopAllBackground(msext NewRootVC.m:549-552 等价)。
【H5 音频】
- AVAudioSession.setCategory(.playback) 在 AppDelegate 启动期设置,让原生
+ WKWebView 内 <audio> 都不受静音键影响(msext RootVC.m:1302 等多处等价)
- WKWebViewConfiguration.mediaTypesRequiringUserActionForPlayback = [],子游戏
push 后立即 loadFileURL 自播背景音不再被 WebKit 静默拦截
- LocalAudioHandler.register 增 assetsRoot 参数,srcIsloop 按容器各自的 H5
根目录拼 assets/wav/{src}(msext gameController.m:364 等价,大厅 / 子游戏
音频文件在各自 zip 内不重叠)。修前所有路径都指向大厅根,子游戏读不到自己的
音频文件,AVAudioPlayer 报 OSStatus 2003334207 (wht?
kAudioFileUnsupportedFileTypeError)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 21:33:46 +08:00 |
|
 joywayerandClaude Opus 4.7
|
053232ed26
|
微信授权链路诊断 print 清理
授权链路已跑通(commit 625f504),撤掉 a060f29 加的临时诊断 print
+ 本次 shim 的 install 成功日志。AccreditLoginHandler / WeChatManager /
WXOpenURLShim 三处共 13 行 print 一次性清干净,行为不变。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 17:24:59 +08:00 |
|
 joywayerandClaude Opus 4.7
|
625f504b85
|
微信 1.8.2 兼容 iOS 17+ SDK:swizzle 弃用的 openURL: 转发到新 API
WeChat OpenSDK 1.8.2 内部仍调 -[UIApplication openURL:],本项目链接
iOS 26 SDK 时被系统强制返回 NO("BUG IN CLIENT OF UIKIT ... Force
returning false"),导致 WXApi.sendAuthReq / sendReq 一律失败、H5 拿到
authStepFailed(-3)。
WXOpenURLShim 用 method_setImplementation 把这一个 selector 的 IMP
替换为转发到 open(_:options:completionHandler:) 并同步返回 true,
作用域仅限此老 API。在 WXApi.registerApp 之前 install 一次。
daoqi/msext 用 1.8.2 没此问题,是因为它 IPA 链接的 base SDK 更低、
系统对老 app 放行 openURL:。新外壳无此豁免,需要这层兜底。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 17:22:17 +08:00 |
|
 joywayerandClaude Opus 4.7
|
93bbbc3d59
|
修复 1.8.2 WXApi 调用:用 Swift 简化后的方法名
Swift 编译器对 ObjC API 自动应用名称简化规则(Swift 3+ SE-0044),
即使是 1.x 老 SDK header 也会被简化。ObjC selector 不变,运行时
仍调 +[WXApi getApiVersion] / +[WXApi sendReq:],但 Swift 端必须
写简化后的名字:
- ObjC +(NSString *)getApiVersion → Swift WXApi.getVersion()
- ObjC +(BOOL)sendReq:(BaseReq*)req → Swift WXApi.send(_:)
错误信息里 Swift 编译器明确给出:"has been renamed to 'getVersion()'"
+ "obsoleted in Swift 3"。
注释里同步说明 selector 与 Swift 名映射关系,避免未来维护者误以为
我们调到的是 2.x SDK 的 getVersion(实际仍是 1.8.2 的 getApiVersion)。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 09:13:08 +08:00 |
|
 joywayerandClaude Opus 4.7
|
05f1396645
|
微信 SDK 降级 2.0.5 → 1.8.2(与 daoqi msext 同款,避开 UL 强制)
H5 调 accreditlogin 后 sendAuthReq success=false,根因:微信 OpenSDK
2.0.x 强制 Universal Link,传空串被 SDK 拒绝;daoqi msext 用 1.8.x 不强制
UL,多年稳定上线。沿用 1.8.2 避免无谓的 UL 运维负担(HTTPS 域名 + AASA +
微信开放平台审核 30min-1d)。
文件改动:
- 新增 Vendor/WechatSDK/v1.8.2/{WXApi,WXApiObject,WechatAuthSDK}.h +
libWeChatSDK.a(16MB,fat: i386+armv7+armv7s+x86_64+arm64,从 daoqi
msext/Class/SDKExport/ 直接拷贝)
- 删除 Vendor/WechatSDK/WechatOpenSDK-NoPay.xcframework(2.0.5)
- 新增 ylgamehall/ylgamehall-Bridging-Header.h(1.x .a 无 Swift module,
必须通过 Bridging Header 导入 WXApi.h / WXApiObject.h)
代码侧(5 个 Swift 文件):
- WeChatSDK.swift:register() 改为 1.x 单参数 WXApi.registerApp(appID),
去掉 universalLink 参数;handleOpenURL 不变(1.x/2.x 同款)
- WeChatManager.swift:去掉 #if canImport(WechatOpenSDK) 守卫(Bridging
Header 直接导入,符号编译期必在);authorize sendAuthReq 改 1.x BOOL
返回(无 completion);shareLink/shareImage 改 WXApi.sendReq(req)
(1.x BOOL 返回,无 completion);getVersion → getApiVersion
- AccreditLoginHandler / WechatShare:同样去掉 canImport 守卫和 stub 分支
- BackGameDataHandler:更新注释说明 1.x WXApi delegate 由 sendAuthReq
逐次传入,WeChatManager singleton 持有不需要清理(1.x 早期 setDelegate:
API 已废弃)
文档:
- Vendor/WechatSDK/README.md 重写,说明 1.8.2 决策原因 + 文件结构 +
Xcode 接入 5 步 + M Mac simulator 已知限制
- docs/SDK-Integration-Guide.md §A.1 同步重写
⚠️ **本 commit 后 BuildProject 会失败**,需要用户在 Xcode UI 完成 5 步:
1. 删 Frameworks 里旧 WechatOpenSDK-NoPay.xcframework 引用
2. Add Vendor/WechatSDK/v1.8.2/libWeChatSDK.a(Do Not Embed)
3. Add 4 个系统库:libz.tbd / libsqlite3.tbd / Security.framework /
CFNetwork.framework(Do Not Embed)
4. Build Settings → Header Search Paths += $(PROJECT_DIR)/Vendor/WechatSDK/v1.8.2
5. Build Settings → Objective-C Bridging Header = ylgamehall/ylgamehall-Bridging-Header.h
详细操作步骤见 Vendor/WechatSDK/README.md「Xcode 接入步骤」。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 08:27:40 +08:00 |
|
 joywayerandClaude Opus 4.7
|
a060f29070
|
微信授权诊断 print:定位 H5 调用 → 拉起微信卡在哪一步
H5 调 accreditlogin 仍无反应。加临时 print 排查链路:
AccreditLoginHandler:
- 入口"H5 调用命中 ✅" → 确认 WVJB 协议握手 + handler 注册 OK
- canImport(WechatOpenSDK) 真值 → 确认 SDK 是否真链接进了 binary
- Task @MainActor 入口 → 确认 actor hop 没卡死
- 失败分支区分用户取消(errCode -2)/ 其它
WeChatManager.authorize():
- isWXAppInstalled() → 用户手机是否真装了微信
- getVersion() → 加载的 SDK 版本字符串(验证链接的 framework)
- topmostViewController() nil 检测 → present 上下文是否拿到
- sendAuthReq completion success=true/false → SDK 是否成功拉起微信 app
跑一次 H5 调 accreditlogin 看 Xcode console 输出序列,按 print 出现/缺失
精确定位卡点(H5 没调进来 / SDK 未链接 / VC 拿不到 / SDK 拉起失败)。
排查完成后可删除 print。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 07:36:57 +08:00 |
|
 joywayerandClaude Opus 4.7
|
624dd93c65
|
修复微信授权无响应:authorize 改用 sendAuthReq:viewController:delegate:
H5 调 accreditlogin 后无反应。对照 daoqi msext 找到根因:
msext NewRootVC.m → WXApiRequestHandler.m:sendAuthRequestScope: 用的是
`[WXApi sendAuthReq:req viewController:vc delegate:[WXApiManager sharedManager]]`
(新版 SDK 2.x 推荐路径,传 viewController + delegate)。
而 ylgamehall 之前用 `WXApi.send(req)`——旧版 1.x 方法,新版头里已无声明,
不传 viewController(SDK 无法弹起微信 modal context)+ 不传 delegate
(SDK 不知道往哪发回包)= H5 调了无反应。
修复:
- authorize:改用 `WXApi.sendAuthReq(_:viewController:delegate:completion:)`,
传当前最顶层 VC(新增 topmostViewController() helper:connectedScenes →
foregroundActive windowScene → keyWindow.rootVC → 穿过 presented / nav.visible)
+ self(WXApiDelegate)。completion 处理拉起失败立即 fail continuation。
- shareLink / shareImage:从旧 send 改新 `WXApi.send(_:completion:)`
(sendReq:completion: 的 Swift 桥名)。分享回包仍由 SceneDelegate
openURL → WXApi.handleOpen(url:delegate:WeChatManager.shared) → onResp
路径接收,不需要在 send 时再传 delegate。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 07:33:58 +08:00 |
|
 joywayerandClaude Opus 4.7
|
be09e6e697
|
splash 进度条宽度从屏幕 45% 加长到 70%
原 0.45 倍宽度在横屏中央显得偏短,下载进度反馈不够明显。加长到 0.7 倍
更接近常见 app 启动进度条视觉。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 07:29:57 +08:00 |
|
 joywayerandClaude Opus 4.7
|
1247c14351
|
splash 启动图改 scaleAspectFill 全屏覆盖(与 WebView 同款撤销黑边)
接上一 commit f4307cc(WebView 全屏铺满):splash 启动图也应保持一致策略,
否则启动瞬间(iOS 系统 LaunchScreen)和过渡期(SplashOverlay)仍会显示
左右大黑边,与 WebView 全屏的视觉不连续。
3 处 contentMode 从 scaleAspectFit 改为 scaleAspectFill:
- LaunchScreen.storyboard(iOS 启动瞬间)
- WebContainerViewController 内 SplashOverlay.imageView
- SubGameViewController 内 SplashOverlay.imageView
scaleAspectFill 行为:素材 1136×640(16:9)在现代 iPhone 横屏(19.5:9)
上高度撑满,宽度按比例放大→左右轻微裁切。启动图左右是空白底色,logo
+ 文字主体居中可见,裁切无影响。
等价 daoqi msext LaunchImage Asset Catalog 时代「启动图覆盖整屏」体感。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 07:26:15 +08:00 |
|
 joywayerandClaude Opus 4.7
|
f4307ccc80
|
WebView 改全屏铺满(与 daoqi msext 同款),撤销 16:9 letterbox
用户反馈:现代 iPhone 横屏(19.5:9 比例)下 16:9 letterbox 出现明显
左右大黑边,与原项目视觉不一致。
调研 daoqi msext NewRootVC.m:256:实际用 CGRectMake(0, 0, DEVW, DEVH)
直接全屏 WKWebView,没有任何 letterbox。H5 viewport 自身处理设计稿
(1280×720)→ 设备实际宽高的伸缩。msext 上线多年用户已习惯此视觉。
改 WebContainerViewController + SubGameViewController 的 setupBridgedWebView:
- 去掉 16:9 aspect 约束 + 居中 + width/height max/fill 优先级方案
- 改为 top/bottom/leading/trailing 四 edge 直接撑满 view
文件头注释同步:从「16:9 letterbox」改为「全屏铺满」。
原 Design §6.3.5 的 letterbox 决策已被现网实测推翻,未来如某些 H5
页面需要恢复 letterbox 再单独抽 LetterboxLayout(YAGNI)。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 07:23:34 +08:00 |
|
 joywayerandClaude Opus 4.7
|
4ca4459ad5
|
启动期错误态精确分类:NWPath 区分真断网/权限拒,按钮语义动态切换
用户反馈:之前的文案统一映射 NSURLError code,没区分"真没网"和"权限
被拒"——两类 iOS 14+ 上都报 -1009,但 NWPath.unsatisfiedReason 里有
wifiDenied / cellularDenied 标记可精确区分。点击重试时也没有针对权限
被拒场景做特殊处理(重发请求被同样的权限决定拒绝,原地循环)。
改动:
1. **NWPath snapshot 精确分类**
- 新增 BootErrorKind: noNetworkAccess / wifiDenied / cellularDenied /
networkTimeout / networkCannotReach / localResourceMissing / unknown
- 新增 currentPathSnapshot() async:临时 NWPathMonitor 等首次 update
拿当前 path
- classifyBootError 接 path 参数:先看 NWPath.unsatisfiedReason
(iOS 14.2+)区分权限拒/真断网;fallback 才按 NSURLError code
- runBootPipeline 的 catch 内 await snapshot,传入精确 kind
2. **按钮语义按 kind 切换**(同一个按钮承担双语义,UX 一致)
- 权限拒(wifiDenied / cellularDenied)→ 按钮显示"前往设置",
点击跳 UIApplication.openSettingsURLString
- 其他(noNetworkAccess / timeout / unreachable / localResource)→
按钮显示"重试",点击重跑 runBootPipeline
- SplashOverlay.showError 加 actionTitle 参数,通过 UIButton.Configuration
动态改 title
3. **didBecomeActive 自动重试**(行业最佳做法)
- 权限拒分支跳设置时挂 UIApplication.didBecomeActiveNotification 监听
- 用户在设置改完权限切回 app → 自动 runBootPipeline → 进入大厅
无需用户再点任何按钮
- 一次性 observer:触发后自动取消,避免重复
- runBootPipeline 入口同时 stop NWPathMonitor + stop settingsReturnObserver
保证多次重试干净
文案精确化:
- noNetworkAccess:"当前未联网 / 请检查 Wi-Fi 或蜂窝数据后重试"
- wifiDenied:"未授权使用无线局域网 / 请前往 iOS 设置 → 本应用 → 打开
「无线数据」,授权后将自动重试"
- cellularDenied:"未授权使用蜂窝数据 / 请前往 iOS 设置 → 本应用 → 打开
「无线数据」,或连接 Wi-Fi 后重试"
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 07:19:50 +08:00 |
|
 joywayerandClaude Opus 4.7
|
0e446df97a
|
splash 进度条配色:systemBlue 填充 + .label 15% alpha 轨道
跟错误态"重试"主按钮 systemBlue 视觉一致;轨道用 .label 系统语义色
的 15% alpha,浅深模式自适应(浅模式淡灰、深模式淡白),在浅色启动图
上比默认 UIProgressView 灰条更显眼。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 07:13:13 +08:00 |
|
 joywayerandClaude Opus 4.7
|
34f76e8824
|
启动期错误态 UX 调整:浅背景深色字,仅保留"重试"按钮
按用户反馈精简错误态 UI:
- splash 启动图是浅色背景(米白底 + logo + 蓝色文字),白色文字在上面
看不清。错误态显示时改为 backgroundColor = .systemBackground(浅模式
白、深模式黑),同时隐藏启动图 imageView,文字用 .label / .secondaryLabel
系统语义色确保两种 appearance 下都对比清晰
- 去掉"前往系统设置"次按钮:仅保留蓝色填充主按钮"重试"
- "无法连接服务器"文案删去"或前往系统设置"尾句
"权限弹窗"诉求的实际实现:
重试按钮直接重跑 runBootPipeline → RemoteConfigClient.fetch →
URLSession.dataTask 重新发起请求。这正是 Apple 推荐的"让网络请求
自然触发权限弹窗"模式:
- 首次启动若系统网络权限弹窗未响应/被拒,重新发请求会再次触发系统
对权限状态的评估,可能复弹
- 用户在 iOS 隐私管理里改了权限后回 app,点重试就能成功
- NWPathMonitor 也在静默监听,切到正常网络后无需点击即自动重试
不需要额外的"手动申请权限" API 代码(iOS 没有公开的 API 强制弹起
被拒绝的网络权限弹窗);现有「重新发请求」即是教科书做法。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 07:10:23 +08:00 |
|
 joywayerandClaude Opus 4.7
|
5731588d33
|
启动期错误处理 v2:撤回降级,splash 错误态 + NWPathMonitor 自动重试
撤回上一 commit 74f6925 的"远端 config 失败就降级进大厅"路径——错过升级
判定 = 用户用旧 IPA 不被强制升级 = 业务风险。新策略:
**远端 config 失败 → 停留 splash 显示错误态**(不弹 modal alert / 不暴露原始 NSError)
行业主流方案:
1. SplashOverlay 扩展错误态 UI(隐藏 label / progress,显示 title +
message + "重试" 主按钮 + "前往系统设置" 次按钮),通过 onRetry /
onOpenSettings closure 与 controller 解耦
2. WebContainerViewController.presentBootError 按 BootErrorKind 分类给出
友好文案(networkOffline / networkTimeout / networkCannotReach /
localResourceMissing / unknown)
3. 启动期独立 NWPathMonitor,仅在"从无网变有网"时静默自动重试一次
(初始 nil 状态不触发,避免死循环;用户切到 Wi-Fi 后无需手动操作)
4. runBootPipeline 重新进入时 stopBootRetryWaiting + splash.hideError
保证多次重试干净(防止 monitor 泄漏)
进大厅的必要条件(恢复严格):
- ResourceUnzipper.ensureReady 成功(本地 H5 zip 就位)
- RemoteConfigClient.fetch 拿到正确 config(升级判定可执行)
- 本地 appVersion >= 远端(否则走 IPA 升级 modal alert)
- showmessage 为空(否则走运营 modal alert)
- LobbyZipUpgrader 完成(含跳过升级 / 真升级两路径)
业务流程信号(operationalMessage / ipaUpgradeRequired)继续用 modal alert
(这两类不是错误,是产品决策的弹窗永停);技术错误(网络/解压/写文件等)
统一进 splash 错误态 + 自动重试。
raw error 仍 print 到 Xcode console(含 NSURLErrorDomain code 等开发期
排查信息),用户视角永不暴露技术细节。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 06:47:04 +08:00 |
|
 joywayerandClaude Opus 4.7
|
74f6925be3
|
启动期错误处理 UX 改造:优雅降级 + 友好文案,不暴露技术细节
修复用户反馈的体验问题:网络权限未同意 / 长期不响应权限弹窗时,
启动会弹出"启动失败" + 原始 NSError("Error Domain=NSURLErrorDomain
Code=-1009 ...大量技术细节..."),用户被吓懵。
按行业主流做法(参考微信/支付宝/网易云音乐启动期错误处理)改两件事:
1. **优雅降级:远端 config 失败 ≠ 启动失败**
runBootPipelineSteps 把"拉远端 config + 升级判定"整段包 do/catch:
- 业务流程信号(operationalMessage / ipaUpgradeRequired)正常抛出弹窗
- 网络 / 解析 / H5 zip 下载任何失败 → 用 LocalVersionReader 构造
fallback ResolvedVersion,跳过升级判定,直接加载本地 H5 大厅
- splash 文案区分在线 "加载大厅..." / 离线 "离线加载..."
- raw error 仍 print 到 Xcode console 便于排查
理论依据:ResourceUnzipper.ensureReady 之后本地 H5 zip 已就绪,
远端 config 仅用于升级判定,失败时本地大厅完全可用。
2. **友好文案 + 多按钮 alert(showRetryableAlert)**
- 新增 BootErrorKind 分类:剥洋葱看 NSURLError code 映射到
networkOffline (-1009) / networkTimeout (-1001) / networkCannotReach
(-1003/-1004/-1005/-1009/-1018/-1019/-1020) / localResourceMissing /
unknown
- 每类配友好标题 + 正文("网络未连接"/"网络较慢"/"暂时无法连接服务器"
而非 raw "Error Domain=...")
- 按钮组合按类别:网络类含"去设置"(跳 UIApplication.openSettingsURLString)
+ 重试 + 稍后再说;本地资源类只重试 + 稍后再说
- "重试" 不强制("稍后再说" 让用户自由退出,避免单按钮卡死)
结果:用户在断网 / 未授权 Wi-Fi / 测试域名不可达等场景下,splash 多停
约 7s(3 次重试 + 退避)后自动降级进入大厅,**不弹任何 alert**。仅当
本地资源也失效(极罕见)时才弹友好提示,永不暴露原始 NSError。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 03:12:33 +08:00 |
|
 joywayerandClaude Opus 4.7
|
a1c5883cc5
|
Phase 9.2:H5ErrorRelay 把 H5 异常中继到原生 print
零修改 H5(原则 A):WKUserScript .atDocumentStart 在业务 JS 跑前 hook
三类异常源:
- `console.error(...)`:保留原函数链式调用 + 转发 args
- `window.onerror`:onmessage / filename / lineno / colno / error.stack
- `unhandledrejection`:reason + reason.stack
走独立的 webkit.messageHandlers.h5error 通道(与 WVJB 不同 channel,不冲突)。
原生侧 MessageProxy weak target 切断 retain cycle。仅 BridgedWebView 安装
(大厅+子游戏),OverlayViewController 是第三方外链,不挂。
开发期价值:H5 出错时 Xcode console 直接能看到 "[H5 onerror] ... at xxx.js:42:8",
减少调试盲点。生产期 print 走 OSLog,对体积/性能影响极小。
新增:
- Source/Bridge/H5ErrorRelay.swift(@MainActor singleton)
接入:
- BridgedWebView.init:H5ErrorRelay.shared.install(into:) 在 WVJB 之后
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 02:04:46 +08:00 |
|
 joywayerandClaude Opus 4.7
|
ded6b5d86b
|
Phase 8.4:PhoneStateMonitor → phonestate 反向 callback
契约 §3.1 [14]phonestate 落地。msext 用已废弃的 CTCallCenter(iOS 10+
deprecated);新外壳改用 CallKit CXCallObserver,对 H5 输出字符串一致:
- "2" = 来电响铃中(hasConnected=false / isOutgoing=false / hasEnded=false)
或已连接(hasConnected=true)
- "0" = 挂断(hasEnded=true)
- 其它(拨出未连接、保持)不发
新增:
- Source/Resource/PhoneStateMonitor.swift(@MainActor singleton,与 BatteryMonitor /
NetworkMonitor 同款 pattern;onChange closure + start/stop 生命周期)
接入:
- SubGameViewController.setupExternalSubscriptions:start + onChange
- SubGameViewController.teardownExternalSubscriptions:onChange=nil + stop
- 大厅 WebContainerViewController 不订阅(msext gameController 同款边界,
契约表注「已注释」表示大厅不发;H5 子游戏才依赖此事件做哑麦/暂停)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-23 02:01:29 +08:00 |
|