接入桌面壳单实例唤醒

Tauri 桌面壳启用 single-instance 插件

重复启动只唤醒并聚焦已有主窗口

禁止把第二实例参数透传给 H5

补充单实例检查测试和架构文档
This commit is contained in:
2026-06-18 08:07:50 +08:00
parent 88e363586e
commit 94046153c6
6 changed files with 61 additions and 0 deletions

View File

@@ -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');

View File

@@ -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"

View File

@@ -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"

View File

@@ -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!(

View File

@@ -46,6 +46,7 @@
- 2026-06-18 剪贴板读取能力:新增 `clipboard.readText` HostBridge capabilityH5 只能读取纯文本结果,契约限制返回文本最多 100000 字符Expo 壳通过 `expo-clipboard` 读取系统剪贴板文本Tauri 壳通过 Rust 侧 `tauri-plugin-clipboard-manager` 读取文本且不开放插件 JS guest API。该能力不读取图片、HTML、文件列表或剪贴板监听事件宿主未声明或读取失败时由 H5 视作失败并保留原流程。
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capabilityH5 统一通过 `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固定玩法在各宿主中读取同一作品数据和运行态 snapshotAI sandbox 无法直接调用 HostBridgeTauri release 不允许任意远端页面调用桌面命令。
- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md``docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`

View File

@@ -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宿主能力扩展
- 移动端接入系统分享、推送、原生登录和渠道支付。