From 33e56fb56fcfdf2b106cb9c5ae662e961b4805f9 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 17:49:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E4=BA=8B=E4=BB=B6=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面壳移除未由 Rust 真实驱动的 network.statusChanged 能力 桌面网络状态保留 Rust 主站可达性主动查询 门禁禁止桌面网络事件回退到 WebView online 状态 文档和项目记忆同步桌面网络能力边界 --- apps/desktop-shell/scripts/check-config.mjs | 16 ++++-- apps/desktop-shell/src-tauri/src/app.rs | 3 +- .../src-tauri/src/host_bridge/capabilities.rs | 3 -- .../src-tauri/src/host_bridge/runtime.rs | 4 -- .../src-tauri/src/shell/lifecycle.rs | 5 -- .../src-tauri/src/shell/network.rs | 53 +------------------ .../src-tauri/src/shell/webview.rs | 4 +- .../shared-memory/decision-log.md | 10 ++-- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 10 ++-- ...前端架构】宿主壳能力统一协议-2026-06-17.md | 2 +- packages/shared/src/contracts/hostBridge.ts | 1 - scripts/check-native-shells.mjs | 14 +++-- 12 files changed, 38 insertions(+), 87 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 60aa3f5e1..f7b564c5f 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -1640,6 +1640,18 @@ for (const expectedNetworkSnippet of [ if (desktopShellNetworkSource.includes('"app.genarrative.world"')) { throw new Error('desktop shell network probe must not duplicate the public web host'); } +for (const blockedDesktopNetworkEventSnippet of [ + 'register_desktop_network_events', + 'navigator.onLine', + 'window.addEventListener(\'online\'', + 'window.addEventListener(\'offline\'', +]) { + if (desktopShellNetworkSource.includes(blockedDesktopNetworkEventSnippet)) { + throw new Error( + `desktop shell must not declare network.statusChanged until Rust owns the event source: ${blockedDesktopNetworkEventSnippet}`, + ); + } +} for (const expectedShareSnippet of [ 'normalize_public_share_url', @@ -2339,7 +2351,6 @@ const requiredRustHostSnippets = [ '"host.events"', '"app.lifecycle"', '"network.status"', - '"network.statusChanged"', '"share.open"', '"share.setTarget"', '"navigation.openNativePage"', @@ -2432,7 +2443,6 @@ const requiredRustHostSnippets = [ 'log_desktop_network_probe_failure', 'desktop network reachability probe failed', 'desktop_network_reachability_reports_missing_probe_target', - 'network.statusChanged', 'register_desktop_navigation_events', 'desktop_navigation_state_script', 'navigation.canGoBack', @@ -2976,7 +2986,6 @@ for (const appSetupSnippet of [ 'WebviewWindowBuilder::from_config', 'desktop_new_window_external_url(&url)', 'desktop_new_window_response', - 'register_desktop_network_events(&window)?', 'register_desktop_navigation_events(&window)?', 'log_desktop_host_event_result(', ]) { @@ -2989,6 +2998,7 @@ for (const blockedAppSetupSnippet of [ 'app.config().app.windows.get(0)', 'app.config().app.windows[0]', 'if let Some(config) = window_config', + 'register_desktop_network_events(&window)', 'let _ = register_desktop_network_events(&window)', 'let _ = register_desktop_navigation_events(&window)', 'let _ = emit_current_desktop_lifecycle_event(&window)', diff --git a/apps/desktop-shell/src-tauri/src/app.rs b/apps/desktop-shell/src-tauri/src/app.rs index 1902a576e..5f9184e5b 100644 --- a/apps/desktop-shell/src-tauri/src/app.rs +++ b/apps/desktop-shell/src-tauri/src/app.rs @@ -13,7 +13,7 @@ use crate::shell::webview::{ open_desktop_external_navigation, open_normalized_desktop_external_url, register_desktop_file_drop_events, register_desktop_lifecycle_events, register_desktop_navigation_events, - register_desktop_network_events, replay_desktop_webview_state, + replay_desktop_webview_state, should_allow_desktop_webview_download, should_allow_desktop_webview_navigation, should_replay_desktop_webview_state_on_page_load, }; @@ -110,7 +110,6 @@ pub(crate) fn run() { "app.lifecycle", emit_current_desktop_lifecycle_event(&window), ); - register_desktop_network_events(&window)?; register_desktop_navigation_events(&window)?; register_desktop_file_drop_events(&window); register_desktop_deep_link_events(app)?; diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs b/apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs index 8ba0969b5..aed005368 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs @@ -13,7 +13,6 @@ pub(crate) fn capabilities() -> Vec<&'static str> { "app.setTitle", "app.setBadgeCount", "network.status", - "network.statusChanged", "clipboard.writeText", "clipboard.readText", "file.exportText", @@ -52,7 +51,6 @@ mod tests { "app.setTitle", "app.setBadgeCount", "network.status", - "network.statusChanged", "clipboard.writeText", "clipboard.readText", "file.exportText", @@ -90,7 +88,6 @@ mod tests { "app.setTitle", "app.setBadgeCount", "network.status", - "network.statusChanged", "clipboard.writeText", "clipboard.readText", "file.exportText", diff --git a/apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs b/apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs index b85c1be4b..a04d7ad1b 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs @@ -49,10 +49,6 @@ mod tests { .as_array() .unwrap() .contains(&json!("network.status"))); - assert!(result["capabilities"] - .as_array() - .unwrap() - .contains(&json!("network.statusChanged"))); assert!(result["capabilities"] .as_array() .unwrap() diff --git a/apps/desktop-shell/src-tauri/src/shell/lifecycle.rs b/apps/desktop-shell/src-tauri/src/shell/lifecycle.rs index 4ad884ade..0075311cc 100644 --- a/apps/desktop-shell/src-tauri/src/shell/lifecycle.rs +++ b/apps/desktop-shell/src-tauri/src/shell/lifecycle.rs @@ -1,6 +1,5 @@ use crate::shell::events::host_bridge_event_script; use crate::shell::navigation::register_desktop_navigation_events; -use crate::shell::network::register_desktop_network_events; use serde_json::json; use tauri::webview::PageLoadEvent; use tauri::{WebviewWindow, WindowEvent}; @@ -138,10 +137,6 @@ pub(crate) fn replay_desktop_webview_state(window: &WebviewWindow) { "app.lifecycle", emit_current_desktop_lifecycle_event(window), ); - log_desktop_host_event_result( - "network.statusChanged", - register_desktop_network_events(window), - ); log_desktop_host_event_result( "navigation.canGoBack", register_desktop_navigation_events(window), diff --git a/apps/desktop-shell/src-tauri/src/shell/network.rs b/apps/desktop-shell/src-tauri/src/shell/network.rs index 09ee169f2..7d0c2c5dd 100644 --- a/apps/desktop-shell/src-tauri/src/shell/network.rs +++ b/apps/desktop-shell/src-tauri/src/shell/network.rs @@ -1,9 +1,8 @@ -use crate::shell::events::host_bridge_event_script; use crate::shell::url::WEB_APP_ORIGIN; use serde_json::{json, Value}; use std::net::{TcpStream, ToSocketAddrs}; use std::time::Duration; -use tauri::{Url, WebviewWindow}; +use tauri::Url; pub(crate) const DESKTOP_NETWORK_CHECK_TIMEOUT_MS: u64 = 1200; @@ -83,56 +82,6 @@ pub(crate) fn resolve_desktop_network_status() -> Value { desktop_network_status_payload(is_reachable) } -pub(crate) fn register_desktop_network_events(window: &WebviewWindow) -> tauri::Result<()> { - let online_script = host_bridge_event_script( - "network.statusChanged", - desktop_network_status_payload(true), - ) - .map_err(tauri::Error::Json)?; - let offline_script = host_bridge_event_script( - "network.statusChanged", - desktop_network_status_payload(false), - ) - .map_err(tauri::Error::Json)?; - let current_status_script = host_bridge_event_script( - "network.statusChanged", - json!({ - "isConnected": "__GENARRATIVE_DESKTOP_ONLINE__", - "isInternetReachable": "__GENARRATIVE_DESKTOP_ONLINE__", - "connectionType": "__GENARRATIVE_DESKTOP_CONNECTION_TYPE__", - "nativeType": "__GENARRATIVE_DESKTOP_NATIVE_TYPE__", - }), - ) - .map_err(tauri::Error::Json)? - .replace("\"__GENARRATIVE_DESKTOP_ONLINE__\"", "navigator.onLine") - .replace( - "\"__GENARRATIVE_DESKTOP_CONNECTION_TYPE__\"", - "(navigator.onLine ? 'unknown' : 'none')", - ) - .replace( - "\"__GENARRATIVE_DESKTOP_NATIVE_TYPE__\"", - "(navigator.onLine ? 'online' : 'offline')", - ); - - let script = format!( - "(() => {{ - if (window.__GENARRATIVE_DESKTOP_NETWORK_LISTENER_INSTALLED__) {{ - return true; - }} - window.__GENARRATIVE_DESKTOP_NETWORK_LISTENER_INSTALLED__ = true; - const emitOnline = () => {{ {} }}; - const emitOffline = () => {{ {} }}; - window.addEventListener('online', emitOnline); - window.addEventListener('offline', emitOffline); - {} - return true; - }})();", - online_script, offline_script, current_status_script - ); - - window.eval(script) -} - #[cfg(test)] mod tests { use super::*; diff --git a/apps/desktop-shell/src-tauri/src/shell/webview.rs b/apps/desktop-shell/src-tauri/src/shell/webview.rs index 6eaea895b..e92747041 100644 --- a/apps/desktop-shell/src-tauri/src/shell/webview.rs +++ b/apps/desktop-shell/src-tauri/src/shell/webview.rs @@ -10,9 +10,7 @@ pub(crate) use crate::shell::navigation::{ register_desktop_navigation_events, should_allow_desktop_webview_download, should_allow_desktop_webview_navigation, }; -pub(crate) use crate::shell::network::{ - register_desktop_network_events, resolve_desktop_network_status, -}; +pub(crate) use crate::shell::network::resolve_desktop_network_status; pub(crate) use crate::shell::runtime::{color_scheme_from_theme, desktop_platform}; pub(crate) use crate::shell::url::{ desktop_h5_url_with_host_context, desktop_window_config_with_runtime_platform, WEB_APP_ORIGIN, diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e3c4fbebf..b28c01f79 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -107,11 +107,11 @@ - 2026-06-18 宿主外观只读查询:新增 `appearance.getColorScheme` HostBridge capability,Expo 壳通过 React Native `Appearance.getColorScheme()` 读取系统配色,Tauri 壳通过主窗口 `theme()` 读取窗口主题;该能力只返回 `light` / `dark` / `unknown`,不设置 H5 主题、不覆盖系统主题,也不作为强制 UI 样式入口。 - 2026-06-19 原生壳外观查询边界:Expo `appearance.getColorScheme` 的系统配色读取、HostBridge 配色归一和成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/appearance.ts`;Tauri `appearance.getColorScheme` 的主窗口 `theme()` 读取、`light / dark / unknown` 映射和 HostBridge 响应包装统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs`。两端 `dispatch` 只负责委托对应 appearance 模块,配置检查会拒绝分发层直接读取系统配色、窗口主题或包装外观查询成功响应。 - 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。 +- 2026-06-18 原生壳网络状态:新增 `network.status` 与 `network.statusChanged` HostBridge capability,Expo 壳通过 `expo-network` 查询和订阅真实系统网络状态;Tauri 壳只声明 `network.status`,从 `WEB_APP_ORIGIN` 解析主站 host / port 后做短超时 TCP 可达性查询,暂不声明 `network.statusChanged`,避免把 WebView `online` / `offline` 当作桌面 Rust 网络事实。H5 统一使用 `getHostNetworkStatus()` / `subscribeHostNetworkStatusChange()`,不得直接读取 Expo / Tauri 私有网络 API。 - 2026-06-19 移动壳本地通知边界:Expo `notification.showLocal` 的 payload 归一、权限确认、iOS 仅 alert 且不请求 badge/sound、Android 固定 channel、即时调度、通知 handler 和成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/notifications.ts`;`dispatch.ts` 只负责把 HostBridge request 委托给 `showMobileHostBridgeLocalNotification(...)`,不得直接调用 `expo-notifications` 调度或权限 API,不得重新做通知 payload 归一,也不得包装本地通知成功响应。移动壳配置检查会覆盖该模块结构、固定 channel、即时调度形态和 dispatch 委托关系,避免后续混入远程推送、后台推送或散落的本地通知实现。 - 2026-06-18 移动壳 WebView 状态重放:Expo WebView 每次同源主站页面成功加载后都会补发当前 `app.lifecycle` 与 `network.statusChanged` 状态,覆盖首载、受控刷新、H5 刷新和系统回收 WebView 进程后的新 JS 上下文;补发不新增 HostBridge capability,也不向 `about:blank`、外域或错误页注入宿主状态。 - 2026-06-20 移动壳加载失败边界:Expo 壳 `onError` / `onHttpError` 只对同源 H5 主页面展示原生失败兜底层,外域、`about:blank`、危险协议、favicon 和非当前主页面资源失败不得触发兜底。移动壳配置检查会反查 `loadFailure.test.ts` 的同源、favicon 和当前主页面测试,避免错误页策略漂移。 -- 2026-06-18 桌面壳 WebView 状态重放:Tauri 主 WebView 每次页面加载完成后都会回放当前 `app.lifecycle` 并重新安装 `network.statusChanged` 监听脚本,覆盖托盘刷新、`app.reloadWebView` 和 H5 自刷新后的新 JS 上下文;桌面 runtime 同步声明 `host.events` 表示该真实事件通道可用,但仍不开放 Tauri event 插件或额外 command。 +- 2026-06-18 桌面壳 WebView 状态重放:Tauri 主 WebView 每次页面加载完成后都会回放当前 `app.lifecycle`,覆盖托盘刷新、`app.reloadWebView` 和 H5 自刷新后的新 JS 上下文;桌面 runtime 同步声明 `host.events` 表示生命周期、返回栈和拖拽图片事件通道可用,但桌面暂不声明 `network.statusChanged`,仍不开放 Tauri event 插件或额外 command。 - 2026-06-18 桌面壳 H5 返回栈事件:Tauri 壳开始声明 `navigation.canGoBack`,但只通过固定注入脚本追踪当前 H5 文档内的 `pushState` / `replaceState` / `popstate` 路由栈并派发 HostBridge event;不把该能力实现为 request method,不开放 H5 到 Tauri 的 event 写入通道,也不声明跨文档 native back-forward list 真相。 - 2026-06-18 移动壳 H5 返回栈事件:Expo 壳开始用固定 WebView 注入脚本追踪当前 H5 文档内的 `pushState` / `replaceState` / `popstate` 路由栈,并通过内部 `genarrative.mobile.historyState` 消息回传给壳层;壳层把该状态与 `react-native-webview` 原生 `canGoBack` 合成为 HostBridge `navigation.canGoBack` 事件。Android 返回键优先回退 H5 当前文档路由栈,H5 不可回退时才走 WebView 原生 `goBack()`;该内部消息不是 HostBridge request method,不开放通用 H5 -> 原生事件通道,外域 / 危险页面消息仍在进入 HostBridge 前丢弃。 - 2026-06-18 外部生成队列轮询接入宿主网络状态:H5 新增 `useHostNetworkOnline()`,宿主未声明网络能力时按在线处理以保持浏览器和旧壳行为;宿主明确 `isConnected=false` 或 `isInternetReachable=false` 时,平台外部生成队列概览暂停 HTTP 轮询,恢复在线后重新刷新。该能力只减少离线请求,不改变外部生成队列、作品架、弹窗或后端任务状态事实。 @@ -2530,7 +2530,7 @@ ## 2026-06-19 HostBridge event 白名单跨壳门禁 - 背景:HostBridge request method 已有共享白名单和跨壳检查,但宿主注入给 H5 的 event 名如果仍是裸字符串,AI sandbox 或壳层新增事件时可能绕过契约,导致 H5 订阅到共享协议外事件,或 Tauri Rust 镜像与 TypeScript 契约漂移。 -- 决策:`HOST_BRIDGE_EVENTS` 以 `packages/shared/src/contracts/hostBridge.ts` 为唯一事件名来源,当前只包含 `app.lifecycle`、`network.statusChanged`、`navigation.canGoBack` 和 `file.imageDropped`,且每个事件名必须同时是 HostBridge capability。Expo 移动壳事件注入函数必须使用共享 `HostBridgeEventName` 类型;Tauri 桌面壳 `shell/events.rs` 镜像同一事件清单并在脚本生成前拒绝未知事件;H5 `nativeAppHostBridge` 只分发 `isHostBridgeEventName()` 认可的事件。 +- 决策:`HOST_BRIDGE_EVENTS` 以 `packages/shared/src/contracts/hostBridge.ts` 为唯一事件名来源,当前只包含 `app.lifecycle`、`network.statusChanged`、`navigation.canGoBack` 和 `file.imageDropped`;事件名必须存在于 capability 白名单,各宿主壳只声明自身真实发射的事件 capability。Expo 移动壳事件注入函数必须使用共享 `HostBridgeEventName` 类型;Tauri 桌面壳 `shell/events.rs` 镜像同一事件清单并在脚本生成前拒绝未知事件;H5 `nativeAppHostBridge` 只分发 `isHostBridgeEventName()` 认可的事件。 - 影响范围:`packages/shared/src/contracts/hostBridge.ts`、`src/services/host-bridge/nativeAppHostBridge.ts`、`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/scripts/check-config.mjs`、`apps/desktop-shell/src-tauri/src/shell/events.rs`、`apps/desktop-shell/scripts/check-config.mjs`、`scripts/check-native-shells.mjs`、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run test -- packages/shared/src/contracts/hostBridge.test.ts src/services/host-bridge/nativeAppHostBridge.test.ts`、`npm run mobile-shell:typecheck`、`npm run desktop-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 @@ -2906,8 +2906,8 @@ ## 2026-06-20 桌面壳 HostBridge 事件失败不可静默 -- 背景:Tauri 桌面壳已经声明 `host.events`、`app.lifecycle`、`network.statusChanged`、`navigation.canGoBack` 和 `file.imageDropped`,H5 会据此订阅生命周期、网络、返回栈和拖拽图片事件;如果 WebView 事件脚本注册或发射失败仍被静默忽略,H5 会误以为宿主能力可用。 -- 决策:桌面壳启动阶段安装 `network.statusChanged` 和 `navigation.canGoBack` 脚本失败时直接阻断启动;生命周期首发、页面加载重放、窗口生命周期事件和拖拽图片事件阶段的 `app.lifecycle`、`network.statusChanged`、`navigation.canGoBack`、`file.imageDropped` 失败必须通过统一 helper 记录日志,不允许 `let _ = register_desktop_*`、`let _ = emit_current_*` 或 `let _ = emit_desktop_image_drop_event` 静默吞错。配置检查反查该错误处理路径。 +- 背景:Tauri 桌面壳已经声明 `host.events`、`app.lifecycle`、`navigation.canGoBack` 和 `file.imageDropped`,H5 会据此订阅生命周期、返回栈和拖拽图片事件;如果 WebView 事件脚本注册或发射失败仍被静默忽略,H5 会误以为宿主能力可用。桌面网络变化事件在 Rust 侧具备真实事件源前不声明 `network.statusChanged`。 +- 决策:桌面壳启动阶段安装 `navigation.canGoBack` 脚本失败时直接阻断启动;生命周期首发、页面加载重放、窗口生命周期事件和拖拽图片事件阶段的 `app.lifecycle`、`navigation.canGoBack`、`file.imageDropped` 失败必须通过统一 helper 记录日志,不允许 `let _ = register_desktop_*`、`let _ = emit_current_*` 或 `let _ = emit_desktop_image_drop_event` 静默吞错。配置检查反查该错误处理路径,并禁止桌面网络事件回退到 WebView `online` / `offline`。 - 影响范围:`apps/desktop-shell/src-tauri/src/app.rs`、`apps/desktop-shell/src-tauri/src/shell/lifecycle.rs`、`apps/desktop-shell/src-tauri/src/shell/file_drop.rs`、`apps/desktop-shell/src-tauri/src/shell/webview.rs`、`apps/desktop-shell/scripts/check-config.mjs`、宿主壳能力统一协议文档。 - 验证方式:`cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 62d88b6e8..31e3c65f5 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -146,7 +146,7 @@ type HostBridgeEvent = { | `app.setTitle` | 同步宿主窗口标题 | 不声明 | 支持 | | `app.setBadgeCount` | 设置应用 / 任务栏角标 | 仅 iOS 支持,Android 不声明 | 支持;平台底层不支持时返回错误 | | `network.status` | 查询宿主网络状态 | 支持 Expo Network | 支持主站可达性短超时查询 | -| `network.statusChanged` | 通知 H5 网络状态变化 | 支持 Expo Network 事件 | 支持 WebView online / offline 事件 | +| `network.statusChanged` | 通知 H5 网络状态变化 | 支持 Expo Network 事件 | 桌面暂不声明,避免把 WebView online / offline 当作 Rust 桌面网络事实 | | `clipboard.writeText` | 写剪贴板 | 支持 | 支持 | | `clipboard.readText` | 读取纯文本剪贴板 | 支持 | 支持 | | `file.exportText` | 导出文本到用户选择的本地文件 | 支持系统分享 / 保存面板 | 支持系统保存对话框 | @@ -391,9 +391,9 @@ GameBridge 禁止: - 实现 runtime、openExternalUrl、clipboard、share fallback、窗口标题同步。 - 验证 macOS / Windows / Linux 至少一条本地 smoke。 -当前状态:已新增 `apps/desktop-shell/`,Tauri dev 直接加载本地主站 Vite,release 打包根 `dist` 主站资产。Rust 侧只把 `host_bridge_request` command 授给主窗口,`appearance.getColorScheme` 由 Rust 内部读取主窗口 `theme()` 并返回 `light` / `dark` / `unknown`,不设置或覆盖系统主题;`app.lifecycle` 由主窗口 focus / blur、窗口 resize 后的状态读取、托盘隐藏 / 恢复和页面加载重放注入统一状态,不开放 Tauri event 插件给前端。桌面壳只向 H5 派发共享契约里的 `active` / `inactive` / `background`,隐藏到托盘和系统最小化都归一为 `background`,`hidden`、`minimized`、`focused`、`blurred` 只写入 `nativeState` 便于排障;H5 通过 `useHostLifecycleActive()` 统一归一窗口焦点和后台状态,WebAudio 背景音乐和拼图、抓大鹅等固定玩法 `