diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index c177f2354..add2a6f16 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -8,6 +8,11 @@ const appPath = new URL('../App.tsx', import.meta.url); const appSource = fs.readFileSync(appPath, 'utf8'); const shellAppPath = new URL('../src/shell/ShellApp.tsx', import.meta.url); const shellAppSource = fs.readFileSync(shellAppPath, 'utf8'); +const expoExportSmokePath = new URL( + '../scripts/check-expo-export.mjs', + import.meta.url, +); +const expoExportSmokeSource = fs.readFileSync(expoExportSmokePath, 'utf8'); const shellAppTestPath = new URL('../src/shell/ShellApp.test.tsx', import.meta.url); const shellAppTestSource = fs.readFileSync(shellAppTestPath, 'utf8'); const qrScannerOverlayPath = new URL('../src/shell/QrScannerOverlay.tsx', import.meta.url); @@ -650,6 +655,20 @@ assertSameList( 'mobile shell smoke scripts', ); +for (const snippet of [ + 'const requiredNativeHostContextTokens = [', + "'native_app'", + "'expo_mobile'", + "'hostCapabilities'", + "'hostVersion'", + "'bridgeVersion'", + 'production bundle must include native host context token', +]) { + if (!expoExportSmokeSource.includes(snippet)) { + throw new Error(`mobile shell Expo export smoke must verify host context token ${snippet}`); + } +} + assertNoDevScaffoldTerms( productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)), ); diff --git a/apps/mobile-shell/scripts/check-expo-export.mjs b/apps/mobile-shell/scripts/check-expo-export.mjs index 1ac020a08..f128b8608 100644 --- a/apps/mobile-shell/scripts/check-expo-export.mjs +++ b/apps/mobile-shell/scripts/check-expo-export.mjs @@ -41,6 +41,13 @@ function readHostBridgePublicWebUrl() { } const expectedPublicWebUrl = readHostBridgePublicWebUrl(); +const requiredNativeHostContextTokens = [ + 'native_app', + 'expo_mobile', + 'hostCapabilities', + 'hostVersion', + 'bridgeVersion', +]; function runExpoExport(platform) { const outputDir = `.expo-export-smoke/${platform}`; @@ -123,6 +130,14 @@ function assertBundle(platform, bundlePath) { ); } + for (const token of requiredNativeHostContextTokens) { + if (!bundleSource.includes(token)) { + throw new Error( + `Expo ${platform} production bundle must include native host context token ${token}`, + ); + } + } + for (const blockedPattern of blockedDevelopmentWebUrlPatterns) { if (blockedPattern.test(bundleSource)) { throw new Error( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index ff7a95c05..8781ae582 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -172,6 +172,7 @@ - 2026-06-20 桌面 HostBridge command facade 单测边界:Tauri 唯一 `host_bridge_request` command 必须先通过 `prepare_host_bridge_request(...)` 做 envelope、method 和 request id 校验,再进入 `HostBridgeReplayState` reserve / wait / execute;`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs` 的单测必须覆盖非法 envelope 在 replay 前返回 `invalid_request` 且不会占用对应 request id 的 replay slot,桌面配置检查会反查该测试存在。 - 2026-06-20 桌面 HostBridge replay 内部失败边界:Tauri `HostBridgeReplayState` 的 cache lock、slot lock 和 condvar wait 异常不得 panic,也不得把 Rust 内部错误细节回传给 H5;桌面壳只写 `desktop host bridge replay failed for ...` stderr 观测日志,并统一返回 `host_error: desktop host bridge request failed`。桌面配置检查反查 `reserve(...)` 的 `Result` 出口、稳定错误响应和 poison lock 单测。 - 2026-06-20 移动 HostBridge runtime 能力回包边界:Expo `host.getRuntime` 回包里的 `capabilities` 必须直接等于共享契约 `HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES` 或 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES`,并使用与 `platform` 字段一致的归一平台值选择 profile;移动壳 runtime 单测和配置检查反查精确 profile 断言,避免 H5 实际消费的能力回包与入口 URL 能力 query 或共享 profile 分叉。 +- 2026-06-20 移动 production bundle 宿主上下文边界:`apps/mobile-shell/scripts/check-expo-export.mjs` 必须读取 iOS / Android Metro export bundle,确认可分发 bundle 包含共享生产 H5 URL、`native_app`、`expo_mobile`、`hostCapabilities`、`hostVersion` 和 `bridgeVersion`,且不包含本机开发 H5 URL;移动壳配置检查反查 export smoke 的这些 token,避免生产 bundle 丢失宿主上下文或混入本机入口。 - 2026-06-20 移动壳协议 helper 单测边界:`apps/mobile-shell/src/host-bridge/protocol.test.ts` 直接覆盖 Expo 移动壳 HostBridge JSON 解析、envelope 和 request id 校验、未知 method 拒绝、ok / failure 响应包装、unsupported / invalid_request 错误构造,以及 native helper 错误归一时只透传共享错误码与字符串 message,不泄露非法错误码、nativeStack 或其它私有字段;根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,避免协议边界只靠完整 bridge 流程间接覆盖。 - 2026-06-20 移动扫码 overlay 单测边界:`apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx` 直接覆盖移动扫码 overlay 的相机权限请求、二维码扫码成功、权限拒绝失败和关闭取消;单端配置检查会反查该组件测试存在,根级 `npm run check:native-shells` 会把该测试文件列入移动 shell 层结构清单,避免扫码 UI 容器只靠 `ShellApp.test.tsx` 的完整 HostBridge 流程间接覆盖。 - 2026-06-18 HostBridge method 白名单跨壳门禁:`packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_METHODS` 是唯一协议来源;Expo 壳 HostBridge 分发不得处理共享契约外 method,Tauri 壳 Rust `HOST_BRIDGE_METHODS` 必须与共享契约逐项一致。新增宿主 method 必须先更新共享契约,再落两端壳实现或明确 unsupported。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 2c2bada2c..72dc150bd 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -451,7 +451,7 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳接入 Tauri deep-link 插件,但不开放插件 JS guest API,也不把 deep-link 命令加入 capability。桌面配置只注册 `genarrative` scheme;Rust 层只接受 `genarrative://open/...`、`genarrative://app/...`、`genarrative://` 和 `https://app.genarrative.world/...`,统一跳到同源 H5 路径并补写 `clientRuntime=native_app`、`hostShell=tauri_desktop`、当前平台、版本与真实 `hostCapabilities`。外域、`http:`、`mailto:`、`javascript:`、`file:` 等来源不进入主 WebView;归一成功后的 `window.navigate(...)` 与主窗口恢复 / 聚焦失败必须记录桌面宿主事件日志,不能静默吞错。 -2026-06-20 追加:移动壳 production bundle smoke 必须读取 Metro 导出的 iOS / Android JS bundle,并确认 bundle 内包含共享 HostBridge 契约中的生产 H5 URL,且不包含 `http://localhost`、`http://127.0.0.1` 或 `http://[::1]` 这类本机开发 H5 入口。开发态仍允许 `EXPO_PUBLIC_GENARRATIVE_WEB_URL` 指向本机 Vite,但可分发包验收不能让本机 URL 混入生产 bundle。 +2026-06-20 追加:移动壳 production bundle smoke 必须读取 Metro 导出的 iOS / Android JS bundle,并确认 bundle 内包含共享 HostBridge 契约中的生产 H5 URL、`native_app` 宿主上下文、`expo_mobile` 壳标识以及 `hostCapabilities` / `hostVersion` / `bridgeVersion` 等启动 query token,且不包含 `http://localhost`、`http://127.0.0.1` 或 `http://[::1]` 这类本机开发 H5 入口。开发态仍允许 `EXPO_PUBLIC_GENARRATIVE_WEB_URL` 指向本机 Vite,但可分发包验收不能让本机 URL 混入生产 bundle,也不能丢掉 H5 进入原生宿主运行态所需的上下文 token。 2026-06-18 追加:桌面壳安装包身份固定为 `world.genarrative.desktop`,产品名为 `Genarrative`,Tauri、Node package 与 Cargo package 版本统一为 `0.1.0`。Release 主窗口只能从打包进二进制的 `index.html` 进入根 `dist` H5 资产,dev URL 只能指向本机 Vite 调试入口;CSP 必须保持 `script-src 'self'`,不得加入 `unsafe-eval`、`tauri:` 或 `file:` 这类扩大桌面攻击面的来源。当前不配置自动更新器,直到存在真实更新端点、签名密钥和发布流程再接入;`apps/desktop-shell/scripts/check-config.mjs` 会校验这些包身份、版本、CSP 和 updater 禁用约束。