拆薄桌面壳入口
新增桌面壳 app.rs 承接 Tauri 应用装配 收敛 main.rs 为薄入口并调用 app::run 扩展原生壳门禁锁定桌面入口职责边界 更新宿主壳方案和共享决策记录
This commit is contained in:
@@ -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}`);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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` 会检查这些目录清单。
|
||||
|
||||
## 首批能力
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user