diff --git a/CLAUDE.md b/CLAUDE.md index 5d85404..5147c2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,30 @@ daoqi 仓库当前并存两条工作线: | `CLAUDE.md`(本文件) | 项目协作约定、契约边界声明 | 每次对话先读 | | `docs/H5-Native-Contract.md` | H5 ↔ 原生 桥接**契约**(黑盒可观察行为) | 涉及桥接接口必读 | | `docs/H5-Native-Implementation-Design.md` | 新外壳实施蓝图(架构、模块、代码骨架) | 实现新外壳必读 | +| `docs/Development-Plan.md` | 开发执行计划(Phase 0–10、依赖图、进度追踪) | 排期 / 进度更新必读 | + +--- + +## Resources/ 目录约定 + +仓库根目录的 `Resources/` 是新外壳运行所需的**项目方提供资源**统一入库位置,由打包脚本在 Build Phase 引入,**不**直接放进 Xcode 工程内的 `ylgamehall/` 源码目录。 + +### 当前内容(2026-06-21) + +- **`gamehall.zip`** — 大厅 H5 资源包(≈ 11 MB)。启动时由 `ResourceUnzipper` 解压到 `Library/Caches/{gamedir}/`。当前文件为 **2023-12 旧版**,开发期足以跑通管道,**上线前必须由 H5 团队提供最新版替换** +- **`Images.xcassets/`** — 原生层 Asset Catalog(AppIcon / LaunchImage / 微信/QQ/抖音分享平台图标) +- **`Res/`** — 散落原生资源: + - `sharelogo.png` — 分享缩略图(契约 §3.1【2】`friendsSharetypeUrlToptitleDescript` type=1 时使用) + - `shake_sound_male.mp3` — 摇一摇音效(受 `SwitchShake` 开关控制) + - `BackBT.png` / `Sistem_back.png` / `Default-568h@2x~iphone.png` / `Icon180.png` — 原 msext 兼容图,新外壳是否还需引用待 Phase 1 验证 + +### 后续约定 + +- 11 个**渠道注入目录**(`qiniudomain` / `gameid` / `channel` / `gamedir` / `gamestart` / `gameconfig` / `market` / `agent` / `appversion` / `other` / `appleconfig`)将放在 `Resources/ChannelInjection/` 子目录下,由 `Scripts/inject_channel.sh` 打包前按渠道动态生成 +- 任何新增需打入包的项目方资源(CDN 兜底图、本地音效、字体等)一律放 `Resources/`,**不**散落到 `ylgamehall/` 内 +- `Resources/` 内文件可入 git(与 `Vendor/` 同 — 为闭源资源 / 二进制锁定版本,保证多人 + CI 可复现);唯一例外是签名 / 敏感配置(已被 `.gitignore` 拦截) + +详细资源加载流程见 `docs/H5-Native-Implementation-Design.md` §7 资源 & 渠道注入。 --- diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29.png new file mode 100644 index 0000000..7cde344 Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29@2x.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29@2x.png new file mode 100644 index 0000000..69407ba Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29@2x.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29@3x.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29@3x.png new file mode 100644 index 0000000..699582e Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-129x29@3x.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-140x40@2x.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-140x40@2x.png new file mode 100644 index 0000000..e5b02e3 Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-140x40@2x.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-140x40@3x.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-140x40@3x.png new file mode 100644 index 0000000..caac756 Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-140x40@3x.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-157x57.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-157x57.png new file mode 100644 index 0000000..e950abc Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-157x57.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-157x57@2x.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-157x57@2x.png new file mode 100644 index 0000000..ee5d16c Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-157x57@2x.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-160x60@2x.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-160x60@2x.png new file mode 100644 index 0000000..caac756 Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-160x60@2x.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-160x60@3x.png b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-160x60@3x.png new file mode 100644 index 0000000..1a69a62 Binary files /dev/null and b/Resources/Images.xcassets/AppIcon-1.appiconset/AppIcon-160x60@3x.png differ diff --git a/Resources/Images.xcassets/AppIcon-1.appiconset/Contents.json b/Resources/Images.xcassets/AppIcon-1.appiconset/Contents.json new file mode 100755 index 0000000..e63d652 --- /dev/null +++ b/Resources/Images.xcassets/AppIcon-1.appiconset/Contents.json @@ -0,0 +1,77 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-129x29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-129x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-129x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-140x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-140x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-157x57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "AppIcon-157x57@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "AppIcon-160x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "AppIcon-160x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Images.xcassets/Contents.json b/Resources/Images.xcassets/Contents.json new file mode 100755 index 0000000..da4a164 --- /dev/null +++ b/Resources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/Contents.json b/Resources/Images.xcassets/LaunchImage-1.launchimage/Contents.json new file mode 100755 index 0000000..366e690 --- /dev/null +++ b/Resources/Images.xcassets/LaunchImage-1.launchimage/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "extent" : "full-screen", + "filename" : "LaunchImage-1-1100-Portrait-2436h@3x.png", + "idiom" : "iphone", + "minimum-system-version" : "11.0", + "orientation" : "portrait", + "scale" : "3x", + "subtype" : "2436h" + }, + { + "extent" : "full-screen", + "filename" : "LaunchImage-1-800-Portrait-736h@3x.png", + "idiom" : "iphone", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "3x", + "subtype" : "736h" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "minimum-system-version" : "8.0", + "orientation" : "landscape", + "scale" : "3x", + "subtype" : "736h" + }, + { + "extent" : "full-screen", + "filename" : "LaunchImage-1-800-667h@2x.png", + "idiom" : "iphone", + "minimum-system-version" : "8.0", + "orientation" : "portrait", + "scale" : "2x", + "subtype" : "667h" + }, + { + "extent" : "full-screen", + "filename" : "LaunchImage-1@2x.png", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "filename" : "LaunchImage-1-700-568h@2x.png", + "idiom" : "iphone", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x", + "subtype" : "retina4" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "orientation" : "landscape", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "minimum-system-version" : "7.0", + "orientation" : "landscape", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "filename" : "LaunchImage-1.png", + "idiom" : "iphone", + "orientation" : "portrait", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "filename" : "LaunchImage-1@2x 1.png", + "idiom" : "iphone", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "filename" : "LaunchImage-1-568h@2x.png", + "idiom" : "iphone", + "orientation" : "portrait", + "scale" : "2x", + "subtype" : "retina4" + }, + { + "extent" : "to-status-bar", + "idiom" : "ipad", + "orientation" : "portrait", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "orientation" : "portrait", + "scale" : "1x" + }, + { + "extent" : "to-status-bar", + "idiom" : "ipad", + "orientation" : "landscape", + "scale" : "1x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "orientation" : "landscape", + "scale" : "1x" + }, + { + "extent" : "to-status-bar", + "idiom" : "ipad", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "extent" : "to-status-bar", + "idiom" : "ipad", + "orientation" : "landscape", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "ipad", + "orientation" : "landscape", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-1100-Portrait-2436h@3x.png b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-1100-Portrait-2436h@3x.png new file mode 100644 index 0000000..5c64158 Binary files /dev/null and b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-1100-Portrait-2436h@3x.png differ diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-568h@2x.png b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-568h@2x.png new file mode 100644 index 0000000..e54790a Binary files /dev/null and b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-568h@2x.png differ diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-700-568h@2x.png b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-700-568h@2x.png new file mode 100644 index 0000000..e54790a Binary files /dev/null and b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-700-568h@2x.png differ diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-800-667h@2x.png b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-800-667h@2x.png new file mode 100644 index 0000000..d280ccd Binary files /dev/null and b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-800-667h@2x.png differ diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-800-Portrait-736h@3x.png b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-800-Portrait-736h@3x.png new file mode 100644 index 0000000..c93ee29 Binary files /dev/null and b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1-800-Portrait-736h@3x.png differ diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1.png b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1.png new file mode 100644 index 0000000..74c685e Binary files /dev/null and b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1.png differ diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1@2x 1.png b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1@2x 1.png new file mode 100644 index 0000000..dcbb36a Binary files /dev/null and b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1@2x 1.png differ diff --git a/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1@2x.png b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1@2x.png new file mode 100644 index 0000000..dcbb36a Binary files /dev/null and b/Resources/Images.xcassets/LaunchImage-1.launchimage/LaunchImage-1@2x.png differ diff --git a/Resources/Images.xcassets/shareDouyin.imageset/Contents.json b/Resources/Images.xcassets/shareDouyin.imageset/Contents.json new file mode 100644 index 0000000..55a6335 --- /dev/null +++ b/Resources/Images.xcassets/shareDouyin.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "shareDouyin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "shareDouyin@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shareDouyin@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin.png b/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin.png new file mode 100644 index 0000000..e39ea4b Binary files /dev/null and b/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin.png differ diff --git a/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin@2x.png b/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin@2x.png new file mode 100644 index 0000000..b9b589c Binary files /dev/null and b/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin@2x.png differ diff --git a/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin@3x.png b/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin@3x.png new file mode 100644 index 0000000..bafd499 Binary files /dev/null and b/Resources/Images.xcassets/shareDouyin.imageset/shareDouyin@3x.png differ diff --git a/Resources/Images.xcassets/shareQQ.imageset/Contents.json b/Resources/Images.xcassets/shareQQ.imageset/Contents.json new file mode 100644 index 0000000..ab721d6 --- /dev/null +++ b/Resources/Images.xcassets/shareQQ.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "shareQQ.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "shareQQ@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shareQQ@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Images.xcassets/shareQQ.imageset/shareQQ.png b/Resources/Images.xcassets/shareQQ.imageset/shareQQ.png new file mode 100644 index 0000000..f93fa44 Binary files /dev/null and b/Resources/Images.xcassets/shareQQ.imageset/shareQQ.png differ diff --git a/Resources/Images.xcassets/shareQQ.imageset/shareQQ@2x.png b/Resources/Images.xcassets/shareQQ.imageset/shareQQ@2x.png new file mode 100644 index 0000000..fc8d964 Binary files /dev/null and b/Resources/Images.xcassets/shareQQ.imageset/shareQQ@2x.png differ diff --git a/Resources/Images.xcassets/shareQQ.imageset/shareQQ@3x.png b/Resources/Images.xcassets/shareQQ.imageset/shareQQ@3x.png new file mode 100644 index 0000000..3f3d6d5 Binary files /dev/null and b/Resources/Images.xcassets/shareQQ.imageset/shareQQ@3x.png differ diff --git a/Resources/Images.xcassets/shareWechat.imageset/Contents.json b/Resources/Images.xcassets/shareWechat.imageset/Contents.json new file mode 100644 index 0000000..3612a2f --- /dev/null +++ b/Resources/Images.xcassets/shareWechat.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "shareWechat.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "shareWechat@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "shareWechat@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Images.xcassets/shareWechat.imageset/shareWechat.png b/Resources/Images.xcassets/shareWechat.imageset/shareWechat.png new file mode 100644 index 0000000..700805b Binary files /dev/null and b/Resources/Images.xcassets/shareWechat.imageset/shareWechat.png differ diff --git a/Resources/Images.xcassets/shareWechat.imageset/shareWechat@2x.png b/Resources/Images.xcassets/shareWechat.imageset/shareWechat@2x.png new file mode 100644 index 0000000..73c3115 Binary files /dev/null and b/Resources/Images.xcassets/shareWechat.imageset/shareWechat@2x.png differ diff --git a/Resources/Images.xcassets/shareWechat.imageset/shareWechat@3x.png b/Resources/Images.xcassets/shareWechat.imageset/shareWechat@3x.png new file mode 100644 index 0000000..94ae88d Binary files /dev/null and b/Resources/Images.xcassets/shareWechat.imageset/shareWechat@3x.png differ diff --git a/Resources/Res/BackBT.png b/Resources/Res/BackBT.png new file mode 100755 index 0000000..2718a1f Binary files /dev/null and b/Resources/Res/BackBT.png differ diff --git a/Resources/Res/Default-568h@2x~iphone.png b/Resources/Res/Default-568h@2x~iphone.png new file mode 100644 index 0000000..beb69b4 Binary files /dev/null and b/Resources/Res/Default-568h@2x~iphone.png differ diff --git a/Resources/Res/Icon180.png b/Resources/Res/Icon180.png new file mode 100644 index 0000000..caac756 Binary files /dev/null and b/Resources/Res/Icon180.png differ diff --git a/Resources/Res/Sistem_back.png b/Resources/Res/Sistem_back.png new file mode 100755 index 0000000..9f0e9b7 Binary files /dev/null and b/Resources/Res/Sistem_back.png differ diff --git a/Resources/Res/shake_sound_male.mp3 b/Resources/Res/shake_sound_male.mp3 new file mode 100755 index 0000000..b18210d Binary files /dev/null and b/Resources/Res/shake_sound_male.mp3 differ diff --git a/Resources/Res/sharelogo.png b/Resources/Res/sharelogo.png new file mode 100644 index 0000000..d1896ab Binary files /dev/null and b/Resources/Res/sharelogo.png differ diff --git a/Resources/gamehall.zip b/Resources/gamehall.zip new file mode 100644 index 0000000..f07681d Binary files /dev/null and b/Resources/gamehall.zip differ diff --git a/docs/Development-Plan.md b/docs/Development-Plan.md new file mode 100644 index 0000000..d05623d --- /dev/null +++ b/docs/Development-Plan.md @@ -0,0 +1,882 @@ +# 进贤聚友棋牌 iOS 新外壳开发计划 + +> 配套文档: +> - `docs/H5-Native-Implementation-Design.md` — 架构蓝图(**WHAT to build / HOW to design**) +> - `docs/H5-Native-Contract.md` — H5↔原生契约(**外部可观察行为,验收以此为准**) +> +> 本文档:执行图(**WHEN / IN WHAT ORDER / ACCEPTANCE CRITERIA**),随实施持续更新。 +> +> 起点:M0 部分完成(项目基础配置就绪);终点:通过契约 §10 全部 26 项验收清单后灰度上线。 + +--- + +## 1. 文档三角关系 + +``` +Contract Design Plan(本文档) + ↓ ↓ ↓ +契约边界 架构蓝图 执行路线 +"必须长这样" "推荐怎么做" "按这个顺序做" + +不可越界 可自由重构 随进度更新 +``` + +- **Contract 与 Design 是稳定文档**,除非契约或架构本身要变更,否则不动 +- **Plan 是活文档**,每完成一个 Phase 就在 §8 进度追踪里勾掉、把后续 Phase 调整为新现实 +- **冲突时优先级**:Contract > Design > Plan(不能为了赶进度违背契约) + +--- + +## 2. 起点:当前项目状态快照(2026-06-21) + +### 2.1 已完成 + +- [x] Xcode 工程骨架(File System Synchronized Group,objectVersion 77,Xcode 26.5) +- [x] iOS Deployment Target 15.6 +- [x] Swift 6.0 + `SWIFT_APPROACHABLE_CONCURRENCY=YES` + `SWIFT_DEFAULT_ACTOR_ISOLATION=MainActor` +- [x] iPhone + iPad 仅横屏,`UIRequiresFullScreen=true` +- [x] 删除默认 Storyboard,改用 `SceneDelegate` 代码启动 → `RootViewController`(M0 占位) +- [x] Info.plist 契约前置项:`UIStatusBarHidden=NO` / `UIViewControllerBasedStatusBarAppearance=YES` +- [x] `AppDelegate` 设置 `applicationSupportsShakeToEdit=true` +- [x] `.gitignore`(标准 iOS/Swift 模板,含签名 / 敏感配置 / xcuserdata) +- [x] CLAUDE.md 加入"及时提交"规则 +- [x] BuildProject 验证通过 + +### 2.2 已就位的项目方资源 + +仓库根 `Resources/` 已包含(详见 CLAUDE.md「Resources/ 目录约定」与 Design §7.0): + +- ✅ `Resources/gamehall.zip`(2023-12 旧版,开发期足够,上线前替换最新版) +- ✅ `Resources/Images.xcassets`(AppIcon / LaunchImage / 分享平台图标) +- ✅ `Resources/Res/`(`sharelogo.png` / `shake_sound_male.mp3` 等散落原生资源) + +→ Phase 1 不再阻塞于 H5 团队,可立即展开。 + +### 2.3 待项目方协调的外部资源(阻塞项) + +| 资源 | 用途 | 阻塞起始 Phase | 协调对象 | +|------|------|--------------|---------| +| `gamehall.zip` **最新版**(上线前替换) | 真实 H5 业务代码 | Phase 10 灰度前 | H5/前端团队 | +| 11 个渠道注入目录的目录名值 | 渠道 / 游戏 ID / 七牛域名 / market 等 | Phase 1 | 项目方/运营 | +| 微信 OpenSDK `.framework` | 登录 / 分享 | Phase 4 | 项目方(可从 msext/Vendor 拷贝) | +| 微信 AppID(`wx586a9b321e56efb7`)+ Universal Links 配置 | 微信回调 | Phase 4 | 项目方 | +| QQ OpenSDK `.framework` + AppID | QQ 分享 | Phase 4 | 项目方 | +| 后台 `/wechat/login` 中转接口 | secret 不入 IPA 的前提 | Phase 4 | 后台团队 | +| 高德地图 `APIKey` + SDK Pod | 定位 | Phase 5 | 已有 key `b0d4a8e3fcbbcc0dd96283b7df6a4494`,但需新建 Bundle ID 注册 | +| 七牛上传 token 颁发接口 | 录音上传 | Phase 3 | 后台团队 | +| 极光 AppKey | 用户统计 | Phase 9 | 项目方 | +| Sentry DSN | 崩溃监控 | Phase 9 | 项目方(需开通账号) | +| Agora AppID(视频房间) | 子游戏视频 | Phase 8(默认 stub,可延后) | 项目方 | +| 闲聊 SDK | 闲聊分享 | Phase 9(默认 stub) | 项目方 | +| 开发 / 企业签名证书 + 描述文件 | 真机调试 / 分发 | Phase 1 起持续 | 项目方/iOS 账号管理员 | + +> **协调原则**:当某 Phase 阻塞在外部资源时,**先做不依赖该资源的 Phase**(并行机会见 §3 依赖图)。 + +### 2.4 已确定的项目约束 + +| 项 | 值 | +|---|---| +| H5 设计分辨率 | **1280 × 720(16:9)** | +| WebView 适配策略 | 保持 16:9 比例(设备比例不匹配时上下/左右 letterbox) | +| Bundle ID | `com.skyapp.ylgamehall`(与 msext 分离) | +| Team ID | NX5W3B4QP3 | +| 设备 | iPhone + iPad(不去 iPad) | +| 支持方向 | 仅横屏(LandscapeLeft / LandscapeRight) | +| Swift 版本 | 6.0 | +| 最低 iOS | 15.6 | + +--- + +## 3. 全局依赖图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Phase 0 工程化基线(剩余项:SwiftLint / 测试 target / CI) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Phase 1 资源层 + 桥核心 + 第一个 handler │ +│ ResourceKit / BundleConfig / BridgeCore / BridgedWebView │ +│ 验收:H5 加载 + 一个简单 handler 双向通 │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌─────────────┼─────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ Phase 2 │ │ Phase 3 │ │ Phase 5 │ + │ 简单 │ │ 音频体系 │ │ 定位 │ + │ handler │ │ │ │ │ + │ + 反向cb │ │ │ │ │ + └────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ + └─────────────┴─────────────┘ + │ 并行可行 + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Phase 4 分享 + 登录(前置:项目方提供 SDK / 后台接口) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Phase 6 子游戏容器(SwitchOverGameData / backgameData) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Phase 7 弹层(Overlay + window.settings polyfill) │ +│ 完成后契约 §10 A/B/C/E 节可全跑通 │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Phase 8 视频房间(默认 NoopVideoRoom;Agora 启用是开关) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Phase 9 SDK 真实化(Sentry / 极光)+ Stub 替换为真实 SDK │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Phase 10 多渠道打包 + 真机回归 + 灰度切流 │ +└─────────────────────────────────────────────────────────────┘ +``` + +**关键并行机会**:Phase 2 / 3 / 5 互不依赖,可按当下手头的资源到位情况自由调度。 + +--- + +## 4. Phase 总览 + +| Phase | 目标 | 关键产出 | 验收来源 | 相对成本 | +|-------|------|---------|---------|---------| +| **P0** | 工程化基线 | SwiftLint、单测 target、CI 流水线 | 本文档 §5.0 | 小 | +| **P1** | 最小垂直闭环 | `BundleConfig` / `ResourceUnzipper` / `BridgeCore` / `BridgedWebView` + `vibrator` handler | 契约 §10 中 `vibrator` 项;H5 file:// 加载成功 | 大 | +| **P2** | 大厅简单 handler + 反向 callback | `DeviceKit` / `Pasteboard` / 振动 / 摇一摇 / 网络 / 电池 / 前后台 | 契约 §10 B 节 + C 节 | 中 | +| **P3** | 音频体系 | `AudioKit`(播放 / 录音 / AMR↔WAV / 七牛上传) | 契约 §10 中 `srcIsloop` / `mediaTypeAudio` / `prepareaudio` 项 | 大 | +| **P4** | 分享 + 登录 | `LoginKit`(WeChat OAuth)/ `ShareKit`(WeChat + Noop 闲聊) | 契约 §10 B 节 `accreditlogin` / `friendsShare...` 项 | 大 | +| **P5** | 定位 | `LocationKit`(高德 SDK 封装) | 契约 §10 B 节 `startlocation` 项 | 中 | +| **P6** | 子游戏容器 | `SubGameViewController` + `Coordinator` + `SwitchOverGameData` / `backgameData` | 契约 §10 中 `SwitchOverGameData` 项 | 中 | +| **P7** | 弹层 | `OverlayViewController` + `window.settings` polyfill + 节流 | 契约 §10 中 `OpenurlTitleData` / `settings.*` 项 | 中 | +| **P8** | 视频房间 stub | 3 个 stub handler + Native→H5 `getVideoinfo` / `phonestate` / `recordSuccess` callback | 契约 §10 D 节(NoopVideoRoom 路径) | 小 | +| **P9** | SDK 真实化 + 监控 | Sentry / 极光 / 七牛 SPM 接入;闲聊 / Bugly 决策落地 | 契约 §10 全 26 项 | 中 | +| **P10** | 多渠道 + 灰度 | 渠道注入脚本 / CI 多渠道出包 / 真机回归 / 灰度切流 | 真机验收 + 灰度无回退 | 大 | + +**相对成本说明**: +- 小:1 个工作日内可独立完成 +- 中:2–4 个工作日,含联调 +- 大:5–10 个工作日,跨多个模块或需对外联调 + +--- + +## 5. 各 Phase 详细计划 + +### Phase 0: 工程化基线(剩余项) + +#### 目标 +建立项目级别的代码质量与回归基线,使后续 Phase 每个 commit 都可被自动验证。 + +#### 前置 +无。当前 M0 基础配置已完成。 + +#### 任务清单 + +- [ ] **0.1** 加入 SwiftLint(SPM 插件方式,避免全局安装依赖) + - `Package.swift` 加 `SwiftLintPlugin`;规则文件 `.swiftlint.yml` 按 Design §13.4 配(行长 120、文件 ≤400、函数 ≤40) + - 验收:`xcodebuild` 时 lint 自动跑,违规 warning 出现 +- [ ] **0.2** 新建 `ylgamehallTests` 单测 target(Testing framework,不引 XCTest 旧 API) + - 第一个测试 `BootstrapTests.testAppDelegateRespondsToShake`,确认 `applicationSupportsShakeToEdit=true` 已生效 + - 验收:`xcodebuild test` 通过 +- [ ] **0.3** 新建 `ylgamehallContractTests` 契约测试 target(与 unit test 分离,跑得久也无所谓) + - 暂只放空骨架,Phase 1 起逐项填入 +- [ ] **0.4** 新建 `ylgamehallUITests` UI 测试 target(XCUI) + - 暂只放空骨架,Phase 10 真机回归用 +- [ ] **0.5** 加 CI 配置(GitHub Actions 或本地 Jenkinsfile,按项目方实际 CI 平台) + - 跑:`xcodebuild test -scheme ylgamehall` + lint + - 验收:push 后 CI 绿 +- [ ] **0.6** 准备 SPM 包目录结构(**先建空 `Package.swift` 但暂不切 target**) + - 决策:先单 target 内按目录组织,等 P3 之后代码量 > 5K 行再切 SPM + - 这样早期开发不被 SPM 切分成本拖慢 + +#### 验收 + +- `xcodebuild test` 通过;CI 绿;SwiftLint 报告 0 违规 + +#### 风险 + +- 单测 / UI 测 target 的 signing 需配置(自动签 + 不同 Bundle ID 后缀) + +--- + +### Phase 1: 最小垂直闭环(资源 + 桥 + 第一个 handler) + +#### 目标 +打通**从启动 → 渠道注入读取 → 解压 H5 → 加载 file:// → JS 与原生互调**的全链路,用最简单的 handler `vibrator` 作为验证标的。 + +#### 前置 +- ✅ Phase 0 基线 +- ✅ `Resources/gamehall.zip`(2023-12 旧版,足以验收 Phase 1) +- ⏳ 项目方提供 11 个渠道注入目录名值(缺则用临时 demo 值) +- 备选:若 zip 加载有问题,临时用一个最小 H5(`` 含一个 button 调 `bridge.callHandler('vibrator')`)做工程内测排查 + +#### 任务清单 + +##### 1.A 资源层 + +- [ ] **1.1** 创建 `Resources/ChannelInjection/` 目录结构(11 个空容器目录占位) + - `qiniudomain/<待填>`、`gameid/<待填>` 等 11 项 + - 文件夹下放 `.gitkeep` +- [ ] **1.2** 实现 `Source/Resource/BundleConfig.swift` + - `static func readInjected(_ key: String) -> String`:扫描 Bundle 子目录,返回首个非隐藏子项名 + - 单测 fixture:建立 mock bundle,验证 11 个 key 全部能读出 +- [ ] **1.3** 实现 `Source/Resource/SandboxPaths.swift` + - 常量:`caches` / `documents` / `bundle` + - `lobbyIndex() -> URL` 拼出 `{Caches}/{gamedir}/{gamestart}/index.html` +- [ ] **1.4** 加 ZIPFoundation SPM 依赖 +- [ ] **1.5** 实现 `Source/Resource/ResourceUnzipper.swift`(actor) + - `ensureReady() async throws`:检测 `version.xml` 不存在则解压 Bundle 内 `gamehall.zip` 到 caches + - 单测:用 fixture zip 验证解压幂等性 + +##### 1.B 桥核心 + +- [ ] **1.6** 实现 `Source/Bridge/BridgeProtocol.swift` + - `BridgeProtocol` / `BridgeData` / `BridgeHandler` / `BridgeCallback` 类型 +- [ ] **1.7** 实现 `Source/Bridge/BridgeBus.swift`(@MainActor) + - `register(_:handler:)` / `call(_:data:callback:)` / `didReceive(_:)` + - 单测:mock WKWebView,验证 handler 注册 / 分发 / callback 配对 +- [ ] **1.8** 引入 WVJB JS 端协议源码 + - 从 [marcuswestin/WebViewJavascriptBridge](https://github.com/marcuswestin/WebViewJavascriptBridge) 取 `WebViewJavascriptBridge.js.txt`,作为 `Resources/JS/WebViewJavascriptBridge.js` + - 注入方式:`WKUserScript`,`atDocumentStart` + +##### 1.C WebView 容器 + +- [ ] **1.9** 实现 `Source/WebView/BridgedWebView.swift` + - WKWebView 配置照 Contract §4.1(`javaScriptCanOpenWindowsAutomatically=NO`、`minimumFontSize=10`、`bounces=NO`、`scrollEnabled=NO` 等) + - `WKProcessPool` 单例 `SharedProcessPool.shared` +- [ ] **1.10** 实现 `Source/WebView/WebContainerViewController.swift`(基类) + - 持有 `BridgedWebView` + `BridgeBus` + - 16:9 比例布局:屏幕比 < 16:9 用宽度撑满上下黑边;屏幕比 > 16:9 用高度撑满左右黑边 + - 实现 `webViewWebContentProcessDidTerminate:` 退避 reload(Design §3.6) +- [ ] **1.11** 实现 `Source/Bridge/Handlers/VibratorHandler.swift` + - `register(_ bridge)` 注册 `vibrator` handler → `AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)` + responseCallback `"vibrator"` + +##### 1.D 集成 + +- [ ] **1.12** `SceneDelegate` → `LobbyViewController`(继承 `WebContainerViewController`) + - viewDidLoad: `ResourceUnzipper.ensureReady()` await 完成后 → `BridgedWebView.load(SandboxPaths.lobbyIndex())` +- [ ] **1.13** 把渠道目录里临时填入的 demo 值(如 `gamedir/demo` / `gamestart/gamehall`)+ 一个 demo H5 跑通 + +#### 验收 + +- 真机启动后能看到 H5 demo 页 +- H5 按钮调 `bridge.callHandler('vibrator')` 后真机震动 +- H5 收到 `responseCallback("vibrator")` +- BuildProject 通过;契约测试 `VibratorContractTest` 通过 + +#### 风险 + +- WVJB JS 端协议握手时序对加载顺序敏感,必须用 `WKUserScript` 在 documentStart 注入 +- iPad letterbox 实现细节:用 `aspectRatio` constraint vs 手算 frame;推荐用 Auto Layout `aspectRatio=16/9` + centerX/centerY + +--- + +### Phase 2: 大厅简单 handler + 反向 callback + +#### 目标 +把不依赖外部 SDK 的 11 个 handler 全部实现,使大厅 H5 在不接微信/七牛/高德的前提下能完整跑大部分业务。 + +#### 前置 +- Phase 1 完成 + +#### 任务清单 + +##### 2.A H5→Native handler(无外部依赖) + +- [ ] **2.1** `gameCopytext` / `gamepastetext`(剪贴板) +- [ ] **2.2** `vibrator` / `repeatvibrator` / `canclevibrator`(已有,扩展) +- [ ] **2.3** `startshake` / `stopshake` / `SwitchShake`(摇一摇开关 + 音效开关) +- [ ] **2.4** `voicePlaying`(语音播放总开关) +- [ ] **2.5** `getphoneInfo` → 触发反向 callback `getphoneinfo`(注意小写 i) +- [ ] **2.6** `browser` → 系统 Safari 打开 URL +- [ ] **2.7** `opensaoma` 空实现注册(契约 §3.1【22】要求) + +##### 2.B Native→H5 反向 callback + +- [ ] **2.8** `Source/Device/DeviceInfo.swift`:6 字段 snapshot + - `getphoneInfo` 收到调用后 → `bridge.call("getphoneinfo", data: ...)` 反向 +- [ ] **2.9** `Source/Device/BatteryMonitor.swift`:监听 `UIDeviceBatteryLevelDidChangeNotification` + - 触发 → `bridge.call("getBattery", data: .string("%.2f"))` +- [ ] **2.10** `Source/Device/NetworkMonitor.swift`:`NWPathMonitor` 包装 + - 状态变化 → `bridge.call("getnetwork", data: .string("1"/"2"/"3"))` +- [ ] **2.11** `Source/Device/AppLifecycleObserver.swift`:监听 SceneDelegate 前后台通知 + - → `bridge.call("appservice", data: .string("1"=后台 / "2"=前台))` +- [ ] **2.12** `Source/WebView/ShakeDetector.swift`:在 `WebContainerViewController` 重写 `motionEnded:` + - → `bridge.call("shakeEnd", data: nil)`(仅 `canshake=YES` 时触发) + +##### 2.C 外部订阅生命周期 + +- [ ] **2.13** 实现 Design §2.4.2 的 `ExternalSubscriptions` 模式 + - `viewWillAppear` resume / `viewWillDisappear` suspend + - 防双发:栈深 ≥ 2 时下层不发桥事件 + +#### 验收 + +- 契约 §10 B 节中以下项目通过: + - `getphoneInfo` 6 字段完整 + - `gameCopytext` + `gamepastetext` + - `vibrator` / `repeatvibrator` +- 契约 §10 C 节全部通过: + - 前后台切换 `appservice("1"/"2")` + - 电量变化 `getBattery("0.XX")` + - 飞行模式切换 `getnetwork("1"/"2"/"3")` + - 摇一摇 `shakeEnd` + +#### 风险 + +- `motionEnded:` 需要 `become first responder`,在 `viewDidAppear` 内调用 `becomeFirstResponder()` +- iOS 14+ `WKWebView.scrollView.bounces` 在某些版本被重置,需 viewWillAppear 内重新设置 +- `NWPathMonitor` 必须保持引用,否则会被释放停止监听 + +--- + +### Phase 3: 音频体系 + +#### 目标 +打通本地音频播放 / 远程语音播放 / 录音 → AMR 转码 → 七牛上传的完整链路。 + +#### 前置 +- Phase 1 完成 +- ⏳ 项目方提供:opencore-amr 静态库(可从 msext 拷贝)+ 七牛上传 token 颁发接口 + +#### 任务清单 + +##### 3.A 本地音频 + +- [ ] **3.1** `Source/Audio/AudioPlayer.swift`(@MainActor) + - `background` / `button` / `voice` 三类 AVAudioPlayer + - `srcIsloop` handler:`isloop=0` 单次 / `isloop=1` 循环背景 / `isloop=-1` 停同名背景 +- [ ] **3.2** 真机验证:放一个 `.wav` 进 Resources,H5 调 `srcIsloop({src:"test.wav", isloop:0})` 出声 + +##### 3.B AMR 转码 + +- [ ] **3.3** 从 msext 拷贝 `libopencore-amrnb.a` / `libopencore-amrwb.a`(已 segalign 8 修复版)到 `Vendor/` +- [ ] **3.4** 拷贝 `VoiceConverter` 头文件 + 实现 → 改写为 `Source/Audio/VoiceCoder.swift` Swift wrapper + - `amrToWav(_:dest:)` / `wavToAmr(_:dest:)` + - 单测:fixture amr → 转 wav → 转回 amr,比较前后 hash + +##### 3.C 录音 + 上传 + +- [ ] **3.5** `Source/Audio/AudioRecorder.swift`(actor) + - `record() async throws -> AudioFile`:`AVAudioRecorder` 录 WAV + - 麦克风权限:Info.plist 加 `NSMicrophoneUsageDescription = "{gamehallname}需要访问您的麦克风录制语音消息"` +- [ ] **3.6** 七牛 SPM 依赖(v8+)+ `Source/Network/QiniuUploader.swift` + - `upload(_:token:) async throws -> UploadedFile` + - 单测:mock token,模拟上传成功 / 失败 +- [ ] **3.7** `prepareaudio` handler:拉起录音 → 停止录音 → AMR 转码 → 上传 → 反向 callback `getaudiourl` + `recordSuccess`(仅子游戏触发) + +##### 3.D 远程语音回放 + +- [ ] **3.8** `mediaTypeAudio` handler:下载 AMR → 转 WAV → `AVAudioPlayer` 播放 + - 开始播放 → `bridge.call("gameui_play_voice", user)` + - 播放结束 → `bridge.call("gameui_stop_voice", user)` + - 受 `voicePlaying` 开关控制(=1 才播) + +#### 验收 + +- 契约 §10 B 节中: + - `srcIsloop` 背景音循环 / 停止 + - `mediaTypeAudio` 远端播放 + play/stop callback + - `prepareaudio` → 录音 → 上传 → `getaudiourl({audiourl, time})` + +#### 风险 + +- 麦克风权限被拒后的 UI 兜底(H5 alert 提示,契约 §3.1【4】) +- 七牛 token 时效性:每次录音前后端动态颁发 vs 长 token 缓存 +- AVAudioSession 与背景音 / 通话音的混音规则(`.playAndRecord` mode) + +--- + +### Phase 4: 分享 + 登录 + +#### 目标 +微信授权登录 + 微信好友/朋友圈分享 + 闲聊分享(Noop stub)。 + +#### 前置 +- Phase 1 完成 +- ⏳ 微信 OpenSDK `.framework` + AppID + Universal Links +- ⏳ QQ OpenSDK `.framework` + AppID +- ⏳ 后台 `/wechat/login` 接口(或先用客户端直拼 fallback) + +#### 任务清单 + +##### 4.A SDK 接入 + +- [ ] **4.1** `Vendor/WechatSDK/` 拷贝微信 SDK + - Info.plist 加 URL Scheme(`wx586a9b321e56efb7`)+ `LSApplicationQueriesSchemes`(`weixin` / `weixinULAPI` / `weixinURLParamsAPI`) + - Entitlement 加 Associated Domains(Universal Links) +- [ ] **4.2** `Vendor/QQShare/` 拷贝 QQ SDK + - Info.plist 加 QQ URL Scheme + `LSApplicationQueriesSchemes`(`mqq*`) +- [ ] **4.3** `Source/SDK/WeChat/WeChatSDK.swift` 启动注册(Design §4.2 `registerFull` + 全部 12 个 `MMAPP_SUPPORT_*` flag) +- [ ] **4.4** `Source/SDK/WeChat/WeChatManager.swift`(@MainActor) + - 持久 delegate + - `authorize() async throws -> WXAuthCode`(state UUID 配对) + - `share(_:scene:) async throws`(FIFO 串行) + - 详见 Design §8.5 +- [ ] **4.5** `SceneDelegate.openURLContexts` 接入 QQ → WXApi 顺序 + +##### 4.B 授权登录 + +- [ ] **4.6** `Source/Login/WeChatAuth.swift`(actor) + - `authorize() async throws -> WeChatUser`:内部调 `WeChatManager.authorize()` 拿 code → 转给后台 `/wechat/login`(或客户端 fallback)→ 拿到 7 字段 user + - 后台未就绪时的 fallback:客户端直拼 `api.weixin.qq.com/sns/oauth2/access_token` —— 文件头标 `// FIXME: 待后台 /wechat/login 就绪后切换为后台中转` +- [ ] **4.7** `accreditlogin` handler:触发 OAuth → 反向 callback `sharelogin`(7 字段,注意 `Province` 大写 P) + - **关键契约测试**:`sharelogin` payload 字段名严格 1:1 + +##### 4.C 分享 + +- [ ] **4.8** `Source/Share/SharePlatform.swift` 协议 + `WeChatShare` / `NoopSharePlatform(name:"xianliao")` +- [ ] **4.9** `Source/Share/ShareCenter.swift`:策略分发(type=1/2/3 × sharetype=1/2/3,共 9 种组合) +- [ ] **4.10** `friendsSharetypeUrlToptitleDescript` handler:参数解析 → ShareCenter.dispatch → 反向 callback `sharesuccess({success:"2", type:<原 sharefriend>})` +- [ ] **4.11** 截图分享:`getImageWithFullScreenshot` Swift 等价实现(`UIGraphicsImageRenderer`) +- [ ] **4.12** 远程图片分享:URLSession 下载 → 重打包 + +#### 验收 + +- 契约 §10 B 节: + - `accreditlogin` 收到 7 字段 `sharelogin`(Province 大写) + - `friendsSharetypeUrlToptitleDescript` type=1/2/3 各跑一次,收到 `sharesuccess` +- 契约 §10 E 节:微信 / QQ 回调命中(QQ 必须先判) + +#### 风险 + +- 微信审核:Universal Links 配置错误 → 授权回不来 +- AppSecret 客户端泄露 = 严重安全问题;fallback 路径上线前必须切到后台 +- 截图分享在 Liquid Glass / 新版 UIKit 下 `drawHierarchy` API 行为变化,需查 DocumentationSearch 验证 + +--- + +### Phase 5: 定位 + +#### 目标 +高德定位 + 逆地理 → 反向 callback `getlocationinfo` 9 字段。 + +#### 前置 +- Phase 1 完成 +- ⏳ 高德 SDK(CocoaPods 接入,APIKey 已有但 Bundle ID 需重新注册) + +#### 任务清单 + +- [ ] **5.1** 加 Podfile + 引入 `AMapLocation` 9.x +- [ ] **5.2** Info.plist 加 `NSLocationWhenInUseUsageDescription = "{gamehallname}需要访问您的位置以提供本地化服务"` +- [ ] **5.3** `Source/SDK/AMap/AMapWrapper.swift`:启动期 `updatePrivacyShow` + `updatePrivacyAgree` + `apiKey` 设置 +- [ ] **5.4** `Source/Location/LocationService.swift`(actor) + - `requestOnce() async throws -> LocationPayload` + - `startContinuous(onUpdate:)` / `stop()` +- [ ] **5.5** `startlocation` handler:`data=1` 持续 / 其他一次性 → 反向 callback `getlocationinfo` 9 字段 + - **关键契约**:`latitude` / `longitude` 是 **string**(`stringWithFormat:%f` 等价产物) + - **关键契约**:`province` 是小写 p(与 `sharelogin.Province` 大写不同) +- [ ] **5.6** 失败路径:`getlocationinfo({errorCode:12, errorMsg:"缺少定位权限"})` + +#### 验收 + +- 契约 §10 B 节 `startlocation` → 9 字段完整 +- 拒绝权限后 H5 收到 errorCode 12 + +#### 风险 + +- 高德 Bundle ID 绑定:APIKey 是绑死 Bundle ID 的,必须用项目方控制台重新申请(不能复用 msext 的 key) + +--- + +### Phase 6: 子游戏容器 + +#### 目标 +大厅 push 子游戏,子游戏返回大厅,`getWebdata` 链路打通。 + +#### 前置 +- Phase 1 完成(大厅可加载) +- Phase 2/3/4/5 不强制,但子游戏页同样需要 19 个共享 handler + +#### 任务清单 + +- [ ] **6.1** `Source/Coordinator/AppCoordinator.swift`:path 节流 + 栈深约束(Design §2.4.3) +- [ ] **6.2** `Source/Containers/SubGameViewController.swift`:继承 WebContainer,注入 SubGameHandlers +- [ ] **6.3** `Source/Bridge/Handlers/SubGameHandlers.swift`:19 共享 + `backgameData` + 3 个视频房间 stub +- [ ] **6.4** `SwitchOverGameData` handler(仅大厅注册):解参 → AppCoordinator.showSubGame → 节流 2s +- [ ] **6.5** `backgameData` handler(仅子游戏注册):停 audio → 发 `.subGameDidReturn` 通知 → coordinator.popSubGame +- [ ] **6.6** 大厅监听 `.subGameDidReturn` → `bridge.call("getWebdata", data)` +- [ ] **6.7** 子游戏 zip 下载 / 解压(H5 端通过 SwitchOverGameData 传 `gamedownloadurl`) + - `Source/Resource/SubGameDownloader.swift`:URLSession + ZIPFoundation + - 缓存策略:`Library/Caches/{Gamedirectory}/` 已存在则跳过 + +#### 验收 + +- 契约 §10 B 节 `SwitchOverGameData` → push 子游戏 → 子游戏调 `backgameData` → 回大厅 → 大厅收 `getWebdata` +- 栈深永远 ≤ 3(Lobby + SubGame + Overlay) + +#### 风险 + +- 子游戏 zip 下载失败兜底:超时 / 网络断 / hash 校验失败 → 弹错误页且 pop 回大厅 +- 大厅 + 子游戏并存时双发桥事件(已由 Phase 2 ExternalSubscriptions 解决) + +--- + +### Phase 7: 弹层(Overlay) + +#### 目标 +H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settings.*` 三接口操作,关闭后大厅收 `getWebdata`。 + +#### 前置 +- Phase 1 完成 + +#### 任务清单 + +- [ ] **7.1** `Source/Containers/OverlayViewController.swift`:继承 WebContainer,但 WKWebsiteDataStore 用 `.nonPersistent()` 隔离 +- [ ] **7.2** `Source/WebView/OverlayBridge.swift` polyfill JS:`window.settings = {backgameData, browser, finishweb}` → `webkit.messageHandlers.*` +- [ ] **7.3** `WKScriptMessageHandler` 注册三 handler:`overlayBackgameData` / `overlayBrowser` / `overlayFinishweb` +- [ ] **7.4** `OpenurlTitleData` handler(大厅 + 子游戏都注册):解参(注意 `"title "` 末尾空格契约) → AppCoordinator.showOverlay → 3 秒节流 +- [ ] **7.5** Overlay backgameData → 发 `.subGameDidReturn` → 上层 H5 收 `getWebdata` +- [ ] **7.6** Overlay `finishweb` → coordinator.popOverlay +- [ ] **7.7** Overlay `browser` → 系统 Safari 打开 + +#### 验收 + +- 契约 §10 B 节 `OpenurlTitleData` 完整链路 + `settings.finishweb()` 关闭 + `settings.backgameData(data)` 回传 + +#### 风险 + +- 弹层第三方外链 Cookie 不污染主业务态(`.nonPersistent()` 已隔离) +- 3 秒节流时间戳由谁持有(`OpenUrlHandler` 内部 actor 状态) + +--- + +### Phase 8: 视频房间 stub + +#### 目标 +注册 3 个视频房间 handler 的 stub 实现 + 子游戏独有 3 个反向 callback,以满足契约边界 —— 即使业务暂不上视频,H5 也不会报错。 + +#### 前置 +- Phase 6 完成(子游戏容器) + +#### 任务清单 + +- [ ] **8.1** `Source/VideoRoom/VideoRoom.swift` 协议 +- [ ] **8.2** `Source/VideoRoom/NoopVideoRoom.swift` 实现(log + 不动) +- [ ] **8.3** `SubGameHandlers` 注入 NoopVideoRoom,注册: + - `createRoom` stub → `cb("createRoom")` + - `getVideoinfo` stub → `cb("getVideoinfo")` + - `exitRoom` stub → `cb("exitRoom")` +- [ ] **8.4** `Source/Telephony/CallCenterMonitor.swift`:CXCallObserver 监听通话状态 + - 来电 → `bridge.call("phonestate", "2")` + - 挂断 → `bridge.call("phonestate", "0")` +- [ ] **8.5** `RecordUploader.onUploaded` 已在 Phase 3,需要在 SubGameHandlers 里**额外**触发 `recordSuccess({fileUrl, fileName, fileKey})`(契约 §3.2 表 [15]) +- [ ] **8.6** `AgoraVideoRoom.swift` 留蓝图骨架(`#if AGORA_ENABLED` 包裹,默认 OFF) + +#### 验收 + +- 契约 §10 D 节中: + - `createRoom` / `exitRoom` H5 调用不报 "bridge not found" + - 子游戏接电话 → H5 收 `phonestate("2")` / `phonestate("0")` + - 子游戏录音上传 → H5 收 `getaudiourl` **AND** `recordSuccess`(双 callback) + +#### 风险 + +- `CXCallObserver` 在模拟器无效,必须真机测 +- 视频房间真要启用时再走 §8.6 的 AgoraVideoRoom 实现路径 + +--- + +### Phase 9: SDK 真实化 + 监控接入 + +#### 目标 +把所有 Noop / stub 替换成真实 SDK;接入 Sentry 崩溃监控 + 极光统计。 + +#### 前置 +- Phase 1–8 完成 + +#### 任务清单 + +- [ ] **9.1** Sentry-Cocoa SPM 依赖 + `Source/Analytics/SentryCrashReporter.swift` + - 编译开关 `SENTRY_ENABLED`(默认 ON)+ `NoopCrashReporter` fallback + - DSN 通过 xcconfig 注入,不入 git +- [ ] **9.2** `Source/Bridge/Handlers/H5ErrorRelay.swift`:注入 `window.onerror` / `unhandledrejection` polyfill + `reportH5Error` 桥 handler(Design §11.4,新增 handler 不破坏契约) +- [ ] **9.3** 极光 SDK(Vendor)+ `Source/Analytics/JAnalytics.swift` 启动期注册 +- [ ] **9.4** 闲聊:保持 NoopSharePlatform(业务方未启用闲聊分享,stub 已满足契约) +- [ ] **9.5** Agora:保持 NoopVideoRoom + 编译开关 OFF +- [ ] **9.6** Bugly:不集成(Sentry 已覆盖) + +#### 验收 + +- 强制崩溃 → Sentry 后台收到 issue +- H5 内 `throw new Error()` → Sentry 看到 H5 错误 + `container_role` 标签 +- 极光后台看到设备激活 + +#### 风险 + +- Sentry 账号必须 ≥ 2 人持有访问凭证(避免重蹈 Bugly 覆辙) +- 极光 / Sentry / 自家后台埋点数据重叠:明确每个监控的"唯一职责"(Sentry=崩溃,极光=用户激活,自家=业务事件) + +--- + +### Phase 10: 多渠道 + 真机回归 + 灰度 + +#### 目标 +完成打包脚本 + CI 多渠道流水线 + 真机覆盖回归 + 内部灰度切流。 + +#### 前置 +- Phase 1–9 完成 + +#### 任务清单 + +##### 10.A 多渠道打包 + +- [ ] **10.1** `Scripts/inject_channel.sh`:按 Design §7.4 实现 +- [ ] **10.2** `Scripts/archive.sh`:xcodebuild archive + exportArchive 流水线 +- [ ] **10.3** `Scripts/release.sh`:循环渠道列表,逐个出包到 `dist/` +- [ ] **10.4** 渠道环境文件 `channels/*.env` 模板 + +##### 10.B CI + +- [ ] **10.5** CI 加 archive smoke test(至少一个渠道能出 ipa) +- [ ] **10.6** 契约测试纳入 CI 必跑 + +##### 10.C 真机回归 + +- [ ] **10.7** 按契约 §10 26 项清单逐项真机过: + - A 节 启动(4 项) + - B 节 桥接(12 项) + - C 节 系统事件(4 项) + - D 节 视频房间(4 项,stub 模式下只验 phonestate / recordSuccess / createRoom 不报错) + - E 节 回归(3 项 + 截图分享) +- [ ] **10.8** 覆盖设备矩阵:iPhone(最新 + 一台老款)/ iPad / iOS 15.6 / iOS 26.x + +##### 10.D 灰度 + +- [ ] **10.9** 与 msext 旧外壳并存(Bundle ID 已分离),内部 50 人手动覆盖装 +- [ ] **10.10** 监控 Sentry crash-free rate ≥ 99.5%、用户反馈 +- [ ] **10.11** 50 → 100 → 全量切流(按渠道,小渠道先切) + +#### 验收 + +- 契约 §10 全 26 项真机通过 +- 全量灰度内零回退要求 +- CI 多渠道出包稳定 + +#### 风险 + +- 真机覆盖不全:单人测试时间有限,必要时项目方协调测试人力 +- 灰度期间发现 P0 → 必须能 24h 内热修(设计 Sentry breadcrumb + 远程开关) + +--- + +## 6. 横向工作流(贯穿全程) + +### 6.1 SDK 集成节奏 + +| 集成方式 | SDK | 集成 Phase | +|---------|-----|----------| +| **SPM 优先** | ZIPFoundation / Sentry / 七牛 v8+ | Phase 1 / 9 / 3 | +| **CocoaPods** | 高德 AMap | Phase 5 | +| **Vendor `.framework`** | 微信 / QQ / 闲聊(暂不) / Agora(暂不) / JAnalytics | Phase 4 / 9 | +| **Vendor `.a`** | opencore-amr | Phase 3 | + +每集成一个 SDK 立即提交一个独立 commit(CLAUDE.md "及时提交" 规则)。 + +### 6.2 测试节奏 + +- **Phase 1 起每 Phase 必须有契约测试用例**纳入 `ylgamehallContractTests` +- 单元测试覆盖率目标:BridgeCore ≥ 90%、ResourceKit ≥ 85%、其余 Kit ≥ 70%(Design §12.1) +- UI 集成测在 Phase 10 完成 26 项验收清单的 XCUI 自动化 + +### 6.3 监控接入 + +- Sentry 默认 ON 但可关(编译开关 `SENTRY_ENABLED`) +- DSN / 七牛 token 接口 URL / 微信 AppID 等敏感配置走 xcconfig + `.gitignore` +- H5 错误回流通道(`reportH5Error` 桥 handler)在 Phase 9 接入 + +### 6.4 文档同步 + +- 每个 Phase 结束更新本文档 §8 进度追踪 +- 桥接 handler 任何变动必须同步更新 Contract(Design §18.6) +- Phase 完成时如发现 Contract 描述与实测不符 → 优先修 Contract(事实为准) + +--- + +## 7. 真实风险与缓解(项目独有) + +| 风险 | 触发条件 | 影响 | 缓解 | +|------|---------|------|------| +| 已就位的 `gamehall.zip` 是 2023-12 旧版,与现网 H5 有契约漂移 | Phase 1–9 联调 | 联调期发现 handler 名 / 字段被旧版掩盖、真上线时反而暴露 | 上线前阶段(Phase 10 灰度前)强制由 H5 团队提供最新版替换并复跑契约 §10 全部 26 项;联调期发现的契约疑点立即对照现网 msext 与 H5 团队对齐 | +| 微信 SDK 审核失败 / 改 Bundle ID 引起 Universal Links 失效 | Phase 4 真机调试 | 微信回调收不到 | Bundle ID 注册微信开放平台 + 配置 Universal Links 流程独立做一次(项目方协调) | +| 后台 `/wechat/login` 接口延期 | Phase 4 上线时 | secret 仍驻 IPA | 接受临时 fallback(标 TODO),上线前必须切到后台 | +| 高德 APIKey 与 Bundle ID 绑定失败 | Phase 5 真机定位 | 定位全失败 | 项目方控制台重新申请 key(本项目独立 key) | +| Swift 6 严格并发产生大量 warning/error | Phase 2 起 | 进度延迟 | 已启用 Approachable Concurrency(渐进模式),逐 Phase 修复,不一开始就 Complete 模式 | +| WKWebView 内 `evaluateJavaScript` 在 Liquid Glass 时代 API 变化 | iOS 26+ 真机 | 桥消息派发失败 | DocumentationSearch 优先查最新 API,不假设知识截止前的写法 | +| 真机回归人力不足(单人 + AI) | Phase 10 | 漏测 → 上线翻车 | 把 26 项验收清单写成 XCUI 自动化(可重复跑),手测只覆盖体验类 | +| Sentry 账号长期无人维护变 Bugly 翻版 | Phase 9 上线后 | 崩溃监控形同虚设 | 接入时同步把账号责任写入团队 onboarding,季度回顾会上检查存活 | +| 灰度期发现 P0 但无热修通道 | Phase 10 灰度 | 用户体验受损 | 设计期就要预留远程配置开关(如:`Sentry breadcrumbs` 中查 root cause + 一个 plist 控制的 feature flag)| + +--- + +## 8. 进度追踪 + +> 每完成一项就把 `[ ]` 改 `[x]`,并在 commit message 里附 Phase X.Y 编号方便 git log 检索。 + +### Phase 0 工程化基线 +- [x] 0.0 M0 基础配置(横屏 / Swift 6 / 代码启动) +- [x] 0.0.1 `.gitignore` + xcuserdata 清理 +- [ ] 0.1 SwiftLint +- [ ] 0.2 单测 target +- [ ] 0.3 契约测试 target +- [ ] 0.4 UI 测试 target +- [ ] 0.5 CI 流水线 +- [ ] 0.6 SPM 包目录决策(暂单 target) + +### Phase 1 最小垂直闭环 +- [ ] 1.1 ChannelInjection 目录占位 +- [ ] 1.2 BundleConfig +- [ ] 1.3 SandboxPaths +- [ ] 1.4 ZIPFoundation SPM +- [ ] 1.5 ResourceUnzipper +- [ ] 1.6 BridgeProtocol +- [ ] 1.7 BridgeBus +- [ ] 1.8 WVJB JS 协议 +- [ ] 1.9 BridgedWebView +- [ ] 1.10 WebContainerViewController(16:9 letterbox) +- [ ] 1.11 VibratorHandler +- [ ] 1.12 SceneDelegate → LobbyViewController +- [ ] 1.13 Demo H5 联调 + +### Phase 2 大厅简单 handler + 反向 callback +- [ ] 2.1 剪贴板 +- [ ] 2.2 振动扩展 +- [ ] 2.3 摇一摇开关 +- [ ] 2.4 voicePlaying +- [ ] 2.5 getphoneInfo +- [ ] 2.6 browser +- [ ] 2.7 opensaoma 空注册 +- [ ] 2.8 DeviceInfo +- [ ] 2.9 BatteryMonitor +- [ ] 2.10 NetworkMonitor +- [ ] 2.11 AppLifecycleObserver +- [ ] 2.12 ShakeDetector +- [ ] 2.13 ExternalSubscriptions + +### Phase 3 音频体系 +- [ ] 3.1 AudioPlayer +- [ ] 3.2 本地音频真机验证 +- [ ] 3.3 opencore-amr Vendor 接入 +- [ ] 3.4 VoiceCoder Swift wrapper +- [ ] 3.5 AudioRecorder + 麦克风权限 +- [ ] 3.6 七牛 SPM + QiniuUploader +- [ ] 3.7 prepareaudio handler +- [ ] 3.8 mediaTypeAudio handler + +### Phase 4 分享 + 登录 +- [ ] 4.1 微信 SDK Vendor + URL Scheme +- [ ] 4.2 QQ SDK Vendor +- [ ] 4.3 WeChatSDK registerFull +- [ ] 4.4 WeChatManager(state map + FIFO) +- [ ] 4.5 SceneDelegate openURL 顺序 +- [ ] 4.6 WeChatAuth(后台中转或 fallback) +- [ ] 4.7 accreditlogin → sharelogin +- [ ] 4.8 SharePlatform 协议 +- [ ] 4.9 ShareCenter 9 种组合 +- [ ] 4.10 friendsShare... handler +- [ ] 4.11 截图分享 +- [ ] 4.12 远程图片分享 + +### Phase 5 定位 +- [ ] 5.1 AMap Pod +- [ ] 5.2 定位权限文案 +- [ ] 5.3 AMapWrapper 启动注册 +- [ ] 5.4 LocationService +- [ ] 5.5 startlocation handler(latitude/longitude string,province 小写) +- [ ] 5.6 失败回包 errorCode 12 + +### Phase 6 子游戏 +- [ ] 6.1 AppCoordinator 栈深节流 +- [ ] 6.2 SubGameViewController +- [ ] 6.3 SubGameHandlers +- [ ] 6.4 SwitchOverGameData +- [ ] 6.5 backgameData +- [ ] 6.6 getWebdata 通知链 +- [ ] 6.7 SubGameDownloader + +### Phase 7 弹层 +- [ ] 7.1 OverlayViewController + 私有 dataStore +- [ ] 7.2 window.settings polyfill +- [ ] 7.3 3 个 WKScriptMessageHandler +- [ ] 7.4 OpenurlTitleData handler("title " 末尾空格) +- [ ] 7.5 Overlay backgameData +- [ ] 7.6 Overlay finishweb +- [ ] 7.7 Overlay browser + +### Phase 8 视频房间 stub +- [ ] 8.1 VideoRoom 协议 +- [ ] 8.2 NoopVideoRoom +- [ ] 8.3 3 个 stub handler +- [ ] 8.4 CallCenterMonitor → phonestate +- [ ] 8.5 recordSuccess 双 callback +- [ ] 8.6 AgoraVideoRoom 蓝图(#if AGORA_ENABLED OFF) + +### Phase 9 SDK 真实化 + 监控 +- [ ] 9.1 Sentry SPM + CrashReporter +- [ ] 9.2 H5ErrorRelay +- [ ] 9.3 极光 SDK +- [ ] 9.4 闲聊保持 Noop +- [ ] 9.5 Agora 编译开关 OFF +- [ ] 9.6 不集成 Bugly + +### Phase 10 多渠道 + 回归 + 灰度 +- [ ] 10.1 inject_channel.sh +- [ ] 10.2 archive.sh +- [ ] 10.3 release.sh +- [ ] 10.4 渠道环境文件 +- [ ] 10.5 CI archive smoke +- [ ] 10.6 契约测试入 CI +- [ ] 10.7 26 项验收清单真机过 +- [ ] 10.8 设备矩阵覆盖 +- [ ] 10.9 内部 50 人灰度 +- [ ] 10.10 Sentry 监控 +- [ ] 10.11 全量切流 + +--- + +## 9. 决策记录(ADR-lite) + +> 关键技术 / 工程决策的简短记录,便于回溯"为什么这样选"。新增决策追加到本节末尾。 + +### ADR-001:单 target 起步,暂不切 SPM(2026-06-21) +- **决策**:Phase 0–2 阶段保持单 target,按目录组织(`Source/Bridge`、`Source/Resource` 等);待代码量 > 5K 行(约 Phase 3 完成时)再评估切 SPM +- **理由**:早期 SPM 切分成本(target 间循环依赖排查、增量编译配置、测试 target 接入)会拖慢核心垂直闭环验证;Design §2.2 也注明"目标稳态 8-10 个 target,以日常开发体验为先" +- **回滚条件**:单 target 编译时间 > 30s 时切 SPM + +### ADR-002:使用已就位的 2023-12 旧版 gamehall.zip 起步(2026-06-21,原方案"H5 demo 先行"已作废) +- **背景**:仓库根 `Resources/gamehall.zip` 已存在(旧版,2023-12,11.5 MB),无需等 H5 团队最新版即可启动 Phase 1 +- **决策**:Phase 1 直接用该 zip 跑通"渠道注入 → 解压 → 加载 H5 → 桥消息"全链路;旧 H5 业务代码与新版的契约差异不影响"桥本身是否工作"的验证 +- **理由**: + - 桥接口名 / 参数字段名 / JS 协议属于契约范畴,按 Contract 1:1 实现即可,不依赖 H5 业务代码新旧 + - 提早用真 H5 跑通比 demo H5 更能暴露真实问题(如 WVJB 握手时序、JS 注入文件路径等) +- **风险**:联调期遇到的契约疑点可能来自旧版 H5 已废弃接口,需对照现网 msext + H5 团队双确认 +- **切换条件**:Phase 10 灰度前必须由 H5 团队提供最新版替换并复跑契约 §10 全部 26 项 + +### ADR-004:Resources/ 目录位于仓库根(非 Xcode 工程内)(2026-06-21) +- **决策**:项目方提供的所有资源(`gamehall.zip` / 原生 Asset Catalog / 散落 png / mp3 / 后续渠道注入目录)统一放仓库根 `Resources/`,由打包脚本通过 Build Phase 引入;**不**直接放进 Xcode 工程的 `ylgamehall/` 源码目录 +- **理由**: + - 与源码物理隔离,git diff 更清晰(资源变动 vs 代码变动不混淆) + - Build Phase 引入便于按渠道动态切换内容(同一份源码 × 多个 Resources 配置) + - 符合 Design §2.2 的 SPM 包结构规划(`Resources/` 与 `Sources/` 并列) +- **现状**:`Resources/` 已含 `gamehall.zip` / `Images.xcassets/` / `Res/`;11 个渠道注入目录预留位 `Resources/ChannelInjection/` 待 Phase 1 由 `Scripts/inject_channel.sh` 生成 + +### ADR-003:微信登录走后台中转 vs 客户端 fallback(2026-06-21) +- **决策**:默认走后台 `/wechat/login` 中转;后台未就绪时临时客户端直拼,但代码标 `// FIXME` 且**禁止以 fallback 状态上线** +- **理由**:客户端 secret 一旦进 IPA 永久泄露 + AppSecret 不可重置(Contract §0.4 / Design §17);上线前必须闭环 +- **复盘节点**:Phase 4 完成时检查后台接口状态 + +--- + +文档完成日期:2026-06-21 +最后更新:2026-06-21(初版) diff --git a/docs/H5-Native-Implementation-Design.md b/docs/H5-Native-Implementation-Design.md index 0b6fb79..75dc13a 100644 --- a/docs/H5-Native-Implementation-Design.md +++ b/docs/H5-Native-Implementation-Design.md @@ -1046,6 +1046,34 @@ public actor ConfigService { ## 7. 资源 & 渠道注入 +### 7.0 仓库根 `Resources/` 目录现状(2026-06-21) + +新外壳运行所需的项目方资源统一存放在仓库根 `Resources/`(**不**在 Xcode 工程内的 `ylgamehall/` 源码目录里)。这与 §2.2 的 SPM 包结构里 `Resources/` 是同一份物理目录,后续打包脚本通过 Build Phase 引用。 + +当前实际内容(可直接展开 Phase 1 开发): + +``` +Resources/ +├── gamehall.zip ← 大厅 H5 资源包 (≈ 11 MB, 2023-12 旧版) +│ 启动时 ResourceUnzipper 解压到 +│ Library/Caches/{gamedir}/ +│ ⚠️ 上线前必须由 H5 团队提供最新版替换 +├── Images.xcassets/ ← 原生 Asset Catalog (AppIcon / +│ LaunchImage / 微信 / QQ / 抖音 +│ 分享平台图标) +└── Res/ ← 散落原生资源 (未走 Asset Catalog 的旧文件): + ├── sharelogo.png ← 分享缩略图 (友圈/微信链接分享用) + ├── shake_sound_male.mp3 ← 摇一摇音效 (SwitchShake 开关控制) + ├── BackBT.png ← msext 兼容图,新外壳是否引用待 Phase 1 验证 + ├── Sistem_back.png ← 同上 + ├── Icon180.png ← 同上 + └── Default-568h@2x~iphone.png ← 同上 +``` + +> **Phase 1 含义**:`gamehall.zip` 实物已就位,虽是旧版但足以跑通"渠道注入 → 解压 → 加载 H5 → 桥消息"全链路,不必等 H5 团队最新版才开始。版本差异不影响契约边界(handler 名 / 参数 / 数据结构)。 + +后续 11 个**渠道注入目录**(`qiniudomain` / `gameid` / `channel` / `gamedir` / `gamestart` / `gameconfig` / `market` / `agent` / `appversion` / `other` / `appleconfig`)将由 `Scripts/inject_channel.sh` 在打包前于 `Resources/ChannelInjection/` 下动态生成,运行时通过 `BundleConfig.readInjected(_:)` 扫描 Bundle 子目录读取(§7.2)。 + ### 7.1 SandboxPaths 集中管理 ```swift