From 629d8f9974dff415559017da9d20fca5a13a0f1c Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 06:20:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E6=A1=8C=E9=9D=A2=E5=A3=B3?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E6=8E=A2=E6=B5=8B=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 桌面壳网络探测从 WEB_APP_ORIGIN 解析主站 host 和端口 桌面壳配置检查禁止 network.rs 重新写死公开主站域名 同步 HostBridge 协议文档、壳方案文档和共享决策记录 --- apps/desktop-shell/scripts/check-config.mjs | 25 ++++++++++++++ .../src-tauri/src/shell/network.rs | 34 +++++++++++++++++-- .../shared-memory/decision-log.md | 2 +- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 4 +-- ...前端架构】宿主壳能力统一协议-2026-06-17.md | 2 +- 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index c283f4f18..5963aae52 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -68,6 +68,14 @@ const desktopShellUrlPath = new URL( import.meta.url, ); const desktopShellUrlSource = fs.readFileSync(desktopShellUrlPath, 'utf8'); +const desktopShellNetworkPath = new URL( + '../src-tauri/src/shell/network.rs', + import.meta.url, +); +const desktopShellNetworkSource = fs.readFileSync( + desktopShellNetworkPath, + 'utf8', +); const productionSourceRoots = [ new URL('../package.json', import.meta.url), new URL('../src-tauri/Cargo.toml', import.meta.url), @@ -1103,6 +1111,23 @@ if (desktopPublicWebOrigin !== sharedPublicWebOrigin) { ); } +for (const expectedNetworkSnippet of [ + 'use crate::shell::url::WEB_APP_ORIGIN', + 'Url::parse(WEB_APP_ORIGIN)', + 'url.host_str()', + 'url.port_or_known_default()', +]) { + if (!desktopShellNetworkSource.includes(expectedNetworkSnippet)) { + throw new Error( + `desktop shell network probe must derive its target from WEB_APP_ORIGIN: missing ${expectedNetworkSnippet}`, + ); + } +} + +if (desktopShellNetworkSource.includes('"app.genarrative.world"')) { + throw new Error('desktop shell network probe must not duplicate the public web host'); +} + for (const expectedShareSnippet of [ 'normalize_public_share_url', 'raw_url.starts_with("//")', diff --git a/apps/desktop-shell/src-tauri/src/shell/network.rs b/apps/desktop-shell/src-tauri/src/shell/network.rs index fe89fdd6a..881de0655 100644 --- a/apps/desktop-shell/src-tauri/src/shell/network.rs +++ b/apps/desktop-shell/src-tauri/src/shell/network.rs @@ -1,11 +1,24 @@ 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::WebviewWindow; +use tauri::{Url, WebviewWindow}; const DESKTOP_NETWORK_CHECK_TIMEOUT_MS: u64 = 1200; +fn desktop_network_probe_target() -> Option<(String, u16)> { + let url = Url::parse(WEB_APP_ORIGIN).ok()?; + if url.scheme() != "https" { + return None; + } + + Some(( + url.host_str()?.to_string(), + url.port_or_known_default()?, + )) +} + pub(crate) fn desktop_network_status_payload(is_online: bool) -> Value { json!({ "isConnected": is_online, @@ -17,8 +30,8 @@ pub(crate) fn desktop_network_status_payload(is_online: bool) -> Value { pub(crate) fn resolve_desktop_network_status() -> Value { let timeout = Duration::from_millis(DESKTOP_NETWORK_CHECK_TIMEOUT_MS); - let is_reachable = ("app.genarrative.world", 443) - .to_socket_addrs() + let is_reachable = desktop_network_probe_target() + .and_then(|(host, port)| (host.as_str(), port).to_socket_addrs().ok()) .map(|addresses| { addresses.into_iter().any(|address| { TcpStream::connect_timeout(&address, timeout) @@ -89,6 +102,21 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn desktop_network_probe_target_uses_public_web_origin() { + let origin = Url::parse(WEB_APP_ORIGIN).expect("desktop web origin"); + + assert_eq!( + desktop_network_probe_target(), + Some(( + origin.host_str().expect("desktop web host").to_string(), + origin + .port_or_known_default() + .expect("desktop web default port") + )) + ); + } + #[test] fn desktop_network_status_payload_reports_reachability() { assert_eq!( diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c3dbfb7b7..3e19ac376 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -39,7 +39,7 @@ - 2026-06-18 草稿生成未读角标:平台壳层把“可见作品架里未读的草稿生成完成更新”同步到 `app.setBadgeCount`;同一草稿的 work/profile/session 等多个恢复 ID 只计 1,已读、失败、生成中和不可见草稿不计入。该角标只消费已有 HostBridge 能力,宿主不支持或设置失败不影响 H5 红点、作品架或后端状态。 - 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 壳通过短超时连接 `app.genarrative.world:443` 查询主站可达性,并通过 WebView `online` / `offline` 注入变化事件;H5 统一使用 `getHostNetworkStatus()` / `subscribeHostNetworkStatusChange()`,不得直接读取 Expo / Tauri 私有网络 API。 +- 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 移动壳 WebView 状态重放:Expo WebView 每次同源主站页面成功加载后都会补发当前 `app.lifecycle` 与 `network.statusChanged` 状态,覆盖首载、受控刷新、H5 刷新和系统回收 WebView 进程后的新 JS 上下文;补发不新增 HostBridge capability,也不向 `about:blank`、外域或错误页注入宿主状态。 - 2026-06-18 桌面壳 WebView 状态重放:Tauri 主 WebView 每次页面加载完成后都会回放当前 `app.lifecycle` 并重新安装 `network.statusChanged` 监听脚本,覆盖托盘刷新、`app.reloadWebView` 和 H5 自刷新后的新 JS 上下文;桌面 runtime 同步声明 `host.events` 表示该真实事件通道可用,但仍不开放 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 真相。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 6245ea7b9..5d8da1be9 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -340,7 +340,7 @@ GameBridge 禁止: 2026-06-18 追加:移动壳 HostBridge 协议名和协议版本统一从 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION` 读取。Expo 入口 query、WebView 事件注入、`host.getRuntime` 回包和 Expo public config smoke 都必须反查共享常量;移动壳配置检查会拒绝重新写死 `GenarrativeHostBridge` 或字面量版本。 -2026-06-19 追加:公开 H5 主站 origin / 默认 URL 统一以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_PUBLIC_WEB_ORIGIN` / `HOST_BRIDGE_PUBLIC_WEB_URL` 为源。移动壳 `DEFAULT_MOBILE_SHELL_WEB_URL` 与 `ALLOWED_PRODUCTION_WEB_ORIGIN` 只是壳层语义别名,分享 URL 归一、WebView 下载协议判定和启动 URL 回退都不得重新写死 `https://app.genarrative.world`;桌面 Rust 侧 `WEB_APP_ORIGIN` 保留为运行时镜像常量,但 `apps/desktop-shell/scripts/check-config.mjs` 必须反查共享 origin。两端配置检查会拒绝本地复刻或漂移同值 origin。 +2026-06-19 追加:公开 H5 主站 origin / 默认 URL 统一以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_PUBLIC_WEB_ORIGIN` / `HOST_BRIDGE_PUBLIC_WEB_URL` 为源。移动壳 `DEFAULT_MOBILE_SHELL_WEB_URL` 与 `ALLOWED_PRODUCTION_WEB_ORIGIN` 只是壳层语义别名,分享 URL 归一、WebView 下载协议判定和启动 URL 回退都不得重新写死 `https://app.genarrative.world`;桌面 Rust 侧 `WEB_APP_ORIGIN` 保留为运行时镜像常量,但 `apps/desktop-shell/scripts/check-config.mjs` 必须反查共享 origin,桌面网络探测也必须从该 origin 解析 host / port,不得在 `network.rs` 里重新写死主站域名。两端配置检查会拒绝本地复刻或漂移同值 origin。 2026-06-18 追加:桌面壳 HostBridge 协议名和协议版本也必须反查同一共享契约。Tauri Rust 侧仍保留 `host_bridge/protocol.rs` 常量作为运行时代码入口,但 `apps/desktop-shell/scripts/check-config.mjs` 会把 Rust `HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION` 与 `packages/shared/src/contracts/hostBridge.ts` 对齐,避免桌面壳事件注入、runtime 回包和 H5 transport 分叉。 @@ -369,7 +369,7 @@ 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 背景音乐和拼图、抓大鹅等固定玩法 `