diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 34fe003bc..176312b7c 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -43,6 +43,7 @@ - 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。 - 2026-06-20 H5 原生能力门控收口:除 `host.getRuntime` 为了支持旧入口 URL 缺少 capability 时回读真实 runtime 可保留特殊判断外,H5 facade 中所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控,不得在业务能力函数内直接读取 `runtime.hostCapabilities.includes(...)`,避免各能力复制门控规则;根级 `npm run check:native-shells` 会从共享 `HOST_BRIDGE_METHODS` 自动派生需门控的 request capability 清单,新增 method 时必须同步补齐 H5 facade 门控。 - 2026-06-20 移动壳未声明 method 覆盖:Expo 移动壳对未进入 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 的共享 request method 必须由测试从 `HOST_BRIDGE_METHODS` 自动派生覆盖,并在请求到达时返回明确 `unsupported_method`;平台差异能力如 Android 不声明的 `app.setBadgeCount` 保持独立 `unsupported_capability` 语义,不混入未声明 method 清单。 +- 2026-06-20 壳文档能力清单反查:`npm run check:native-shells` 会同时反查移动壳 / 桌面壳主状态段落能力清单和后续完整能力清单;主状态段落按集合检查,允许按叙述需要调整顺序,但不得漏写或多写 capability,完整能力清单继续按共享 profile 顺序检查。 - 2026-06-18 宿主 runtime 回读:主 App 启动时会通过真实 `host.getRuntime` 回读 Expo / Tauri runtime 并缓存过滤后的能力清单,能力来源为 URL `hostCapabilities` 与宿主真实回包的并集;裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时也能启用真实声明能力,但仍不会仅凭 `native_app` 或 transport 存在推断能力可用。该回读请求的短超时由共享契约 `HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS` 声明,H5 facade 不得本地重声明。 - 2026-06-18 壳能力防漂移:`npm run mobile-shell:typecheck` 与 `npm run desktop-shell:typecheck` 会校验 Expo / Tauri 壳声明的 capability 均来自共享 HostBridge 白名单,并校验壳 runtime 回包、H5 URL `hostCapabilities` 和实现分支保持一致;微信小程序 `WECHAT_HOST_CAPABILITIES` 由 `miniprogram/host-bridge/protocol.test.js` 和根级 `npm run check:native-shells` 反查共享 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES`。新增能力必须先更新契约和真实壳实现,再通过这些检查。 - 2026-06-19 宿主上下文 query 契约收口:`packages/shared/src/contracts/hostBridge.ts` 是宿主上下文 query 字段和值的唯一 TypeScript 来源;`HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY` 覆盖 H5 runtime parser 字段,`HOST_BRIDGE_NATIVE_APP_QUERY_KEY` / `HOST_BRIDGE_NATIVE_APP_QUERY_KEYS` / `HOST_BRIDGE_NATIVE_APP_QUERY` 固定 Expo / Tauri 原生壳入口 query,`HOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY` 固定微信 WebView 来源标记,`HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS` 固定 H5 页面内导航需要保留的宿主字段。Expo 壳直接引用共享常量,Tauri Rust 和微信 CommonJS 镜像由 `npm run check:native-shells` / 单壳配置检查反查;微信请求头必须从 `WEB_VIEW_SOURCE_QUERY` 读取 `clientType` / `clientRuntime`,不得另起常量。 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index 32eb2f20c..baace57ba 100644 --- a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md +++ b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md @@ -92,7 +92,7 @@ Tauri 桌面壳的文件能力边界统一在 `apps/desktop-shell/src-tauri/src/ 2. `authService` 保留原导出,但内部委托 HostBridge,避免一次性改动 AuthGate。 3. 分享弹窗、分享目标同步、九宫切图、微信小程序支付和订阅授权改用 HostBridge 通用接口;旧微信命名服务只作为兼容导出。 4. 后续新增 `native_app` adapter 时只补桥接实现和测试,业务层不新增平台分叉;主 App 启动会触发一次 `host.getRuntime` 回读并订阅能力变化,避免裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时长期隐藏真实可用能力。 -5. 每次新增或调整 native capability、HostBridge event 或宿主上下文 query 后,必须先更新 `packages/shared/src/contracts/hostBridge.ts` 中对应微信 / Expo / Tauri capability profile、事件白名单和 query 契约,再运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、微信小程序页面路由与 H5 常量反查、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。桌面壳未声明的共享 request method 必须自动派生为 `unsupported_method` 覆盖清单,当前包括 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`,不得伪造成功;Expo 移动壳未声明的共享 request method 必须由 `HOST_BRIDGE_METHODS - HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 自动派生测试覆盖,确保未接 SDK / 渠道的 method 返回明确 `unsupported_method`;H5 facade 除 `host.getRuntime` 真实回读外,所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控,根级门禁会从共享 `HOST_BRIDGE_METHODS` 自动派生需检查清单;排查单端问题时再单独运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。 +5. 每次新增或调整 native capability、HostBridge event 或宿主上下文 query 后,必须先更新 `packages/shared/src/contracts/hostBridge.ts` 中对应微信 / Expo / Tauri capability profile、事件白名单和 query 契约,再运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、微信小程序页面路由与 H5 常量反查、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。移动壳和桌面壳文档中的主状态段落能力清单与完整能力清单都必须反查共享 capability profile,避免同一文档内部漂移;桌面壳未声明的共享 request method 必须自动派生为 `unsupported_method` 覆盖清单,当前包括 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`,不得伪造成功;Expo 移动壳未声明的共享 request method 必须由 `HOST_BRIDGE_METHODS - HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 自动派生测试覆盖,确保未接 SDK / 渠道的 method 返回明确 `unsupported_method`;H5 facade 除 `host.getRuntime` 真实回读外,所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控,根级门禁会从共享 `HOST_BRIDGE_METHODS` 自动派生需检查清单;排查单端问题时再单独运行 `npm run mobile-shell:typecheck`、`npm run mobile-shell:test`、`npm run mobile-shell:config`、`npm run mobile-shell:export`、`npm run desktop-shell:typecheck`、`npm run desktop-shell:test` 或 `npm run desktop-shell:build -- --no-bundle`。 ## 验收 diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index 9aaaddb4c..2552b7655 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -387,7 +387,9 @@ const documentedShellLayerGroups = [ ]; const capabilityListMarkers = { desktop: '桌面壳当前真实能力完整清单为', + desktopCurrentState: '当前真实能力为', mobile: '移动壳当前通用真实能力完整清单为', + mobileCurrentState: '首轮真实能力包括', mobileIosExtra: '移动壳 iOS 额外真实能力为', wechat: '微信小程序壳当前真实能力完整清单为', }; @@ -762,6 +764,12 @@ function assertSameList(actual, expected, label) { } } +function assertSameSet(actual, expected, label) { + const sortedActual = [...actual].sort(); + const sortedExpected = [...expected].sort(); + assertSameList(sortedActual, sortedExpected, label); +} + function readDirectoryEntryList(directory, label) { const entries = fs .readdirSync(directory, { withFileTypes: true }) @@ -1260,12 +1268,16 @@ function assertH5NativeAppTransportTimeoutBoundaries() { } function extractDocumentCapabilityList(source, marker) { + return extractDocumentCapabilityListBefore(source, marker, '。'); +} + +function extractDocumentCapabilityListBefore(source, marker, terminator) { const markerIndex = source.indexOf(marker); if (markerIndex === -1) { throw new Error(`native shell plan missing ${marker}`); } - const sentenceEnd = source.indexOf('。', markerIndex); + const sentenceEnd = source.indexOf(terminator, markerIndex); const sentence = source.slice( markerIndex, sentenceEnd === -1 ? undefined : sentenceEnd, @@ -1443,6 +1455,15 @@ function assertNativeShellCapabilityPlan() { mobileCapabilities, 'mobile shell documented common capabilities', ); + assertSameSet( + extractDocumentCapabilityListBefore( + planSource, + capabilityListMarkers.mobileCurrentState, + ';', + ).filter((capability) => capability !== 'Android 返回键回退'), + mobileCapabilities, + 'mobile shell current state documented capabilities', + ); assertSameList( extractDocumentCapabilityList(planSource, capabilityListMarkers.mobileIosExtra), iosExtraCapabilities, @@ -1453,6 +1474,15 @@ function assertNativeShellCapabilityPlan() { desktopCapabilities, 'desktop shell documented capabilities', ); + assertSameSet( + extractDocumentCapabilityListBefore( + planSource, + capabilityListMarkers.desktopCurrentState, + ';', + ), + desktopCapabilities, + 'desktop shell current state documented capabilities', + ); } function assertExternalUrlProtocolParity() {