From aea7724cdb06341577a7ec3d5d1fab074df8d102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Wed, 3 Jun 2026 16:27:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20embed=20=E8=B7=A8=E5=9F=9F?= =?UTF-8?q?=E5=B9=B6=E6=8C=81=E4=B9=85=E5=8C=96=E5=9C=86=E6=A1=8C=20Step3?= =?UTF-8?q?=20=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gateway 默认 CORS 白名单增加 5174,embed 文档补充跨域配置说明 - 圆桌草稿新增 step3 快照字段与迁移,结果绘制 HTML 可随草稿恢复 Co-authored-by: Cursor --- frontend-web/docs/embed-iframe-chat.md | 22 +- .../src/roundtable-planning/api/drafts.ts | 29 ++- .../components/HighFidelityReport.tsx | 9 + .../components/Step3Panel.tsx | 8 + .../components/Step3ReportMode.tsx | 164 ++++++++++++--- .../hooks/useDraftPersistence.ts | 31 ++- .../hooks/useStep3Report.ts | 189 +++++++++++++++--- .../pages/RoundtablePlanningPage.tsx | 15 ++ .../backend/app/gateway/app.py | 4 + .../roundtable-report/SOUL.md | 71 ++++--- .../app/gateway/routers/multi_agent.py | 12 +- .../app/gateway/routers/roundtable_drafts.py | 4 + .../20260603_01_roundtable_draft_step3.py | 49 +++++ .../persistence/roundtable_drafts/model.py | 4 + .../persistence/roundtable_drafts/sql.py | 5 + 15 files changed, 527 insertions(+), 89 deletions(-) create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260603_01_roundtable_draft_step3.py diff --git a/frontend-web/docs/embed-iframe-chat.md b/frontend-web/docs/embed-iframe-chat.md index 84cf84b8..45ed5de2 100644 --- a/frontend-web/docs/embed-iframe-chat.md +++ b/frontend-web/docs/embed-iframe-chat.md @@ -348,8 +348,26 @@ const timer = setInterval(async () => { ### 5.3 跨域注意 -- 父页面 `fetch` 的是 **Gateway 源**(如 `8001`),不是 iframe 的 Vite 源;需 Gateway 配置 CORS 允许父页面 origin(若跨域)。 -- iframe `src` 的前端域名需与你们部署一致;后端地址由 DeerFlow 前端 `.env` 的 `VITE_BACKEND_BASE_URL` 决定。 +父页面从浏览器 `fetch` Gateway(如 `http://127.0.0.1:8001`)时,**Origin 必须是后端 CORS 白名单里的完整地址**(含协议、主机、端口),与 API 地址是否写 `localhost` / `127.0.0.1` 无关。 + +**本地开发**(前端默认 `5174`)请在 Gateway 环境变量中配置: + +```bash +# offline-backend-20260512/.env 或启动脚本注入 +GATEWAY_CORS_ORIGINS=http://localhost:5174,http://127.0.0.1:5174,http://localhost:5173,http://127.0.0.1:5173 +``` + +修改后**重启 Gateway**(`make dev` / `make gateway`)。若父页面部署在其它域名,把该域名的 origin 一并加入逗号分隔列表。 + +临时调试(勿用于公网生产): + +```bash +GATEWAY_CORS_ALLOW_ALL=1 +``` + +常见报错:`No 'Access-Control-Allow-Origin' header` → 当前页面的 origin 未出现在 `GATEWAY_CORS_ORIGINS` 中。 + +### 5.4 其它部署注意 --- diff --git a/frontend-web/src/roundtable-planning/api/drafts.ts b/frontend-web/src/roundtable-planning/api/drafts.ts index b878ab31..f038503a 100644 --- a/frontend-web/src/roundtable-planning/api/drafts.ts +++ b/frontend-web/src/roundtable-planning/api/drafts.ts @@ -56,6 +56,22 @@ export interface DraftStep2Snapshot { chain?: { id: string; title: string } | null; } +/** + * Step 3「结果绘制」snapshot — the generated visualization report, stored inline + * so a reloaded draft can show it without depending on the ephemeral report + * 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). */ + summary?: string; +} + /** Lightweight metadata row returned by the list endpoint. */ export interface DraftMeta { id: string; @@ -70,6 +86,7 @@ export interface DraftMeta { export interface DraftRecord extends DraftMeta { step1: DraftStep1Snapshot | null; step2: DraftStep2Snapshot | null; + step3: DraftStep3Snapshot | null; } /** Fields accepted when creating a draft. */ @@ -80,6 +97,7 @@ export interface DraftCreatePayload { furthestStep: 1 | 2 | 3; step1: DraftStep1Snapshot; step2: DraftStep2Snapshot; + step3?: DraftStep3Snapshot | null; } /** Partial update — send only what changed. Omitting `title` preserves a rename. */ @@ -88,6 +106,7 @@ export interface DraftUpdatePayload { furthestStep?: 1 | 2 | 3; step1?: DraftStep1Snapshot; step2?: DraftStep2Snapshot; + step3?: DraftStep3Snapshot | null; } const BASE = () => `${getBackendBaseURL()}/api/roundtable-drafts`; @@ -115,6 +134,7 @@ interface ServerDraftMeta { interface ServerDraftFull extends ServerDraftMeta { step1: DraftStep1Snapshot | null; step2: DraftStep2Snapshot | null; + step3: DraftStep3Snapshot | null; } function toMeta(s: ServerDraftMeta): DraftMeta { @@ -129,7 +149,12 @@ function toMeta(s: ServerDraftMeta): DraftMeta { } function toRecord(s: ServerDraftFull): DraftRecord { - return { ...toMeta(s), step1: s.step1 ?? null, step2: s.step2 ?? null }; + return { + ...toMeta(s), + step1: s.step1 ?? null, + step2: s.step2 ?? null, + step3: s.step3 ?? null, + }; } function fromCreate(p: DraftCreatePayload) { @@ -139,6 +164,7 @@ function fromCreate(p: DraftCreatePayload) { furthest_step: p.furthestStep, step1: p.step1, step2: p.step2, + ...(p.step3 !== undefined ? { step3: p.step3 } : {}), }; } @@ -148,6 +174,7 @@ function fromUpdate(p: DraftUpdatePayload) { if (p.furthestStep !== undefined) out.furthest_step = p.furthestStep; if (p.step1 !== undefined) out.step1 = p.step1; if (p.step2 !== undefined) out.step2 = p.step2; + if (p.step3 !== undefined) out.step3 = p.step3; return out; } diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 74a30069..8cb7a0d0 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -26,6 +26,7 @@ import { Step3ReportMode, type ModelOption, } from "@/roundtable-planning/components/Step3ReportMode"; +import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts"; import type { ReportIntent } from "@/roundtable-planning/hooks/useStep3Report"; /** @@ -70,6 +71,10 @@ interface HighFidelityReportProps { availableModels: ReadonlyArray; modelsLoading: boolean; setSelectedModel: (model: string) => void; + /** 草稿里已存的上次绘制结果 —— 透传给「结果绘制」恢复展示。 */ + savedReport?: DraftStep3Snapshot | null; + /** 一次绘制完成时回调,把结果交给上层存进草稿 step3。 */ + onReportSaved?: (snapshot: DraftStep3Snapshot) => void; /** Resets state and navigates back to Step 1. */ onReset: () => void; } @@ -107,6 +112,8 @@ export const HighFidelityReport: React.FC = ({ availableModels, modelsLoading, setSelectedModel, + savedReport, + onReportSaved, onReset, }) => { // 「结果绘制」模式:把现有报告内容收起到左栏,右侧由方案可视化智能体绘制图表 HTML。 @@ -249,6 +256,8 @@ export const HighFidelityReport: React.FC = ({ availableModels={availableModels} modelsLoading={modelsLoading} setSelectedModel={setSelectedModel} + savedReport={savedReport} + onReportSaved={onReportSaved} onBack={() => setDrawMode(false)} /> ); diff --git a/frontend-web/src/roundtable-planning/components/Step3Panel.tsx b/frontend-web/src/roundtable-planning/components/Step3Panel.tsx index 6249f4c8..d4f4e640 100644 --- a/frontend-web/src/roundtable-planning/components/Step3Panel.tsx +++ b/frontend-web/src/roundtable-planning/components/Step3Panel.tsx @@ -8,6 +8,7 @@ * 拉取 / 文本类预览 / window.open 下载 / 「重新起草」按钮等。 */ import { HighFidelityReport } from "@/roundtable-planning/components/HighFidelityReport"; +import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts"; import type { ModelOption } from "@/roundtable-planning/components/Step3ReportMode"; import type { Step2Dialogue } from "@/roundtable-planning/hooks/useStep2Orchestration"; import type { ReportIntent } from "@/roundtable-planning/hooks/useStep3Report"; @@ -30,6 +31,9 @@ export interface Step3PanelProps { availableModels: ReadonlyArray; modelsLoading: boolean; setSelectedModel: (model: string) => void; + /** 草稿里已存的上次绘制结果 + 完成回调 —— 透传给「结果绘制」做持久化。 */ + savedReport?: DraftStep3Snapshot | null; + onReportSaved?: (snapshot: DraftStep3Snapshot) => void; onReset: () => void; } @@ -46,6 +50,8 @@ export function Step3Panel(props: Step3PanelProps) { availableModels, modelsLoading, setSelectedModel, + savedReport, + onReportSaved, onReset, } = props; @@ -74,6 +80,8 @@ export function Step3Panel(props: Step3PanelProps) { availableModels={availableModels} modelsLoading={modelsLoading} setSelectedModel={setSelectedModel} + savedReport={savedReport} + onReportSaved={onReportSaved} onReset={onReset} /> ); diff --git a/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx b/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx index 18e91dae..616700de 100644 --- a/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx +++ b/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx @@ -20,11 +20,15 @@ import { FileText, ListOrdered, Loader2, + PanelLeftClose, + PanelLeftOpen, RefreshCw, Sparkles, X, } from "lucide-react"; +import { useArtifacts } from "@/components/workspace/artifacts"; +import { cn } from "@/lib/utils"; import { buildArtifactDownloadUrl, listSessionArtifacts, @@ -36,6 +40,7 @@ import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToB import { Step2Message } from "@/roundtable-planning/components/Step2Panel"; import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout"; import { markdownComponents } from "@/roundtable-planning/components/markdown-components"; +import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts"; import { useAutoScroll } from "@/roundtable-planning/hooks/useAutoScroll"; import { useStep3Report, @@ -65,6 +70,10 @@ interface Step3ReportModeProps { availableModels: ReadonlyArray; modelsLoading: boolean; setSelectedModel: (model: string) => void; + /** 草稿里已存的上次绘制结果 —— 有则进入时恢复展示,不重新绘制。 */ + savedReport?: DraftStep3Snapshot | null; + /** 一次绘制完成时回调,把结果交给上层存进草稿 step3。 */ + onReportSaved?: (snapshot: DraftStep3Snapshot) => void; onBack: () => void; } @@ -82,6 +91,8 @@ export function Step3ReportMode({ availableModels, modelsLoading, setSelectedModel, + savedReport, + onReportSaved, onBack, }: Step3ReportModeProps) { const reportSeatDeliveries = useMemo( @@ -97,6 +108,7 @@ export function Step3ReportMode({ seatStubMessages, activeArtifactThreadId, start, + hydrateSaved, redraw, abort, openArtifactFromStep, @@ -106,16 +118,35 @@ export function Step3ReportMode({ seatDeliveries: reportSeatDeliveries, consensusText, selectedModel, + savedReport, + onReportSaved, }); const [modal, setModal] = useState(null); + // 左栏「研讨原料」收起态:打开沙箱时默认收起成图标条(对齐第 2 步侧栏收起), + // 关闭沙箱时自动展开;中途用户可手动切换(effect 只在沙箱开关变化的边沿触发)。 + const { open: artifactsOpen } = useArtifacts(); + const [leftCollapsed, setLeftCollapsed] = useState(false); + const prevArtifactsOpenRef = useRef(artifactsOpen); + useEffect(() => { + if (artifactsOpen !== prevArtifactsOpenRef.current) { + prevArtifactsOpenRef.current = artifactsOpen; + setLeftCollapsed(artifactsOpen); + } + }, [artifactsOpen]); + // 进入即自动开跑;卸载(含返回普通视图)中断在途请求。 // 不加"永久闸门"——否则 StrictMode 的 mount→unmount→remount 会在 cleanup 里 abort // 掉首次 run,remount 又因闸门跳过重启,导致永远不绘制。run() 自带 runningRef 去重, // remount 重新 start() 即可(开发期至多多建一个空线程,生产无 StrictMode 只跑一次)。 + // 若草稿里已存上次绘制结果(savedReport)→ 直接恢复展示,不重新跑模型;用户可点「重新绘制」覆盖。 useEffect(() => { - start(); + if (savedReport?.html) { + hydrateSaved(savedReport); + } else { + start(); + } return () => abort(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -178,34 +209,86 @@ export function Step3ReportMode({ {/* 主体:左源卡 + 右沙箱 */}
- {/* 左栏:收起的源内容 */} -