From c970711d7a352eb7751ff2754d852f45d82c430e Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 17:21:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E7=A7=BB=E5=8A=A8=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E8=83=BD=E5=8A=9B=E4=B8=8A=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一移动壳 host.getRuntime 的平台归一化来源 要求运行时 capabilities 复用同一个平台值 同步移动壳运行时能力边界记录 --- apps/mobile-shell/scripts/check-config.mjs | 21 +++++++++++++++++ apps/mobile-shell/src/host-bridge/dispatch.ts | 23 +++++++++++++------ .../shared-memory/decision-log.md | 1 + 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 17aa284d3..541144390 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1643,6 +1643,27 @@ if (!dispatchSource.includes('hostVersion: MOBILE_SHELL_HOST_VERSION')) { throw new Error('mobile shell runtime response must use the shared mobile shell host version'); } +for (const runtimeSnippet of [ + 'function getMobileRuntimePlatform()', + "return Platform.OS === 'ios' ? 'ios' : 'android'", + 'function getRuntime()', + 'const platform = getMobileRuntimePlatform();', + 'platform,', + 'capabilities: resolveMobileHostCapabilities(platform)', + "case 'host.getRuntime':", + 'return ok(request, getRuntime());', +]) { + if (!dispatchSource.includes(runtimeSnippet)) { + throw new Error(`mobile shell runtime response missing ${runtimeSnippet}`); + } +} + +if (dispatchSource.includes('capabilities: resolveMobileHostCapabilities(),')) { + throw new Error( + 'mobile shell runtime capabilities must use the same normalized platform reported in host.getRuntime', + ); +} + for (const snippet of [ "import appConfig from '../../app.json'", 'resolveMobileShellHostVersion(', diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts index 492ea94b5..51814bee7 100644 --- a/apps/mobile-shell/src/host-bridge/dispatch.ts +++ b/apps/mobile-shell/src/host-bridge/dispatch.ts @@ -219,6 +219,21 @@ function getColorScheme() { }; } +function getMobileRuntimePlatform() { + return Platform.OS === 'ios' ? 'ios' : 'android'; +} + +function getRuntime() { + const platform = getMobileRuntimePlatform(); + return { + shell: 'expo_mobile', + platform, + hostVersion: MOBILE_SHELL_HOST_VERSION, + bridgeVersion: HOST_BRIDGE_VERSION, + capabilities: resolveMobileHostCapabilities(platform), + }; +} + function openNativePage(payload: unknown) { if (!navigation) { throw unsupported('navigation.openNativePage'); @@ -257,13 +272,7 @@ export async function dispatchMobileHostBridgeRequest( ) { switch (request.method) { case 'host.getRuntime': - return ok(request, { - shell: 'expo_mobile', - platform: Platform.OS === 'ios' ? 'ios' : 'android', - hostVersion: MOBILE_SHELL_HOST_VERSION, - bridgeVersion: HOST_BRIDGE_VERSION, - capabilities: resolveMobileHostCapabilities(), - }); + return ok(request, getRuntime()); case 'appearance.getColorScheme': return ok(request, getColorScheme()); case 'app.openExternalUrl': diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index ad732f3b6..0d039c7c6 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2572,6 +2572,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` 必须反查共享契约并拒绝漂移。 +- 追加: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` 归属,避免把一次用户选择扩大成任意本地文件访问。 - 影响范围:`packages/shared/src/contracts/hostBridge.ts`、`apps/mobile-shell/src/host-bridge/files.ts`、`apps/mobile-shell/scripts/check-config.mjs`、`apps/desktop-shell/src-tauri/src/host_bridge/`、`apps/desktop-shell/scripts/check-config.mjs`、Expo / Tauri HostBridge 方案文档。