收口移动文档导入响应边界
将 Expo 文本、文档和音频导入成功响应包装收口到 files 模块 让移动 dispatch 只委托 DocumentPicker 导入请求 同步移动壳配置门禁和共享记忆
This commit is contained in:
@@ -20,6 +20,8 @@ const clipboardPath = new URL('../src/host-bridge/clipboard.ts', import.meta.url
|
||||
const clipboardSource = fs.readFileSync(clipboardPath, 'utf8');
|
||||
const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url);
|
||||
const dispatchSource = fs.readFileSync(dispatchPath, 'utf8');
|
||||
const filesPath = new URL('../src/host-bridge/files.ts', import.meta.url);
|
||||
const filesSource = fs.readFileSync(filesPath, 'utf8');
|
||||
const hapticsPath = new URL('../src/host-bridge/haptics.ts', import.meta.url);
|
||||
const hapticsSource = fs.readFileSync(hapticsPath, 'utf8');
|
||||
const hostBridgeNavigationPath = new URL(
|
||||
@@ -1674,6 +1676,28 @@ assertMobileDocumentPickerBoundary(
|
||||
'MOBILE_AUDIO_DOCUMENT_PICKER_TYPES',
|
||||
);
|
||||
|
||||
for (const [wrapperName, importCall] of [
|
||||
['importMobileHostBridgeTextFile', 'ok(request, await importTextFile())'],
|
||||
['importMobileHostBridgeDocumentFile', 'ok(request, await importDocumentFile())'],
|
||||
['importMobileHostBridgeAudioFile', 'ok(request, await importAudioFile())'],
|
||||
]) {
|
||||
const wrapperBody = extractFunctionBody(filesSource, wrapperName);
|
||||
if (!wrapperBody.includes(importCall)) {
|
||||
throw new Error(`mobile shell file import HostBridge wrapper missing ${importCall}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!dispatchSource.includes('importMobileHostBridgeTextFile(request)') ||
|
||||
!dispatchSource.includes('importMobileHostBridgeDocumentFile(request)') ||
|
||||
!dispatchSource.includes('importMobileHostBridgeAudioFile(request)') ||
|
||||
dispatchSource.includes('ok(request, await importTextFile())') ||
|
||||
dispatchSource.includes('ok(request, await importDocumentFile())') ||
|
||||
dispatchSource.includes('ok(request, await importAudioFile())')
|
||||
) {
|
||||
throw new Error('mobile shell dispatch must delegate document picker imports to files.ts');
|
||||
}
|
||||
|
||||
if (
|
||||
!hostBridgeNavigationSource.includes(
|
||||
'const externalUrlPayload = normalizeHostBridgeExternalUrlPayload(',
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
exportAudioFile,
|
||||
exportImageFile,
|
||||
exportTextFile,
|
||||
importAudioFile,
|
||||
importDocumentFile,
|
||||
importMobileHostBridgeAudioFile,
|
||||
importMobileHostBridgeDocumentFile,
|
||||
importMobileHostBridgeTextFile,
|
||||
importImageFile,
|
||||
importTextFile,
|
||||
} from './files';
|
||||
import {
|
||||
failure,
|
||||
@@ -66,9 +66,9 @@ export async function dispatchMobileHostBridgeRequest(
|
||||
case 'file.exportText':
|
||||
return ok(request, await exportTextFile(request.payload));
|
||||
case 'file.importText':
|
||||
return ok(request, await importTextFile());
|
||||
return importMobileHostBridgeTextFile(request);
|
||||
case 'file.importDocument':
|
||||
return ok(request, await importDocumentFile());
|
||||
return importMobileHostBridgeDocumentFile(request);
|
||||
case 'file.exportImage':
|
||||
return ok(request, await exportImageFile(request.payload));
|
||||
case 'file.importImage':
|
||||
@@ -78,7 +78,7 @@ export async function dispatchMobileHostBridgeRequest(
|
||||
case 'scanner.scanQrCode':
|
||||
return scanMobileHostBridgeQrCode(request);
|
||||
case 'file.importAudio':
|
||||
return ok(request, await importAudioFile());
|
||||
return importMobileHostBridgeAudioFile(request);
|
||||
case 'file.exportAudio':
|
||||
return ok(request, await exportAudioFile(request.payload));
|
||||
case 'haptics.impact':
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type HostBridgeDocumentMimeType,
|
||||
type HostBridgeError,
|
||||
type HostBridgeImageMimeType,
|
||||
type HostBridgeRequest,
|
||||
type HostBridgeTextMimeType,
|
||||
HOST_BRIDGE_AUDIO_MIME_TYPES,
|
||||
HOST_BRIDGE_DOCUMENT_MIME_TYPES,
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
HOST_BRIDGE_TEXT_MIME_TYPES,
|
||||
normalizeHostBridgeExportFileName,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import { invalidRequest } from './protocol';
|
||||
import { invalidRequest, ok } from './protocol';
|
||||
|
||||
const HOST_BRIDGE_TEXT_MIME_TYPE_SET = new Set<HostBridgeTextMimeType>(
|
||||
HOST_BRIDGE_TEXT_MIME_TYPES,
|
||||
@@ -415,6 +416,12 @@ export async function importTextFile(): Promise<FileImportTextResult> {
|
||||
};
|
||||
}
|
||||
|
||||
export async function importMobileHostBridgeTextFile(
|
||||
request: HostBridgeRequest,
|
||||
) {
|
||||
return ok(request, await importTextFile());
|
||||
}
|
||||
|
||||
export async function importDocumentFile(): Promise<FileImportDocumentResult> {
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
copyToCacheDirectory: true,
|
||||
@@ -466,6 +473,12 @@ export async function importDocumentFile(): Promise<FileImportDocumentResult> {
|
||||
};
|
||||
}
|
||||
|
||||
export async function importMobileHostBridgeDocumentFile(
|
||||
request: HostBridgeRequest,
|
||||
) {
|
||||
return ok(request, await importDocumentFile());
|
||||
}
|
||||
|
||||
export async function exportImageFile(
|
||||
payload: unknown,
|
||||
): Promise<FileExportImageResult> {
|
||||
@@ -786,3 +799,9 @@ export async function importAudioFile(): Promise<FileImportAudioResult> {
|
||||
bytes,
|
||||
};
|
||||
}
|
||||
|
||||
export async function importMobileHostBridgeAudioFile(
|
||||
request: HostBridgeRequest,
|
||||
) {
|
||||
return ok(request, await importAudioFile());
|
||||
}
|
||||
|
||||
@@ -91,8 +91,8 @@
|
||||
- 2026-06-19 移动壳剪贴板边界:Expo `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写、共享 100000 字符归一、payload 校验和 HostBridge 成功 / 失败响应边界统一收口在 `apps/mobile-shell/src/host-bridge/clipboard.ts`;`dispatch.ts` 只负责把 HostBridge request 委托给 `writeMobileHostBridgeClipboardText(request)` / `readMobileHostBridgeClipboardText(request)`,不得直接导入 `expo-clipboard`、调用 `Clipboard.setStringAsync` / `Clipboard.getStringAsync` 或包装剪贴板成功响应。移动壳配置检查会覆盖该模块结构、共享文本边界和 dispatch 委托关系,避免剪贴板能力散落到分发层。
|
||||
- 2026-06-19 桌面壳剪贴板边界:Tauri `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写、共享 100000 字符归一、payload 校验和响应边界统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs`;`dispatch.rs` 只负责把 HostBridge request 委托给 `write_desktop_host_bridge_clipboard_text(...)` / `read_desktop_host_bridge_clipboard_text(...)`,不得直接承接剪贴板文本截断、payload 解析或插件读写细节;桌面 share fallback 仍可调用底层写剪贴板函数复制分享文本。桌面壳配置检查会覆盖该模块结构、共享文本边界和 dispatch 委托关系,避免剪贴板能力散落到分发层。
|
||||
- 2026-06-19 桌面壳本地通知边界:Tauri `notification.showLocal` 的 payload 清洗、权限状态检查、prompt 权限请求和系统通知发送统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs`;`dispatch.rs` 只负责把 HostBridge request 委托给 `show_desktop_local_notification(...)` 并映射响应,不得直接调用 `app.notification()`、`NotificationExt` 或 `PermissionState`。桌面壳配置检查会覆盖该模块结构、权限语义和 dispatch 委托关系,避免本地通知能力散落到分发层。
|
||||
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。
|
||||
- 2026-06-19 文档文件导入能力:新增 `file.importDocument` HostBridge capability,作为创作 Agent 工作台优先导入路径;Expo 壳通过 DocumentPicker、Tauri 壳通过系统文件选择框读取文本类文档或 DOCX 副本。两端只接受文本 MIME / DOCX MIME 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、base64 内容和字节数,不暴露设备 URI、本机绝对路径,也不开放通用文件系统;H5 把返回内容转换成浏览器 `File` 后继续走后端 `/api/runtime/creation-agent/document-inputs/parse`,不在前端解析 DOCX。旧壳只声明 `file.importText` 时继续使用文本导入兜底。
|
||||
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。Expo 文本导入的 DocumentPicker 调用、大小校验、文本读取和 HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts`,`dispatch.ts` 只委托文件模块。
|
||||
- 2026-06-19 文档文件导入能力:新增 `file.importDocument` HostBridge capability,作为创作 Agent 工作台优先导入路径;Expo 壳通过 DocumentPicker、Tauri 壳通过系统文件选择框读取文本类文档或 DOCX 副本。两端只接受文本 MIME / DOCX MIME 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、base64 内容和字节数,不暴露设备 URI、本机绝对路径,也不开放通用文件系统;H5 把返回内容转换成浏览器 `File` 后继续走后端 `/api/runtime/creation-agent/document-inputs/parse`,不在前端解析 DOCX。旧壳只声明 `file.importText` 时继续使用文本导入兜底。Expo 文档导入的 DocumentPicker 调用、大小校验、base64 读取和 HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts`,`dispatch.ts` 只委托文件模块。
|
||||
- 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability,不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘,必须通过托盘“退出”结束应用;托盘注册失败不得阻断主窗口启动,也不得拦截关闭,避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。
|
||||
- 2026-06-18 Tauri 单实例:桌面壳启用 `tauri-plugin-single-instance` 并要求该插件最先注册;重复启动 App 时第二实例退出,只唤醒、取消最小化并聚焦已有主窗口,不把第二实例 argv / cwd 作为事件透传给 H5。Windows / Linux 的二次实例深链只通过 single-instance 的 `deep-link` feature 交给 Tauri deep-link 插件,再由 `shell/deep_link.rs` 做受控 URL 归一。
|
||||
- 2026-06-18 Tauri 桌面深链:桌面壳启用 `tauri-plugin-deep-link`,但不安装 JS guest 包、不把 deep-link command 加入主窗口 capability,也不新增 HostBridge capability。Tauri 配置只注册 `genarrative` scheme;Rust 层只接受 `genarrative://open/...`、`genarrative://app/...`、`genarrative://<path>` 和 `https://app.genarrative.world/...`,统一跳转到同源 H5 并补写 `native_app`、`tauri_desktop`、当前平台、版本和真实 capability 清单;外域、明文协议和危险协议不进入主 WebView。
|
||||
@@ -2383,7 +2383,7 @@
|
||||
## 2026-06-18 原生壳音频文件导入只返回受控内容副本
|
||||
|
||||
- 背景:木鱼等固定玩法的音频上传面板需要在 Expo 移动壳和 Tauri 桌面壳内走真实系统选择器;如果直接暴露设备 URI、本机路径或通用文件系统能力,会把一次用户选择扩大成长期本地文件权限。
|
||||
- 决策:新增 HostBridge method `file.importAudio`、H5 facade `importHostAudioFile()` 和通用音频输入面板接入。移动端通过 Expo DocumentPicker,picker 展示范围包含 `audio/*` 和当前允许的精确音频 MIME;桌面端通过 Tauri 系统文件选择框。HostBridge 返回 H5 前,两端仍只接受 `audio/mpeg`、`audio/mp4`、`audio/wav`、`audio/ogg`、`audio/webm` 或对应扩展名,并要求音频 bytes 与归一后的 MIME 匹配,单次不超过 20 MiB。宿主成功时只返回清洗后的文件名、MIME、base64 内容和字节数,不返回设备 URI 或本机绝对路径,也不开放通用文件系统。H5 将宿主结果转换成现有浏览器 `File`,继续复用 `readFileAsAsset(file, 'uploaded')` 音频处理链路。
|
||||
- 决策:新增 HostBridge method `file.importAudio`、H5 facade `importHostAudioFile()` 和通用音频输入面板接入。移动端通过 Expo DocumentPicker,picker 展示范围包含 `audio/*` 和当前允许的精确音频 MIME;桌面端通过 Tauri 系统文件选择框。HostBridge 返回 H5 前,两端仍只接受 `audio/mpeg`、`audio/mp4`、`audio/wav`、`audio/ogg`、`audio/webm` 或对应扩展名,并要求音频 bytes 与归一后的 MIME 匹配,单次不超过 20 MiB。宿主成功时只返回清洗后的文件名、MIME、base64 内容和字节数,不返回设备 URI 或本机绝对路径,也不开放通用文件系统。H5 将宿主结果转换成现有浏览器 `File`,继续复用 `readFileAsAsset(file, 'uploaded')` 音频处理链路。Expo 音频导入的 DocumentPicker 调用、大小校验、base64 读取和 HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts`,`dispatch.ts` 只委托文件模块。
|
||||
- 影响范围:`packages/shared/src/contracts/hostBridge.ts`、`src/services/host-bridge/hostBridge.ts`、`src/components/common/CreativeAudioInputPanel.tsx`、`apps/mobile-shell/`、`apps/desktop-shell/`、原生壳能力检查脚本和 HostBridge 架构文档。
|
||||
- 验证方式:`npm run check:native-shells`、`npm run test -- src/components/common/CreativeAudioInputPanel.test.tsx`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user