Commit Graph
100 Commits
Author SHA1 Message Date
joywayer 113b4d4960 微信分享去sdk 2026-06-30 17:41:10 +08:00
joywayer c1e51d2183 微信分享去sdk 2026-06-30 17:40:54 +08:00
joywayerandClaude Opus 4.7 04e702a99b Phase 1.F: WebView 加载从 file:// 迁到 ylgame://h5 自定义 scheme
为未来 Cocos 子游戏 H5 build 适配铺路(file:// 下 Cocos XHR/fetch 受
null-origin 限制几乎必踩坑),同时保留大厅 + 子游戏跨页 localStorage
共享语义(用单虚拟 host `h5` 让所有 H5 same-origin)。

变更:
- 新增 AppSchemeHandler(WKURLSchemeHandler 单例 + Range/MIME/异步 IO/
  取消语义;闭包只携 Sendable ObjectIdentifier,不捕获 task)
- SandboxPaths 加 lobbyIndexAppURL / subGameIndexAppURL builders
- BridgedWebView 注册 scheme handler(WKWebView init 前)
- WebContainerViewController / SubGameViewController 的 loadFileURL
  → webView.load(URLRequest),OverlayViewController 不变

文档:
- Plan 新增 Phase 1.F (1.18-1.21) + ADR-010 决策记录 + 进度勾选
- Design 新增 §7.6 包含 URL 结构 / 实现要点 / 等价性表 / Cocos 预检脚本
- Contract §0.2 / §4.1 / §10 验收清单同步切换说明(H5 可观察差异:
  location.protocol "file:" → "ylgame:",项目方已 grep 确认现网 H5
  不依赖此字面)

存量影响:file:// → ylgame:// origin 切换时老用户 localStorage 一次性
清零,已与项目方确认业务可接受、不做迁移补偿。

BuildProject 通过。Plan 进度已勾选 1.18/1.19/1.20。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 08:58:00 +08:00
joywayerandClaude Opus 4.7 960adc5491 IPA 升级 itms-services 路径补主动 suspend:用户回桌面 + 看到下载进度
【背景修正】
上个 commit (befe205) commit msg 里说"iOS 调 itms-services 时会自动把 App 切后台"
不准确 —— 实测系统弹窗只是模态覆盖在本 App 上,用户点「安装」后 App 回到 active,
桌面图标下载进度需要用户手动按 Home 才能看到。

【方案】
调 UIApplication.open(itms-services URL) 后,0.3s 用私有 selector "suspend"
把 App 主动切后台(等价按 Home / 上滑):
- 用户立即看到桌面 + 系统安装确认窗(弹窗是系统级,覆盖在桌面上)
- 用户点「安装」→ 弹窗 dismiss → 用户已经在桌面 → 看到 App 图标下载进度
- App 进程留在内存,IPA 替换时由 iOS 自动结束

【私有 API 风险评估】
suspend selector 是 iOS 私有 API,AppStore 审核会拒。本项目企业签 / 渠道分发,
不上架 AppStore,可以用 (CLAUDE.md 第 6 节稳定>一切原则下接受)。
UIControl().sendAction 比 perform() 更安全:selector 不响应时静默忽略,不 crash。

【仅 itms-services 路径触发 suspend】
Safari 路径(普通 https URL)不调 suspend:用户已经在 Safari,主动切后台反而
让 Safari 也消失,糟糕的 UX。

BuildProject 通过。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 22:12:59 +08:00
joywayerandClaude Opus 4.7 befe205bf9 IPA 升级 URL 自动包成 itms-services:跳过 Safari、直接弹系统安装窗
【UX 目标】
点「确定」→ 直接弹系统安装确认窗(不跳 Safari)→ 用户点「安装」→
App 自然切后台、用户看到桌面图标下载进度,App 进程留在内存不退出。

【方案】
makeInstallURL 规范化 appDownload:
- itms-services://...        → 原样
- https?://...*.plist (mfst) → 包成 itms-services://?action=download-manifest&url=...
- 其它(网页 / 短链)         → 原样跳 Safari(后台需把 URL 改成上面两种之一才能跳过 Safari)

不再需要 exit(0);iOS 处理 itms-services 时本来就会把 App 切后台,自动满足用户
"回到桌面但 App 不退出"的需求。

文案改为"开始升级,系统会自动下载并安装"匹配新流程。

BuildProject 通过。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 21:46:43 +08:00
joywayerandClaude Opus 4.7 692c849e0d fallback 链头嵌入根(RemoteConfig 顶层):所有 7 字段真正统一,删字段级特殊兜底
【动机】
上一版"统一接口"虽然合并了 resolve / resolveAudio,但 resolve 内部仍有不对称:
- showmessage / audioDomain / audioBucket: pickString(chain) ?? config.xxx
- appVersion / appDownload / gameVersion / gameZip: 只 pickInt/String(chain),
  顶层声明这 4 个字段也读不到

【方案】
- RemoteConfig 加 4 个 String? 字段(appVersion/appDownload/gameVersion/gameZip)
  配齐与节点 struct 同款 7 字段集合
- extension RemoteConfig: RemoteConfigNode(自然 conform)
- buildChain 首行 var chain = [config]:根始终作为最浅一层
- resolve() 去掉 ?? config.xxx 三处特殊兜底,全 7 字段一律 pickString/Int(chain)

【效果】
- 节点链 1..5 长(含根);所有字段一视同仁,pickString 倒序找即天然包含顶层
- 任何字段都可以在顶层 / agent / game / channel / market 任一层声明,最深层赢
- 算法对称、代码无字段级特殊分支

【硬约束(Plan ADR-009 同步)】
新增字段:加进协议 + 5 个 struct(含 RemoteConfig)+ ResolvedVersion +
resolve() 内一行 pickString/Int(chain, \.xxx);不允许 ?? config.xxx 类回退。

BuildProject 通过。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 21:38:03 +08:00
joywayerandClaude Opus 4.7 485f0c6679 合并 resolveAudio 进 resolve():所有 RemoteConfig 字段走单一入口 + 同款 4 层 fallback
【动机】
上个 commit 把 audio_domain/audio_bucket 改成 4 层 fallback,但用了独立的
resolveAudio() 入口 —— 与 resolve() 并存意味着调用方要记两个 API,违反
"统一接口"原则。

【方案】
- ResolvedVersion 加 audioDomain / audioBucket 两个字段(扁平结构)
- resolve() 内直接 pickString(chain, \.audioDomain) ?? config.audioDomain 一次解出
- 删除 resolveAudio() 公开入口
- WebContainerViewController parsed 分支:单次 resolve() 调用 + 同一 r 取所有字段

【硬约束(Plan ADR-009 同步)】
远端 RemoteConfig 所有字段必须走 VersionResolver.resolve(...) 单一入口、
同一套 4 层 fallback;新增字段时加进协议 + 节点 struct + ResolvedVersion +
一行 pickString,不允许为某字段单独写 if 链或 helper。

BuildProject 通过。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 21:32:57 +08:00
joywayerandClaude Opus 4.7 a54d1511be audio_domain/audio_bucket 改 4 层 fallback + 顶层兜底,修"音频服务不可用"误报
【bug】
ADR-009 初版只从 RemoteConfig 顶层读 audio_domain / audio_bucket,但渠道方
配置时习惯把这俩字段放在 agent 节点(与 app_version / game_zip 等版本字段同
款层级),导致客户端读到空值后误报 BootError.audioConfigMissing 启动期致命。

【方案】
对齐版本字段的 4 层 fallback 算法:
- Agent / Game / Channel / Market 4 个节点 struct 各加 audioDomain / audioBucket
- RemoteConfigNode 协议加这俩 getter
- VersionResolver 新增 resolveAudio(...) → (domain:String?, bucket:String?)
  逻辑:agent → game → channel → market 倒序找第一个非空,整链空时 fallback 顶层
- WebContainerViewController parsed 分支改用 resolveAudio + print 诊断日志

【兼容性】
顶层 audio_domain / audio_bucket 仍然支持(作为整链兜底),后台不需要改配置
位置;放节点上也能读到——任意一种 layout 都工作。

【文档】
Plan ADR-009 决策表 + 注入时序段 同步更新 + 加 2026-06-27 修订说明记录此次踩坑。

BuildProject 通过,Xcode 诊断 clean。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 21:30:10 +08:00
joywayerandClaude Opus 4.7 6742308e14 IPA 升级 alert 文案补"退出 App 自动完成"步骤说明
行为不变(点确定 → 跳 Safari,App 留在后台),但 alert 文案明确告知用户
需要在安装确认弹窗点「安装」后从多任务列表上滑退出本 App,更新才会自动完成。
按钮恢复"确定"沿用 msext 风格。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 21:22:32 +08:00
joywayerandClaude Opus 4.7 74d478f5ac 回退 IPA 升级自动 exit(0):itms-services 安装弹窗需 source app 在内存中
【bug】
上个 commit (b548039) 在跳 Safari 后等 didEnterBackground 自动 exit(0),
实测后果是 iOS 系统级"安装"确认弹窗根本不弹了 —— 整个升级链路被悄悄中断。

