From 2d74f8883e20ba98ed9cb260e2db7d31b9df94e5 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Thu, 11 Jun 2026 21:05:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?Step3=20=E6=80=BB=E7=BB=93=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E4=B8=8E=E9=A1=B6=E6=A0=8F=E4=B8=BB=E9=A2=98=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 roundtable-summary 智能体及前后端 Step3 Markdown 总结流程;顶栏在浅/深主题下分别使用红/深蓝横幅背景。 --- .../src/roundtable-planning/api/drafts.ts | 44 +- .../roundtable-planning/api/multi-agent.ts | 25 + .../components/HighFidelityReport.tsx | 56 +- .../components/Step3Panel.tsx | 16 +- .../components/Step3SummaryMode.tsx | 843 ++++++++++++++++ .../hooks/useStep3Summary.ts | 933 ++++++++++++++++++ .../pages/RoundtablePlanningPage.tsx | 15 + .../src/strategy-components/Header.tsx | 6 +- frontend-web/src/styles/globals.css | 13 + .../app/gateway/roundtable_run_policy.py | 20 +- .../app/gateway/routers/_roundtable_seed.py | 8 +- .../roundtable-summary/SOUL.md | 73 ++ .../roundtable-summary/config.yaml | 3 + .../app/gateway/routers/multi_agent.py | 23 + 14 files changed, 2060 insertions(+), 18 deletions(-) create mode 100644 frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx create mode 100644 frontend-web/src/roundtable-planning/hooks/useStep3Summary.ts create mode 100644 offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-summary/SOUL.md create mode 100644 offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-summary/config.yaml diff --git a/frontend-web/src/roundtable-planning/api/drafts.ts b/frontend-web/src/roundtable-planning/api/drafts.ts index ac9ba0ff..1906a07b 100644 --- a/frontend-web/src/roundtable-planning/api/drafts.ts +++ b/frontend-web/src/roundtable-planning/api/drafts.ts @@ -103,14 +103,44 @@ export interface DraftStep2RunSnapshot { * thread. Optional — drafts saved before this feature (or never drawn) won't carry it. */ export interface DraftStep3Snapshot { - /** The full self-contained HTML written to outputs/方案总览.html. */ - html: string; - /** ISO timestamp when the report was generated. */ - generatedAt: string; - /** The inference model used to draw it. */ - model: string; - /** The agent's closing one-liner (rendered as the report message bubble). */ + /** + * The full self-contained HTML written to outputs/方案总览.html by the + * `roundtable-report` (HTML 看板) agent. Optional — a draft may only carry the + * Markdown 总结报告 (`summaryReport`) and never draw the HTML 看板. + */ + html?: string; + /** ISO timestamp when the HTML report was generated. */ + generatedAt?: string; + /** The inference model used to draw the HTML report. */ + model?: string; + /** The HTML agent's closing one-liner (rendered as the report message bubble). */ summary?: string; + /** + * Step 3「总结报告」snapshot — output of the `roundtable-summary` agent + * (Markdown report). Stored alongside (not instead of) the HTML report so the + * two Step-3 surfaces persist independently. Nested in the free-form step3 JSON + * blob, so the backend needs no schema change. Optional. + */ + summaryReport?: DraftStep3SummarySnapshot | null; +} + +/** + * Step 3「总结报告」snapshot — the Markdown report written by `roundtable-summary`, + * stored inline so a reloaded draft can show it without depending on the ephemeral + * summary thread. `threadId` keeps the real summary thread id so a reloaded draft + * can continue Q&A / refinement on the same thread (the agent keeps its context). + */ +export interface DraftStep3SummarySnapshot { + /** The full Markdown written to outputs/方案总结报告.md. */ + md: string; + /** The real summary thread id — reused for follow-up Q&A / refine after reload. */ + threadId?: string; + /** ISO timestamp when the summary was generated. */ + generatedAt: string; + /** The inference model used to write it. */ + model: string; + /** The agent's closing chat text (the report body it echoed into the conversation). */ + closing?: string; } /** Lightweight metadata row returned by the list endpoint. */ diff --git a/frontend-web/src/roundtable-planning/api/multi-agent.ts b/frontend-web/src/roundtable-planning/api/multi-agent.ts index 961fab42..bdb833f2 100644 --- a/frontend-web/src/roundtable-planning/api/multi-agent.ts +++ b/frontend-web/src/roundtable-planning/api/multi-agent.ts @@ -818,3 +818,28 @@ export async function initReportThread( const json = (await res.json()) as { agent_name: string; thread_id: string }; return { agentName: json.agent_name, threadId: json.thread_id }; } + +/** + * Create a thread for the Step 3「总结报告」summary agent (`roundtable-summary`, + * a built-in singleton — distinct from `roundtable-report`; this only spins up a + * cheap thread, it never re-creates the agent). Run it afterwards with + * {@link streamMultiAgent} (`agentType: "special"`, `agentName: "roundtable-summary"`, + * `dAgentThreadId: { [agentName]: threadId }`). Follow-up Q&A / refinements reuse + * the SAME thread (do not re-init) so the agent keeps the report context. + */ +export async function initSummaryThread( + options: { signal?: AbortSignal } = {}, +): Promise { + const res = await apiFetch(`${getBackendBaseURL()}/api/multi-agent/summary/init`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + signal: options.signal, + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`init summary thread failed (${res.status}): ${text}`); + } + const json = (await res.json()) as { agent_name: string; thread_id: string }; + return { agentName: json.agent_name, threadId: json.thread_id }; +} diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 7ffe0069..45226bb4 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -27,7 +27,11 @@ import { Step3ReportMode, type ModelOption, } from "@/roundtable-planning/components/Step3ReportMode"; -import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts"; +import { Step3SummaryMode } from "@/roundtable-planning/components/Step3SummaryMode"; +import type { + DraftStep3Snapshot, + DraftStep3SummarySnapshot, +} from "@/roundtable-planning/api/drafts"; import type { ReportIntent } from "@/roundtable-planning/hooks/useStep3Report"; interface SeatDelivery { @@ -62,6 +66,12 @@ interface HighFidelityReportProps { savedReport?: DraftStep3Snapshot | null; /** 一次绘制完成时回调,把结果交给上层存进草稿 step3。 */ onReportSaved?: (snapshot: DraftStep3Snapshot) => void; + /** 业务链写给总控的编排提示 —— 注入「总结报告」智能体作为最高优先级口径。 */ + coordinatorPrompt?: string | null; + /** 草稿里已存的上次总结结果 —— 透传给「总结报告」恢复展示。 */ + savedSummary?: DraftStep3SummarySnapshot | null; + /** 一次总结完成时回调,把结果交给上层存进草稿 step3.summaryReport。 */ + onSummarySaved?: (snapshot: DraftStep3SummarySnapshot) => void; /** Resets state and navigates back to Step 1. */ onReset: () => void; openArtifactPreview: (threadId: string, path: string) => void; @@ -103,6 +113,9 @@ export const HighFidelityReport: React.FC = ({ setSelectedModel, savedReport, onReportSaved, + coordinatorPrompt, + savedSummary, + onSummarySaved, onReset, openArtifactPreview, seatStubMessages, @@ -115,6 +128,9 @@ export const HighFidelityReport: React.FC = ({ return `圆桌总控结论-${short}`; }, [intentReady?.objective]); + // 第 3 步**主页** = 总结报告(进第三步默认自动跑「方案总结报告智能体」)。 + // 用户点「最终方案展示」切到静态页(summaryMode=false),再从静态页可进「结果绘制」HTML。 + const [summaryMode, setSummaryMode] = useState(true); const [drawMode, setDrawMode] = useState(false); const [fileSandboxMode, setFileSandboxMode] = useState(false); const [fileSandboxTarget, setFileSandboxTarget] = useState<{ @@ -126,8 +142,11 @@ export const HighFidelityReport: React.FC = ({ // 不会自动关)。点「结果绘制」后 Step3ReportMode 会按需开自己的沙箱,互不串扰。 const { deselect: deselectArtifact } = useArtifacts(); useEffect(() => { + // 总结报告主页(summaryMode)由 Step3SummaryMode 自管沙箱——这里不 deselect, + // 否则会盖掉它在恢复草稿时同步打开的 md 预览。切到静态页时才清空第二步遗留沙箱。 + if (summaryMode) return; deselectArtifact(); - }, [deselectArtifact]); + }, [deselectArtifact, summaryMode]); // Build the per-seat list (excludes the coordinator). useMemo so the file // fetch effect doesn't re-fire on every parent render. const seatThreadEntries = useMemo(() => { @@ -248,6 +267,28 @@ export const HighFidelityReport: React.FC = ({ [], ); + if (summaryMode) { + return ( + setSummaryMode(false)} + /> + ); + } + if (fileSandboxMode && fileSandboxTarget) { return ( = ({ ))} + + + + + + {/* 主体:左源卡 + 右沙箱 */} +
+ + + {/* 右栏:智能体进度 + 沙箱(复用第 2 步) */} +
+ + + +
+
+ + {/* 弹窗 */} + {modal === "consensus" && ( + setModal(null)} + headerAction={ + + } + > + {consensusText ? ( + + {consensusText} + + ) : ( +

暂无总控结论。

+ )} +
+ )} + + {modal === "seats" && ( + setModal(null)}> + + + )} + + {modal === "files" && ( + setModal(null)} + /> + )} + + ); +} + +// ── 左栏源卡 ───────────────────────────────────────────────────────────── + +function SourceCard({ + icon, + title, + hint, + badge, + disabled, + onClick, +}: { + icon: React.ReactNode; + title: string; + hint: string; + badge?: string; + disabled?: boolean; + onClick: () => void; +}) { + return ( + + ); +} + +function CollapsedSourceCard({ + icon, + title, + disabled, + onClick, +}: { + icon: React.ReactNode; + title: string; + disabled?: boolean; + onClick: () => void; +}) { + return ( + + ); +} + +// ── 右栏:智能体进度面板(沙箱的 60% 区域)+ 底部对话输入框 ────────────────── + +function AgentProgressPanel({ + phase, + dialogues, + error, + canAsk, + onRetry, + onAsk, + onArtifactStepClick, + onArtifactPreview, + selectedModel, + availableModels, + modelsLoading, + setSelectedModel, +}: { + phase: ReturnType["phase"]; + dialogues: ReturnType["dialogues"]; + error: string | null; + canAsk: boolean; + onRetry: () => void; + onAsk: (message: string) => void; + onArtifactStepClick: ReturnType["openArtifactFromStep"]; + onArtifactPreview: ReturnType["openArtifactPreview"]; + selectedModel: string; + availableModels: ReadonlyArray; + modelsLoading: boolean; + setSelectedModel: (model: string) => void; +}) { + const busy = phase === "creating" || phase === "streaming"; + const scrollRef = useRef(null); + const { atBottom, scrollToBottom } = useAutoScroll(scrollRef, [dialogues]); + const [askText, setAskText] = useState(""); + + const handleAskSubmit = (e: FormEvent) => { + e.preventDefault(); + const text = askText.trim(); + if (!text || !canAsk) return; + setAskText(""); + onAsk(text); + }; + + return ( +
+
+
+
+ 📝 +
+
+
方案总结报告智能体
+
据各席位成果撰写 Markdown 总结报告
+
+
+ +
+ + {/* 消息列表 —— 与第 2 步同款气泡(流式正文 + 工具步骤卡 + 文件卡)。 */} +
+
+ {phase === "creating" && dialogues.length === 0 && ( +
+ + 正在初始化总结线程… +
+ )} + + {dialogues.map((msg) => ( + onArtifactStepClick(msg, step)} + onFileCardClick={(path) => { + if (msg.agentThreadId) onArtifactPreview(msg.agentThreadId, path); + }} + /> + ))} + + {busy && ( +
+ + 报告正文实时输出,右侧沙箱同步预览 Markdown +
+ )} + + {phase === "error" && ( +
+
总结失败
+
{error}
+ +
+ )} + + {phase === "done" && ( +
+ + 已生成方案总结报告,右侧可预览 / 下载 .md;可在下方继续提修改意见或提问。 +
+ )} +
+ {!atBottom && } +
+
+