From 991bb6dd30f21ac67b4ce065eb759cd82d559662 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Mon, 25 May 2026 14:01:28 +0800 Subject: [PATCH 1/5] fix(generation): reject empty @ references --- backend/apps/generation/views.py | 23 +++++++++++++++++++++-- backend/tests/test_ark_prompt_format.py | 10 ++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index 36ec146..6fe927d 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -1,4 +1,5 @@ import logging +import re from rest_framework import status from rest_framework.decorators import api_view, permission_classes, parser_classes @@ -90,6 +91,14 @@ def _format_prompt_for_ark(prompt, label_placeholders): return result +_ORPHAN_MATERIAL_MENTION_RE = re.compile(r'@(?:图片|视频|音频|素材)\S*') + + +def _has_orphan_material_mention(prompt, references): + """Detect a material @mention in prompt when no reference payload arrived.""" + return bool(prompt and not references and _ORPHAN_MATERIAL_MENTION_RE.search(prompt)) + + def _get_token_price(config, model, has_video_ref, resolution): """根据模型、是否含视频、分辨率选择单价。 @@ -226,9 +235,21 @@ def video_generate_view(request): aspect_ratio = serializer.validated_data['aspect_ratio'] # serializer 已设 default='720p' + choices 约束,validated_data 必有合法值 resolution = serializer.validated_data['resolution'] + references = serializer.validated_data.get('references', []) search_mode = request.data.get('search_mode', 'off') seed = _safe_int(request.data.get('seed', -1), -1) + if _has_orphan_material_mention(prompt, references): + logger.warning( + 'Blocked generate with material @mention but empty references (user=%s prompt=%s)', + user.id, + prompt[:120], + ) + return Response({ + 'error': 'missing_references', + 'message': '@对应的内容为空', + }, status=status.HTTP_400_BAD_REQUEST) + # 1080P 仅 Seedance 2.0 支持,Fast 不支持 if resolution == '1080p' and model == 'seedance_2.0_fast': return Response({ @@ -238,7 +259,6 @@ def video_generate_view(request): # ── 预估 token 和费用 ── config = QuotaConfig.objects.get_or_create(pk=1)[0] - references = request.data.get('references', []) w, h = get_resolution(aspect_ratio, resolution) has_video_ref = _has_video_reference(references) input_video_dur = _sum_video_duration(references) if has_video_ref else 0 @@ -335,7 +355,6 @@ def video_generate_view(request): }, status=status.HTTP_429_TOO_MANY_REQUESTS) # 构建参考素材 - references = request.data.get('references', []) # 火山限制最多 9 张参考图片 image_count = sum(1 for r in references if r.get('type', 'image') == 'image') if image_count > 9: diff --git a/backend/tests/test_ark_prompt_format.py b/backend/tests/test_ark_prompt_format.py index 6922cfe..dcc6e6f 100644 --- a/backend/tests/test_ark_prompt_format.py +++ b/backend/tests/test_ark_prompt_format.py @@ -146,6 +146,16 @@ class TestVideoGenerateArkPrompt(TestCase): 'references': references, }, format='json') + @mock.patch('apps.generation.views.create_task') + def test_reject_material_mention_without_references(self, mock_create_task): + """事故回归:prompt 含 @图片 但 references 为空时,不能创建 refs=0 脏任务。""" + resp = self._post_generate('@图片1 走过来', []) + self.assertEqual(resp.status_code, 400, resp.content) + self.assertEqual(resp.json().get('error'), 'missing_references') + self.assertEqual(resp.json().get('message'), '@对应的内容为空') + self.assertFalse(mock_create_task.called) + self.assertFalse(GenerationRecord.objects.filter(user=self.user).exists()) + @mock.patch('apps.generation.tasks.poll_video_task') @mock.patch('apps.generation.views.create_task') def test_view_converts_prompt_for_local_assets(self, mock_create_task, mock_poll): From 540f8ef4bb7f31beeb3bebeee4c0e53ea998592c Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Mon, 25 May 2026 14:07:29 +0800 Subject: [PATCH 2/5] fix(web): validate empty @ references before submit --- web/src/store/generation.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/src/store/generation.ts b/web/src/store/generation.ts index 5a62325..c415a42 100644 --- a/web/src/store/generation.ts +++ b/web/src/store/generation.ts @@ -54,6 +54,15 @@ function mapProgress(backendStatus: string): number { return 50; } +const ORPHAN_MATERIAL_MENTION_RE = /@(?:图片|视频|音频|素材)\S*/; + +function hasOrphanMaterialMention(input: ReturnType): boolean { + if (input.mode !== 'universal') return false; + const hasDirectRefs = input.references.length > 0; + const hasAssetMentions = (input.assetMentions || []).length > 0; + return !hasDirectRefs && !hasAssetMentions && ORPHAN_MATERIAL_MENTION_RE.test(input.prompt); +} + /** Check if a URL is an asset library reference (case-insensitive protocol). */ function isAssetUrl(url: string): boolean { return url.startsWith('asset://') || url.startsWith('Asset://'); @@ -314,6 +323,10 @@ export const useGenerationStore = create((set, get) => ({ addTask: async () => { const input = useInputBarStore.getState(); + if (hasOrphanMaterialMention(input)) { + showToast('@对应的内容为空'); + return null; + } if (!input.canSubmit()) return null; // Collect files to upload (or existing TOS URLs for regeneration) From ac12c0fbd491664673be9fa5a71fdb63ed93fb28 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Mon, 25 May 2026 14:09:39 +0800 Subject: [PATCH 3/5] fix(generation): include missing @ mention in error --- backend/apps/generation/views.py | 16 ++++++++++------ backend/tests/test_ark_prompt_format.py | 2 +- web/src/store/generation.ts | 14 ++++++++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index 6fe927d..bd320cb 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -91,12 +91,15 @@ def _format_prompt_for_ark(prompt, label_placeholders): return result -_ORPHAN_MATERIAL_MENTION_RE = re.compile(r'@(?:图片|视频|音频|素材)\S*') +_ORPHAN_MATERIAL_MENTION_RE = re.compile(r'@(?:图片|视频|音频|素材)[^\s,。!?、;:,.!?;:))]*') -def _has_orphan_material_mention(prompt, references): - """Detect a material @mention in prompt when no reference payload arrived.""" - return bool(prompt and not references and _ORPHAN_MATERIAL_MENTION_RE.search(prompt)) +def _find_orphan_material_mention(prompt, references): + """Return a material @mention in prompt when no reference payload arrived.""" + if not prompt or references: + return None + match = _ORPHAN_MATERIAL_MENTION_RE.search(prompt) + return match.group(0) if match else None def _get_token_price(config, model, has_video_ref, resolution): @@ -239,7 +242,8 @@ def video_generate_view(request): search_mode = request.data.get('search_mode', 'off') seed = _safe_int(request.data.get('seed', -1), -1) - if _has_orphan_material_mention(prompt, references): + orphan_mention = _find_orphan_material_mention(prompt, references) + if orphan_mention: logger.warning( 'Blocked generate with material @mention but empty references (user=%s prompt=%s)', user.id, @@ -247,7 +251,7 @@ def video_generate_view(request): ) return Response({ 'error': 'missing_references', - 'message': '@对应的内容为空', + 'message': f'{orphan_mention} 对应的内容为空', }, status=status.HTTP_400_BAD_REQUEST) # 1080P 仅 Seedance 2.0 支持,Fast 不支持 diff --git a/backend/tests/test_ark_prompt_format.py b/backend/tests/test_ark_prompt_format.py index dcc6e6f..6e5b536 100644 --- a/backend/tests/test_ark_prompt_format.py +++ b/backend/tests/test_ark_prompt_format.py @@ -152,7 +152,7 @@ class TestVideoGenerateArkPrompt(TestCase): resp = self._post_generate('@图片1 走过来', []) self.assertEqual(resp.status_code, 400, resp.content) self.assertEqual(resp.json().get('error'), 'missing_references') - self.assertEqual(resp.json().get('message'), '@对应的内容为空') + self.assertEqual(resp.json().get('message'), '@图片1 对应的内容为空') self.assertFalse(mock_create_task.called) self.assertFalse(GenerationRecord.objects.filter(user=self.user).exists()) diff --git a/web/src/store/generation.ts b/web/src/store/generation.ts index c415a42..69bb2b0 100644 --- a/web/src/store/generation.ts +++ b/web/src/store/generation.ts @@ -54,13 +54,14 @@ function mapProgress(backendStatus: string): number { return 50; } -const ORPHAN_MATERIAL_MENTION_RE = /@(?:图片|视频|音频|素材)\S*/; +const ORPHAN_MATERIAL_MENTION_RE = /@(?:图片|视频|音频|素材)[^\s,。!?、;:,.!?;:))]*/; -function hasOrphanMaterialMention(input: ReturnType): boolean { - if (input.mode !== 'universal') return false; +function findOrphanMaterialMention(input: ReturnType): string | null { + if (input.mode !== 'universal') return null; const hasDirectRefs = input.references.length > 0; const hasAssetMentions = (input.assetMentions || []).length > 0; - return !hasDirectRefs && !hasAssetMentions && ORPHAN_MATERIAL_MENTION_RE.test(input.prompt); + if (hasDirectRefs || hasAssetMentions) return null; + return input.prompt.match(ORPHAN_MATERIAL_MENTION_RE)?.[0] || null; } /** Check if a URL is an asset library reference (case-insensitive protocol). */ @@ -323,8 +324,9 @@ export const useGenerationStore = create((set, get) => ({ addTask: async () => { const input = useInputBarStore.getState(); - if (hasOrphanMaterialMention(input)) { - showToast('@对应的内容为空'); + const orphanMention = findOrphanMaterialMention(input); + if (orphanMention) { + showToast(`${orphanMention} 对应的内容为空`); return null; } if (!input.canSubmit()) return null; From a08234e54ba5f1f66dad496d60fec525bbf6a14a Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Mon, 25 May 2026 15:47:18 +0800 Subject: [PATCH 4/5] fix(web): require uploaded material urls before generation --- web/src/components/PromptInput.tsx | 8 +- web/src/components/UniversalUpload.module.css | 10 ++ web/src/components/UniversalUpload.tsx | 33 +++--- web/src/store/generation.ts | 66 +++++++++++ web/src/store/inputBar.ts | 103 ++++++++++++++---- 5 files changed, 183 insertions(+), 37 deletions(-) diff --git a/web/src/components/PromptInput.tsx b/web/src/components/PromptInput.tsx index 1fa225a..ecc0f21 100644 --- a/web/src/components/PromptInput.tsx +++ b/web/src/components/PromptInput.tsx @@ -715,12 +715,16 @@ export function PromptInput() { }} >
- {ref.type === 'video' ? ( + {ref.uploading ? ( + ... + ) : ref.type === 'video' && ref.previewUrl ? (
{ref.label} diff --git a/web/src/components/UniversalUpload.module.css b/web/src/components/UniversalUpload.module.css index 09a1ba0..c3d69cc 100644 --- a/web/src/components/UniversalUpload.module.css +++ b/web/src/components/UniversalUpload.module.css @@ -282,6 +282,16 @@ color: var(--color-text-secondary); } +.uploadPlaceholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-overlay-soft); + color: var(--color-text-secondary); +} + /* Upload status overlay */ .uploadOverlay { position: absolute; diff --git a/web/src/components/UniversalUpload.tsx b/web/src/components/UniversalUpload.tsx index 5e23475..6939657 100644 --- a/web/src/components/UniversalUpload.tsx +++ b/web/src/components/UniversalUpload.tsx @@ -142,28 +142,29 @@ export function UniversalUpload() { }} >
- {ref.type === 'video' ? ( + {ref.uploading ? ( +
+ +
+ ) : ref.uploadError ? ( +
{ e.stopPropagation(); retryUpload(ref.id); }} + title="点击重试" + > + +
+ ) : ref.type === 'video' && ref.previewUrl ? (