完善 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

@@ -1,4 +1,6 @@
import type {
DesignAsset,
DesignAssetUploadInput,
DesignCapabilities,
DesignConfirmGenerationInput,
DesignCreateWorkspaceInput,
@@ -41,6 +43,7 @@ export interface DesignWorkspaceModule {
submitMessage(input: DesignSubmitMessageInput): Promise<DesignWorkspace>;
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignWorkspace>;
listTasks(workspaceId: string): Promise<DesignGenerationTask[]>;
uploadAsset?(input: DesignAssetUploadInput): Promise<DesignAsset>;
openWorkspaceEvents?(
input: DesignWorkspaceEventSubscriptionInput,
): Promise<DesignWorkspaceEventSubscription>;

View File

@@ -1,5 +1,6 @@
import type {
DesignAsset,
DesignAssetUploadInput,
DesignAssistantDeltaEvent,
DesignBrief,
DesignCapabilities,
@@ -266,22 +267,26 @@ function mapTask(task: ServerTask): DesignGenerationTask {
quoteId: task.quote_id,
quotedDesignPoints: task.quoted_design_points,
failureCode: task.failure_code,
resultAssets: task.result_assets.map((asset) => ({
assetId: asset.asset_id,
workspaceId: task.workspace_id,
mediaType: asset.media_type,
mimeType: asset.mime_type,
width: asset.width,
height: asset.height,
durationMilliseconds: asset.duration_milliseconds,
createdAt: asset.created_at,
contentPath: designAssetContentPath(task.workspace_id, asset.asset_id),
})),
resultAssets: task.result_assets.map((asset) => mapAsset(task.workspace_id, asset)),
createdAt: task.created_at,
updatedAt: task.updated_at,
};
}
function mapAsset(workspaceId: string, asset: ServerAsset): DesignAsset {
return {
assetId: asset.asset_id,
workspaceId,
mediaType: asset.media_type,
mimeType: asset.mime_type,
width: asset.width,
height: asset.height,
durationMilliseconds: asset.duration_milliseconds,
createdAt: asset.created_at,
contentPath: designAssetContentPath(workspaceId, asset.asset_id),
};
}
function createClientId(prefix: string): string {
const id = globalThis.crypto?.randomUUID?.()
?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
@@ -913,6 +918,20 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
return tasks.map(mapTask);
}
async uploadAsset(input: DesignAssetUploadInput): Promise<DesignAsset> {
const form = new FormData();
form.set(
'file',
new Blob([Uint8Array.from(input.bytes)], { type: input.mimeType }),
input.fileName,
);
const asset = await this.requestJson<ServerAsset>(
`/api/design/workspaces/${encodeURIComponent(input.workspaceId)}/assets`,
{ method: 'POST', body: form },
);
return mapAsset(input.workspaceId, asset);
}
async openWorkspaceEvents(
input: DesignWorkspaceEventSubscriptionInput,
): Promise<DesignWorkspaceEventSubscription> {
@@ -1141,7 +1160,9 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
...init,
headers: {
Accept: 'application/json',
...(init.body ? { 'Content-Type': 'application/json' } : {}),
...(init.body && !(init.body instanceof FormData)
? { 'Content-Type': 'application/json' }
: {}),
...(init.headers ?? {}),
},
});