From ef198f6d4f8a847c5c644d53ae86926a5f1304f3 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 02:18:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=A7=BB=E5=8A=A8=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=AF=BC=E5=85=A5=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 将 Expo 图片选择和拍摄成功响应包装收口到 files 模块 让移动 dispatch 只委托图片导入和拍摄请求 同步移动壳配置门禁和共享记忆 --- apps/mobile-shell/scripts/check-config.mjs | 8 +++++++- apps/mobile-shell/src/host-bridge/dispatch.ts | 8 ++++---- apps/mobile-shell/src/host-bridge/files.ts | 12 ++++++++++++ docs/project-memory/shared-memory/decision-log.md | 4 ++-- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 46a96fdd1..2300490f2 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1679,6 +1679,8 @@ assertMobileDocumentPickerBoundary( for (const [wrapperName, importCall] of [ ['importMobileHostBridgeTextFile', 'ok(request, await importTextFile())'], ['importMobileHostBridgeDocumentFile', 'ok(request, await importDocumentFile())'], + ['importMobileHostBridgeImageFile', 'ok(request, await importImageFile())'], + ['captureMobileHostBridgeImageFile', 'ok(request, await captureImageFile())'], ['importMobileHostBridgeAudioFile', 'ok(request, await importAudioFile())'], ]) { const wrapperBody = extractFunctionBody(filesSource, wrapperName); @@ -1690,12 +1692,16 @@ for (const [wrapperName, importCall] of [ if ( !dispatchSource.includes('importMobileHostBridgeTextFile(request)') || !dispatchSource.includes('importMobileHostBridgeDocumentFile(request)') || + !dispatchSource.includes('importMobileHostBridgeImageFile(request)') || + !dispatchSource.includes('captureMobileHostBridgeImageFile(request)') || !dispatchSource.includes('importMobileHostBridgeAudioFile(request)') || dispatchSource.includes('ok(request, await importTextFile())') || dispatchSource.includes('ok(request, await importDocumentFile())') || + dispatchSource.includes('ok(request, await importImageFile())') || + dispatchSource.includes('ok(request, await captureImageFile())') || dispatchSource.includes('ok(request, await importAudioFile())') ) { - throw new Error('mobile shell dispatch must delegate document picker imports to files.ts'); + throw new Error('mobile shell dispatch must delegate file imports to files.ts'); } if ( diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts index 756c89a66..25ff4646e 100644 --- a/apps/mobile-shell/src/host-bridge/dispatch.ts +++ b/apps/mobile-shell/src/host-bridge/dispatch.ts @@ -1,13 +1,13 @@ import { type HostBridgeRequest } from '../../../../packages/shared/src/contracts/hostBridge'; import { - captureImageFile, + captureMobileHostBridgeImageFile, exportAudioFile, exportImageFile, exportTextFile, importMobileHostBridgeAudioFile, importMobileHostBridgeDocumentFile, + importMobileHostBridgeImageFile, importMobileHostBridgeTextFile, - importImageFile, } from './files'; import { failure, @@ -72,9 +72,9 @@ export async function dispatchMobileHostBridgeRequest( case 'file.exportImage': return ok(request, await exportImageFile(request.payload)); case 'file.importImage': - return ok(request, await importImageFile()); + return importMobileHostBridgeImageFile(request); case 'file.captureImage': - return ok(request, await captureImageFile()); + return captureMobileHostBridgeImageFile(request); case 'scanner.scanQrCode': return scanMobileHostBridgeQrCode(request); case 'file.importAudio': diff --git a/apps/mobile-shell/src/host-bridge/files.ts b/apps/mobile-shell/src/host-bridge/files.ts index 6a38c9764..0f92ad7da 100644 --- a/apps/mobile-shell/src/host-bridge/files.ts +++ b/apps/mobile-shell/src/host-bridge/files.ts @@ -621,6 +621,12 @@ export async function importImageFile(): Promise { return imagePickerResultToImportPayload(result, 'selected'); } +export async function importMobileHostBridgeImageFile( + request: HostBridgeRequest, +) { + return ok(request, await importImageFile()); +} + export async function captureImageFile(): Promise { const permission = await ImagePicker.requestCameraPermissionsAsync(); if (permission.status !== ImagePicker.PermissionStatus.GRANTED) { @@ -641,6 +647,12 @@ export async function captureImageFile(): Promise { return imagePickerResultToImportPayload(result, 'captured'); } +export async function captureMobileHostBridgeImageFile( + request: HostBridgeRequest, +) { + return ok(request, await captureImageFile()); +} + function audioFileExtension(mimeType: HostBridgeAudioMimeType) { if (mimeType === 'audio/mpeg') { return 'mp3'; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 46fe1fcd4..c36825db0 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -60,8 +60,8 @@ - 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 轮询,恢复在线后重新刷新。该能力只减少离线请求,不改变外部生成队列、作品架、弹窗或后端任务状态事实。 - 2026-06-18 桌面图片导入:新增 `file.importImage` 与 `file.imageDropped` HostBridge capability,Tauri 壳通过系统文件选择框和主窗口拖拽事件读取用户选择 / 拖入的真实图片,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;H5 统一使用 `importHostImageFile()` / `subscribeHostImageDrop()`,宿主只回传文件名、MIME、base64 内容、字节数和可选坐标,不暴露本地绝对路径,也不开放通用文件系统。 -- 2026-06-18 移动图片导入:Expo 壳开始声明并实现 `file.importImage`,通过 `expo-image-picker` 请求相册权限并打开系统相册选择器,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;picker 调用必须固定为单选、禁用编辑、禁用 EXIF、请求 base64 且 `mediaTypes` 只允许 `images`,不得扩大到视频或任意媒体。成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。 -- 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则;相机 picker 必须禁用编辑、禁用 EXIF、请求 base64 且 `mediaTypes` 只允许 `images`,成功回传 `action=captured`,不暴露设备本地 URI;该拍摄能力不使用麦克风权限,移动壳麦克风权限只服务同源 H5 实时玩法。Tauri 壳不声明该能力,不伪造桌面拍摄。 +- 2026-06-18 移动图片导入:Expo 壳开始声明并实现 `file.importImage`,通过 `expo-image-picker` 请求相册权限并打开系统相册选择器,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;picker 调用必须固定为单选、禁用编辑、禁用 EXIF、请求 base64 且 `mediaTypes` 只允许 `images`,不得扩大到视频或任意媒体。成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。Expo 图片导入的相册权限、ImagePicker 调用、MIME / 体积 / 图片字节校验和 HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts`,`dispatch.ts` 只委托文件模块。 +- 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则;相机 picker 必须禁用编辑、禁用 EXIF、请求 base64 且 `mediaTypes` 只允许 `images`,成功回传 `action=captured`,不暴露设备本地 URI;该拍摄能力不使用麦克风权限,移动壳麦克风权限只服务同源 H5 实时玩法。Tauri 壳不声明该能力,不伪造桌面拍摄。Expo 图片拍摄的相机权限、ImagePicker 调用、MIME / 体积 / 图片字节校验和 HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts`,`dispatch.ts` 只委托文件模块。 - 2026-06-19 移动二维码扫描:Expo 壳新增 `scanner.scanQrCode` HostBridge capability,通过 `expo-camera` 请求相机权限并打开真实扫码 overlay,成功只返回共享契约清洗后的二维码文本和 `qr_code` 格式,空值、控制字符和超长文本按 `normalizeHostBridgeQrCodeValue` 处理;HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/scanner.ts`,`dispatch.ts` 只委托扫码请求。用户关闭扫码返回 `cancelled`,H5 个人中心扫码入口不再连带弹出浏览器摄像头权限。Tauri 桌面壳只把 `scanner.scanQrCode` 保留在 method 白名单中返回 `unsupported_method`,不声明 capability、不伪造桌面扫码;宿主缺能力或非法结果时 H5 继续走原浏览器扫码 fallback。 - 2026-06-18 H5 图片上传接入宿主导入:`CreativeImageInputPanel` 在 `native_app` 且声明 `file.importImage` / `file.captureImage` 时,主图上传和描述参考图上传可分别调用 `importHostImageFile()` / `captureHostImageFile()`,并把宿主返回的 base64 图片转换为现有 `File` 回调;浏览器、小程序和未声明能力的裁剪壳继续走原生 `` 路径,不新增玩法侧上传分叉。 - 2026-06-18 移动壳安全区:Expo 壳根布局使用 `react-native-safe-area-context` 的 `SafeAreaProvider` 与四边 `SafeAreaView` 保护 WebView,避免 H5 主站内容贴进 iOS 刘海、底部 Home Indicator、Android 状态栏或横屏边缘;该能力属于宿主壳布局保护,不新增 H5 占位 UI,不改变玩法 runtime 或 HostBridge capability。