【根因】
iOS 的 itms-services:// URL scheme 启动系统安装确认弹窗时,依赖 source app
仍在内存里(安全机制:只允许有效 source app 触发系统级安装流程)。我们提前
exit 后,Safari 收到 URL 但系统拒绝弹确认窗。

【方案】
不再自动 exit。让 App 自然留在后台,iOS 在 IPA 替换时会自动结束旧进程
(或要求用户上滑),无需主动介入。

【UX】
alert 文案重新写成"前往下载"+"安装确认弹窗出现后请点击「安装」,等待下载
完成即可自动替换",对齐 iOS 实际安装流程。

代码注释里写清楚为什么不能 exit,避免未来再踩同一坑。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 21:19:37 +08:00
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
joywayerandClaude Opus 4.7 800497fd18 新增 H5-Debug-Guide:Safari Web Inspector 调试 H5 控制台的操作手册
整理大厅 / 子游戏 / 弹层三处 WKWebView 的 isInspectable 启用条件、
Mac Safari 与真机端开关、多 WebView 排查思路(Develop 菜单不刷新等
常见坑),并在 CLAUDE.md 文档索引登记。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-24 21:37:09 +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 41538840c6 放宽 commit 规范:可直接提交、可合并、不必每次确认
旧规范要求每次 commit 前给用户审建议 message、严格按职责拆分,操作链冗余。
新规范:
- 改动通过最小验证后直接 commit,不必每次先征求同意
- commit 颗粒以"未来回看时能不能看懂、能不能二分回滚"为准;密切相关的
  改动可合并,无关的拆开;不强制按职责拆
- 桥接契约改动 / 不可逆结构性变更仍要求独立 commit

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 21:36:49 +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 f6142ced41 完成 WeChat SDK 1.8.2 静态库链接配置
补齐 commit 05f1396 降级时遗漏的链接条目:libWeChatSDK.a / libz.tbd /
libsqlite3.tbd 加入 Link Binary、Security/CFNetwork framework 补齐、
HEADER/LIBRARY_SEARCH_PATHS + SWIFT_OBJC_BRIDGING_HEADER 配上。

关键:OTHER_LDFLAGS = "-ObjC",否则静态库内 NSMutableArray(MTAQueue)
category 不被加载,MTA 上报 runtime 调 enqueue: 立即崩
(与 daoqi/msext 的 Pods-msext.*.xcconfig 一致)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 17:22:04 +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 323fb9074b 修复 AMap 真机链接错:补 3 个系统 framework + 永久文档化
真机首次跑 ylgamehall 时 linker 报 7 个 Undefined symbol:
- _CNCopyCurrentNetworkInfo / _CNCopySupportedInterfaces / _SCNetworkReachability*
  → SystemConfiguration.framework
- _OBJC_CLASS_$_CTTelephonyNetworkInfo → CoreTelephony.framework
- _OBJC_CLASS_$_EAAccessoryManager → ExternalAccessory.framework

根因:AMap 是 fat 静态库(`Vendor/AMap/*.framework`),它内部引用的系统 API
不会被自动带入 transitive 依赖,必须项目方手动 link。CocoaPods 时代由
podspec 自动声明(daoqi msext 走 Pods 故未在 pbxproj 显式 link
ExternalAccessory);本项目走 Vendor 手动接入路线,必须显式补齐。

工程:pbxproj 加 3 个 PBXBuildFile + PBXFileReference(Xcode UI 操作自动写入)

文档(永久记录避免重蹈):
- Vendor/AMap/README.md「工程接入步骤」第 5 步补齐 3 个系统 framework
  + 列出每个 framework 对应的 symbol
- Vendor/AMap/README.md 末尾加「如果未来真机链接报 C++ symbol 缺失」
  备用方案(加 libc++.tbd,daoqi 已加;当前 AMap 2.12.0 未触发)
- docs/SDK-Integration-Guide.md §A.2 第 5 步同步

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 02:50:29 +08:00
joywayerandClaude Opus 4.7 e6bcb32820 Verification-Checklist:覆盖 Phase 3.6 / 4.B-F / 5 / 6 / 7 / 8.4 / 9.2
之前 Verification-Checklist 只覆盖到 Phase 1 / 2 / 3.A / 4 stub;后续
Phase 全部留 "待实现" 占位。一次性补齐,并:

- §I.5 七牛 token 自签 + 上传单元验证(Phase 3.6 落地后的真机/单测项)
- §J 微信授权 → sharelogin 7 字段(Province 大写 P)
- §K 微信链接分享 type=1 → sharesuccess 反向
- §L SharePanel 三选一(动画 / 半透明背景 / 平台未装行为)
- §M QQ URL Scheme 分享(中文 encode / 朋友圈 to_qzone / 乐观策略)
- §N 抖音相册 + URL Scheme 4 候选
- §O 微信截图 type=2 / 远端图 type=3(thumb 0.4x / errCode -2 cancelled)
- §P 高德定位 9 字段(latitude/longitude string、province 小写 p、errorCode 12)
- §Q 子游戏 push + 节流 + 栈深 + backgameData + ExternalSubscriptions 防双发 + SubGameDownloader 缓存/升级
- §R 弹层 OpenurlTitleData + finishweb/browser/backgameData 三 settings + Cookie 隔离 + overlay→sub-game 链路
- §S 视频房间 stub + 大厅不注册边界
- §T phonestate(CXCallObserver 真机;仅子游戏订阅;pop 后停止)
- §U H5ErrorRelay 三类异常源 + Overlay 不挂 + 原始 console.error 链未断

新增「通用准备 → 真机 vs 模拟器跑哪些」清单,明示 M Mac 模拟器
不能跑的章节(§J/K/L/M/N/O/P/T/I.5.3)+ 建议路径(先模拟器跑大头,
再真机跑特定章节)。

末尾「契约 §10 映射表」从 11 行扩到 23 行,每行带 Phase 状态。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 02:37:04 +08:00
joywayerandClaude Opus 4.7 f7d472d91c Phase 9.3-9.6 决策落定:极光/闲聊/Agora/Bugly 全部不集成
四个 SDK 的「集成与否」决策对齐当前项目状态:
- 9.3 极光 JAnalytics:YAGNI,H5 业务不调埋点 handler,建空协议反而
  misleading;未来真要启用按 Vendor → 抽 Tracker 协议 → Noop fallback
- 9.4 闲聊:SDK 已停更 + 业务方未启用;SharePanel 三按钮已只含
  微信/QQ/抖音,连 stub 都不需要
- 9.5 Agora:用户明确视频房暂不需要;VideoRoomHandlers stub 已满足
  契约边界(H5 调 3 个 handler 不报 not found),未来重启按 8.6 蓝图
- 9.6 Bugly:沿用 msext CLAUDE.md「账号已废弃」决策,崩溃监控完全
  依赖 Sentry(如 9.1 启用)+ 用户/运营反馈链

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 02:05:37 +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 5885f97715 Plan §8 进度补漏:Phase 8.1-8.3 视频房间 stub 全勾
8.1/8.2/8.3 视频房间 3 件套 stub 实际在 Phase 6 commit C(VideoRoomHandlers.swift)
就已落地,§5 已勾,§8 当时遗漏,一并补上并明确「不抽 protocol/Noop 类
避免过度设计」的选择。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 02:02:05 +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
joywayerandClaude Opus 4.7 8fc37b0827 Plan §8 进度补漏:Phase 2 / 4.11-4.12 / 7.6-7.7 全勾
Phase 2 大厅简单 handler 实际在 Phase 1 / 4.B 顺带都已落地(13 个文件全
存在于 Source/Bridge/Handlers/ + Source/Resource/),只是 §8 进度表
长期未同步。一次性补勾,并附实际实现文件路径方便检索。

Phase 4.11 / 4.12(截图 / 远端图分享)由 commit 8e4c36d 落地,§5 已勾,
§8 这里也勾上。

Phase 7.6 finishweb / 7.7 browser overlay 实际在 OverlayViewController
首版(commit 0427bb9)就已实现 handleMessage 三分支,与 7.5 同时落地,
但 §8 当时只勾了 7.1-7.5,遗漏 7.6/7.7。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 01:59:01 +08:00
joywayerandClaude Opus 4.7 8e4c36d1c8 Phase 4.F:微信截图分享 type=2 / 远端图分享 type=其它
按 msext gameController.m:637-678 字面行为,WechatShare 按 content.type
分发:
- type=1 → shareLink(已有)
- type=2 → captureScreenshot → JPEG 0.6 + thumb 0.4x → shareImage
- type=其它 → downloadRemote(URL) → JPEG 0.9 + thumb 0.4x → shareImage

