From 87f2532a02268ae1283dac11131d2d5eeca23fb8 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 02:29:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E6=A1=8C=E9=9D=A2=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E5=93=8D=E5=BA=94=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Tauri host.getRuntime 成功响应包装收口到 runtime 模块 让桌面 dispatch 只委托运行时请求 同步桌面壳配置门禁和共享记忆 --- apps/desktop-shell/scripts/check-config.mjs | 7 ++++++- .../src-tauri/src/host_bridge/dispatch.rs | 7 +++---- .../src-tauri/src/host_bridge/runtime.rs | 11 ++++++++++- docs/project-memory/shared-memory/decision-log.md | 4 ++-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/desktop-shell/scripts/check-config.mjs b/apps/desktop-shell/scripts/check-config.mjs index 8b34e792d..609217bf4 100644 --- a/apps/desktop-shell/scripts/check-config.mjs +++ b/apps/desktop-shell/scripts/check-config.mjs @@ -1718,6 +1718,9 @@ if (!rustHostSource.includes('host_version: env!("CARGO_PKG_VERSION")')) { for (const runtimeSnippet of [ 'pub(crate) fn desktop_runtime() -> HostBridgeRuntime', + 'pub(crate) fn desktop_host_bridge_runtime_response(', + 'request: &HostBridgeRequest', + 'ok(request.id.clone(), json!(desktop_runtime()))', 'shell: "tauri_desktop"', 'platform: desktop_platform()', 'host_version: env!("CARGO_PKG_VERSION")', @@ -1731,8 +1734,10 @@ for (const runtimeSnippet of [ if ( !desktopHostBridgeDispatchSource.includes( - '"host.getRuntime" => ok(request.id, json!(desktop_runtime()))', + '"host.getRuntime" => desktop_host_bridge_runtime_response(&request)', ) || + desktopHostBridgeDispatchSource.includes('json!(desktop_runtime())') || + desktopHostBridgeDispatchSource.includes(' ok(') || desktopHostBridgeDispatchSource.includes('HostBridgeRuntime') || desktopHostBridgeDispatchSource.includes('HOST_BRIDGE_VERSION') || desktopHostBridgeDispatchSource.includes('capabilities()') || 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 a5f176f1c..6165028a5 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/dispatch.rs @@ -16,14 +16,13 @@ use crate::host_bridge::navigation::{ use crate::host_bridge::network::resolve_desktop_host_bridge_network_status; use crate::host_bridge::notifications::show_desktop_local_notification; use crate::host_bridge::protocol::{ - failed, ok, validate_request, HostBridgeRequest, HostBridgeResponse, + failed, validate_request, HostBridgeRequest, HostBridgeResponse, }; -use crate::host_bridge::runtime::desktop_runtime; +use crate::host_bridge::runtime::desktop_host_bridge_runtime_response; use crate::host_bridge::share::{ open_desktop_host_bridge_share, set_desktop_host_bridge_share_target, }; use crate::host_bridge::title::set_desktop_host_bridge_window_title; -use serde_json::json; pub(crate) fn resolve_host_bridge_request(request: HostBridgeRequest) -> HostBridgeResponse { if let Some(response) = validate_request(&request) { @@ -31,7 +30,7 @@ pub(crate) fn resolve_host_bridge_request(request: HostBridgeRequest) -> HostBri } match request.method.as_str() { - "host.getRuntime" => ok(request.id, json!(desktop_runtime())), + "host.getRuntime" => desktop_host_bridge_runtime_response(&request), _ => failed( request.id, "unsupported_method", 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 8fdb059b6..b85c1be4b 100644 --- a/apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs +++ b/apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs @@ -1,6 +1,9 @@ use crate::host_bridge::capabilities::capabilities; -use crate::host_bridge::protocol::{HostBridgeRuntime, HOST_BRIDGE_VERSION}; +use crate::host_bridge::protocol::{ + ok, HostBridgeRequest, HostBridgeResponse, HostBridgeRuntime, HOST_BRIDGE_VERSION, +}; use crate::shell::webview::desktop_platform; +use serde_json::json; pub(crate) fn desktop_runtime() -> HostBridgeRuntime { HostBridgeRuntime { @@ -12,6 +15,12 @@ pub(crate) fn desktop_runtime() -> HostBridgeRuntime { } } +pub(crate) fn desktop_host_bridge_runtime_response( + request: &HostBridgeRequest, +) -> HostBridgeResponse { + ok(request.id.clone(), json!(desktop_runtime())) +} + #[cfg(test)] mod tests { use super::*; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 15bbd9163..e5e27de4a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -77,7 +77,7 @@ - 2026-06-18 移动壳安装包身份:Expo 移动壳的 iOS bundle identifier 与 Android package 统一固定为 `world.genarrative.mobile`,应用版本固定为 `0.1.0`,iOS `buildNumber` 从字符串 `"1"` 起步,Android `versionCode` 从整数 `1` 起步;后续分发安装包时递增构建号 / versionCode,产品版本号按发布节奏调整。移动壳配置检查会校验 `app.json` 与 `package.json` 版本一致,并拒绝缺失或漂移的包标识,当前不写入假商店元数据、假更新端点或占位渠道 SDK 配置。 - 2026-06-19 移动壳 HostBridge 版本运行时来源:Expo 移动壳的 H5 入口 query 和 `host.getRuntime` 回包都读取 `MOBILE_SHELL_HOST_VERSION`,该值必须从移动壳 `app.json` 的 Expo `version` 配置解析,异常配置只回退到与 `app.json` / `package.json` 一致的受检 fallback;配置检查会拒绝 `App.tsx`、`bridge.ts` 或 `runtime.ts` 重新散落硬编码版本,避免安装包版本升级时 H5 首屏上下文与 runtime 回读分叉。 - 2026-06-19 移动壳 runtime 桥接边界:Expo `host.getRuntime` 的平台归一、hostVersion、bridgeVersion、capability 清单组装和 HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/runtime.ts`;`dispatch.ts` 只负责把 `host.getRuntime` 委托给 `getMobileHostBridgeRuntimeResponse(...)`。移动壳配置检查会拒绝分发层重新读取 `MOBILE_SHELL_HOST_VERSION`、`HOST_BRIDGE_VERSION`、`resolveMobileHostCapabilities(...)`、`Platform.OS` 或包装 runtime 成功响应,避免入口 URL、runtime 回包和能力清单继续分叉。 -- 2026-06-19 桌面壳 runtime 桥接边界:Tauri `host.getRuntime` 的平台归一、hostVersion、bridgeVersion 和 capability 清单组装统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs`;`dispatch.rs` 只负责把 `host.getRuntime` 委托给 `desktop_runtime()` 并序列化返回。桌面壳配置检查会拒绝分发层重新读取 `env!("CARGO_PKG_VERSION")`、`HOST_BRIDGE_VERSION`、`capabilities()` 或 `desktop_platform()`,避免入口 URL、runtime 回包和能力清单继续分叉。 +- 2026-06-19 桌面壳 runtime 桥接边界:Tauri `host.getRuntime` 的平台归一、hostVersion、bridgeVersion、capability 清单组装和 HostBridge 成功响应包装统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/runtime.rs`;`dispatch.rs` 只负责把 `host.getRuntime` 委托给 `desktop_host_bridge_runtime_response(&request)`。桌面壳配置检查会拒绝分发层重新读取 `env!("CARGO_PKG_VERSION")`、`HOST_BRIDGE_VERSION`、`capabilities()`、`desktop_platform()` 或包装 runtime 成功响应,避免入口 URL、runtime 回包和能力清单继续分叉。 - 2026-06-18 移动壳发布通道边界:Expo 移动壳默认显式关闭 OTA 更新,只允许 `updates.enabled=false`;在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程落地前,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖。移动壳配置检查和 Expo public config smoke 会拒绝这些发布通道能力被提前打开,根 `package-lock.json` 也不得解析 `expo-updates`、Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 等真实发布 / 观测 SDK。 - 2026-06-18 移动壳观测与渠道 SDK 初始化边界:移动壳生产入口、HostBridge、启动 URL 和 runtime 配置不得提前初始化 Sentry、Firebase Analytics、PostHog、Amplitude、Segment、CodePush 或 Expo Updates;这些 SDK 必须等真实发布通道、采集字段、用户授权、隐私披露、签名 / 回滚策略和团队发布流程落地后逐项接入。配置检查会同时拒绝相关依赖、锁文件解析、Expo 配置和源码初始化片段;`expo-application` 可能由 Expo 自身传递解析,但项目不得主动 direct 依赖它实现渠道逻辑。 - 2026-06-18 桌面图片拖入接入主图槽位:`CreativeImageInputPanel` 在桌面壳声明 `file.imageDropped` 时订阅宿主拖入事件,只在拖入坐标命中当前主图卡片且未被上层元素遮挡时消费事件,避免窗口级拖入被多个创作面板同时接收;成功后仍转换为现有 `File` 上传回调。 @@ -2588,7 +2588,7 @@ - 背景:文件导入导出、剪贴板、角标、本地通知和 request id 都已经在 Expo 与 Tauri 两套壳里有运行时校验;如果 MIME 清单、字节上限或文本长度只靠人工同步,新增文件类型或调整上限时会出现 H5 契约、移动壳和桌面壳互相漂移。 - 决策:`packages/shared/src/contracts/hostBridge.ts` 是 HostBridge 载荷边界的声明来源,导出文本 / 图片 / 音频 MIME 清单、文档导入 MIME 清单、导入 / 导出字节上限、导出文件名 fallback / 长度上限、request id 长度、角标上限、窗口标题长度、外链 URL payload、分享 payload、剪贴板文本长度、触觉反馈 style 和本地通知标题 / 正文长度。Expo 移动壳必须直接导入这些共享常量,`apps/mobile-shell/scripts/check-config.mjs` 会拒绝移动壳重新本地声明文件大小或 MIME 清单;移动壳 `file.importText` / `file.importDocument` / `file.importAudio` 必须在读取文本内容或 base64 前,通过 picker `size` 或 Expo `File.size` 拿到可信 byte count 并完成上限校验,无法拿到可信大小时直接拒绝导入。H5 facade 消费 `file.importText` / `file.importDocument` / `file.importImage` / `file.captureImage` / `file.importAudio` / `file.imageDropped` 返回结果时必须分别通过共享导入结果 normalizer 再校验文件名、MIME、内容、base64、字节数和图片拖拽坐标,避免旧壳或异常壳返回超界数据被业务层消费。`share.open` 必须通过共享 `normalizeHostBridgeShareOpenPayload()` 把 `url`、`href`、`path`、`targetPath` 和 `work` 归一到公开 H5 同源 URL,H5 facade 和 Expo 移动壳都执行该边界,Tauri 壳用 Rust URL parser 镜像同一规则。`app.openExternalUrl` 必须先通过共享 `normalizeHostBridgeExternalUrlPayload()` 清洗为 `{ url }`,H5 facade 和 Expo 移动壳都执行该边界,Tauri 壳用 Rust URL parser 镜像同一协议清单。`app.setTitle` 必须拒绝空值和控制字符,并按共享 80 字符上限截断;H5 facade 和 Tauri 壳都执行该边界。`clipboard.writeText` / `clipboard.readText` 两个方向都必须执行同一个 100000 字符上限;H5 facade 发起 `clipboard.writeText` 前先按共享上限归一化 payload,Expo 与 Tauri 壳仍必须再次执行同一边界,不允许只信 H5 facade 的预校验。H5 facade 发起 `haptics.impact` 前也必须按共享 style 清单归一化,未知 style 不发往宿主,Expo 壳仍二次拒绝未知值。`file.exportText` 必须在 H5 facade 发起请求前通过 `normalizeHostBridgeExportTextPayload()` 预校验文件名、文本内容、可选 MIME 和 5 MiB 上限;可选 `mimeType` 只能来自 `HOST_BRIDGE_TEXT_MIME_TYPES`,缺省为 `text/plain`,Expo 与 Tauri 都必须拒绝图片、音频或二进制 MIME,避免 H5 通过文本导出通道伪装落盘;`file.exportImage` / `file.exportAudio` 必须在 H5 facade 发起请求前分别通过 `normalizeHostBridgeExportImagePayload()` / `normalizeHostBridgeExportAudioPayload()` 预校验文件名、MIME、base64 和共享导出上限,Expo 与 Tauri 壳仍必须按真实字节和 MIME 二次校验,不允许只信 H5 预检。两端 config check 必须反查该边界。Tauri 桌面壳按 Rust 运行时代码镜像实现,`apps/desktop-shell/scripts/check-config.mjs` 必须反查共享契约并拒绝漂移。 -- 追加:Tauri 桌面壳 `host.getRuntime` 回包必须由 `host_bridge/runtime.rs` 的 `desktop_runtime()` 单一函数生成,内部统一读取 `desktop_platform()`、`env!("CARGO_PKG_VERSION")`、`HOST_BRIDGE_VERSION` 和 `capabilities()`;`resolve_host_bridge_request` 只负责把该结构序列化返回。`apps/desktop-shell/scripts/check-config.mjs` 必须拒绝把 `HostBridgeRuntime` 重新内联到 match arm,避免平台、版本或 capability 来源分叉。 +- 追加:Tauri 桌面壳 `host.getRuntime` 回包必须由 `host_bridge/runtime.rs` 的 `desktop_runtime()` 单一函数生成,内部统一读取 `desktop_platform()`、`env!("CARGO_PKG_VERSION")`、`HOST_BRIDGE_VERSION` 和 `capabilities()`;同文件的 `desktop_host_bridge_runtime_response(&request)` 负责把该结构包装成 HostBridge 成功响应,`resolve_host_bridge_request` 只做 method 委托。`apps/desktop-shell/scripts/check-config.mjs` 必须拒绝把 `HostBridgeRuntime` 或 `ok(json!(desktop_runtime()))` 重新内联到 match arm,避免平台、版本、capability 来源或响应形状分叉。 - 追加:Expo 移动壳 `host.getRuntime` 返回的 `platform` 与 `capabilities` 必须来自同一个归一化平台值,避免 iOS/Android 能力清单与上报平台分开计算后漂移。`apps/mobile-shell/src/host-bridge/dispatch.ts` 先通过 `getMobileRuntimePlatform()` 得到 `ios` / `android`,再把同一个值传给 `resolveMobileHostCapabilities(platform)`;`apps/mobile-shell/scripts/check-config.mjs` 必须拒绝恢复为无参 `resolveMobileHostCapabilities()`。 - 追加:Expo 移动壳的 DocumentPicker 调用也属于 HostBridge 文件边界的一部分。文本、文档和音频导入必须固定 `copyToCacheDirectory: true`、`multiple: false`,并分别使用文本导入清单、`MOBILE_DOCUMENT_PICKER_TYPES` 和 `MOBILE_AUDIO_DOCUMENT_PICKER_TYPES`;宿主只读取用户本次选择后复制到缓存的单个文件副本,不扩展为多选、目录访问或长期设备 URI 访问。`apps/mobile-shell/scripts/check-config.mjs` 必须按函数精确反查这些 picker option。 - 追加:Tauri 桌面壳的系统文件对话框过滤器也是 HostBridge 文件边界的一部分:文本导出只允许 `txt/json/md/csv` 保存,文本导入只允许 `txt/md/markdown/csv/json` 选择,文档导入额外允许 `docx`,图片导入导出只允许 `png/jpg/jpeg/webp`,音频导入允许 `mp3/m4a/mp4/wav/ogg/webm`,音频导出只允许 `mp3/m4a/wav/ogg/webm`。`apps/desktop-shell/scripts/check-config.mjs` 必须按 method 精确检查 `.add_filter(...)` 与 `blocking_save_file` / `blocking_pick_file` 归属,避免把一次用户选择扩大成任意本地文件访问。