From bacb17506dcdcb4572b3d2f07a770ae46c405eb0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 19:40:54 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=A4=B1=E8=B4=A5=E8=A7=82=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面剪贴板和通知插件异常记录日志 Rust 单测覆盖稳定错误和日志边界 配置门禁和文档同步桌面插件观测约束 --- apps/desktop-shell/scripts/check-config.mjs | 11 ++++++ .../src-tauri/src/host_bridge/clipboard.rs | 30 +++++++++++++- .../src/host_bridge/notifications.rs | 39 +++++++++++++++++-- .../shared-memory/decision-log.md | 6 +++ ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 2 + 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 38a157e3a..d764828f3 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -2065,6 +2065,10 @@ for (const snippet of [ 'write_desktop_clipboard_text(app, text)', 'read_desktop_clipboard_text(app)', 'normalize_clipboard_text(&text)', + 'log_desktop_clipboard_failure', + 'desktop clipboard failed for {label}: {error}', + 'log_desktop_clipboard_failure("write.text", &error)', + 'log_desktop_clipboard_failure("read.text", &error)', 'clipboard_write_unavailable_response', 'clipboard_read_unavailable_response', 'required_string_payload(request, "text")', @@ -2073,6 +2077,7 @@ for (const snippet of [ '"clipboard write unavailable"', '"clipboard read unavailable"', 'clipboard_unavailable_responses_are_stable', + 'clipboard_failures_are_logged_without_exposing_native_detail', ]) { if (!desktopHostBridgeClipboardSource.includes(snippet)) { throw new Error(`desktop shell clipboard module is missing ${snippet}`); @@ -2699,6 +2704,11 @@ for (const snippet of [ 'desktop_notification_permission_action', 'notification_permission_unavailable_response', 'notification_delivery_unavailable_response', + 'log_desktop_notification_failure', + 'desktop notification failed for {label}: {error}', + 'log_desktop_notification_failure("permission.state", &error.to_string())', + 'log_desktop_notification_failure("permission.request", &error.to_string())', + 'log_desktop_notification_failure("delivery.show", &error.to_string())', 'PermissionState::Granted', 'PermissionState::Denied', 'PermissionState::Prompt | PermissionState::PromptWithRationale', @@ -2707,6 +2717,7 @@ for (const snippet of [ 'local_notification_payload_truncates_to_shared_contract_limits', 'notification_permission_unavailable_response_is_stable', 'notification_delivery_unavailable_response_is_stable', + 'notification_failures_are_logged_without_exposing_native_detail', '"a".repeat(HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH)', '"b".repeat(HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH)', '"notification permission unavailable"', diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs b/apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs index 38ea6d041..bb741d6be 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs @@ -27,6 +27,14 @@ pub(crate) fn read_desktop_clipboard_text(app: &tauri::AppHandle) -> Result bool { + if !error.is_empty() { + eprintln!("desktop clipboard failed for {label}: {error}"); + } + + false +} + pub(crate) fn clipboard_write_unavailable_response( request: &HostBridgeRequest, ) -> HostBridgeResponse { @@ -58,7 +66,10 @@ pub(crate) fn write_desktop_host_bridge_clipboard_text( match write_desktop_clipboard_text(app, text) { Ok(()) => ok(request.id.clone(), json!(true)), - Err(_) => clipboard_write_unavailable_response(request), + Err(error) => { + log_desktop_clipboard_failure("write.text", &error); + clipboard_write_unavailable_response(request) + } } } @@ -73,7 +84,10 @@ pub(crate) fn read_desktop_host_bridge_clipboard_text( "text": text, }), ), - Err(_) => clipboard_read_unavailable_response(request), + Err(error) => { + log_desktop_clipboard_failure("read.text", &error); + clipboard_read_unavailable_response(request) + } } } @@ -140,4 +154,16 @@ mod tests { assert_eq!(read_error.code, "host_error"); assert_eq!(read_error.message, "clipboard read unavailable"); } + + #[test] + fn clipboard_failures_are_logged_without_exposing_native_detail() { + assert!(!log_desktop_clipboard_failure( + "write.text", + "private clipboard detail" + )); + assert!(!log_desktop_clipboard_failure( + "read.text", + "private clipboard detail" + )); + } } diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs b/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs index a4abfb8b1..65a224646 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs @@ -105,6 +105,14 @@ fn notification_delivery_unavailable_response(request: &HostBridgeRequest) -> Ho ) } +fn log_desktop_notification_failure(label: &str, error: &str) -> bool { + if !error.is_empty() { + eprintln!("desktop notification failed for {label}: {error}"); + } + + false +} + pub(crate) fn show_desktop_local_notification( app: &tauri::AppHandle, request: &HostBridgeRequest, @@ -116,7 +124,10 @@ pub(crate) fn show_desktop_local_notification( let notification_manager = app.notification(); let permission_state = notification_manager .permission_state() - .map_err(|_| notification_permission_unavailable_response(request)); + .map_err(|error| { + log_desktop_notification_failure("permission.state", &error.to_string()); + notification_permission_unavailable_response(request) + }); let permission_state = match permission_state { Ok(permission_state) => permission_state, Err(response) => return response, @@ -125,7 +136,10 @@ pub(crate) fn show_desktop_local_notification( if permission_action == DesktopNotificationPermissionAction::Request { let requested_state = notification_manager .request_permission() - .map_err(|_| notification_permission_unavailable_response(request)); + .map_err(|error| { + log_desktop_notification_failure("permission.request", &error.to_string()); + notification_permission_unavailable_response(request) + }); let requested_state = match requested_state { Ok(requested_state) => requested_state, Err(response) => return response, @@ -150,7 +164,10 @@ pub(crate) fn show_desktop_local_notification( request.id.clone(), desktop_notification_delivered_to_system_result(), ), - Err(_) => notification_delivery_unavailable_response(request), + Err(error) => { + log_desktop_notification_failure("delivery.show", &error.to_string()); + notification_delivery_unavailable_response(request) + } } } @@ -219,6 +236,22 @@ mod tests { assert_eq!(error.message, "notification delivery unavailable"); } + #[test] + fn notification_failures_are_logged_without_exposing_native_detail() { + assert!(!log_desktop_notification_failure( + "permission.state", + "private notification detail" + )); + assert!(!log_desktop_notification_failure( + "permission.request", + "private notification detail" + )); + assert!(!log_desktop_notification_failure( + "delivery.show", + "private notification detail" + )); + } + #[test] fn local_notification_payload_rejects_empty_and_control_text() { let mut empty = request("notification.showLocal"); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3948c6bee..b71b2c8b0 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3118,6 +3118,12 @@ - 决策:`apps/mobile-shell/src/host-bridge/clipboard.ts` 必须在剪贴板写入 / 读取失败时记录 `mobile clipboard failed for ...` 日志;`apps/mobile-shell/src/host-bridge/haptics.ts` 必须在触觉派发失败时记录 `mobile haptics failed for ...` 日志;`apps/mobile-shell/src/host-bridge/network.ts` 必须在 HostBridge 网络状态查询失败时记录 `mobile network failed for ...` 日志。HostBridge 对 H5 仍只返回稳定错误语义;该约束不新增后台网络探测、任意系统能力或 H5 业务兜底路径。 - 验证方式:`npm run mobile-shell:test -- src/host-bridge/clipboard.test.ts src/host-bridge/haptics.test.ts src/host-bridge/network.test.ts`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-20 桌面壳剪贴板与通知异常必须可观测 + +- 背景:Tauri 桌面壳已声明剪贴板读写和即时本地通知;这些能力会触发 Tauri clipboard-manager 和 notification 插件。如果插件异常只被折叠成稳定 HostBridge 错误,H5 语义安全,但开发侧无法区分系统剪贴板不可用、通知权限查询异常或系统通知投递失败。 +- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs` 必须在剪贴板写入 / 读取失败时记录 `desktop clipboard failed for ...` 日志;`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs` 必须在通知权限状态读取、权限请求和通知投递失败时记录 `desktop notification failed for ...` 日志。HostBridge 对 H5 仍只返回稳定错误语义;该约束不新增遥测 SDK、后台通知、自动更新或 H5 直连 Tauri JS 插件。 +- 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml host_bridge::clipboard host_bridge::notifications`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-20 移动壳门禁脚本必须自登记自扫描 - 背景:Expo 移动壳单端检查已把 `apps/mobile-shell/scripts/` 纳入生产源码扫描入口,但 `check-config.mjs` 自身仍被排除在脚本清单和替身词扫描之外;这会让移动壳与桌面壳门禁结构不一致,也可能让后续门禁反查内容绕过生产替身词规则。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index bd2d5a746..0c4c79caa 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -346,6 +346,8 @@ GameBridge 禁止: 2026-06-19 追加:移动壳 `haptics.impact` 的 HostBridge payload 解析、共享 style 归一、Expo style 映射和 `Haptics.impactAsync(...)` 真实调用统一收口在 `apps/mobile-shell/src/host-bridge/haptics.ts`;`dispatch.ts` 只按 method 委托 `runMobileHostBridgeHapticsImpact(request.payload)`。 +2026-06-20 追加:桌面壳剪贴板和本地通知插件异常必须可观测。Tauri clipboard-manager 读写失败、notification permission state / request 失败、系统通知 show 失败时,桌面壳必须分别记录 `desktop clipboard failed for ...` 或 `desktop notification failed for ...` 日志;HostBridge 回包仍只暴露稳定 `clipboard write unavailable`、`clipboard read unavailable`、`notification permission unavailable` 或 `notification delivery unavailable` 语义。该约束不新增遥测 SDK、后台通知、自动更新或 H5 直连 Tauri JS 插件。 + 2026-06-18 追加:H5 账号状态刷新开始消费 `app.reloadWebView`。用户登录成功、退出登录、其它身份边界变化或登录状态异常页点击重新尝试时,`AuthGate` 会优先请求 Expo 壳刷新当前 WebView;宿主未声明或刷新失败时再回退浏览器刷新,避免在移动壳内绕过受控容器刷新入口。 2026-06-18 追加:移动壳 WebView 内容 / 渲染进程终止时复用同一受控刷新路径。iOS `onContentProcessDidTerminate` 和 Android `onRenderProcessGone` 只调用当前 `react-native-webview` 的 `reload()`,不改写 H5 URL、不注入额外脚本、不新增宿主恢复页面,避免系统回收 WebView 进程后留下空白容器。