fix: 修复图片补图删除回填
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 内闭环),重开素材库即可见。
|
||||
|
||||
@@ -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 —— 左栏会话列表 / 新对话 / 切换 / 重命名 / 删除
|
||||
|
||||
@@ -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({
|
||||
<span className="gen-failed">
|
||||
<X size={18} />
|
||||
<span className="gf-text">这张生成失败</span>
|
||||
<button type="button" className="gf-retry" onClick={() => regenSingleImage(batch)}>
|
||||
<button type="button" className="gf-retry" onClick={() => regenSingleImage(batch, failedTaskId)}>
|
||||
<RefreshCw size={12} />
|
||||
重跑这张
|
||||
</button>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 图片生成无法删除失败图片 BUG TODO
|
||||
|
||||
> 最终状态:已完成(2026-07-16)
|
||||
>
|
||||
> 补充修复:三种图片生成模式的单图删除、失败格重跑与页面回填已统一。重跑图会绑定其替代的失败格;删除该补图后,对应失败格不会在刷新或切换页面后复活。历史补图记录也按原始批次张数回显,不会出现“最多 4 张却显示 5 张”。
|
||||
>
|
||||
> 验证:后端重跑关联回归测试通过;前端 TypeScript 与生产构建通过;单图仍进入原有资产垃圾桶,整批删除与“图片异常批次”流程保持不变。
|
||||
|
||||
> 状态:Step 2 方案已确认,待进入实施
|
||||
> 页面:图片生成入口 `/asset-factory` 所进入的模特上身图、平台套图、图片创作
|
||||
> 协作规则:一次只执行一个 Step;在确认问题表现前,不排查、不修改代码。
|
||||
|
||||
Reference in New Issue
Block a user