固定桌面壳运行平台标记
桌面壳创建主窗口前将 hostPlatform 归一为真实系统平台 桌面壳配置检查新增运行平台归一守卫 同步宿主壳方案与共享决策记录
This commit is contained in:
@@ -521,6 +521,9 @@ const requiredMainSnippets = [
|
||||
'network.statusChanged',
|
||||
'file.imageDropped',
|
||||
'app.notification().builder()',
|
||||
'desktop_entry_url_with_platform',
|
||||
'desktop_window_config_with_runtime_platform',
|
||||
'desktop_window_config_with_runtime_platform(config)',
|
||||
];
|
||||
|
||||
assertSameList(
|
||||
|
||||
@@ -7,11 +7,12 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tauri::menu::{Menu, MenuItem};
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::DragDropEvent;
|
||||
use tauri::Manager;
|
||||
use tauri::Theme;
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::Url;
|
||||
use tauri::WebviewUrl;
|
||||
use tauri::WebviewWindow;
|
||||
use tauri::WindowEvent;
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
@@ -97,6 +98,74 @@ fn desktop_platform() -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn desktop_entry_url_with_platform(raw_url: &str) -> String {
|
||||
let platform = desktop_platform();
|
||||
if let Ok(mut url) = Url::parse(raw_url) {
|
||||
let query_pairs = url
|
||||
.query_pairs()
|
||||
.filter(|(key, _)| key != "hostPlatform")
|
||||
.map(|(key, value)| (key.into_owned(), value.into_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
url.query_pairs_mut()
|
||||
.clear()
|
||||
.extend_pairs(
|
||||
query_pairs
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str())),
|
||||
)
|
||||
.append_pair("hostPlatform", platform);
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
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| {
|
||||
!pair.is_empty()
|
||||
&& !pair
|
||||
.split_once('=')
|
||||
.map(|(key, _)| key == "hostPlatform")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(str::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
pairs.push(format!("hostPlatform={platform}"));
|
||||
let normalized_url = format!("{path}?{}", pairs.join("&"));
|
||||
if let Some(hash) = hash {
|
||||
format!("{normalized_url}#{hash}")
|
||||
} else {
|
||||
normalized_url
|
||||
}
|
||||
}
|
||||
|
||||
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_platform(url.as_str())).unwrap_or(url),
|
||||
),
|
||||
WebviewUrl::CustomProtocol(url) => WebviewUrl::CustomProtocol(
|
||||
Url::parse(&desktop_entry_url_with_platform(url.as_str())).unwrap_or(url),
|
||||
),
|
||||
WebviewUrl::App(path) => WebviewUrl::App(PathBuf::from(desktop_entry_url_with_platform(
|
||||
path.to_string_lossy().as_ref(),
|
||||
))),
|
||||
other => other,
|
||||
};
|
||||
config
|
||||
}
|
||||
|
||||
fn capabilities() -> Vec<&'static str> {
|
||||
vec![
|
||||
"host.getRuntime",
|
||||
@@ -290,20 +359,23 @@ fn normalize_plain_text(
|
||||
fn local_notification_payload(
|
||||
request: &HostBridgeRequest,
|
||||
) -> Result<(String, Option<String>), HostBridgeResponse> {
|
||||
let payload = request.payload.as_ref().ok_or_else(|| {
|
||||
failed(
|
||||
request.id.clone(),
|
||||
"invalid_request",
|
||||
"title is required",
|
||||
)
|
||||
})?;
|
||||
let payload = request
|
||||
.payload
|
||||
.as_ref()
|
||||
.ok_or_else(|| failed(request.id.clone(), "invalid_request", "title is required"))?;
|
||||
let title = match normalize_plain_text(
|
||||
payload.get("title").and_then(Value::as_str),
|
||||
LOCAL_NOTIFICATION_TITLE_MAX_LENGTH,
|
||||
true,
|
||||
) {
|
||||
Some(Some(title)) => title,
|
||||
_ => return Err(failed(request.id.clone(), "invalid_request", "title is required")),
|
||||
_ => {
|
||||
return Err(failed(
|
||||
request.id.clone(),
|
||||
"invalid_request",
|
||||
"title is required",
|
||||
))
|
||||
}
|
||||
};
|
||||
let body = match normalize_plain_text(
|
||||
payload.get("body").and_then(Value::as_str),
|
||||
@@ -311,7 +383,13 @@ fn local_notification_payload(
|
||||
false,
|
||||
) {
|
||||
Some(body) => body,
|
||||
None => return Err(failed(request.id.clone(), "invalid_request", "body is invalid")),
|
||||
None => {
|
||||
return Err(failed(
|
||||
request.id.clone(),
|
||||
"invalid_request",
|
||||
"body is invalid",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok((title, body))
|
||||
@@ -1339,8 +1417,7 @@ async fn host_bridge_request(
|
||||
Err(error) => return failed(request.id, "host_error", error.to_string()),
|
||||
};
|
||||
let import_result =
|
||||
tauri::async_runtime::spawn_blocking(move || import_audio_file_payload(path))
|
||||
.await;
|
||||
tauri::async_runtime::spawn_blocking(move || import_audio_file_payload(path)).await;
|
||||
match import_result {
|
||||
Ok(Ok(payload)) => ok(request.id, payload),
|
||||
Ok(Err(error)) => failed(request.id, "invalid_request", error),
|
||||
@@ -1500,6 +1577,7 @@ fn main() {
|
||||
};
|
||||
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 window =
|
||||
tauri::WebviewWindowBuilder::from_config(app.handle(), &config)?.build()?;
|
||||
register_desktop_window_close_events(&window, tray_registered);
|
||||
@@ -1616,6 +1694,36 @@ mod tests {
|
||||
.contains(&json!("notification.showLocal")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_entry_url_replaces_static_platform_marker() {
|
||||
let platform = desktop_platform();
|
||||
|
||||
let dev_url = desktop_entry_url_with_platform(
|
||||
"http://127.0.0.1:3000/?clientRuntime=native_app&hostPlatform=unknown&bridgeVersion=1",
|
||||
);
|
||||
let dev_url = Url::parse(&dev_url).expect("dev url");
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
let packaged_url = desktop_entry_url_with_platform(
|
||||
"index.html?clientRuntime=native_app&hostPlatform=unknown&bridgeVersion=1#works",
|
||||
);
|
||||
|
||||
assert!(packaged_url.contains(&format!("hostPlatform={platform}")));
|
||||
assert!(!packaged_url.contains("hostPlatform=unknown"));
|
||||
assert!(packaged_url.ends_with("#works"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_method_is_explicit() {
|
||||
let response = resolve_host_bridge_request(request("payment.request"));
|
||||
@@ -1652,7 +1760,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clipboard_text_is_truncated_to_contract_limit() {
|
||||
assert_eq!(normalize_clipboard_text("作品号 PZ-1".to_string()), "作品号 PZ-1");
|
||||
assert_eq!(
|
||||
normalize_clipboard_text("作品号 PZ-1".to_string()),
|
||||
"作品号 PZ-1"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_clipboard_text("a".repeat(CLIPBOARD_TEXT_MAX_LENGTH + 10)).len(),
|
||||
CLIPBOARD_TEXT_MAX_LENGTH
|
||||
@@ -2155,8 +2266,7 @@ mod tests {
|
||||
"mimeType": "audio/webm"
|
||||
}));
|
||||
|
||||
let (file_name, _bytes) =
|
||||
export_audio_payload(&missing_extension).expect("audio payload");
|
||||
let (file_name, _bytes) = export_audio_payload(&missing_extension).expect("audio payload");
|
||||
|
||||
assert_eq!(file_name, "敲击音效.webm");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user