新增:
- ImageProvider.scaledThumb(_:scale:):等价 msext FuncPublic
  imageByScalingAndCroppingForSize 0.4x 缩放;微信 SDK setThumbImage:
  内部再压到 32KB
- WeChatManager.ShareImage 结构 + shareImage(_:scene:) 异步包装
  (WXImageObject + WXMediaMessage.setThumbImage,FIFO 串行复用 sharePending)

契约:与 type=1 一致,调成功 → sharesuccess 反向 callback(success/cancelled
都发);errCode=-2 → cancelled;其它 errCode → failed。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 01:33:17 +08:00
joywayerandClaude Opus 4.7 d9d9554d9f Plan 进度同步:Phase 3.6 / 4.1 / 4.3-4.10 / 5.1-5.6 勾选
按 CLAUDE.md「进度同步规范」要求,把已落地的子项在 Development-Plan.md
§5 / §8 中同步勾选。同时修正几处描述与当前实现的差异:

- 4.1 微信 SDK:xcframework 2.0.5 入库 Vendor(不是 Embed Frameworks),
  Universal Links 不接(企业签分发不需要)
- 4.3 WeChatSDK:新 SDK 2.x 无 MMAPP_SUPPORT_* flag,简单 registerApp
- 4.6 WeChatAuth:客户端直拼 sns/oauth2/access_token + sns/userinfo,
  接受 msext 同款 secret 路径(不走后台 /wechat/login)
- 5.1 AMap:高德官方不出 xcframework,回退 fat .framework,M Mac 模拟器
  链接失败是接受的现状(详见 Vendor/AMap/README.md「已知限制」)
- 5.3 AMapWrapper:updatePrivacyShow/Agree 是 AMapLocationManager 类方法
- 5.4 LocationService:仅 requestOnce + stop,未实现持续模式(msext 也
  只是单次,H5 未要求持续)
- 3.6 Qiniu:CryptoKit HMAC-SHA1 自签 token + QNUploadManager 走
  initWithConfiguration + defaultConfigurationV2

