From b2b0f4514ba0e0aad53db8514520fff432b816dd Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 14:15:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E5=8E=9F=E7=94=9F=E5=A3=B3?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E4=BE=9D=E8=B5=96=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳配置检查锁定Expo与React Native关键依赖版本 桌面壳配置检查锁定Tauri CLI与Cargo插件版本 宿主壳方案文档补充依赖版本门禁 共享决策日志记录原生壳关键依赖边界 --- apps/desktop-shell/scripts/check-config.mjs | 103 ++++++++++++++++++ apps/mobile-shell/scripts/check-config.mjs | 69 ++++++++++++ .../shared-memory/decision-log.md | 7 ++ ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 5 + 4 files changed, 184 insertions(+) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 23f1d1799..1f4536c73 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -120,6 +120,26 @@ function extractCargoPackageString(source, key) { return match[1]; } +function extractCargoDependencyLine(source, sectionName, dependencyName) { + let inSection = false; + for (const line of source.split('\n')) { + if (/^\[[^\]]+\]$/.test(line)) { + inSection = line === `[${sectionName}]`; + continue; + } + + if (!inSection) { + continue; + } + + if (line.match(new RegExp(`^${escapeRegExp(dependencyName)}\\s*=`))) { + return line; + } + } + + throw new Error(`Cargo ${sectionName}.${dependencyName} is missing`); +} + function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } @@ -207,6 +227,34 @@ function assertPackageScript(packageJson, packageLabel, scriptName, expected) { } } +function assertPackageDependencyVersion( + packageJson, + packageLabel, + section, + dependency, + expected, +) { + const actual = packageJson[section]?.[dependency]; + if (actual !== expected) { + throw new Error( + `${packageLabel} ${section}.${dependency} drifted: expected ${expected} but got ${actual}`, + ); + } +} + +function assertCargoDependencyLine(sectionName, dependencyName, expectedLine) { + const actualLine = extractCargoDependencyLine( + cargoManifest, + sectionName, + dependencyName, + ); + if (actualLine !== expectedLine) { + throw new Error( + `Cargo ${sectionName}.${dependencyName} drifted: expected ${expectedLine} but got ${actualLine}`, + ); + } +} + function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { @@ -281,6 +329,61 @@ for (const [scriptName, expected] of Object.entries({ assertPackageScript(rootPackageConfig, 'root package', scriptName, expected); } +for (const [dependency, expected] of Object.entries({ + '@tauri-apps/cli': '^2.11.2', + typescript: '~5.8.2', +})) { + assertPackageDependencyVersion( + packageConfig, + 'desktop shell package', + 'devDependencies', + dependency, + expected, + ); + assertPackageDependencyVersion( + rootPackageConfig, + 'root package', + 'devDependencies', + dependency, + expected, + ); +} + +for (const [sectionName, dependencyName, expectedLine] of [ + ['build-dependencies', 'tauri-build', 'tauri-build = { version = "2.6.2", features = [] }'], + ['dependencies', 'base64', 'base64 = "0.22"'], + ['dependencies', 'serde', 'serde = { version = "1", features = ["derive"] }'], + ['dependencies', 'serde_json', 'serde_json = "1"'], + ['dependencies', 'tauri', 'tauri = { version = "2.11.2", features = ["tray-icon"] }'], + [ + 'dependencies', + 'tauri-plugin-clipboard-manager', + 'tauri-plugin-clipboard-manager = "2.3.2"', + ], + [ + 'dependencies', + 'tauri-plugin-dialog', + 'tauri-plugin-dialog = "2.7.1"', + ], + [ + 'dependencies', + 'tauri-plugin-notification', + 'tauri-plugin-notification = "2.3.3"', + ], + [ + 'dependencies', + 'tauri-plugin-opener', + 'tauri-plugin-opener = "2.5.4"', + ], + [ + 'dependencies', + 'tauri-plugin-single-instance', + 'tauri-plugin-single-instance = "2.4.2"', + ], +]) { + assertCargoDependencyLine(sectionName, dependencyName, expectedLine); +} + function readPngSize(file) { const buffer = fs.readFileSync(file); if ( diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index d292bb1c6..f52e22eec 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -197,6 +197,21 @@ function assertPackageScript(packageJson, packageLabel, scriptName, expected) { } } +function assertPackageDependencyVersion( + packageJson, + packageLabel, + section, + dependency, + expected, +) { + const actual = packageJson[section]?.[dependency]; + if (actual !== expected) { + throw new Error( + `${packageLabel} ${section}.${dependency} drifted: expected ${expected} but got ${actual}`, + ); + } +} + function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { @@ -303,6 +318,60 @@ for (const [scriptName, expected] of Object.entries({ assertPackageScript(rootPackageConfig, 'root package', scriptName, expected); } +for (const [dependency, expected] of Object.entries({ + '@expo/metro-runtime': '^56.0.15', + expo: '^56.0.12', + 'expo-clipboard': '^56.0.4', + 'expo-document-picker': '^56.0.4', + 'expo-file-system': '^56.0.8', + 'expo-haptics': '^56.0.3', + 'expo-image-picker': '^56.0.18', + 'expo-linking': '^56.0.14', + 'expo-network': '^56.0.5', + 'expo-notifications': '^56.0.18', + 'expo-sharing': '^56.0.18', + 'expo-status-bar': '^56.0.4', + react: '^19.0.0', + 'react-native': '^0.86.0', + 'react-native-safe-area-context': '^5.8.0', + 'react-native-webview': '^13.16.1', +})) { + assertPackageDependencyVersion( + packageConfig, + 'mobile shell package', + 'dependencies', + dependency, + expected, + ); + assertPackageDependencyVersion( + rootPackageConfig, + 'root package', + 'dependencies', + dependency, + expected, + ); +} + +for (const [dependency, expected] of Object.entries({ + typescript: '~5.8.2', + vitest: '^0.34.6', +})) { + assertPackageDependencyVersion( + packageConfig, + 'mobile shell package', + 'devDependencies', + dependency, + expected, + ); + assertPackageDependencyVersion( + rootPackageConfig, + 'root package', + 'devDependencies', + dependency, + expected, + ); +} + const sharedCapabilities = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_CAPABILITIES', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 8ba054c38..18bf28af1 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2452,6 +2452,13 @@ - 影响范围:`packages/shared/src/contracts/hostBridge.ts`、`src/services/host-bridge/nativeAppHostBridge.ts`、`apps/desktop-shell/scripts/check-config.mjs`、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run test -- src/services/host-bridge/nativeAppHostBridge.test.ts packages/shared/src/contracts/hostBridge.test.ts`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-18 原生壳关键依赖版本收口 + +- 背景:Expo / React Native WebView / Tauri / Cargo 插件版本会直接影响 WebView 安全默认值、managed config 解析、production bundle、Tauri capability、插件初始化和 release 构建行为;如果只改依赖声明,壳行为可能绕过 HostBridge 门禁和现有验收口径静默漂移。 +- 决策:移动壳配置检查锁定 `apps/mobile-shell/package.json` 和根 `package.json` 中当前 Expo SDK 56、React 19、React Native 0.86、`react-native-webview`、`react-native-safe-area-context`、Expo Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 与 Vitest 版本。桌面壳配置检查锁定 `apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI / TypeScript 版本,并锁定 `src-tauri/Cargo.toml` 中 `tauri-build`、`tauri`、`base64`、`serde`、`serde_json` 和 clipboard、dialog、notification、opener、single-instance 插件版本及 Tauri `tray-icon` feature。后续升级这些依赖必须同步更新配置门禁、方案文档和验证结果。 +- 影响范围:`apps/mobile-shell/scripts/check-config.mjs`、`apps/desktop-shell/scripts/check-config.mjs`、Expo / Tauri HostBridge 方案文档。 +- 验证方式:`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-18 原生 HostBridge 入站消息来源收口 - 背景:H5 主站会同时承载原生壳 HostBridge 和后续 AI H5 sandbox / GameBridge;如果 H5 侧只按 JSON envelope 识别 HostBridge response / event,sandbox iframe 可以构造同形 `postMessage` 干扰待处理宿主请求或伪造宿主事件。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 59229d8ea..cd5c440f0 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -235,6 +235,7 @@ GameBridge 禁止: - 每个请求必须有超时,重复 `id` 不得重复执行支付、登录、系统分享、文件导入导出、本地通知等宿主副作用;Expo 和 Tauri 壳都必须按 request id 回放首次完成结果。 - 能力按 `capabilities` / `hostCapabilities` 下发,H5 会过滤未知能力,并根据声明结果决定是否展示入口、发起宿主请求或走 fallback;进入 `native_app` 后主 App 会再通过真实 `host.getRuntime` 回读一次宿主 runtime 并缓存能力,用来补齐裁剪壳或旧入口 URL 缺少 `hostCapabilities` 的场景。不能只凭 `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 白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口和两端壳生产源码临时替身词扫描不得漂移。 +- Expo SDK、React Native、`react-native-webview`、Tauri CLI、Tauri Rust crate 和桌面 Cargo 插件版本属于宿主壳行为边界。升级这些依赖前必须同步更新壳配置检查、本文档和对应验证结果,不能只改 package / Cargo 版本让生产壳行为静默漂移。 - 登录和支付能力在真实 SDK、渠道流程、后端契约和失败回退全部落地前不得进入 Expo / Tauri capabilities,也不得写进入口 URL `hostCapabilities`;两端配置检查会拒绝 `auth.requestLogin` 和 `payment.request` 的伪声明。 - 宿主壳不得把长期 token、支付密钥或用户敏感资料回传给 H5。 - 桌面壳不得提前安装或初始化崩溃上报、analytics、遥测日志、自动更新或渠道分发 SDK;这类能力必须先补齐真实后端 / 第三方端点、采集口径、用户授权、隐私披露、签名和发布流程,再进入 Tauri 配置、Cargo 依赖、Node 依赖或 Rust 初始化代码。 @@ -307,6 +308,8 @@ GameBridge 禁止: 2026-06-18 追加:移动壳命令入口进入配置门禁。`apps/mobile-shell/package.json` 的 `dev`、`android`、`ios`、`test`、`config:smoke`、`export:smoke`、`typecheck` 以及根 `package.json` 的 `mobile-shell:*` 入口必须保持指向真实 Expo / RN / Vitest / Expo config / Metro export / 配置检查流程,不能替换成只跑静态脚本或绕过生产 bundler 的快捷命令。 +2026-06-18 追加:移动壳关键依赖版本进入配置门禁。`apps/mobile-shell/package.json` 和根 `package.json` 的 Expo SDK 56、React 19、React Native 0.86、`react-native-webview` 13.16、`react-native-safe-area-context`、Expo Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 和 Vitest 版本必须保持一致;升级这些依赖必须同步审查 WebView 安全开关、Expo managed config、production export、HostBridge 能力实现和 H5 fallback,不能只更新依赖声明。 + ### Phase 3:Tauri 桌面壳 MVP - 新增 `apps/desktop-shell/`。 @@ -352,6 +355,8 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳 JS guest 依赖进入门禁。`apps/desktop-shell/package.json` 和根 H5 `package.json` 不得安装 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*` 包,避免生产前端绕过 `nativeAppHostBridge` 直接调用 Tauri JS 客户端 API;Tauri CLI 仍只作为构建工具留在 devDependencies,桌面系统能力继续由 Rust 侧 Cargo 插件和唯一 `host_bridge_request` command 承接。 +2026-06-18 追加:桌面壳关键依赖版本进入配置门禁。`apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI 和 TypeScript 版本必须一致;`src-tauri/Cargo.toml` 的 `tauri-build`、`tauri`、`base64`、`serde`、`serde_json` 以及 clipboard、dialog、notification、opener、single-instance 插件版本和 Tauri `tray-icon` feature 都由 `apps/desktop-shell/scripts/check-config.mjs` 固定检查。升级这些依赖必须同步审查 capability、CSP、唯一 command、插件初始化、release build smoke 和本文档。 + 2026-06-18 追加:H5 到 Tauri 的 command 名进入共享契约。`packages/shared/src/contracts/hostBridge.ts` 导出 `HOST_BRIDGE_TAURI_COMMAND='host_bridge_request'`,`nativeAppHostBridge` 只能通过该常量调用 Tauri 注入的 `core.invoke`;桌面壳配置检查会对齐共享常量、Tauri build manifest、Rust `generate_handler!` 和 H5 transport,禁止 H5 侧写死或调用其它 Tauri command。 2026-06-18 追加:原生壳注入消息来源进入门禁。Expo 和 Tauri 注入给 H5 的 HostBridge response / event 都显式带 `origin: window.location.origin` 和 `source: window`;H5 `nativeAppHostBridge` listener 会忽略带非当前窗口 source 或非当前页面 origin 的 message。这样后续 AI sandbox iframe 即使能向父页面 `postMessage` 同形 envelope,也不能结算宿主请求或伪造宿主事件;GameBridge 继续走单独 allowlist。