按用户约定调整 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>
12 KiB
功能验证清单(开发期累积,统一验收)
定位:各 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 1:launch 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>/
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] 渠道注入读取结果(11 项)
[SandboxPaths] 路径计算结果
[ResourceUnzipper] ensureReady 完成
[RemoteConfigClient] 拉取成功 .parsed
[VersionResolver] chulishengji 双子树合并结果
[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_LaunchtypeL 大写、app_getwifisignalLevelwifi 小写 + signal/Level 区分) - C.3 app_battery.js 变量名带 get:
var app_getbattery=N;(不是app_battery) - C.4 app_network.js 变量名带 get:
var 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反向 callback(bridge.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"1";回前台 → 收 "2" - F.4 摇一摇 → shakeEnd(见 E.7)
- F.5 getphoneInfo 触发 getphoneinfo(见 E.10)
G. ExternalSubscriptions 生命周期
Phase 2.C 已实现 setup/teardown 配对,但 Phase 6 子游戏容器才能完整验证"防双发"。当前只验证基本生命周期。
- G.1 viewWillAppear setup:app 启动后 onChange 闭包都已挂钩(F.1-F.4 都生效)
- G.2 viewWillDisappear teardown:手动触发场景待 Phase 6 子游戏 push 上来时验证
Phase 3 — 音频体系(部分实现:3.A 完成;3.B/C/D 待外部资源)
H. 本地音频(srcIsloop,3.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 stub(3.B/3.C/3.D 待实现)
- 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 远端播放完整实现后升级。
Phase 4 — 分享 + 登录(部分实现:4.A stub 完成;4.B/4.C 待外部资源)
J. accreditlogin / friendsShare stub(4.A 完成)
- E.19 accreditlogin → cb "Response from accreditlogin"(stub 仅 cb,无微信授权调起、无 sharelogin 反向)
- E.20 friendsSharetypeUrlToptitleDescript → cb "sharefriend"(stub 仅 cb,无微信/QQ 分享调起、无 sharesuccess 反向)
4.B 微信 SDK + 后台 /wechat/login + 4.C QQ URL Scheme 完整实现后升级。
K. 微信 SDK + Universal Links(4.B 待实现)
项目方提供 microSDK + AppID + UL 后填充。预备验证项:
- sharelogin 7 字段(含大写 Province + 小写 city 经 danbian: 处理)
- sharesuccess({success:"2", type:"1"/"2"})
- app 切到后台微信授权 → 切回 app sharelogin 触发
L. QQ URL Scheme 分享(4.C 待实现,不依赖 SDK)
移植 msext QQShareManager.m URL Scheme fallback 后填充。预备验证项:
- 链接分享 →
mqqapi://share/to_fri?调起 QQ app- 朋友圈分享 →
mqqapi://share/to_qzone?调起 QQ 空间- 调起即视为 success(URL Scheme 单向无返回,msext 同款乐观策略)
Phase 5 — 定位(待实现)
Phase 5 实施后填充。预备验证项:
- startlocation 一次性 + 持续 + getlocationinfo 9 字段(含小写 province)
Phase 6 — 子游戏(待实现)
Phase 6 实施后填充。预备验证项:
- SwitchOverGameData push 子游戏
- 子游戏 backgameData 回大厅 + getWebdata
- 子游戏 push 上来后大厅 ExternalSubscriptions teardown(验证不会双发)
Phase 7-10(待实现)
各 Phase 实施后陆续填充。
附:契约 §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 实现 |
| 音频 / 录音 / 远端语音 | §B 桥接(音频相关) | Phase 3 待实现 |
| 分享 / 登录 | §B 桥接 + §E 回归 | Phase 4 待实现 |
| 定位 | §B 桥接 | Phase 5 待实现 |
| 子游戏 / 视频房间 | §B + §D 视频房间 | Phase 6 + 8 待实现 |
维护规则
- 每完成一个 Phase 子项 → 在对应章节补 / 勾验证步骤
- 不要把验证步骤复制到 Contract §10:Contract §10 是契约边界("必须长这样"),本文档是验证操作手册("怎么验证它长这样"),两份保持独立
- 验证发现的 bug → 修复后 commit 时在 message 引用本文档对应 checkbox 编号(如 "修 E.10 getphoneInfo 大小写错")