Phase 4.11 / 4.12(截图 / 远端图分享)下个 commit 实现。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 01:33:05 +08:00
joywayerandClaude Opus 4.7 f137618070 入库微信 OpenSDK xcframework(2.0.5 NoPay,12 MB)
WechatOpenSDK-NoPay.xcframework 跟随仓库分发,与 Vendor/AMap/*.framework
同样跟踪到 git,新机 clone 后直接可链接,无需额外下载步骤。

- xcframework 含 ios-arm64 + ios-arm64_x86_64-simulator 两 slice
- 模块名 WechatOpenSDK(外层目录名 NoPay 是 zip 包形态,内部 framework 仍叫
  WechatOpenSDK,canImport(WechatOpenSDK) 已生效)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 01:07:22 +08:00
joywayerandClaude Opus 4.7 01780704cd docs:明确 AMap fat framework 不含 simulator arm64 是项目接受的现状
高德官方至今不提供 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>
2026-06-23 01:07:02 +08:00
joywayerandClaude Opus 4.7 a3679aba7d 修复 SDK 真实接入后的 Swift 编译错误 + Xcode 链接配置
实测三个 SDK(微信 / 高德 / 七牛)链接进 Target 后,暴露出 9 个 Swift 编译错误,逐一修正:

代码侧(5 个 Swift 文件):
- AMapWrapper:updatePrivacyShow / updatePrivacyAgree 是 AMapLocationManager
  类方法(since 2.8.0),不在 AMapServices 上;改为 AMapLocationManager.*
  并补 import AMapLocationKit
- BackGameDataHandler:删 `WXApi.delegate = nil`。新版 SDK 2.x 无 static
  delegate API,delegate 由调用方逐次传给 handleOpenURL/sendAuthReq 并被
  SDK 弱引用;WeChatManager singleton 长生命周期不析构,无需 cleanup
- QiniuConfig / QiniuTokenSigner:所有 static 成员标 nonisolated,让 actor
  QiniuUploader 可直接调,无需 await MainActor.run
- QiniuUploader:QNUploadManager() 在七牛 SDK 8.x 已 kQNDeprecated,改用
  initWithConfiguration: + defaultConfigurationV2

工程侧(pbxproj,Xcode UI 自动写入):
- 新 Frameworks group 容纳 Vendor/WechatSDK / Vendor/AMap 引用
- WechatOpenSDK-NoPay.xcframework:Embed & Sign(动态库)
- AMapFoundationKit / AMapLocationKit:Do Not Embed(静态 fat .framework)

BuildProject 验证:Swift 全部编译通过;剩余 AMap fat framework arch 冲突
(M Mac iOS-simulator)是工程配置选择,下个 commit 处理。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 01:03:29 +08:00
joywayerandClaude Opus 4.7 52ad421d22 SDK 选型修正:微信回 Vendor 路径(Tencent 无官方 SPM)
之前文档把 https://github.com/Tencent/WechatOpenSDK-XCFramework 当作
SPM 入口,实际该仓库不存在(404)。Tencent 官方至今未维护 SPM;社区
wrapper(yanyin1986 / anotheren)非官方,按 daoqi CLAUDE.md ADR-006
「闭源 SDK 走 Vendor」否决。微信改回与 AMap 同款 Vendor .xcframework
处置:

- Vendor/WechatSDK/README.md 重写为腾讯开放平台下载页 + Embed & Sign 说明
- docs/SDK-Integration-Guide.md §A / §A.1 / §1.3 同步修正
  - §A 总览:SPM × 1(七牛)+ Vendor × 2(微信 / 高德)
  - §A.1:重写为 Vendor Add Files 流程,强调 Embed & Sign 与 AMap
    静态库 Do Not Embed 的区别
  - §1.3:微信 xcframework 状态标记  等用户从官方下载放置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 00:02:45 +08:00
joywayerandClaude Opus 4.7 4e5600a9ee Phase 3.C:Xcode 加入七牛 SPM 依赖 objc-sdk 8.9.2
按 SDK-Integration-Guide.md §A.3 操作,加入 https://github.com/qiniu/objc-sdk
(传递依赖 happy-dns-objc 1.0.4 自动解析)。canImport(QiniuSDK) 现在
返回 true,QiniuUploader.upload(...) 真实路径激活。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 00:02:31 +08:00
joywayerandClaude Opus 4.7 1d7e258130 Phase 4.E + Phase 5 + Phase 3.C 七牛上传:代码侧 SDK 接入(SPM 优先 / Vendor 兜底)
选型(CLAUDE.md ADR-006「SPM 优先 + Vendor xcframework 兜底」):
- 微信 → SPM(Tencent 官方 https://github.com/Tencent/WechatOpenSDK-XCFramework);
  项目方下载的 WechatOpenSDK-NoPay.xcframework 已撤出 Vendor,docs/res 内副本作离线备份
- 高德 → Vendor 手动(官方未提供 SPM;Vendor/AMap/{AMapFoundationKit,AMapLocationKit}.framework
  随仓库分发,fat .framework 同时含 x86_64 + arm64,Do Not Embed)
- 七牛 → SPM(https://github.com/qiniu/objc-sdk v8.9.x)

代码侧(全部 #if canImport 守卫,Xcode 加 SDK 前编译为 no-op):
- Source/SDK/WeChat/WeChatSDK.swift:WXApi.registerApp + handleOpenURL(沿用 msext AppID
  wx586a9b321e56efb7,universalLink 空字符串走非 ULAPI 路径)
- Source/SDK/WeChat/WeChatManager.swift:WXApiDelegate + state UUID 配对的 authorize +
  FIFO 串行 share async/await wrapper
- Source/Login/WeChatAuth.swift:客户端直拼 sns/oauth2/access_token + sns/userinfo →
  7 字段(Province 大写 P / city 经 danbian 去单引号),沿用 msext 同款路径
- Source/SDK/AMap/AMapWrapper.swift:iOS 14+ 隐私合规 3 步 + apiKey 注入
- Source/Location/LocationService.swift:actor + AMapLocationManager 异步包装 + 9 字段
- Source/Network/QiniuConfig.swift:4 项常量沿用 msext(AccessKey/SecretKey/Bucket/Domain)
- Source/Network/QiniuTokenSigner.swift:纯 Swift CryptoKit HMAC-SHA1 + Base64URL 自签
  token(与 msext QiniuManager.m:200-230 等价,不依赖 Qiniu SDK 工作)
- Source/Network/QiniuUploader.swift:actor 包 QNUploadManager async/await

Handler 升级:
- AccreditLoginHandler:拉起授权 → 反向 callback sharelogin 7 字段
- WechatShare:真实链接分享(type=2/3 截图待 Phase 4.F)
- StartLocationHandler:真实定位 → 9 字段(latitude/longitude string, province 小写 p)

生命周期:
- AppDelegate.didFinishLaunchingWithOptions:WeChatSDK.register + AMapWrapper.bootstrap
- SceneDelegate.openURLContexts:WeChatSDK.handleOpenURL 接入回调
- BackGameDataHandler:子游戏 backgameData pop 时 WXApi.delegate=nil + LocationService.stop()

Info.plist:CFBundleURLTypes 加 wx586a9b321e56efb7;LSApplicationQueriesSchemes 追加
weixin/weixinULAPI/weixinURLParamsAPI;NSLocationWhenInUseUsageDescription +
NSMicrophoneUsageDescription(gamehallname 中文文案)。

.gitignore:排除 docs/res/{AMap_iOS_Loc_ALL,objc-sdk-8.9.2}/(235MB 项目方下载副本,
已走 SPM/Vendor 接入不需要副本)。

文档:docs/SDK-Integration-Guide.md 重写 §A 用户手动 Xcode UI 三步(SPM × 2 +
Add Files × 2);Vendor/{AMap,WechatSDK}/README.md 记录各自接入路径选型。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 22:22:23 +08:00
joywayerandClaude Opus 4.7 a2baf742b6 docs:新增 SDK-Integration-Guide.md(微信 / 高德定位 / opencore-amr / 七牛)
按 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>
2026-06-22 21:36:51 +08:00
joywayerandClaude Opus 4.7 0427bb9542 Phase 7 完整实现:OpenurlTitleData 弹层(OverlayViewController + 3 个 settings)
- AppCoordinator 扩 OverlayRequest + lastOverlayAt(3s 节流 / 栈深 < 3 守门) +
  showOverlay / popOverlay(data==nil 仅 pop 即 finishweb 路径;
  data!=nil 同时发 .subGameDidReturn 即 backgameData 路径)
- 新增 Source/WebView/OverlayViewController.swift:独立 WKWebView,
  WKWebViewConfiguration.websiteDataStore = .nonPersistent() 隔离 Cookie / 缓存;
  .atDocumentEnd 注入 polyfill:window.settings = {browser/finishweb/backgameData}
  → webkit.messageHandlers.<name>(与 WVJB 不同 channel 可同名共存);
  ScriptMessageProxy weak target 切断 retain cycle
- OpenurlTitleDataHandler 升级为真实 push:解 url / "title "(末尾空格契约)/
  data / orientation → AppCoordinator.showOverlay,cb 字面始终回 "OpenurlTitleData"
- SubGameViewController.setupExternalSubscriptions 也订阅 .subGameDidReturn
  (overlay 可从子游戏 push,pop 回子游戏时需要 callback 子游戏 H5 的 getWebdata;
  与 viewWillAppear/Disappear 生命周期对齐,栈顶才挂钩避免双发)
- orientation 字段保留契约但 landscape-only app 暂不实施旋转(msext 旋转 transform
  在我们项目里无意义,记注释,如有 H5 反馈再补)
- Plan §5.7.1-7.7 / §8 进度已勾选

至此 SwitchOverGameData → push 子游戏 / OpenurlTitleData → push 弹层 / backgameData ↔
finishweb → pop + getWebdata 反向闭环全部接通。栈深永远 ≤ 3。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 21:30:03 +08:00
joywayerandClaude Opus 4.7 5b67e414b3 BackGameDataHandler:补 SDK 接入 TODO 清单(防止未来漏清理)
记录 Phase 4.E 微信 / Phase 5 定位 / Phase 3.B-D 录音上传 / Phase 8 Agora
真实接入时必须在此处补的对应清理钩子(msext 在 backgameData / cleanUpAction
里逐项清理;我们当前 stub 状态无需要清的状态,但落地任一 SDK 后必须回到此处补 1 行)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 21:23:56 +08:00
joywayerandClaude Opus 4.7 c77037f407 Phase 8 视频房间 3 件套 stub:createRoom / getVideoinfo / exitRoom
业务暂未启用视频功能,按运营确认仅维持桥契约,让 H5 子游戏调用不报 "no handler"。

- 新增 Source/Bridge/Handlers/VideoRoomHandlers.swift:单一 enum.register 3 个 stub,
  各自 callback 字面回包名(与 OpenSaomaHandler 同款 stub 模式)
- SubGameViewController.registerBridgeHandlers 挂上 VideoRoomHandlers
- 不抽 VideoRoom protocol / NoopVideoRoom 类(违反"不为假设的未来需求设计"原则);
  未来重新接入 Agora 时把 3 个 stub 展开实现即可,注册点不变
- Plan §5.6.3 / §5.8.1-8.3 进度已勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 21:19:55 +08:00
joywayerandClaude Opus 4.7 5f3a435180 Phase 6 commit C:backgameData pop + getWebdata 反向 callback
- 新增 Source/Bridge/Handlers/BackGameDataHandler.swift(仅 SubGameViewController 注册):
  - 调 AudioPlayer.stopAllBackground 停背景音
  - AppCoordinator.popSubGame(returningData:) → popViewController + 发 .subGameDidReturn
  - data 兼容 string / object(非字符串走 JSONSerialization 序列化透传)
  - 字面 cb "backgameData"(msext gameController.m:691-709 等价,
    WXApi 清理跳过 — 微信 SDK 待 Phase 4.E)
- AudioPlayer.stopAllBackground:无条件停背景音(msext 不看 type 直接 nil 行为)
- SubGameViewController.registerBridgeHandlers:挂上 BackGameDataHandler,
  注释更新 exitRoom/getVideoinfo/createRoom 推迟到 Phase 8
- WebContainerViewController:在 setupExternalSubscriptions 挂 .subGameDidReturn
  观察者 → bridge.call("getWebdata", .string(data)),teardown 时 removeObserver
  (生命周期与 battery/network/appservice 同一对,子游戏栈顶时大厅已 teardown
  避免双发)
- Plan §5.6.3 / 6.5 / 6.6 / §8 进度已勾选

至此 SwitchOverGameData → push 子游戏 → backgameData → pop + getWebdata
完整链路接通;子游戏视频房间(exitRoom/getVideoinfo/createRoom)留待 Phase 8。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 21:05:23 +08:00
joywayerandClaude Opus 4.7 a9eaf86c63 Phase 6 commit B:SubGameDownloader zip 下载 + 解压
- 新增 Source/Resource/SubGameDownloader.swift(actor):URLSession + ZIPFoundation,
  缓存命中直接复用(看 {Caches}/{gameDir}/{gameStart}/index.html 是否存在),
  未命中下载 → staging 解压 → 原子 rename(与 LobbyZipUpgrader 同思路;
  msext gameController.m:1340-1401 等价,以 staging 替代 msext 直接覆盖避免半残留)
- SubGameViewController.runBootPipelineSteps:接入 ensureReady,splash 实时更新下载进度,
  移除 commit A 占位的 SubGameBootError
- DownloadProgressDelegate 与 LobbyZipUpgrader 同款就地复制(两处用不抽公共,
  三处出现时再合并)
- Plan §5.6.7 / §8 进度已勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 21:01:53 +08:00
joywayerandClaude Opus 4.7 7a5644053d Phase 6 commit A:AppCoordinator + SubGameViewController 框架 + SwitchOverGameData push
- AppCoordinator:2s 节流 + 栈深 < 2 守门 + .subGameDidReturn 通知名(Design §2.4.3)
- SubGameViewController:复用 BridgedWebView/Splash/16:9 letterbox/ExternalSubscriptions,
  AppDataWriter(.subGame) 写 4 个 app_*.js;H5 未安装时抛错(待 commit B 接入下载)
- SwitchOverGameHandler:解 Gamedirectory/gamedownloadurl/data → AppCoordinator.showSubGame
  (msext NewRootVC.m:541-566 等价,节流与栈深由 Coordinator 守门,cb 字面始终回)
- SceneDelegate:UINavigationController 承载大厅,nav 引用交给 AppCoordinator
- Plan 进度已勾选 §5 / §8(commit B/C 留待 SubGameDownloader 与 backgameData/getWebdata 链路)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 20:59:00 +08:00
joywayerandClaude Opus 4.7 68cf7140d7 Phase 6/7 stub:SwitchOverGameData + OpenurlTitleData
补齐 H5 桥 §3.1 [15][17]两个 stub handler,让 H5 调子游戏切换 / 弹层
入口时不再报 "no handler registered",避免业务流程卡住。完整流程留
Phase 6 / Phase 7 实施。

新增:
  - Source/Bridge/Handlers/SwitchOverGameHandler.swift §3.1 [17]stub
    cb "SwitchOverGameData"(驼峰首字母大写沿用 msext 字面)
    完整实现需 AppCoordinator + SubGameViewController + SubGameDownloader
    (Phase 6.4 落地)
  - Source/Bridge/Handlers/OpenurlTitleDataHandler.swift §3.1 [15]stub
    cb "OpenurlTitleData"
    完整实现需 OverlayViewController + window.settings polyfill + 3s 节流
    (Phase 7 落地);注意契约硬约束 "title " 末尾空格已在注释提示

WebContainer.registerBridgeHandlers 接入。

至此 H5 桥 §3.1 大厅可调的全部 16 项 handler(除 backgameData / createRoom
等子游戏专属 §H 4 项)都已注册(含完整实现 / stub 两种状态),
H5 启动 / 业务期任何 handler 调用都不会报 "no handler"。

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 20:14:31 +08:00
joywayerandClaude Opus 4.7 39c1b5164d Phase 4.D:DouyinShare URL Scheme + Photos 真实落地(不依赖 SDK)
移植 msext DouyinShareManager.m:201-269 shareImageToDouyin 完整流程到 Swift。

新增基础设施(Phase 4.F 截图 / 远端图也共用):

Source/Share/ImageProvider.swift:
  - captureScreenshot() throws -> UIImage:UIGraphicsImageRenderer 截
    keyWindow(含状态栏 + letterbox 黑边 + WebView),与 msext
    FuncPublic.getImageWithFullScreenshot 等价
  - downloadRemote(_:) async throws -> UIImage:URLSession.data(from:)
    下载远端 URL → UIImage 解码
  - ImageProviderError 错误分类(noKeyWindow / downloadFailed / decodeFailed /
    badURL)

Source/Share/PhotoLibrarySaver.swift:
  - save(_:) async throws:使用 PHAccessLevel.addOnly(iOS 14+ 推荐,仅写
    相册不读,弹窗更友好)+ PHAssetChangeRequest.creationRequestForAsset
  - 自动处理权限三态(authorized/notDetermined 请求 + denied/restricted 抛错)

DouyinShare.share 完整实现:
  - 拿图片源:
    * type == "2" 截图 / type == "1" 链接(退化为当前屏截屏,msext 行为)
      → ImageProvider.captureScreenshot
    * type == 其它(远端图 URL)→ ImageProvider.downloadRemote(webpageUrl)
  - PhotoLibrarySaver.save 保存到相册(含 .permissionDenied 错误回 .failed)
  - UIPasteboard.general.image = image 兜底(抖音可能从剪贴板读图)
  - 按 msext 顺序尝试 4 个 URL Scheme:
    snssdk1128://camera / publish / share/image / 基础 scheme
    canOpenURL 命中即 await UIApplication.open + 调起即 success
  - 全部失败返回 .failed("所有抖音 URL Scheme 都调起失败")

Info.plist 加 NSPhotoLibraryAddUsageDescription("需要将分享内容保存到
相册,以便您在抖音中选取分享"),addOnly 权限弹窗用此文案。

BuildProject 通过

Plan §5 Phase 4.4 勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 20:07:45 +08:00
joywayerandClaude Opus 4.7 aff5ee7633 Phase 4.C:QQShare URL Scheme 真实落地(不依赖 SDK)
移植 msext QQShareManager.m:716-771 simpleShareToQQFriend 简化版到 Swift。

QQShare.share 完整实现:
  - canOpenURL("mqqapi://") 检测 QQ 安装;未装返回 .notInstalled
  - 仅链接分享走完整 URL Scheme(type == "1");type == "2"/其它(截图 /
    远端图)暂返回 .success 避免 H5 业务卡住,Phase 4.F 接入
  - URL 构造(与 msext 等价):
    * scene == .timeline → mqqapi://share/to_qzone? (QQ 空间)
    * scene == .friend   → mqqapi://share/to_fri?   (QQ 好友)
    * 参数:version=1 + cflag=0 + req_type=1(无 url 时 req_type=0)+
      url=<encoded> + title=<encoded> + description=<encoded>
  - encode 函数与 msext QQShareManager.m:1262-1268 等价:仅保留 RFC 3986
    unreserved 字符(alphanumeric + "-._~"),其它全部 percent encode
  - canOpenURL 二次检查 + await UIApplication.open
  - 调起即视为 success(msext 同款乐观策略,URL Scheme 单向无返回)

Info.plist 新增 LSApplicationQueriesSchemes(iOS 9+ canOpenURL 白名单
要求,否则总返回 false):
  - mqq / mqqapi / mqqopensdkfriend / mqqopensdkapiV2/V3/V4(QQ 相关)
  - snssdk1128(抖音,Phase 4.D 用)
  - 微信相关待 Phase 4.E 接入 SDK 时再加(weixin / weixinULAPI 等)

BuildProject 通过

Plan §5 Phase 4.3 勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 20:04:36 +08:00
joywayerandClaude Opus 4.7 2ad4563d16 Phase 4.B:SharePlatform 框架 + SharePanel 三选一 UI + ShareCenter 分发
按 msext gameController.m:575-597 / SharePanel.m 模式落地分享框架。

新增 Source/Share/:
  - SharePlatform.swift:
    * SharePlatform 协议(name / isInstalled / share async → ShareResult)
    * ShareContent struct(nonisolated Sendable,6 字段 sharefriend/sharetype/
      type/webpageUrl/title/desc)
    * ShareResult enum (success / cancelled / notInstalled / failed)
    * ShareScene enum (friend / timeline)
  - SharePanel.swift:
    * @MainActor UIView,半透明黑底覆盖全屏 + 底部圆角内容视图滑入
    * 三按钮(微信绿 / QQ 蓝 / 抖音黑)+ label 等间距居中布局
    * 已安装平台按钮高亮,未装置灰
    * 点击空白区 dismiss + completion(.cancelled)
    * 点击按钮 dismiss + 触发对应 SharePlatform.share + completion(result)
    * 0.25s 滑入/滑出动画
    * 与 msext SharePanel.m 等价行为
  - ShareCenter.swift:
    * sharefriend == "1" → SharePanel.show 三选一
    * sharefriend == "2" → WechatShare.share scene: .timeline 朋友圈
    * 与 msext gameController.m:585 行为 1:1
  - WechatShare.swift:stub(Phase 4.E 等微信 SDK 接入)
  - QQShare.swift:stub + canOpenURL("mqqapi://") 检测(Phase 4.C 升级
    URL Scheme 真实调起)
  - DouyinShare.swift:stub + canOpenURL("snssdk1128://") 检测(Phase 4.D 升级
    URL Scheme + Photos)

FriendsShareHandler 升级:
  - 入参解析为 ShareContent
  - 调 ShareCenter.dispatch
  - completion 内根据 result 触发 sharesuccess 反向 callback:
    * success / cancelled → bridge.call("sharesuccess", {success:"2", type:sharefriend})
    * notInstalled / failed → 不发(H5 业务期 timeout 后自行 fallback,msext 乐观策略)

ShareContent 标 nonisolated 解 Swift 6 actor 隔离(项目默认 MainActor isolation
让 struct 跨 actor 受限)。

BuildProject 通过

Plan §5 Phase 4.2 勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 20:02:34 +08:00
joywayerandClaude Opus 4.7 b071ced657 Phase 4 方向再次纠正:撤销 A,QQ + 抖音 SharePanel 三选一不依 SDK
用户提示"原项目还有 QQ 分享、抖音分享"。深度调研发现:

  上一轮 A 方向决策 (a6095ef) 错了 — grep 范围之前仅看 NewRootVC.m,
  漏了 gameController.m。msext gameController.m:575-597 显示 H5 调
  friendsShare 时原生**弹 SharePanel 三选一面板**:

    sharefriend == 1 → SharePanel.showWithDictionary(微信好友 / QQ / 抖音)
    sharefriend == 2 → WechatShareManager.shareWithContent(朋友圈)

  Contract §3.1 [2]旧描述(sharetype=3 闲聊)已被 msext 新代码覆盖;
  sharetype 字段在新代码里读了但不用。

调研 QQ + 抖音是否需要 SDK:
  - QQShareManager.m:14: __has_include(<TencentOpenAPI/QQApiInterface.h>)
    条件编译 + line 716-771 simpleShareToQQFriend URL Scheme fallback
    (mqqapi://share/to_fri?...) — **不依赖 SDK**
  - DouyinShareManager.m: 只 import Photos + MobileCoreServices;
    全程 URL Scheme(snssdk1128://share/video / camera)+ 保存截图到
    相册让用户在抖音里选 — **不依赖 SDK**

撤销 A 决策。新方向(Plan §5 4.2-4.4):
  - 4.2 SharePanel 三选一面板(msext gameController.m:585 等价行为)
  - 4.3 QQShare URL Scheme(移植 simpleShareToQQFriend)
  - 4.4 DouyinShare URL Scheme + Photos
  - 4.5(原 4.B)微信 SDK 接入(仍需项目方提供 framework + AppID + UL)

文档同步:
  - Plan §2.3 阻塞表:QQ 改为"不需要 SDK,走 URL Scheme";抖音同款
  - Plan §5 Phase 4:4.2 重写为 SharePanel 设计 + 4.3 QQ + 4.4 抖音 + 4.5 微信
  - CLAUDE.md 依赖管理段:"不接入 QQ SDK / 也不接入抖音 SDK",
    含 LSApplicationQueriesSchemes + NSPhotoLibraryAddUsageDescription 说明
  - Verification-Checklist L/M/N 章节:SharePanel + QQ + 抖音验证项
  - Plan §6.4 里程碑加纠正记录(标 a6095ef 已撤销)

下一步:代码实施 4.B SharePlatform 协议 + SharePanel UI + ShareCenter 分发。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:59:00 +08:00
joywayerandClaude Opus 4.7 a6095ef4c8 Phase 4 方向最终决策 A:QQ 不在 H5 桥范围,不实施
深度 grep msext NewRootVC.m / gameController.m 未发现 QQShareManager
引用 — QQ 完全不接 H5 桥。Contract §3.1 [2] sharetype 取值仅 "1"/"2"=
微信 / "3"=闲聊,无 QQ 选项。

按 CLAUDE.md 原则 A(H5 端零修改不可妥协):契约不变 → H5 桥不实现
QQ 分享。msext 里 QQShareManager 只服务于原生 SharePanel(独立功能),
新外壳无原生 SharePanel-like 入口需求 → 也不需要 QQShareManager。

文档同步调整为"不实施":
  - Plan §2.3 阻塞表:QQ OpenSDK 改写为详细理由(H5 桥契约无 QQ
    选项 + msext QQShareManager 不接 H5 桥的 grep 证据)
  - Plan §5 Phase 4.2:从"QQ 改走 URL Scheme"改为"不实施",但保留
    URL Scheme 备用知识库(未来如需原生入口可参 msext fallback)
  - CLAUDE.md 依赖管理段:"不接入 QQ SDK 也不实现 QQ 分享",明示
    原因(契约无 QQ + msext 行为)
  - Verification-Checklist L 章节:QQ 验证项删除(不实施无需验证)

Phase 4 至此剩 4.B 微信 SDK 接入(项目方阻塞)+ 4.D 截图/远端图分享
(依赖 4.B)。当前 stub 已让 H5 调登录/分享不报 "no handler",
等微信 SDK 到位后升级。

Plan §6.4 里程碑加一行决策记录。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:45:12 +08:00
joywayerandClaude Opus 4.7 aa80aec18c Phase 4.A:登录+分享 stub + QQ 改走 URL Scheme 不依 SDK
按用户约定调整 Phase 4 方向:微信继续走 SDK(等项目方提供),QQ 分享
改用 URL Scheme 直接调起 QQ app,**不依赖 SDK**。

调研 daoqi/msext/Class/Utils/QQShareManager.m 发现已有完整的 URL Scheme
fallback 路径:
  - line 14: __has_include(<TencentOpenAPI/QQApiInterface.h>) 条件编译
  - line 231 注释: "Fallback: 使用 URL Scheme (mqqapi://) - 无需 SDK,
    直接调起 (Legacy)"
  - mqqapi://share/to_fri / mqqapi://share/to_qzone / mqqopensdkfriend://share
    等 schema 实现见 line 733/786/789

新增 handler stub(让 H5 启动 / 业务期调登录 / 分享不报 "no handler"):
  - Source/Bridge/Handlers/AccreditLoginHandler.swift
    accreditlogin cb "Response from accreditlogin"
    完整实现需 Phase 4.B 微信 SDK + WeChatManager.authorize() + WeChatAuth.exchangeForUser
    + bridge.call("sharelogin", 7 字段含大写 Province) 反向 callback
  - Source/Bridge/Handlers/FriendsShareHandler.swift
    friendsSharetypeUrlToptitleDescript cb "sharefriend"
    完整实现需 ShareCenter 策略分发 + sharesuccess 反向 callback

WebContainer.registerBridgeHandlers 接入。

文档调整:
  - Plan §2.3 阻塞表:QQ OpenSDK 标"不需要",改 URL Scheme 路径
  - Plan §5 Phase 4 前置:去掉 QQ OpenSDK 一行
  - Plan §5 Phase 4.2:完整改写为 URL Scheme 方案,含 LSApplicationQueriesSchemes
    清单 + URL 构造样例 + "调起即 success" 乐观策略说明
  - Plan §8 进度追踪:4.2 QQ SDK Vendor 标"不需要"
  - Plan §6.4 里程碑加 3 行(前两轮 commit hash 补 + 本次)
  - CLAUDE.md 依赖管理段加 "不接入 QQ SDK" 约定(与"不引入 CocoaPods"
    并列的负面清单)
  - Verification-Checklist Phase 4 章节:E.19/E.20 stub + K/L 待 4.B/4.C
    完整后填充验证项

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:19:23 +08:00
joywayerandClaude Opus 4.7 0602b9d4b6 Phase 3.A:srcIsloop 本地音频 + 3.B/3.C/3.D handler stub
3.A 本地音频完整落地(与 msext NewRootVC initJSdata 等价):

  Source/Audio/AudioPlayer.swift(@MainActor 状态机):
    - playOnce(url) 单次按钮音;buttonPlayers 数组保活池避免多次点击
      互相打断(duration + 0.5s 后清理)
    - loopBackground(url, type:) 循环背景音;numberOfLoops = -1 +
      backgroundType 记录用于同名停止
    - stopBackground(type:) 同名校验后才停止(msext 同款语义)

  Source/Bridge/Handlers/LocalAudioHandler.swift:
    - 注册 srcIsloop handler(@MainActor enum)
    - 入参 src + isloop,按 0/1/-1 分支调 AudioPlayer.shared 对应方法
    - audioFileURL 路径:{Caches}/{gamedir}/{gamestart}/assets/wav/{src}
      与 msext NewRootVC 路径约定 1:1(H5 zip 内 assets/wav 子目录)
    - audioFileURL 标 nonisolated 因 SandboxPaths 是 nonisolated 静态
    - cb 字面 "Response from srcIsloop" 严格保持

  Source/Bridge/Handlers/RemoteAudioHandler.swift(3.B/3.C/3.D stub):
    - prepareaudio + mediaTypeAudio 仅 cb 维持契约,无真实业务
    - 待 Phase 3.B(opencore-amr .a 接入)+ 3.C(七牛 SPM + token)+
      3.D(远端播放)完整后升级
    - 按 CLAUDE.md 原则 A:H5 调啥就得有人接,即使是 stub

WebContainerViewController.registerBridgeHandlers 接入:
  - LocalAudioHandler.register(完整实现)
  - RemoteAudioHandler.register(stub)

Plan §5 Phase 3.1 勾选;3.2 真机验证留待 H5 提供音频文件后做。

Verification-Checklist.md 新增 Phase 3 章节:
  - H. 本地音频(E.14-E.16 srcIsloop 三个 isloop 值)
  - I. prepareaudio / mediaTypeAudio stub(E.17 E.18)

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:11:34 +08:00
joywayerandClaude Opus 4.7 b2ee1b38eb 新增 Verification-Checklist 功能验证清单 + Phase 1-2 验证步骤
按用户指示:功能验证写到专门的文档用来说明指导,等功能开发完毕统一
验证,不在 Phase 进行中频繁跑(避免反复重装 app 浪费时间)。

新增 docs/Verification-Checklist.md:
  - 文档定位:开发期累积 / 统一验收的操作手册,与 Contract §10
    契约边界保持独立
  - 通用准备:环境工具(Xcode console / Safari Web Inspector / 模拟器
    Features 菜单)+ 重装 app 清 launch snapshot + xcrun simctl
    get_app_container 沙盒访问
  - Phase 1 章节(已实现):
    A. 启动图 + LaunchScreen 方向(3 项)
    B. 启动流水线时序(含 Xcode console 期望输出 + 黑边 + 总耗时)
    C. app_*.js 落盘 + 命名(含 cat 命令)— 6 项硬约束
    D. window.app_* H5 console 对账(15 项变量 + 撤销变量验证)
  - Phase 2 章节(已实现):
    E. H5→Native handler 响应(13 项验证步骤)
    F. Native→H5 反向 callback(5 项触发场景含模拟器手动操作)
    G. ExternalSubscriptions 生命周期(Phase 6 才能完整验证)
  - Phase 3-10 占位章节:实施后陆续填充
  - 附录:与 Contract §10 26 项验收清单的章节映射表
  - 维护规则:每完成 Phase 子项补勾、不复制到 Contract §10、bug 修复
    时在 commit message 引用本文档 checkbox 编号

CLAUDE.md 文档索引表新增一行指向 Verification-Checklist.md。

Plan §6.4 里程碑加 3 行(前面遗漏的 4986f36 / 09f071e + 本次 commit)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:08:32 +08:00
joywayerandClaude Opus 4.7 09f071e38f Phase 2.C:ExternalSubscriptions 生命周期管理(防双发预备)
按 Design §2.4.2 把 battery / network / appservice 三组外部事件源的订阅
挂钩与 VC 生命周期配对:viewWillAppear → setup,viewWillDisappear →
teardown。配套约束:未来 Phase 6 子游戏 push 上来时大厅自动 teardown,
避免桥事件双发(栈深 ≤ 2,§2.4.3)。

WebContainerViewController 改造:
  - 新增 setupExternalSubscriptions():
    * 幂等 start 3 个 monitor(NetworkMonitor / BatteryMonitor /
      AppLifecycleObserver)
    * 设 BatteryMonitor.onChange → evaluateJavaScript("window.app_getbattery=N")
      + bridge.call("getBattery", "%.2f")
    * 设 NetworkMonitor.onChange → evaluateJavaScript("window.app_getnetwork=N")
      + bridge.call("getnetwork", "1"/"2"/"3")
    * 设 AppLifecycleObserver.onBackground/Foreground → bridge.call("appservice",
      "1"/"2")
  - 新增 teardownExternalSubscriptions():
    * 解绑 onChange / on{Background,Foreground} = nil
    * 释放对 bridge/webView 的 closure 引用避免子游戏 push 后双发
    * monitor 不停(保持后台 currentXxx 状态新鲜),下次 setup 直接 resume
  - viewWillAppear / viewWillDisappear override 调 setup / teardown 配对
  - writeAppDataFiles 精简:只写首次值 + 启动 NetworkMonitor/BatteryMonitor
    (需要 currentXxx 读首次值);AppLifecycleObserver 启动挪到 setup

至此 Phase 2 全部完成(2.A 8 个 handler + 2.B 5 项反向 callback + 2.C
生命周期管理)。

BuildProject 通过

Plan §5 Phase 2.13 勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 19:02:35 +08:00
joywayerandClaude Opus 4.7 4986f36967 业务期 app_getbattery / app_getnetwork 改为 evaluateJavaScript 重新赋值
按用户指示调整变化时机制:

  启动期(保持):loadFileURL 之前写 4 个 app_*.js 文件,H5 同步
    <script src> 引入读到实际值

  业务期(调整):变化时**不重写文件**,改为
    webView.evaluateJavaScript("window.app_getbattery=N;") 直接重新赋值
    全局变量。原因:H5 已加载完后 <script src> 不会再 fetch,重写文件
    对当前页面无影响(仅影响下次 reload),改 evaluateJavaScript 即时
    生效 + 无文件 IO 开销。

  与 §3.2 反向 callback 双轨同时执行:
    - evaluateJavaScript 让 H5 业务期同步读 app_xxx 永远拿到最新值
    - bridge.call 让 H5 注册的 callback handler 收到事件主动响应

WebContainerViewController.writeAppDataFiles 改动:
  - BatteryMonitor.onChange:Task @MainActor await
    webView.evaluateJavaScript("window.app_getbattery=N;") + bridge.call
  - NetworkMonitor.onChange:同款 evaluateJavaScript + bridge.call
  - AppLifecycleObserver:appservice 无对应 app_* 全局变量,只走 bridge.call

文档同步:
  - Design §7.5.2 改为「两阶段」表述:启动期写文件 + 业务期 evaluateJavaScript
  - Design §7.5.4 接入点代码示例同步
  - Contract §4.2 app_battery.js / app_network.js 子节更新时机段
  - Contract §4.2 修订记录加一行
  - Plan §6.4 里程碑加一行 + 前两轮 commit hash 补充

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:50:53 +08:00
joywayerandClaude Opus 4.7 23b3a1fdad Phase 2.B:3 项反向 callback 落地(getBattery / getnetwork / appservice)
完成大厅业务期实时状态推送给 H5 的事件驱动链路。每次变化时同时(1)
重写对应 app_*.js 文件供下次 reload,(2)bridge.call 反向推送给当前
页 H5 — 双轨与原 msext 行为等价。

新增 Source/Resource/BatteryMonitor.swift:
  - @MainActor public final class BatteryMonitor,shared 单例 + 幂等 start()
  - 监听 UIDevice.batteryLevelDidChangeNotification + 启动期读首次值
  - currentLevel 同步快照(max(level, 0) 兜底模拟器 -1)+ onChange 回调
  - notification block 内 MainActor.assumeIsolated 安全跨到 main actor

新增 Source/Resource/AppLifecycleObserver.swift:
  - @MainActor public final class AppLifecycleObserver,shared 单例 + 幂等 start()
  - 监听 didEnterBackgroundNotification + willEnterForegroundNotification
  - onBackground / onForeground 双钩子;assumeIsolated 同上

WebContainerViewController.writeAppDataFiles 接入(替换 inline addObserver):
  - 3 个 monitor 启动统一聚合(NetworkMonitor + BatteryMonitor + AppLifecycle)
  - BatteryMonitor.onChange:writeBattery + bridge.call("getBattery", "%.2f")
  - NetworkMonitor.onChange:writeNetwork + bridge.call("getnetwork", "1"/"2"/"3")
  - AppLifecycle.onBackground/Foreground:bridge.call("appservice", "1"/"2")
  - 首次值改读 BatteryMonitor.currentLevel 而非 UIDevice 原始值

至此 Phase 2.B 全部完成(5 项反向 callback:getphoneinfo / getBattery /
getnetwork / appservice / shakeEnd 全部接好)。

剩 Phase 2.C ExternalSubscriptions 生命周期管理(防双发,栈深 ≥ 2 时
下层不发桥事件)。

BuildProject 通过

Plan §5 Phase 2.B 全部勾选

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:45:03 +08:00
joywayerandClaude Opus 4.7 d38ff0f8c4 Phase 2.A:大厅 8 个简单 handler 落地 + BridgeData 加 nonisolated
闭合 Phase 1.17 联调最后一公里(解决 Xcode 日志 4 个 "no handler registered"
+ 提前落地 4 个无外部依赖的简单 handler)。

新增 7 个 handler 模块(Source/Bridge/Handlers/):
  - ClipboardHandler.swift           §3.1 [13][14]剪贴板 UIPasteboard.general
  - ShakeHandler.swift               §3.1 [7][8][9]摇一摇 + §3.2 [5]shakeEnd 反向
                                       canShake / canVoice 状态 + motionEnded 转发钩子
  - VoicePlayingHandler.swift        §3.1 [6]actor VoiceCenter.shared 开关位
  - DeviceInfoHandler.swift          §3.1 [21]+ §3.2 [1]getphoneInfo 大写 I 入、
                                       getphoneinfo 小写 i 出反向 callback
                                       DeviceInfoSnapshot 6 字段(IDFA 用 idfv 兜底,
                                       CLAUDE.md 不接 ATT/IDFA)
  - BrowserHandler.swift             §3.1 [16]UIApplication.open async(Task @MainActor
                                       异步打开,闭包先 cb 不等 open 完成)
  - OpenSaomaHandler.swift           §3.1 [22]空 stub(契约要求注册)
  - StartLocationHandler.swift       §3.1 [20]Phase 5 完整实现,Phase 2 stub

扩展 VibratorHandler.swift:
  - §3.1 [10]vibrator(原有)+ [11]repeatvibrator(msext RootVC.m:1820 沿用:
    实际单次 vibrate 不真循环)+ [12]canclevibrator

BridgeProtocol.swift BridgeData 加 nonisolated:
  - 项目默认 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 让 BridgeData 跨 actor
    访问被拦;asString / asDouble / asInt / asBool / asArray / asObject /
    subscript 全部标 nonisolated(纯值类型本就该是 isolation-free)
  - 影响所有 @Sendable async handler 闭包内的 data?.asXxx 访问

WebContainerViewController 接入:
  - 加 private let shakeHandler = ShakeHandler() 持有
  - registerBridgeHandlers() 注册 8 个 handler 模块
  - 加 canBecomeFirstResponder = true + viewDidAppear becomeFirstResponder
  - 加 motionEnded(_:with:) 转发到 shakeHandler.handleMotionEnded
    (motion 检测必需 first responder,否则不触发)

BuildProject 通过。重装 app 后 H5 启动应不再报 "no handler registered" 错误。

Plan §5 Phase 2.A 全部勾选(含 startlocation bonus stub + BridgeData
nonisolated 重构)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:38:00 +08:00
joywayerandClaude Opus 4.7 0a313afe8e app_appversion 业务语义纠正:审核切换标志 0/1(非版本号)
调研 daoqi/msext NewRootVC.m:1190-1208 initJSdata 发现 app_appversion
真实业务语义是审核切换标志,**不是 App 的版本号**:

  if (远端 ResolvedVersion.appVersion >= 本地 BundleConfig.appVersion) {
      app_gameconfig = BundleConfig.gameConfig;  // 正常业务接口
      app_appversion = '0';                       // 标志:正常
  } else {  // 远端 < 本地(罕见,审核期 / IPA 已升远端未跟上)
      app_gameconfig = BundleConfig.appleConfig; // 苹果审核期接口
      app_appversion = '1';                       // 标志:审核期
  }

H5 端 if (app_appversion === '1') 切苹果审核期分支;99% 时间下都是 '0'
+ gameconfig。

当前代码两处错误:
   app_appversion 写成 "43"(版本号字符串)
   app_gameconfig 永远是 BundleConfig.gameConfig 硬编码

不符合契约(H5 拿到错误标志会走错分支)。按 CLAUDE.md 原则 A 第一准则
必须 1:1 等价 msext。

修复(Source/WebView/AppDataWriter.swift writeAppData):
  - 加 result 计算:localAppVer = Int(bc.appVersion), remoteAppVer =
    resolvedVersion?.appVersion ?? localAppVer;result = remoteAppVer >=
    localAppVer ? 0 : 1
  - 加 gameConfigStr 切换:result == 0 ? bc.gameConfig : bc.appleConfig
  - app_appversion 写字面 '\(result)' 单引号包裹(与 msext "var
    app_appversion='%d';" 等价)
  - Logger 输出加 result 说明 + local/remote 版本号便于调试

Contract §4.2:新增详细业务语义说明段(含 msext 原 ObjC 代码 + result
0/1 含义解读 + app_gameconfig 动态切换说明),修订记录加一行。

Design §7.5.1 表格:#2 app_gameconfig 数据源改为"动态切换",#6
app_appversion 数据源改为"审核切换标志"。

Plan §6.4 里程碑加一行。

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:28:08 +08:00
joywayerandClaude Opus 4.7 7c856d0d66 AppDataWriter:app_data.js 内 5 个关键字段提到声明顺序最前
按用户要求调整 app_data.js 内 var 声明顺序,把 H5 调试时最关注的 5 个
核心字段提到最前:

  1. app_gameconfig     远端配置接口
  2. app_market         渠道市场标识
  3. app_agent          登录态 agent
  4. app_channel        渠道 ID
  5. app_Launchtype     大厅(0) / 子游戏(1) 标识

之后按原 msext 顺序:app_version / app_gamedir / app_gamestart /
app_appversion / app_getwifisignalLevel / app_gamename / app_invitationcode。

JS 内 var 声明的顺序对最终读取值无影响(var 提升 + 整段 script 同步
执行完才退出),契约不破,与 msext 行为等价。仅便于 H5 console 一眼
看到核心字段。

Logger.debug 输出顺序同步调整保持与文件内容顺序一致,便于联调对账。

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 18:14:10 +08:00
joywayerandClaude Opus 4.7 829cc8f825 代码层落地 AppDataWriter + NetworkMonitor + WebContainer 接入
按 Design §7.5 蓝图落地,让 H5 启动时能从沙盒读到实际渠道/启动/设备
值,闭合 Phase 1.17 联调"app_gameconfig 不正确"现象。

新增 Source/WebView/AppDataWriter.swift:
  - public struct AppDataWriter(@MainActor)
  - ContainerRole enum:.lobby / .subGame(name:, dir:)
  - writeInitial():写 app_data.js(12 项)+ app_gamesname.js(暂只写当前
    gameStart 一项,Phase 6 子游戏完整时扩为扫描已装列表)
  - writeBattery(level:):写 app_battery.js(var app_getbattery=N;)
  - writeNetwork(code:):写 app_network.js(var app_getnetwork=N;)
  - 字面严格对齐 msext:
    * 字符串单引号包裹 'value'
    * 数值无引号(version=1 / Launchtype=0 / getwifisignalLevel=1)
    * app_gamesname 用 "var  app_gamesname=new Array(...)"(var 后两空格)
    * 大小写硬约束:Launchtype L 大、getwifisignalLevel wifi 小 + signal/Level 区分
  - escape 转义反斜杠 / 单引号 / 换行(避免渠道字段含单引号导致 H5 JS 解析错)
  - Logger debug 输出文件路径 + 每个 key=value 一行,便于 §7.5.8 联调对账

新增 Source/Resource/NetworkMonitor.swift:
  - @MainActor public final class NetworkMonitor
  - NWPathMonitor 最简 wrap:currentCode 同步快照(默认 2 WiFi,启动后被
    首次 path 更新)+ onChange MainActor 回调
  - shared 单例 + 幂等 start()

WebContainer.runBootPipelineSteps 接入:
  - switch outcome 前提取 let resolved: ResolvedVersion,避免变量作用域
    限制 case 内
  - 新增 writeAppDataFiles(resolved:):启动 NetworkMonitor + 开 battery
    监控 + 写 4 个文件首次值 + 挂 addObserver(battery) + onChange(network)
  - 调用位置:step 5 LobbyZipUpgrader 之后、step 6 loadFileURL 之前
    (与 msext NewRootVC.initJSdata 等价时序)

BuildProject 通过

Plan §6.4 里程碑加一行。

Phase 1.17 联调验证:启动后 H5 console 应读到实际 app_channel /
app_gameconfig 等值,不再是 H5 zip 包内默认占位。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 12:44:45 +08:00
joywayerandClaude Opus 4.7 13cc24b463 §7.5 路径回退到 writeToFile:撤销 evaluateJavaScript 路径
CLAUDE.md 原则 A 升级为第一准则(H5 端零修改不可妥协)后,重新评估
上一轮 commit 246f215 引入的 evaluateJavaScript 路径:

  - WKUserScript(.atDocumentStart) 注入会被 H5 自带 var app_xxx 声明
    覆盖回退
  - WKUserScript(.atDocumentEnd) 又太晚,H5 业务顶层 <script> 已经
    读过默认值
  - 没有"在 <script src> 之后、业务 <script> 之前"的精确时机
  - 任何"让 H5 改一行 / 改一个文件"的妥协方案违反原则 A 第一准则
  - 详细分析见 CLAUDE.md「典型案例 3」

撤销方案:回到原 msext writeToFile 路径,原生在 loadFileURL 之前写
4 个 app_*.js 文件到沙盒,H5 启动时 <script src> 同步引入即可读到
实际值,时序 100% 等价 msext。

Design §7.5 全面回退(-285 / +126 行):
  - §7.5 头部:标题 / 简介改回写文件路径;中间走过两次弯路的完整决策
    链记录在头部(commit f68b0db246f215 → 本次 commit)
  - §7.5.2 注入时机表 → 4 个文件写入时机表,附"为什么不要照搬
    WKUserScript 注入"警告段
  - §7.5.3 删除 AppDataInjector + .legacy 标注;AppDataWriter 升回主路径
    (保留 logger debug 输出 / msext 字面对齐细节 / 转义 / 大小写)
  - §7.5.4 接入点:writeInitial + writeBattery + writeNetwork 三阶段,
    addObserver 持续监听
  - §7.5.5 大厅 / 子游戏 / 弹层差异表回到 "app_data.js / app_gamesname.js
    / app_battery.js / app_network.js" 维度
  - §7.5.6 与 msext 差异表回到"writeToFile 等价化"对比
  - §7.5.7 与 §3.4.1 polyfill 关系:§7.5 文件预写为主路径;
    WKUserScript + evaluateJavaScript "探索后撤回"
  - §7.5.8 联调路径恢复 cat 文件路径,加 4 种现象诊断表

Contract §4.2 同步:
  - 头部说明改回 "原生 writeToFile 写文件" + 撤销中间 evaluateJavaScript
    路径的复盘段
  - 子节标题回到 "写到 {gamedir}/{gamestart}/app_*.js"
  - 子节"更新时机"段回到 loadFileURL 前 / addObserver / 前台切换
  - 删除"H5 团队 zip 自带 + 默认占位"约定(不再适用,原生重新负责写入)
  - 修订记录新增一行"路径再次调整(最终)"

Contract §10 验收清单同步:
  - "H5 zip 自带 4 个 .js" → "沙盒下 4 个 .js 存在(xcrun simctl + cat 验证)"
  - 删"原生 WKUserScript 覆盖生效"项 → 加"值是实际渠道值而非占位"
  - "阶段 B 动态覆盖" → "battery/network 变化重写文件"

Plan §6.4 里程碑加两行:CLAUDE.md A 升级 + §7.5 最终回退。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 12:40:07 +08:00