From fb02e89fa365d2be0107c7918f7e9e8ffc28d501 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 08:24:07 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳 navigation helper 增加独立单测覆盖 原生壳结构门禁登记移动导航测试 宿主壳文档和共享决策同步导航测试边界 --- apps/mobile-shell/scripts/check-config.mjs | 24 +++ .../src/host-bridge/navigation.test.ts | 188 ++++++++++++++++++ .../shared-memory/decision-log.md | 1 + ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 +- ...前端架构】宿主壳能力统一协议-2026-06-17.md | 2 +- scripts/check-native-shells.mjs | 1 + 6 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 apps/mobile-shell/src/host-bridge/navigation.test.ts diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 68a88d8ae..3571c5ef3 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -48,6 +48,14 @@ const hostBridgeNavigationSource = fs.readFileSync( hostBridgeNavigationPath, 'utf8', ); +const hostBridgeNavigationTestPath = new URL( + '../src/host-bridge/navigation.test.ts', + import.meta.url, +); +const hostBridgeNavigationTestSource = fs.readFileSync( + hostBridgeNavigationTestPath, + 'utf8', +); const hostBridgeNetworkPath = new URL( '../src/host-bridge/network.ts', import.meta.url, @@ -1928,6 +1936,22 @@ if ( 'mobile shell app.openExternalUrl must normalize payloads and use the shared external navigation helper', ); } +for (const snippet of [ + 'openMobileHostBridgeExternalUrl', + 'openMobileHostBridgeNativePage', + 'reloadMobileHostBridgeWebView', + 'Linking.canOpenURL', + 'Linking.openURL', + 'javascript:alert(1)', + 'external URL cannot be opened', + 'navigation.openNativePage unsupported in mobile shell', + 'app.reloadWebView unsupported in mobile shell', + 'hostCapabilities', +]) { + if (!hostBridgeNavigationTestSource.includes(snippet)) { + throw new Error(`mobile shell HostBridge navigation test missing ${snippet}`); + } +} if ( !clipboardSource.includes( 'const clipboardText = normalizeHostBridgeClipboardText(', diff --git a/apps/mobile-shell/src/host-bridge/navigation.test.ts b/apps/mobile-shell/src/host-bridge/navigation.test.ts new file mode 100644 index 000000000..2d53eda45 --- /dev/null +++ b/apps/mobile-shell/src/host-bridge/navigation.test.ts @@ -0,0 +1,188 @@ +import * as Linking from 'expo-linking'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +import { + HOST_BRIDGE_PROTOCOL, + HOST_BRIDGE_VERSION, + type HostBridgeRequest, +} from '../../../../packages/shared/src/contracts/hostBridge'; +import type { MobileHostBridgeNavigation } from './protocol'; +import { + openMobileHostBridgeExternalUrl, + openMobileHostBridgeNativePage, + reloadMobileHostBridgeWebView, +} from './navigation'; + +vi.mock('expo-linking', () => ({ + canOpenURL: vi.fn(), + openURL: vi.fn(), +})); + +function request(method: HostBridgeRequest['method'], payload?: unknown) { + return { + bridge: HOST_BRIDGE_PROTOCOL, + version: HOST_BRIDGE_VERSION, + id: `${method}-request`, + method, + payload, + } satisfies HostBridgeRequest; +} + +function navigation(): MobileHostBridgeNavigation { + return { + allowedOrigin: 'https://app.genarrative.world', + urlOptions: { + platform: 'ios', + hostVersion: '0.1.0', + capabilities: ['navigation.openNativePage', 'app.reloadWebView'], + }, + openWebViewUrl: vi.fn(), + reloadWebView: vi.fn(), + }; +} + +beforeEach(() => { + vi.mocked(Linking.canOpenURL).mockReset(); + vi.mocked(Linking.openURL).mockReset(); +}); + +describe('mobile HostBridge navigation helpers', () => { + test('opens allowed external URLs through Expo Linking', async () => { + vi.mocked(Linking.canOpenURL).mockResolvedValue(true); + + await expect( + openMobileHostBridgeExternalUrl( + request('app.openExternalUrl', { + url: ' https://example.com/path?from=native ', + }), + ), + ).resolves.toEqual({ + bridge: HOST_BRIDGE_PROTOCOL, + version: HOST_BRIDGE_VERSION, + id: 'app.openExternalUrl-request', + ok: true, + result: true, + }); + expect(Linking.canOpenURL).toHaveBeenCalledWith( + 'https://example.com/path?from=native', + ); + expect(Linking.openURL).toHaveBeenCalledWith( + 'https://example.com/path?from=native', + ); + }); + + test.each([ + undefined, + {}, + { url: '/works/detail?work=PZ-1' }, + { url: 'javascript:alert(1)' }, + { url: 'https://example.com/\u0000' }, + ])('rejects unsafe external URL payloads: %o', async (payload) => { + await expect( + openMobileHostBridgeExternalUrl( + request('app.openExternalUrl', payload), + ), + ).rejects.toMatchObject({ + code: 'invalid_request', + message: 'url must use an allowed external protocol', + }); + expect(Linking.canOpenURL).not.toHaveBeenCalled(); + expect(Linking.openURL).not.toHaveBeenCalled(); + }); + + test('converts system external open failures to host_error', async () => { + vi.mocked(Linking.canOpenURL).mockResolvedValue(false); + + await expect( + openMobileHostBridgeExternalUrl( + request('app.openExternalUrl', { + url: 'mailto:hello@example.com', + }), + ), + ).rejects.toMatchObject({ + code: 'host_error', + message: 'external URL cannot be opened', + }); + expect(Linking.openURL).not.toHaveBeenCalled(); + }); + + test('opens same-origin native page targets in the WebView with host context', () => { + const nav = navigation(); + + expect( + openMobileHostBridgeNativePage( + request('navigation.openNativePage', { + url: '/works/detail?work=PZ-1#play', + }), + nav, + ), + ).toEqual({ + bridge: HOST_BRIDGE_PROTOCOL, + version: HOST_BRIDGE_VERSION, + id: 'navigation.openNativePage-request', + ok: true, + result: true, + }); + expect(nav.openWebViewUrl).toHaveBeenCalledTimes(1); + const [targetUrl] = vi.mocked(nav.openWebViewUrl).mock.calls[0] ?? []; + expect(targetUrl).toBeTypeOf('string'); + if (typeof targetUrl !== 'string') { + throw new Error('navigation.openNativePage did not open a WebView URL'); + } + const target = new URL(targetUrl); + expect(target.origin).toBe('https://app.genarrative.world'); + expect(target.pathname).toBe('/works/detail'); + expect(target.searchParams.get('work')).toBe('PZ-1'); + expect(target.hash).toBe('#play'); + expect(target.searchParams.get('clientType')).toBe('native_app'); + expect(target.searchParams.get('hostShell')).toBe('expo_mobile'); + expect(target.searchParams.get('hostPlatform')).toBe('ios'); + expect(target.searchParams.get('hostCapabilities')).toBe( + 'navigation.openNativePage,app.reloadWebView', + ); + }); + + test.each([ + undefined, + { url: 'https://example.com/evil' }, + { url: 'javascript:alert(1)' }, + ])('rejects unsafe native page targets: %o', (payload) => { + const nav = navigation(); + + expect(() => + openMobileHostBridgeNativePage( + request('navigation.openNativePage', payload), + nav, + ), + ).toThrowError(); + expect(nav.openWebViewUrl).not.toHaveBeenCalled(); + }); + + test('requires navigation adapter for native page and reload requests', () => { + expect(() => + openMobileHostBridgeNativePage( + request('navigation.openNativePage', { + url: '/works/detail?work=PZ-1', + }), + null, + ), + ).toThrowError('navigation.openNativePage unsupported in mobile shell'); + expect(() => + reloadMobileHostBridgeWebView(request('app.reloadWebView'), null), + ).toThrowError('app.reloadWebView unsupported in mobile shell'); + }); + + test('reloads the WebView through the navigation adapter', () => { + const nav = navigation(); + + expect(reloadMobileHostBridgeWebView(request('app.reloadWebView'), nav)) + .toEqual({ + bridge: HOST_BRIDGE_PROTOCOL, + version: HOST_BRIDGE_VERSION, + id: 'app.reloadWebView-request', + ok: true, + result: true, + }); + expect(nav.reloadWebView).toHaveBeenCalledTimes(1); + }); +}); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 70de908bb..f3a118a95 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -87,6 +87,7 @@ - 2026-06-18 移动壳媒体策略:Expo WebView 允许内联媒体播放和用户触发的全屏视频,但保留 `mediaPlaybackRequiresUserAction`,不允许无手势自动播放;固定玩法和 AI H5 sandbox 的音频仍由 H5 用户开关、运行态状态和宿主生命周期控制,壳层不注入额外播放器或假播放状态。移动壳配置检查会拒绝 WebView 媒体策略漂移。 - 2026-06-18 移动壳启动 URL 归一:Expo 壳的 `EXPO_PUBLIC_GENARRATIVE_WEB_URL` 和 deep link 基准地址只接受生产主站 `https://app.genarrative.world`,以及本机开发联调 `http://127.0.0.1`、`http://localhost`、`http://[::1]`;空值、相对路径、外域、`file:`、`javascript:` 等非法配置回退到默认 H5 地址后再附加 `native_app` 宿主上下文;deep link 仍只映射归一后基准 origin 的 H5 路径,禁止把外域或危险协议页面装进带完整 HostBridge 的 WebView。 - 2026-06-18 移动壳主动导航上下文:Expo 壳的 `navigation.openNativePage` 与 deep link 都必须复用 `buildMobileShellUrl(...)` 补写 `native_app`、`expo_mobile`、真实平台、版本和 capability 清单;受控导航只接受当前允许 origin 的同源 H5 URL。移动壳配置检查会拒绝主动导航或 deep link 绕过该宿主上下文构造入口。 +- 2026-06-20 移动壳导航单测边界:`apps/mobile-shell/src/host-bridge/navigation.test.ts` 直接覆盖 `app.openExternalUrl` 的共享外链 helper 调用、危险 URL 拒绝、系统不能打开时的 `host_error`,以及 `navigation.openNativePage` 的同源 H5 跳转、宿主上下文补写、缺失 navigation adapter 的 unsupported 语义和 `app.reloadWebView` adapter 调用;根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,避免宿主导航边界只靠 WebView shell 测试或完整 bridge 流程间接覆盖。 - 2026-06-18 移动壳协议常量来源:Expo 壳的 HostBridge 事件注入、入口 URL `bridgeVersion`、`host.getRuntime` 回包和 Expo public config smoke 必须使用 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION`,不得在壳层重新写死协议名或版本字面量;配置检查会拒绝这些常量漂移。 - 2026-06-19 公开 Web origin 单一来源:原生壳允许加载 / 分享 / 跳转的公开 H5 主站 origin 以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_PUBLIC_WEB_ORIGIN` / `HOST_BRIDGE_PUBLIC_WEB_URL` 为源;Expo 移动壳只能通过 `DEFAULT_MOBILE_SHELL_WEB_URL` / `ALLOWED_PRODUCTION_WEB_ORIGIN` 语义别名引用共享常量,Tauri 桌面壳 `WEB_APP_ORIGIN` 作为 Rust 运行时镜像常量必须由 `apps/desktop-shell/scripts/check-config.mjs` 反查同一共享值。两端不得在分享、WebView policy、启动 URL 或桌面导航里另行复刻 `https://app.genarrative.world` 作为独立真相。 - 2026-06-18 桌面壳协议常量来源:Tauri Rust 侧 `host_bridge/protocol.rs` 的 `HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION`、桌面入口 URL `bridgeVersion`、HostBridge event 注入和 runtime 回包必须与 `packages/shared/src/contracts/hostBridge.ts` 保持一致;桌面配置检查会反查共享契约并拒绝协议名或协议版本漂移。`tauri.conf.json` 只保留基础入口,`shell/url.rs` 统一补写桌面宿主上下文和真实 capability 清单,配置检查会拒绝把 `hostCapabilities` 等宿主 query 长串重新写回 Tauri 配置。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 7903da967..38ebdcc5d 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -521,7 +521,7 @@ GameBridge 禁止: 2026-06-18 追加:`app.openExternalUrl` 的协议白名单以共享 HostBridge 契约 `HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS` 为唯一来源,当前只允许 `http:`、`https:`、`mailto:`、`tel:`。Expo 壳直接复用共享归一化逻辑,Tauri 壳 Rust 侧用 URL parser 镜像同一清单;`npm run check:native-shells` 会反查共享契约与桌面壳协议清单,防止某一端单独放宽外链协议。 -2026-06-18 追加:微信、移动端和桌面端桥接层文件结构按职责对齐。微信小程序的 `web-view`、支付、九宫切图和订阅消息桥接逻辑统一迁入 `miniprogram/host-bridge/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面目录只保留页面生命周期、WXML/WXSS 和装配;移动壳拆成 `apps/mobile-shell/src/host-bridge/protocol.ts`、`capabilities.ts`、`dispatch.ts`、`files.ts`、`filePayloads.ts`、`scanner.ts`、`share.test.ts`、`share.ts` 和 facade `bridge.ts`,与桌面端 `host_bridge/protocol.rs`、`capabilities.rs`、`dispatch.rs`、`files.rs`、`file_payloads.rs`、`share.rs`、`mod.rs` 对齐,其中移动 `files.ts` 只承接 Expo 系统文件交互和 HostBridge 响应包装,`filePayloads.ts` 承接 MIME、大小、base64、文件名和 picker payload 边界;移动壳根 `App.tsx` 也保持薄入口,只装配 `src/shell/ShellApp.tsx`,WebView 容器、深链、网络、生命周期、安全区、扫码 overlay 和安全策略全部留在 `src/shell/`;桌面壳 Rust 源码拆成 `apps/desktop-shell/src-tauri/src/app.rs`、`host_bridge/*.rs` 与 `shell/*.rs`,其中 `app.rs` 承接 Tauri builder / plugin / window 装配,`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 门面,薄 `main.rs` 只声明模块并调用 `app::run()`。根级 `npm run check:native-shells` 会锁定三端桥接层目录清单,避免后续把能力逻辑重新散落到页面、移动入口或桌面入口。 +2026-06-18 追加:微信、移动端和桌面端桥接层文件结构按职责对齐。微信小程序的 `web-view`、支付、九宫切图和订阅消息桥接逻辑统一迁入 `miniprogram/host-bridge/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面目录只保留页面生命周期、WXML/WXSS 和装配;移动壳拆成 `apps/mobile-shell/src/host-bridge/protocol.ts`、`capabilities.ts`、`dispatch.ts`、`files.ts`、`filePayloads.ts`、`navigation.test.ts`、`navigation.ts`、`scanner.ts`、`share.test.ts`、`share.ts` 和 facade `bridge.ts`,与桌面端 `host_bridge/protocol.rs`、`capabilities.rs`、`dispatch.rs`、`files.rs`、`file_payloads.rs`、`share.rs`、`mod.rs` 对齐,其中移动 `files.ts` 只承接 Expo 系统文件交互和 HostBridge 响应包装,`filePayloads.ts` 承接 MIME、大小、base64、文件名和 picker payload 边界;移动壳根 `App.tsx` 也保持薄入口,只装配 `src/shell/ShellApp.tsx`,WebView 容器、深链、网络、生命周期、安全区、扫码 overlay 和安全策略全部留在 `src/shell/`;桌面壳 Rust 源码拆成 `apps/desktop-shell/src-tauri/src/app.rs`、`host_bridge/*.rs` 与 `shell/*.rs`,其中 `app.rs` 承接 Tauri builder / plugin / window 装配,`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 门面,薄 `main.rs` 只声明模块并调用 `app::run()`。根级 `npm run check:native-shells` 会锁定三端桥接层目录清单,避免后续把能力逻辑重新散落到页面、移动入口或桌面入口。 2026-06-19 追加:HostBridge 载荷边界以共享契约为单一声明来源。`packages/shared/src/contracts/hostBridge.ts` 导出文本 / 图片 / 音频 MIME 清单、导入 / 导出字节上限、导出文件名 fallback 与长度上限,以及 request id、角标、剪贴板和本地通知文本长度边界;Expo 移动壳必须直接导入这些共享常量,不再本地重声明文件大小或 MIME 清单,并且文本 / 音频导入必须在读取内容前通过 picker `size` 或 Expo `File.size` 完成大小门禁,无法拿到可信 byte count 时直接拒绝导入;Tauri 桌面壳的配置检查会反查 Rust 镜像实现,拒绝文件大小、MIME 清单、文件名、通知、剪贴板或 request id 边界与共享契约漂移。新增文件类型或调整体积上限必须先更新共享契约、壳实现和门禁,再进入玩法或 H5 facade。 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index 3e63e9f32..396e1e045 100644 --- a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md +++ b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md @@ -43,7 +43,7 @@ AI H5 sandbox 生产替身词扫描只覆盖上述壳源码、分发配置、共享 HostBridge 契约和已接入真实宿主能力的 H5 调用链;Expo export、Tauri `target/`、Cargo / Metro 缓存和 release 构建产物不进入扫描范围,避免本地或 CI 生成文件污染源码门禁。 -结构门禁按完整相对路径反查文档和目录:微信桥接层为 `miniprogram/host-bridge/dispatch.js`、`miniprogram/host-bridge/payment.js`、`miniprogram/host-bridge/protocol.js`、`miniprogram/host-bridge/shareGrid.js`、`miniprogram/host-bridge/subscribeMessage.js`、`miniprogram/host-bridge/webView.js`;微信 shell 层为 `miniprogram/shell/payment.js`、`miniprogram/shell/shareGrid.js`、`miniprogram/shell/subscribeMessage.js`、`miniprogram/shell/webView.js`;微信页面包装层为 `miniprogram/pages/share-grid/index.js`、`miniprogram/pages/share-grid/index.json`、`miniprogram/pages/share-grid/index.wxml`、`miniprogram/pages/share-grid/index.wxss`、`miniprogram/pages/subscribe-message/index.js`、`miniprogram/pages/subscribe-message/index.json`、`miniprogram/pages/subscribe-message/index.wxml`、`miniprogram/pages/subscribe-message/index.wxss`、`miniprogram/pages/web-view/index.js`、`miniprogram/pages/web-view/index.json`、`miniprogram/pages/web-view/index.wxml`、`miniprogram/pages/web-view/index.wxss`、`miniprogram/pages/wechat-pay/index.js`、`miniprogram/pages/wechat-pay/index.json`、`miniprogram/pages/wechat-pay/index.wxml`、`miniprogram/pages/wechat-pay/index.wxss`;移动源码根为 `apps/mobile-shell/src/env.d.ts`;移动桥接层为 `apps/mobile-shell/src/host-bridge/appearance.test.ts`、`apps/mobile-shell/src/host-bridge/appearance.ts`、`apps/mobile-shell/src/host-bridge/badge.test.ts`、`apps/mobile-shell/src/host-bridge/badge.ts`、`apps/mobile-shell/src/host-bridge/bridge.ts`、`apps/mobile-shell/src/host-bridge/capabilities.ts`、`apps/mobile-shell/src/host-bridge/clipboard.test.ts`、`apps/mobile-shell/src/host-bridge/clipboard.ts`、`apps/mobile-shell/src/host-bridge/dispatch.ts`、`apps/mobile-shell/src/host-bridge/filePayloads.test.ts`、`apps/mobile-shell/src/host-bridge/filePayloads.ts`、`apps/mobile-shell/src/host-bridge/files.ts`、`apps/mobile-shell/src/host-bridge/haptics.test.ts`、`apps/mobile-shell/src/host-bridge/haptics.ts`、`apps/mobile-shell/src/host-bridge/navigation.ts`、`apps/mobile-shell/src/host-bridge/network.test.ts`、`apps/mobile-shell/src/host-bridge/network.ts`、`apps/mobile-shell/src/host-bridge/notifications.test.ts`、`apps/mobile-shell/src/host-bridge/notifications.ts`、`apps/mobile-shell/src/host-bridge/protocol.ts`、`apps/mobile-shell/src/host-bridge/runtime.test.ts`、`apps/mobile-shell/src/host-bridge/runtime.ts`、`apps/mobile-shell/src/host-bridge/scanner.ts`、`apps/mobile-shell/src/host-bridge/share.test.ts`、`apps/mobile-shell/src/host-bridge/share.ts`;移动 shell 层为 `apps/mobile-shell/src/shell/QrScannerOverlay.tsx`、`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/deepLink.ts`、`apps/mobile-shell/src/shell/lifecycle.ts`、`apps/mobile-shell/src/shell/loadFailure.ts`、`apps/mobile-shell/src/shell/navigation.ts`、`apps/mobile-shell/src/shell/network.ts`、`apps/mobile-shell/src/shell/runtime.ts`、`apps/mobile-shell/src/shell/safeArea.ts`、`apps/mobile-shell/src/shell/url.ts`、`apps/mobile-shell/src/shell/webViewGlobals.d.ts`、`apps/mobile-shell/src/shell/webViewHistory.ts`、`apps/mobile-shell/src/shell/webViewPolicy.ts`;桌面入口为 `apps/desktop-shell/src-tauri/src/app.rs`、`apps/desktop-shell/src-tauri/src/main.rs`;桌面桥接层为 `apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/badge.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/files.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/navigation.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/network.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/share.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/title.rs`;桌面 shell 层为 `apps/desktop-shell/src-tauri/src/shell/deep_link.rs`、`apps/desktop-shell/src-tauri/src/shell/events.rs`、`apps/desktop-shell/src-tauri/src/shell/file_drop.rs`、`apps/desktop-shell/src-tauri/src/shell/lifecycle.rs`、`apps/desktop-shell/src-tauri/src/shell/menu.rs`、`apps/desktop-shell/src-tauri/src/shell/mod.rs`、`apps/desktop-shell/src-tauri/src/shell/navigation.rs`、`apps/desktop-shell/src-tauri/src/shell/network.rs`、`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`apps/desktop-shell/src-tauri/src/shell/tray.rs`、`apps/desktop-shell/src-tauri/src/shell/url.rs`、`apps/desktop-shell/src-tauri/src/shell/webview.rs`、`apps/desktop-shell/src-tauri/src/shell/window_state.rs`。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。 + 结构门禁按完整相对路径反查文档和目录:微信桥接层为 `miniprogram/host-bridge/dispatch.js`、`miniprogram/host-bridge/payment.js`、`miniprogram/host-bridge/protocol.js`、`miniprogram/host-bridge/shareGrid.js`、`miniprogram/host-bridge/subscribeMessage.js`、`miniprogram/host-bridge/webView.js`;微信 shell 层为 `miniprogram/shell/payment.js`、`miniprogram/shell/shareGrid.js`、`miniprogram/shell/subscribeMessage.js`、`miniprogram/shell/webView.js`;微信页面包装层为 `miniprogram/pages/share-grid/index.js`、`miniprogram/pages/share-grid/index.json`、`miniprogram/pages/share-grid/index.wxml`、`miniprogram/pages/share-grid/index.wxss`、`miniprogram/pages/subscribe-message/index.js`、`miniprogram/pages/subscribe-message/index.json`、`miniprogram/pages/subscribe-message/index.wxml`、`miniprogram/pages/subscribe-message/index.wxss`、`miniprogram/pages/web-view/index.js`、`miniprogram/pages/web-view/index.json`、`miniprogram/pages/web-view/index.wxml`、`miniprogram/pages/web-view/index.wxss`、`miniprogram/pages/wechat-pay/index.js`、`miniprogram/pages/wechat-pay/index.json`、`miniprogram/pages/wechat-pay/index.wxml`、`miniprogram/pages/wechat-pay/index.wxss`;移动源码根为 `apps/mobile-shell/src/env.d.ts`;移动桥接层为 `apps/mobile-shell/src/host-bridge/appearance.test.ts`、`apps/mobile-shell/src/host-bridge/appearance.ts`、`apps/mobile-shell/src/host-bridge/badge.test.ts`、`apps/mobile-shell/src/host-bridge/badge.ts`、`apps/mobile-shell/src/host-bridge/bridge.ts`、`apps/mobile-shell/src/host-bridge/capabilities.ts`、`apps/mobile-shell/src/host-bridge/clipboard.test.ts`、`apps/mobile-shell/src/host-bridge/clipboard.ts`、`apps/mobile-shell/src/host-bridge/dispatch.ts`、`apps/mobile-shell/src/host-bridge/filePayloads.test.ts`、`apps/mobile-shell/src/host-bridge/filePayloads.ts`、`apps/mobile-shell/src/host-bridge/files.ts`、`apps/mobile-shell/src/host-bridge/haptics.test.ts`、`apps/mobile-shell/src/host-bridge/haptics.ts`、`apps/mobile-shell/src/host-bridge/navigation.test.ts`、`apps/mobile-shell/src/host-bridge/navigation.ts`、`apps/mobile-shell/src/host-bridge/network.test.ts`、`apps/mobile-shell/src/host-bridge/network.ts`、`apps/mobile-shell/src/host-bridge/notifications.test.ts`、`apps/mobile-shell/src/host-bridge/notifications.ts`、`apps/mobile-shell/src/host-bridge/protocol.ts`、`apps/mobile-shell/src/host-bridge/runtime.test.ts`、`apps/mobile-shell/src/host-bridge/runtime.ts`、`apps/mobile-shell/src/host-bridge/scanner.ts`、`apps/mobile-shell/src/host-bridge/share.test.ts`、`apps/mobile-shell/src/host-bridge/share.ts`;移动 shell 层为 `apps/mobile-shell/src/shell/QrScannerOverlay.tsx`、`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/deepLink.ts`、`apps/mobile-shell/src/shell/lifecycle.ts`、`apps/mobile-shell/src/shell/loadFailure.ts`、`apps/mobile-shell/src/shell/navigation.ts`、`apps/mobile-shell/src/shell/network.ts`、`apps/mobile-shell/src/shell/runtime.ts`、`apps/mobile-shell/src/shell/safeArea.ts`、`apps/mobile-shell/src/shell/url.ts`、`apps/mobile-shell/src/shell/webViewGlobals.d.ts`、`apps/mobile-shell/src/shell/webViewHistory.ts`、`apps/mobile-shell/src/shell/webViewPolicy.ts`;桌面入口为 `apps/desktop-shell/src-tauri/src/app.rs`、`apps/desktop-shell/src-tauri/src/main.rs`;桌面桥接层为 `apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/badge.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/files.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/navigation.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/network.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/share.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/title.rs`;桌面 shell 层为 `apps/desktop-shell/src-tauri/src/shell/deep_link.rs`、`apps/desktop-shell/src-tauri/src/shell/events.rs`、`apps/desktop-shell/src-tauri/src/shell/file_drop.rs`、`apps/desktop-shell/src-tauri/src/shell/lifecycle.rs`、`apps/desktop-shell/src-tauri/src/shell/menu.rs`、`apps/desktop-shell/src-tauri/src/shell/mod.rs`、`apps/desktop-shell/src-tauri/src/shell/navigation.rs`、`apps/desktop-shell/src-tauri/src/shell/network.rs`、`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`apps/desktop-shell/src-tauri/src/shell/tray.rs`、`apps/desktop-shell/src-tauri/src/shell/url.rs`、`apps/desktop-shell/src-tauri/src/shell/webview.rs`、`apps/desktop-shell/src-tauri/src/shell/window_state.rs`。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。 Tauri 桌面壳启动时必须按 `label="main"` 解析 `tauri.conf.json` 主窗口配置,并在创建 WebView 前补写 `native_app`、`tauri_desktop` 和真实 capability 上下文;缺少主窗口配置时启动直接失败,不允许按 `windows[0]` 兜底或无主窗口静默运行。 diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index c03e6d8a6..33959c420 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -264,6 +264,7 @@ const expectedMobileHostBridgeFiles = [ 'files.ts', 'haptics.test.ts', 'haptics.ts', + 'navigation.test.ts', 'navigation.ts', 'network.test.ts', 'network.ts',