diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 70f907ab9..555b66072 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -1719,6 +1719,30 @@ for (const snippet of [ } } +if ( + !desktopHostBridgeDispatchSource.includes( + 'write_desktop_host_bridge_clipboard_text(&app, &request)', + ) || + !desktopHostBridgeDispatchSource.includes( + 'read_desktop_host_bridge_clipboard_text(&app, &request)', + ) || + desktopHostBridgeDispatchSource.includes('required_string_payload(&request, "text")') || + desktopHostBridgeDispatchSource.includes('read_desktop_clipboard_text(&app)') +) { + throw new Error('desktop shell clipboard HostBridge methods must delegate to clipboard module'); +} +for (const snippet of [ + 'write_desktop_clipboard_text(app, text)', + 'read_desktop_clipboard_text(app)', + 'normalize_clipboard_text(&text)', + 'required_string_payload(request, "text")', + '"text": text', +]) { + if (!desktopHostBridgeClipboardSource.includes(snippet)) { + throw new Error(`desktop shell clipboard module is missing ${snippet}`); + } +} + if (config.build?.frontendDist !== '../../../dist') { throw new Error('desktop shell must package the root H5 dist'); } @@ -1986,9 +2010,8 @@ const requiredRustHostSnippets = [ '"file.exportAudio"', '"file.imageDropped"', '"notification.showLocal"', - 'write_desktop_clipboard_text(&app, text)', - 'read_desktop_clipboard_text(&app)', - 'normalize_clipboard_text(&text)', + 'write_desktop_host_bridge_clipboard_text(&app, &request)', + 'read_desktop_host_bridge_clipboard_text(&app, &request)', 'tauri_plugin_dialog::init()', 'tauri_plugin_notification::init()', 'tauri_plugin_notification::{NotificationExt, PermissionState}', 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 621d5c477..e7569416d 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs @@ -1,5 +1,10 @@ use tauri_plugin_clipboard_manager::ClipboardExt; +use crate::host_bridge::protocol::{ + failed, ok, required_string_payload, HostBridgeRequest, HostBridgeResponse, +}; +use serde_json::json; + pub(crate) const CLIPBOARD_TEXT_MAX_LENGTH: usize = 100000; pub(crate) fn normalize_clipboard_text(text: &str) -> String { @@ -22,6 +27,36 @@ pub(crate) fn read_desktop_clipboard_text(app: &tauri::AppHandle) -> Result HostBridgeResponse { + let text = match required_string_payload(request, "text") { + Ok(text) => text, + Err(response) => return response, + }; + + match write_desktop_clipboard_text(app, text) { + Ok(()) => ok(request.id.clone(), json!(true)), + Err(error) => failed(request.id.clone(), "host_error", error), + } +} + +pub(crate) fn read_desktop_host_bridge_clipboard_text( + app: &tauri::AppHandle, + request: &HostBridgeRequest, +) -> HostBridgeResponse { + match read_desktop_clipboard_text(app) { + Ok(text) => ok( + request.id.clone(), + json!({ + "text": text, + }), + ), + Err(error) => failed(request.id.clone(), "host_error", error), + } +} + #[cfg(test)] mod tests { use super::*; @@ -34,4 +69,22 @@ mod tests { CLIPBOARD_TEXT_MAX_LENGTH ); } + + #[test] + fn clipboard_read_response_reports_contract_shape() { + let response = ok( + "clipboard-read".to_string(), + json!({ + "text": normalize_clipboard_text("邀请码 GEN-1") + }), + ); + + assert!(response.ok); + assert_eq!( + response.result.expect("clipboard result"), + json!({ + "text": "邀请码 GEN-1" + }) + ); + } } 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 4352e9564..f9019e99c 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs @@ -1,7 +1,10 @@ use crate::host_bridge::appearance::desktop_appearance_color_scheme; use crate::host_bridge::badge::set_desktop_app_badge_count; use crate::host_bridge::capabilities::capabilities; -use crate::host_bridge::clipboard::{read_desktop_clipboard_text, write_desktop_clipboard_text}; +use crate::host_bridge::clipboard::{ + read_desktop_host_bridge_clipboard_text, write_desktop_clipboard_text, + write_desktop_host_bridge_clipboard_text, +}; use crate::host_bridge::files::{ export_audio_payload, export_image_payload, export_text_payload, import_audio_file_payload, import_document_file_payload, import_image_file_payload, import_text_file_payload, @@ -68,32 +71,12 @@ pub(super) async fn execute_host_bridge_request( } match request.method.as_str() { - "app.openExternalUrl" => { - open_desktop_host_bridge_external_url(&app, &request) - } + "app.openExternalUrl" => open_desktop_host_bridge_external_url(&app, &request), "appearance.getColorScheme" => desktop_appearance_color_scheme(&app, &request), "navigation.openNativePage" => open_desktop_host_bridge_native_page(&app, &request), "app.reloadWebView" => reload_desktop_host_bridge_webview(&app, &request), - "clipboard.writeText" => { - let text = match required_string_payload(&request, "text") { - Ok(text) => text, - Err(response) => return response, - }; - - match write_desktop_clipboard_text(&app, text) { - Ok(()) => ok(request.id, json!(true)), - Err(error) => failed(request.id, "host_error", error), - } - } - "clipboard.readText" => match read_desktop_clipboard_text(&app) { - Ok(text) => ok( - request.id, - json!({ - "text": text, - }), - ), - Err(error) => failed(request.id, "host_error", error), - }, + "clipboard.writeText" => write_desktop_host_bridge_clipboard_text(&app, &request), + "clipboard.readText" => read_desktop_host_bridge_clipboard_text(&app, &request), "file.exportText" => { let (file_name, content) = match export_text_payload(&request) { Ok(payload) => payload, @@ -311,12 +294,10 @@ pub(super) async fn execute_host_bridge_request( Ok(()) => ok(request.id, json!(true)), Err(response) => response, }, - "network.status" => { - match resolve_desktop_host_bridge_network_status().await { - Ok(status) => ok(request.id, status), - Err(error) => failed(request.id, "host_error", error.to_string()), - } - } + "network.status" => match resolve_desktop_host_bridge_network_status().await { + Ok(status) => ok(request.id, status), + 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, @@ -491,5 +472,4 @@ mod tests { 80 ); } - } diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 9ad78803a..60f14a15b 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1666,13 +1666,25 @@ if ( dispatchSource.includes("from 'expo-clipboard'") || dispatchSource.includes('Clipboard.setStringAsync') || dispatchSource.includes('Clipboard.getStringAsync') || - !dispatchSource.includes('writeMobileClipboardText(') || - !dispatchSource.includes('readMobileClipboardText()') + dispatchSource.includes('ClipboardWriteTextPayload') || + !dispatchSource.includes('writeMobileHostBridgeClipboardText(request.payload)') || + !dispatchSource.includes('readMobileHostBridgeClipboardText()') ) { throw new Error( 'mobile shell dispatch must delegate clipboard IO to clipboard.ts', ); } +for (const snippet of [ + 'writeMobileHostBridgeClipboardText', + 'type ClipboardWriteTextPayload', + 'writeMobileClipboardText(text)', + 'readMobileHostBridgeClipboardText', + 'readMobileClipboardText()', +]) { + if (!clipboardSource.includes(snippet)) { + throw new Error(`mobile shell clipboard module is missing ${snippet}`); + } +} if ( !hapticsSource.includes('normalizeHostBridgeHapticsImpactStyle(rawStyle)') || diff --git a/apps/mobile-shell/src/host-bridge/clipboard.ts b/apps/mobile-shell/src/host-bridge/clipboard.ts index d28ea7aff..0599f9186 100644 --- a/apps/mobile-shell/src/host-bridge/clipboard.ts +++ b/apps/mobile-shell/src/host-bridge/clipboard.ts @@ -2,9 +2,11 @@ import * as Clipboard from 'expo-clipboard'; import { type ClipboardReadTextResult, + type ClipboardWriteTextPayload, type HostBridgeError, normalizeHostBridgeClipboardText, } from '../../../../packages/shared/src/contracts/hostBridge'; +import { invalidRequest } from './protocol'; export async function writeMobileClipboardText(rawText: unknown) { const clipboardText = normalizeHostBridgeClipboardText(rawText); @@ -16,6 +18,19 @@ export async function writeMobileClipboardText(rawText: unknown) { return true; } +export async function writeMobileHostBridgeClipboardText(payload: unknown) { + const text = (payload as ClipboardWriteTextPayload | undefined)?.text; + if (!(await writeMobileClipboardText(text))) { + throw invalidRequest('text is required'); + } + + return true; +} + +export function readMobileHostBridgeClipboardText() { + return readMobileClipboardText(); +} + export async function readMobileClipboardText(): Promise { const result = normalizeHostBridgeClipboardText( await Clipboard.getStringAsync(), diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts index ed74fcebe..545d49d74 100644 --- a/apps/mobile-shell/src/host-bridge/dispatch.ts +++ b/apps/mobile-shell/src/host-bridge/dispatch.ts @@ -2,7 +2,6 @@ import * as Linking from 'expo-linking'; import { Platform } from 'react-native'; import { - type ClipboardWriteTextPayload, type HapticsImpactPayload, HOST_BRIDGE_VERSION, type HostBridgeRequest, @@ -31,8 +30,8 @@ import { resetQrScannerForTest, scanQrCode } from './scanner'; import { openShare } from './share'; import { showMobileLocalNotification } from './notifications'; import { - readMobileClipboardText, - writeMobileClipboardText, + readMobileHostBridgeClipboardText, + writeMobileHostBridgeClipboardText, } from './clipboard'; import { runMobileHapticsImpact } from './haptics'; import { setMobileAppBadgeCount } from './badge'; @@ -53,18 +52,6 @@ export function configureMobileHostBridgeNavigation( navigation = nextNavigation; } -async function writeClipboard(payload: unknown) { - if ( - !(await writeMobileClipboardText( - (payload as ClipboardWriteTextPayload | undefined)?.text, - )) - ) { - throw invalidRequest('text is required'); - } - - return true; -} - async function runHaptics(payload: unknown) { if ( !(await runMobileHapticsImpact( @@ -119,9 +106,9 @@ export async function dispatchMobileHostBridgeRequest( case 'network.status': return ok(request, await getMobileHostBridgeNetworkStatus()); case 'clipboard.writeText': - return ok(request, await writeClipboard(request.payload)); + return ok(request, await writeMobileHostBridgeClipboardText(request.payload)); case 'clipboard.readText': - return ok(request, await readMobileClipboardText()); + return ok(request, await readMobileHostBridgeClipboardText()); case 'file.exportText': return ok(request, await exportTextFile(request.payload)); case 'file.importText': diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d21b447b2..d93b4aa6b 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -83,8 +83,8 @@ - 2026-06-18 草稿生成完成 / 失败通知:平台壳层的 `markDraftReady` / `markDraftFailed` 统一收口会在原生壳声明 `notification.showLocal` 时请求即时本地通知;通知 payload 只包含生成完成 / 失败标题和草稿来源正文,按草稿来源去重,同一草稿重新进入生成中后才允许再次通知。该能力不替代现有完成 / 错误弹窗、作品架红点、队列概览或后端状态回读,通知失败不阻断主流程。根级原生壳门禁必须覆盖平台壳同步层通过真实 HostBridge transport 发出 `notification.showLocal`,避免只测模型文案。 - 2026-06-19 草稿生成 HostBridge 消费门禁:`PlatformEntryFlowShellImpl` 只负责派生草稿通知和未读数量,实际宿主同步经 `platformHostBridgeSync.ts` 调用 `showHostLocalNotification` / `setHostAppBadgeCount`;`npm run check:native-shells` 必须运行该同步层的真实 Tauri transport 测试,并继续覆盖通知模型、未读计数模型、音频导入和文档导入等 H5 HostBridge 消费测试。 - 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 移动壳剪贴板边界:Expo `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写、共享 100000 字符归一、payload 校验和响应边界统一收口在 `apps/mobile-shell/src/host-bridge/clipboard.ts`;`dispatch.ts` 只负责把 HostBridge request 委托给 `writeMobileHostBridgeClipboardText(...)` / `readMobileHostBridgeClipboardText()`,不得直接导入 `expo-clipboard` 或调用 `Clipboard.setStringAsync` / `Clipboard.getStringAsync`。移动壳配置检查会覆盖该模块结构、共享文本边界和 dispatch 委托关系,避免剪贴板能力散落到分发层。 +- 2026-06-19 桌面壳剪贴板边界:Tauri `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写、共享 100000 字符归一、payload 校验和响应边界统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`;`dispatch.rs` 只负责把 HostBridge request 委托给 `write_desktop_host_bridge_clipboard_text(...)` / `read_desktop_host_bridge_clipboard_text(...)`,不得直接承接剪贴板文本截断、payload 解析或插件读写细节;桌面 share fallback 仍可调用底层写剪贴板函数复制分享文本。桌面壳配置检查会覆盖该模块结构、共享文本边界和 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` 时继续使用文本导入兜底。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index e18652635..fa9f28c1d 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -64,7 +64,7 @@ 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 分发和宿主能力调用,`appearance.ts` 承接系统配色读取,`navigation.ts` 承接外链打开、受控 H5 跳转和 WebView 刷新,`network.ts` 承接网络状态查询,`badge.ts` 承接受控角标能力,`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 状态,`appearance.rs` 承接窗口主题读取和 HostBridge 配色归一,`navigation.rs` 承接外链打开、受控 H5 跳转和主窗口刷新,`network.rs` 承接网络状态查询,`badge.rs` 承接受控任务栏角标能力,`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()`。 +三端宿主桥接层按职责对齐命名:微信小程序页面路由仍保留在 `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 分发和宿主能力调用,`appearance.ts` 承接系统配色读取,`navigation.ts` 承接外链打开、受控 H5 跳转和 WebView 刷新,`network.ts` 承接网络状态查询,`badge.ts` 承接受控角标能力,`clipboard.ts` 承接剪贴板读写与 HostBridge payload / 响应边界,`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 状态,`appearance.rs` 承接窗口主题读取和 HostBridge 配色归一,`navigation.rs` 承接外链打开、受控 H5 跳转和主窗口刷新,`network.rs` 承接网络状态查询,`badge.rs` 承接受控任务栏角标能力,`clipboard.rs` 承接剪贴板读写与 HostBridge payload / 响应边界,`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`;移动桥接层 `appearance.ts`、`badge.ts`、`bridge.ts`、`capabilities.ts`、`clipboard.ts`、`dispatch.ts`、`files.ts`、`haptics.ts`、`navigation.ts`、`network.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`;桌面桥接层 `appearance.rs`、`badge.rs`、`capabilities.rs`、`clipboard.rs`、`dispatch.rs`、`files.rs`、`mod.rs`、`navigation.rs`、`network.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`。 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index 78d5cec3d..8241a51fb 100644 --- a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md +++ b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md @@ -37,7 +37,7 @@ 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 分发和宿主能力调用,`appearance.ts` 承接系统配色读取,`navigation.ts` 承接外链打开、受控 H5 跳转和 WebView 刷新,`network.ts` 承接网络状态查询,`badge.ts` 承接受控角标能力,`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 状态,`appearance.rs` 承接窗口主题读取和 HostBridge 配色归一,`navigation.rs` 承接外链打开、受控 H5 跳转和主窗口刷新,`network.rs` 承接网络状态查询,`badge.rs` 承接受控任务栏角标能力,`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` 会检查这些目录清单。 +桥接层文件结构按宿主统一为“协议 / 能力清单 / 分发 / 宿主容器行为”四类职责。微信小程序不硬套 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 分发和宿主能力调用,`appearance.ts` 承接系统配色读取,`navigation.ts` 承接外链打开、受控 H5 跳转和 WebView 刷新,`network.ts` 承接网络状态查询,`badge.ts` 承接受控角标能力,`clipboard.ts` 承接剪贴板读写与 HostBridge payload / 响应边界,`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 状态,`appearance.rs` 承接窗口主题读取和 HostBridge 配色归一,`navigation.rs` 承接外链打开、受控 H5 跳转和主窗口刷新,`network.rs` 承接网络状态查询,`badge.rs` 承接受控任务栏角标能力,`clipboard.rs` 承接剪贴板读写与 HostBridge payload / 响应边界,`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`;移动桥接层 `appearance.ts`、`badge.ts`、`bridge.ts`、`capabilities.ts`、`clipboard.ts`、`dispatch.ts`、`files.ts`、`haptics.ts`、`navigation.ts`、`network.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`;桌面桥接层 `appearance.rs`、`badge.rs`、`capabilities.rs`、`clipboard.rs`、`dispatch.rs`、`files.rs`、`mod.rs`、`navigation.rs`、`network.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`。