From 94046153c6f32c347f8ba1b0737c3460ec970638 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 08:07:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E5=8D=95=E5=AE=9E=E4=BE=8B=E5=94=A4=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tauri 桌面壳启用 single-instance 插件 重复启动只唤醒并聚焦已有主窗口 禁止把第二实例参数透传给 H5 补充单实例检查测试和架构文档 --- apps/desktop-shell/scripts/check-config.mjs | 17 +++++++++++++ apps/desktop-shell/src-tauri/Cargo.lock | 16 +++++++++++++ apps/desktop-shell/src-tauri/Cargo.toml | 1 + apps/desktop-shell/src-tauri/src/main.rs | 24 +++++++++++++++++++ .../shared-memory/decision-log.md | 1 + ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 ++ 6 files changed, 61 insertions(+) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index d3b1c297..45ec4e14 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -134,6 +134,8 @@ const requiredPermissions = [ ]; const requiredBuildCommands = ['host_bridge_request']; const requiredMainSnippets = [ + 'tauri_plugin_single_instance::init', + 'resolve_desktop_single_instance_action', 'tauri_plugin_clipboard_manager::init()', 'TrayIconBuilder::with_id', 'register_desktop_tray(app)', @@ -216,6 +218,21 @@ if (!cargoManifest.includes('features = ["tray-icon"]')) { throw new Error('desktop shell must enable the Tauri tray-icon feature'); } +if (!cargoManifest.includes('tauri-plugin-single-instance = "2.4.2"')) { + throw new Error('desktop shell must depend on tauri-plugin-single-instance'); +} + +if ( + main.indexOf('tauri_plugin_single_instance::init') > + main.indexOf('tauri_plugin_clipboard_manager::init()') +) { + throw new Error('desktop shell must register the single-instance plugin first'); +} + +if (main.includes('single-instance",') || main.includes('"single-instance"')) { + throw new Error('desktop shell must not emit secondary-instance argv to H5'); +} + const icon = fs.readFileSync(iconPath); if (icon.length < 10000) { throw new Error('desktop shell icon must use a real brand asset'); diff --git a/apps/desktop-shell/src-tauri/Cargo.lock b/apps/desktop-shell/src-tauri/Cargo.lock index 83d90b98..1c855fea 100644 --- a/apps/desktop-shell/src-tauri/Cargo.lock +++ b/apps/desktop-shell/src-tauri/Cargo.lock @@ -1286,6 +1286,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-notification", "tauri-plugin-opener", + "tauri-plugin-single-instance", ] [[package]] @@ -3799,6 +3800,21 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-runtime" version = "2.11.2" diff --git a/apps/desktop-shell/src-tauri/Cargo.toml b/apps/desktop-shell/src-tauri/Cargo.toml index d2342808..ea4781c3 100644 --- a/apps/desktop-shell/src-tauri/Cargo.toml +++ b/apps/desktop-shell/src-tauri/Cargo.toml @@ -16,3 +16,4 @@ tauri-plugin-clipboard-manager = "2.3.2" tauri-plugin-dialog = "2.7.1" tauri-plugin-notification = "2.3.3" tauri-plugin-opener = "2.5.4" +tauri-plugin-single-instance = "2.4.2" diff --git a/apps/desktop-shell/src-tauri/src/main.rs b/apps/desktop-shell/src-tauri/src/main.rs index da3179ad..d0ecc43e 100644 --- a/apps/desktop-shell/src-tauri/src/main.rs +++ b/apps/desktop-shell/src-tauri/src/main.rs @@ -904,6 +904,11 @@ enum DesktopWindowCloseAction { CloseWindow, } +#[derive(Debug, PartialEq, Eq)] +enum DesktopSingleInstanceAction { + ShowMainWindow, +} + fn resolve_desktop_tray_menu_action(menu_id: &str) -> DesktopTrayAction { match menu_id { TRAY_MENU_SHOW => DesktopTrayAction::ShowMainWindow, @@ -932,6 +937,10 @@ fn resolve_desktop_window_close_action(tray_registered: bool) -> DesktopWindowCl } } +fn resolve_desktop_single_instance_action() -> DesktopSingleInstanceAction { + DesktopSingleInstanceAction::ShowMainWindow +} + fn show_main_window(app: &tauri::AppHandle) -> tauri::Result<()> { if let Some(window) = app.get_webview_window("main") { window.show()?; @@ -1470,6 +1479,13 @@ async fn host_bridge_request( fn main() { tauri::Builder::default() .manage(DesktopShareState::default()) + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + match resolve_desktop_single_instance_action() { + DesktopSingleInstanceAction::ShowMainWindow => { + let _ = show_main_window(app); + } + } + })) .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_notification::init()) @@ -1773,6 +1789,14 @@ mod tests { ); } + #[test] + fn desktop_single_instance_only_restores_existing_window() { + assert_eq!( + resolve_desktop_single_instance_action(), + DesktopSingleInstanceAction::ShowMainWindow + ); + } + #[test] fn external_url_normalization_allows_only_safe_protocols() { assert_eq!( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index eb36bf1f..b7b86585 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -46,6 +46,7 @@ - 2026-06-18 剪贴板读取能力:新增 `clipboard.readText` HostBridge capability,H5 只能读取纯文本结果,契约限制返回文本最多 100000 字符;Expo 壳通过 `expo-clipboard` 读取系统剪贴板文本,Tauri 壳通过 Rust 侧 `tauri-plugin-clipboard-manager` 读取文本且不开放插件 JS guest API。该能力不读取图片、HTML、文件列表或剪贴板监听事件,宿主未声明或读取失败时由 H5 视作失败并保留原流程。 - 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。 - 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability,不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘,必须通过托盘“退出”结束应用;托盘注册失败不得阻断主窗口启动,也不得拦截关闭,避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。 +- 2026-06-18 Tauri 单实例:桌面壳启用 `tauri-plugin-single-instance` 并要求该插件最先注册;重复启动 App 时第二实例退出,只唤醒、取消最小化并聚焦已有主窗口,不把第二实例 argv / cwd / 深链内容作为事件透传给 H5。桌面深链后续如需接入,必须先定义受控 URL 归一和宿主边界,不能借单实例回调直接开放任意启动参数。 - 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。 - 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。 - 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 37419a4d..1cd9ae2b 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -295,6 +295,8 @@ GameBridge 禁止: 2026-06-18 追加:桌面壳启用 Tauri 真实系统托盘,并复用品牌图标。托盘菜单只提供宿主壳级动作:显示主窗口、刷新主窗口和退出应用;左键点击托盘图标恢复并聚焦主窗口。该能力不进入 HostBridge capability 清单,不向 H5 暴露托盘 API、菜单 API、shell API 或任意窗口控制;如果当前桌面环境无法注册托盘,壳会继续启动主窗口。托盘注册成功时,用户点击主窗口关闭按钮会隐藏到托盘,必须通过托盘“退出”动作结束应用;托盘注册失败时不拦截关闭,避免窗口消失后没有恢复入口。 +2026-06-18 追加:桌面壳启用 Tauri 单实例。用户重复启动桌面 App 时,新实例会退出并唤醒已有主窗口;该回调只执行显示、取消最小化和聚焦主窗口,不把第二实例的命令行参数、工作目录或深链内容作为事件透传给 H5。桌面深链若后续需要接入,必须单独定义受控来源、路径归一和 HostBridge / GameBridge 边界。 + ### Phase 4:宿主能力扩展 - 移动端接入系统分享、推送、原生登录和渠道支付。