Files
Genarrative/apps/desktop-shell/src-tauri/src/shell/url.rs
T
kdletters 8bd8363ffa 修正原生壳公开主站配置
将原生壳公开主站统一为 www.genarrative.world

调整 Tauri release 直接加载线上主站并移除 frontendDist 打包

补齐创作入口配置开发代理与回归测试

同步原生壳方案文档和共享记忆
2026-06-22 15:07:02 +08:00

267 lines
9.3 KiB
Rust

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://www.genarrative.world";
const HOST_CONTEXT_QUERY_KEYS: [&str; 7] = [
"clientRuntime",
"clientType",
"hostShell",
"hostPlatform",
"hostVersion",
"bridgeVersion",
"hostCapabilities",
];
fn append_desktop_host_context(url: &mut Url) {
url.query_pairs_mut()
.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(","));
}
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_desktop_host_context(&mut target_url);
if target_url.origin() != base_url.origin() {
return None;
}
Some(target_url)
}
fn append_desktop_host_context_to_url(mut url: Url) -> String {
let retained_query_pairs = url
.query_pairs()
.filter(|(key, _)| !HOST_CONTEXT_QUERY_KEYS.contains(&key.as_ref()))
.map(|(key, value)| (key.into_owned(), value.into_owned()))
.collect::<Vec<_>>();
url.query_pairs_mut()
.clear()
.extend_pairs(
retained_query_pairs
.iter()
.map(|(key, value)| (key.as_str(), value.as_str())),
);
append_desktop_host_context(&mut url);
url.to_string()
}
pub(crate) fn desktop_entry_url_with_host_context(raw_url: &str) -> String {
if let Ok(url) = Url::parse(raw_url) {
return append_desktop_host_context_to_url(url);
}
let (without_hash, hash) = raw_url
.split_once('#')
.map(|(path, hash)| (path, Some(hash)))
.unwrap_or((raw_url, None));
let mut parts = without_hash.splitn(2, '?');
let path = parts.next().unwrap_or_default();
let query = parts.next();
let mut pairs = query
.map(|query| {
query
.split('&')
.filter(|pair| {
if pair.is_empty() {
return false;
}
let key = pair.split_once('=').map(|(key, _)| key).unwrap_or(pair);
!HOST_CONTEXT_QUERY_KEYS.contains(&key)
})
.map(str::to_owned)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let mut context_url = Url::parse(WEB_APP_ORIGIN).expect("desktop web origin");
append_desktop_host_context(&mut context_url);
pairs.extend(
context_url
.query_pairs()
.map(|(key, value)| format!("{key}={value}")),
);
let normalized_url = format!("{path}?{}", pairs.join("&"));
if let Some(hash) = hash {
format!("{normalized_url}#{hash}")
} else {
normalized_url
}
}
pub(crate) fn desktop_window_config_with_runtime_platform(
mut config: tauri::utils::config::WindowConfig,
) -> tauri::utils::config::WindowConfig {
config.url = match config.url {
WebviewUrl::External(url) => WebviewUrl::External(
Url::parse(&desktop_entry_url_with_host_context(url.as_str())).unwrap_or(url),
),
WebviewUrl::CustomProtocol(url) => WebviewUrl::CustomProtocol(
Url::parse(&desktop_entry_url_with_host_context(url.as_str())).unwrap_or(url),
),
WebviewUrl::App(path) => WebviewUrl::App(PathBuf::from(desktop_entry_url_with_host_context(
path.to_string_lossy().as_ref(),
))),
other => other,
};
config
}
#[cfg(test)]
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://www.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_adds_host_context_from_plain_entries() {
let platform = desktop_platform();
let dev_url = desktop_entry_url_with_host_context("http://127.0.0.1:3000/");
let dev_url = Url::parse(&dev_url).expect("dev url");
assert_eq!(
dev_url
.query_pairs()
.find(|(key, _)| key == "clientRuntime"),
Some(("clientRuntime".into(), "native_app".into()))
);
assert_eq!(
dev_url.query_pairs().find(|(key, _)| key == "hostPlatform"),
Some(("hostPlatform".into(), platform.into()))
);
assert_eq!(
dev_url
.query_pairs()
.filter(|(key, _)| key == "hostPlatform")
.count(),
1
);
assert_eq!(
dev_url.query_pairs().find(|(key, _)| key == "hostVersion"),
Some(("hostVersion".into(), env!("CARGO_PKG_VERSION").into()))
);
assert_eq!(
dev_url.query_pairs().find(|(key, _)| key == "bridgeVersion"),
Some(("bridgeVersion".into(), HOST_BRIDGE_VERSION.to_string().into()))
);
assert_eq!(
dev_url.query_pairs().find(|(key, _)| key == "hostCapabilities"),
Some(("hostCapabilities".into(), capabilities().join(",").into()))
);
let packaged_url = desktop_entry_url_with_host_context("index.html#works");
assert!(packaged_url.contains("clientRuntime=native_app"));
assert!(packaged_url.contains("clientType=native_app"));
assert!(packaged_url.contains("hostShell=tauri_desktop"));
assert!(packaged_url.contains(&format!("hostPlatform={platform}")));
assert!(packaged_url.contains(&format!("hostVersion={}", env!("CARGO_PKG_VERSION"))));
assert!(packaged_url.contains(&format!("bridgeVersion={HOST_BRIDGE_VERSION}")));
assert!(packaged_url.contains(&format!("hostCapabilities={}", capabilities().join(","))));
assert!(packaged_url.ends_with("#works"));
}
#[test]
fn desktop_entry_url_removes_stale_host_context_before_appending_current_context() {
let url = desktop_entry_url_with_host_context(
"index.html?clientRuntime=browser&hostShell=old_shell&hostCapabilities=old&work=PZ-1#works",
);
let query = url.split_once('?').expect("context query").1;
assert!(url.contains("work=PZ-1"));
assert!(url.contains("clientRuntime=native_app"));
assert!(url.contains("hostShell=tauri_desktop"));
assert!(url.contains(&format!("hostCapabilities={}", capabilities().join(","))));
assert_eq!(query.matches("clientRuntime=").count(), 1);
assert_eq!(query.matches("hostShell=").count(), 1);
assert_eq!(query.matches("hostCapabilities=").count(), 1);
assert!(!url.contains("clientRuntime=browser"));
assert!(!url.contains("hostShell=old_shell"));
assert!(!url.contains("hostCapabilities=old"));
assert!(url.ends_with("#works"));
}
}