收紧编辑器媒体引用契约

禁止当前画布与外部编辑器接口接收 Data URL 和 Blob URL
生成请求统一先上传 OSS 并使用稳定资源引用
角色动画和视频签名前校验资源归属并支持资源与素材 ID
同步前后端 256KB 引用限制及相关文档测试
This commit is contained in:
2026-07-17 16:44:01 +08:00
parent 93b27a2307
commit 18af49c27f
18 changed files with 697 additions and 393 deletions
@@ -411,7 +411,6 @@ export const SEEDANCE_VIDEO_REFERENCE_LIMITS = {
image: 9,
video: 3,
audio: 3,
requestBytes: 64 * 1024 * 1024,
} as const;
export const QUICK_EDIT_REFERENCE_LIMIT = 8;
export const CHARACTER_ANIMATION_ACTION_PROMPTS = [
@@ -91,11 +91,6 @@ export function resolveImageReferenceSubmissionSource(reference: {
return reference.objectKey?.trim() || reference.src;
}
function estimateReferenceRequestBytes(references: Array<{ src: string }>) {
// 中文注释:JSON 包装字段还有少量额外字节;这里按 src 字符串长度估算,前后端都会再兜底校验 64MB。
return references.reduce((sum, reference) => sum + reference.src.length, 0);
}
function buildSeedanceVideoReferenceInput(
references: NonNullable<GenerateDialogState['generationReferences']>,
) {
@@ -129,13 +124,6 @@ function buildSeedanceVideoReferenceInput(
) {
throw new Error('参考音频必须搭配参考图片或参考视频');
}
if (
estimateReferenceRequestBytes(references) >
SEEDANCE_VIDEO_REFERENCE_LIMITS.requestBytes
) {
throw new Error('参考文件总请求体不能超过 64MB');
}
return {
referenceImageSrcs: imageReferences.map(resolveReferenceSource),
referenceVideoSrcs: videoReferences.map(resolveReferenceSource),
@@ -537,6 +537,31 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(firstOptions.pathSegments).not.toEqual(secondOptions.pathSegments);
});
it('uploads image references without an object reference before generation', async () => {
resolveEditorImageReferenceDataUrlMock.mockResolvedValueOnce(
'data:image/png;base64,ZXhhbXBsZQ==',
);
const result = await resolveEditorGenerationMediaReference(
{ src: '/creation-type-references/example.webp' },
'image',
'project-1',
);
expect(resolveEditorImageReferenceDataUrlMock).toHaveBeenCalledWith(
'/creation-type-references/example.webp',
);
expect(uploadEditorMediaAssetFileMock).toHaveBeenCalledWith(
expect.any(File),
'image',
expect.objectContaining({
assetKind: 'editor_generation_reference_image',
}),
);
expect(result).toBe(
'generated-character-drafts/editor/generation-references/reference.png',
);
});
it('submits quick edits and updates the source layer directly', async () => {
editEditorImageMock.mockResolvedValueOnce(
createGenerated({
@@ -218,20 +218,27 @@ export async function resolveEditorGenerationMediaReference(
projectId?: string | null,
options: EditorGenerationMediaReferenceOptions = {},
) {
const resourceId = reference.resourceId?.trim();
const hasRegisteredReference =
options.allowRegisteredIds !== false &&
((resourceId &&
!resourceId.startsWith('local-resource-') &&
!resourceId.startsWith('generation-dialog:')) ||
reference.sourceAssetId?.trim());
const source = resolveEditorGenerationMediaReferenceSource(
reference,
options.allowRegisteredIds !== false,
);
const inlineSource = /^(?:data|blob):/iu.test(source);
const remoteImageSource =
options.requireImageObjectReference === true &&
const imageSourceRequiresUpload =
options.requireImageObjectReference !== false &&
mediaType === 'image' &&
!reference.objectKey?.trim() &&
/^(?:https?:|\/api\/)/iu.test(source);
if (!inlineSource && !remoteImageSource) {
!hasRegisteredReference;
if (!inlineSource && !imageSourceRequiresUpload) {
return source;
}
const uploadSource = remoteImageSource
const uploadSource = imageSourceRequiresUpload && !inlineSource
? await resolveEditorImageReferenceDataUrl(source)
: source;
return uploadEditorGenerationInlineMediaSource(