4d84d76092
桌面壳本地通知失败只记录固定阶段标签 扩展桌面壳配置门禁禁止通知日志输出插件错误细节 补充宿主壳方案和共享决策中的通知诊断边界
289 lines
9.4 KiB
Rust
289 lines
9.4 KiB
Rust
use crate::host_bridge::protocol::{failed, ok, HostBridgeRequest, HostBridgeResponse};
|
|
use serde_json::json;
|
|
use serde_json::Value;
|
|
use tauri_plugin_notification::{NotificationExt, PermissionState};
|
|
|
|
const HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION: &str = "delivered_to_system";
|
|
const HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: usize = 80;
|
|
const HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH: usize = 240;
|
|
|
|
fn normalize_plain_text(
|
|
value: Option<&str>,
|
|
max_length: usize,
|
|
required: bool,
|
|
) -> Option<Option<String>> {
|
|
let Some(value) = value else {
|
|
return if required { None } else { Some(None) };
|
|
};
|
|
if value.chars().any(char::is_control) {
|
|
return None;
|
|
}
|
|
|
|
let text = value.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
if text.is_empty() {
|
|
return if required { None } else { Some(None) };
|
|
}
|
|
|
|
Some(Some(text.chars().take(max_length).collect()))
|
|
}
|
|
|
|
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 title = match normalize_plain_text(
|
|
payload.get("title").and_then(Value::as_str),
|
|
HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH,
|
|
true,
|
|
) {
|
|
Some(Some(title)) => title,
|
|
_ => {
|
|
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),
|
|
HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH,
|
|
false,
|
|
) {
|
|
Some(body) => body,
|
|
None => {
|
|
return Err(failed(
|
|
request.id.clone(),
|
|
"invalid_request",
|
|
"body is invalid",
|
|
))
|
|
}
|
|
};
|
|
|
|
Ok((title, body))
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum DesktopNotificationPermissionAction {
|
|
Show,
|
|
Request,
|
|
Reject,
|
|
}
|
|
|
|
fn desktop_notification_permission_action(
|
|
permission_state: PermissionState,
|
|
) -> DesktopNotificationPermissionAction {
|
|
match permission_state {
|
|
PermissionState::Granted => DesktopNotificationPermissionAction::Show,
|
|
PermissionState::Denied => DesktopNotificationPermissionAction::Reject,
|
|
PermissionState::Prompt | PermissionState::PromptWithRationale => {
|
|
DesktopNotificationPermissionAction::Request
|
|
}
|
|
}
|
|
}
|
|
|
|
fn desktop_notification_delivered_to_system_result() -> serde_json::Value {
|
|
json!({"action": HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION})
|
|
}
|
|
|
|
fn notification_permission_unavailable_response(request: &HostBridgeRequest) -> HostBridgeResponse {
|
|
failed(
|
|
request.id.clone(),
|
|
"host_error",
|
|
"notification permission unavailable",
|
|
)
|
|
}
|
|
|
|
fn notification_delivery_unavailable_response(request: &HostBridgeRequest) -> HostBridgeResponse {
|
|
failed(
|
|
request.id.clone(),
|
|
"host_error",
|
|
"notification delivery unavailable",
|
|
)
|
|
}
|
|
|
|
fn log_desktop_notification_failure(label: &str) -> bool {
|
|
eprintln!("desktop notification failed for {label}");
|
|
false
|
|
}
|
|
|
|
pub(crate) fn show_desktop_local_notification(
|
|
app: &tauri::AppHandle,
|
|
request: &HostBridgeRequest,
|
|
) -> HostBridgeResponse {
|
|
let (title, body) = match local_notification_payload(request) {
|
|
Ok(payload) => payload,
|
|
Err(response) => return response,
|
|
};
|
|
let notification_manager = app.notification();
|
|
let permission_state = notification_manager
|
|
.permission_state()
|
|
.map_err(|_error| {
|
|
log_desktop_notification_failure("permission.state");
|
|
notification_permission_unavailable_response(request)
|
|
});
|
|
let permission_state = match permission_state {
|
|
Ok(permission_state) => permission_state,
|
|
Err(response) => return response,
|
|
};
|
|
let mut permission_action = desktop_notification_permission_action(permission_state);
|
|
if permission_action == DesktopNotificationPermissionAction::Request {
|
|
let requested_state = notification_manager
|
|
.request_permission()
|
|
.map_err(|_error| {
|
|
log_desktop_notification_failure("permission.request");
|
|
notification_permission_unavailable_response(request)
|
|
});
|
|
let requested_state = match requested_state {
|
|
Ok(requested_state) => requested_state,
|
|
Err(response) => return response,
|
|
};
|
|
permission_action = desktop_notification_permission_action(requested_state);
|
|
}
|
|
if permission_action != DesktopNotificationPermissionAction::Show {
|
|
return failed(
|
|
request.id.clone(),
|
|
"host_error",
|
|
"notification permission denied",
|
|
);
|
|
}
|
|
|
|
let mut notification = app.notification().builder().title(title);
|
|
if let Some(body) = body {
|
|
notification = notification.body(body);
|
|
}
|
|
|
|
match notification.show() {
|
|
Ok(()) => ok(
|
|
request.id.clone(),
|
|
desktop_notification_delivered_to_system_result(),
|
|
),
|
|
Err(_error) => {
|
|
log_desktop_notification_failure("delivery.show");
|
|
notification_delivery_unavailable_response(request)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::host_bridge::protocol::request;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn local_notification_payload_is_normalized() {
|
|
let mut request = request("notification.showLocal");
|
|
request.payload = Some(json!({
|
|
"title": " 生成完成 ",
|
|
"body": " 作品已准备好 可以试玩 "
|
|
}));
|
|
|
|
let (title, body) = local_notification_payload(&request).expect("payload");
|
|
|
|
assert_eq!(title, "生成完成");
|
|
assert_eq!(body.as_deref(), Some("作品已准备好 可以试玩"));
|
|
}
|
|
|
|
#[test]
|
|
fn local_notification_payload_truncates_to_shared_contract_limits() {
|
|
let mut request = request("notification.showLocal");
|
|
request.payload = Some(json!({
|
|
"title": "a".repeat(90),
|
|
"body": "b".repeat(250)
|
|
}));
|
|
|
|
let (title, body) = local_notification_payload(&request).expect("payload");
|
|
let expected_body = "b".repeat(HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH);
|
|
|
|
assert_eq!(title, "a".repeat(HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH));
|
|
assert_eq!(body.as_deref(), Some(expected_body.as_str()));
|
|
}
|
|
|
|
#[test]
|
|
fn local_notification_success_result_reports_system_delivery() {
|
|
assert_eq!(
|
|
desktop_notification_delivered_to_system_result(),
|
|
json!({"action": "delivered_to_system"})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn notification_permission_unavailable_response_is_stable() {
|
|
let request = request("notification.showLocal");
|
|
|
|
let response = notification_permission_unavailable_response(&request);
|
|
let error = response.error.expect("error");
|
|
|
|
assert_eq!(error.code, "host_error");
|
|
assert_eq!(error.message, "notification permission unavailable");
|
|
}
|
|
|
|
#[test]
|
|
fn notification_delivery_unavailable_response_is_stable() {
|
|
let request = request("notification.showLocal");
|
|
|
|
let response = notification_delivery_unavailable_response(&request);
|
|
let error = response.error.expect("error");
|
|
|
|
assert_eq!(error.code, "host_error");
|
|
assert_eq!(error.message, "notification delivery unavailable");
|
|
}
|
|
|
|
#[test]
|
|
fn notification_failures_are_logged_without_exposing_native_detail() {
|
|
assert!(!log_desktop_notification_failure("permission.state"));
|
|
assert!(!log_desktop_notification_failure("permission.request"));
|
|
assert!(!log_desktop_notification_failure("delivery.show"));
|
|
}
|
|
|
|
#[test]
|
|
fn notification_failures_log_stable_label_only() {
|
|
assert!(!log_desktop_notification_failure("delivery.show"));
|
|
}
|
|
|
|
#[test]
|
|
fn local_notification_payload_rejects_empty_and_control_text() {
|
|
let mut empty = request("notification.showLocal");
|
|
empty.payload = Some(json!({
|
|
"title": " "
|
|
}));
|
|
|
|
let response = local_notification_payload(&empty).expect_err("empty title");
|
|
|
|
assert_eq!(response.error.expect("error").code, "invalid_request");
|
|
|
|
let mut control = request("notification.showLocal");
|
|
control.payload = Some(json!({
|
|
"title": "生成\n完成"
|
|
}));
|
|
|
|
let response = local_notification_payload(&control).expect_err("control title");
|
|
|
|
assert_eq!(response.error.expect("error").code, "invalid_request");
|
|
}
|
|
|
|
#[test]
|
|
fn desktop_notification_permission_state_controls_delivery() {
|
|
assert_eq!(
|
|
desktop_notification_permission_action(PermissionState::Granted),
|
|
DesktopNotificationPermissionAction::Show
|
|
);
|
|
assert_eq!(
|
|
desktop_notification_permission_action(PermissionState::Denied),
|
|
DesktopNotificationPermissionAction::Reject
|
|
);
|
|
assert_eq!(
|
|
desktop_notification_permission_action(PermissionState::Prompt),
|
|
DesktopNotificationPermissionAction::Request
|
|
);
|
|
assert_eq!(
|
|
desktop_notification_permission_action(PermissionState::PromptWithRationale),
|
|
DesktopNotificationPermissionAction::Request
|
|
);
|
|
}
|
|
}
|