接入原生壳文本文件导入能力
新增 file.importText HostBridge 契约和 H5 facade 移动端通过 Expo DocumentPicker 读取受控文本文件 桌面端通过 Tauri 文件选择框读取受控文本文件 更新壳能力检查、测试、方案文档和共享决策记录
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
getHostRuntime,
|
||||
getNativeAppHostRuntime,
|
||||
importHostImageFile,
|
||||
importHostTextFile,
|
||||
isWechatMiniProgramWebViewRuntime,
|
||||
navigateHostNativePage,
|
||||
openHostExternalUrl,
|
||||
@@ -607,6 +608,14 @@ describe('hostBridge', () => {
|
||||
mimeType: 'image/png',
|
||||
bytes: 5,
|
||||
}
|
||||
: request.method === 'file.importText'
|
||||
? {
|
||||
action: 'selected',
|
||||
fileName: '剧情.md',
|
||||
content: '暖灯猫街',
|
||||
mimeType: 'text/markdown',
|
||||
bytes: 12,
|
||||
}
|
||||
: true,
|
||||
};
|
||||
});
|
||||
@@ -627,6 +636,7 @@ describe('hostBridge', () => {
|
||||
'app.setBadgeCount',
|
||||
'network.status',
|
||||
'share.open',
|
||||
'file.importText',
|
||||
'file.exportImage',
|
||||
'file.importImage',
|
||||
'file.imageDropped',
|
||||
@@ -697,6 +707,13 @@ describe('hostBridge', () => {
|
||||
mimeType: 'image/png',
|
||||
bytes: 5,
|
||||
});
|
||||
await expect(importHostTextFile()).resolves.toEqual({
|
||||
action: 'selected',
|
||||
fileName: '剧情.md',
|
||||
content: '暖灯猫街',
|
||||
mimeType: 'text/markdown',
|
||||
bytes: 12,
|
||||
});
|
||||
await expect(
|
||||
showHostLocalNotification({
|
||||
title: ' 生成完成 ',
|
||||
@@ -806,6 +823,12 @@ describe('hostBridge', () => {
|
||||
timeoutMs: 30000,
|
||||
}),
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
request: expect.objectContaining({
|
||||
method: 'file.importText',
|
||||
timeoutMs: 30000,
|
||||
}),
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
request: expect.objectContaining({
|
||||
method: 'notification.showLocal',
|
||||
@@ -882,6 +905,7 @@ describe('hostBridge', () => {
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
await expect(importHostImageFile()).resolves.toBe(false);
|
||||
await expect(importHostTextFile()).resolves.toBe(false);
|
||||
await expect(
|
||||
showHostLocalNotification({
|
||||
title: '生成完成',
|
||||
@@ -906,6 +930,7 @@ describe('hostBridge', () => {
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
await expect(importHostImageFile()).resolves.toBe(false);
|
||||
await expect(importHostTextFile()).resolves.toBe(false);
|
||||
await expect(
|
||||
showHostLocalNotification({
|
||||
title: '生成完成',
|
||||
@@ -1155,6 +1180,82 @@ describe('hostBridge', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('原生 App 宿主通过 HostBridge 导入文本文件', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (_command: string, args?: Record<string, unknown>) => {
|
||||
const request = (args as { request: { id: string } }).request;
|
||||
return {
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
id: request.id,
|
||||
ok: true,
|
||||
result: {
|
||||
action: 'selected',
|
||||
fileName: ' 剧情.md ',
|
||||
content: '暖灯猫街',
|
||||
mimeType: 'text/markdown',
|
||||
bytes: 12,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
nativeAppPath(['file.importText']),
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: {
|
||||
invoke: asTauriInvoke(invoke),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(importHostTextFile()).resolves.toEqual({
|
||||
action: 'selected',
|
||||
fileName: '剧情.md',
|
||||
content: '暖灯猫街',
|
||||
mimeType: 'text/markdown',
|
||||
bytes: 12,
|
||||
});
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
request: expect.objectContaining({
|
||||
method: 'file.importText',
|
||||
timeoutMs: 30000,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test('原生 App 宿主取消导入文本时回退为 false', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (_command: string, args?: Record<string, unknown>) => {
|
||||
const request = (args as { request: { id: string } }).request;
|
||||
return {
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
id: request.id,
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'cancelled',
|
||||
message: 'file import cancelled',
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
nativeAppPath(['file.importText']),
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: {
|
||||
invoke: asTauriInvoke(invoke),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(importHostTextFile()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test('原生 App 宿主取消导入图片时回退为 false', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (_command: string, args?: Record<string, unknown>) => {
|
||||
|
||||
@@ -7,11 +7,13 @@ import type {
|
||||
FileExportTextPayload,
|
||||
FileExportTextResult,
|
||||
FileImportImageResult,
|
||||
FileImportTextResult,
|
||||
HapticsImpactPayload,
|
||||
HostBridgeCapability,
|
||||
HostBridgeImageMimeType,
|
||||
HostBridgeMethod,
|
||||
HostBridgeRuntimeResult,
|
||||
HostBridgeTextMimeType,
|
||||
LocalNotificationPayload,
|
||||
NetworkStatusResult,
|
||||
OpenExternalUrlPayload,
|
||||
@@ -94,6 +96,8 @@ export type HostFileExportImageRequest = FileExportImagePayload;
|
||||
|
||||
export type HostFileImportImageResult = FileImportImageResult;
|
||||
|
||||
export type HostFileImportTextResult = FileImportTextResult;
|
||||
|
||||
export type HostClipboardWriteTextRequest = {
|
||||
text: string;
|
||||
};
|
||||
@@ -830,6 +834,61 @@ export async function importHostImageFile() {
|
||||
}
|
||||
}
|
||||
|
||||
const HOST_BRIDGE_TEXT_MIME_TYPES = new Set<HostBridgeTextMimeType>([
|
||||
'text/plain',
|
||||
'text/markdown',
|
||||
'text/csv',
|
||||
'application/json',
|
||||
]);
|
||||
|
||||
function normalizeHostTextImportResult(
|
||||
payload: FileImportTextResult | null | undefined,
|
||||
): FileImportTextResult | false {
|
||||
if (
|
||||
!payload ||
|
||||
typeof payload !== 'object' ||
|
||||
payload.action !== 'selected' ||
|
||||
!HOST_BRIDGE_TEXT_MIME_TYPES.has(payload.mimeType) ||
|
||||
typeof payload.fileName !== 'string' ||
|
||||
!payload.fileName.trim() ||
|
||||
typeof payload.content !== 'string' ||
|
||||
typeof payload.bytes !== 'number' ||
|
||||
!Number.isInteger(payload.bytes) ||
|
||||
payload.bytes <= 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'selected',
|
||||
fileName: payload.fileName.trim(),
|
||||
content: payload.content,
|
||||
mimeType: payload.mimeType,
|
||||
bytes: payload.bytes,
|
||||
};
|
||||
}
|
||||
|
||||
export async function importHostTextFile() {
|
||||
if (!canUseNativeHostCapability('file.importText')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return normalizeHostTextImportResult(
|
||||
await requestNativeAppHostBridge<FileImportTextResult>(
|
||||
'file.importText',
|
||||
undefined,
|
||||
{ timeoutMs: 30000 },
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (isUnsupportedHostBridgeError(error)) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestHostHapticsImpact(
|
||||
params: HostHapticsImpactRequest = {},
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user