From ff0a93677a91c801c9cc87eb0acb5213190d0933 Mon Sep 17 00:00:00 2001 From: hh <2587203630@qq.com> Date: Wed, 15 Jul 2026 16:07:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=88=87=E6=8D=A2=E5=95=86=E5=93=81=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E6=89=B9=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/frontend/src/routes/ai-tools.tsx | 31 ++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/core/frontend/src/routes/ai-tools.tsx b/core/frontend/src/routes/ai-tools.tsx index 703a030..aad72b2 100644 --- a/core/frontend/src/routes/ai-tools.tsx +++ b/core/frontend/src/routes/ai-tools.tsx @@ -605,6 +605,18 @@ type GenBatch = { pendingIds?: string[]; }; +/** + * 判断本地临时卡与后端恢复卡是否代表同一真实任务组。 + * 新提交后先有前端 b- 临时 id,随后才回填后端 batch_id;两者在商品切换时会短暂共存, + * 必须按真实 batch_id(或任务 id 重叠的提交/恢复交错兜底)合并,不能按前端卡片 id 比较。 + */ +function isSameWorkbenchBatch(left: GenBatch, right: GenBatch): boolean { + if (left.backendBatchId && right.backendBatchId && left.backendBatchId === right.backendBatchId) return true; + const leftIds = left.pendingIds || []; + const rightIds = new Set(right.pendingIds || []); + return leftIds.some((id) => rightIds.has(id)); +} + /* R100:后端 platform_id(douyin/taobao…) → 前端平台 key(dy/tb…),恢复批次时还原平台分组 */ const PLATFORM_KEY_MAP: Record = Object.fromEntries( Object.entries(PLATFORM_ID_MAP).map(([key, canonical]) => [canonical, key]) @@ -691,6 +703,10 @@ export function ImageWorkbenchPage({ const [openMore, setOpenMore] = useState(""); // 批次列表:每次生成/重跑追加一条,各自展示;可多批并行 generating const [batches, setBatches] = useState([]); + // 商品切换后的后端恢复回调不依赖 batches,避免恢复 effect 因批次状态变化反复触发; + // 用 ref 读取最新内存卡,识别仍由首次提交轮询的临时批次。 + const batchesRef = useRef(batches); + batchesRef.current = batches; /* ── 图片创作「对话」(真实体,后端 ImageConversation)── conversations = 左栏会话列表;activeConvId = 当前选中对话(空 = 还没建,首次发送时后端自动开)。 activeConvRef 让 startBatch 在不进 deps 的情况下读到最新 active id。 */ @@ -1251,10 +1267,19 @@ export function ImageWorkbenchPage({ .then((res) => { if (cancelled) return; const next = batchesFromWorkbenchTasks(res.tasks || []); - // 竞态保护:拉取在途时用户又提交了新批次(本地 id 以 b- 开头、还在生成中)→ 合并保留,别被整体替换冲掉 - setBatches((prev) => [...next, ...prev.filter((b) => b.id.startsWith("b-") && b.status === "generating")]); + // 竞态保护:拉取在途时用户又提交了新批次(本地 id 以 b- 开头、还在生成中)→ 合并保留,别被整体替换冲掉。 + // 同时,切走再回来时 next 会带回这张临时卡对应的真实 batch_id:若直接追加, + // 页面会出现两张同一任务卡,并对同一 pendingIds 启动两次轮询。 + const inMemoryGenerating = batchesRef.current.filter((b) => b.id.startsWith("b-") && b.status === "generating"); + const recoveredToResume = next.filter((b) => !inMemoryGenerating.some((local) => isSameWorkbenchBatch(local, b))); + setBatches((prev) => { + const localGenerating = prev.filter((b) => b.id.startsWith("b-") && b.status === "generating"); + const recovered = next.filter((b) => !localGenerating.some((local) => isSameWorkbenchBatch(local, b))); + return [...recovered, ...localGenerating]; + }); if (onResume) { - for (const b of next) { + // 仅恢复当前内存中不存在的批次;已匹配的临时卡仍由首次提交流程继续轮询。 + for (const b of recoveredToResume) { if (b.status !== "generating" || !(b.pendingIds && b.pendingIds.length)) continue; onResume(mode, b.pendingIds).then((r) => { if (cancelled || !r?.assets) return;