收口移动壳导入文件名归一

新增共享 HostBridge 导入文件名清洗函数

移动文件导入改用导入专用文件名归一边界

补充契约测试、移动配置门禁和协议文档
This commit is contained in:
2026-06-20 03:04:21 +08:00
parent eb869fb844
commit e2fc7d78f3
6 changed files with 46 additions and 19 deletions
@@ -702,6 +702,7 @@ const sharedPayloadBoundaryImports = [
'HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES',
'HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES',
'HOST_BRIDGE_MOBILE_LOCAL_NOTIFICATION_CHANNEL_ID',
'normalizeHostBridgeImportFileName',
'normalizeHostBridgeQrCodeValue',
'HOST_BRIDGE_TEXT_MIME_TYPES',
];
+28 -4
View File
@@ -32,6 +32,7 @@ import {
HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES,
HOST_BRIDGE_TEXT_MIME_TYPES,
normalizeHostBridgeExportFileName,
normalizeHostBridgeImportFileName,
} from '../../../../packages/shared/src/contracts/hostBridge';
import { invalidRequest, ok } from './protocol';
@@ -239,6 +240,17 @@ function normalizeExportedImageFileName(
: `${fileName}.${extension}`;
}
function normalizeImportedImageFileName(
rawFileName: unknown,
mimeType: HostBridgeImageMimeType,
) {
const fileName = normalizeHostBridgeImportFileName(rawFileName);
const extension = imageFileExtension(mimeType);
return fileName.toLowerCase().endsWith(`.${extension}`)
? fileName
: `${fileName}.${extension}`;
}
export async function exportTextFile(
payload: unknown,
): Promise<FileExportTextResult> {
@@ -392,7 +404,7 @@ export async function importTextFile(): Promise<FileImportTextResult> {
throw invalidRequest('text file is required');
}
const fileName = normalizeHostBridgeExportFileName(
const fileName = normalizeHostBridgeImportFileName(
asset.name || 'genarrative-import.txt',
);
const mimeType = normalizeImportedTextMimeType(asset.mimeType, fileName);
@@ -446,7 +458,7 @@ export async function importDocumentFile(): Promise<FileImportDocumentResult> {
throw invalidRequest('document file is required');
}
const fileName = normalizeHostBridgeExportFileName(
const fileName = normalizeHostBridgeImportFileName(
asset.name || 'genarrative-import-document.txt',
);
const mimeType = normalizeImportedDocumentMimeType(asset.mimeType, fileName);
@@ -603,8 +615,9 @@ function imagePickerResultToImportPayload(
return {
action,
fileName: normalizeHostBridgeExportFileName(
fileName: normalizeImportedImageFileName(
asset.fileName || fallbackImportedImageFileName(mimeType),
mimeType,
),
base64Data,
mimeType,
@@ -692,6 +705,17 @@ function normalizeExportedAudioFileName(
: `${fileName}.${extension}`;
}
function normalizeImportedAudioFileName(
rawFileName: unknown,
mimeType: HostBridgeAudioMimeType,
) {
const fileName = normalizeHostBridgeImportFileName(rawFileName);
const extension = audioFileExtension(mimeType);
return fileName.toLowerCase().endsWith(`.${extension}`)
? fileName
: `${fileName}.${extension}`;
}
function normalizeImportedAudioMimeType(
value: unknown,
fileName: string,
@@ -796,7 +820,7 @@ export async function importAudioFile(): Promise<FileImportAudioResult> {
throw invalidRequest('audio file is required');
}
const fileName = normalizeHostBridgeExportFileName(
const fileName = normalizeHostBridgeImportFileName(
asset.name || 'genarrative-import-audio.webm',
);
const mimeType = normalizeImportedAudioMimeType(asset.mimeType, fileName);
@@ -2791,6 +2791,13 @@
- 影响范围:`apps/mobile-shell/scripts/check-config.mjs`、宿主壳能力统一协议文档和共享开发流程记忆。
- 验证方式:`npm run mobile-shell:typecheck``npm run check:native-shells``npm run check:encoding``git diff --check`
## 2026-06-20 移动壳导入文件名归一来源收口
- 背景:HostBridge 导出文件名已由共享 `normalizeHostBridgeExportFileName()` 清洗路径字符、非法字符、空白和长度;导入结果此前只在共享契约里 trim,Expo 移动壳却复用导出清洗器返回清洗后的导入文件名,导致 H5 复核与壳返回语义存在隐性差异。
- 决策:共享契约新增导出 `normalizeHostBridgeImportFileName()`,导入文本、文档、图片和音频结果都通过该函数清洗文件名;Expo 移动壳文件导入实现必须直接使用该导入专用函数,配置检查强制反查,避免继续混用导出函数或本地文件名规则。
- 影响范围:`packages/shared/src/contracts/hostBridge.ts``packages/shared/src/contracts/hostBridge.test.ts``apps/mobile-shell/src/host-bridge/files.ts``apps/mobile-shell/scripts/check-config.mjs` 和宿主壳能力统一协议文档。
- 验证方式:`npm run mobile-shell:test``npm run mobile-shell:typecheck``npm run test -- packages/shared/src/contracts/hostBridge.test.ts``npm run check:native-shells``npm run check:encoding``git diff --check`
## 2026-06-19 原生宿主二维码扫码超时单一来源
- 背景:二维码扫码属于真实相机交互,等待时间应长于普通宿主请求;此前 H5 `scanHostQrCode()` 直接手写 `timeoutMs: 60000`,会让扫码等待边界和共享 HostBridge 契约漂移。
File diff suppressed because one or more lines are too long
@@ -46,6 +46,7 @@ import {
normalizeHostBridgeHapticsImpactStyle,
normalizeHostBridgeImportAudioResult,
normalizeHostBridgeImportDocumentResult,
normalizeHostBridgeImportFileName,
normalizeHostBridgeImportImageResult,
normalizeHostBridgeImportTextResult,
normalizeHostBridgeLifecycleState,
@@ -423,17 +424,20 @@ describe('HostBridge shared contract helpers', () => {
});
test('归一化宿主文件导入结果', () => {
expect(normalizeHostBridgeImportFileName(' ../剧情:草稿?.md ')).toBe(
'剧情-草稿-.md',
);
expect(
normalizeHostBridgeImportTextResult({
action: 'selected',
fileName: ' 剧情.md ',
fileName: ' ../剧情:草稿?.md ',
content: '暖灯猫街',
mimeType: 'text/markdown',
bytes: 12,
}),
).toEqual({
action: 'selected',
fileName: '剧情.md',
fileName: '剧情-草稿-.md',
content: '暖灯猫街',
mimeType: 'text/markdown',
bytes: 12,
+2 -11
View File
@@ -721,17 +721,8 @@ function normalizeHostBridgeImportedBytes(rawBytes: unknown, maxBytes: number) {
return rawBytes;
}
function normalizeHostBridgeImportFileName(rawFileName: unknown) {
if (typeof rawFileName !== 'string') {
return null;
}
const fileName = rawFileName.trim();
if (!fileName || hasHostBridgeControlCharacter(fileName)) {
return null;
}
return fileName;
export function normalizeHostBridgeImportFileName(rawFileName: unknown) {
return normalizeHostBridgeExportFileName(rawFileName);
}
export function normalizeHostBridgeImportTextResult(