移动壳补齐图片导出扩展名

按图片 MIME 归一移动壳导出文件名扩展

补充移动 HostBridge 图片导出测试和配置门禁

更新宿主壳文件导出文档
This commit is contained in:
2026-06-18 21:04:38 +08:00
parent 3deb901179
commit 106798f505
5 changed files with 48 additions and 4 deletions
@@ -970,6 +970,7 @@ for (const snippet of [
'ensureAudioBytesMatchMimeType',
"'image bytes do not match MIME'",
"'audio bytes do not match MIME'",
'normalizeExportedImageFileName',
'normalizeHostBridgeExportFileName',
'normalizeHostBridgeClipboardText',
'normalizeHostBridgeHapticsImpactStyle',
@@ -943,7 +943,7 @@ describe('handleMobileHostBridgeMessage', () => {
test('file.exportImage 写入缓存图片并调起系统分享', async () => {
const response = await send(
request('file.exportImage', {
fileName: ' ../分享:卡?.png ',
fileName: ' ../分享:卡? ',
base64Data: PNG_BASE64,
mimeType: 'image/png',
}),
@@ -971,6 +971,25 @@ describe('handleMobileHostBridgeMessage', () => {
dialogTitle: '分享-卡-.png',
},
);
const jpegResponse = await send(
request('file.exportImage', {
fileName: '分享卡.png',
base64Data: JPEG_BASE64,
mimeType: 'image/jpeg',
}),
);
expect(expectOk(jpegResponse).result).toEqual({
action: 'saved',
fileName: '分享卡.png.jpg',
bytes: 7,
});
expect(writtenFiles.at(-1)).toEqual({
uri: 'file:///cache/分享卡.png.jpg',
content: JPEG_BASE64,
options: { encoding: 'base64' },
});
});
test('file.exportImage 拒绝非图片 MIME 与超限内容', async () => {
+25 -1
View File
@@ -208,6 +208,27 @@ function ensureAudioBytesMatchMimeType(
}
}
function imageFileExtension(mimeType: HostBridgeImageMimeType) {
if (mimeType === 'image/jpeg') {
return 'jpg';
}
if (mimeType === 'image/webp') {
return 'webp';
}
return 'png';
}
function normalizeExportedImageFileName(
rawFileName: unknown,
mimeType: HostBridgeImageMimeType,
) {
const fileName = normalizeHostBridgeExportFileName(rawFileName);
const extension = imageFileExtension(mimeType);
return fileName.toLowerCase().endsWith(`.${extension}`)
? fileName
: `${fileName}.${extension}`;
}
export async function exportTextFile(
payload: unknown,
): Promise<FileExportTextResult> {
@@ -353,7 +374,10 @@ export async function exportImageFile(
} satisfies HostBridgeError;
}
const fileName = normalizeHostBridgeExportFileName(exportPayload?.fileName);
const fileName = normalizeExportedImageFileName(
exportPayload?.fileName,
mimeType as HostBridgeImageMimeType,
);
const file = new File(Paths.cache, fileName);
file.write(base64Data, { encoding: 'base64' });
await Sharing.shareAsync(file.uri, {
@@ -70,7 +70,7 @@
- 2026-06-18 桌面壳顶层导航边界:Tauri 主 WebView 只允许打包资产 URL 和 `https://app.genarrative.world` 同源 H5 route 留在主窗口;外域 `http:` / `https:``mailto:``tel:` 导航与 `window.open` 请求交给系统 opener 后拒绝 WebView 留壳;`javascript:``file:` 等危险协议直接拒绝。该规则不进入 HostBridge capability,不开放 opener JS guest API,配置检查和 cargo test 覆盖导航策略。
- 2026-06-18 桌面壳默认下载边界:Tauri 主 WebView 的下载事件默认拒绝网页自动下载和 `<a download>` 落盘,桌面文件保存只能通过 `file.exportText``file.exportImage``file.exportAudio` 等已声明 HostBridge method 进入 Rust 侧系统保存对话框,并继续执行 MIME、大小、文件名清洗和用户确认。该规则不进入 HostBridge capability,配置检查和 cargo test 覆盖下载拒绝策略。
- 2026-06-18 桌面壳文件 bytes 校验:Tauri 图片 / 音频导入导出不得只信扩展名或 H5 声明 MIME;Rust 侧必须识别 PNG / JPEG / WebP、MP3 / MP4-M4A / WAV / OGG / WebM bytes 头部,要求导入文件扩展名对应 MIME 与真实 bytes 匹配,导出 payload 的 `mimeType``base64Data` 解码 bytes 匹配。不匹配返回 `invalid_request`,继续不暴露本机绝对路径或通用文件系统能力。配置检查和 cargo test 覆盖该边界。
- 2026-06-18 移动壳文件 bytes 校验:Expo 图片 / 音频导入导出不得只信系统 picker 返回 MIME、文件扩展名或 H5 声明 MIME;移动壳必须识别 PNG / JPEG / WebP、MP3 / MP4-M4A / WAV / OGG / WebM base64 bytes 头部,要求导入 MIME 归一结果与真实 bytes 匹配,导出 payload 的 `mimeType``base64Data` 解码 bytes 匹配。不匹配返回 `invalid_request`,不会写入缓存文件、调起系统分享或把内容回传给 H5。配置检查和移动壳测试覆盖该边界。
- 2026-06-18 移动壳文件 bytes 校验:Expo 图片 / 音频导入导出不得只信系统 picker 返回 MIME、文件扩展名或 H5 声明 MIME;移动壳必须识别 PNG / JPEG / WebP、MP3 / MP4-M4A / WAV / OGG / WebM base64 bytes 头部,要求导入 MIME 归一结果与真实 bytes 匹配,导出 payload 的 `mimeType``base64Data` 解码 bytes 匹配。不匹配返回 `invalid_request`,不会写入缓存文件、调起系统分享或把内容回传给 H5。移动图片导出还必须按 MIME 给系统分享 / 保存面板补齐 `.png` / `.jpg` / `.webp` 文件名扩展,避免缓存文件名与真实图片类型漂移。配置检查和移动壳测试覆盖该边界。
- 2026-06-18 桌面壳 DevTools 边界:Tauri 主 WebView 配置必须显式 `devtools=false`Cargo 依赖不得启用 Tauri `devtools` feature;桌面壳本地调试走普通浏览器和 Vite,不把 debug / release 桌面包变成可打开浏览器检查器的调试容器。配置检查会拒绝主窗口 DevTools 或 release feature 被重新打开。
- 2026-06-18 桌面壳 Tauri 命令白名单:桌面壳源码、Tauri build manifest、主窗口 capability 和本地自动生成权限目录都只能暴露 `host_bridge_request` 一个受控 command;所有桌面能力继续在 Rust 内部按 HostBridge method 白名单分发,不新增可被 H5 直接 `invoke` 的 Tauri command,也不授予插件 JS guest API。检查脚本会拒绝多余 command、权限列表顺序漂移和残留的自动生成权限文件。
- 2026-06-18 HostBridge request id replayExpo 和 Tauri 壳都必须按 request id 回放首次完成结果;同 id 进行中的请求共享同一执行结果,已完成请求直接回放缓存响应,避免系统分享、外链、剪贴板、文件选择 / 保存、本地通知、窗口导航等宿主副作用被重复触发。两端配置检查和测试会锁住 replay 结构。
@@ -279,7 +279,7 @@ GameBridge 禁止:
2026-06-18 追加:移动壳声明并实现 `file.importText`,通过 Expo DocumentPicker 打开系统文档选择器,只接受 `text/plain``text/markdown``text/csv``application/json` 或对应扩展名,单次不超过 5 MiB;成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备本地 URI,也不开放通用文件系统。H5 创作 Agent 工作台在原生壳声明该能力时优先打开宿主系统选择器,再把返回文本转换成现有浏览器 `File` 并继续调用 `/api/runtime/creation-agent/document-inputs/parse`,不在前端绕过后端文档解析、大小校验或 docx 处理。
2026-06-18 追加:移动壳图片和音频导入 / 导出不再只信 Expo picker 返回的 MIME 或 H5 声明的 MIME。Expo 壳会对 PNG / JPEG / WebP、MP3 / MP4-M4A / WAV / OGG / WebM 做 base64 bytes 头部识别,导入时要求系统 picker 的 MIME 与 bytes 匹配,导出时要求 H5 payload 的 `mimeType``base64Data` 解码后的 bytes 匹配;不匹配统一返回 `invalid_request`,不会写入缓存文件、调起系统分享或把内容回传给 H5。
2026-06-18 追加:移动壳图片和音频导入 / 导出不再只信 Expo picker 返回的 MIME 或 H5 声明的 MIME。Expo 壳会对 PNG / JPEG / WebP、MP3 / MP4-M4A / WAV / OGG / WebM 做 base64 bytes 头部识别,导入时要求系统 picker 的 MIME 与 bytes 匹配,导出时要求 H5 payload 的 `mimeType``base64Data` 解码后的 bytes 匹配;不匹配统一返回 `invalid_request`,不会写入缓存文件、调起系统分享或把内容回传给 H5。移动图片导出还会按 MIME 补齐 `.png` / `.jpg` / `.webp` 文件名扩展,避免系统分享 / 保存面板拿到无扩展或错扩展的缓存图片。
2026-06-18 追加:H5 个人中心的邀请码填写和兑换码弹窗开始消费 `clipboard.readText`。Expo 壳仍只通过 `expo-clipboard` 返回纯文本,H5 只把文本填入现有输入框,不自动提交,也不把剪贴板内容交给宿主侧业务处理;普通浏览器、小程序和未声明该能力的裁剪壳不显示粘贴动作。