fix(ai): 图片创作重跑三连修——丢参考图/裂新批次/提示词矛盾
① 重跑丢参考图:「重跑这张/重跑整批」原来不带 refs → 后端收不到 reference_image_ids 落到纯文生图,出图与上传素材无关。现 GenBatch.refs 存 assetId(上传后回写;恢复批次由 tasks 接口新带的 id 还原),重跑原样带回。 ② 重跑裂新聊天记录:enqueue 每次新铸 batch_id,后端没有「补进原批次」 概念,刷新/切对话后重跑图裂成新卡、原卡失败格还挂着。现生成接口收/回 batch_id(UUID 校验,非法兜底新铸),重跑沿用原批次并打 batch_append 标记; 前端批次卡存 backendBatchId(提交回调即落,整批全失败也不丢),重跑带回。 恢复分组时 rerun 任务不计入「应出张数」→ 补图成功后失败格自然收掉; 批次状态改按「有任务在跑」判定,已有好图+补图在途也能接续轮询。 ③ 同批换装一半不换:带参考图模板「严格保留款式/不要换款」与用户要求 (如"生成现代服装的穿着")直接矛盾,模型每张随机听一边。改为只锁主体身份 (人物五官/发型/体型,商品品类/外形/Logo),用户明确要求改变的以用户要求 为最高优先级,未提及的才默认与参考图一致。 测试:apps 全量 214 过(需 --settings=airshelf.settings.test); 新增 test_rerun_with_batch_id_reuses_batch_and_marks_append。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -608,8 +608,10 @@ type GenBatch = {
|
||||
modelName?: string;
|
||||
/** 该批次选中的平台 id 列表(平台套图:多选 → 各平台分组,P0③) */
|
||||
platformIds?: string[];
|
||||
/** 该批次提交的参考图(图片创作:用户上传作生成参考),用于批次头回显「参考了哪些图」 */
|
||||
refs?: { name: string; url: string }[];
|
||||
/** 该批次提交的参考图(图片创作:用户上传作生成参考):批次头回显 + 重跑时凭 assetId 原样复用 */
|
||||
refs?: { name: string; url: string; assetId?: string }[];
|
||||
/** 后端批次 id:重跑/补图带它回去,新任务归回原批次(否则后端裂成新批次,刷新后多出一条记录) */
|
||||
backendBatchId?: string;
|
||||
/** 该批次已提交、尚未终态的生图任务 id:切走再回来可据此对每一批各自续轮询(PMC#5/#10) */
|
||||
pendingIds?: string[];
|
||||
};
|
||||
@@ -637,7 +639,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[]; onSubmitted?: (taskIds: string[]) => void }) => Promise<{ assets: Asset[]; conversation_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; 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;
|
||||
@@ -844,18 +846,26 @@ export function ImageWorkbenchPage({
|
||||
const live = list.filter((t) => t.status !== "succeeded" || (t.assets || []).length > 0);
|
||||
if (!live.length) continue;
|
||||
const assets = live.flatMap((t) => t.assets || []);
|
||||
const allTerminal = live.every((t) => TERMINAL.has(t.status));
|
||||
const status: GenBatch["status"] = assets.length > 0 ? "done" : allTerminal ? "failed" : "generating";
|
||||
// 有任务还在跑(含重跑补图)就是 generating,不能只看「有没有图」——
|
||||
// 否则「已有部分好图 + 重跑在途」被判 done,轮询不接续,补的图要手动刷新才出现
|
||||
const hasRunning = live.some((t) => !TERMINAL.has(t.status));
|
||||
const status: GenBatch["status"] = hasRunning ? "generating" : assets.length > 0 ? "done" : "failed";
|
||||
// 应出张数只数原始任务:重跑/补图任务(rerun)是替补,计入会把失败格越滚越多
|
||||
// (原失败任务仍在批里,重跑一次多一个格)。成功补图后 results 增长,失败格自然收掉。
|
||||
const intended = live.filter((t) => !t.rerun).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: live.length,
|
||||
count: Math.max(1, intended),
|
||||
status,
|
||||
results: assets,
|
||||
ts: new Date(live[0]?.created_at || Date.now()).getTime(),
|
||||
// 该批用过的参考图(后端按 reference_image_ids 解析回 {name,url}),切换/刷新后仍可见
|
||||
refs: (live[0]?.reference_images || []).length ? live[0].reference_images : undefined,
|
||||
refs: refSrc.length ? refSrc.map((r) => ({ name: r.name, url: r.url, assetId: r.id })) : undefined,
|
||||
// 真 batch_id 才能作重跑归属;老任务无 batch_id 时 key=任务 id,不能带给后端
|
||||
backendBatchId: live[0]?.batch_id || undefined,
|
||||
});
|
||||
}
|
||||
// 旧批次在上、新批次在下(对话流自上而下时间序)
|
||||
@@ -882,17 +892,20 @@ export function ImageWorkbenchPage({
|
||||
const status: GenBatch["status"] = hasRunning ? "generating" : assets.length ? "done" : "failed";
|
||||
const first = live[0];
|
||||
const platformKey = first.platform_id ? PLATFORM_KEY_MAP[first.platform_id] : "";
|
||||
// 应出张数只数原始任务(重跑/补图任务是替补,不计入,与 batchesFromConvTasks 同理)
|
||||
const intended = live.filter((t) => !t.rerun).length;
|
||||
result.push({
|
||||
id: key,
|
||||
prompt: first.prompt || "",
|
||||
ratio: first.ratio || meta.ratio,
|
||||
count: live.length,
|
||||
count: Math.max(1, intended),
|
||||
status,
|
||||
results: assets,
|
||||
ts: new Date(first.created_at || Date.now()).getTime(),
|
||||
productId: first.product_id || undefined,
|
||||
modelId: first.model_id || undefined,
|
||||
platformIds: platformKey ? [platformKey] : undefined,
|
||||
backendBatchId: first.batch_id || undefined,
|
||||
// 续轮询要带上整批任务 id(含已终态的):onResume 的结果会整体替换 results,
|
||||
// 只传未完成 id 会把已出的好图从结果里丢掉(终态任务首轮轮询即回带成图,秒完成)。
|
||||
pendingIds: hasRunning ? live.map((t) => t.id) : undefined,
|
||||
@@ -1011,6 +1024,8 @@ export function ImageWorkbenchPage({
|
||||
reuseBatchId?: string;
|
||||
/** PMC#25:单图重跑——把新图追加进这张已有批次卡(不清旧好图、不新开卡) */
|
||||
appendToBatchId?: string;
|
||||
/** 重跑/补图:原批次的后端 batch_id,带给后端沿用 → 新任务归回原批次(刷新后不裂新记录) */
|
||||
batchId?: string;
|
||||
}) {
|
||||
// 原地重跑/追加:沿用原批次 id,不再生成新 id、不再新增卡片
|
||||
const batchId = opts.reuseBatchId || opts.appendToBatchId || `b-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
@@ -1034,8 +1049,8 @@ export function ImageWorkbenchPage({
|
||||
modelId: opts.modelId,
|
||||
modelName: opts.modelName,
|
||||
platformIds: opts.platformIds,
|
||||
// 批次头回显「参考了哪些图」(只存名+预览,不存 file)
|
||||
refs: opts.refs?.map((r) => ({ name: r.name, url: r.url }))
|
||||
// 批次头回显「参考了哪些图」(存名+预览+已知 assetId,不存 file;上传成功后统一回写 assetId)
|
||||
refs: opts.refs?.map((r) => ({ name: r.name, url: r.url, assetId: r.assetId }))
|
||||
};
|
||||
setBatches((prev) => [...prev, newBatch]);
|
||||
}
|
||||
@@ -1057,11 +1072,15 @@ export function ImageWorkbenchPage({
|
||||
})
|
||||
);
|
||||
referenceImageIds = ids.filter((x): x is string => !!x);
|
||||
// 上传得到的 assetId 回写进批次 refs:重跑时凭它原样复用参考图(不再重新上传、不丢参考)
|
||||
const refsWithIds = opts.refs.map((r, i) => ({ name: r.name, url: r.url, assetId: r.assetId || ids[i] || undefined }));
|
||||
setBatches((prev) => prev.map((b) => (b.id === batchId ? { ...b, refs: refsWithIds } : b)));
|
||||
}
|
||||
// 带上当前对话 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,
|
||||
onSubmitted: (taskIds) => setBatches((prev) => prev.map((b) => (b.id === batchId ? { ...b, pendingIds: taskIds } : 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,
|
||||
onSubmitted: (taskIds, submittedBatchId) => setBatches((prev) => prev.map((b) => (b.id === batchId ? { ...b, pendingIds: taskIds, backendBatchId: b.backendBatchId || submittedBatchId } : b))) });
|
||||
// 首发新对话:把后端建的对话登记进左栏并设为 active;刷新列表拿到真标题/计数
|
||||
const convId = result?.conversation_id;
|
||||
if (convId && convId !== activeConvRef.current) {
|
||||
@@ -1071,13 +1090,15 @@ export function ImageWorkbenchPage({
|
||||
setBatches((prev) => prev.map((b) => {
|
||||
if (b.id !== batchId) return b;
|
||||
const newAssets = result?.assets || [];
|
||||
// 后端批次 id 落进批次卡:之后重跑这张/重跑整批都带它回原批次
|
||||
const backendBatchId = b.backendBatchId || result?.batch_id;
|
||||
// 单图追加重跑:把新图并进原有好图(按 id 去重),不覆盖;其余路径直接用本批结果
|
||||
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, status: merged.length ? ("done" as const) : ("failed" as const), results: merged, pendingIds: undefined };
|
||||
return { ...b, backendBatchId, status: merged.length ? ("done" as const) : ("failed" as const), results: merged, pendingIds: undefined };
|
||||
}
|
||||
return { ...b, status: newAssets.length ? ("done" as const) : ("failed" as const), results: newAssets, pendingIds: undefined };
|
||||
return { ...b, backendBatchId, status: newAssets.length ? ("done" as const) : ("failed" as const), results: newAssets, pendingIds: undefined };
|
||||
}));
|
||||
} catch {
|
||||
// 追加重跑失败:保留原有好图,只把状态落回(有图=done,无图=failed)
|
||||
@@ -1141,6 +1162,9 @@ export function ImageWorkbenchPage({
|
||||
modelName: src.modelName,
|
||||
platformIds: src.platformIds,
|
||||
platformId: src.platformIds?.[0] ? PLATFORM_ID_MAP[src.platformIds[0]] : undefined,
|
||||
// 原批次的参考图 + 后端批次 id 一并带回:重跑仍参考原素材,且任务归回原批次不裂新记录
|
||||
refs: src.refs,
|
||||
batchId: src.backendBatchId,
|
||||
reuseBatchId: src.id
|
||||
});
|
||||
}
|
||||
@@ -1197,6 +1221,9 @@ export function ImageWorkbenchPage({
|
||||
modelName: src.modelName,
|
||||
platformIds: src.platformIds,
|
||||
platformId: src.platformIds?.[0] ? PLATFORM_ID_MAP[src.platformIds[0]] : undefined,
|
||||
// 原批次的参考图 + 后端批次 id 一并带回:补的这张仍参考原素材,且归回原批次不裂新记录
|
||||
refs: src.refs,
|
||||
batchId: src.backendBatchId,
|
||||
appendToBatchId: src.id
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user