import { requestJson } from '../apiClient'; import { getSignedAssetReadUrl } from '../assetReadUrlService'; type EditorSeedanceReferenceMediaType = 'video' | 'audio'; type DirectUploadTicketResponse = { upload: { bucket: string; host: string; objectKey: string; legacyPublicPath: string; formFields: Record; }; }; type ConfirmAssetObjectResponse = { assetObject: { assetObjectId: string; objectKey: string; assetKind: string; }; }; export type UploadedEditorSeedanceReference = { src: string; objectKey: string; assetObjectId: string; legacyPublicPath: string; }; const EDITOR_SEEDANCE_REFERENCE_READ_EXPIRE_SECONDS = 60 * 60; function resolveReferenceAssetKind(mediaType: EditorSeedanceReferenceMediaType) { return mediaType === 'video' ? 'editor_seedance_reference_video' : 'editor_seedance_reference_audio'; } function resolveReferenceEntityId(mediaType: EditorSeedanceReferenceMediaType) { return `editor-seedance-reference-${mediaType}`; } function sanitizeReferenceFileName(fileName: string, mediaType: EditorSeedanceReferenceMediaType) { const trimmedName = fileName.trim(); if (trimmedName) { return trimmedName; } return mediaType === 'video' ? 'seedance-reference.mp4' : 'seedance-reference.mp3'; } async function postDirectUploadFile( upload: DirectUploadTicketResponse['upload'], file: File, ) { const formData = new FormData(); Object.entries(upload.formFields).forEach(([key, value]) => { if (value !== null && value !== undefined) { formData.append(key, value); } }); formData.append('file', file, file.name); const response = await fetch(upload.host, { method: 'POST', body: formData, }); if (!response.ok) { throw new Error('上传参考素材失败'); } } export async function uploadEditorSeedanceReferenceFile( file: File, mediaType: EditorSeedanceReferenceMediaType, ): Promise { const assetKind = resolveReferenceAssetKind(mediaType); const fileName = sanitizeReferenceFileName(file.name, mediaType); const contentType = file.type.trim() || 'application/octet-stream'; const ticket = await requestJson( '/api/assets/direct-upload-tickets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ legacyPrefix: 'generated-character-drafts', pathSegments: ['editor', 'seedance-references', mediaType, `${Date.now()}`], fileName, contentType, access: 'private', maxSizeBytes: file.size, successActionStatus: 200, metadata: { asset_kind: assetKind, editor_seedance_reference_type: mediaType, }, }), }, '创建参考素材上传凭证失败', ); await postDirectUploadFile(ticket.upload, file); const confirmed = await requestJson( '/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: resolveReferenceEntityId(mediaType), }), }, '确认参考素材资产失败', ); // 中文注释:返回短期 signed URL 供前端立即预览;生成请求最终优先提交 objectKey,由后端统一重新签名。 const signedUrl = await getSignedAssetReadUrl( { objectKey: confirmed.assetObject.objectKey, expireSeconds: EDITOR_SEEDANCE_REFERENCE_READ_EXPIRE_SECONDS, }, undefined, { bypassCache: true }, ); return { src: signedUrl, objectKey: confirmed.assetObject.objectKey, assetObjectId: confirmed.assetObject.assetObjectId, legacyPublicPath: ticket.upload.legacyPublicPath, }; }