接入 Tauri 桌面剪贴板能力

新增 Tauri clipboard-manager 依赖并通过 HostBridge 写入系统剪贴板

同步桌面壳能力清单和真实品牌图标配置

补充桌面壳配置守门、测试和宿主壳文档记忆
This commit is contained in:
2026-06-17 21:54:53 +08:00
parent 9b7da18879
commit 4acc81747a
8 changed files with 423 additions and 14 deletions

View File

@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_opener::OpenerExt;
const HOST_BRIDGE_PROTOCOL: &str = "GenarrativeHostBridge";
@@ -57,7 +58,11 @@ fn desktop_platform() -> &'static str {
}
fn capabilities() -> Vec<&'static str> {
vec!["host.getRuntime", "app.openExternalUrl"]
vec![
"host.getRuntime",
"app.openExternalUrl",
"clipboard.writeText",
]
}
fn ok(id: String, result: Value) -> HostBridgeResponse {
@@ -97,6 +102,24 @@ fn validate_request(request: &HostBridgeRequest) -> Option<HostBridgeResponse> {
None
}
fn required_string_payload<'a>(
request: &'a HostBridgeRequest,
field: &'static str,
) -> Result<&'a str, HostBridgeResponse> {
request
.payload
.as_ref()
.and_then(|value| value.get(field))
.and_then(Value::as_str)
.ok_or_else(|| {
failed(
request.id.clone(),
"invalid_request",
format!("{} is required", field),
)
})
}
fn resolve_host_bridge_request(request: HostBridgeRequest) -> HostBridgeResponse {
if let Some(response) = validate_request(&request) {
return response;
@@ -132,13 +155,9 @@ async fn host_bridge_request(
match request.method.as_str() {
"app.openExternalUrl" => {
let Some(url) = request
.payload
.as_ref()
.and_then(|value| value.get("url"))
.and_then(Value::as_str)
else {
return failed(request.id, "invalid_request", "url is required");
let url = match required_string_payload(&request, "url") {
Ok(url) => url,
Err(response) => return response,
};
match app.opener().open_url(url, None::<&str>) {
@@ -146,12 +165,24 @@ async fn host_bridge_request(
Err(error) => failed(request.id, "host_error", error.to_string()),
}
}
"clipboard.writeText" => {
let text = match required_string_payload(&request, "text") {
Ok(text) => text,
Err(response) => return response,
};
match app.clipboard().write_text(text) {
Ok(()) => ok(request.id, json!(true)),
Err(error) => failed(request.id, "host_error", error.to_string()),
}
}
_ => resolve_host_bridge_request(request),
}
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
let window_config = app.config().app.windows.get(0).cloned();
@@ -210,4 +241,17 @@ mod tests {
assert!(!response.ok);
assert_eq!(response.error.expect("error").code, "invalid_request");
}
#[test]
fn invalid_string_payload_is_rejected() {
let mut invalid = request("clipboard.writeText");
invalid.payload = Some(json!({ "text": 123 }));
let response = required_string_payload(&invalid, "text").expect_err("invalid payload");
assert!(!response.ok);
let error = response.error.expect("error");
assert_eq!(error.code, "invalid_request");
assert_eq!(error.message, "text is required");
}
}