Files
youle_app_ios_v2/docs/Verification-Checklist.md
T
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

24 KiB
Raw Blame History

功能验证清单(开发期累积,统一验收)

定位:各 Phase 实施期间陆续累积的验证项目,本文档不是契约(契约在 H5-Native-Contract.md §10),而是面向开发者的指导步骤——告诉验证者"应该做什么操作、看到什么现象、用什么工具"。

使用方式

  • 每完成一个 Phase 子项 → 把对应验证步骤勾掉
  • 不要在 Phase 进行中频繁手动验证,避免反复重装 app 浪费时间
  • 多个 Phase 累积后统一跑一次完整验证清单
  • 与 Contract §10 契约验收清单并行Contract §10 是黑盒契约边界("必须长这样"),本文档是怎么验证它长这样的操作手册

通用准备

环境

工具 用途 启用方式
Xcode console 看原生 os.log 输出 Xcode 底部输出窗口
Safari Web Inspector 看 H5 端 JS console / DOM / Network macOS Safari → 开发菜单 → Simulator / 真机 → 当前 H5 页面
模拟器 Features 菜单 切换 Battery State / Network Link Conditioner / Toggle In-Call Status Simulator → Features
真机 Safari 调试 真机 H5 网页检查器 iPhone → 设置 → Safari → 高级 → 网页检查器 = ON

重装 app(每次跑完整验证前都做一遍)

按 CLAUDE.md「typical case 1launch snapshot 缓存粘滞」教训:

  • 模拟器/真机:长按 app → 删除 → Xcode 重新 Run
  • 或:xcrun simctl uninstall booted com.skyapp.ylgamehall 然后 Xcode Run
  • 这样可以清掉 iOS sandbox 内的 launch snapshot 缓存,避免改了 Info.plist / LaunchScreen 后还是看到旧版本

沙盒访问

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 — 启动闭环

A. 启动图 + LaunchScreen

  • A.1 启动图方向正确:横屏设备启动瞬间看到完整 logo + "快乐游戏 / 健康生活" 文字,画面正向(不是横躺)
  • A.2 状态栏方向:电池/时间在画面顶部居中横向,不在画面左侧竖向(竖向 = iOS 错误地按 portrait 渲染 LaunchScreen,参 CLAUDE.md「典型案例 1」)
  • A.3 黑边合理:比 16:9 更宽的 iPhone(如 16 Pro)启动图两侧补黑边可接受;iPad 4:3 上下补黑边可接受

B. 启动流水线时序

预期 Xcode console 顺序:

[BundleConfig] 渠道注入读取结果(10 项)
[SandboxPaths] 路径计算结果
[ResourceUnzipper] ensureReady 完成
[RemoteConfigClient] 拉取成功 .parsed
[VersionResolver] 单链 4 层 fallback 结果
[LocalVersionReader] 本地 vs 远端对比
[LobbyZipUpgrader] 无需升级 / 升级完成 N → M
[lobby] write app_data.js → ...    ← Phase 1.7 落地(AppDataWriter
[lobby] write app_gamesname.js → ...
[lobby] write app_battery.js: app_getbattery=0.XX → ...
[lobby] write app_network.js: app_getnetwork=2 → ...
  • B.1 启动一气呵成splash 显示 → "拉取配置中" → ("下载更新中 XX%") → "加载大厅..." → splash 0.3s 淡出,看到 H5 大厅
  • B.2 16:9 letterbox 黑边:H5 大厅在屏幕中间,左右(或上下)有黑边,画面比例 16:9
  • B.3 启动总耗时 < 2s(无升级路径),有升级时进度条实时更新

C. app_*.js 文件落盘 + 全局变量命名

# 进入沙盒看 4 个文件
xcrun simctl get_app_container booted com.skyapp.ylgamehall data
# 然后 cat 4 个文件
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_data.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_battery.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_network.js
cat <容器>/Library/Caches/<gamedir>/<gamestart>/app_gamesname.js
  • C.1 4 个文件全部存在且内容非空
  • C.2 app_data.js 内 12 个 var 命名 100% 正确(特别注意大小写:app_Launchtype L 大写、app_getwifisignalLevel wifi 小写 + signal/Level 区分)
  • C.3 app_battery.js 变量名带 getvar app_getbattery=N;(不是 app_battery
  • C.4 app_network.js 变量名带 getvar app_getnetwork=N;
  • C.5 app_gamesname.js 沿用 msext 字面var app_gamesname=new Array('xxx');var 后两个空格 + new Array(...)
  • C.6 不存在已撤销变量app_data.js 内没有 app_gameid / app_compareCode(早期 Contract 误列,原 msext 不写)

D. window.app_* H5 console 对账

Safari Web Inspector → Console

console.log("app_gameconfig =", app_gameconfig);  // 期望 ChannelConfig.plist gameconfig 实际值
console.log("app_market =", app_market);          // 期望 "2"
console.log("app_agent =", app_agent);            // 期望 ChannelConfig.plist agent 实际值
console.log("app_channel =", app_channel);
console.log("app_Launchtype =", app_Launchtype);  // 大厅期望 0
console.log("app_appversion =", app_appversion);  // 期望 '0' 字符串(审核切换标志非版本号)
console.log("app_getbattery =", app_getbattery);  // 期望 0.XX
console.log("app_getnetwork =", app_getnetwork);  // 期望 1/2/3
console.log("app_gamesname =", app_gamesname);    // 期望 Array
console.log("app_gameid =", app_gameid);          // 期望 undefined ✓
console.log("app_compareCode =", app_compareCode); // 期望 undefined ✓
  • D.1 所有 15 个 app_ 全局变量都 !== undefined*
  • D.2 关键字段都是 ChannelConfig.plist 的实际值(不是 H5 zip 默认占位)
  • D.3 app_appversion === '0'(审核标志,正常业务);app_gameconfig 取 BundleConfig.gameConfig

Phase 2 — 大厅 handler + 反向 callback

E. H5 → Native handler 响应

每项的验证方式:在 H5 console 调对应 bridge.callHandler('xxx', data, function(resp){console.log(resp);}),看 Xcode console 有无副作用 + H5 收到 cb。

  • E.1 vibrator → 真机震动 + H5 cb "vibrator"
  • E.2 repeatvibrator → 真机震动 + cb "repeatvibrator"
  • E.3 canclevibrator → cb "canclevibrator"(无可感知副作用,正常)
  • E.4 gameCopytext('test123') → 系统剪贴板含 "test123";其他 app 粘贴可验证;iOS 14+ 会出"已粘贴自 ylgamehall" banner(平台行为非 bug
  • E.5 gamepastetext → cb 是当前剪贴板内容(不是字面 "gamepastetext"
  • E.6 SwitchShake(1) + SwitchShake(0) → 内部 canVoice 状态切换(无可视化副作用,cb "SwitchShake"
  • E.7 startshake → 摇一摇手机 → H5 收到 shakeEnd 反向 callbackbridge.registerHandler('shakeEnd', fn) 先注册)
  • E.8 stopshake → 摇一摇 → H5 不再收到 shakeEnd
  • E.9 voicePlaying(1) / voicePlaying(0) → 内部 actor VoiceCenter 开关位(cb "Response from voicePlaying");mediaTypeAudio 实现后效果可验证
  • E.10 getphoneInfo → cb "getphoneInfo"大写 I+ 反向 callback getphoneinfo小写 i)含 6 字段(PhoneAdresseMAC / PhoneDeviceBrand / PhoneIMEI / PhoneModel / PhoneProvidersName / PhoneVersion
  • E.11 browser('https://www.apple.com') → Safari 打开苹果首页
  • E.12 opensaoma → cb "opensaoma"(空实现,无可感知副作用,符合契约)
  • E.13 startlocation → cb "startlocation"Phase 2 stub,无定位真实业务;Phase 5 升级后再验真实定位)

F. Native → H5 反向 callback

H5 需先用 bridge.registerHandler('callbackName', fn) 注册:

  • F.1 模拟器 Features → Battery State 切换 → H5 收到 getBattery "0.XX" + window.app_getbattery 实时跟随
  • F.2 模拟器 Features → Network Link Conditioner 切换 100% Loss → H5 收到 getnetwork "1" + window.app_getnetwork === 1;切回 WiFi → 收 "2";切 3G → 收 "3"
  • F.3 App 切后台(Home 键 / 上滑)→ H5 收到 appservice "2";回前台 → 收 "1" ⚠️ 值错位沿用 msext WKWebView 路径历史,参 Contract.md §3.2
  • F.4 摇一摇 → shakeEnd(见 E.7
  • F.5 getphoneInfo 触发 getphoneinfo(见 E.10

G. ExternalSubscriptions 生命周期

Phase 2.C 已实现 setup/teardown 配对,但 Phase 6 子游戏容器才能完整验证"防双发"。当前只验证基本生命周期。

  • G.1 viewWillAppear setupapp 启动后 onChange 闭包都已挂钩(F.1-F.4 都生效)
  • G.2 viewWillDisappear teardown:手动触发场景待 Phase 6 子游戏 push 上来时验证

Phase 3 — 音频体系(部分实现:3.A 完成;3.B/C/D 待外部资源)

H. 本地音频(srcIsloop3.A 完成)

  • E.14 srcIsloop({src:"xxx.wav", isloop:0}) → 听到单次按钮音;cb "Response from srcIsloop"
  • E.15 srcIsloop({src:"bg.wav", isloop:1}) → 听到背景循环音 + 同时调 srcIsloop({src:"另一首.wav", isloop:1}) → 替换为新背景循环
  • E.16 srcIsloop({src:"bg.wav", isloop:-1}) → 停止 bg.wav 背景;如果当前背景不是 bg.wav 则无效(与 msext 同名 type 校验)

依赖:测试需要 H5 zip 内 assets/wav/ 目录有可用音频文件。 H5 团队 / 项目方提供后做。

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 远端播放完整实现后升级(参 SDK-Integration-Guide.md §C,需用户手动配置 opencore-amr ObjC 源码)。

I.5 七牛 token 自签 + 上传单元验证(3.6 已落地,依赖 SDK 接入)

  • I.5.1 token 字符串结构正确:在 Xcode console 或 Swift Playground 调
    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 — 分享 + 登录(已落地)

真机要求:微信 / QQ / 抖音 SDK + URL Scheme 调起在模拟器都不可靠,必须真机。

J. 微信授权登录(4.B 已落地)

H5 console 先注册 callback

bridge.registerHandler('sharelogin', function(d){ console.log('sharelogin', d); });

然后调:

bridge.callHandler('accreditlogin', null, function(r){ console.log('cb', r); });
  • 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 容错:用户取消授权 → 不发 sharelogin(msext 同款乐观策略)
  • E.19.3 未装微信H5 alert 提示"请安装微信"(具体文案由 H5 决定,原生只保证不崩)
  • E.19.4 SceneDelegate 回调命中:app 切到微信 → 授权 → 切回 ylgamehall → 立刻收 sharelogin(不是过 N 秒)

K. 微信链接分享 type=1(4.E 已落地)

H5 注册 + 调用:

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);
  • E.20 sharefriend=2 type=1 → 直接拉起微信朋友圈分享面板,标题/描述/链接正确
  • E.20.1 sharesuccess 反向:分享成功或取消都收 sharesuccess({success:"2", type:"2"})type 是原 sharefriend);notInstalled / failed 不发(乐观策略)

L. SharePanel 三选一面板(4.B 已落地)

调用:

bridge.callHandler('friendsSharetypeUrlToptitleDescript', { sharefriend:"1", ... }, null);
  • L.1 底部弹出半透明面板,3 个按钮:微信 / QQ / 抖音
  • L.2 0.25s 滑入/滑出动画
  • L.3 半透明背景点击 → 关闭面板(不分享)
  • L.4 选微信 → 走 §K 路径;选 QQ → 走 §M;选抖音 → 走 §N
  • L.5 平台未安装 → 按钮置灰可点但触发后无副作用(与 msext 一致)

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):

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 — 高德定位(已落地)

真机要求fat .framework 在 M Mac 模拟器链接失败(参 Vendor/AMap/README.md 「已知限制」),所有定位验证只能跑真机。

P. startlocation 真实路径

H5 注册 + 调用:

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 / streetNumberprovince 小写 p,与 §J sharelogin 的 Province 大写 P 对照)
  • P.3 拒绝权限 → H5 收到 getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})
  • P.4 飞行模式 / 无网络 → 高德 SDK 返回错误 → H5 收 errorCode 12(或其它非 0

Phase 6 — 子游戏(已落地)

Q. SwitchOverGameData → 子游戏 push + backgameData → pop

H5 调用:

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 — 弹层 Overlay(已落地)

R. OpenurlTitleData 弹层 + 三个 settings

H5 调用:

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

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.errorSafari Web Inspector → 大厅 console 执行 console.error("test", 123, {a:1}) → Xcode console 看到 [H5 console.error] test 123 [object Object]
  • U.2 H5 onerrorH5 console 执行 undefinedVar.foo → Xcode console 看到 [H5 onerror] ReferenceError: ... at xxx.js:N:N + stack
  • U.3 H5 unhandledrejectionH5 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

附:契约 §10 26 项验收清单映射

本文档每个章节对应 Contract §10 的 A/B/C/D/E 五节:

本文档章节 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 / 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

维护规则

  • 每完成一个 Phase 子项 → 在对应章节补 / 勾验证步骤
  • 不要把验证步骤复制到 Contract §10Contract §10 是契约边界("必须长这样"),本文档是验证操作手册("怎么验证它长这样"),两份保持独立
  • 验证发现的 bug → 修复后 commit 时在 message 引用本文档对应 checkbox 编号(如 "修 E.10 getphoneInfo 大小写错"