代码优化

This commit is contained in:
董宏凯 committed 2026-07-19 16:08:40 +08:00
1 parent 760b88d6f1
commit bb09c5991b
48 files changed
+8397 -6812

No files matched your search

@@ -0,0 +1,267 @@
import { useCallback, useEffect, useMemo, useRef, type MutableRefObject } from "react";
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
import { useStep3Dashboard } from "@/roundtable-planning/hooks/useStep3Dashboard";
import { useStep3Report, type ReportIntent } from "@/roundtable-planning/hooks/useStep3Report";
import { useStep3Summary } from "@/roundtable-planning/hooks/useStep3Summary";
import type { Step2Dialogue } from "@/roundtable-planning/hooks/useStep2Orchestration";
import type { OrchestrationPlan } from "@/roundtable-planning/lib/dag-types";
import { isDashboardHtml } from "@/roundtable-planning/lib/report-template";
import { stripReasoning } from "@/roundtable-planning/lib/reasoning";
const STEP3_FLOW_STAGGER_MS = 300;
export interface Step3ConversationSnapshot {
dialogues: unknown[];
seatStubMessages: Record<string, unknown[]>;
ingestStatus?: string;
}
export interface UseRoundtableStep3FlowsOptions {
intentReady: ReportIntent | null;
step2RoundtableDialogues: Step2Dialogue[];
consensusText: string;
selectedModel: string;
orchestrationPlan: OrchestrationPlan | null;
step3Report: DraftStep3Snapshot | null;
onReportSaved: (snapshot: DraftStep3Snapshot) => void;
onSummarySaved: (snapshot: NonNullable<DraftStep3Snapshot["summaryReport"]>) => void;
ingestAvailable: boolean;
ingestSkill: string;
ingestBusinessCode: string | null;
taskId: string | null;
chainTitle: string | null;
isDark: boolean;
currentStep: number;
draftId: string | null;
summaryConvoRef: MutableRefObject<Step3ConversationSnapshot>;
reportConvoRef: MutableRefObject<Step3ConversationSnapshot>;
dashboardConvoRef: MutableRefObject<Step3ConversationSnapshot>;
}
export function useRoundtableStep3Flows({
intentReady,
step2RoundtableDialogues,
consensusText,
selectedModel,
orchestrationPlan,
step3Report,
onReportSaved: handleReportSaved,
onSummarySaved: handleSummarySaved,
ingestAvailable,
ingestSkill,
ingestBusinessCode,
taskId,
chainTitle,
isDark,
currentStep,
draftId,
summaryConvoRef: step3SummaryConvoRef,
reportConvoRef: step3ReportConvoRef,
dashboardConvoRef: step3DashConvoRef,
}: UseRoundtableStep3FlowsOptions) {
// ── Step3 三个生成流提升到页面层(2026-06-13)────────────────────────────
// 总结报告 / 结果绘制(经典) / 大屏多页的流式 run **不随 Step3 子视图卸载而中断**:
// 用户切回第二步、在总结/绘制/最终方案视图间往返,流都在页面层继续;
// 回头进入对应视图即可续看进行中的过程或最终结果。
const step3SeatDeliveries = useMemo(
() =>
step2RoundtableDialogues
.filter((d) => d.confidence === "子智能体交付")
.map((d) => ({
sender: String(d.sender ?? ""),
content: stripReasoning(String(d.content ?? "")),
})),
[step2RoundtableDialogues],
);
// 业务链条层级(取 stages 的「本阶段目标」goal,有序)——taskId 深链模式自动结构化输出时
// 作流程图层级骨架(任务 → 层级1 → …)。memo 稳定数组身份,避免 run useCallback 频繁失效。
const chainLevels = useMemo(
() =>
(orchestrationPlan?.stages ?? [])
.map((s) => s.goal?.trim() ?? "")
.filter((g) => g.length > 0),
[orchestrationPlan],
);
// 第三步首次生成报告后即**自动**续跑结构化输出(流程图)——不再限 taskId 深链,
// 任何会商进第三步首次出报告都自动出流程图(用户 2026-06-17 要求)。
const autoSummaryFlow = true;
const summaryFlow = useStep3Summary({
intentReady: intentReady,
seatDeliveries: step3SeatDeliveries,
consensusText: consensusText,
selectedModel,
coordinatorPrompt: orchestrationPlan?.coordinatorPrompt ?? null,
chainLevels,
autoFlow: autoSummaryFlow,
savedSummary: step3Report?.summaryReport ?? null,
onSummarySaved: handleSummarySaved,
ingestAvailable,
// 入库仅深链(taskId)模式(会商 rwfx-A 等)才有;taskId 入库时必传给技能。
taskId: taskId,
// 入库场景线索:适用性判定选中的技能 + 业务码/链名,入库时引导智能体挑对入库技能。
ingestSkill,
ingestBusinessCode,
ingestChainName: chainTitle,
});
// render 期镜像总结报告的实时对话(文字 + 步骤条) + stub + 入库状态,供 getStep3Snapshot 落库。
step3SummaryConvoRef.current = {
dialogues: summaryFlow.dialogues,
seatStubMessages: summaryFlow.seatStubMessages,
ingestStatus: summaryFlow.ingestStatus,
};
const reportFlow = useStep3Report({
intentReady: intentReady,
seatDeliveries: step3SeatDeliveries,
consensusText: consensusText,
selectedModel,
savedReport: step3Report,
onReportSaved: handleReportSaved,
});
// render 期镜像经典看板的实时对话(文字 + 步骤条) + stub,供 getStep3Snapshot 落库。
step3ReportConvoRef.current = {
dialogues: reportFlow.dialogues,
seatStubMessages: reportFlow.seatStubMessages,
};
const dashboardFlow = useStep3Dashboard({
intentReady: intentReady,
seatDeliveries: step3SeatDeliveries,
consensusText: consensusText,
selectedModel,
themeMode: isDark ? "dark" : "light",
savedReport: step3Report,
onReportSaved: handleReportSaved,
});
// render 期镜像大屏的实时对话(文字 + 步骤条) + stub,供 getStep3Snapshot 落库。
step3DashConvoRef.current = {
dialogues: dashboardFlow.dialogues,
seatStubMessages: dashboardFlow.seatStubMessages,
};
// 进入第三步:总结报告 + 结果绘制(经典) + 大屏多页 **三流并行**自动启动(互无依赖);
// 已有存稿则恢复展示。只在「本次会话首次进入第三步」时编排一次 —— 之后进出第三步
// 全是续看(流在页面层不中断);切换草稿 / 新建任务时复位入口闸门。
const step3EnteredRef = useRef(false);
// 进第三步「错峰起跑」定时器。三流虽互无依赖、要并行展示进度,但若**同一刻**一起
// 调 start()(各自 init thread + run/stream),会把单 worker 网关 + 单连接 sqlite
// checkpoint 锁的瞬时压力顶满,回环 /runs/stream 概率性返回非 200 → 前端冒 502。
// 这里把后两个流的**起跑**各往后挪一点点(仅错开 start_run 的写尖峰,不是串行等前一个
// 跑完),削峰即可。定时器存 ref,切草稿 / 重新 hydrate / 新建任务时统一清掉,避免迟发的
// start() 打到已被复位/中断的上下文。
const step3StartTimersRef = useRef<number[]>([]);
const clearStep3StartTimers = useCallback(() => {
for (const t of step3StartTimersRef.current) {
if (typeof window !== "undefined") window.clearTimeout(t);
}
step3StartTimersRef.current = [];
}, []);
// step3Report 用 ref 持最新值,供 hydrate 同步读取(避免闭包取到旧草稿快照)。
const step3ReportRef = useRef(step3Report);
step3ReportRef.current = step3Report;
// 切草稿即在**渲染期**同步复位入口闸门:保证随后的 hydrate effect 能重新跑一次
// (此前闸门只在 currentStep 变化时复位,切历史记录若仍停在第三步 currentStep 不变,
// 大屏/报告就留着上一条记录的数据不刷新 —— 即本次修复的 bug 根因)。
const prevDraftIdRef = useRef(draftId);
if (prevDraftIdRef.current !== draftId) {
const wasRealDraft = prevDraftIdRef.current != null;
prevDraftIdRef.current = draftId;
// 仅「真正切到另一份草稿」才复位入口闸门重新 hydrate。草稿**首次创建**(null→id,
// 多由本会话第一次自动保存触发)不算切换——若这时复位,正在并行跑的 Step3 三流(尤其
// 大屏)会被下方 hydrate 当成「未启动」重新 start(),既丢了在途产物又反复重画。
if (wasRealDraft) step3EnteredRef.current = false;
}
// 三流 hydrate/start(按当前草稿快照恢复或新建)。进第三步、切草稿后停留第三步都复用它,
// 保证总结/经典/大屏的展示数据跟随当前草稿实时切换。
const hydrateStep3Flows = useCallback(() => {
step3EnteredRef.current = true;
// 重新编排前先清掉上一轮可能还没触发的错峰定时器(切草稿重 hydrate 时)。
clearStep3StartTimers();
const rep = step3ReportRef.current;
// 错峰起跑助手:把一个尚未起跑的流延迟 delayMs 后再 start();触发前再校验
// step3EnteredRef(被新建任务 / 切草稿复位为 false 则跳过),避免迟发的 start()
// 打到已被中断的上下文。delay=0 表示立即(同步)起跑。
const staggerStart = (start: () => void, delayMs: number) => {
if (delayMs <= 0 || typeof window === "undefined") {
start();
return;
}
const id = window.setTimeout(() => {
if (!step3EnteredRef.current) return;
start();
}, delayMs);
step3StartTimersRef.current.push(id);
};
// ① 方案总结报告(主页,立即起跑)
const savedSum = rep?.summaryReport;
if (savedSum && (savedSum.md.trim() || (savedSum.closing ?? "").trim())) {
summaryFlow.hydrateSaved(savedSum);
} else {
summaryFlow.start();
}
// ② 结果绘制(经典模式一)。旧版草稿可能把大屏 HTML 存在 html 槽位(带 marker),
// 那不是经典产物 → 经典照常启动。错峰 ~300ms,削掉与总结的同刻 start 尖峰。
if (rep?.html && !isDashboardHtml(rep.html)) {
reportFlow.hydrateSaved(rep);
} else {
staggerStart(() => reportFlow.start(), STEP3_FLOW_STAGGER_MS); // 仍并行绘制,只是起跑错开
}
// ③ 大屏多页(模式二):优先 dashboardJson(按当前主题渲染);旧版 marker html 兜底。
// 带上大屏生成过程的完整对话(文字 + 步骤条),恢复时整段回显。错峰 ~600ms。
const dashSaved = {
dialogues: rep?.dashboardDialogues,
seatStubMessages: rep?.dashboardSeatStubMessages,
};
if (rep?.dashboardJson) {
dashboardFlow.hydrateFromJson(rep.dashboardJson, dashSaved);
} else if (rep?.html && isDashboardHtml(rep.html)) {
dashboardFlow.hydrateSaved(rep.html, dashSaved);
} else {
staggerStart(() => dashboardFlow.start(), STEP3_FLOW_STAGGER_MS * 2);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [summaryFlow, reportFlow, dashboardFlow, clearStep3StartTimers]);
// 切换草稿:先停掉旧草稿的在途 Step3 流(防串台)。声明在 hydrate effect **之前**,
// 保证同一提交内先 abort 旧流、再由下方 effect hydrate 新草稿,顺序不颠倒。
// **仅在真正切到另一份草稿时中断**——草稿首次创建(null→id)不是切换,那时三流可能正
// 跑着(尤其大屏绘制),误 abort 会让产物没存上、随后还被重新 hydrate 重画。
const abortPrevDraftIdRef = useRef(draftId);
useEffect(() => {
const prev = abortPrevDraftIdRef.current;
abortPrevDraftIdRef.current = draftId;
if (prev && draftId && prev !== draftId) {
clearStep3StartTimers(); // 切草稿:取消尚未触发的错峰 start,防打到新草稿上下文
summaryFlow.abort();
reportFlow.abort();
dashboardFlow.abort();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [draftId]);
// 进第三步 / 切草稿后仍停留第三步:按当前草稿快照恢复或新建三流(闸门已在渲染期复位)。
useEffect(() => {
if (currentStep !== 3 || step3EnteredRef.current) return;
hydrateStep3Flows();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentStep, draftId]);
const reset = useCallback(() => {
clearStep3StartTimers();
summaryFlow.abort();
reportFlow.abort();
dashboardFlow.abort();
step3EnteredRef.current = false;
}, [clearStep3StartTimers, summaryFlow, reportFlow, dashboardFlow]);
return {
summaryFlow,
reportFlow,
dashboardFlow,
reset,
};
}