拆薄桌面壳入口

新增桌面壳 app.rs 承接 Tauri 应用装配

收敛 main.rs 为薄入口并调用 app::run

扩展原生壳门禁锁定桌面入口职责边界

更新宿主壳方案和共享决策记录
This commit is contained in:
2026-06-19 04:07:08 +08:00
parent 49fffbca74
commit 81b51ed405
7 changed files with 154 additions and 92 deletions
+35 -5
View File
@@ -34,6 +34,8 @@ const nativeAppHostBridgePath = new URL(
import.meta.url,
);
const nativeAppHostBridgeSource = fs.readFileSync(nativeAppHostBridgePath, 'utf8');
const appPath = new URL('../src-tauri/src/app.rs', import.meta.url);
const app = fs.readFileSync(appPath, 'utf8');
const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url);
const main = fs.readFileSync(mainPath, 'utf8');
const rustSourceDir = new URL('../src-tauri/src/', import.meta.url);
@@ -852,6 +854,7 @@ function extractTauriInvokeCommands(source) {
return match[1]
.split(',')
.map((command) => command.trim())
.map((command) => command.split('::').pop())
.filter(Boolean);
}
@@ -1395,6 +1398,7 @@ const requiredRustHostModules = [
'host_bridge/mod.rs',
'host_bridge/protocol.rs',
'host_bridge/share.rs',
'app.rs',
'main.rs',
'shell/deep_link.rs',
'shell/events.rs',
@@ -1640,7 +1644,7 @@ assertSameList(
'desktop shell build manifest commands',
);
assertSameList(
extractTauriInvokeCommands(main),
extractTauriInvokeCommands(app),
allowedTauriCommands,
'desktop shell invoke handler commands',
);
@@ -1700,24 +1704,50 @@ const orderedDesktopPluginSnippets = [
'tauri_plugin_opener::init()',
];
for (const pluginSnippet of orderedDesktopPluginSnippets) {
if (!main.includes(pluginSnippet)) {
throw new Error(`desktop shell main.rs missing plugin ${pluginSnippet}`);
if (!app.includes(pluginSnippet)) {
throw new Error(`desktop shell app.rs missing plugin ${pluginSnippet}`);
}
}
for (let index = 1; index < orderedDesktopPluginSnippets.length; index += 1) {
const previousPlugin = orderedDesktopPluginSnippets[index - 1];
const currentPlugin = orderedDesktopPluginSnippets[index];
if (main.indexOf(previousPlugin) > main.indexOf(currentPlugin)) {
if (app.indexOf(previousPlugin) > app.indexOf(currentPlugin)) {
throw new Error(
`desktop shell plugin order drifted: ${previousPlugin} must be registered before ${currentPlugin}`,
);
}
}
if (main.includes('single-instance",') || main.includes('"single-instance"')) {
if (app.includes('single-instance",') || app.includes('"single-instance"')) {
throw new Error('desktop shell must not emit secondary-instance argv to H5');
}
if (!main.includes('mod app;') || !main.includes('app::run();')) {
throw new Error('desktop shell main.rs must stay a thin app entrypoint');
}
for (const entrypointOnlySnippet of [
'tauri::Builder::default()',
'tauri::generate_handler!',
'WebviewWindowBuilder::from_config',
]) {
if (main.includes(entrypointOnlySnippet)) {
throw new Error(
`desktop shell main.rs must not own Tauri app setup: ${entrypointOnlySnippet}`,
);
}
}
for (const appSetupSnippet of [
'tauri::Builder::default()',
'crate::host_bridge::host_bridge_request',
'WebviewWindowBuilder::from_config',
]) {
if (!app.includes(appSetupSnippet)) {
throw new Error(`desktop shell app.rs missing setup snippet ${appSetupSnippet}`);
}
}
for (const snippet of requiredRustHostSnippets) {
if (!rustHostSource.includes(snippet)) {
throw new Error(`desktop shell Rust host bridge missing ${snippet}`);
+88
View File
@@ -0,0 +1,88 @@
use crate::host_bridge::{DesktopShareState, HostBridgeReplayState};
use crate::shell::deep_link::{
register_desktop_deep_link_events, register_desktop_deep_link_schemes,
};
use crate::shell::menu::register_desktop_app_menu;
use crate::shell::tray::{
register_desktop_tray, register_desktop_window_close_events,
resolve_desktop_single_instance_action, show_main_window, DesktopSingleInstanceAction,
};
use crate::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_navigation_events,
register_desktop_network_events, replay_desktop_webview_state,
should_allow_desktop_webview_download, should_allow_desktop_webview_navigation,
should_replay_desktop_webview_state_on_page_load,
};
use crate::shell::window_state::desktop_window_state_plugin;
use tauri::webview::NewWindowResponse;
pub(crate) fn run() {
tauri::Builder::default()
.manage(DesktopShareState::default())
.manage(HostBridgeReplayState::default())
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
match resolve_desktop_single_instance_action() {
DesktopSingleInstanceAction::ShowMainWindow => {
let _ = show_main_window(app);
}
}
}))
.plugin(desktop_window_state_plugin())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
register_desktop_app_menu(app)?;
let tray_registered = match register_desktop_tray(app) {
Ok(()) => true,
Err(error) => {
eprintln!("desktop tray registration failed: {error}");
false
}
};
let window_config = app.config().app.windows.get(0).cloned();
if let Some(config) = window_config {
let config = desktop_window_config_with_runtime_platform(config);
let app_handle = app.handle().clone();
let new_window_app_handle = app.handle().clone();
let window = tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?
.on_navigation(move |url| {
if should_allow_desktop_webview_navigation(url) {
true
} else {
open_desktop_external_navigation(&app_handle, url);
false
}
})
.on_new_window(move |url, _features| {
open_desktop_external_navigation(&new_window_app_handle, &url);
NewWindowResponse::Deny
})
.on_page_load(|window, payload| {
if should_replay_desktop_webview_state_on_page_load(payload.event()) {
replay_desktop_webview_state(&window);
}
})
.on_download(|_webview, event| should_allow_desktop_webview_download(&event))
.build()?;
register_desktop_window_close_events(&window, tray_registered);
register_desktop_lifecycle_events(&window);
let _ = emit_desktop_lifecycle_event(&window, "active", true, "created");
let _ = register_desktop_network_events(&window);
let _ = register_desktop_navigation_events(&window);
register_desktop_file_drop_events(&window);
register_desktop_deep_link_events(app)?;
register_desktop_deep_link_schemes(app);
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
crate::host_bridge::host_bridge_request
])
.run(tauri::generate_context!())
.expect("failed to run Genarrative desktop shell");
}
+2 -82
View File
@@ -1,87 +1,7 @@
mod app;
mod host_bridge;
mod shell;
use host_bridge::{host_bridge_request, DesktopShareState, HostBridgeReplayState};
use shell::deep_link::{register_desktop_deep_link_events, register_desktop_deep_link_schemes};
use shell::menu::register_desktop_app_menu;
use shell::tray::{
register_desktop_tray, register_desktop_window_close_events,
resolve_desktop_single_instance_action, show_main_window, DesktopSingleInstanceAction,
};
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_navigation_events,
register_desktop_network_events, replay_desktop_webview_state,
should_allow_desktop_webview_download, should_allow_desktop_webview_navigation,
should_replay_desktop_webview_state_on_page_load,
};
use shell::window_state::desktop_window_state_plugin;
use tauri::webview::NewWindowResponse;
fn main() {
tauri::Builder::default()
.manage(DesktopShareState::default())
.manage(HostBridgeReplayState::default())
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
match resolve_desktop_single_instance_action() {
DesktopSingleInstanceAction::ShowMainWindow => {
let _ = show_main_window(app);
}
}
}))
.plugin(desktop_window_state_plugin())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
register_desktop_app_menu(app)?;
let tray_registered = match register_desktop_tray(app) {
Ok(()) => true,
Err(error) => {
eprintln!("desktop tray registration failed: {error}");
false
}
};
let window_config = app.config().app.windows.get(0).cloned();
if let Some(config) = window_config {
let config = desktop_window_config_with_runtime_platform(config);
let app_handle = app.handle().clone();
let new_window_app_handle = app.handle().clone();
let window = tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?
.on_navigation(move |url| {
if should_allow_desktop_webview_navigation(url) {
true
} else {
open_desktop_external_navigation(&app_handle, url);
false
}
})
.on_new_window(move |url, _features| {
open_desktop_external_navigation(&new_window_app_handle, &url);
NewWindowResponse::Deny
})
.on_page_load(|window, payload| {
if should_replay_desktop_webview_state_on_page_load(payload.event()) {
replay_desktop_webview_state(&window);
}
})
.on_download(|_webview, event| should_allow_desktop_webview_download(&event))
.build()?;
register_desktop_window_close_events(&window, tray_registered);
register_desktop_lifecycle_events(&window);
let _ = emit_desktop_lifecycle_event(&window, "active", true, "created");
let _ = register_desktop_network_events(&window);
let _ = register_desktop_navigation_events(&window);
register_desktop_file_drop_events(&window);
register_desktop_deep_link_events(app)?;
register_desktop_deep_link_schemes(app);
}
Ok(())
})
.invoke_handler(tauri::generate_handler![host_bridge_request])
.run(tauri::generate_context!())
.expect("failed to run Genarrative desktop shell");
app::run();
}
@@ -2514,7 +2514,7 @@
## 2026-06-18 三端宿主桥接层文件结构对齐
- 背景:微信小程序壳、Expo 移动壳和 Tauri 桌面壳都在承接宿主能力;如果微信页面继续散落 `index.shared.js`,桌面端继续把桥接分发堆在 `main.rs`,后续新增登录、支付、文件、通知或 sandbox 转发能力时会很难跨端对照 owner。
- 决策:三端桥接层按职责对齐,但保留各宿主真实边界。微信小程序页面路由不改,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面目录只保留生命周期和装配,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Expo 移动壳拆成 `apps/mobile-shell/src/host-bridge/protocol.ts`、`capabilities.ts`、`dispatch.ts`、`files.ts`、`share.ts` 和 facade `bridge.ts`,分别负责 envelope / request 校验 / ok-failure 响应 / replay 基础类型、能力清单与 iOS 差异、method 分发、文件能力、分享能力和 WebView message 入口 / request id replay 编排;根 `App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,`apps/mobile-shell/src/shell/*.ts(x)` 负责 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳拆成 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs`、`capabilities.rs`、`dispatch.rs`、`files.rs`、`share.rs` 和 command facade `mod.rs`,分别负责 envelope / method 白名单 / request 校验 / replay 状态、能力清单、method 分发、文件能力、分享能力和 `host_bridge_request` command / replay 编排;`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`main.rs` 只做 builder、plugin、窗口和状态装配。`scripts/check-native-shells.mjs` 锁定三端桥接层目录清单,并拒绝移动根入口直接引用 HostBridge。
- 决策:三端桥接层按职责对齐,但保留各宿主真实边界。微信小程序页面路由不改,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面目录只保留生命周期和装配,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Expo 移动壳拆成 `apps/mobile-shell/src/host-bridge/protocol.ts`、`capabilities.ts`、`dispatch.ts`、`files.ts`、`share.ts` 和 facade `bridge.ts`,分别负责 envelope / request 校验 / ok-failure 响应 / replay 基础类型、能力清单与 iOS 差异、method 分发、文件能力、分享能力和 WebView message 入口 / request id replay 编排;根 `App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,`apps/mobile-shell/src/shell/*.ts(x)` 负责 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy;Tauri 桌面壳拆成 `apps/desktop-shell/src-tauri/src/app.rs`、`host_bridge/protocol.rs`、`capabilities.rs`、`dispatch.rs`、`files.rs`、`share.rs` 和 command facade `mod.rs`,分别负责 Tauri builder / plugin / window 装配、envelope / method 白名单 / request 校验 / replay 状态、能力清单、method 分发、文件能力、分享能力和 `host_bridge_request` command / replay 编排;`apps/desktop-shell/src-tauri/src/shell/*.rs` 承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`main.rs` 只做薄入口并调用 `app::run()`。`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/webView.test.js miniprogram/host-bridge/payment.test.js miniprogram/host-bridge/shareGrid.test.js miniprogram/host-bridge/subscribeMessage.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`。
@@ -64,7 +64,7 @@ src/
已落地:`packages/shared/src/contracts/hostBridge.ts` 保存消息 envelope、method、payload 和错误码,H5、Expo 壳与 Tauri 壳共享同一份协议类型。
三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂统一放在 `miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面入口只做 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/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 门面,`main.rs` 只保留 Tauri builder / plugin / window 装配。
三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂统一放在 `miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面入口只做 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/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 门面,`apps/desktop-shell/src-tauri/src/app.rs` 承接 Tauri builder / plugin / window 装配,`main.rs` 只保留薄入口并调用 `app::run()`。
## HostBridge 消息协议
@@ -467,7 +467,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`、`files.ts`、`share.ts` 和 facade `bridge.ts`,与桌面端 `host_bridge/protocol.rs`、`files.rs`、`share.rs`、`mod.rs` 对齐;移动壳根 `App.tsx` 也保持薄入口,只装配 `src/shell/ShellApp.tsx`,WebView 容器、深链、网络、生命周期和安全策略全部留在 `src/shell/`;桌面壳 Rust 源码拆成 `apps/desktop-shell/src-tauri/src/host_bridge/*.rs` 与 `apps/desktop-shell/src-tauri/src/shell/*.rs`,其中 `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` 只声明两个模块并装配 Tauri builder / plugin / window。根级 `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`、`files.ts`、`share.ts` 和 facade `bridge.ts`,与桌面端 `host_bridge/protocol.rs`、`files.rs`、`share.rs`、`mod.rs` 对齐;移动壳根 `App.tsx` 也保持薄入口,只装配 `src/shell/ShellApp.tsx`,WebView 容器、深链、网络、生命周期和安全策略全部留在 `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。
@@ -37,7 +37,7 @@ AI H5 sandbox
-> parent HostBridge adapter
```
桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`;`miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`main.rs` 只保留 Tauri builder / plugin / window 装配。`npm run check:native-shells` 会检查这些目录清单。
桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`;`miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` 分别承接文件和分享能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘和 WebView 门面,`apps/desktop-shell/src-tauri/src/app.rs` 承接 Tauri builder / plugin / window 装配,`main.rs` 只保留薄入口并调用 `app::run()`。`npm run check:native-shells` 会检查这些目录清单。
## 首批能力
+25 -1
View File
@@ -836,10 +836,34 @@ function assertHostBridgeLayerLayout() {
.sort();
assertSameList(
desktopEntrypointRustFiles,
['main.rs'],
['app.rs', 'main.rs'],
'desktop shell entrypoint Rust files',
);
const desktopMainSource = fs.readFileSync(
'apps/desktop-shell/src-tauri/src/main.rs',
'utf8',
);
const desktopAppSource = fs.readFileSync(
'apps/desktop-shell/src-tauri/src/app.rs',
'utf8',
);
if (!desktopMainSource.includes('mod app;') || !desktopMainSource.includes('app::run();')) {
throw new Error('desktop shell main.rs must stay a thin app entrypoint');
}
if (
desktopMainSource.includes('tauri::Builder::default()') ||
desktopMainSource.includes('tauri::generate_handler!')
) {
throw new Error('desktop shell main.rs must not own Tauri app setup');
}
if (
!desktopAppSource.includes('tauri::Builder::default()') ||
!desktopAppSource.includes('crate::host_bridge::host_bridge_request')
) {
throw new Error('desktop shell app.rs must own Tauri app setup');
}
const desktopHostBridgeRustFiles = fs
.readdirSync('apps/desktop-shell/src-tauri/src/host_bridge', { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.rs'))