桌面壳主动导航保留宿主上下文
抽出 Tauri H5 宿主上下文补写方法 让桌面深链和主动同源跳转共用宿主上下文 补充桌面壳检查脚本和文档决策
This commit is contained in:
@@ -1221,6 +1221,9 @@ const requiredRustHostSnippets = [
|
||||
'file.imageDropped',
|
||||
'app.notification().builder()',
|
||||
'desktop_entry_url_with_platform',
|
||||
'desktop_h5_url_with_host_context',
|
||||
'desktop_h5_url_with_host_context(target_url)',
|
||||
'desktop_h5_url_with_host_context(normalized_url)',
|
||||
'desktop_window_config_with_runtime_platform',
|
||||
'desktop_window_config_with_runtime_platform(config)',
|
||||
'should_allow_desktop_webview_navigation',
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
use crate::host_bridge::capabilities::capabilities;
|
||||
use crate::host_bridge::protocol::HOST_BRIDGE_VERSION;
|
||||
use crate::shell::tray::show_main_window;
|
||||
use crate::shell::webview::{desktop_platform, WEB_APP_ORIGIN};
|
||||
use crate::shell::webview::{desktop_h5_url_with_host_context, WEB_APP_ORIGIN};
|
||||
use tauri::{Manager, Url, WebviewWindow};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
const DESKTOP_DEEP_LINK_HOSTS: [&str; 2] = ["open", "app"];
|
||||
const HOST_CONTEXT_QUERY_KEYS: [&str; 7] = [
|
||||
"clientRuntime",
|
||||
"clientType",
|
||||
"hostShell",
|
||||
"hostPlatform",
|
||||
"hostVersion",
|
||||
"bridgeVersion",
|
||||
"hostCapabilities",
|
||||
];
|
||||
|
||||
fn extract_path_from_custom_scheme(url: &Url) -> String {
|
||||
let host = url.host_str().unwrap_or_default();
|
||||
@@ -65,36 +54,12 @@ pub(crate) fn normalize_desktop_deep_link_url(raw_url: &Url) -> Option<Url> {
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let mut target_url = base_url.join(&target_path).ok()?;
|
||||
let target_url = base_url.join(&target_path).ok()?;
|
||||
if target_url.origin() != base_url.origin() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let retained_query_pairs = target_url
|
||||
.query_pairs()
|
||||
.filter(|(key, _)| !HOST_CONTEXT_QUERY_KEYS.contains(&key.as_ref()))
|
||||
.map(|(key, value)| (key.into_owned(), value.into_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
target_url
|
||||
.query_pairs_mut()
|
||||
.clear()
|
||||
.extend_pairs(
|
||||
retained_query_pairs
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str())),
|
||||
)
|
||||
.append_pair("clientRuntime", "native_app")
|
||||
.append_pair("clientType", "native_app")
|
||||
.append_pair("hostShell", "tauri_desktop")
|
||||
.append_pair("hostPlatform", desktop_platform())
|
||||
.append_pair("hostVersion", env!("CARGO_PKG_VERSION"))
|
||||
.append_pair("bridgeVersion", &HOST_BRIDGE_VERSION.to_string())
|
||||
.append_pair("hostCapabilities", &capabilities().join(","));
|
||||
if target_url.origin() != base_url.origin() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(target_url)
|
||||
desktop_h5_url_with_host_context(target_url)
|
||||
}
|
||||
|
||||
fn open_desktop_deep_link_url(window: &WebviewWindow, url: &Url) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::shell::events::host_bridge_event_script;
|
||||
use crate::shell::url::WEB_APP_ORIGIN;
|
||||
use crate::shell::url::{desktop_h5_url_with_host_context, WEB_APP_ORIGIN};
|
||||
use serde_json::{json, Value};
|
||||
use tauri::webview::DownloadEvent;
|
||||
use tauri::{Url, WebviewWindow};
|
||||
@@ -176,7 +176,7 @@ pub(crate) fn normalize_native_page_url(raw_url: &str) -> Option<Url> {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(normalized_url)
|
||||
desktop_h5_url_with_host_context(normalized_url)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -270,23 +270,46 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn native_page_url_normalization_allows_same_origin_routes() {
|
||||
let route =
|
||||
normalize_native_page_url("/works/detail?work=PZ-1").expect("same-origin route");
|
||||
assert_eq!(route.origin().ascii_serialization(), WEB_APP_ORIGIN);
|
||||
assert_eq!(route.path(), "/works/detail");
|
||||
assert_eq!(
|
||||
normalize_native_page_url("/works/detail?work=PZ-1")
|
||||
.expect("same-origin route")
|
||||
.as_str(),
|
||||
"https://app.genarrative.world/works/detail?work=PZ-1"
|
||||
route
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "work")
|
||||
.unwrap()
|
||||
.1,
|
||||
"PZ-1"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_native_page_url("works/detail?work=PZ-1")
|
||||
.expect("relative route")
|
||||
.as_str(),
|
||||
"https://app.genarrative.world/works/detail?work=PZ-1"
|
||||
route
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "clientRuntime")
|
||||
.unwrap()
|
||||
.1,
|
||||
"native_app"
|
||||
);
|
||||
assert_eq!(
|
||||
route
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "hostShell")
|
||||
.unwrap()
|
||||
.1,
|
||||
"tauri_desktop"
|
||||
);
|
||||
assert!(route
|
||||
.query_pairs()
|
||||
.any(|(key, value)| key == "hostCapabilities" && value.contains("host.getRuntime")));
|
||||
assert!(normalize_native_page_url("works/detail?work=PZ-1")
|
||||
.expect("relative route")
|
||||
.query_pairs()
|
||||
.any(|(key, value)| key == "clientType" && value == "native_app"));
|
||||
assert!(
|
||||
normalize_native_page_url("https://app.genarrative.world/works/detail?work=PZ-1")
|
||||
.expect("absolute same-origin route")
|
||||
.as_str(),
|
||||
"https://app.genarrative.world/works/detail?work=PZ-1"
|
||||
.query_pairs()
|
||||
.any(|(key, value)| key == "hostShell" && value == "tauri_desktop")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,53 @@
|
||||
use crate::host_bridge::capabilities::capabilities;
|
||||
use crate::host_bridge::protocol::HOST_BRIDGE_VERSION;
|
||||
use crate::shell::runtime::desktop_platform;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{Url, WebviewUrl};
|
||||
|
||||
pub(crate) const WEB_APP_ORIGIN: &str = "https://app.genarrative.world";
|
||||
const HOST_CONTEXT_QUERY_KEYS: [&str; 7] = [
|
||||
"clientRuntime",
|
||||
"clientType",
|
||||
"hostShell",
|
||||
"hostPlatform",
|
||||
"hostVersion",
|
||||
"bridgeVersion",
|
||||
"hostCapabilities",
|
||||
];
|
||||
|
||||
pub(crate) fn desktop_h5_url_with_host_context(mut target_url: Url) -> Option<Url> {
|
||||
let base_url = Url::parse(WEB_APP_ORIGIN).ok()?;
|
||||
if target_url.scheme() != "https" || target_url.origin() != base_url.origin() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let retained_query_pairs = target_url
|
||||
.query_pairs()
|
||||
.filter(|(key, _)| !HOST_CONTEXT_QUERY_KEYS.contains(&key.as_ref()))
|
||||
.map(|(key, value)| (key.into_owned(), value.into_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
target_url
|
||||
.query_pairs_mut()
|
||||
.clear()
|
||||
.extend_pairs(
|
||||
retained_query_pairs
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str())),
|
||||
)
|
||||
.append_pair("clientRuntime", "native_app")
|
||||
.append_pair("clientType", "native_app")
|
||||
.append_pair("hostShell", "tauri_desktop")
|
||||
.append_pair("hostPlatform", desktop_platform())
|
||||
.append_pair("hostVersion", env!("CARGO_PKG_VERSION"))
|
||||
.append_pair("bridgeVersion", &HOST_BRIDGE_VERSION.to_string())
|
||||
.append_pair("hostCapabilities", &capabilities().join(","));
|
||||
|
||||
if target_url.origin() != base_url.origin() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(target_url)
|
||||
}
|
||||
|
||||
pub(crate) fn desktop_entry_url_with_platform(raw_url: &str) -> String {
|
||||
let platform = desktop_platform();
|
||||
@@ -76,6 +121,65 @@ pub(crate) fn desktop_window_config_with_runtime_platform(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn desktop_h5_url_with_host_context_rewrites_runtime_query_once() {
|
||||
let platform = desktop_platform();
|
||||
let url = Url::parse(
|
||||
"https://app.genarrative.world/creation/puzzle?work=PZ-1&clientRuntime=browser&hostCapabilities=old#draft",
|
||||
)
|
||||
.expect("desktop H5 url");
|
||||
let url = desktop_h5_url_with_host_context(url).expect("context url");
|
||||
|
||||
assert_eq!(url.origin().ascii_serialization(), WEB_APP_ORIGIN);
|
||||
assert_eq!(url.path(), "/creation/puzzle");
|
||||
assert_eq!(url.fragment(), Some("draft"));
|
||||
assert_eq!(
|
||||
url.query_pairs()
|
||||
.filter(|(key, _)| key == "clientRuntime")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
url.query_pairs()
|
||||
.find(|(key, _)| key == "clientRuntime")
|
||||
.unwrap()
|
||||
.1,
|
||||
"native_app"
|
||||
);
|
||||
assert_eq!(
|
||||
url.query_pairs()
|
||||
.find(|(key, _)| key == "hostPlatform")
|
||||
.unwrap()
|
||||
.1,
|
||||
platform
|
||||
);
|
||||
assert_eq!(
|
||||
url.query_pairs()
|
||||
.find(|(key, _)| key == "hostVersion")
|
||||
.unwrap()
|
||||
.1,
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
assert_eq!(
|
||||
url.query_pairs()
|
||||
.find(|(key, _)| key == "hostCapabilities")
|
||||
.unwrap()
|
||||
.1,
|
||||
capabilities().join(",")
|
||||
);
|
||||
assert_eq!(
|
||||
url.query_pairs().find(|(key, _)| key == "work").unwrap().1,
|
||||
"PZ-1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_h5_url_with_host_context_rejects_non_h5_origin() {
|
||||
let url = Url::parse("https://example.com/works/detail?work=PZ-1").expect("external url");
|
||||
|
||||
assert_eq!(desktop_h5_url_with_host_context(url), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_entry_url_replaces_static_platform_marker() {
|
||||
let platform = desktop_platform();
|
||||
|
||||
@@ -12,4 +12,6 @@ pub(crate) use crate::shell::network::{
|
||||
register_desktop_network_events, resolve_desktop_network_status,
|
||||
};
|
||||
pub(crate) use crate::shell::runtime::{color_scheme_from_theme, desktop_platform};
|
||||
pub(crate) use crate::shell::url::{desktop_window_config_with_runtime_platform, WEB_APP_ORIGIN};
|
||||
pub(crate) use crate::shell::url::{
|
||||
desktop_h5_url_with_host_context, desktop_window_config_with_runtime_platform, WEB_APP_ORIGIN,
|
||||
};
|
||||
|
||||
@@ -2508,6 +2508,13 @@
|
||||
- 影响范围:`apps/mobile-shell/src/host-bridge/`、`apps/mobile-shell/src/shell/ShellApp.tsx`、Expo / Tauri HostBridge 方案文档。
|
||||
- 验证方式:`npm run mobile-shell:test`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run typecheck -- --pretty false`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-06-18 桌面壳主动导航保留宿主上下文
|
||||
|
||||
- 背景:Tauri 桌面壳 release / dev 入口和 deep link 都会给 H5 追加 `native_app`、`tauri_desktop`、当前平台、版本和 capability query;但 H5 通过 HostBridge 调用 `navigation.openNativePage` 主动跳转同源 route 时,如果只把裸同源 URL 交给主窗口,目标页可能按普通浏览器运行态启动。
|
||||
- 决策:`navigation.openNativePage` 仍只接受 `https://app.genarrative.world` 同源 H5 route,不新增真实原生页面、不放宽外域导航;通过校验后的目标 URL 必须复用 `desktop_h5_url_with_host_context(...)`,与桌面 deep link 一样重写宿主上下文 query,确保主动导航后的页面继续带 `clientRuntime=native_app`、`hostShell=tauri_desktop`、当前平台、宿主版本和真实 capability 清单。
|
||||
- 影响范围:`apps/desktop-shell/src-tauri/src/shell/url.rs`、`apps/desktop-shell/src-tauri/src/shell/navigation.rs`、`apps/desktop-shell/src-tauri/src/shell/deep_link.rs`、`apps/desktop-shell/scripts/check-config.mjs`、Expo / Tauri HostBridge 方案文档。
|
||||
- 验证方式:`npm run desktop-shell:typecheck`、`npm run desktop-shell:test`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-06-18 桌面壳窗口状态持久化
|
||||
|
||||
- 背景:Tauri 桌面壳已经具备系统托盘、单实例、深链和受控 HostBridge 能力,但用户调整主窗口尺寸、位置或最大化状态后,重启桌面 App 仍回到固定初始窗口配置;如果直接保存完整窗口状态,又可能把托盘隐藏后的可见性状态带到下次启动。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user