fix(roundtable): 模型容错提示顺序前置 + 回看历史时残留「加载中」气泡强制收尾

- 第二步模型自动容错:系统提示气泡改为插入到失败气泡正前方,使「已切换其他
  模型」先于新模型重答过程显示(旧实现追加到列表末尾,反而排在答案之后)。
- 加载历史草稿(hydrateFromDraft)时把任何残留 streaming 的气泡强制收尾为静止
  态(去加载光标、角标改「已结束」、空正文补「(已结束)」),避免用户点聊天
  记录回看时误以为研讨仍在进行。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-21 20:42:54 +08:00
1 parent 6b963fd918
commit 6dd1f9c017
1 file changed
+43 -25
@@ -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);