diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 14481ec49..23b792963 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -732,6 +732,16 @@ function collectRustSourceBasenames(files) { return files.map((file) => file.pathname.split('/').pop()).sort(); } +function collectRustSourceRelativePaths(files) { + const rootPath = rustSourceDir.pathname.endsWith('/') + ? rustSourceDir.pathname + : `${rustSourceDir.pathname}/`; + + return files + .map((file) => file.pathname.replace(rootPath, '')) + .sort(); +} + function extractNativeAppTauriInvokeCommands(source) { return [...source.matchAll(/\btauriInvoke[\s\S]*?\(\s*([^,\n]+?)\s*,/g)] .map((match) => match[1].trim()) @@ -1024,13 +1034,14 @@ const sharedTauriCommand = extractTsStringConst( ); const allowedTauriCommands = [sharedTauriCommand]; const requiredRustHostModules = [ - 'desktop_host_bridge.rs', - 'desktop_host_bridge_files.rs', - 'desktop_host_bridge_protocol.rs', - 'desktop_host_bridge_share.rs', - 'desktop_shell_tray.rs', - 'desktop_shell_webview.rs', + 'host_bridge/files.rs', + 'host_bridge/mod.rs', + 'host_bridge/protocol.rs', + 'host_bridge/share.rs', 'main.rs', + 'shell/mod.rs', + 'shell/tray.rs', + 'shell/webview.rs', ]; const requiredRustHostSnippets = [ 'tauri_plugin_single_instance::init', @@ -1138,7 +1149,7 @@ assertSameList( 'shared Tauri HostBridge command', ); for (const moduleName of requiredRustHostModules) { - if (!collectRustSourceBasenames(rustHostSourceFiles).includes(moduleName)) { + if (!collectRustSourceRelativePaths(rustHostSourceFiles).includes(moduleName)) { throw new Error(`desktop shell Rust bridge module missing ${moduleName}`); } } diff --git a/apps/desktop-shell/src-tauri/src/desktop_host_bridge_files.rs b/apps/desktop-shell/src-tauri/src/host_bridge/files.rs similarity index 99% rename from apps/desktop-shell/src-tauri/src/desktop_host_bridge_files.rs rename to apps/desktop-shell/src-tauri/src/host_bridge/files.rs index 05e4a7ff1..cf4e476d6 100644 --- a/apps/desktop-shell/src-tauri/src/desktop_host_bridge_files.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/files.rs @@ -1,4 +1,4 @@ -use crate::desktop_host_bridge_protocol::{failed, HostBridgeRequest, HostBridgeResponse}; +use crate::host_bridge::protocol::{failed, HostBridgeRequest, HostBridgeResponse}; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use serde_json::{json, Value}; use std::fs; @@ -431,7 +431,7 @@ pub(crate) fn import_audio_file_payload(path: PathBuf) -> Result #[cfg(test)] mod tests { use super::*; - use crate::desktop_host_bridge_protocol::request; + use crate::host_bridge::protocol::request; #[test] fn export_file_name_normalization_rejects_path_like_characters() { diff --git a/apps/desktop-shell/src-tauri/src/desktop_host_bridge.rs b/apps/desktop-shell/src-tauri/src/host_bridge/mod.rs similarity index 98% rename from apps/desktop-shell/src-tauri/src/desktop_host_bridge.rs rename to apps/desktop-shell/src-tauri/src/host_bridge/mod.rs index 062f61afd..08ab885f0 100644 --- a/apps/desktop-shell/src-tauri/src/desktop_host_bridge.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/mod.rs @@ -1,15 +1,22 @@ -use crate::desktop_host_bridge_files::{ +pub(crate) mod files; +pub(crate) mod protocol; +mod share; + +pub(crate) use protocol::HostBridgeReplayState; +pub(crate) use share::DesktopShareState; + +use crate::host_bridge::files::{ export_audio_payload, export_image_payload, export_text_payload, import_audio_file_payload, import_image_file_payload, import_text_file_payload, write_export_bytes_file, write_export_text_file, }; -use crate::desktop_host_bridge_protocol::{ +use crate::host_bridge::protocol::{ capabilities, failed, normalize_request_id, ok, required_string_payload, validate_request, - HostBridgeReplayReservation, HostBridgeReplayState, HostBridgeRequest, HostBridgeResponse, - HostBridgeRuntime, HOST_BRIDGE_VERSION, + HostBridgeReplayReservation, HostBridgeRequest, HostBridgeResponse, HostBridgeRuntime, + HOST_BRIDGE_VERSION, }; -use crate::desktop_host_bridge_share::{share_text_from_request, DesktopShareState}; -use crate::desktop_shell_webview::{ +use crate::host_bridge::share::share_text_from_request; +use crate::shell::webview::{ color_scheme_from_theme, desktop_platform, normalize_external_url, normalize_native_page_url, resolve_desktop_network_status, }; @@ -527,7 +534,7 @@ pub(crate) async fn host_bridge_request( #[cfg(test)] mod tests { use super::*; - use crate::desktop_host_bridge_protocol::request; + use crate::host_bridge::protocol::request; #[test] fn runtime_response_reports_tauri_shell() { diff --git a/apps/desktop-shell/src-tauri/src/desktop_host_bridge_protocol.rs b/apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs similarity index 100% rename from apps/desktop-shell/src-tauri/src/desktop_host_bridge_protocol.rs rename to apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs diff --git a/apps/desktop-shell/src-tauri/src/desktop_host_bridge_share.rs b/apps/desktop-shell/src-tauri/src/host_bridge/share.rs similarity index 95% rename from apps/desktop-shell/src-tauri/src/desktop_host_bridge_share.rs rename to apps/desktop-shell/src-tauri/src/host_bridge/share.rs index 1c148ef28..c201db5ac 100644 --- a/apps/desktop-shell/src-tauri/src/desktop_host_bridge_share.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/share.rs @@ -1,5 +1,5 @@ -use crate::desktop_host_bridge_protocol::{failed, HostBridgeRequest, HostBridgeResponse}; -use crate::desktop_shell_webview::WEB_APP_ORIGIN; +use crate::host_bridge::protocol::{failed, HostBridgeRequest, HostBridgeResponse}; +use crate::shell::webview::WEB_APP_ORIGIN; use serde_json::Value; use std::sync::Mutex; @@ -84,7 +84,7 @@ pub(crate) fn share_text_from_request( #[cfg(test)] mod tests { use super::*; - use crate::desktop_host_bridge_protocol::request; + use crate::host_bridge::protocol::request; use serde_json::json; #[test] diff --git a/apps/desktop-shell/src-tauri/src/main.rs b/apps/desktop-shell/src-tauri/src/main.rs index 5758ad868..5be11efbc 100644 --- a/apps/desktop-shell/src-tauri/src/main.rs +++ b/apps/desktop-shell/src-tauri/src/main.rs @@ -1,18 +1,12 @@ -mod desktop_host_bridge; -mod desktop_host_bridge_files; -mod desktop_host_bridge_protocol; -mod desktop_host_bridge_share; -mod desktop_shell_tray; -mod desktop_shell_webview; +mod host_bridge; +mod shell; -use desktop_host_bridge::host_bridge_request; -use desktop_host_bridge_protocol::HostBridgeReplayState; -use desktop_host_bridge_share::DesktopShareState; -use desktop_shell_tray::{ +use host_bridge::{host_bridge_request, DesktopShareState, HostBridgeReplayState}; +use shell::tray::{ register_desktop_tray, register_desktop_window_close_events, resolve_desktop_single_instance_action, show_main_window, DesktopSingleInstanceAction, }; -use desktop_shell_webview::{ +use shell::webview::{ desktop_window_config_with_runtime_platform, emit_desktop_lifecycle_event, open_desktop_external_navigation, register_desktop_file_drop_events, register_desktop_lifecycle_events, register_desktop_network_events, diff --git a/apps/desktop-shell/src-tauri/src/shell/mod.rs b/apps/desktop-shell/src-tauri/src/shell/mod.rs new file mode 100644 index 000000000..a7d7130e2 --- /dev/null +++ b/apps/desktop-shell/src-tauri/src/shell/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod tray; +pub(crate) mod webview; diff --git a/apps/desktop-shell/src-tauri/src/desktop_shell_tray.rs b/apps/desktop-shell/src-tauri/src/shell/tray.rs similarity index 100% rename from apps/desktop-shell/src-tauri/src/desktop_shell_tray.rs rename to apps/desktop-shell/src-tauri/src/shell/tray.rs diff --git a/apps/desktop-shell/src-tauri/src/desktop_shell_webview.rs b/apps/desktop-shell/src-tauri/src/shell/webview.rs similarity index 99% rename from apps/desktop-shell/src-tauri/src/desktop_shell_webview.rs rename to apps/desktop-shell/src-tauri/src/shell/webview.rs index 30f3aab23..da66781ea 100644 --- a/apps/desktop-shell/src-tauri/src/desktop_shell_webview.rs +++ b/apps/desktop-shell/src-tauri/src/shell/webview.rs @@ -1,5 +1,5 @@ -use crate::desktop_host_bridge_files::{import_image_file_payload, import_image_mime_type}; -use crate::desktop_host_bridge_protocol::{HOST_BRIDGE_PROTOCOL, HOST_BRIDGE_VERSION}; +use crate::host_bridge::files::{import_image_file_payload, import_image_mime_type}; +use crate::host_bridge::protocol::{HOST_BRIDGE_PROTOCOL, HOST_BRIDGE_VERSION}; use serde_json::{json, Value}; use std::net::{TcpStream, ToSocketAddrs}; use std::path::PathBuf; diff --git a/apps/mobile-shell/App.tsx b/apps/mobile-shell/App.tsx index f67a37b30..cbe79fcb0 100644 --- a/apps/mobile-shell/App.tsx +++ b/apps/mobile-shell/App.tsx @@ -16,25 +16,25 @@ import { configureMobileHostBridgeNavigation, handleMobileHostBridgeMessage, resolveMobileHostCapabilities, -} from './src/mobileHostBridge'; -import { buildMobileShellUrlFromDeepLink } from './src/mobileShellDeepLink'; -import { lifecyclePayloadFromAppState } from './src/mobileShellLifecycle'; +} from './src/host-bridge/mobileHostBridge'; +import { buildMobileShellUrlFromDeepLink } from './src/shell/mobileShellDeepLink'; +import { lifecyclePayloadFromAppState } from './src/shell/mobileShellLifecycle'; import { resolveMobileShellExternalUrl, shouldAcceptMobileShellHostBridgeMessage, shouldOpenInMobileShellWebView, -} from './src/mobileShellNavigation'; +} from './src/shell/mobileShellNavigation'; import { getMobileNetworkStatus, subscribeMobileNetworkStatus, -} from './src/mobileShellNetwork'; -import { MOBILE_SHELL_HOST_VERSION } from './src/mobileShellRuntime'; -import { MOBILE_SHELL_SAFE_AREA_EDGES } from './src/mobileShellSafeArea'; +} from './src/shell/mobileShellNetwork'; +import { MOBILE_SHELL_HOST_VERSION } from './src/shell/mobileShellRuntime'; +import { MOBILE_SHELL_SAFE_AREA_EDGES } from './src/shell/mobileShellSafeArea'; import { buildMobileShellUrl, resolveMobileShellBaseWebUrl, -} from './src/mobileShellUrl'; -import { BLOCK_WEBVIEW_DOWNLOAD_SCRIPT } from './src/mobileShellWebViewPolicy'; +} from './src/shell/mobileShellUrl'; +import { BLOCK_WEBVIEW_DOWNLOAD_SCRIPT } from './src/shell/mobileShellWebViewPolicy'; function buildHostBridgeMessageScript(message: unknown) { return `window.dispatchEvent(new MessageEvent('message', { data: ${JSON.stringify( diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 0d3a4e6c3..7963a8b41 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -6,11 +6,11 @@ const appConfigPath = new URL('../app.json', import.meta.url); const appConfig = JSON.parse(fs.readFileSync(appConfigPath, 'utf8')).expo; const appPath = new URL('../App.tsx', import.meta.url); const appSource = fs.readFileSync(appPath, 'utf8'); -const bridgePath = new URL('../src/mobileHostBridge.ts', import.meta.url); +const bridgePath = new URL('../src/host-bridge/mobileHostBridge.ts', import.meta.url); const bridgeSource = fs.readFileSync(bridgePath, 'utf8'); -const mobileShellUrlPath = new URL('../src/mobileShellUrl.ts', import.meta.url); +const mobileShellUrlPath = new URL('../src/shell/mobileShellUrl.ts', import.meta.url); const mobileShellUrlSource = fs.readFileSync(mobileShellUrlPath, 'utf8'); -const mobileShellRuntimePath = new URL('../src/mobileShellRuntime.ts', import.meta.url); +const mobileShellRuntimePath = new URL('../src/shell/mobileShellRuntime.ts', import.meta.url); const mobileShellRuntimeSource = fs.readFileSync(mobileShellRuntimePath, 'utf8'); const sharedContractPath = new URL( '../../../packages/shared/src/contracts/hostBridge.ts', diff --git a/apps/mobile-shell/src/mobileHostBridge.test.ts b/apps/mobile-shell/src/host-bridge/mobileHostBridge.test.ts similarity index 99% rename from apps/mobile-shell/src/mobileHostBridge.test.ts rename to apps/mobile-shell/src/host-bridge/mobileHostBridge.test.ts index 8dea89953..3115d1ff8 100644 --- a/apps/mobile-shell/src/mobileHostBridge.test.ts +++ b/apps/mobile-shell/src/host-bridge/mobileHostBridge.test.ts @@ -20,7 +20,7 @@ import { type HostBridgeMethod, type HostBridgeRequest, type HostBridgeResponse, -} from '../../../packages/shared/src/contracts/hostBridge'; +} from '../../../../packages/shared/src/contracts/hostBridge'; import { configureMobileHostBridgeNavigation, handleMobileHostBridgeMessage, diff --git a/apps/mobile-shell/src/mobileHostBridge.ts b/apps/mobile-shell/src/host-bridge/mobileHostBridge.ts similarity index 99% rename from apps/mobile-shell/src/mobileHostBridge.ts rename to apps/mobile-shell/src/host-bridge/mobileHostBridge.ts index 663163717..21694fa73 100644 --- a/apps/mobile-shell/src/mobileHostBridge.ts +++ b/apps/mobile-shell/src/host-bridge/mobileHostBridge.ts @@ -49,10 +49,10 @@ import { type OpenExternalUrlPayload, type SetBadgeCountPayload, type ShareOpenPayload, -} from '../../../packages/shared/src/contracts/hostBridge'; -import { resolveMobileShellWebViewUrl } from './mobileShellNavigation'; -import { getMobileNetworkStatus } from './mobileShellNetwork'; -import { MOBILE_SHELL_HOST_VERSION } from './mobileShellRuntime'; +} from '../../../../packages/shared/src/contracts/hostBridge'; +import { resolveMobileShellWebViewUrl } from '../shell/mobileShellNavigation'; +import { getMobileNetworkStatus } from '../shell/mobileShellNetwork'; +import { MOBILE_SHELL_HOST_VERSION } from '../shell/mobileShellRuntime'; const WEB_APP_ORIGIN = 'https://app.genarrative.world'; const EXPORT_TEXT_MAX_BYTES = 5 * 1024 * 1024; diff --git a/apps/mobile-shell/src/mobileShellDeepLink.test.ts b/apps/mobile-shell/src/shell/mobileShellDeepLink.test.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellDeepLink.test.ts rename to apps/mobile-shell/src/shell/mobileShellDeepLink.test.ts diff --git a/apps/mobile-shell/src/mobileShellDeepLink.ts b/apps/mobile-shell/src/shell/mobileShellDeepLink.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellDeepLink.ts rename to apps/mobile-shell/src/shell/mobileShellDeepLink.ts diff --git a/apps/mobile-shell/src/mobileShellLifecycle.test.ts b/apps/mobile-shell/src/shell/mobileShellLifecycle.test.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellLifecycle.test.ts rename to apps/mobile-shell/src/shell/mobileShellLifecycle.test.ts diff --git a/apps/mobile-shell/src/mobileShellLifecycle.ts b/apps/mobile-shell/src/shell/mobileShellLifecycle.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellLifecycle.ts rename to apps/mobile-shell/src/shell/mobileShellLifecycle.ts diff --git a/apps/mobile-shell/src/mobileShellNavigation.test.ts b/apps/mobile-shell/src/shell/mobileShellNavigation.test.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellNavigation.test.ts rename to apps/mobile-shell/src/shell/mobileShellNavigation.test.ts diff --git a/apps/mobile-shell/src/mobileShellNavigation.ts b/apps/mobile-shell/src/shell/mobileShellNavigation.ts similarity index 91% rename from apps/mobile-shell/src/mobileShellNavigation.ts rename to apps/mobile-shell/src/shell/mobileShellNavigation.ts index 0981d1734..e6b769a9f 100644 --- a/apps/mobile-shell/src/mobileShellNavigation.ts +++ b/apps/mobile-shell/src/shell/mobileShellNavigation.ts @@ -1,4 +1,4 @@ -import { normalizeHostBridgeExternalUrl } from '../../../packages/shared/src/contracts/hostBridge'; +import { normalizeHostBridgeExternalUrl } from '../../../../packages/shared/src/contracts/hostBridge'; export function shouldOpenInMobileShellWebView( rawUrl: string, diff --git a/apps/mobile-shell/src/mobileShellNetwork.test.ts b/apps/mobile-shell/src/shell/mobileShellNetwork.test.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellNetwork.test.ts rename to apps/mobile-shell/src/shell/mobileShellNetwork.test.ts diff --git a/apps/mobile-shell/src/mobileShellNetwork.ts b/apps/mobile-shell/src/shell/mobileShellNetwork.ts similarity index 94% rename from apps/mobile-shell/src/mobileShellNetwork.ts rename to apps/mobile-shell/src/shell/mobileShellNetwork.ts index aedbe2819..5bb9a25f5 100644 --- a/apps/mobile-shell/src/mobileShellNetwork.ts +++ b/apps/mobile-shell/src/shell/mobileShellNetwork.ts @@ -3,7 +3,7 @@ import * as Network from 'expo-network'; import { type NetworkStatusResult, normalizeHostBridgeConnectionType, -} from '../../../packages/shared/src/contracts/hostBridge'; +} from '../../../../packages/shared/src/contracts/hostBridge'; export function normalizeMobileNetworkStatus( state: Network.NetworkState, diff --git a/apps/mobile-shell/src/mobileShellRuntime.ts b/apps/mobile-shell/src/shell/mobileShellRuntime.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellRuntime.ts rename to apps/mobile-shell/src/shell/mobileShellRuntime.ts diff --git a/apps/mobile-shell/src/mobileShellSafeArea.test.ts b/apps/mobile-shell/src/shell/mobileShellSafeArea.test.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellSafeArea.test.ts rename to apps/mobile-shell/src/shell/mobileShellSafeArea.test.ts diff --git a/apps/mobile-shell/src/mobileShellSafeArea.ts b/apps/mobile-shell/src/shell/mobileShellSafeArea.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellSafeArea.ts rename to apps/mobile-shell/src/shell/mobileShellSafeArea.ts diff --git a/apps/mobile-shell/src/mobileShellUrl.test.ts b/apps/mobile-shell/src/shell/mobileShellUrl.test.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellUrl.test.ts rename to apps/mobile-shell/src/shell/mobileShellUrl.test.ts diff --git a/apps/mobile-shell/src/mobileShellUrl.ts b/apps/mobile-shell/src/shell/mobileShellUrl.ts similarity index 96% rename from apps/mobile-shell/src/mobileShellUrl.ts rename to apps/mobile-shell/src/shell/mobileShellUrl.ts index 001f67854..33e14e560 100644 --- a/apps/mobile-shell/src/mobileShellUrl.ts +++ b/apps/mobile-shell/src/shell/mobileShellUrl.ts @@ -1,7 +1,7 @@ import type { HostBridgeCapability, NativeHostPlatform, -} from '../../../packages/shared/src/contracts/hostBridge'; +} from '../../../../packages/shared/src/contracts/hostBridge'; export type MobileShellUrlOptions = { platform: Extract; diff --git a/apps/mobile-shell/src/mobileShellWebViewPolicy.test.ts b/apps/mobile-shell/src/shell/mobileShellWebViewPolicy.test.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellWebViewPolicy.test.ts rename to apps/mobile-shell/src/shell/mobileShellWebViewPolicy.test.ts diff --git a/apps/mobile-shell/src/mobileShellWebViewPolicy.ts b/apps/mobile-shell/src/shell/mobileShellWebViewPolicy.ts similarity index 100% rename from apps/mobile-shell/src/mobileShellWebViewPolicy.ts rename to apps/mobile-shell/src/shell/mobileShellWebViewPolicy.ts diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 36c8e1619..4f9a1c31a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2397,14 +2397,14 @@ - 背景:H5 transport 会为每个 HostBridge 请求生成 id 并设置超时,但系统分享、外链打开、文件选择 / 保存、本地通知和窗口导航等宿主副作用如果收到重复 id,不应因为消息重试、快速双发或 WebView 事件抖动被执行两次。 - 决策:Expo 移动壳缓存已完成响应,并让进行中的同 id 请求共用同一个执行 Promise;Tauri 桌面壳在唯一 `host_bridge_request` command 外层通过 `HostBridgeReplayState` 让同 id 请求等待 / 回放首次结果。重复 id 只返回首次响应,不二次执行宿主能力。 -- 影响范围:`apps/mobile-shell/src/mobileHostBridge.ts`、`apps/desktop-shell/src-tauri/src/main.rs`、两端配置检查、Expo / Tauri HostBridge 方案文档。 +- 影响范围:`apps/mobile-shell/src/host-bridge/mobileHostBridge.ts`、`apps/desktop-shell/src-tauri/src/host_bridge/`、`apps/desktop-shell/src-tauri/src/main.rs`、两端配置检查、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 ## 2026-06-18 HostBridge request envelope 校验 - 背景:HostBridge 请求来自 H5 WebView / Tauri 注入通道,TypeScript 类型不能替代宿主运行时校验;空 id、控制字符 id、过长 id 或未知 method 如果进入 replay / 能力分发,可能污染缓存、绕过方法白名单或造成错误语义混乱。 - 决策:共享契约提供 `isHostBridgeMethod` 和 `normalizeHostBridgeRequestId`;Expo 壳直接复用,Tauri 壳镜像同一 `HOST_BRIDGE_METHODS` 和 request id 规则。request id 归一后必须为 1-120 字符且不含控制字符;未知 method 在进入能力分发前返回 `invalid_request`,只有白名单内但当前壳未实现的登录 / 支付等 method 返回 `unsupported_method`。 -- 影响范围:`packages/shared/src/contracts/hostBridge.ts`、`apps/mobile-shell/src/mobileHostBridge.ts`、`apps/desktop-shell/src-tauri/src/main.rs`、两端配置检查、Expo / Tauri HostBridge 方案文档。 +- 影响范围:`packages/shared/src/contracts/hostBridge.ts`、`apps/mobile-shell/src/host-bridge/mobileHostBridge.ts`、`apps/desktop-shell/src-tauri/src/host_bridge/`、`apps/desktop-shell/src-tauri/src/main.rs`、两端配置检查、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 ## 2026-06-18 HostBridge method 白名单跨壳门禁 @@ -2473,6 +2473,6 @@ ## 2026-06-18 三端宿主桥接层文件结构对齐 - 背景:微信小程序壳、Expo 移动壳和 Tauri 桌面壳都在承接宿主能力;如果微信页面继续散落 `index.shared.js`,桌面端继续把桥接分发堆在 `main.rs`,后续新增登录、支付、文件、通知或 sandbox 转发能力时会很难跨端对照 owner。 -- 决策:三端桥接层按职责对齐。微信小程序页面路由不改,但可测试桥接逻辑统一放到 `miniprogram/host-bridge/wechatHostBridge*.js`,页面目录只保留生命周期和装配;Expo 移动壳保持 `mobileHostBridge.ts` 负责协议分发,`mobileShell*.ts` 负责 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳拆成 `desktop_host_bridge*.rs` 与 `desktop_shell*.rs`,`main.rs` 只做 builder、plugin、窗口和状态装配。`scripts/check-native-shells.mjs` 锁定三端桥接层文件清单。 +- 决策:三端桥接层按职责对齐。微信小程序页面路由不改,但可测试桥接逻辑统一放到 `miniprogram/host-bridge/wechatHostBridge*.js`,页面目录只保留生命周期和装配;Expo 移动壳拆成 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 负责协议分发,`apps/mobile-shell/src/shell/mobileShell*.ts` 负责 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳拆成 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 与 `apps/desktop-shell/src-tauri/src/shell/*.rs`,`main.rs` 只做 builder、plugin、窗口和状态装配。`scripts/check-native-shells.mjs` 锁定三端桥接层目录清单。 - 影响范围:`miniprogram/host-bridge/`、`miniprogram/pages/*/index.js`、`apps/mobile-shell/src/`、`apps/desktop-shell/src-tauri/src/`、`scripts/check-native-shells.mjs`、宿主壳方案文档。 - 验证方式:`npm run test -- miniprogram/host-bridge/wechatHostBridgeWebView.test.js miniprogram/host-bridge/wechatHostBridgePayment.test.js miniprogram/host-bridge/wechatHostBridgeShareGrid.test.js miniprogram/host-bridge/wechatHostBridgeSubscribeMessage.test.js miniprogram/pages/web-view/index.style.test.js`、`npm run check:native-shells`、`npm run typecheck`、`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 571a3ed19..0b1601317 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -63,7 +63,7 @@ src/ 已落地:`packages/shared/src/contracts/hostBridge.ts` 保存消息 envelope、method、payload 和错误码,H5、Expo 壳与 Tauri 壳共享同一份协议类型。 -三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,可测试桥接逻辑统一放在 `miniprogram/host-bridge/wechatHostBridge*.js`;Expo 移动壳使用 `apps/mobile-shell/src/mobileHostBridge.ts` 承接协议分发,`mobileShell*.ts` 承接 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/desktop_host_bridge*.rs` 承接协议、分发、文件和分享,`desktop_shell*.rs` 承接 WebView、托盘和容器行为,`main.rs` 只保留 Tauri builder / plugin / window 装配。 +三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,可测试桥接逻辑统一放在 `miniprogram/host-bridge/wechatHostBridge*.js`;Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 承接协议分发,`apps/mobile-shell/src/shell/mobileShell*.ts` 承接 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 承接协议、分发、文件和分享,`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接 WebView、托盘和容器行为,`main.rs` 只保留 Tauri builder / plugin / window 装配。 ## HostBridge 消息协议 @@ -310,7 +310,7 @@ GameBridge 禁止: 2026-06-18 追加:移动壳安装包身份固定为 `world.genarrative.mobile`。Expo `app.json` 中的 `ios.bundleIdentifier` 与 `android.package` 使用同一包标识,应用版本为 `0.1.0`,iOS `buildNumber` 从字符串 `"1"` 起步,Android `versionCode` 从整数 `1` 起步;后续每次生成可分发安装包时只递增构建号 / versionCode,产品版本号按发布节奏单独调整。`apps/mobile-shell/scripts/check-config.mjs` 会校验这些字段与 `package.json` 版本一致,避免 iOS、Android 和 H5 HostBridge `hostVersion` 发生静默漂移;`npm run mobile-shell:config` 会调用真实 Expo CLI 解析 public managed config,确认最终 Expo 配置仍保留同一包身份、深链、安全字段、插件权限和 HostBridge 版本。当前仍不写入假商店上架信息、假更新端点或占位渠道 SDK 配置。 -2026-06-18 追加:移动壳 H5 入口 query 和 `host.getRuntime` 回包统一读取 `MOBILE_SHELL_HOST_VERSION`,该常量必须与 Expo `app.json` / `package.json` 版本一致。配置检查会拒绝在 `App.tsx` 或 `mobileHostBridge.ts` 内重新散落硬编码版本,避免升级移动安装包时 H5 首屏上下文和宿主 runtime 回读版本不一致。 +2026-06-18 追加:移动壳 H5 入口 query 和 `host.getRuntime` 回包统一读取 `MOBILE_SHELL_HOST_VERSION`,该常量必须与 Expo `app.json` / `package.json` 版本一致。配置检查会拒绝在 `App.tsx` 或 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 内重新散落硬编码版本,避免升级移动安装包时 H5 首屏上下文和宿主 runtime 回读版本不一致。 2026-06-18 追加:移动壳默认显式关闭 Expo OTA 更新,直到存在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程后再接入。`app.json` 只允许 `updates.enabled=false`,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖;`apps/mobile-shell/scripts/check-config.mjs` 和 Expo public config smoke 会共同拒绝这些发布通道能力被提前打开,移动壳生产入口、HostBridge 和 URL/runtime 配置也不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates。由于移动壳运行依赖会从根安装树解析,根 H5 `package.json` 也不得直接安装这些移动端发布通道、崩溃上报、analytics 或 CodePush SDK;根 `package-lock.json` 也不得解析 `expo-updates`、Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 等真实发布 / 观测 SDK。`expo-application` 可能由 Expo 自身传递解析,但项目不得把它作为 direct dependency 主动用于渠道逻辑。 @@ -411,7 +411,7 @@ GameBridge 禁止: 2026-06-18 追加:移动壳 HostBridge 消息入口增加来源校验。`onMessage` 不只依赖导航拦截和 `originWhitelist`,还会读取 `event.nativeEvent.url`,只有同源主站页面才能进入 `handleMobileHostBridgeMessage`;`about:blank`、外域 URL、协议降级或危险协议页面发来的消息全部丢弃,不返回 HostBridge 错误细节。该校验与 `navigation.openNativePage` 共用同源规则,防止历史中间页或异常页面在带完整 HostBridge 的 WebView 中发起宿主能力请求。 -2026-06-18 追加:微信、移动端和桌面端桥接层文件结构按职责对齐。微信小程序的 `web-view`、支付、九宫切图和订阅消息桥接逻辑统一迁入 `miniprogram/host-bridge/wechatHostBridge*.js`,页面目录只保留页面生命周期、WXML/WXSS 和装配;移动壳继续保持 `mobileHostBridge.ts` + `mobileShell*.ts`;桌面壳 Rust 源码拆成 `desktop_host_bridge.rs`、`desktop_host_bridge_protocol.rs`、`desktop_host_bridge_files.rs`、`desktop_host_bridge_share.rs`、`desktop_shell_webview.rs`、`desktop_shell_tray.rs` 和薄 `main.rs`。根级 `npm run check:native-shells` 会锁定三端桥接层文件清单,避免后续把能力逻辑重新散落到页面或桌面入口。 +2026-06-18 追加:微信、移动端和桌面端桥接层文件结构按职责对齐。微信小程序的 `web-view`、支付、九宫切图和订阅消息桥接逻辑统一迁入 `miniprogram/host-bridge/wechatHostBridge*.js`,页面目录只保留页面生命周期、WXML/WXSS 和装配;移动壳拆成 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 和 `apps/mobile-shell/src/shell/mobileShell*.ts`;桌面壳 Rust 源码拆成 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 与 `apps/desktop-shell/src-tauri/src/shell/*.rs`,薄 `main.rs` 只声明两个模块并装配 Tauri builder / plugin / window。根级 `npm run check:native-shells` 会锁定三端桥接层目录清单,避免后续把能力逻辑重新散落到页面或桌面入口。 ### Phase 4:宿主能力扩展 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index 93946b492..959a6c3d1 100644 --- a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md +++ b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md @@ -37,7 +37,7 @@ AI H5 sandbox -> parent HostBridge adapter ``` -桥接层文件结构按宿主统一为“协议 / 分发 / 宿主容器行为”三类职责。微信小程序的可测试桥接逻辑统一放在 `miniprogram/host-bridge/wechatHostBridge*.js`,页面目录只保留页面装配;Expo 移动壳使用 `mobileHostBridge.ts` 和 `mobileShell*.ts`;Tauri 桌面壳使用 `desktop_host_bridge*.rs` 和 `desktop_shell*.rs`,`main.rs` 不再承载 HostBridge 分发细节。`npm run check:native-shells` 会检查这些文件清单。 +桥接层文件结构按宿主统一为“协议 / 分发 / 宿主容器行为”三类职责。微信小程序的可测试桥接逻辑统一放在 `miniprogram/host-bridge/wechatHostBridge*.js`,页面目录只保留页面装配;Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/mobileHostBridge.ts` 承接协议分发,`apps/mobile-shell/src/shell/mobileShell*.ts` 承接 URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 承接协议、分发、文件和分享,`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接 WebView、托盘和容器行为,`main.rs` 只保留 Tauri builder / plugin / window 装配。`npm run check:native-shells` 会检查这些目录清单。 ## 首批能力 diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index 43176ddbf..01657effc 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -17,25 +17,37 @@ const expectedWechatHostBridgeFiles = [ 'wechatHostBridgeWebView.js', 'wechatHostBridgeWebView.test.js', ]; -const expectedMobileShellFiles = [ +const expectedMobileHostBridgeFiles = [ + 'mobileHostBridge.test.ts', 'mobileHostBridge.ts', +]; +const expectedMobileShellFiles = [ + 'mobileShellDeepLink.test.ts', 'mobileShellDeepLink.ts', + 'mobileShellLifecycle.test.ts', 'mobileShellLifecycle.ts', + 'mobileShellNavigation.test.ts', 'mobileShellNavigation.ts', + 'mobileShellNetwork.test.ts', 'mobileShellNetwork.ts', 'mobileShellRuntime.ts', + 'mobileShellSafeArea.test.ts', 'mobileShellSafeArea.ts', + 'mobileShellUrl.test.ts', 'mobileShellUrl.ts', + 'mobileShellWebViewPolicy.test.ts', 'mobileShellWebViewPolicy.ts', ]; +const expectedDesktopHostBridgeRustFiles = [ + 'files.rs', + 'mod.rs', + 'protocol.rs', + 'share.rs', +]; const expectedDesktopShellRustFiles = [ - 'desktop_host_bridge.rs', - 'desktop_host_bridge_files.rs', - 'desktop_host_bridge_protocol.rs', - 'desktop_host_bridge_share.rs', - 'desktop_shell_tray.rs', - 'desktop_shell_webview.rs', - 'main.rs', + 'mod.rs', + 'tray.rs', + 'webview.rs', ]; const productionShellExtensions = new Set([ '.json', @@ -210,11 +222,21 @@ function assertHostBridgeLayerLayout() { } } - const mobileShellFiles = fs - .readdirSync('apps/mobile-shell/src', { withFileTypes: true }) - .filter((entry) => entry.isFile() && !entry.name.endsWith('.test.ts')) + const mobileHostBridgeFiles = fs + .readdirSync('apps/mobile-shell/src/host-bridge', { withFileTypes: true }) + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .sort(); + assertSameList( + mobileHostBridgeFiles, + expectedMobileHostBridgeFiles, + 'mobile host bridge files', + ); + + const mobileShellFiles = fs + .readdirSync('apps/mobile-shell/src/shell', { withFileTypes: true }) + .filter((entry) => entry.isFile()) .map((entry) => entry.name) - .filter((name) => name.startsWith('mobileHostBridge') || name.startsWith('mobileShell')) .sort(); assertSameList( mobileShellFiles, @@ -222,11 +244,33 @@ function assertHostBridgeLayerLayout() { 'mobile shell bridge files', ); - const desktopShellRustFiles = fs + const desktopEntrypointRustFiles = fs .readdirSync('apps/desktop-shell/src-tauri/src', { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.endsWith('.rs')) .map((entry) => entry.name) .sort(); + assertSameList( + desktopEntrypointRustFiles, + ['main.rs'], + 'desktop shell entrypoint Rust files', + ); + + const desktopHostBridgeRustFiles = fs + .readdirSync('apps/desktop-shell/src-tauri/src/host_bridge', { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.rs')) + .map((entry) => entry.name) + .sort(); + assertSameList( + desktopHostBridgeRustFiles, + expectedDesktopHostBridgeRustFiles, + 'desktop host bridge Rust files', + ); + + const desktopShellRustFiles = fs + .readdirSync('apps/desktop-shell/src-tauri/src/shell', { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.rs')) + .map((entry) => entry.name) + .sort(); assertSameList( desktopShellRustFiles, expectedDesktopShellRustFiles,