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>
This commit is contained in:
joywayer
2026-06-23 02:37:04 +08:00
co-authored by Claude Opus 4.7
parent f7d472d91c
commit e6bcb32820
+208 -48
View File
@@ -35,6 +35,22 @@ xcrun simctl get_app_container booted com.skyapp.ylgamehall data
# 输出形如 /Users/.../Containers/Data/Application/<UUID>/
```
### 真机 vs 模拟器跑哪些
模拟器(M Mac 当前)能跑的:Phase 1 / 2 / 3.A / 6 / 7 / 9.2 / 部分 4QQ/抖音 URL Scheme 调起需要设备装 app 才有意义)。
**必须真机的章节**(M Mac 模拟器跑不起来或行为不可靠):
| 章节 | 原因 |
|---|---|
| §J / §K / §O 微信 | 微信 SDK 在模拟器无法真实拉起微信 app |
| §L / §M / §N SharePanel + QQ + 抖音 | 模拟器没装 QQ / 抖音 appURL Scheme 调起失败 |
| §P 高德定位 | AMap fat .framework 在 M Mac 模拟器链接失败(参 `Vendor/AMap/README.md`+ 模拟器定位精度低 |
| §T phonestate | CXCallObserver 在模拟器无效,必须真机 |
| §I.5.3 七牛上传真机 | 上传 URL 公网可达验证 |
建议路径:先在模拟器跑完 Phase 1 / 2 / 6 / 7 / 9.2 大头(覆盖率最高),再单独真机跑 §J~§T 一遍。
---
## Phase 1 — 启动闭环
@@ -160,73 +176,206 @@ H5 需先用 `bridge.registerHandler('callbackName', fn)` 注册:
> **依赖**:测试需要 H5 zip 内 `assets/wav/` 目录有可用音频文件。
> H5 团队 / 项目方提供后做。
### I. prepareaudio / mediaTypeAudio stub3.B/3.C/3.D 待实现
### I. prepareaudio / mediaTypeAudio stub3.B/3.C/3.D 待 opencore-amr 接入
- [ ] **E.17 prepareaudio** → cb "Response from prepareaudio"stub 仅 cb,无录音 / 无 getaudiourl 反向)
- [ ] **E.18 mediaTypeAudio** → cb "Response from mediaTypeAudio"stub 仅 cb,无下载 / 无播放 / 无 gameui_play_voice 反向)
> 3.B AMR 转码 + 3.C 七牛上传 + 3.D 远端播放完整实现后升级。
> 3.B AMR 转码 + 3.C 录音 + 3.D 远端播放完整实现后升级(参 SDK-Integration-Guide.md §C,需用户手动配置 opencore-amr ObjC 源码)
### I.5 七牛 token 自签 + 上传单元验证(3.6 已落地,依赖 SDK 接入)
- [ ] **I.5.1 token 字符串结构正确**:在 Xcode console 或 Swift Playground 调
```swift
let token = QiniuTokenSigner.uploadToken(key: "test/foo.amr")
print(token)
```
应输出 `AccessKey:encodedSign:encodedPolicy` 三段(用 `:` 分隔,无空格);中间段是 base64url(含 `-_`、无 `+/=`
- [ ] **I.5.2 putPolicy 反序列化合法**:取出第三段 base64url decode → JSON `{"scope": "iosaudio:test/foo.amr", "deadline": <future-unix-ts>}`deadline 应为约 1 小时后
- [ ] **I.5.3 真机上传**:临时写一段 await `QiniuUploader.shared.upload(<本地小文件>, timeSec: 3)` → 拿到 `UploadedFile.fileUrl` → 浏览器粘贴 URL 能直接下载内容
---
## Phase 4 — 分享 + 登录(部分实现:4.A stub 完成;4.B/4.C 待外部资源
## Phase 4 — 分享 + 登录(已落地
### J. accreditlogin / friendsShare stub4.A 完成)
> **真机要求**:微信 / QQ / 抖音 SDK + URL Scheme 调起在模拟器都不可靠,必须真机。
- [ ] **E.19 accreditlogin** → cb "Response from accreditlogin"stub 仅 cb,无微信授权调起、无 sharelogin 反向
- [ ] **E.20 friendsSharetypeUrlToptitleDescript** → cb "sharefriend"stub 仅 cb,无微信/QQ 分享调起、无 sharesuccess 反向)
### J. 微信授权登录(4.B 已落地
> 4.B 微信 SDK + 后台 /wechat/login + 4.C QQ URL Scheme 完整实现后升级。
H5 console 先注册 callback
```js
bridge.registerHandler('sharelogin', function(d){ console.log('sharelogin', d); });
```
然后调:
```js
bridge.callHandler('accreditlogin', null, function(r){ console.log('cb', r); });
```
### K. 微信 SDK + Universal Links4.B 待实现)
- [ ] **E.19 accreditlogin** → 拉起微信授权 → 同意 → 大厅 H5 收到 `sharelogin({ openid, headimgurl, nickname, sex, city, Province, unionid })`
- [ ] **E.19.1 关键契约**payload 内 `Province` **大写 P**(不是 `province` 小写——与 Phase 5 startlocation 的 province 小写形成对照)
- [ ] **E.19.2 容错**:用户取消授权 → 不发 shareloginmsext 同款乐观策略)
- [ ] **E.19.3 未装微信**H5 alert 提示"请安装微信"(具体文案由 H5 决定,原生只保证不崩)
- [ ] **E.19.4 SceneDelegate 回调命中**app 切到微信 → 授权 → 切回 ylgamehall → 立刻收 sharelogin(不是过 N 秒)
> 项目方提供 microSDK + AppID + UL 后填充。预备验证项:
> - sharelogin 7 字段(含大写 Province + 小写 city 经 danbian: 处理)
> - sharesuccess({success:"2", type:"1"/"2"})
> - app 切到后台微信授权 → 切回 app sharelogin 触发
### K. 微信链接分享 type=14.E 已落地)
### L. SharePanel 三选一面板(4.B 待实现)
H5 注册 + 调用:
```js
bridge.registerHandler('sharesuccess', function(d){ console.log('sharesuccess', d); });
bridge.callHandler('friendsSharetypeUrlToptitleDescript', {
sharefriend: "2", // "2" 直接走微信朋友圈,"1" 弹 SharePanel
sharetype: "1",
type: "1", // 链接
webpageUrl: "https://www.apple.com",
title: "测试标题",
description: "测试描述"
}, null);
```
> H5 调 friendsShare(sharefriend=1) → 弹 SharePanel 让用户选微信好友 / QQ / 抖音
> 实施后填充验证项:
> - 三个按钮点击响应正确(dismiss 后再触发对应平台分享)
> - 半透明背景点击关闭面板
- [ ] **E.20 sharefriend=2 type=1** → 直接拉起微信朋友圈分享面板,标题/描述/链接正确
- [ ] **E.20.1 sharesuccess 反向**:分享成功或取消都收 `sharesuccess({success:"2", type:"2"})`type 是原 sharefriend);notInstalled / failed 不发(乐观策略)
### M. QQ URL Scheme 分享(4.C 待实现,**不依赖 SDK**
### L. SharePanel 三选一面板(4.B 已落地
> 移植 msext QQShareManager.m simpleShareToQQFriend 后填充
> - QQ 已安装:`mqqapi://share/to_fri?...` 调起 QQ app 分享链接
> - QQ 未安装:H5 alert 提示"未安装 QQ"
> - 调起即视为 successURL Scheme 单向无返回,乐观策略)
调用
```js
bridge.callHandler('friendsSharetypeUrlToptitleDescript', { sharefriend:"1", ... }, null);
```
### N. 抖音 URL Scheme + Photos 分享(4.D 待实现,**不依赖 SDK**
- [ ] **L.1** 底部弹出半透明面板,3 个按钮:微信 / QQ / 抖音
- [ ] **L.2** 0.25s 滑入/滑出动画
- [ ] **L.3** 半透明背景点击 → 关闭面板(不分享)
- [ ] **L.4** 选微信 → 走 §K 路径;选 QQ → 走 §M;选抖音 → 走 §N
- [ ] **L.5** 平台未安装 → 按钮置灰可点但触发后无副作用(与 msext 一致)
> 移植 msext DouyinShareManager.m 后填充:
> - 抖音已安装:保存截图到相册 + `snssdk1128://share/video` 调起抖音
> - 抖音未安装:H5 alert 提示"未安装抖音"
> - Info.plist 已配 NSPhotoLibraryAddUsageDescription 写入相册权限
### M. QQ URL Scheme 分享 type=14.C 已落地,无 SDK
- [ ] **M.1 QQ 已安装** → `mqqapi://share/to_fri?...` 调起 QQ app 选好友页
- [ ] **M.2 朋友圈**sharefriend=2(朋友圈)→ 走 `mqqapi://share/to_qzone?...`
- [ ] **M.3 调起即 success**URL Scheme 单向无返回,不发 sharesuccessmsext 乐观策略)
- [ ] **M.4 encode 正确**:含中文 / 空格的 title / desc 经 RFC 3986 percent encoding(与 msext line 1262-1268 等价)
### N. 抖音 URL Scheme + Photos 分享(4.D 已落地,无 SDK
- [ ] **N.1 截图分享(type=1/2 退化为截屏)**:抖音已装 → 系统首次弹出相册写入权限弹窗 → 同意 → 截图存进相册 + 调起抖音 → 用户在抖音里选图发布
- [ ] **N.2 远端图(type≠1,2**:下载 URL → 存相册 + 调起抖音
- [ ] **N.3 PhotoLibrary 拒绝权限**H5 alert 提示(具体文案由 H5
- [ ] **N.4 抖音未安装**:尝试 4 个 URL Schemecamera / publish / share/image / 基础)都失败 → 不报错(乐观)
### O. 微信截图 / 远端图分享(4.F 已落地,commit 8e4c36d
H5 调用(截图 type=2):
```js
bridge.callHandler('friendsSharetypeUrlToptitleDescript', {
sharefriend:"2", sharetype:"1", type:"2",
webpageUrl:"", title:"", description:""
}, null);
```
- [ ] **O.1 type=2 截图**:当前 H5 整屏被截 → JPEG 0.6 大图 + 0.4x 缩略图 → 拉起微信朋友圈分享,看到截图缩略图 + 大图
- [ ] **O.2 type=3 远端图**:传 `type:"3", webpageUrl:"http://xxx/qr.png"` → 下载远端图 → 微信分享带远端图(msext 公众号二维码分享场景)
- [ ] **O.3 thumb ≤ 32KB 自动压缩**:分享调起时不报"缩略图过大"错(微信 SDK 内部 setThumbImage 自动压缩,外层保证 0.4x 尺寸够小)
- [ ] **O.4 errCode 处理**:用户在微信侧取消 → 收 cancellederrCode -2);其它 errCode → failed
- [ ] **O.5 关键回归**type=1 链接分享仍正常(O 不该影响 §K)
---
## Phase 5 — 定位(待实现
## Phase 5 — 高德定位(已落地
> Phase 5 实施后填充。预备验证项:
> - startlocation 一次性 + 持续 + getlocationinfo 9 字段(含小写 province
> **真机要求**fat .framework 在 M Mac 模拟器链接失败(参 `Vendor/AMap/README.md` 「已知限制」),所有定位验证只能跑真机。
### P. startlocation 真实路径
H5 注册 + 调用:
```js
bridge.registerHandler('getlocationinfo', function(d){ console.log('loc', d); });
bridge.callHandler('startlocation', null, function(r){ console.log('cb', r); });
```
- [ ] **P.1 首次启动权限弹窗**:iOS 系统弹"是否允许访问位置",文案是中文 `进贤聚友棋牌需要访问您的位置以提供本地化服务`
- [ ] **P.2 同意权限** → H5 收到 `getlocationinfo` 包含 **9 字段**
- `latitude` string 类型,`String(format:"%f")` 产物)
- `longitude` string,同上)
- `altitude`
- `country` / `province` / `city` / `district` / `street` / `streetNumber`**province 小写 p**,与 §J sharelogin 的 Province 大写 P 对照)
- [ ] **P.3 拒绝权限** → H5 收到 `getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})`
- [ ] **P.4 飞行模式 / 无网络** → 高德 SDK 返回错误 → H5 收 errorCode 12(或其它非 0
---
## Phase 6 — 子游戏(待实现
## Phase 6 — 子游戏(已落地
> Phase 6 实施后填充。预备验证项:
> - SwitchOverGameData push 子游戏
> - 子游戏 backgameData 回大厅 + getWebdata
> - 子游戏 push 上来后大厅 ExternalSubscriptions teardown(验证不会双发)
### Q. SwitchOverGameData → 子游戏 push + backgameData → pop
H5 调用:
```js
bridge.callHandler('SwitchOverGameData', { gameid:"<sub_id>", ... }, null);
```
- [ ] **Q.1 push 子游戏**:大厅 → push 出新 VC 加载子游戏 H5;过程中 splash + "进入子游戏..." 进度
- [ ] **Q.2 2s 节流**:连续点 SwitchOverGameData 两次(间隔 < 2s)→ 第二次被忽略(AppCoordinator lastSwitchAt 守门)
- [ ] **Q.3 栈深约束**:连续 push 3 个子游戏 → 第 3 次被拒(viewControllers.count < 2 限制)
- [ ] **Q.4 backgameData 回大厅**:子游戏 H5 调 `bridge.callHandler('backgameData', "transfer-payload", null)` → pop 回大厅 + 大厅 H5 收到 `getWebdata("transfer-payload")`
- [ ] **Q.5 backgameData 对象入参**:传 `{x:1, y:"a"}` 而非字符串 → 大厅 getWebdata 收到 JSON 序列化字符串
- [ ] **Q.6 ExternalSubscriptions teardown 防双发**:子游戏 push 上来时大厅 viewWillDisappear 解绑 Monitor 闭包 → 模拟器切电池/网络 → H5 console 看大厅与子游戏各自只收一次(不是双发)
- [ ] **Q.7 SubGameDownloader 缓存命中**:第一次进子游戏看到下载进度;第二次进同一子游戏跳过下载(gameVersion 一致)
- [ ] **Q.8 SubGameDownloader 升级**:远端 gameVersion 提升 → 第二次进重新下载并原子 rename 替换
---
## Phase 7-10(待实现
## Phase 7 — 弹层 Overlay(已落地
> 各 Phase 实施后陆续填充。
### R. OpenurlTitleData 弹层 + 三个 settings
H5 调用:
```js
bridge.callHandler('OpenurlTitleData', { url:"https://www.apple.com", "title ":"标题(注意末尾空格)" }, null);
```
- [ ] **R.1 弹层 push**:大厅栈深 + 1,全屏加载外链页(黑底 + 标题栏)
- [ ] **R.2 入参 "title " 末尾空格**H5 console 调时 key 必须带空格(契约 §3.1 [27]);少空格则原生收不到 title fall back 默认值
- [ ] **R.3 3s 节流**:连续调 OpenurlTitleData 两次(< 3s)→ 第二次拒(AppCoordinator 守门)
- [ ] **R.4 window.settings.finishweb()**:在弹层 H5 console 调 → pop 回大厅,不带数据
- [ ] **R.5 window.settings.backgameData("payload")**pop 回大厅 + 大厅 H5 收 `getWebdata("payload")`(与 Q.4 同链路)
- [ ] **R.6 window.settings.browser("https://apple.com")**Safari 打开 URL(不退出 ylgamehall
- [ ] **R.7 Cookie 隔离**:弹层登录某第三方网站 → 弹层 pop 后 → 主大厅 H5 不带这个 cookieWKWebsiteDataStore.nonPersistent 隔离)
- [ ] **R.8 子游戏 → 弹层 → backgameData → 子游戏**:从子游戏 push overlay → overlay 调 backgameData → pop 回子游戏 + 子游戏 H5 收到 getWebdata(覆盖 .subGameDidReturn 通知链)
---
## Phase 8 — 视频房间 stub + phonestate(已落地)
### S. 视频房间 3 件套 stub
- [ ] **S.1 createRoom** → cb "createRoom"(无真实 Agora 加入)
- [ ] **S.2 getVideoinfoH5→Native** → cb "getVideoinfo"(无真实排版)
- [ ] **S.3 exitRoom** → cb "exitRoom"(无真实 leaveChannel
- [ ] **S.4 大厅误调 createRoom**:契约规定大厅不注册 → bridge handler not foundH5 console 看到错误信号(不该崩)
### T. phonestatePhoneStateMonitor,仅子游戏订阅)
H5 在子游戏 console
```js
bridge.registerHandler('phonestate', function(d){ console.log('phonestate', d); });
```
- [ ] **T.1 子游戏内来电响铃**:模拟器 Features → Toggle In-Call Status 或真机来电 → H5 收 `phonestate("2")`
- [ ] **T.2 接通通话** → H5 收 `phonestate("2")`hasConnected=true 分支)
- [ ] **T.3 挂断** → H5 收 `phonestate("0")`
- [ ] **T.4 大厅页不订阅**:在大厅期间来电 → 大厅 H5 **不**收 phonestate(仅 SubGameViewController 注册)
- [ ] **T.5 子游戏 pop 后停止订阅**:子游戏返回大厅 → 期间来电 → 大厅 H5 不收(PhoneStateMonitor.stop() 已 detach
---
## Phase 9 — H5ErrorRelay9.2 已落地)
### U. H5 错误中继到原生 print
- [ ] **U.1 H5 console.error**Safari Web Inspector → 大厅 console 执行 `console.error("test", 123, {a:1})` → Xcode console 看到 `[H5 console.error] test 123 [object Object]`
- [ ] **U.2 H5 onerror**H5 console 执行 `undefinedVar.foo` → Xcode console 看到 `[H5 onerror] ReferenceError: ... at xxx.js:N:N` + stack
- [ ] **U.3 H5 unhandledrejection**H5 console 执行 `Promise.reject("boom")` → Xcode console 看到 `[H5 unhandledrejection] boom`
- [ ] **U.4 仅大厅+子游戏拦截**:弹层 OverlayViewController 内执行 console.error → Xcode console **不**看到(Overlay 不挂 H5ErrorRelay
- [ ] **U.5 原始函数链未断**H5 业务的 console.error 仍能在 Safari Web Inspector 里看到(hook 保留 origErr.apply
---
@@ -236,17 +385,28 @@ H5 需先用 `bridge.registerHandler('callbackName', fn)` 注册:
| 本文档章节 | Contract §10 节 | 状态 |
|---|---|---|
| A. 启动图 + LaunchScreen | §A 启动(启动相关) | Phase 1 实现 |
| B. 启动流水线时序 | §A 启动 | Phase 1 实现 |
| C. app_*.js 落盘 + 命名 | §A 启动 | Phase 1 实现 |
| D. window.app_* H5 console | §A 启动 + §B 桥接 | Phase 1 实现 |
| E. H5→Native handler | §B 桥接 | Phase 2 实现 |
| F. 反向 callback | §C 系统事件 + §B 桥接 | Phase 2 实现 |
| G. 生命周期 | Design §2.4.2 内部约束 | Phase 2 实现 |
| 音频 / 录音 / 远端语音 | §B 桥接(音频相关) | Phase 3 待实现 |
| 分享 / 登录 | §B 桥接 + §E 回归 | Phase 4 待实现 |
| 定位 | §B 桥接 | Phase 5 待实现 |
| 子游戏 / 视频房间 | §B + §D 视频房间 | Phase 6 + 8 待实现 |
| A. 启动图 + LaunchScreen | §A 启动(启动相关) | Phase 1 |
| B. 启动流水线时序 | §A 启动 | Phase 1 |
| C. app_*.js 落盘 + 命名 | §A 启动 | Phase 1 |
| D. window.app_* H5 console | §A 启动 + §B 桥接 | Phase 1 |
| E. H5→Native handler | §B 桥接 | Phase 2 |
| F. 反向 callback | §C 系统事件 + §B 桥接 | Phase 2 |
| G. 生命周期(防双发) | Design §2.4.2 | Phase 2 ✅ / Phase 6 联动 |
| H. 本地音频 srcIsloop | §B 桥接 | Phase 3.A ✅ |
| I. 录音 / 远端语音 stub | §B 桥接 | Phase 3.B/C/D ⏳(待 opencore-amr|
| I.5. 七牛 token + 上传 | §B 桥接 | Phase 3.6 ✅(链路完整,需 SDK 接入后真机验) |
| J. 微信授权 sharelogin | §B + §E 回归 | Phase 4.B ✅ |
| K. 微信链接分享 type=1 | §B + §E | Phase 4.E ✅ |
| L. SharePanel 三选一 | §B | Phase 4.B ✅ |
| M. QQ URL Scheme 分享 | §B + §E | Phase 4.C ✅ |
| N. 抖音 URL Scheme + Photos | §B + §E | Phase 4.D ✅ |
| O. 微信截图 / 远端图 type=2/3 | §B + §E | Phase 4.F ✅ |
| P. 高德定位 9 字段 | §B 桥接 | Phase 5 ✅(**仅真机**|
| Q. 子游戏 push / backgameData | §B + §D | Phase 6 ✅ |
| R. 弹层 OpenurlTitleData / settings 3 件 | §B + §D | Phase 7 ✅ |
| S. 视频房间 stub | §B + §D 视频 | Phase 8.1-8.3 ✅(stub 满足契约边界)|
| T. phonestate(来电/挂断)| §C 系统事件 | Phase 8.4 ✅(**仅真机** CXCallObserver|
| U. H5ErrorRelay | Design §11.4,原则 B 内部加强)| Phase 9.2 ✅ |
---