From af6aea3de23e049b56b4d9854bf94bb70233be81 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 21:37:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E6=A1=8C=E9=9D=A2=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Tauri 本地通知发送收口到 notifications 模块 让桌面 HostBridge dispatch 只负责通知 payload 分发 同步原生壳结构门禁和架构文档清单 --- apps/desktop-shell/scripts/check-config.mjs | 56 +++++- .../src-tauri/src/host_bridge/dispatch.rs | 173 +---------------- .../src-tauri/src/host_bridge/mod.rs | 1 + .../src/host_bridge/notifications.rs | 180 ++++++++++++++++++ .../shared-memory/decision-log.md | 5 +- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 6 +- ...前端架构】宿主壳能力统一协议-2026-06-17.md | 6 +- scripts/check-native-shells.mjs | 1 + 8 files changed, 248 insertions(+), 180 deletions(-) create mode 100644 apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index d533e351a..8e94b96ed 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -70,6 +70,14 @@ const desktopHostBridgeDispatchSource = fs.readFileSync( desktopHostBridgeDispatchPath, 'utf8', ); +const desktopHostBridgeNotificationsPath = new URL( + '../src-tauri/src/host_bridge/notifications.rs', + import.meta.url, +); +const desktopHostBridgeNotificationsSource = fs.readFileSync( + desktopHostBridgeNotificationsPath, + 'utf8', +); const desktopHostBridgeSharePath = new URL( '../src-tauri/src/host_bridge/share.rs', import.meta.url, @@ -190,6 +198,7 @@ const expectedDesktopHostBridgeRustFiles = [ 'dispatch.rs', 'files.rs', 'mod.rs', + 'notifications.rs', 'protocol.rs', 'share.rs', ]; @@ -1286,11 +1295,11 @@ const desktopHostBridgePayloadLimits = { 'CLIPBOARD_TEXT_MAX_LENGTH', ), HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: extractRustNumberConst( - rustHostSource, + desktopHostBridgeNotificationsSource, 'LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', ), HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH: extractRustNumberConst( - rustHostSource, + desktopHostBridgeNotificationsSource, 'LOCAL_NOTIFICATION_BODY_MAX_LENGTH', ), HOST_BRIDGE_FILE_NAME_MAX_LENGTH: extractRustNumberConst( @@ -1799,6 +1808,7 @@ const expectedRustHostBridgeFiles = [ 'dispatch.rs', 'files.rs', 'mod.rs', + 'notifications.rs', 'protocol.rs', 'share.rs', ]; @@ -1904,8 +1914,6 @@ const requiredRustHostSnippets = [ 'PermissionState::Granted', 'PermissionState::Denied', 'PermissionState::Prompt | PermissionState::PromptWithRationale', - 'notification_manager.permission_state()', - 'notification_manager.request_permission()', '"notification permission denied"', '"copied_to_clipboard"', '"file export cancelled"', @@ -2040,6 +2048,46 @@ assertSameList( if (nativeAppHostBridgeSource.includes("'host_bridge_request'")) { throw new Error('H5 native app HostBridge must use HOST_BRIDGE_TAURI_COMMAND'); } +if ( + !desktopHostBridgeDispatchSource.includes( + 'show_desktop_local_notification(&app, &request)', + ) || + desktopHostBridgeDispatchSource.includes('app.notification()') || + desktopHostBridgeDispatchSource.includes('NotificationExt') || + desktopHostBridgeDispatchSource.includes('PermissionState') +) { + throw new Error( + 'desktop shell notification HostBridge method must delegate to notifications module', + ); +} +for (const snippet of [ + 'tauri_plugin_notification::{NotificationExt, PermissionState}', + 'LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', + 'LOCAL_NOTIFICATION_BODY_MAX_LENGTH', + 'DesktopNotificationPermissionAction', + 'desktop_notification_permission_action', + 'PermissionState::Granted', + 'PermissionState::Denied', + 'PermissionState::Prompt | PermissionState::PromptWithRationale', + '"notification permission denied"', + 'app.notification().builder()', +]) { + if (!desktopHostBridgeNotificationsSource.includes(snippet)) { + throw new Error(`desktop shell notification module is missing ${snippet}`); + } +} +if ( + !desktopHostBridgeNotificationsSource.match( + /notification_manager\s*\.\s*permission_state\s*\(\s*\)/, + ) || + !desktopHostBridgeNotificationsSource.match( + /notification_manager\s*\.\s*request_permission\s*\(\s*\)/, + ) +) { + throw new Error( + 'desktop shell notification module must check and request notification permission', + ); +} if (!h5HostBridgeSource.includes('HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS,')) { throw new Error('H5 HostBridge facade must import shared runtime refresh timeout'); } diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs b/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs index bbe68de05..6a56c3914 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs @@ -5,6 +5,7 @@ use crate::host_bridge::files::{ import_document_file_payload, import_image_file_payload, import_text_file_payload, write_export_bytes_file, write_export_text_file, }; +use crate::host_bridge::notifications::show_desktop_local_notification; use crate::host_bridge::protocol::{ failed, ok, required_string_payload, validate_request, HostBridgeRequest, HostBridgeResponse, HostBridgeRuntime, HOST_BRIDGE_VERSION, @@ -17,11 +18,8 @@ use crate::shell::webview::{ use serde_json::{json, Value}; use tauri::Manager; use tauri_plugin_dialog::DialogExt; -use tauri_plugin_notification::{NotificationExt, PermissionState}; const BADGE_COUNT_MAX: i64 = 99999; -const LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: usize = 80; -const LOCAL_NOTIFICATION_BODY_MAX_LENGTH: usize = 240; const WINDOW_TITLE_MAX_LENGTH: usize = 80; fn normalize_window_title(raw_title: &str) -> Option { @@ -58,84 +56,6 @@ fn badge_count_payload(request: &HostBridgeRequest) -> Result, HostB Ok(if count == 0 { None } else { Some(count) }) } -fn normalize_plain_text( - value: Option<&str>, - max_length: usize, - required: bool, -) -> Option> { - 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::>().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), 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), - 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), - 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_runtime() -> HostBridgeRuntime { HostBridgeRuntime { shell: "tauri_desktop", @@ -487,38 +407,10 @@ pub(super) async fn execute_host_bridge_request( Err(error) => failed(request.id, "host_error", error.to_string()), } } - "notification.showLocal" => { - let (title, body) = match local_notification_payload(&request) { - Ok(payload) => payload, - Err(response) => return response, - }; - let notification_manager = app.notification(); - let permission_state = match notification_manager.permission_state() { - Ok(permission_state) => permission_state, - Err(error) => return failed(request.id, "host_error", error.to_string()), - }; - let mut permission_action = desktop_notification_permission_action(permission_state); - if permission_action == DesktopNotificationPermissionAction::Request { - let requested_state = match notification_manager.request_permission() { - Ok(permission_state) => permission_state, - Err(error) => return failed(request.id, "host_error", error.to_string()), - }; - permission_action = desktop_notification_permission_action(requested_state); - } - if permission_action != DesktopNotificationPermissionAction::Show { - return failed(request.id, "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, json!(true)), - Err(error) => failed(request.id, "host_error", error.to_string()), - } - } + "notification.showLocal" => match show_desktop_local_notification(&app, &request) { + Ok(()) => ok(request.id, json!(true)), + Err(response) => response, + }, "share.setTarget" => { let target = request .payload @@ -671,61 +563,6 @@ mod tests { } } - #[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_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 - ); - } - #[test] fn window_title_normalization_requires_visible_text() { assert_eq!( diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/mod.rs b/apps/desktop-shell/src-tauri/src/host_bridge/mod.rs index 0d1722b06..069d7e250 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/mod.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod capabilities; mod clipboard; mod dispatch; pub(crate) mod files; +mod notifications; pub(crate) mod protocol; mod share; diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs b/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs new file mode 100644 index 000000000..777e9c9a2 --- /dev/null +++ b/apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs @@ -0,0 +1,180 @@ +use crate::host_bridge::protocol::{failed, HostBridgeRequest, HostBridgeResponse}; +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; + +fn normalize_plain_text( + value: Option<&str>, + max_length: usize, + required: bool, +) -> Option> { + 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::>().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), 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), + 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), + 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 + } + } +} + +pub(crate) fn show_desktop_local_notification( + app: &tauri::AppHandle, + request: &HostBridgeRequest, +) -> Result<(), HostBridgeResponse> { + let (title, body) = local_notification_payload(request)?; + let notification_manager = app.notification(); + let permission_state = notification_manager + .permission_state() + .map_err(|error| failed(request.id.clone(), "host_error", error.to_string()))?; + 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| failed(request.id.clone(), "host_error", error.to_string()))?; + permission_action = desktop_notification_permission_action(requested_state); + } + if permission_action != DesktopNotificationPermissionAction::Show { + return Err(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); + } + + notification + .show() + .map_err(|error| failed(request.id.clone(), "host_error", error.to_string())) +} + +#[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_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 + ); + } +} diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 4c6482523..d5c0a20cd 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -83,6 +83,7 @@ - 2026-06-18 剪贴板读取能力:新增 `clipboard.readText` HostBridge capability,H5 只能读取纯文本结果,契约限制返回文本最多 100000 字符;Expo 壳通过 `expo-clipboard` 读取系统剪贴板文本,Tauri 壳通过 Rust 侧 `tauri-plugin-clipboard-manager` 读取文本且不开放插件 JS guest API。该能力不读取图片、HTML、文件列表或剪贴板监听事件,宿主未声明或读取失败时由 H5 视作失败并保留原流程。 - 2026-06-19 移动壳剪贴板边界:Expo `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写与共享 100000 字符归一统一收口在 `apps/mobile-shell/src/host-bridge/clipboard.ts`;`dispatch.ts` 只负责从 HostBridge payload 取 `text` 并调用 `writeMobileClipboardText(...)` / `readMobileClipboardText()`,不得直接导入 `expo-clipboard` 或调用 `Clipboard.setStringAsync` / `Clipboard.getStringAsync`。移动壳配置检查会覆盖该模块结构、共享文本边界和 dispatch 委托关系,避免剪贴板能力散落到分发层。 - 2026-06-19 桌面壳剪贴板边界:Tauri `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写与共享 100000 字符归一统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`;`dispatch.rs` 只负责从 HostBridge payload 取 `text` 并调用 `write_desktop_clipboard_text(...)` / `read_desktop_clipboard_text(...)`,不得直接承接剪贴板文本截断或插件读写细节。桌面壳配置检查会覆盖该模块结构、共享文本边界和 dispatch 委托关系,避免剪贴板能力散落到分发层。 +- 2026-06-19 桌面壳本地通知边界:Tauri `notification.showLocal` 的 payload 清洗、权限状态检查、prompt 权限请求和系统通知发送统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs`;`dispatch.rs` 只负责把 HostBridge request 委托给 `show_desktop_local_notification(...)` 并映射响应,不得直接调用 `app.notification()`、`NotificationExt` 或 `PermissionState`。桌面壳配置检查会覆盖该模块结构、权限语义和 dispatch 委托关系,避免本地通知能力散落到分发层。 - 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。 - 2026-06-19 文档文件导入能力:新增 `file.importDocument` HostBridge capability,作为创作 Agent 工作台优先导入路径;Expo 壳通过 DocumentPicker、Tauri 壳通过系统文件选择框读取文本类文档或 DOCX 副本。两端只接受文本 MIME / DOCX MIME 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、base64 内容和字节数,不暴露设备 URI、本机绝对路径,也不开放通用文件系统;H5 把返回内容转换成浏览器 `File` 后继续走后端 `/api/runtime/creation-agent/document-inputs/parse`,不在前端解析 DOCX。旧壳只声明 `file.importText` 时继续使用文本导入兜底。 - 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability,不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘,必须通过托盘“退出”结束应用;托盘注册失败不得阻断主窗口启动,也不得拦截关闭,避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。 @@ -2660,8 +2661,8 @@ ## 2026-06-18 桌面壳通知权限门禁 - 背景:桌面壳已经声明并实现 `notification.showLocal`,且 Tauri capability 只授权 `allow-host-bridge-request`;但 Rust handler 在清洗 payload 后直接调用 `notification.show()`,没有显式检查系统通知权限,也没有把权限拒绝固定成 HostBridge 失败语义。 -- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs` 在发送即时本地通知前先调用 `permission_state()`,已授权才发送;处于 prompt / prompt-with-rationale 时只在 Rust 侧调用 `request_permission()` 后复判;最终未授权返回 `host_error: notification permission denied`。桌面壳仍不把 `notification:*` 插件命令加入 capability permissions,不向 H5 暴露 notification 插件 JS guest API、远程推送、定时提醒或通知 token。 -- 影响范围:`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/scripts/check-config.mjs`、宿主壳能力统一协议文档、Expo / Tauri HostBridge 方案文档。 +- 决策:`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs` 在发送即时本地通知前先调用 `permission_state()`,已授权才发送;处于 prompt / prompt-with-rationale 时只在 Rust 侧调用 `request_permission()` 后复判;最终未授权返回 `host_error: notification permission denied`。`dispatch.rs` 只保留 method 委托和 HostBridge 响应映射。桌面壳仍不把 `notification:*` 插件命令加入 capability permissions,不向 H5 暴露 notification 插件 JS guest API、远程推送、定时提醒或通知 token。 +- 影响范围:`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/scripts/check-config.mjs`、宿主壳能力统一协议文档、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run desktop-shell:typecheck`、`npm run desktop-shell:test`、`npm run check:native-shells`、`npm run typecheck -- --pretty false`、`npm run check:encoding`、`git diff --check`。 ## 2026-06-19 桌面壳主窗口启动门禁 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index bc1bc54ef..baebe20ad 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -64,11 +64,11 @@ src/ 已落地:`packages/shared/src/contracts/hostBridge.ts` 保存消息 envelope、method、payload 和错误码,H5、Expo 壳与 Tauri 壳共享同一份协议类型。 -三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂统一放在 `miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面入口只做 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` / `scanner.ts` 分别承接文件、分享和扫码能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区、扫码 overlay 和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面,`apps/desktop-shell/src-tauri/src/app.rs` 承接 Tauri builder / plugin / window 装配,`main.rs` 只保留薄入口并调用 `app::run()`。 +三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `miniprogram/pages/*`,`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,不把微信小程序硬改成 Expo / Tauri 的 request 总线;Page 生命周期、`wx.*` 容器调用、WebView 容器行为和页面工厂统一放在 `miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`,页面入口只做 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` / `scanner.ts` 分别承接文件、分享和扫码能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区、扫码 overlay 和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` / `notifications.rs` 分别承接文件、分享和本地通知能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面,`apps/desktop-shell/src-tauri/src/app.rs` 承接 Tauri builder / plugin / window 装配,`main.rs` 只保留薄入口并调用 `app::run()`。 -当前 `npm run check:native-shells` 锁定的生产文件清单为:微信桥接层 `dispatch.js`、`payment.js`、`protocol.js`、`shareGrid.js`、`subscribeMessage.js`、`webView.js`;微信 shell 层 `payment.js`、`shareGrid.js`、`subscribeMessage.js`、`webView.js`;微信页面包装层 `share-grid/index.js`、`share-grid/index.json`、`share-grid/index.wxml`、`share-grid/index.wxss`、`subscribe-message/index.js`、`subscribe-message/index.json`、`subscribe-message/index.wxml`、`subscribe-message/index.wxss`、`web-view/index.js`、`web-view/index.json`、`web-view/index.wxml`、`web-view/index.wxss`、`wechat-pay/index.js`、`wechat-pay/index.json`、`wechat-pay/index.wxml`、`wechat-pay/index.wxss`;移动源码根 `env.d.ts`;移动桥接层 `bridge.ts`、`capabilities.ts`、`clipboard.ts`、`dispatch.ts`、`files.ts`、`haptics.ts`、`protocol.ts`、`scanner.ts`、`share.ts`;移动 shell 层 `QrScannerOverlay.tsx`、`ShellApp.tsx`、`deepLink.ts`、`lifecycle.ts`、`loadFailure.ts`、`navigation.ts`、`network.ts`、`runtime.ts`、`safeArea.ts`、`url.ts`、`webViewGlobals.d.ts`、`webViewHistory.ts`、`webViewPolicy.ts`;桌面入口 `app.rs`、`main.rs`;桌面桥接层 `capabilities.rs`、`clipboard.rs`、`dispatch.rs`、`files.rs`、`mod.rs`、`protocol.rs`、`share.rs`;桌面 shell 层 `deep_link.rs`、`events.rs`、`file_drop.rs`、`lifecycle.rs`、`menu.rs`、`mod.rs`、`navigation.rs`、`network.rs`、`runtime.rs`、`tray.rs`、`url.rs`、`webview.rs`、`window_state.rs`。 +当前 `npm run check:native-shells` 锁定的生产文件清单为:微信桥接层 `dispatch.js`、`payment.js`、`protocol.js`、`shareGrid.js`、`subscribeMessage.js`、`webView.js`;微信 shell 层 `payment.js`、`shareGrid.js`、`subscribeMessage.js`、`webView.js`;微信页面包装层 `share-grid/index.js`、`share-grid/index.json`、`share-grid/index.wxml`、`share-grid/index.wxss`、`subscribe-message/index.js`、`subscribe-message/index.json`、`subscribe-message/index.wxml`、`subscribe-message/index.wxss`、`web-view/index.js`、`web-view/index.json`、`web-view/index.wxml`、`web-view/index.wxss`、`wechat-pay/index.js`、`wechat-pay/index.json`、`wechat-pay/index.wxml`、`wechat-pay/index.wxss`;移动源码根 `env.d.ts`;移动桥接层 `bridge.ts`、`capabilities.ts`、`clipboard.ts`、`dispatch.ts`、`files.ts`、`haptics.ts`、`notifications.ts`、`protocol.ts`、`scanner.ts`、`share.ts`;移动 shell 层 `QrScannerOverlay.tsx`、`ShellApp.tsx`、`deepLink.ts`、`lifecycle.ts`、`loadFailure.ts`、`navigation.ts`、`network.ts`、`runtime.ts`、`safeArea.ts`、`url.ts`、`webViewGlobals.d.ts`、`webViewHistory.ts`、`webViewPolicy.ts`;桌面入口 `app.rs`、`main.rs`;桌面桥接层 `capabilities.rs`、`clipboard.rs`、`dispatch.rs`、`files.rs`、`mod.rs`、`notifications.rs`、`protocol.rs`、`share.rs`;桌面 shell 层 `deep_link.rs`、`events.rs`、`file_drop.rs`、`lifecycle.rs`、`menu.rs`、`mod.rs`、`navigation.rs`、`network.rs`、`runtime.rs`、`tray.rs`、`url.rs`、`webview.rs`、`window_state.rs`。 -结构门禁按完整相对路径反查文档和目录:微信桥接层为 `miniprogram/host-bridge/dispatch.js`、`miniprogram/host-bridge/payment.js`、`miniprogram/host-bridge/protocol.js`、`miniprogram/host-bridge/shareGrid.js`、`miniprogram/host-bridge/subscribeMessage.js`、`miniprogram/host-bridge/webView.js`;微信 shell 层为 `miniprogram/shell/payment.js`、`miniprogram/shell/shareGrid.js`、`miniprogram/shell/subscribeMessage.js`、`miniprogram/shell/webView.js`;微信页面包装层为 `miniprogram/pages/share-grid/index.js`、`miniprogram/pages/share-grid/index.json`、`miniprogram/pages/share-grid/index.wxml`、`miniprogram/pages/share-grid/index.wxss`、`miniprogram/pages/subscribe-message/index.js`、`miniprogram/pages/subscribe-message/index.json`、`miniprogram/pages/subscribe-message/index.wxml`、`miniprogram/pages/subscribe-message/index.wxss`、`miniprogram/pages/web-view/index.js`、`miniprogram/pages/web-view/index.json`、`miniprogram/pages/web-view/index.wxml`、`miniprogram/pages/web-view/index.wxss`、`miniprogram/pages/wechat-pay/index.js`、`miniprogram/pages/wechat-pay/index.json`、`miniprogram/pages/wechat-pay/index.wxml`、`miniprogram/pages/wechat-pay/index.wxss`;移动源码根为 `apps/mobile-shell/src/env.d.ts`;移动桥接层为 `apps/mobile-shell/src/host-bridge/bridge.ts`、`apps/mobile-shell/src/host-bridge/capabilities.ts`、`apps/mobile-shell/src/host-bridge/clipboard.ts`、`apps/mobile-shell/src/host-bridge/dispatch.ts`、`apps/mobile-shell/src/host-bridge/files.ts`、`apps/mobile-shell/src/host-bridge/haptics.ts`、`apps/mobile-shell/src/host-bridge/notifications.ts`、`apps/mobile-shell/src/host-bridge/protocol.ts`、`apps/mobile-shell/src/host-bridge/scanner.ts`、`apps/mobile-shell/src/host-bridge/share.ts`;移动 shell 层为 `apps/mobile-shell/src/shell/QrScannerOverlay.tsx`、`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/deepLink.ts`、`apps/mobile-shell/src/shell/lifecycle.ts`、`apps/mobile-shell/src/shell/loadFailure.ts`、`apps/mobile-shell/src/shell/navigation.ts`、`apps/mobile-shell/src/shell/network.ts`、`apps/mobile-shell/src/shell/runtime.ts`、`apps/mobile-shell/src/shell/safeArea.ts`、`apps/mobile-shell/src/shell/url.ts`、`apps/mobile-shell/src/shell/webViewGlobals.d.ts`、`apps/mobile-shell/src/shell/webViewHistory.ts`、`apps/mobile-shell/src/shell/webViewPolicy.ts`;桌面入口为 `apps/desktop-shell/src-tauri/src/app.rs`、`apps/desktop-shell/src-tauri/src/main.rs`;桌面桥接层为 `apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/files.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/share.rs`;桌面 shell 层为 `apps/desktop-shell/src-tauri/src/shell/deep_link.rs`、`apps/desktop-shell/src-tauri/src/shell/events.rs`、`apps/desktop-shell/src-tauri/src/shell/file_drop.rs`、`apps/desktop-shell/src-tauri/src/shell/lifecycle.rs`、`apps/desktop-shell/src-tauri/src/shell/menu.rs`、`apps/desktop-shell/src-tauri/src/shell/mod.rs`、`apps/desktop-shell/src-tauri/src/shell/navigation.rs`、`apps/desktop-shell/src-tauri/src/shell/network.rs`、`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`apps/desktop-shell/src-tauri/src/shell/tray.rs`、`apps/desktop-shell/src-tauri/src/shell/url.rs`、`apps/desktop-shell/src-tauri/src/shell/webview.rs`、`apps/desktop-shell/src-tauri/src/shell/window_state.rs`。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。 +结构门禁按完整相对路径反查文档和目录:微信桥接层为 `miniprogram/host-bridge/dispatch.js`、`miniprogram/host-bridge/payment.js`、`miniprogram/host-bridge/protocol.js`、`miniprogram/host-bridge/shareGrid.js`、`miniprogram/host-bridge/subscribeMessage.js`、`miniprogram/host-bridge/webView.js`;微信 shell 层为 `miniprogram/shell/payment.js`、`miniprogram/shell/shareGrid.js`、`miniprogram/shell/subscribeMessage.js`、`miniprogram/shell/webView.js`;微信页面包装层为 `miniprogram/pages/share-grid/index.js`、`miniprogram/pages/share-grid/index.json`、`miniprogram/pages/share-grid/index.wxml`、`miniprogram/pages/share-grid/index.wxss`、`miniprogram/pages/subscribe-message/index.js`、`miniprogram/pages/subscribe-message/index.json`、`miniprogram/pages/subscribe-message/index.wxml`、`miniprogram/pages/subscribe-message/index.wxss`、`miniprogram/pages/web-view/index.js`、`miniprogram/pages/web-view/index.json`、`miniprogram/pages/web-view/index.wxml`、`miniprogram/pages/web-view/index.wxss`、`miniprogram/pages/wechat-pay/index.js`、`miniprogram/pages/wechat-pay/index.json`、`miniprogram/pages/wechat-pay/index.wxml`、`miniprogram/pages/wechat-pay/index.wxss`;移动源码根为 `apps/mobile-shell/src/env.d.ts`;移动桥接层为 `apps/mobile-shell/src/host-bridge/bridge.ts`、`apps/mobile-shell/src/host-bridge/capabilities.ts`、`apps/mobile-shell/src/host-bridge/clipboard.ts`、`apps/mobile-shell/src/host-bridge/dispatch.ts`、`apps/mobile-shell/src/host-bridge/files.ts`、`apps/mobile-shell/src/host-bridge/haptics.ts`、`apps/mobile-shell/src/host-bridge/notifications.ts`、`apps/mobile-shell/src/host-bridge/protocol.ts`、`apps/mobile-shell/src/host-bridge/scanner.ts`、`apps/mobile-shell/src/host-bridge/share.ts`;移动 shell 层为 `apps/mobile-shell/src/shell/QrScannerOverlay.tsx`、`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/deepLink.ts`、`apps/mobile-shell/src/shell/lifecycle.ts`、`apps/mobile-shell/src/shell/loadFailure.ts`、`apps/mobile-shell/src/shell/navigation.ts`、`apps/mobile-shell/src/shell/network.ts`、`apps/mobile-shell/src/shell/runtime.ts`、`apps/mobile-shell/src/shell/safeArea.ts`、`apps/mobile-shell/src/shell/url.ts`、`apps/mobile-shell/src/shell/webViewGlobals.d.ts`、`apps/mobile-shell/src/shell/webViewHistory.ts`、`apps/mobile-shell/src/shell/webViewPolicy.ts`;桌面入口为 `apps/desktop-shell/src-tauri/src/app.rs`、`apps/desktop-shell/src-tauri/src/main.rs`;桌面桥接层为 `apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/files.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/share.rs`;桌面 shell 层为 `apps/desktop-shell/src-tauri/src/shell/deep_link.rs`、`apps/desktop-shell/src-tauri/src/shell/events.rs`、`apps/desktop-shell/src-tauri/src/shell/file_drop.rs`、`apps/desktop-shell/src-tauri/src/shell/lifecycle.rs`、`apps/desktop-shell/src-tauri/src/shell/menu.rs`、`apps/desktop-shell/src-tauri/src/shell/mod.rs`、`apps/desktop-shell/src-tauri/src/shell/navigation.rs`、`apps/desktop-shell/src-tauri/src/shell/network.rs`、`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`apps/desktop-shell/src-tauri/src/shell/tray.rs`、`apps/desktop-shell/src-tauri/src/shell/url.rs`、`apps/desktop-shell/src-tauri/src/shell/webview.rs`、`apps/desktop-shell/src-tauri/src/shell/window_state.rs`。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。 ## HostBridge 消息协议 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index 7dd71f422..f3ffc9686 100644 --- a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md +++ b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md @@ -37,11 +37,11 @@ AI H5 sandbox -> parent HostBridge adapter ``` -桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`;`miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` / `scanner.ts` 分别承接文件、分享和扫码能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区、扫码 overlay 和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` 分别承接文件和分享能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面,`apps/desktop-shell/src-tauri/src/app.rs` 承接 Tauri builder / plugin / window 装配,`main.rs` 只保留薄入口并调用 `app::run()`。`npm run check:native-shells` 会检查这些目录清单。 +桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 Expo / Tauri 的 request 总线:`miniprogram/host-bridge/protocol.js` 只沉淀微信壳能力、页面 URL、结果 hash / storage key 和分享消息类型等常量,`dispatch.js` 只作为 `protocol`、`webView`、`payment`、`shareGrid`、`subscribeMessage` 的薄索引,真实协议归一、支付 / 订阅 / 分享结果编解码仍分别放在 `webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js`;`miniprogram/shell/webView.js`、`payment.js`、`shareGrid.js`、`subscribeMessage.js` 承接 Page 生命周期、`wx.*` 容器调用、WebView 容器行为、支付页和订阅页装配,页面目录只保留 `Page(createWechat...Page())` 装配。Expo 移动壳使用 `apps/mobile-shell/src/host-bridge/protocol.ts` 承接 envelope、request 校验、ok / failure 响应和 replay 基础类型,`capabilities.ts` 只引用共享 HostBridge capability profile 并选择 iOS 差异能力,`dispatch.ts` 承接 method 分发和宿主能力调用,`files.ts` / `share.ts` / `scanner.ts` 分别承接文件、分享和扫码能力,`bridge.ts` 只作为 WebView message 入口、request id replay 编排和对外 facade;`apps/mobile-shell/App.tsx` 只装配 `apps/mobile-shell/src/shell/ShellApp.tsx`,由 `apps/mobile-shell/src/shell/*.ts(x)` 承接 WebView 容器、URL、导航、网络、生命周期、安全区、扫码 overlay 和 WebView policy。Tauri 桌面壳使用 `apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs` 承接 envelope、method 白名单、request 校验和 replay 状态,`capabilities.rs` 承接共享桌面 capability profile 的 Rust 运行时镜像,`dispatch.rs` 承接 method 分发和宿主能力调用,`files.rs` / `share.rs` / `notifications.rs` 分别承接文件、分享和本地通知能力,`mod.rs` 只保留模块声明、必要 re-export、`host_bridge_request` command facade 和 replay 编排;`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`url.rs`、`navigation.rs`、`network.rs`、`lifecycle.rs`、`file_drop.rs`、`events.rs`、`deep_link.rs`、`tray.rs`、`menu.rs`、`window_state.rs` 和 `webview.rs` 分别承接运行态、入口 URL、导航 / 下载、网络、生命周期、拖拽图片、HostBridge 事件注入、深链、托盘、应用菜单、窗口状态持久化和 WebView 门面,`apps/desktop-shell/src-tauri/src/app.rs` 承接 Tauri builder / plugin / window 装配,`main.rs` 只保留薄入口并调用 `app::run()`。`npm run check:native-shells` 会检查这些目录清单。 -当前 `npm run check:native-shells` 锁定的生产文件清单为:微信桥接层 `dispatch.js`、`payment.js`、`protocol.js`、`shareGrid.js`、`subscribeMessage.js`、`webView.js`;微信 shell 层 `payment.js`、`shareGrid.js`、`subscribeMessage.js`、`webView.js`;微信页面包装层 `share-grid/index.js`、`share-grid/index.json`、`share-grid/index.wxml`、`share-grid/index.wxss`、`subscribe-message/index.js`、`subscribe-message/index.json`、`subscribe-message/index.wxml`、`subscribe-message/index.wxss`、`web-view/index.js`、`web-view/index.json`、`web-view/index.wxml`、`web-view/index.wxss`、`wechat-pay/index.js`、`wechat-pay/index.json`、`wechat-pay/index.wxml`、`wechat-pay/index.wxss`;移动源码根 `env.d.ts`;移动桥接层 `bridge.ts`、`capabilities.ts`、`clipboard.ts`、`dispatch.ts`、`files.ts`、`haptics.ts`、`protocol.ts`、`scanner.ts`、`share.ts`;移动 shell 层 `QrScannerOverlay.tsx`、`ShellApp.tsx`、`deepLink.ts`、`lifecycle.ts`、`loadFailure.ts`、`navigation.ts`、`network.ts`、`runtime.ts`、`safeArea.ts`、`url.ts`、`webViewGlobals.d.ts`、`webViewHistory.ts`、`webViewPolicy.ts`;桌面入口 `app.rs`、`main.rs`;桌面桥接层 `capabilities.rs`、`clipboard.rs`、`dispatch.rs`、`files.rs`、`mod.rs`、`protocol.rs`、`share.rs`;桌面 shell 层 `deep_link.rs`、`events.rs`、`file_drop.rs`、`lifecycle.rs`、`menu.rs`、`mod.rs`、`navigation.rs`、`network.rs`、`runtime.rs`、`tray.rs`、`url.rs`、`webview.rs`、`window_state.rs`。 +当前 `npm run check:native-shells` 锁定的生产文件清单为:微信桥接层 `dispatch.js`、`payment.js`、`protocol.js`、`shareGrid.js`、`subscribeMessage.js`、`webView.js`;微信 shell 层 `payment.js`、`shareGrid.js`、`subscribeMessage.js`、`webView.js`;微信页面包装层 `share-grid/index.js`、`share-grid/index.json`、`share-grid/index.wxml`、`share-grid/index.wxss`、`subscribe-message/index.js`、`subscribe-message/index.json`、`subscribe-message/index.wxml`、`subscribe-message/index.wxss`、`web-view/index.js`、`web-view/index.json`、`web-view/index.wxml`、`web-view/index.wxss`、`wechat-pay/index.js`、`wechat-pay/index.json`、`wechat-pay/index.wxml`、`wechat-pay/index.wxss`;移动源码根 `env.d.ts`;移动桥接层 `bridge.ts`、`capabilities.ts`、`clipboard.ts`、`dispatch.ts`、`files.ts`、`haptics.ts`、`notifications.ts`、`protocol.ts`、`scanner.ts`、`share.ts`;移动 shell 层 `QrScannerOverlay.tsx`、`ShellApp.tsx`、`deepLink.ts`、`lifecycle.ts`、`loadFailure.ts`、`navigation.ts`、`network.ts`、`runtime.ts`、`safeArea.ts`、`url.ts`、`webViewGlobals.d.ts`、`webViewHistory.ts`、`webViewPolicy.ts`;桌面入口 `app.rs`、`main.rs`;桌面桥接层 `capabilities.rs`、`clipboard.rs`、`dispatch.rs`、`files.rs`、`mod.rs`、`notifications.rs`、`protocol.rs`、`share.rs`;桌面 shell 层 `deep_link.rs`、`events.rs`、`file_drop.rs`、`lifecycle.rs`、`menu.rs`、`mod.rs`、`navigation.rs`、`network.rs`、`runtime.rs`、`tray.rs`、`url.rs`、`webview.rs`、`window_state.rs`。 -结构门禁按完整相对路径反查文档和目录:微信桥接层为 `miniprogram/host-bridge/dispatch.js`、`miniprogram/host-bridge/payment.js`、`miniprogram/host-bridge/protocol.js`、`miniprogram/host-bridge/shareGrid.js`、`miniprogram/host-bridge/subscribeMessage.js`、`miniprogram/host-bridge/webView.js`;微信 shell 层为 `miniprogram/shell/payment.js`、`miniprogram/shell/shareGrid.js`、`miniprogram/shell/subscribeMessage.js`、`miniprogram/shell/webView.js`;微信页面包装层为 `miniprogram/pages/share-grid/index.js`、`miniprogram/pages/share-grid/index.json`、`miniprogram/pages/share-grid/index.wxml`、`miniprogram/pages/share-grid/index.wxss`、`miniprogram/pages/subscribe-message/index.js`、`miniprogram/pages/subscribe-message/index.json`、`miniprogram/pages/subscribe-message/index.wxml`、`miniprogram/pages/subscribe-message/index.wxss`、`miniprogram/pages/web-view/index.js`、`miniprogram/pages/web-view/index.json`、`miniprogram/pages/web-view/index.wxml`、`miniprogram/pages/web-view/index.wxss`、`miniprogram/pages/wechat-pay/index.js`、`miniprogram/pages/wechat-pay/index.json`、`miniprogram/pages/wechat-pay/index.wxml`、`miniprogram/pages/wechat-pay/index.wxss`;移动源码根为 `apps/mobile-shell/src/env.d.ts`;移动桥接层为 `apps/mobile-shell/src/host-bridge/bridge.ts`、`apps/mobile-shell/src/host-bridge/capabilities.ts`、`apps/mobile-shell/src/host-bridge/clipboard.ts`、`apps/mobile-shell/src/host-bridge/dispatch.ts`、`apps/mobile-shell/src/host-bridge/files.ts`、`apps/mobile-shell/src/host-bridge/haptics.ts`、`apps/mobile-shell/src/host-bridge/notifications.ts`、`apps/mobile-shell/src/host-bridge/protocol.ts`、`apps/mobile-shell/src/host-bridge/scanner.ts`、`apps/mobile-shell/src/host-bridge/share.ts`;移动 shell 层为 `apps/mobile-shell/src/shell/QrScannerOverlay.tsx`、`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/deepLink.ts`、`apps/mobile-shell/src/shell/lifecycle.ts`、`apps/mobile-shell/src/shell/loadFailure.ts`、`apps/mobile-shell/src/shell/navigation.ts`、`apps/mobile-shell/src/shell/network.ts`、`apps/mobile-shell/src/shell/runtime.ts`、`apps/mobile-shell/src/shell/safeArea.ts`、`apps/mobile-shell/src/shell/url.ts`、`apps/mobile-shell/src/shell/webViewGlobals.d.ts`、`apps/mobile-shell/src/shell/webViewHistory.ts`、`apps/mobile-shell/src/shell/webViewPolicy.ts`;桌面入口为 `apps/desktop-shell/src-tauri/src/app.rs`、`apps/desktop-shell/src-tauri/src/main.rs`;桌面桥接层为 `apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/files.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/share.rs`;桌面 shell 层为 `apps/desktop-shell/src-tauri/src/shell/deep_link.rs`、`apps/desktop-shell/src-tauri/src/shell/events.rs`、`apps/desktop-shell/src-tauri/src/shell/file_drop.rs`、`apps/desktop-shell/src-tauri/src/shell/lifecycle.rs`、`apps/desktop-shell/src-tauri/src/shell/menu.rs`、`apps/desktop-shell/src-tauri/src/shell/mod.rs`、`apps/desktop-shell/src-tauri/src/shell/navigation.rs`、`apps/desktop-shell/src-tauri/src/shell/network.rs`、`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`apps/desktop-shell/src-tauri/src/shell/tray.rs`、`apps/desktop-shell/src-tauri/src/shell/url.rs`、`apps/desktop-shell/src-tauri/src/shell/webview.rs`、`apps/desktop-shell/src-tauri/src/shell/window_state.rs`。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。 +结构门禁按完整相对路径反查文档和目录:微信桥接层为 `miniprogram/host-bridge/dispatch.js`、`miniprogram/host-bridge/payment.js`、`miniprogram/host-bridge/protocol.js`、`miniprogram/host-bridge/shareGrid.js`、`miniprogram/host-bridge/subscribeMessage.js`、`miniprogram/host-bridge/webView.js`;微信 shell 层为 `miniprogram/shell/payment.js`、`miniprogram/shell/shareGrid.js`、`miniprogram/shell/subscribeMessage.js`、`miniprogram/shell/webView.js`;微信页面包装层为 `miniprogram/pages/share-grid/index.js`、`miniprogram/pages/share-grid/index.json`、`miniprogram/pages/share-grid/index.wxml`、`miniprogram/pages/share-grid/index.wxss`、`miniprogram/pages/subscribe-message/index.js`、`miniprogram/pages/subscribe-message/index.json`、`miniprogram/pages/subscribe-message/index.wxml`、`miniprogram/pages/subscribe-message/index.wxss`、`miniprogram/pages/web-view/index.js`、`miniprogram/pages/web-view/index.json`、`miniprogram/pages/web-view/index.wxml`、`miniprogram/pages/web-view/index.wxss`、`miniprogram/pages/wechat-pay/index.js`、`miniprogram/pages/wechat-pay/index.json`、`miniprogram/pages/wechat-pay/index.wxml`、`miniprogram/pages/wechat-pay/index.wxss`;移动源码根为 `apps/mobile-shell/src/env.d.ts`;移动桥接层为 `apps/mobile-shell/src/host-bridge/bridge.ts`、`apps/mobile-shell/src/host-bridge/capabilities.ts`、`apps/mobile-shell/src/host-bridge/clipboard.ts`、`apps/mobile-shell/src/host-bridge/dispatch.ts`、`apps/mobile-shell/src/host-bridge/files.ts`、`apps/mobile-shell/src/host-bridge/haptics.ts`、`apps/mobile-shell/src/host-bridge/notifications.ts`、`apps/mobile-shell/src/host-bridge/protocol.ts`、`apps/mobile-shell/src/host-bridge/scanner.ts`、`apps/mobile-shell/src/host-bridge/share.ts`;移动 shell 层为 `apps/mobile-shell/src/shell/QrScannerOverlay.tsx`、`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/deepLink.ts`、`apps/mobile-shell/src/shell/lifecycle.ts`、`apps/mobile-shell/src/shell/loadFailure.ts`、`apps/mobile-shell/src/shell/navigation.ts`、`apps/mobile-shell/src/shell/network.ts`、`apps/mobile-shell/src/shell/runtime.ts`、`apps/mobile-shell/src/shell/safeArea.ts`、`apps/mobile-shell/src/shell/url.ts`、`apps/mobile-shell/src/shell/webViewGlobals.d.ts`、`apps/mobile-shell/src/shell/webViewHistory.ts`、`apps/mobile-shell/src/shell/webViewPolicy.ts`;桌面入口为 `apps/desktop-shell/src-tauri/src/app.rs`、`apps/desktop-shell/src-tauri/src/main.rs`;桌面桥接层为 `apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/files.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/mod.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/protocol.rs`、`apps/desktop-shell/src-tauri/src/host_bridge/share.rs`;桌面 shell 层为 `apps/desktop-shell/src-tauri/src/shell/deep_link.rs`、`apps/desktop-shell/src-tauri/src/shell/events.rs`、`apps/desktop-shell/src-tauri/src/shell/file_drop.rs`、`apps/desktop-shell/src-tauri/src/shell/lifecycle.rs`、`apps/desktop-shell/src-tauri/src/shell/menu.rs`、`apps/desktop-shell/src-tauri/src/shell/mod.rs`、`apps/desktop-shell/src-tauri/src/shell/navigation.rs`、`apps/desktop-shell/src-tauri/src/shell/network.rs`、`apps/desktop-shell/src-tauri/src/shell/runtime.rs`、`apps/desktop-shell/src-tauri/src/shell/tray.rs`、`apps/desktop-shell/src-tauri/src/shell/url.rs`、`apps/desktop-shell/src-tauri/src/shell/webview.rs`、`apps/desktop-shell/src-tauri/src/shell/window_state.rs`。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。 Tauri 桌面壳启动时必须按 `label="main"` 解析 `tauri.conf.json` 主窗口配置,并在创建 WebView 前补写 `native_app`、`tauri_desktop` 和真实 capability 上下文;缺少主窗口配置时启动直接失败,不允许按 `windows[0]` 兜底或无主窗口静默运行。 diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index bc5c58c5b..ec3f461f0 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -297,6 +297,7 @@ const expectedDesktopHostBridgeRustFiles = [ 'dispatch.rs', 'files.rs', 'mod.rs', + 'notifications.rs', 'protocol.rs', 'share.rs', ];