From 9188a003edbffe3092af29d24536e78a2d65a7cf Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 07:17:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E9=80=9A=E7=9F=A5=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面通知成功结果和长度上限改用共享契约镜像命名 桌面壳配置检查锁定通知契约镜像 共享决策记录补充桌面通知载荷边界 --- apps/desktop-shell/scripts/check-config.mjs | 11 ++++++----- .../src-tauri/src/host_bridge/notifications.rs | 11 ++++++----- docs/project-memory/shared-memory/decision-log.md | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 9577be02c..c20ba0ce6 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -1412,11 +1412,11 @@ const desktopHostBridgePayloadLimits = { ), HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: extractRustNumberConst( desktopHostBridgeNotificationsSource, - 'LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', + 'HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', ), HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH: extractRustNumberConst( desktopHostBridgeNotificationsSource, - 'LOCAL_NOTIFICATION_BODY_MAX_LENGTH', + 'HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH', ), HOST_BRIDGE_FILE_NAME_MAX_LENGTH: extractRustNumberConst( rustHostSource, @@ -2427,15 +2427,16 @@ if ( } for (const snippet of [ 'tauri_plugin_notification::{NotificationExt, PermissionState}', - 'LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', - 'LOCAL_NOTIFICATION_BODY_MAX_LENGTH', + 'HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION', + 'HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', + 'HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH', 'DesktopNotificationPermissionAction', 'desktop_notification_permission_action', 'PermissionState::Granted', 'PermissionState::Denied', 'PermissionState::Prompt | PermissionState::PromptWithRationale', 'desktop_notification_delivered_to_system_result', - '"delivered_to_system"', + 'json!({"action": HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION})', '"notification permission denied"', 'app.notification().builder()', ]) { 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 e9ffab05e..ddb502ced 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs @@ -3,8 +3,9 @@ use serde_json::json; use serde_json::Value; use tauri_plugin_notification::{NotificationExt, PermissionState}; -const LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: usize = 80; -const LOCAL_NOTIFICATION_BODY_MAX_LENGTH: usize = 240; +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>, @@ -35,7 +36,7 @@ fn local_notification_payload( .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, + HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH, true, ) { Some(Some(title)) => title, @@ -49,7 +50,7 @@ fn local_notification_payload( }; let body = match normalize_plain_text( payload.get("body").and_then(Value::as_str), - LOCAL_NOTIFICATION_BODY_MAX_LENGTH, + HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH, false, ) { Some(body) => body, @@ -85,7 +86,7 @@ fn desktop_notification_permission_action( } fn desktop_notification_delivered_to_system_result() -> serde_json::Value { - json!({"action": "delivered_to_system"}) + json!({"action": HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION}) } pub(crate) fn show_desktop_local_notification( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 5c34222ab..c3dfff2bb 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -40,6 +40,7 @@ - 2026-06-19 桌面壳窗口标题桥接边界:Tauri `app.setTitle` 的 payload 校验、非空 / 控制字符拒绝、80 字符截断和主窗口 `set_title` 调用统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/title.rs`;`dispatch.rs` 只负责委托 `set_desktop_host_bridge_window_title(...)`。桌面壳配置检查和根级结构门禁会覆盖 `title.rs` 文件清单、共享标题长度镜像和 dispatch 委托关系。 - 2026-06-19 桌面壳文件桥接执行边界:Tauri `file.exportText` / `file.importText` / `file.importDocument` / `file.exportImage` / `file.importImage` / `file.importAudio` / `file.exportAudio` 的系统文件对话框过滤器、用户取消语义、路径转换、异步读写编排和 HostBridge 响应统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/files.rs`;MIME、大小、base64、文件名清洗、本地副本读写和 HostBridge payload 组装统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/file_payloads.rs`;`dispatch.rs` 只负责按 method 委托 `export_desktop_host_bridge_*_file(...)` / `import_desktop_host_bridge_*_file(...)`。桌面壳配置检查会拒绝分发层直接调用 `.dialog()`、`blocking_save_file` / `blocking_pick_file`、文件 payload helper 或落盘 helper,避免文件访问边界重新散落。 - 2026-06-20 移动壳文件桥接载荷边界:Expo `file.exportText` / `file.importText` / `file.importDocument` / `file.exportImage` / `file.importImage` / `file.captureImage` / `file.importAudio` / `file.exportAudio` 的 DocumentPicker、ImagePicker、File、Sharing 系统交互、用户取消语义、缓存读写编排和 HostBridge 响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts`;MIME、大小、base64、文件名清洗、图片 / 音频 bytes 匹配和 picker 结果到 HostBridge payload 的组装统一收口在 `apps/mobile-shell/src/host-bridge/filePayloads.ts`。移动壳单端配置检查和根级 `npm run check:native-shells` 会把 `filePayloads.ts` 纳入结构清单与 HostBridge 源码扫描,避免文件载荷边界重新散落到分发层或 shell 层。 +- 2026-06-20 桌面本地通知契约镜像:Tauri `notification.showLocal` 的 title / body 归一化、长度上限和成功结果 action 必须镜像共享 HostBridge 契约;Rust 侧常量使用 `HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH`、`HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH` 和 `HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION` 命名,桌面单端配置检查会与 `packages/shared/src/contracts/hostBridge.ts` 比对数值并反查成功结果由该 action 常量组装,避免通知 payload 边界变成桌面壳本地规则。 - 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5,WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。 - 2026-06-20 H5 原生导航预校验:`navigateHostNativePage()` 在 `native_app` 下发送 `navigation.openNativePage` 前必须先拒绝空值、控制字符、协议相对 URL、外域绝对 URL 和非 `http:` / `https:` 协议目标;同源绝对 URL、`/path` 和保留给桌面壳兼容的相对 route 继续交给 Expo / Tauri 壳二次归一并补写宿主上下文。微信小程序分支仍按小程序页面 URL 语义走 `wx.miniProgram.navigateTo`,不套原生 App 同源 H5 预校验。根级 `npm run check:native-shells` 会反查 H5 facade 仍使用 `normalizeNativeAppPageUrl(...)` 且发送归一后的 URL,避免明显不安全目标触达原生壳。 - 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。