From 6dd1f9c0173241aa9e1cd6aef8329b7a419706cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Sun, 21 Jun 2026 20:42:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(roundtable):=20=E6=A8=A1=E5=9E=8B=E5=AE=B9?= =?UTF-8?q?=E9=94=99=E6=8F=90=E7=A4=BA=E9=A1=BA=E5=BA=8F=E5=89=8D=E7=BD=AE?= =?UTF-8?q?=20+=20=E5=9B=9E=E7=9C=8B=E5=8E=86=E5=8F=B2=E6=97=B6=E6=AE=8B?= =?UTF-8?q?=E7=95=99=E3=80=8C=E5=8A=A0=E8=BD=BD=E4=B8=AD=E3=80=8D=E6=B0=94?= =?UTF-8?q?=E6=B3=A1=E5=BC=BA=E5=88=B6=E6=94=B6=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 第二步模型自动容错:系统提示气泡改为插入到失败气泡正前方,使「已切换其他 模型」先于新模型重答过程显示(旧实现追加到列表末尾,反而排在答案之后)。 - 加载历史草稿(hydrateFromDraft)时把任何残留 streaming 的气泡强制收尾为静止 态(去加载光标、角标改「已结束」、空正文补「(已结束)」),避免用户点聊天 记录回看时误以为研讨仍在进行。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../hooks/useStep2Orchestration.ts | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts b/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts index 642f317c..ff8615d0 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts @@ -1205,24 +1205,6 @@ export function useStep2Orchestration( [], ); - /** - * 模型自动容错通知(前台):后端切到下一个模型重跑本轮时调用。弹 toast + 在时间线插一条 - * 「系统提示」气泡,让用户看到「『X』模型异常 → 已切换『Y』继续」。 - */ - const notifyModelSwitch = useCallback( - (info: ModelSwitchInfo) => { - const who = info.agentName || "智能体"; - triggerToast(`⚠️「${info.failedModel}」模型${info.reason},已自动切换「${info.nextModel}」继续`); - appendDialogue({ - sender: "系统提示", - avatarType: "amber", - content: `⚠️ 模型容错:「${who}」所用模型「${info.failedModel}」${info.reason},已自动切换到「${info.nextModel}」继续。`, - confidence: "模型容错", - }); - }, - [appendDialogue, triggerToast], - ); - const phaseLabel = useCallback( ( phase: StreamPhase, @@ -1339,17 +1321,38 @@ export function useStep2Orchestration( }, []); /** - * 生成某气泡的 onModelSwitch 处理器:弹提示 + 把该气泡里「失败尝试已累计的半截内容」清空 - * (随后新模型的流式增量会重新填进同一气泡),并把状态标为「模型切换中…」。 + * 生成某气泡的 onModelSwitch 处理器:弹提示 + 在该失败气泡**正前方**插入一条「系统提示」 + * 气泡(「『X』模型异常 → 已切换『Y』继续」),同时把失败气泡里「已累计的半截内容」清空 + * (随后新模型的流式增量会重新填进同一气泡)并标为「模型切换中…」。 + * + * 顺序很关键:系统提示插在失败气泡之前 → 用户**先**看到「已切换其他模型」、**再**看到新 + * 模型重答的过程(旧实现把提示气泡追加到列表末尾,反而排在新模型答案之后,顺序颠倒)。 + * 插「之前」而非「移到末尾」也保住并行批次里各席位气泡的相对次序。 */ const makeModelSwitchHandler = useCallback( (bubbleId: number, isParallel: boolean) => (info: ModelSwitchInfo) => { - notifyModelSwitch(info); + const who = info.agentName || "智能体"; + triggerToast(`⚠️「${info.failedModel}」模型${info.reason},已自动切换「${info.nextModel}」继续`); if (isParallel) parallelStreamRef.current.delete(bubbleId); - setDialogueContent(bubbleId, ""); - setDialogueConfidence(bubbleId, "模型切换中…"); + const systemBubble: Step2Dialogue = { + id: Date.now() + Math.random(), + sender: "系统提示", + avatarType: "amber", + content: `⚠️ 模型容错:「${who}」所用模型「${info.failedModel}」${info.reason},已自动切换到「${info.nextModel}」继续。`, + time: nowTime(), + confidence: "模型容错", + }; + setStep2RoundtableDialogues((prev) => { + const idx = prev.findIndex((d) => d.id === bubbleId); + // 失败气泡找不到(异常兜底):仍按旧行为在末尾追加系统提示。 + if (idx < 0) return [...prev, systemBubble]; + const cleared = { ...prev[idx], content: "", confidence: "模型切换中…" }; + const next = [...prev]; + next.splice(idx, 1, systemBubble, cleared); + return next; + }); }, - [notifyModelSwitch, setDialogueContent, setDialogueConfidence], + [triggerToast], ); // ─── §6 并行流式:全接收 / 惰性渲染 / 节流 ─────────────────────────────── @@ -3093,7 +3096,22 @@ export function useStep2Orchestration( // 需要 ref 立刻可读到作业的真实线程,否则读到旧值导致接管失败。 threadIdsRef.current = snapshot.threadIds; coordinatorNameRef.current = snapshot.coordinatorName; - setStep2RoundtableDialogues(snapshot.step2RoundtableDialogues); + // 历史快照是**静止记录**:任何残留「流式/加载中」的气泡(用户中途点了「停止」, + // 或草稿在流式期间被自动保存)都强制收尾为静止态。否则用户点聊天记录回看时,这些 + // 气泡仍显示「调度中 / 等待响应 / 正在输出…」+ 加载光标,让人误以为还在进行(其实早已结束)。 + // streaming===true 与「加载中」文案一一对应(finalizeDialogue 收尾时必置 false),故据此即可精确识别。 + setStep2RoundtableDialogues( + snapshot.step2RoundtableDialogues.map((d) => { + if (!d.streaming) return d; + const hasText = (d.content ?? "").trim().length > 0; + return { + ...d, + streaming: false, + content: hasText ? d.content : "(已结束)", + confidence: "已结束", + }; + }), + ); setLastLeaderContent(snapshot.lastLeaderContent); setHasConsensus(snapshot.hasConsensus); setConsensusPercentage(snapshot.consensusPercentage);