diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 8e94b96ed..705f665ac 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -46,6 +46,14 @@ const app = fs.readFileSync(appPath, 'utf8'); const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url); const main = fs.readFileSync(mainPath, 'utf8'); const rustSourceDir = new URL('../src-tauri/src/', import.meta.url); +const desktopHostBridgeBadgePath = new URL( + '../src-tauri/src/host_bridge/badge.rs', + import.meta.url, +); +const desktopHostBridgeBadgeSource = fs.readFileSync( + desktopHostBridgeBadgePath, + 'utf8', +); const desktopHostBridgeCapabilitiesPath = new URL( '../src-tauri/src/host_bridge/capabilities.rs', import.meta.url, @@ -193,6 +201,7 @@ const expectedDesktopRustRootEntries = [ 'file:main.rs', ]; const expectedDesktopHostBridgeRustFiles = [ + 'badge.rs', 'capabilities.rs', 'clipboard.rs', 'dispatch.rs', @@ -1283,7 +1292,7 @@ const desktopHostBridgePayloadLimits = { 'DESKTOP_NETWORK_CHECK_TIMEOUT_MS', ), HOST_BRIDGE_BADGE_COUNT_MAX: extractRustNumberConst( - rustHostSource, + desktopHostBridgeBadgeSource, 'BADGE_COUNT_MAX', ), HOST_BRIDGE_APP_TITLE_MAX_LENGTH: extractRustNumberConst( @@ -1803,6 +1812,7 @@ const expectedRustRootEntries = [ 'file:main.rs', ]; const expectedRustHostBridgeFiles = [ + 'badge.rs', 'capabilities.rs', 'clipboard.rs', 'dispatch.rs', @@ -2048,6 +2058,28 @@ assertSameList( if (nativeAppHostBridgeSource.includes("'host_bridge_request'")) { throw new Error('H5 native app HostBridge must use HOST_BRIDGE_TAURI_COMMAND'); } +if ( + !desktopHostBridgeDispatchSource.includes( + 'set_desktop_app_badge_count(&app, &request)', + ) || + desktopHostBridgeDispatchSource.includes('set_badge_count') || + desktopHostBridgeDispatchSource.includes('BADGE_COUNT_MAX') +) { + throw new Error('desktop shell badge HostBridge method must delegate to badge module'); +} +for (const snippet of [ + 'BADGE_COUNT_MAX', + 'fn badge_count_payload', + 'set_desktop_app_badge_count', + '"count must be an integer between 0 and 99999"', +]) { + if (!desktopHostBridgeBadgeSource.includes(snippet)) { + throw new Error(`desktop shell badge module is missing ${snippet}`); + } +} +if (!desktopHostBridgeBadgeSource.match(/window\s*\.\s*set_badge_count\s*\(\s*count\s*\)/)) { + throw new Error('desktop shell badge module must call window.set_badge_count(count)'); +} if ( !desktopHostBridgeDispatchSource.includes( 'show_desktop_local_notification(&app, &request)', diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/badge.rs b/apps/desktop-shell/src-tauri/src/host_bridge/badge.rs new file mode 100644 index 000000000..867a8dfac --- /dev/null +++ b/apps/desktop-shell/src-tauri/src/host_bridge/badge.rs @@ -0,0 +1,78 @@ +use crate::host_bridge::protocol::{failed, HostBridgeRequest, HostBridgeResponse}; +use serde_json::Value; +use tauri::Manager; + +const BADGE_COUNT_MAX: i64 = 99999; + +fn badge_count_payload(request: &HostBridgeRequest) -> Result, HostBridgeResponse> { + let count = request + .payload + .as_ref() + .and_then(|value| value.get("count")) + .and_then(Value::as_i64) + .ok_or_else(|| { + failed( + request.id.clone(), + "invalid_request", + "count must be an integer between 0 and 99999", + ) + })?; + + if !(0..=BADGE_COUNT_MAX).contains(&count) { + return Err(failed( + request.id.clone(), + "invalid_request", + "count must be an integer between 0 and 99999", + )); + } + + Ok(if count == 0 { None } else { Some(count) }) +} + +pub(crate) fn set_desktop_app_badge_count( + app: &tauri::AppHandle, + request: &HostBridgeRequest, +) -> Result<(), HostBridgeResponse> { + let count = badge_count_payload(request)?; + let Some(window) = app.get_webview_window("main") else { + return Err(failed(request.id.clone(), "host_error", "main window not found")); + }; + + window + .set_badge_count(count) + .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 badge_count_payload_accepts_clear_and_positive_counts() { + let mut clear = request("app.setBadgeCount"); + clear.payload = Some(json!({ "count": 0 })); + assert_eq!(badge_count_payload(&clear).expect("clear badge"), None); + + let mut count = request("app.setBadgeCount"); + count.payload = Some(json!({ "count": 12 })); + assert_eq!(badge_count_payload(&count).expect("badge count"), Some(12)); + } + + #[test] + fn badge_count_payload_rejects_invalid_counts() { + for count in [json!(-1), json!(1.5), json!(100000), json!("1")] { + let mut invalid = request("app.setBadgeCount"); + invalid.payload = Some(json!({ "count": count })); + + let response = badge_count_payload(&invalid).expect_err("invalid count"); + let error = response.error.expect("error"); + assert_eq!(error.code, "invalid_request"); + assert_eq!( + error.message, + "count must be an integer between 0 and 99999" + ); + } + } +} 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 6a56c3914..134479200 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs @@ -1,3 +1,4 @@ +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::files::{ @@ -15,11 +16,10 @@ use crate::shell::webview::{ color_scheme_from_theme, desktop_platform, normalize_external_url, normalize_native_page_url, open_normalized_desktop_external_url, resolve_desktop_network_status, }; -use serde_json::{json, Value}; +use serde_json::json; use tauri::Manager; use tauri_plugin_dialog::DialogExt; -const BADGE_COUNT_MAX: i64 = 99999; const WINDOW_TITLE_MAX_LENGTH: usize = 80; fn normalize_window_title(raw_title: &str) -> Option { @@ -31,31 +31,6 @@ fn normalize_window_title(raw_title: &str) -> Option { Some(title.chars().take(WINDOW_TITLE_MAX_LENGTH).collect()) } -fn badge_count_payload(request: &HostBridgeRequest) -> Result, HostBridgeResponse> { - let count = request - .payload - .as_ref() - .and_then(|value| value.get("count")) - .and_then(Value::as_i64) - .ok_or_else(|| { - failed( - request.id.clone(), - "invalid_request", - "count must be an integer between 0 and 99999", - ) - })?; - - if !(0..=BADGE_COUNT_MAX).contains(&count) { - return Err(failed( - request.id.clone(), - "invalid_request", - "count must be an integer between 0 and 99999", - )); - } - - Ok(if count == 0 { None } else { Some(count) }) -} - fn desktop_runtime() -> HostBridgeRuntime { HostBridgeRuntime { shell: "tauri_desktop", @@ -385,20 +360,10 @@ pub(super) async fn execute_host_bridge_request( None => failed(request.id, "host_error", "main window not found"), } } - "app.setBadgeCount" => { - let count = match badge_count_payload(&request) { - Ok(count) => count, - Err(response) => return response, - }; - - match app.get_webview_window("main") { - Some(window) => match window.set_badge_count(count) { - Ok(()) => ok(request.id, json!(true)), - Err(error) => failed(request.id, "host_error", error.to_string()), - }, - None => failed(request.id, "host_error", "main window not found"), - } - } + "app.setBadgeCount" => match set_desktop_app_badge_count(&app, &request) { + Ok(()) => ok(request.id, json!(true)), + Err(response) => response, + }, "network.status" => { let network_status = tauri::async_runtime::spawn_blocking(resolve_desktop_network_status).await; @@ -582,30 +547,4 @@ mod tests { ); } - #[test] - fn badge_count_payload_accepts_clear_and_positive_counts() { - let mut clear = request("app.setBadgeCount"); - clear.payload = Some(json!({ "count": 0 })); - assert_eq!(badge_count_payload(&clear).expect("clear badge"), None); - - let mut count = request("app.setBadgeCount"); - count.payload = Some(json!({ "count": 12 })); - assert_eq!(badge_count_payload(&count).expect("badge count"), Some(12)); - } - - #[test] - fn badge_count_payload_rejects_invalid_counts() { - for count in [json!(-1), json!(1.5), json!(100000), json!("1")] { - let mut invalid = request("app.setBadgeCount"); - invalid.payload = Some(json!({ "count": count })); - - let response = badge_count_payload(&invalid).expect_err("invalid count"); - let error = response.error.expect("error"); - assert_eq!(error.code, "invalid_request"); - assert_eq!( - error.message, - "count must be an integer between 0 and 99999" - ); - } - } } 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 069d7e250..4cfc53072 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/mod.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod capabilities; +mod badge; mod clipboard; mod dispatch; pub(crate) mod files; diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 6f1750dbb..556178b39 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -12,6 +12,8 @@ const qrScannerOverlayPath = new URL('../src/shell/QrScannerOverlay.tsx', import const qrScannerOverlaySource = fs.readFileSync(qrScannerOverlayPath, 'utf8'); const bridgePath = new URL('../src/host-bridge/bridge.ts', import.meta.url); const bridgeSource = fs.readFileSync(bridgePath, 'utf8'); +const badgePath = new URL('../src/host-bridge/badge.ts', import.meta.url); +const badgeSource = fs.readFileSync(badgePath, 'utf8'); const clipboardPath = new URL('../src/host-bridge/clipboard.ts', import.meta.url); const clipboardSource = fs.readFileSync(clipboardPath, 'utf8'); const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url); @@ -75,6 +77,7 @@ const productionSourceRoots = [ const productionFileExtensions = new Set(['.json', '.mjs', '.ts', '.tsx']); const requiredMobileShellSourceModules = [ 'env.d.ts', + 'host-bridge/badge.ts', 'host-bridge/bridge.ts', 'host-bridge/capabilities.ts', 'host-bridge/clipboard.ts', @@ -1813,6 +1816,24 @@ if (!iosMobileCapabilitySet.has('app.setBadgeCount')) { if (mobileCapabilitySet.has('app.setBadgeCount')) { throw new Error('Android mobile shell base capabilities must not include app.setBadgeCount'); } +if ( + !dispatchSource.includes('setMobileAppBadgeCount(request.payload)') || + dispatchSource.includes('PushNotificationIOS') || + dispatchSource.includes('normalizeHostBridgeBadgeCount') || + dispatchSource.includes('HOST_BRIDGE_BADGE_COUNT_MAX') +) { + throw new Error('mobile shell badge HostBridge method must delegate to badge module'); +} +for (const snippet of [ + 'HOST_BRIDGE_BADGE_COUNT_MAX', + 'normalizeHostBridgeBadgeCount', + 'PushNotificationIOS.setApplicationIconBadgeNumber(count)', + 'app badge count is only supported on iOS mobile shell', +]) { + if (!badgeSource.includes(snippet)) { + throw new Error(`mobile shell badge module is missing ${snippet}`); + } +} if (!dispatchSource.includes('Appearance.getColorScheme()')) { throw new Error('mobile shell HostBridge must read the native color scheme'); diff --git a/apps/mobile-shell/src/host-bridge/badge.ts b/apps/mobile-shell/src/host-bridge/badge.ts new file mode 100644 index 000000000..4f5965471 --- /dev/null +++ b/apps/mobile-shell/src/host-bridge/badge.ts @@ -0,0 +1,30 @@ +import { Platform, PushNotificationIOS } from 'react-native'; + +import { + HOST_BRIDGE_BADGE_COUNT_MAX, + type HostBridgeError, + normalizeHostBridgeBadgeCount, + type SetBadgeCountPayload, +} from '../../../../packages/shared/src/contracts/hostBridge'; +import { invalidRequest } from './protocol'; + +export function setMobileAppBadgeCount(payload: unknown) { + if (Platform.OS !== 'ios') { + throw { + code: 'unsupported_capability', + message: 'app badge count is only supported on iOS mobile shell', + } satisfies HostBridgeError; + } + + const count = normalizeHostBridgeBadgeCount( + (payload as SetBadgeCountPayload | undefined)?.count, + ); + if (count === null) { + throw invalidRequest( + `count must be an integer between 0 and ${HOST_BRIDGE_BADGE_COUNT_MAX}`, + ); + } + + PushNotificationIOS.setApplicationIconBadgeNumber(count); + return true; +} diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts index c70e119dc..d58417043 100644 --- a/apps/mobile-shell/src/host-bridge/dispatch.ts +++ b/apps/mobile-shell/src/host-bridge/dispatch.ts @@ -1,24 +1,17 @@ import * as Linking from 'expo-linking'; -import { - Appearance, - Platform, - PushNotificationIOS, -} from 'react-native'; +import { Appearance, Platform } from 'react-native'; import { type ClipboardWriteTextPayload, type HapticsImpactPayload, - HOST_BRIDGE_BADGE_COUNT_MAX, HOST_BRIDGE_VERSION, type HostBridgeError, type HostBridgeRequest, type NavigateNativePagePayload, - normalizeHostBridgeBadgeCount, normalizeHostBridgeColorScheme, normalizeHostBridgeExternalUrlPayload, normalizeHostBridgeLocalNotification, type OpenExternalUrlPayload, - type SetBadgeCountPayload, } from '../../../../packages/shared/src/contracts/hostBridge'; import { openMobileShellExternalNavigation, @@ -53,6 +46,7 @@ import { writeMobileClipboardText, } from './clipboard'; import { runMobileHapticsImpact } from './haptics'; +import { setMobileAppBadgeCount } from './badge'; let currentShareTarget: unknown = null; let navigation: MobileHostBridgeNavigation | null = null; @@ -105,27 +99,6 @@ async function runHaptics(payload: unknown) { return true; } -function setBadgeCount(payload: unknown) { - if (Platform.OS !== 'ios') { - throw { - code: 'unsupported_capability', - message: 'app badge count is only supported on iOS mobile shell', - } satisfies HostBridgeError; - } - - const count = normalizeHostBridgeBadgeCount( - (payload as SetBadgeCountPayload | undefined)?.count, - ); - if (count === null) { - throw invalidRequest( - `count must be an integer between 0 and ${HOST_BRIDGE_BADGE_COUNT_MAX}`, - ); - } - - PushNotificationIOS.setApplicationIconBadgeNumber(count); - return true; -} - async function showLocalNotification(payload: unknown) { const notification = normalizeHostBridgeLocalNotification(payload); if (!notification) { @@ -230,7 +203,7 @@ export async function dispatchMobileHostBridgeRequest( case 'notification.showLocal': return ok(request, await showLocalNotification(request.payload)); case 'app.setBadgeCount': - return ok(request, setBadgeCount(request.payload)); + return ok(request, setMobileAppBadgeCount(request.payload)); case 'share.open': return ok(request, await openShare(request.payload, currentShareTarget)); case 'share.setTarget': diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d5c0a20cd..3e4f0333f 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -45,6 +45,7 @@ - 2026-06-18 分享卡图片导出:新增 `file.exportImage` HostBridge capability,H5 分享卡下载在 native app 中优先把 canvas 生成的 base64 图片交给宿主导出;Expo 壳写缓存图片后交给系统分享 / 保存面板,Tauri 壳通过系统保存对话框写入图片字节。该能力只接受 `image/png` / `image/jpeg` / `image/webp`、单次 5 MiB 内图片数据,成功只返回文件名和字节数,不暴露本机绝对路径;宿主未声明时保留浏览器下载。 - 2026-06-18 应用角标能力:新增 `app.setBadgeCount` HostBridge capability,H5 只传 `0` 到共享契约 `HOST_BRIDGE_BADGE_COUNT_MAX` 之间的整数并在宿主未声明时静默 fallback;Expo 壳只在 iOS 声明并通过 React Native `PushNotificationIOS` 设置应用图标角标,Android 不声明、不伪造成功;Tauri 壳通过主窗口 `set_badge_count` 设置任务栏角标,底层平台不支持时返回真实错误。 - 2026-06-18 草稿生成未读角标:平台壳层把“可见作品架里未读的草稿生成完成更新”同步到 `app.setBadgeCount`;同一草稿的 work/profile/session 等多个恢复 ID 只计 1,已读、失败、生成中和不可见草稿不计入。该角标只消费已有 HostBridge 能力,宿主不支持或设置失败不影响 H5 红点、作品架或后端状态。 +- 2026-06-19 原生壳角标边界:Expo `app.setBadgeCount` 的 iOS 平台判定、共享上限校验和 `PushNotificationIOS.setApplicationIconBadgeNumber(...)` 调用统一收口在 `apps/mobile-shell/src/host-bridge/badge.ts`;Tauri `app.setBadgeCount` 的 payload 校验、清除语义和主窗口 `set_badge_count` 调用统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/badge.rs`。两端 `dispatch` 只负责委托对应 badge 模块和映射响应,配置检查会拒绝分发层直接导入角标底层 API 或重声明数量边界。 - 2026-06-18 宿主外观只读查询:新增 `appearance.getColorScheme` HostBridge capability,Expo 壳通过 React Native `Appearance.getColorScheme()` 读取系统配色,Tauri 壳通过主窗口 `theme()` 读取窗口主题;该能力只返回 `light` / `dark` / `unknown`,不设置 H5 主题、不覆盖系统主题,也不作为强制 UI 样式入口。 - 2026-06-18 原生壳生命周期事件:新增 `app.lifecycle` HostBridge capability,Expo 壳通过 React Native `AppState` 派发 `active` / `inactive` / `background`,Tauri 壳通过主窗口 focus / blur、托盘隐藏 / 恢复和页面加载重放派发统一状态;桌面隐藏到托盘或最小化都归一为 `background`,`hidden`、`minimized`、`focused`、`blurred` 只进入 `nativeState` 便于排障,不扩展共享 `state`。两端都声明 `host.events` 表示事件通过 HostBridge message 注入,但不把它作为 request method,也不开放 Tauri event 插件或 React Native 私有事件 API。H5 只通过 `subscribeHostAppLifecycle()` 订阅统一状态,后续游戏循环、音频和轮询暂停 / 恢复不得直接依赖 Expo / Tauri 平台细节。 - 2026-06-18 原生壳网络状态:新增 `network.status` 与 `network.statusChanged` HostBridge capability,Expo 壳通过 `expo-network` 查询和订阅真实系统网络状态,Tauri 壳从 `WEB_APP_ORIGIN` 解析主站 host / port 后做短超时 TCP 可达性查询,并通过 WebView `online` / `offline` 注入变化事件;H5 统一使用 `getHostNetworkStatus()` / `subscribeHostNetworkStatusChange()`,不得直接读取 Expo / Tauri 私有网络 API。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index baebe20ad..7a5c94cf3 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` / `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 分发和宿主能力调用,`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 状态,`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` 锁定的生产文件清单为:微信桥接层 `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`。 +当前 `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`;移动桥接层 `badge.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`;桌面桥接层 `badge.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/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`。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。 +结构门禁按完整相对路径反查文档和目录:微信桥接层为 `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/badge.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/badge.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 消息协议 @@ -366,7 +366,7 @@ GameBridge 禁止: 2026-06-18 追加:移动壳安装包身份固定为 `world.genarrative.mobile`。Expo `app.json` 中的 `ios.bundleIdentifier` 与 `android.package` 使用同一包标识,应用版本为 `0.1.0`,iOS `buildNumber` 从字符串 `"1"` 起步,Android `versionCode` 从整数 `1` 起步;后续每次生成可分发安装包时只递增构建号 / versionCode,产品版本号按发布节奏单独调整。`apps/mobile-shell/scripts/check-config.mjs` 会校验这些字段与 `package.json` 版本一致,避免 iOS、Android 和 H5 HostBridge `hostVersion` 发生静默漂移;`npm run mobile-shell:config` 会调用真实 Expo CLI 解析 public managed config,确认最终 Expo 配置仍保留同一包身份、深链、安全字段、插件权限和 HostBridge 版本。当前仍不写入假商店上架信息、假更新端点或占位渠道 SDK 配置。 -2026-06-19 追加:移动壳 H5 入口 query 和 `host.getRuntime` 回包统一读取 `MOBILE_SHELL_HOST_VERSION`,该值由移动壳 `app.json` 的 Expo `version` 配置解析,异常配置只回退到与 `app.json` / `package.json` 一致的受检 fallback。配置检查会拒绝在 `apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/host-bridge/bridge.ts` 或 `runtime.ts` 内重新散落硬编码版本,避免升级移动安装包时 H5 首屏上下文和宿主 runtime 回读版本不一致。该收口不引入 `expo-constants`、OTA 更新、渠道分发或应用安装信息业务。 +2026-06-19 追加:移动壳 H5 入口 query 和 `host.getRuntime` 回包统一读取 `MOBILE_SHELL_HOST_VERSION`,该值由移动壳 `app.json` 的 Expo `version` 配置解析,异常配置只回退到与 `app.json` / `package.json` 一致的受检 fallback。配置检查会拒绝在 `apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/host-bridge/badge.ts`、`apps/mobile-shell/src/host-bridge/bridge.ts` 或 `runtime.ts` 内重新散落硬编码版本,避免升级移动安装包时 H5 首屏上下文和宿主 runtime 回读版本不一致。该收口不引入 `expo-constants`、OTA 更新、渠道分发或应用安装信息业务。 2026-06-18 追加:移动壳默认显式关闭 Expo OTA 更新,直到存在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程后再接入。`app.json` 只允许 `updates.enabled=false`,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖;`apps/mobile-shell/scripts/check-config.mjs` 和 Expo public config smoke 会共同拒绝这些发布通道能力被提前打开,移动壳生产入口、HostBridge 和 URL/runtime 配置也不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates。由于移动壳运行依赖会从根安装树解析,根 H5 `package.json` 也不得直接安装这些移动端发布通道、崩溃上报、analytics 或 CodePush SDK;根 `package-lock.json` 也不得解析 `expo-updates`、Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 等真实发布 / 观测 SDK。`expo-application` 可能由 Expo 自身传递解析,但项目不得把它作为 direct dependency 主动用于渠道逻辑。 diff --git a/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md b/docs/【前端架构】宿主壳能力统一协议-2026-06-17.md index f3ffc9686..91e2aacd2 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` / `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 分发和宿主能力调用,`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 状态,`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` 会检查这些目录清单。 -当前 `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`。 +当前 `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`;移动桥接层 `badge.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`;桌面桥接层 `badge.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/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`。这些目录不得新增未登记子目录或生产入口;移动端和桌面端单端配置检查同样会拒绝未登记生产模块。 +结构门禁按完整相对路径反查文档和目录:微信桥接层为 `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/badge.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/badge.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 ec3f461f0..77f4cad9c 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -249,6 +249,7 @@ const expectedWechatPageFilesByRoute = { 'wechat-pay': ['index.js', 'index.json', 'index.wxml', 'index.wxss'], }; const expectedMobileHostBridgeFiles = [ + 'badge.ts', 'bridge.test.ts', 'bridge.ts', 'capabilities.ts', @@ -292,6 +293,7 @@ const expectedMobileShellFiles = [ 'webViewPolicy.ts', ]; const expectedDesktopHostBridgeRustFiles = [ + 'badge.rs', 'capabilities.rs', 'clipboard.rs', 'dispatch.rs',