diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index eb12a1f52..caba80ac9 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3068,7 +3068,7 @@ ## 2026-06-20 原生壳能力声明必须绑定真实链路 - 背景:桌面壳 capability profile 已声明一组 HostBridge request 能力,Expo 移动壳也声明本地通知、拍照、扫码、网络事件和分享等原生能力,H5 平台入口还会通过 `navigation.openNativePage` 打开 `/child-motion-demo` 这类受控内置玩法路由;如果门禁只检查“有 method case”或“文件被扫描”,未来可能退化成 fallback-only 分支或普通 Web 跳转而不被发现。 -- 决策:桌面壳配置检查必须反查每个已声明 request capability 对应的真实模块委托,并拒绝由 `unsupported_method`、`unsupported_capability` 或 fallback-only case 支撑的声明能力;根级 `check:native-shells` 新增 H5 native app route flow 合约,锁定 `/child-motion-demo` 的 `navigateHostNativePage` 调用、浏览器 fallback、路由表和命名交互测试;Expo 移动壳关键 capability flow 合约必须反查共享移动 profile、真实 Expo / React Native API、权限或配置片段、宿主分发文件和对应测试清单。 +- 决策:桌面壳配置检查必须反查每个已声明 request capability 对应的真实模块委托,并拒绝由 `unsupported_method`、`unsupported_capability` 或 fallback-only case 支撑的声明能力;根级 `check:native-shells` 新增 H5 native app route flow 合约,锁定 `/child-motion-demo` 的 `navigateHostNativePage` 调用、浏览器 fallback、路由表和命名交互测试;Expo 移动壳关键 capability flow 合约必须反查共享移动 profile、真实 Expo / React Native API、权限或配置片段、宿主分发文件和对应测试清单;Tauri 桌面壳关键 capability flow 合约必须反查共享桌面 profile、真实 Tauri 插件 / Rust 系统 API、宿主分发文件、事件注入链路和对应单端检查片段。 - 影响范围:`apps/desktop-shell/scripts/check-config.mjs`、`scripts/check-native-shells.mjs`、宿主壳能力统一协议文档。 - 验证方式:`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 939971686..c30180159 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -266,7 +266,7 @@ GameBridge 禁止: - 每个请求必须有超时;H5 的 React Native WebView transport 和 Tauri `invoke` transport 都必须在前端侧按 `timeoutMs` 释放请求,默认请求超时、最大请求超时和用户交互长操作超时以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_DEFAULT_REQUEST_TIMEOUT_MS` / `HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS` / `HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS` 为唯一来源,宿主侧执行超时也只能返回标准 HostBridge 错误。重复 `id` 不得重复执行支付、登录、受控分享动作、文件导入导出、本地通知等宿主副作用;Expo 和 Tauri 壳都必须按 request id 回放首次完成结果,已完成响应缓存上限以共享契约 `HOST_BRIDGE_RESPONSE_CACHE_MAX` 为唯一来源。 - HostBridge 的 capability profile、宿主上下文 query 字段和值、文件 MIME 清单、导入 / 导出体积上限、文件名 fallback / 长度上限、request id 长度、角标上限、剪贴板文本长度、二维码文本长度、本地通知标题 / 正文长度、移动端 Android 本地通知 channel id 和桌面网络探测超时都必须以 `packages/shared/src/contracts/hostBridge.ts` 为声明来源;Expo 移动壳直接导入共享 profile 和契约常量,微信小程序壳和 Tauri 壳分别保留小程序 CommonJS / Rust 运行时代码镜像并由测试和配置门禁反查共享契约。 - 能力按 `capabilities` / `hostCapabilities` 下发,H5 会过滤未知能力,并根据声明结果决定是否展示入口、发起宿主请求或走 fallback;进入 `native_app` 后主 App 会再通过真实 `host.getRuntime` 回读一次宿主 runtime 并缓存能力,用来补齐裁剪壳或旧入口 URL 缺少 `hostCapabilities` 的场景,该回读请求的短超时以共享契约 `HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS` 为唯一来源。不能只凭 `native_app` 宿主类型假设能力可用。 -- 壳能力声明与三端壳验收必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行微信壳测试集合、`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`。声明的 capability 必须来自共享 HostBridge profile 并存在于共享白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、文件载荷边界、微信 WebView / 支付 / 订阅 / 分享桥接行为、微信小程序页面路由、WebView source query、微信请求头运行时标记、H5 runtime parser、H5 路由保留字段、微信壳 H5 / API HTTPS 域名格式、Expo managed config、移动端 production bundle、桌面 release 构建入口和微信 / Expo / Tauri 三端生产源码临时替身词扫描不得漂移。微信小程序壳不使用 Expo / Tauri 式统一 request dispatcher,但每个声明 capability 都必须在根级门禁中映射到真实流程文件、关键 `wx.*` 或页面工厂调用和对应测试清单;Expo 移动壳的关键能力也必须在根级门禁中映射到真实 Expo / React Native API、权限或配置片段、宿主分发文件和对应测试清单。 +- 壳能力声明与三端壳验收必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行微信壳测试集合、`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`。声明的 capability 必须来自共享 HostBridge profile 并存在于共享白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、文件载荷边界、微信 WebView / 支付 / 订阅 / 分享桥接行为、微信小程序页面路由、WebView source query、微信请求头运行时标记、H5 runtime parser、H5 路由保留字段、微信壳 H5 / API HTTPS 域名格式、Expo managed config、移动端 production bundle、桌面 release 构建入口和微信 / Expo / Tauri 三端生产源码临时替身词扫描不得漂移。微信小程序壳不使用 Expo / Tauri 式统一 request dispatcher,但每个声明 capability 都必须在根级门禁中映射到真实流程文件、关键 `wx.*` 或页面工厂调用和对应测试清单;Expo 移动壳和 Tauri 桌面壳的关键能力也必须在根级门禁中映射到真实 Expo / React Native / Tauri API、权限或配置片段、宿主分发文件和对应测试清单。 - Expo SDK、React Native、`react-native-webview`、Tauri CLI、Tauri Rust crate 和桌面 Cargo 插件版本属于宿主壳行为边界。升级这些依赖前必须同步更新壳配置检查、`package-lock.json` / `Cargo.lock` 解析版本、本文档和对应验证结果,不能只改 package / Cargo 版本让生产壳行为静默漂移。 - 登录和支付能力在真实 SDK、渠道流程、后端契约和失败回退全部落地前不得进入 Expo / Tauri capabilities,也不得写进入口 URL `hostCapabilities`;两端配置检查会拒绝 `auth.requestLogin` 和 `payment.request` 的伪声明。 - 宿主壳不得把长期 token、支付密钥或用户敏感资料回传给 H5。 diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index 445604f9b..b50f4e4a6 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -276,7 +276,7 @@ const mobileCapabilityFlowContracts = [ 'apps/mobile-shell/scripts/check-expo-config.mjs', 'android.permission.POST_NOTIFICATIONS', ], - [nativeShellPlanPath, 'Expo 移动壳的关键能力'], + [nativeShellPlanPath, 'Expo 移动壳和 Tauri 桌面壳的关键能力'], ], tests: [ 'apps/mobile-shell/src/host-bridge/notifications.test.ts', @@ -307,7 +307,7 @@ const mobileCapabilityFlowContracts = [ 'ImagePicker.launchCameraAsync', ], ['apps/mobile-shell/scripts/check-config.mjs', 'file.captureImage'], - [nativeShellPlanPath, 'Expo 移动壳的关键能力'], + [nativeShellPlanPath, 'Expo 移动壳和 Tauri 桌面壳的关键能力'], ], tests: [ 'apps/mobile-shell/src/host-bridge/files.test.ts', @@ -340,7 +340,7 @@ const mobileCapabilityFlowContracts = [ ], ['apps/mobile-shell/src/shell/QrScannerOverlay.tsx', 'CameraView'], ['apps/mobile-shell/scripts/check-config.mjs', 'scanner.scanQrCode'], - [nativeShellPlanPath, 'Expo 移动壳的关键能力'], + [nativeShellPlanPath, 'Expo 移动壳和 Tauri 桌面壳的关键能力'], ], tests: [ 'apps/mobile-shell/src/host-bridge/scanner.test.ts', @@ -371,7 +371,7 @@ const mobileCapabilityFlowContracts = [ "logMobileHostEventFailure('network.statusChanged', error)", ], ['apps/mobile-shell/scripts/check-config.mjs', 'network.statusChanged'], - [nativeShellPlanPath, 'Expo 移动壳的关键能力'], + [nativeShellPlanPath, 'Expo 移动壳和 Tauri 桌面壳的关键能力'], ], tests: [ 'apps/mobile-shell/src/shell/network.test.ts', @@ -395,7 +395,7 @@ const mobileCapabilityFlowContracts = [ 'normalizeHostBridgeShareOpenPayload', ], ['apps/mobile-shell/scripts/check-config.mjs', 'share.open'], - [nativeShellPlanPath, 'Expo 移动壳的关键能力'], + [nativeShellPlanPath, 'Expo 移动壳和 Tauri 桌面壳的关键能力'], ], tests: [ 'apps/mobile-shell/src/host-bridge/share.test.ts', @@ -403,6 +403,207 @@ const mobileCapabilityFlowContracts = [ ], }, ]; +const desktopCapabilityFlowContracts = [ + { + capability: 'notification.showLocal', + files: [ + 'apps/desktop-shell/src-tauri/Cargo.toml', + 'apps/desktop-shell/src-tauri/src/app.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs', + 'apps/desktop-shell/scripts/check-config.mjs', + nativeShellPlanPath, + ], + snippets: [ + [ + 'apps/desktop-shell/src-tauri/Cargo.toml', + 'tauri-plugin-notification', + ], + [ + 'apps/desktop-shell/src-tauri/src/app.rs', + 'tauri_plugin_notification::init()', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs', + 'show_desktop_local_notification(&app, &request)', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs', + 'app.notification()', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs', + 'request_permission()', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs', + 'notification.show()', + ], + [ + 'apps/desktop-shell/scripts/check-config.mjs', + 'notification.showLocal', + ], + [nativeShellPlanPath, 'Tauri 壳'], + ], + }, + { + capability: 'file.importDocument', + files: [ + 'apps/desktop-shell/src-tauri/Cargo.toml', + 'apps/desktop-shell/src-tauri/src/app.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/files.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs', + 'apps/desktop-shell/scripts/check-config.mjs', + nativeShellPlanPath, + ], + snippets: [ + ['apps/desktop-shell/src-tauri/Cargo.toml', 'tauri-plugin-dialog'], + ['apps/desktop-shell/src-tauri/src/app.rs', 'tauri_plugin_dialog::init()'], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs', + 'import_desktop_host_bridge_document_file(&app, &request).await', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/files.rs', + 'add_filter(\n "Document"', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/files.rs', + 'blocking_pick_file()', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/files.rs', + 'import_document_file_payload(path)', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs', + 'import_document_file_payload', + ], + [ + 'apps/desktop-shell/scripts/check-config.mjs', + 'file.importDocument', + ], + [nativeShellPlanPath, 'Tauri 壳'], + ], + }, + { + capability: 'file.exportImage', + files: [ + 'apps/desktop-shell/src-tauri/Cargo.toml', + 'apps/desktop-shell/src-tauri/src/app.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/files.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs', + 'apps/desktop-shell/scripts/check-config.mjs', + nativeShellPlanPath, + ], + snippets: [ + ['apps/desktop-shell/src-tauri/Cargo.toml', 'tauri-plugin-dialog'], + ['apps/desktop-shell/src-tauri/src/app.rs', 'tauri_plugin_dialog::init()'], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs', + 'export_desktop_host_bridge_image_file(&app, &request).await', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/files.rs', + 'add_filter("Image", &["png", "jpg", "jpeg", "webp"])', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/files.rs', + 'blocking_save_file()', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/files.rs', + 'write_export_bytes_file(path, bytes)', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs', + 'export_image_payload', + ], + ['apps/desktop-shell/scripts/check-config.mjs', 'file.exportImage'], + [nativeShellPlanPath, 'Tauri 壳'], + ], + }, + { + capability: 'file.imageDropped', + files: [ + 'apps/desktop-shell/src-tauri/src/app.rs', + 'apps/desktop-shell/src-tauri/src/shell/file_drop.rs', + 'apps/desktop-shell/src-tauri/src/shell/events.rs', + 'apps/desktop-shell/scripts/check-config.mjs', + nativeShellPlanPath, + ], + snippets: [ + [ + 'apps/desktop-shell/src-tauri/src/app.rs', + 'register_desktop_file_drop_events(&window)', + ], + [ + 'apps/desktop-shell/src-tauri/src/shell/file_drop.rs', + 'DragDropEvent::Drop', + ], + [ + 'apps/desktop-shell/src-tauri/src/shell/file_drop.rs', + 'host_bridge_event_script("file.imageDropped", payload)', + ], + [ + 'apps/desktop-shell/src-tauri/src/shell/file_drop.rs', + 'import_image_file_payload(path.clone(), "dropped", Some(position))', + ], + [ + 'apps/desktop-shell/src-tauri/src/shell/events.rs', + 'file.imageDropped', + ], + ['apps/desktop-shell/scripts/check-config.mjs', 'file.imageDropped'], + [nativeShellPlanPath, 'Tauri 壳'], + ], + }, + { + capability: 'share.open', + files: [ + 'apps/desktop-shell/src-tauri/Cargo.toml', + 'apps/desktop-shell/src-tauri/src/app.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/share.rs', + 'apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs', + 'apps/desktop-shell/scripts/check-config.mjs', + nativeShellPlanPath, + ], + snippets: [ + [ + 'apps/desktop-shell/src-tauri/Cargo.toml', + 'tauri-plugin-clipboard-manager', + ], + [ + 'apps/desktop-shell/src-tauri/src/app.rs', + 'tauri_plugin_clipboard_manager::init()', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs', + 'open_desktop_host_bridge_share(&app, &request)', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/share.rs', + 'normalize_public_share_url', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/share.rs', + 'write_desktop_clipboard_text(app, &share_text)', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/share.rs', + '"action": "copied_to_clipboard"', + ], + [ + 'apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs', + 'write_desktop_clipboard_text', + ], + ['apps/desktop-shell/scripts/check-config.mjs', 'share.open'], + [nativeShellPlanPath, 'Tauri 壳'], + ], + }, +]; const h5NativeAppRouteFlowContracts = [ { route: '/child-motion-demo', @@ -2267,6 +2468,47 @@ function assertExpoMobileCapabilityFlows() { } } +function assertTauriDesktopCapabilityFlows() { + const sharedContractSource = fs.readFileSync(sharedHostBridgeContractPath, 'utf8'); + const declaredCapabilities = extractTsStringArray( + sharedContractSource, + 'HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES', + ); + const declaredCapabilitySet = new Set(declaredCapabilities); + const contractedCapabilities = desktopCapabilityFlowContracts.map( + ({ capability }) => capability, + ); + + assertUniqueList( + contractedCapabilities, + 'Tauri desktop capability flow contracts', + ); + + for (const contract of desktopCapabilityFlowContracts) { + if (!declaredCapabilitySet.has(contract.capability)) { + throw new Error( + `Tauri desktop capability flow contract declared for missing capability: ${contract.capability}`, + ); + } + + for (const filePath of contract.files) { + if (!fs.existsSync(filePath)) { + throw new Error( + `Tauri desktop ${contract.capability} flow file is missing: ${filePath}`, + ); + } + } + + for (const [filePath, snippet] of contract.snippets) { + assertFileIncludesSnippet( + filePath, + snippet, + `Tauri desktop ${contract.capability} flow`, + ); + } + } +} + function assertH5NativeAppRouteFlows() { const h5HostBridgeTestSet = new Set(h5HostBridgeTests); for (const contract of h5NativeAppRouteFlowContracts) { @@ -2764,6 +3006,9 @@ assertWechatMiniProgramCapabilityFlows(); console.log('[check:native-shells] expo-mobile-capability-flows'); assertExpoMobileCapabilityFlows(); +console.log('[check:native-shells] tauri-desktop-capability-flows'); +assertTauriDesktopCapabilityFlows(); + console.log('[check:native-shells] h5-native-app-route-flows'); assertH5NativeAppRouteFlows();