From 838bd86ea04e46886ba39a2f818dd885d298b967 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 04:01:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E6=9C=AA=E5=A3=B0=E6=98=8E=E6=96=B9=E6=B3=95=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=B4=BE=E7=94=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面壳 Rust 测试从 method 白名单和 capability 差集派生 unsupported 覆盖 桌面壳配置检查反查 unsupported 测试派生路径 架构文档记录桌面壳未声明方法派生规则 --- apps/desktop-shell/scripts/check-config.mjs | 38 ++++++++++++++----- .../src-tauri/src/host_bridge/dispatch.rs | 28 ++++++++++---- .../shared-memory/decision-log.md | 2 +- ...前端架构】宿主壳能力统一协议-2026-06-17.md | 2 +- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index e8c989627..cccc5087b 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -1683,11 +1683,25 @@ if ( ); } const desktopUnsupportedMethodTestMatch = desktopHostBridgeDispatchSource.match( - /fn unsupported_method_is_explicit\(\)[\s\S]*?for method in \[([^\]]+)\]/, + /fn unsupported_method_is_explicit\(\)[\s\S]*?assert_eq!\(\s*unsupported_methods,\s*vec!\[([^\]]+)\]/, ); if (!desktopUnsupportedMethodTestMatch) { throw new Error('desktop shell unsupported HostBridge method test is missing'); } +for (const snippet of [ + 'use crate::host_bridge::capabilities::capabilities;', + 'use crate::host_bridge::protocol::HOST_BRIDGE_METHODS;', + 'let desktop_capabilities = capabilities();', + 'let unsupported_methods = HOST_BRIDGE_METHODS', + '.filter(|method| !desktop_capabilities.contains(method))', + 'for method in unsupported_methods', +]) { + if (!desktopHostBridgeDispatchSource.includes(snippet)) { + throw new Error( + `desktop shell unsupported HostBridge method test must derive coverage from method/capability diff: ${snippet}`, + ); + } +} assertSameList( [...desktopUnsupportedMethodTestMatch[1].matchAll(/"([^"]+)"/g)].map( (match) => match[1], @@ -1762,22 +1776,26 @@ for (const runtimeSnippet of [ } } +const desktopDispatchRuntimeDelegationSource = [ + extractFunctionBody(desktopHostBridgeDispatchSource, 'resolve_host_bridge_request'), + extractFunctionBody(desktopHostBridgeDispatchSource, 'execute_host_bridge_request'), +].join('\n'); if ( - !desktopHostBridgeDispatchSource.includes( + !desktopDispatchRuntimeDelegationSource.includes( '"host.getRuntime" => desktop_host_bridge_runtime_response(&request)', ) || - desktopHostBridgeDispatchSource.includes('json!(desktop_runtime())') || - desktopHostBridgeDispatchSource.includes(' ok(') || - desktopHostBridgeDispatchSource.includes('HostBridgeRuntime') || - desktopHostBridgeDispatchSource.includes('HOST_BRIDGE_VERSION') || - desktopHostBridgeDispatchSource.includes('capabilities()') || - desktopHostBridgeDispatchSource.includes('desktop_platform()') || - desktopHostBridgeDispatchSource.includes('env!("CARGO_PKG_VERSION")') + desktopDispatchRuntimeDelegationSource.includes('json!(desktop_runtime())') || + desktopDispatchRuntimeDelegationSource.includes(' ok(') || + desktopDispatchRuntimeDelegationSource.includes('HostBridgeRuntime') || + desktopDispatchRuntimeDelegationSource.includes('HOST_BRIDGE_VERSION') || + desktopDispatchRuntimeDelegationSource.includes('capabilities()') || + desktopDispatchRuntimeDelegationSource.includes('desktop_platform()') || + desktopDispatchRuntimeDelegationSource.includes('env!("CARGO_PKG_VERSION")') ) { throw new Error('desktop shell dispatch must delegate host.getRuntime to runtime.rs'); } -if (desktopHostBridgeDispatchSource.includes('json!(HostBridgeRuntime {')) { +if (desktopDispatchRuntimeDelegationSource.includes('json!(HostBridgeRuntime {')) { throw new Error('desktop shell host.getRuntime must be built by runtime.rs'); } diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs b/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs index 57352431b..0c49849ee 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs @@ -74,17 +74,31 @@ pub(super) async fn execute_host_bridge_request( #[cfg(test)] mod tests { use super::*; + use crate::host_bridge::capabilities::capabilities; + use crate::host_bridge::protocol::HOST_BRIDGE_METHODS; use crate::host_bridge::protocol::request; #[test] fn unsupported_method_is_explicit() { - for method in [ - "auth.requestLogin", - "payment.request", - "file.captureImage", - "scanner.scanQrCode", - "haptics.impact", - ] { + let desktop_capabilities = capabilities(); + let unsupported_methods = HOST_BRIDGE_METHODS + .iter() + .copied() + .filter(|method| !desktop_capabilities.contains(method)) + .collect::>(); + + assert_eq!( + unsupported_methods, + vec![ + "auth.requestLogin", + "payment.request", + "file.captureImage", + "scanner.scanQrCode", + "haptics.impact", + ], + ); + + for method in unsupported_methods { let response = resolve_host_bridge_request(request(method)); assert!(!response.ok); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cd94b51d5..aa26ac5a4 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -53,7 +53,7 @@ - 2026-06-18 微信壳桥接层纳入统一验收:`npm run check:native-shells` 还会运行 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的微信壳测试,覆盖 WebView 入口、登录触发、分享目标、支付结果、订阅消息结果和九宫切图行为;三端桥接层文件结构检查只证明目录边界,行为回归必须由同一门禁中的微信壳测试证明。 - 2026-06-19 微信壳路由一致性门禁:`npm run check:native-shells` 必须反查 `miniprogram/app.json.pages`、`miniprogram/host-bridge/protocol.js`、H5 `src/services/host-bridge/hostBridge.ts` 小程序页面常量、H5 `src/services/wechatMiniProgramSubscribe.ts` 订阅授权页面常量、`miniprogram/host-bridge/webView.js` 分享入口 / 分享消息类型、`miniprogram/config.js` source query / 域名格式、`miniprogram/shell/webView.js` 请求头来源标记、H5 runtime parser 和 H5 路由保留字段。新增或调整小程序页面、登录派生 URL、支付页、九宫切图页、订阅页、WebView 来源标记、H5 入口域名、API base URL 或宿主上下文 query 字段时,必须同步这几处常量并保持生产 / 开发域名都显式配置为纯 HTTPS domain;运行时开发域名回退生产域名只作为异常兜底。 - 2026-06-18 登录 / 支付能力禁伪声明:`auth.requestLogin` 和 `payment.request` 保留在共享 HostBridge 契约中供未来真实接入,但 Expo / Tauri 壳在真实 SDK、渠道流程和后端契约落地前不得声明这些 capability,也不得把它们写入入口 URL `hostCapabilities`;两端检查脚本会拒绝伪声明,请求实际到达壳层时必须返回明确 `unsupported_method` 并让 H5 fallback,两端壳测试直接覆盖这两个 method。 -- 2026-06-20 桌面壳未声明 method 禁伪成功:Tauri 桌面壳只声明真实可用 capability;共享 HostBridge method 白名单中未进入桌面 capability profile 的 method,例如 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`,请求实际到达桌面壳时必须统一返回明确 `unsupported_method`,不得伪造成功或半接入。桌面单端配置检查会从共享 method 与桌面 capability 差集自动派生 unsupported 测试覆盖清单,后续共享契约新增 method 时必须同步声明真实桌面能力或补进 unsupported 语义。 +- 2026-06-20 桌面壳未声明 method 禁伪成功:Tauri 桌面壳只声明真实可用 capability;共享 HostBridge method 白名单中未进入桌面 capability profile 的 method,例如 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`,请求实际到达桌面壳时必须统一返回明确 `unsupported_method`,不得伪造成功或半接入。桌面 Rust 测试必须从 `HOST_BRIDGE_METHODS` 与 `capabilities()` 差集派生 unsupported 覆盖清单,桌面单端配置检查会反查该派生路径,后续共享契约新增 method 时必须同步声明真实桌面能力或补进 unsupported 语义。 - 2026-06-18 移动壳触觉反馈边界:`haptics.impact` 只接受 `light`、`medium`、`heavy` 三档 impact style,缺省为 `light`;未知值必须返回 `invalid_request`,不得静默降级成真实设备触觉反馈。桌面壳不声明该 capability,H5 继续按 HostBridge fallback 处理。 - 2026-06-19 移动壳触觉反馈模块边界:Expo `haptics.impact` 的 HostBridge payload 解析、共享 style 归一、`light` / `medium` / `heavy` 到 `Haptics.ImpactFeedbackStyle` 的映射、真实 `Haptics.impactAsync(...)` 调用和成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/haptics.ts`;`dispatch.ts` 只负责把完整 request 委托给 `runMobileHostBridgeHapticsImpact(...)`,不得直接导入 `expo-haptics`、读取 `HapticsImpactPayload`、调用 `Haptics.impactAsync` 或包装触觉反馈成功响应。移动壳配置检查会覆盖该模块结构、共享 style 边界和 dispatch 委托关系,避免触觉反馈能力散落到分发层。 - 2026-06-18 分享卡图片导出:新增 `file.exportImage` HostBridge capability,H5 分享卡下载在 native app 中优先把 canvas 生成的 base64 图片交给宿主导出;Expo 壳写缓存图片后交给系统分享 / 保存面板,Tauri 壳通过系统保存对话框写入图片字节。该能力只接受 `image/png` / `image/jpeg` / `image/webp`、单次 5 MiB 内图片数据,成功只返回文件名和字节数,不暴露本机绝对路径;宿主未声明时保留浏览器下载。Expo 图片导出的 payload 校验、缓存写入、系统分享 / 保存面板和 HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts`,`dispatch.ts` 只委托文件模块。 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index ee7e960b0..b249b7515 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 真实调用链的临时替身词扫描。移动壳和桌面壳文档中的主状态段落能力清单与完整能力清单都必须反查共享 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`。 +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 必须由 Rust 测试从 `HOST_BRIDGE_METHODS - capabilities()` 自动派生为 `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`。 ## 验收