Files
Genarrative/src/services/image-editor/editorMediaAssetUploadClient.ts
T
kdletters b6c5fdeb30 修复生成完成提示与项目封面快照
生成完成提示展示后后台确认,避免刷新重复弹出

项目封面改为保存并读取静态快照资源

补充封面快照上传、选择和展示测试

同步外部生成与图片画布契约文档
2026-06-24 19:30:16 +08:00

148 lines
4.0 KiB
TypeScript

import { requestJson } from '../apiClient';
import { getSignedAssetReadUrl } from '../assetReadUrlService';
import {
EDITOR_REQUEST_RETRY_OPTIONS,
postEditorDirectUploadFile,
} from './editorRetryOptions';
export type EditorMediaAssetUploadType = 'image' | 'video' | 'audio';
type DirectUploadTicketResponse = {
upload: {
bucket: string;
host: string;
objectKey: string;
legacyPublicPath: string;
formFields: Record<string, string | null | undefined>;
};
};
type ConfirmAssetObjectResponse = {
assetObject: {
assetObjectId: string;
objectKey: string;
assetKind: string;
};
};
export type UploadedEditorMediaAsset = {
src: string;
objectKey: string;
assetObjectId: string;
legacyPublicPath: string;
};
export type EditorMediaAssetUploadOptions = {
assetKind?: string;
pathSegments?: string[];
entityId?: string;
metadata?: Record<string, string>;
};
const EDITOR_MEDIA_READ_EXPIRE_SECONDS = 60 * 60;
function resolveEditorMediaAssetKind(mediaType: EditorMediaAssetUploadType) {
if (mediaType === 'video') {
return 'editor_uploaded_video';
}
if (mediaType === 'audio') {
return 'editor_uploaded_audio';
}
return 'editor_uploaded_image';
}
function resolveFallbackFileName(mediaType: EditorMediaAssetUploadType) {
if (mediaType === 'video') {
return 'editor-video.mp4';
}
if (mediaType === 'audio') {
return 'editor-audio.mp3';
}
return 'editor-image.png';
}
function sanitizeEditorMediaFileName(
fileName: string,
mediaType: EditorMediaAssetUploadType,
) {
const trimmedName = fileName.trim();
return trimmedName || resolveFallbackFileName(mediaType);
}
export async function uploadEditorMediaAssetFile(
file: File,
mediaType: EditorMediaAssetUploadType,
options: EditorMediaAssetUploadOptions = {},
): Promise<UploadedEditorMediaAsset> {
const assetKind = options.assetKind ?? resolveEditorMediaAssetKind(mediaType);
const fileName = sanitizeEditorMediaFileName(file.name, mediaType);
const contentType = file.type.trim() || 'application/octet-stream';
const ticket = await requestJson<DirectUploadTicketResponse>(
'/api/assets/direct-upload-tickets',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
legacyPrefix: 'generated-character-drafts',
pathSegments: options.pathSegments ?? [
'editor',
'asset-library',
mediaType,
`${Date.now()}`,
],
fileName,
contentType,
access: 'private',
maxSizeBytes: file.size,
successActionStatus: 200,
metadata: {
asset_kind: assetKind,
editor_media_type: mediaType,
...options.metadata,
},
}),
},
'创建素材上传凭证失败',
{ retry: EDITOR_REQUEST_RETRY_OPTIONS },
);
await postEditorDirectUploadFile(ticket.upload, file, '上传素材失败');
const confirmed = await requestJson<ConfirmAssetObjectResponse>(
'/api/assets/objects/confirm',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bucket: ticket.upload.bucket,
objectKey: ticket.upload.objectKey,
contentType,
contentLength: file.size,
assetKind,
accessPolicy: 'private',
entityId: options.entityId ?? `editor-asset-library-${mediaType}`,
}),
},
'确认素材资产失败',
{ retry: EDITOR_REQUEST_RETRY_OPTIONS },
);
// 中文注释:素材库立即预览使用短期 signed URL;持久化仍记录 legacy path 和 objectKey。
const signedUrl = await getSignedAssetReadUrl(
{
objectKey: confirmed.assetObject.objectKey,
expireSeconds: EDITOR_MEDIA_READ_EXPIRE_SECONDS,
},
undefined,
{ bypassCache: true },
);
return {
src: signedUrl,
objectKey: confirmed.assetObject.objectKey,
assetObjectId: confirmed.assetObject.assetObjectId,
legacyPublicPath: ticket.upload.legacyPublicPath,
};
}