From d26500bdbd83c0f7e62da9299d787018b9968e16 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Fri, 26 Jun 2026 13:16:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(pipeline):=20=E5=9F=BA=E7=A1=80=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E9=A1=B5=E4=B8=A4=E4=B8=AA=E8=BD=AE=E8=AF=A2=E7=A9=BA?= =?UTF-8?q?=E9=97=B2=E4=B8=8D=E5=81=9C=20=E2=86=92=20=E7=A9=BA=E9=97=B2?= =?UTF-8?q?=E5=8D=B3=E5=81=9C=E3=80=81=E5=BC=80=E5=B7=A5=E5=8D=B3=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:停在基础资产页(尤其点「重新提取角色/场景」后)pending-assets 与 poll-reviews 一直轮询、刷屏。 根因: - poll-reviews 的停止条件写成「只有先见过 processing 才停」,没有审核中资产时 永远停不下来(8s 空转)。 - pending-assets 在该页无停止条件(4s 一直发)。 - pending-assets 把每渲染换新引用的 onRefreshProject 放进依赖 → 每渲染重订阅 并立刻补发请求,越刷越密。 修复: - pending-assets:一轮发现无在途出图 + genBusy 为空即停;genBusy 变化(开始/结束 生成)时重订阅恢复。onRefreshProject 改走 ref,移出依赖。 - poll-reviews:一轮后端无 processing 即停;genBusy / reviewWake(手动送审)/ pendingGenKey(出片完成→自动送审)任一变化时重订阅,把盾盯到终态再停。 - 终态(active/failed)在最后一轮非空响应即下发,停在空响应安全不丢更新。 空闲坐在基础资产页 = 零轮询;仅有真实生成/审核在途时才轮询且完事即停。 Co-Authored-By: Claude Opus 4.8 --- core/frontend/src/routes/pipeline.tsx | 30 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/core/frontend/src/routes/pipeline.tsx b/core/frontend/src/routes/pipeline.tsx index b51f9dc..45beecb 100644 --- a/core/frontend/src/routes/pipeline.tsx +++ b/core/frontend/src/routes/pipeline.tsx @@ -714,12 +714,21 @@ export function PipelinePage(props: { }); } + // 轮询用:onRefreshProject 每次 App 渲染都换新引用,若直接进 effect 依赖会让轮询每渲染重订阅+立刻重发请求 + // (基础资产页「一直在轮询」的根因之一)。用 ref 稳住,effect 不再因它重跑。 + const onRefreshProjectRef = useRef(onRefreshProject); + useEffect(() => { onRefreshProjectRef.current = onRefreshProject; }); + // 审核轮询的「重启信号」:手动送审/一键送审后 +1,让已停的 poll-reviews 重新开跑去盯新提交的审核。 + const [reviewWake, setReviewWake] = useState(0); // 行38/流程步骤4 · 单卡生成 loading:按「busyKey」分别记录,各按钮互不影响(生成三视图不挡新增人物) const [genBusy, setGenBusy] = useState>(new Set()); // 刷新后从后端「认领」在途出图任务:出图在 worker 跑、内存态 genBusy 丢了,据此重建占位卡 loading const [pendingGen, setPendingGen] = useState>([]); const pendingHas = (kind: string, label: string) => pendingGen.some((p) => p.kind === kind && !p.is_triview && (p.label || "") === (label || "")); + // 在途出图集合的稳定指纹:集合变化(开工/出片完成)才变 → 给审核轮询当「重启信号」, + // 让真人出片后自动送审的盾能被盯到变绿(否则审核轮询可能在送审前就停了)。仅集合变才变,不会每轮抖动。 + const pendingGenKey = useMemo(() => pendingGen.map((p) => `${p.kind}:${p.label}:${p.is_triview}`).join("|"), [pendingGen]); const isBusy = (k: string) => genBusy.has(k); const addBusy = (k: string) => setGenBusy((s) => { const n = new Set(s); n.add(k); return n; }); const delBusy = (k: string) => setGenBusy((s) => { const n = new Set(s); n.delete(k); return n; }); @@ -1071,6 +1080,7 @@ export function PipelinePage(props: { const r = await api.submitAssetReview(id); // 用后端真态,别再 `|| "processing"` 伪造审核中 —— 没真送出去就该回灰盾,而不是骗一个刷新即消失的「审核中」 setReviews((m) => ({ ...m, [id]: r.review_status || "" })); + if ((r.review_status || "") === "processing") setReviewWake((n) => n + 1); // 唤醒审核轮询去盯这条 } catch (e) { // 送审没成(503=审核服务不可用等):如实提示,灰盾保持可重试,不再静默吞掉 onNotify?.("error", e instanceof Error && e.message ? e.message : "审核服务暂不可用,请稍后重试"); @@ -1083,7 +1093,6 @@ export function PipelinePage(props: { useEffect(() => { if (viewStage !== 2 && viewStage !== 3 && viewStage !== 4) return; let alive = true; - let sawProcessing = false; let timer = 0; const tick = async () => { if (document.hidden) return; // 标签页切走时不空跑审核轮询(纯读,回前台自然恢复) @@ -1091,16 +1100,17 @@ export function PipelinePage(props: { if (!alive) return; const map = (r?.reviews || {}) as Record; if (Object.keys(map).length) { - sawProcessing = true; - setReviews((m) => ({ ...m, ...map })); - } else if (sawProcessing) { - window.clearInterval(timer); // 审核都终态了,停轮询省空跑(后端无 processing 资产时返回 {}) + setReviews((m) => ({ ...m, ...map })); // 含终态(active绿/failed红):终态在这一轮就下发了 + } else { + // 后端已无 processing 资产 → 没什么可等,停轮询(旧实现没见过 processing 时会 8s 空跑到永远)。 + // 新生成的真人会自动送审、手动送审会 +reviewWake,届时本 effect 重订阅再开跑。 + window.clearInterval(timer); } }; void tick(); timer = window.setInterval(tick, 8000); return () => { alive = false; window.clearInterval(timer); }; - }, [viewStage, project.id]); + }, [viewStage, project.id, genBusy, reviewWake, pendingGenKey]); // 故事板有在制场(queued/running)时,后台每 5s 静默轮询驱动出图 + 刷新(对标视频段轮询)。 // 不占全局 loading → 单场重跑不会锁住其它场的按钮,可并行重跑多场。 useEffect(() => { @@ -2376,9 +2386,11 @@ export function PipelinePage(props: { const res = await api.pendingAssets(project.id); if (stopped) return; const list = res.pending || []; - if (list.length < prevPendingCountRef.current) void onRefreshProject(); + if (list.length < prevPendingCountRef.current) void onRefreshProjectRef.current(); prevPendingCountRef.current = list.length; setPendingGen(list.map((p) => ({ kind: p.kind, label: p.label, is_triview: p.is_triview }))); + // 没有在途出图、本地也没有生成在跑 → 没什么可等,停轮询;再点生成(genBusy 变)时本 effect 会重订阅恢复。 + if (list.length === 0 && genBusy.size === 0) { stopped = true; return; } } catch { /* 忽略,下一轮再试 */ } @@ -2386,7 +2398,8 @@ export function PipelinePage(props: { }; void tick(); return () => { stopped = true; window.clearTimeout(timer); }; - }, [activeDot, viewStage, project.id, onRefreshProject]); + // genBusy 入依赖:开始/结束一次生成都会重订阅 → 空闲即停、开工即起。onRefreshProject 走 ref,不进依赖(否则每渲染重发)。 + }, [activeDot, viewStage, project.id, genBusy]); // 流程步骤4 · 刷新后认领在途「提取」:内存态 extractState 一刷新就丢,据后端 extract-status 重建「提取中」loading // (和上面出图占位卡同思路)。已有轮询在跑则不重复起。进资产趴时检查一次。 @@ -3860,6 +3873,7 @@ export function PipelinePage(props: { setReviewGate((g) => (g ? { ...g, submitting: true } : g)); const ids = [...new Set(reviewGate.blockers.filter((b) => b.review_status !== "processing" && b.asset_id).map((b) => b.asset_id))]; await Promise.all(ids.map((id) => api.submitAssetReview(id).then((r) => setReviews((m) => ({ ...m, [id]: r.review_status || "processing" }))).catch(() => undefined))); + if (ids.length) setReviewWake((n) => n + 1); // 唤醒审核轮询去盯刚送审的这些 setReviewGate(null); onNotify?.("success", ids.length ? "已提交审核,请稍候待变绿(已过审)后再生成视频" : "素材正在审核中,请稍候待变绿后再生成"); }}