diff --git a/core/backend/apps/ai/services.py b/core/backend/apps/ai/services.py index 8087724..525820f 100644 --- a/core/backend/apps/ai/services.py +++ b/core/backend/apps/ai/services.py @@ -2775,7 +2775,7 @@ def _reap_stale_standalone_image_tasks(*, team) -> None: continue -def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", count: int = 1, product_id: str | None = None, reference_product: bool = False, model_id: str | None = None, model_entity_id: str | None = None, ratio: str | None = None, image_model: str | None = None, conversation=None, reference_image_ids: list[str] | None = None, platform_id: str | None = None, batch_id: str | None = None, tryon_prompt_v2_override: bool = False, tryon_ab: dict | None = None, dispatch: bool = True) -> list[AITask]: +def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", count: int = 1, product_id: str | None = None, reference_product: bool = False, model_id: str | None = None, model_entity_id: str | None = None, ratio: str | None = None, image_model: str | None = None, conversation=None, reference_image_ids: list[str] | None = None, platform_id: str | None = None, batch_id: str | None = None, retry_of_task_id: str | None = None, tryon_prompt_v2_override: bool = False, tryon_ab: dict | None = None, dispatch: bool = True) -> list[AITask]: """独立生图(图片创作 / 模特上身图 / 平台套图)改为**异步**:本函数在 Web 请求里只做「建任务 + 预留额度」这种秒级的活,真正 ~30s 的 ARK 出图交给 Celery worker(generate_standalone_image_task)。 @@ -2812,6 +2812,18 @@ def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", c batch_id = None if not is_append: batch_id = str(uuid.uuid4()) + retry_of_task_id = None + elif retry_of_task_id: + retry_of_task = AITask.objects.filter( + id=retry_of_task_id, + team=team, + project__isnull=True, + is_deleted=False, + purged_at__isnull=True, + request_payload__batch_id=batch_id, + status__in=(AITask.Status.FAILED, AITask.Status.CANCELLED), + ).first() + retry_of_task_id = str(retry_of_task.id) if retry_of_task else None # 平台套图:规范化平台 id(前端 dy/tb… → canonical),用于注入平台版式块(优化版);非 cover 模式忽略。 platform_key = str(platform_id or "").strip() if mode == "cover" else "" platform_name = _PLATFORM_NAMES.get(platform_key, "") @@ -2884,6 +2896,8 @@ def enqueue_standalone_images(*, team, user, prompt: str, mode: str = "image", c # 只在重跑/补图时落键(不落 False):workbench 用 KeyTextTransform 抽文本,"false" 字符串也是真值,会误判 if is_append: request_payload["batch_append"] = True + if retry_of_task_id: + request_payload["retry_of_task_id"] = retry_of_task_id if quote.meta.get("rate"): request_payload["points_per_yuan_snapshot"] = quote.meta["rate"] task = AITask.objects.create( diff --git a/core/backend/apps/ai/tests.py b/core/backend/apps/ai/tests.py index 4d2e2d7..28b308e 100644 --- a/core/backend/apps/ai/tests.py +++ b/core/backend/apps/ai/tests.py @@ -1215,10 +1215,13 @@ class ImageConversationTests(TestCase): conv_id = first.json()["conversation_id"] batch_id = first.json()["batch_id"] self.assertTrue(batch_id) + failed_task = AITask.objects.filter(conversation_id=conv_id).order_by("created_at").first() + failed_task.status = AITask.Status.FAILED + failed_task.save(update_fields=["status"]) # 重跑一张:带原对话 + 原 batch_id → 归回原批次 second = self.client.post( "/api/ai/generate-image/", - {"prompt": "一只猫", "mode": "image", "count": 1, "conversation_id": conv_id, "batch_id": batch_id}, + {"prompt": "一只猫", "mode": "image", "count": 1, "conversation_id": conv_id, "batch_id": batch_id, "retry_of_task_id": str(failed_task.id)}, format="json", ) self.assertEqual(second.status_code, 202, second.content) @@ -1227,6 +1230,7 @@ class ImageConversationTests(TestCase): self.assertEqual(len(tasks), 3) self.assertTrue(all(t["batch_id"] == batch_id for t in tasks)) self.assertEqual([t["rerun"] for t in tasks], [False, False, True]) + self.assertEqual(tasks[-1]["retry_of_task_id"], str(failed_task.id)) # 非法 batch_id(不是 UUID):不沿用,铸新批次,不打 append 标记 third = self.client.post( "/api/ai/generate-image/", diff --git a/core/backend/apps/ai/views.py b/core/backend/apps/ai/views.py index 47e110b..abda627 100644 --- a/core/backend/apps/ai/views.py +++ b/core/backend/apps/ai/views.py @@ -48,6 +48,7 @@ class GenerateImageView(APIView): conversation_id = str(request.data.get("conversation_id") or "").strip() or None # 重跑/补图:前端带原批次 batch_id → enqueue 沿用(UUID 校验),记录归回原批次不裂新卡 batch_id = str(request.data.get("batch_id") or "").strip() or None + retry_of_task_id = str(request.data.get("retry_of_task_id") or "").strip() or None # 用户在图片创作里上传的参考图(已先传成 Asset),按 id 列表带入 → 生成时作多图参考(image_edit) raw_refs = request.data.get("reference_image_ids") or [] if isinstance(raw_refs, str): @@ -69,7 +70,7 @@ class GenerateImageView(APIView): title=(prompt[:24] or "默认创作"), ) try: - tasks = enqueue_standalone_images(team=team, user=request.user, prompt=prompt, mode=mode, count=count, product_id=product_id, reference_product=reference_product, model_id=model_id, model_entity_id=model_entity_id, ratio=ratio, image_model=image_model, conversation=conversation, reference_image_ids=reference_image_ids, platform_id=platform_id, batch_id=batch_id) + tasks = enqueue_standalone_images(team=team, user=request.user, prompt=prompt, mode=mode, count=count, product_id=product_id, reference_product=reference_product, model_id=model_id, model_entity_id=model_entity_id, ratio=ratio, image_model=image_model, conversation=conversation, reference_image_ids=reference_image_ids, platform_id=platform_id, batch_id=batch_id, retry_of_task_id=retry_of_task_id) except ValueError as exc: # 无可用模型 / 余额不足等,立即反馈 internal_kind = "user_credit_insufficient" if str(exc).strip().lower() == "insufficient credit" else "" public_error = classify_generation_error( @@ -365,6 +366,7 @@ class AITaskViewSet(TeamScopedViewSetMixin, ReadOnlyModelViewSet): "model_entity_id": t.rp_model_entity_id or "", "platform_id": t.rp_platform_id or "", "rerun": bool(t.rp_batch_append), + "retry_of_task_id": str((t.request_payload or {}).get("retry_of_task_id") or ""), "created_at": t.created_at, # 软删的图不再出现在工作台记录里(R109:删除资产库图片 → 任务记录联动) "assets": AssetSerializer( @@ -499,6 +501,7 @@ class ImageConversationViewSet(TeamScopedViewSetMixin, ModelViewSet): "ratio": (t.request_payload or {}).get("ratio") or "", # 重跑/补图任务:不计入批次「应出张数」,前端据此正确渲染失败格数量 "rerun": bool((t.request_payload or {}).get("batch_append")), + "retry_of_task_id": str((t.request_payload or {}).get("retry_of_task_id") or ""), "reference_images": resolve_refs((t.request_payload or {}).get("reference_image_ids")), "created_at": t.created_at, "assets": AssetSerializer( diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx index 1daa197..05042d8 100644 --- a/core/frontend/src/App.tsx +++ b/core/frontend/src/App.tsx @@ -601,7 +601,7 @@ export function App() { if (res) setUser(res); } - function generateImages(payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; reference_product?: boolean; model_id?: string; ratio?: string; image_model?: string; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string; onSubmitted?: (taskIds: string[], batchId?: string) => void }) { + function generateImages(payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; reference_product?: boolean; model_id?: string; ratio?: string; image_model?: string; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string; retry_of_task_id?: string; onSubmitted?: (taskIds: string[], batchId?: string) => void }) { // 异步生图:提交后立刻拿到任务列表,前端轮询直到出图。慢的 ARK 出图在 Celery worker 里跑—— // Web 层不被 ~30s 请求占住 → 健康探针不饿死 → 根治"几张图整站 502";且提交成功后浏览器关掉/断网, // worker 仍会把图生成并落库(扣费/退费在 worker 内闭环),重开素材库即可见。 diff --git a/core/frontend/src/api.ts b/core/frontend/src/api.ts index 387cff3..ade8a5b 100644 --- a/core/frontend/src/api.ts +++ b/core/frontend/src/api.ts @@ -719,7 +719,7 @@ export const api = { // 异步生图:提交后秒级返回任务列表(慢出图在 worker 跑),再用 generateImageStatus 轮询取结果。 // 带 conversation_id 则归属该对话;不带则后端自动开一条新对话并回传其 id。 // 带 batch_id(重跑/补图)则任务归回原批次;响应回传本批 batch_id 供前端存进批次卡。 - submitGenerateImage(payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; reference_product?: boolean; model_id?: string; model_entity_id?: string; ratio?: string; image_model?: string; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string }) { + submitGenerateImage(payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; reference_product?: boolean; model_id?: string; model_entity_id?: string; ratio?: string; image_model?: string; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string; retry_of_task_id?: string }) { return request<{ conversation_id: string; batch_id?: string; tasks: { id: string; status: string }[] }>("/api/ai/generate-image/", { method: "POST", body: JSON.stringify(payload) }); }, // 图片创作对话 CRUD —— 左栏会话列表 / 新对话 / 切换 / 重命名 / 删除 diff --git a/core/frontend/src/routes/ai-tools.tsx b/core/frontend/src/routes/ai-tools.tsx index 404bfea..c000ca6 100644 --- a/core/frontend/src/routes/ai-tools.tsx +++ b/core/frontend/src/routes/ai-tools.tsx @@ -605,6 +605,9 @@ type GenBatch = { pendingIds?: string[]; /** 该批全部后端任务 id:删除/恢复批次时使用稳定锚点,终态批次也必须保留。 */ taskIds?: string[]; + /** 补图/单图重跑任务:删掉其成图时不应减少原始批次的失败格数量。 */ + rerunTaskIds?: string[]; + failedTaskIds?: string[]; }; /** @@ -643,7 +646,7 @@ export function ImageWorkbenchPage({ modelConfigs: ModelConfig[]; onBack: () => void; navigate?: (page: Page) => void; - onGenerate: (payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; model_id?: string; ratio?: string; image_model?: string; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string; onSubmitted?: (taskIds: string[], batchId?: string) => void }) => Promise<{ assets: Asset[]; conversation_id?: string; batch_id?: string } | null>; + onGenerate: (payload: { prompt: string; mode?: "image" | "model" | "cover"; count?: number; product_id?: string; model_id?: string; ratio?: string; image_model?: string; platform_id?: string; conversation_id?: string; reference_image_ids?: string[]; batch_id?: string; retry_of_task_id?: string; onSubmitted?: (taskIds: string[], batchId?: string) => void }) => Promise<{ assets: Asset[]; conversation_id?: string; batch_id?: string } | null>; onResume?: (mode: "image" | "model" | "cover", ids: string[]) => Promise<{ assets: Asset[] } | null>; /** 行19:从商品页带入的初始商品 id(可选);未传则回退到第一个商品 */ initialProductId?: string; @@ -876,6 +879,11 @@ export function ImageWorkbenchPage({ const result: GenBatch[] = []; for (const [key, list] of groups) { // R109:成功但成图已全部删除(软删进垃圾桶)的任务不再回显 —— 生成记录与资产库数据绑定 + const coveredFailureIds = new Set( + list.filter((t) => t.rerun && t.status === "succeeded" && t.retry_of_task_id).map((t) => t.retry_of_task_id!) + ); + // 旧补图任务没有记录它替代的失败格;按一张补图抵一张失败格兼容,避免历史批次显示超过原始张数。 + const legacyReplacementCount = list.filter((t) => t.rerun && t.status === "succeeded" && !t.retry_of_task_id).length; const live = list.filter((t) => t.status !== "succeeded" || (t.assets || []).length > 0); if (!live.length) continue; const assets = live.flatMap((t) => t.assets || []); @@ -885,14 +893,19 @@ export function ImageWorkbenchPage({ const status: GenBatch["status"] = hasRunning ? "generating" : assets.length > 0 ? "done" : "failed"; // 应出张数只数原始任务:重跑/补图任务(rerun)是替补,计入会把失败格越滚越多 // (原失败任务仍在批里,重跑一次多一个格)。成功补图后 results 增长,失败格自然收掉。 + const failedTaskIds = list + .filter((t) => !t.rerun && ["failed", "cancelled"].includes(t.status) && !(t.assets || []).length && !coveredFailureIds.has(t.id)) + .map((t) => t.id) + .slice(legacyReplacementCount); const intended = live.filter((t) => !t.rerun).length; + const count = hasRunning ? Math.max(1, intended) : Math.max(1, assets.length + failedTaskIds.length); // 该批用过的参考图(后端解析回 {id,name,url}):批次头回显 + 重跑凭 assetId 原样复用 const refSrc = live.find((t) => (t.reference_images || []).length)?.reference_images || []; result.push({ id: key, prompt: live[0]?.prompt || "", ratio: live[0]?.ratio || meta.ratio, - count: Math.max(1, intended), + count, status, results: assets, ts: new Date(live[0]?.created_at || Date.now()).getTime(), @@ -900,6 +913,8 @@ export function ImageWorkbenchPage({ // 真 batch_id 才能作重跑归属;老任务无 batch_id 时 key=任务 id,不能带给后端 backendBatchId: live[0]?.batch_id || undefined, taskIds: live.map((t) => t.id), + rerunTaskIds: live.filter((t) => t.rerun).map((t) => t.id), + failedTaskIds, }); } // 旧批次在上、新批次在下(对话流自上而下时间序) @@ -920,6 +935,11 @@ export function ImageWorkbenchPage({ const TERMINAL = new Set(["succeeded", "failed", "cancelled"]); const result: GenBatch[] = []; for (const [key, list] of groups) { + const coveredFailureIds = new Set( + list.filter((t) => t.rerun && t.status === "succeeded" && t.retry_of_task_id).map((t) => t.retry_of_task_id!) + ); + // 同图片创作:历史补图没有目标格关联时,仍按一张补图替代一张失败格回放。 + const legacyReplacementCount = list.filter((t) => t.rerun && t.status === "succeeded" && !t.retry_of_task_id).length; const live = list.filter((t) => t.status !== "succeeded" || (t.assets || []).length > 0); if (!live.length) continue; const assets = live.flatMap((t) => t.assets || []); @@ -928,12 +948,17 @@ export function ImageWorkbenchPage({ const first = live[0]; const platformKey = first.platform_id ? PLATFORM_KEY_MAP[first.platform_id] : ""; // 应出张数只数原始任务(重跑/补图任务是替补,不计入,与 batchesFromConvTasks 同理) + const failedTaskIds = list + .filter((t) => !t.rerun && ["failed", "cancelled"].includes(t.status) && !(t.assets || []).length && !coveredFailureIds.has(t.id)) + .map((t) => t.id) + .slice(legacyReplacementCount); const intended = live.filter((t) => !t.rerun).length; + const count = hasRunning ? Math.max(1, intended) : Math.max(1, assets.length + failedTaskIds.length); result.push({ id: key, prompt: first.prompt || "", ratio: first.ratio || meta.ratio, - count: Math.max(1, intended), + count, status, results: assets, ts: new Date(first.created_at || Date.now()).getTime(), @@ -942,6 +967,8 @@ export function ImageWorkbenchPage({ platformIds: platformKey ? [platformKey] : undefined, backendBatchId: first.batch_id || undefined, taskIds: live.map((t) => t.id), + rerunTaskIds: live.filter((t) => t.rerun).map((t) => t.id), + failedTaskIds, // 续轮询要带上整批任务 id(含已终态的):onResume 的结果会整体替换 results, // 只传未完成 id 会把已出的好图从结果里丢掉(终态任务首轮轮询即回带成图,秒完成)。 pendingIds: hasRunning ? live.map((t) => t.id) : undefined, @@ -1062,6 +1089,8 @@ export function ImageWorkbenchPage({ appendToBatchId?: string; /** 重跑/补图:原批次的后端 batch_id,带给后端沿用 → 新任务归回原批次(刷新后不裂新记录) */ batchId?: string; + /** 单格重跑要替代的原失败任务。 */ + retryOfTaskId?: string; }) { // 原地重跑/追加:沿用原批次 id,不再生成新 id、不再新增卡片 const batchId = opts.reuseBatchId || opts.appendToBatchId || `b-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; @@ -1115,8 +1144,16 @@ export function ImageWorkbenchPage({ // 带上当前对话 id(空则后端自动开一条并回传);conversation_id 用 ref 取最新值,避免闭包旧值 // batch_id:重跑/补图带原批次 id → 后端沿用,记录归回原批次(刷新后不裂新聊天记录) // onSubmitted:提交成功拿到任务 id 记进本批 pendingIds → 切走再回来由后端记录接续轮询(R100) - const result = await onGenerate({ prompt: opts.prompt, mode, count: opts.count, product_id: opts.productId, model_id: opts.modelId, ratio: opts.ratio, image_model: genModel, platform_id: opts.platformId, conversation_id: activeConvRef.current || undefined, reference_image_ids: referenceImageIds, batch_id: opts.batchId, - onSubmitted: (taskIds, submittedBatchId) => setBatches((prev) => prev.map((b) => (b.id === batchId ? { ...b, pendingIds: taskIds, taskIds, backendBatchId: b.backendBatchId || submittedBatchId } : b))) }); + const result = await onGenerate({ prompt: opts.prompt, mode, count: opts.count, product_id: opts.productId, model_id: opts.modelId, ratio: opts.ratio, image_model: genModel, platform_id: opts.platformId, conversation_id: activeConvRef.current || undefined, reference_image_ids: referenceImageIds, batch_id: opts.batchId, retry_of_task_id: opts.retryOfTaskId, + onSubmitted: (taskIds, submittedBatchId) => { + setBatches((prev) => prev.map((b) => { + if (b.id !== batchId) return b; + const allTaskIds = opts.appendToBatchId ? [...new Set([...(b.taskIds || []), ...taskIds])] : taskIds; + const rerunTaskIds = opts.appendToBatchId ? [...new Set([...(b.rerunTaskIds || []), ...taskIds])] : b.rerunTaskIds; + return { ...b, pendingIds: taskIds, taskIds: allTaskIds, rerunTaskIds, backendBatchId: b.backendBatchId || submittedBatchId }; + })); + } + }); // 首发新对话:把后端建的对话登记进左栏并设为 active;刷新列表拿到真标题/计数 const convId = result?.conversation_id; if (convId && convId !== activeConvRef.current) { @@ -1132,7 +1169,18 @@ export function ImageWorkbenchPage({ if (opts.appendToBatchId) { const seen = new Set(b.results.map((a) => a.id)); const merged = [...b.results, ...newAssets.filter((a) => !a.id || !seen.has(a.id))]; - return { ...b, backendBatchId, status: merged.length ? ("done" as const) : ("failed" as const), results: merged, pendingIds: undefined }; + const failedTaskIds = opts.retryOfTaskId && newAssets.length + ? b.failedTaskIds?.filter((id) => id !== opts.retryOfTaskId) + : b.failedTaskIds; + return { + ...b, + backendBatchId, + status: merged.length ? ("done" as const) : ("failed" as const), + results: merged, + failedTaskIds, + count: failedTaskIds ? Math.max(1, merged.length + failedTaskIds.length) : b.count, + pendingIds: undefined + }; } return { ...b, backendBatchId, status: newAssets.length ? ("done" as const) : ("failed" as const), results: newAssets, pendingIds: undefined }; })); @@ -1221,9 +1269,21 @@ export function ImageWorkbenchPage({ // 单张:从批次里摘掉;整批删空(且不在生成中)则整卡移除 setBatches((prev) => prev.flatMap((b) => { if (b.id !== target.batchId) return [b]; + const deletedAsset = b.results.find((a) => a.id === target.assetId); + const deletedTaskId = deletedAsset?.origin_task || undefined; const results = b.results.filter((a) => a.id !== target.assetId); - if (!results.length && b.status === "done") return []; - return [{ ...b, results, count: Math.max(1, b.count - 1) }]; + const taskIds = deletedTaskId ? b.taskIds?.filter((id) => id !== deletedTaskId) : b.taskIds; + const rerunTaskIds = deletedTaskId ? b.rerunTaskIds?.filter((id) => id !== deletedTaskId) : b.rerunTaskIds; + // 单图删除只移除该图对应的任务;同批仍有失败、成功或补图任务时,批次卡必须保留。 + if (taskIds && taskIds.length === 0) return []; + return [{ + ...b, + results, + taskIds, + rerunTaskIds, + count: b.failedTaskIds ? Math.max(1, results.length + b.failedTaskIds.length) : Math.max(1, b.count - 1), + status: b.status === "generating" ? "generating" : results.length ? "done" : "failed" + }]; })); return; } @@ -1252,7 +1312,7 @@ export function ImageWorkbenchPage({ /* 单图「再生成」/「重跑这张」(§4.18 悬浮① / YYX#5): PMC#25——补一张图进原批次卡(不新开任务、不覆盖已有好图)。 */ - async function regenSingleImage(src: GenBatch) { + async function regenSingleImage(src: GenBatch, retryOfTaskId?: string) { await startBatch({ prompt: src.prompt, ratio: src.ratio, @@ -1266,7 +1326,8 @@ export function ImageWorkbenchPage({ // 原批次的参考图 + 后端批次 id 一并带回:补的这张仍参考原素材,且归回原批次不裂新记录 refs: src.refs, batchId: src.backendBatchId, - appendToBatchId: src.id + appendToBatchId: src.id, + retryOfTaskId }); } @@ -1335,7 +1396,9 @@ export function ImageWorkbenchPage({ ② 失败格占位 + 单张重跑。R109:成图自动入资产库,不再有「已入库」角标/入库切换。 */ function renderBatchGrid(batch: GenBatch) { const generating = batch.status === "generating"; - const n = batch.results.length || batch.count; + const n = generating + ? Math.max(batch.results.length, batch.count) + : Math.max(1, batch.results.length + (batch.failedTaskIds?.length ?? Math.max(0, batch.count - batch.results.length))); const cols = n >= 4 ? 4 : 2; const ratioBatchVar = batch.ratio.replace(":", " / "); return ( @@ -1345,11 +1408,12 @@ export function ImageWorkbenchPage({ > {/* YYX#5:渲染满 count 个格子 —— 已出图的格用真图;非生成中又缺图的格 = 该张失败, 给失败占位符 + 单张重跑,不再因「部分失败」整批不显示失败格。 */} - {Array.from({ length: Math.max(batch.count, batch.results.length) }).map((_, index) => { + {Array.from({ length: n }).map((_, index) => { const asset = batch.results[index]; + const failedTaskId = batch.failedTaskIds?.[index - batch.results.length]; const url = asset?.files?.[0]?.preview_url; const assetId = asset?.id || ""; - const failed = !generating && !asset; // 非生成中且该格无图 → 这张失败了 + const failed = !generating && !asset && (Boolean(failedTaskId) || !batch.failedTaskIds); // 非生成中且该格无图 → 这张失败了 const key = assetId || `ph-${index}`; const cellKey = `${batch.id}:${assetId || key}`; return ( @@ -1367,7 +1431,7 @@ export function ImageWorkbenchPage({ 这张生成失败 - diff --git a/core/frontend/src/types.ts b/core/frontend/src/types.ts index cddecfc..974b18c 100644 --- a/core/frontend/src/types.ts +++ b/core/frontend/src/types.ts @@ -674,6 +674,7 @@ export type WorkbenchTask = { platform_id: string; /** 重跑/补图任务(复用原批次 batch_id 追加):不计入批次「应出张数」 */ rerun?: boolean; + retry_of_task_id?: string; created_at: string; assets: Asset[]; }; @@ -688,6 +689,7 @@ export type ImageConversationTask = { ratio: string; /** 重跑/补图任务(复用原批次 batch_id 追加):不计入批次「应出张数」 */ rerun?: boolean; + retry_of_task_id?: string; /** id 供重跑时原样复用参考图(老部署/老数据可能没有) */ reference_images: { id?: string; name: string; url: string }[]; created_at: string; diff --git a/docs/bug_todo/图片生成-无法删除失败图片-bug-todo.md b/docs/bug_todo/图片生成-无法删除失败图片-bug-todo.md index 7b94ea6..033f772 100644 --- a/docs/bug_todo/图片生成-无法删除失败图片-bug-todo.md +++ b/docs/bug_todo/图片生成-无法删除失败图片-bug-todo.md @@ -1,5 +1,11 @@ # 图片生成无法删除失败图片 BUG TODO +> 最终状态:已完成(2026-07-16) +> +> 补充修复:三种图片生成模式的单图删除、失败格重跑与页面回填已统一。重跑图会绑定其替代的失败格;删除该补图后,对应失败格不会在刷新或切换页面后复活。历史补图记录也按原始批次张数回显,不会出现“最多 4 张却显示 5 张”。 +> +> 验证:后端重跑关联回归测试通过;前端 TypeScript 与生产构建通过;单图仍进入原有资产垃圾桶,整批删除与“图片异常批次”流程保持不变。 + > 状态:Step 2 方案已确认,待进入实施 > 页面:图片生成入口 `/asset-factory` 所进入的模特上身图、平台套图、图片创作 > 协作规则:一次只执行一个 Step;在确认问题表现前,不排查、不修改代码。