diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index e83931175..c177f2354 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -2672,8 +2672,12 @@ for (const snippet of [ 'reports iOS runtime with host version, bridge version and iOS capabilities', 'reports Android runtime without iOS-only app badge capability', 'wraps runtime metadata in the HostBridge response shape', + 'HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES', + 'HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES', 'hostVersion: MOBILE_SHELL_HOST_VERSION', 'bridgeVersion: HOST_BRIDGE_VERSION', + 'expect(runtime.capabilities).toBe(HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES)', + 'expect(runtime.capabilities).toBe(HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES)', "not.toContain('app.setBadgeCount')", "expect(getMobileRuntimePlatform()).toBe('android')", ]) { diff --git a/apps/mobile-shell/src/host-bridge/runtime.test.ts b/apps/mobile-shell/src/host-bridge/runtime.test.ts index 8b093ef6e..62d909ba1 100644 --- a/apps/mobile-shell/src/host-bridge/runtime.test.ts +++ b/apps/mobile-shell/src/host-bridge/runtime.test.ts @@ -2,6 +2,8 @@ import { Platform } from 'react-native'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { + HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES, + HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES, HOST_BRIDGE_PROTOCOL, HOST_BRIDGE_VERSION, type HostBridgeRequest, @@ -50,15 +52,7 @@ describe('mobile HostBridge runtime helper', () => { bridgeVersion: HOST_BRIDGE_VERSION, }), ); - expect(runtime.capabilities).toEqual( - expect.arrayContaining([ - 'host.getRuntime', - 'appearance.getColorScheme', - 'network.status', - 'app.setBadgeCount', - 'file.captureImage', - ]), - ); + expect(runtime.capabilities).toBe(HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES); }); test('reports Android runtime without iOS-only app badge capability', () => { @@ -75,8 +69,7 @@ describe('mobile HostBridge runtime helper', () => { bridgeVersion: HOST_BRIDGE_VERSION, }), ); - expect(runtime.capabilities).toContain('host.getRuntime'); - expect(runtime.capabilities).toContain('file.captureImage'); + expect(runtime.capabilities).toBe(HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES); expect(runtime.capabilities).not.toContain('app.setBadgeCount'); }); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c1e1c968e..ff7a95c05 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -171,6 +171,7 @@ - 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod` 与 `normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。Expo 壳捕获原生异常时只透传共享 `HostBridgeError.code` 白名单内且 `message` 为字符串的协议错误,Tauri 壳的 `failed(...)` 出口也必须先校验同一错误码白名单;未知原生错误对象或非法错误码统一归一为 `host_error` 和固定失败文案,不把 native 私有字段、任意错误码或非字符串 message 回传给 H5。 - 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 移动壳协议 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 43644f4da..2c2bada2c 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -495,6 +495,8 @@ GameBridge 禁止: 2026-06-20 追加:Tauri 桌面壳 HostBridge replay 的内部锁、等待和缓存状态异常只写入桌面壳 stderr 观测日志,H5 侧统一收到 `host_error` 与固定文案 `desktop host bridge request failed`;`HostBridgeReplayState::reserve(...)` 返回 `Result`,`complete(...)` / `wait_for_response(...)` 也不得用 `expect` 让宿主进程 panic。桌面配置检查会反查 replay 失败日志、稳定错误响应和 poison lock 单测,避免后续把内部 mutex / condvar 细节或 Rust panic 泄露到 H5 调用链。 +2026-06-20 追加:Expo 移动壳 `host.getRuntime` 回包里的 `capabilities` 必须直接等于共享契约中的 `HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES` 或 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES`,且使用同一个已归一的平台值选择 profile;移动壳 runtime 单测和配置检查会反查精确 profile 断言,避免 H5 实际消费的能力回包与入口 URL 或共享 profile 分叉。 + 2026-06-18 追加:HostBridge method 白名单进入跨壳门禁。`packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_METHODS` 是唯一协议来源;Expo 壳的 HostBridge 分发 case 不得处理共享契约外 method,Tauri 壳 Rust `HOST_BRIDGE_METHODS` 必须与共享契约逐项一致。两端配置检查会在 `npm run check:native-shells` 中拒绝 method 白名单漂移,新增宿主能力必须先更新共享契约,再落壳实现。 2026-06-19 追加:HostBridge event 白名单进入跨壳门禁。`packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_EVENTS` 是宿主注入事件名的唯一来源,当前只包含 `app.lifecycle`、`network.statusChanged`、`navigation.canGoBack` 和 `file.imageDropped`;事件名必须存在于 capability 白名单,各宿主壳只声明自身真实发射的事件 capability。Expo 壳事件注入函数使用 `HostBridgeEventName`,Tauri 壳 `shell/events.rs` 镜像同一清单并在脚本生成前拒绝未知事件,H5 `nativeAppHostBridge` 只分发 `isHostBridgeEventName()` 认可的事件。H5 `subscribeHostAppLifecycle()`、`subscribeHostNetworkStatusChange()`、`subscribeHostNavigationCanGoBack()` 和 `subscribeHostImageDrop()` 必须同时校验 `host.events` 与对应事件 capability,缺任一能力时不绑定事件监听;根级 `npm run check:native-shells` 会反查这四个订阅 facade 都通过 `canUseNativeHostEventCapability(...)` 进入同一双能力门控,并拒绝新增事件后漏补 H5 facade 覆盖。