接入桌面壳文本文件导出能力

新增 HostBridge file.exportText 契约、文件名清洗和 H5 导出入口

Tauri 桌面壳通过受控 host_bridge_request 打开保存对话框并写入文本文件

Expo 移动壳对未接入的文件导出能力明确返回 unsupported

更新宿主壳方案、统一协议和项目共享决策记录
This commit is contained in:
2026-06-17 23:02:01 +08:00
parent 6f19e1c3ba
commit d67f9d5725
14 changed files with 540 additions and 12 deletions

View File

@@ -4,6 +4,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import {
canUseHostShareGrid,
exportHostTextFile,
getHostRuntime,
getNativeAppHostRuntime,
isWechatMiniProgramWebViewRuntime,
@@ -358,4 +359,91 @@ describe('hostBridge', () => {
}),
).resolves.toBe(false);
});
test('普通浏览器不处理宿主文本导出', async () => {
await expect(
exportHostTextFile({
fileName: '作品记录.txt',
content: 'content',
}),
).resolves.toBe(false);
});
test('原生 App 宿主通过 HostBridge 导出文本文件', async () => {
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: { id: string; method: string } })
.request;
return {
bridge: 'GenarrativeHostBridge',
version: 1,
id: request.id,
ok: true,
result: {
action: 'saved',
fileName: '作品记录.txt',
bytes: 7,
},
};
},
);
window.history.replaceState(null, '', '/?clientRuntime=native_app');
window.__TAURI__ = {
core: {
invoke: asTauriInvoke(invoke),
},
};
await expect(
exportHostTextFile({
fileName: '作品记录.txt',
content: 'content',
}),
).resolves.toEqual({
action: 'saved',
fileName: '作品记录.txt',
bytes: 7,
});
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
request: expect.objectContaining({
method: 'file.exportText',
payload: {
fileName: '作品记录.txt',
content: 'content',
},
timeoutMs: 30000,
}),
});
});
test('原生 App 宿主不支持文本导出时回退 H5', async () => {
window.history.replaceState(null, '', '/?clientRuntime=native_app');
window.__TAURI__ = {
core: {
invoke: asTauriInvoke(
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: 'unsupported_method',
message: 'unsupported_method',
},
};
}),
),
},
};
await expect(
exportHostTextFile({
fileName: '作品记录.txt',
content: 'content',
}),
).resolves.toBe(false);
});
});

View File

@@ -1,4 +1,6 @@
import type {
FileExportTextPayload,
FileExportTextResult,
HostBridgeMethod,
HostBridgeRuntimeResult,
} from '../../../packages/shared/src/contracts/hostBridge';
@@ -57,6 +59,8 @@ export type HostShareGridRequest = {
publicWorkCode: string;
};
export type HostFileExportTextRequest = FileExportTextPayload;
function isUnsupportedHostBridgeError(error: unknown) {
return (
error instanceof Error &&
@@ -444,3 +448,24 @@ export async function getNativeAppHostRuntime() {
'host.getRuntime',
);
}
export async function exportHostTextFile(
params: HostFileExportTextRequest,
) {
if (getHostRuntime().kind !== 'native_app') {
return false;
}
try {
return await requestNativeAppHostBridge<FileExportTextResult>(
'file.exportText',
params,
{ timeoutMs: 30000 },
);
} catch (error) {
if (isUnsupportedHostBridgeError(error)) {
return false;
}
throw error;
}
}