移动壳补齐图片导出扩展名
按图片 MIME 归一移动壳导出文件名扩展 补充移动 HostBridge 图片导出测试和配置门禁 更新宿主壳文件导出文档
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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 replay:Expo 和 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 只把文本填入现有输入框,不自动提交,也不把剪贴板内容交给宿主侧业务处理;普通浏览器、小程序和未声明该能力的裁剪壳不显示粘贴动作。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user