完善 AI 设计资产预览与视频首帧选择

需求:生成图片需要支持大图查看和本地下载;制作视频时需要从当前项目作品选择首帧,或上传本地图片。

实现:新增首帧选择弹窗、JPEG/PNG/WebP 有界上传、附件 Asset ID 透传、Main 到服务端 multipart 转发,并保留上传失败重试与私有图片下载链路。

验证:相关 57 项单测、TypeScript 类型检查、目标 ESLint 和 Vite 生产构建全部通过。
This commit is contained in:
2026-08-06 11:31:47 +08:00
parent 2a3c9850d1
commit 15b17775cb
11 changed files with 1149 additions and 31 deletions

View File

@@ -13,7 +13,9 @@ import {
ImageWorkspaceApiError,
openImageWorkspaceTaskEvents,
resolveImageWorkspaceAssetUrl,
saveImageWorkspaceAsset,
sendImageWorkspaceMessage,
uploadImageWorkspaceAsset,
} from '@/lib/image-workspace';
vi.mock('@/lib/host-api', () => ({
@@ -99,6 +101,52 @@ describe('AI design renderer API boundary', () => {
expect(String(init?.body)).not.toMatch(/model|resolution|outputCount/);
});
it('sends the selected first-frame Asset id with the conversation turn', async () => {
await sendImageWorkspaceMessage(
'workspace-one',
5,
'使用作品图片作为视频首帧',
'turn-first-frame',
['asset-first-frame'],
);
const [, init] = hostApiFetchMock.mock.calls[0];
expect(JSON.parse(String(init?.body))).toMatchObject({
clientTurnId: 'turn-first-frame',
expectedTurnRevision: 5,
attachmentAssetIds: ['asset-first-frame'],
});
});
it('uploads a supported local image through the authenticated Main boundary', async () => {
const asset = {
assetId: 'asset-uploaded',
workspaceId: 'workspace-one',
mediaType: 'image' as const,
mimeType: 'image/png',
width: 1200,
height: 800,
durationMilliseconds: null,
createdAt: '2026-08-06T10:00:00Z',
contentPath: '/api/works/image-workspace/workspaces/workspace-one/assets/asset-uploaded/content',
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, status: 200, data: asset });
const file = new File([new Uint8Array([1, 2, 3])], 'poster.webp', {
type: 'image/webp',
});
await expect(uploadImageWorkspaceAsset('workspace-one', file)).resolves.toEqual(asset);
const [path, init] = hostApiFetchMock.mock.calls[0];
expect(path).toBe('/api/works/image-workspace/workspaces/workspace-one/assets');
expect(init).toMatchObject({ method: 'POST' });
expect(JSON.parse(String(init?.body))).toEqual({
fileName: 'poster.webp',
mimeType: 'image/webp',
dataBase64: 'AQID',
});
});
it('confirms a Quote through a separate structured action route', async () => {
await confirmImageWorkspaceGeneration('workspace-one', 5, 'quote/one');
@@ -132,4 +180,35 @@ describe('AI design renderer API boundary', () => {
'http://127.0.0.1:13210/api/works/image-workspace/assets/one?token=host-token',
);
});
it('asks Main to save a private image asset without exposing its cloud URL', async () => {
hostApiFetchMock.mockResolvedValueOnce({
success: true,
status: 200,
data: { status: 'saved' },
});
await expect(saveImageWorkspaceAsset({
assetId: 'asset/one',
workspaceId: 'workspace/one',
mediaType: 'image',
mimeType: 'image/png',
width: 1024,
height: 1024,
durationMilliseconds: null,
createdAt: '2026-08-05T12:00:00Z',
contentPath: '/api/works/image-workspace/private/content',
})).resolves.toEqual({ status: 'saved' });
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/image-workspace/workspaces/workspace%2Fone/assets/asset%2Fone/download',
{
method: 'POST',
body: JSON.stringify({
defaultFileName: 'Makelore-AI-Design-asset-one.png',
}),
},
);
expect(JSON.stringify(hostApiFetchMock.mock.calls[0])).not.toContain('private/content');
});
});