diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index cf98ab1ed..3c43317dd 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -162,6 +162,35 @@ const requiredPngIconSizes = new Map([ ['128x128@2x.png', 256], ['icon.png', 512], ]); +const expectedDesktopRustRootEntries = [ + 'dir:host_bridge', + 'dir:shell', + 'file:app.rs', + 'file:main.rs', +]; +const expectedDesktopHostBridgeRustFiles = [ + 'capabilities.rs', + 'dispatch.rs', + 'files.rs', + 'mod.rs', + 'protocol.rs', + 'share.rs', +]; +const expectedDesktopShellRustFiles = [ + 'deep_link.rs', + 'events.rs', + 'file_drop.rs', + 'lifecycle.rs', + 'menu.rs', + 'mod.rs', + 'navigation.rs', + 'network.rs', + 'runtime.rs', + 'tray.rs', + 'url.rs', + 'webview.rs', + 'window_state.rs', +]; function extractCargoPackageString(source, key) { const match = source.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, 'm')); @@ -479,6 +508,7 @@ assertNoBlockedNpmLockPackages(); assertNoBlockedCargoDependencies(); assertNoBlockedCargoLockPackages(); assertNoBlockedDesktopSdkSnippets(); +assertDesktopSourceLayout(); for (const [scriptName, expected] of Object.entries({ dev: 'tauri dev', @@ -898,6 +928,46 @@ function readDirectoryFileList(directory, label) { return files.sort(); } +function assertDesktopSourceLayout() { + assertSameList( + readDirectoryEntryList(rustSourceDir, 'desktop shell Rust root entries'), + expectedDesktopRustRootEntries, + 'desktop shell Rust root entries', + ); + assertSameList( + readDirectoryFileList( + new URL('../src-tauri/src/host_bridge/', import.meta.url), + 'desktop host bridge Rust files', + ), + expectedDesktopHostBridgeRustFiles, + 'desktop host bridge Rust files', + ); + assertSameList( + readDirectoryFileList( + new URL('../src-tauri/src/shell/', import.meta.url), + 'desktop shell Rust files', + ), + expectedDesktopShellRustFiles, + 'desktop shell Rust files', + ); + + if (!main.includes('mod app;') || !main.includes('app::run();')) { + throw new Error('desktop shell main.rs must stay a thin app entrypoint'); + } + if ( + main.includes('tauri::Builder::default()') || + main.includes('tauri::generate_handler!') + ) { + throw new Error('desktop shell main.rs must not own Tauri app setup'); + } + if ( + !app.includes('tauri::Builder::default()') || + !app.includes('crate::host_bridge::host_bridge_request') + ) { + throw new Error('desktop shell app.rs must own Tauri app setup'); + } +} + function extractTauriBuildCommands(source) { const match = source.match(/\.commands\(\s*&\[\s*([^\]]*?)\s*\]\s*\)/); if (!match) { diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 1d2533ae4..e9bd114d0 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -109,6 +109,7 @@ - 2026-06-18 移动壳 WebView 默认下载边界:Expo WebView 内网页自动下载和 `` 直接落盘默认关闭;壳层注入脚本阻断 download 链接,iOS `onFileDownload` 只丢弃不落盘,Android 包配置通过 `blockedPermissions` 移除外部存储读写、管理外部存储和请求安装包权限。移动端文本、图片、音频保存只能通过 `file.exportText`、`file.exportImage`、`file.exportAudio` 等 HostBridge 受控导出能力进入系统分享 / 保存面板。 - 2026-06-18 移动壳 HostBridge 消息来源校验:Expo 移动壳 `onMessage` 必须根据 `event.nativeEvent.url` 校验消息来源,只有同源主站页面能进入 `handleMobileHostBridgeMessage`;`about:blank`、外域、协议降级和危险协议页面消息直接丢弃,不返回宿主能力错误细节。该规则与 WebView 导航留壳规则共用同源判断,配置检查和移动壳导航测试会拒绝移除。 - 2026-06-18 三端桥接层目录同构:微信小程序、Expo 移动壳和 Tauri 桌面壳都按 `host-bridge / shell` 两层管理宿主桥接代码。微信 `miniprogram/host-bridge/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js` 只放协议归一、支付 / 订阅 / 分享结果编解码和可测试桥接函数,`miniprogram/shell/` 下同名职责文件承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂;页面目录只保留 `Page(createWechat...Page())` 装配。Expo `protocol.ts`、`capabilities.ts`、`dispatch.ts`、`files.ts`、`scanner.ts`、`share.ts` 和 facade `bridge.ts` 分别对齐 Tauri `host_bridge/protocol.rs`、`capabilities.rs`、`dispatch.rs`、`files.rs`、`share.rs`、`mod.rs`,根 `App.tsx` 只装配 `src/shell/ShellApp.tsx`,不直接进口 HostBridge。Tauri `shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面。`npm run check:native-shells` 会校验微信、移动和桌面三端目录清单,新增宿主能力必须按同一边界落文件和测试。 +- 2026-06-19 桌面壳单端结构门禁:`apps/desktop-shell/scripts/check-config.mjs` 与根级 `npm run check:native-shells` 同步校验 `src-tauri/src` 根目录、`host_bridge/` 和 `shell/` 的生产模块清单,并要求 `main.rs` 保持薄入口、`app.rs` 承接 Tauri builder / plugin / window 装配。后续新增桌面宿主能力必须先按 HostBridge / shell 职责边界登记文件和测试,不能只靠根门禁或把能力逻辑塞回 `main.rs`。 - 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。 - 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。 - 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。