From 60cc6665cebbf9afb38d1f67ea2fa179555211de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Wed, 17 Jun 2026 20:38:51 +0800 Subject: [PATCH 1/9] tij --- frontend-web/.env.production | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend-web/.env.production b/frontend-web/.env.production index d1fc2180..97415742 100644 --- a/frontend-web/.env.production +++ b/frontend-web/.env.production @@ -5,7 +5,7 @@ VITE_USE_REAL_DATA=true # 任务详情(登录深链 cop-task-detail)数据源:默认真实接口;改成 true 走本地假数据(调试用)。 -VITE_MOCK_COP_TASK_DETAIL=true +VITE_MOCK_COP_TASK_DETAIL=false # Gateway / DeerFlow API VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow From d3880f51b38060aad7435c017306f5bcc338429c Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Wed, 17 Jun 2026 21:31:22 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E7=AC=AC=E4=B8=89=E6=AD=A5=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=E8=BE=93=E5=87=BA=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roundtable-planning/api/flow-extract.ts | 106 ++++ .../components/FlowGraphPanel.tsx | 162 +++++- .../components/Step3SummaryMode.tsx | 61 ++- .../hooks/useStep3Summary.ts | 241 ++++++--- .../lib/flow-graph.test.ts | 51 ++ .../src/roundtable-planning/lib/flow-graph.ts | 58 ++ .../pages/RoundtablePlanningPage.tsx | 5 +- .../backend/app/gateway/app.py | 4 + .../app/gateway/routers/flow_extract.py | 510 ++++++++++++++++++ offline-backend-20260512/backend/config.yaml | 24 +- 10 files changed, 1095 insertions(+), 127 deletions(-) create mode 100644 frontend-web/src/roundtable-planning/api/flow-extract.ts create mode 100644 offline-backend-20260512/backend/app/gateway/routers/flow_extract.py diff --git a/frontend-web/src/roundtable-planning/api/flow-extract.ts b/frontend-web/src/roundtable-planning/api/flow-extract.ts new file mode 100644 index 00000000..ebf2188e --- /dev/null +++ b/frontend-web/src/roundtable-planning/api/flow-extract.ts @@ -0,0 +1,106 @@ +/** + * Step3 流程图「后端抽取 + 校验」SSE 客户端。 + * + * 对应后端 `POST /api/flow-extract/stream`:后端独立调模型(不走总结线程,避开 coordinatorPrompt + * 的报告锚定)两段式产图——①流式抽取 flow-json(实时吐回,前端原样显示给用户看)→ ②模型校验/修复 + * → 返回校验后的结构化图数据 `{title, nodes, edges}`。 + * + * 这样把不稳定的「文本→结构」抽取 + 校验放后端,前端只负责实时显示抽取过程 + 拿最终图绘制。 + */ +import { apiFetch } from "@/core/api/fetch-client"; +import { getBackendBaseURL } from "@/core/config"; +import type { FlowGraphData } from "@/roundtable-planning/lib/flow-graph"; + +export interface FlowExtractRequest { + /** 报告 Markdown(优先数据来源)。 */ + reportMd?: string | null; + /** 各席位交付(无报告时的数据来源)。 */ + seatDeliveries?: Array<{ sender: string; content: string }>; + /** 业务链层级(stages 的 goal)。 */ + chainLevels?: string[]; + /** 业务链总控编排提示(写明层级时优先用它)。 */ + coordinatorPrompt?: string | null; + /** 推理模型名(沿用第三步所选)。 */ + model?: string; + /** Phase 2 问答修改:当前流程图 + 指令 → 按指令改图(其余不变)。 */ + currentGraph?: FlowGraphData | null; + instruction?: string; + signal?: AbortSignal; + /** 抽取流式增量(含 `` 思考 + 正文 + JSON,前端原样渲染:思考块 + 代码块)。 */ + onExtractDelta?: (text: string) => void; + /** 抽取结束、进入校验阶段。 */ + onValidating?: () => void; + /** 校验/修复流式增量(同样含思考 + 正文,前端实时显示校验过程)。 */ + onValidateDelta?: (text: string) => void; +} + +/** + * 消费 SSE,返回校验后的最终图。抽取阶段通过 `onExtractDelta` 实时回吐; + * 出错(后端 error 帧 / 网络)抛异常。 + */ +export async function streamFlowExtract(req: FlowExtractRequest): Promise { + const res = await apiFetch(`${getBackendBaseURL()}/api/flow-extract/stream`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + report_md: req.reportMd ?? null, + seat_deliveries: (req.seatDeliveries ?? []).map((s) => ({ sender: s.sender, content: s.content })), + chain_levels: req.chainLevels ?? [], + coordinator_prompt: req.coordinatorPrompt ?? null, + model: req.model, + current_graph: req.currentGraph ?? null, + instruction: req.instruction ?? null, + }), + signal: req.signal, + }); + if (!res.ok || !res.body) { + const text = await res.text().catch(() => ""); + throw new Error(`flow-extract failed (${res.status}): ${text}`); + } + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let graph: FlowGraphData | null = null; + let errored: string | null = null; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const rawLine of lines) { + const line = rawLine.trimEnd(); + if (!line.startsWith("data:")) continue; + const data = line.slice(5).trim(); + if (!data) continue; + let frame: Record; + try { + frame = JSON.parse(data) as Record; + } catch { + continue; + } + const event = typeof frame.event === "string" ? frame.event : ""; + if (event === "extract_delta") { + const c = typeof frame.content === "string" ? frame.content : ""; + if (c) req.onExtractDelta?.(c); + } else if (event === "validating") { + req.onValidating?.(); + } else if (event === "validate_delta") { + const c = typeof frame.content === "string" ? frame.content : ""; + if (c) req.onValidateDelta?.(c); + } else if (event === "final") { + graph = (frame.graph as FlowGraphData) ?? null; + } else if (event === "error") { + errored = typeof frame.detail === "string" ? frame.detail : "未知错误"; + } + } + } + + if (errored) throw new Error(errored); + if (!graph || !Array.isArray(graph.nodes) || graph.nodes.length === 0) { + throw new Error("flow-extract 未返回有效流程图"); + } + return graph; +} diff --git a/frontend-web/src/roundtable-planning/components/FlowGraphPanel.tsx b/frontend-web/src/roundtable-planning/components/FlowGraphPanel.tsx index f19cf47b..f49e1425 100644 --- a/frontend-web/src/roundtable-planning/components/FlowGraphPanel.tsx +++ b/frontend-web/src/roundtable-planning/components/FlowGraphPanel.tsx @@ -15,6 +15,7 @@ import { useRef, useState, } from "react"; +import { createPortal } from "react-dom"; import { applyNodeChanges, Background, @@ -32,11 +33,14 @@ import { } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { + Check, FileText, Loader2, LocateFixed, Maximize2, Minimize2, + Pencil, + Trash2, Workflow, X, ZoomIn, @@ -45,6 +49,8 @@ import { import { cn } from "@/lib/utils"; import { + applyNodeEdit, + deleteNodeFromGraph, FLOW_NODE_TYPE_META, FLOW_NODE_W, layoutFlowGraph, @@ -59,6 +65,8 @@ interface FlowGraphPanelProps { onClose: () => void; /** 沙箱里有 md 报告时提供:工具栏显示「MD 报告」按钮,点击切回 Markdown 预览。 */ onSwitchToReport?: () => void; + /** Phase 3:画布手动编辑——传入则节点详情弹窗出现「编辑 / 删除」,回写整张新图。 */ + onChange?: (next: FlowGraphData) => void; } // ── 自定义节点(按 type 着色的卡片)──────────────────────────────────────── @@ -106,6 +114,7 @@ function FlowCanvasInner({ onToggleFullscreen, onClose, onSwitchToReport, + onChange, }: FlowGraphPanelProps & { fullscreen: boolean; onToggleFullscreen: () => void }) { const rf = useReactFlow(); const [selectedId, setSelectedId] = useState(null); @@ -276,13 +285,14 @@ function FlowCanvasInner({ - {/* 节点详情弹窗 */} + {/* 节点详情弹窗(传入 onChange 时可编辑 / 删除节点) */} {selectedNode && ( setSelectedId(null)} onJump={(id) => setSelectedId(id)} + onChange={onChange} /> )} @@ -317,17 +327,38 @@ function NodeDetailModal({ data, onClose, onJump, + onChange, }: { node: FlowGraphNode; data: FlowGraphData; onClose: () => void; onJump: (id: string) => void; + /** Phase 3:传入则可编辑 / 删除该节点(回写整张新图)。 */ + onChange?: (next: FlowGraphData) => void; }) { const meta = FLOW_NODE_TYPE_META[node.type]; const labelOf = (id: string) => data.nodes.find((n) => n.id === id)?.label ?? id; const incoming = data.edges.filter((e) => e.to === node.id); const outgoing = data.edges.filter((e) => e.from === node.id); + const [editing, setEditing] = useState(false); + const [label, setLabel] = useState(node.label); + const [group, setGroup] = useState(node.group ?? ""); + const [detail, setDetail] = useState(node.detail ?? ""); + + const handleSave = () => { + onChange?.(applyNodeEdit(data, node.id, { label, group, detail })); + setEditing(false); + onClose(); + }; + const handleDelete = () => { + if (typeof window !== "undefined" && !window.confirm(`删除节点「${node.label}」及其连线?此操作不可撤销。`)) { + return; + } + onChange?.(deleteNodeFromGraph(data, node.id)); + onClose(); + }; + return (
-
-
-
- 详细说明 -
-

- {node.detail?.trim() || "(该节点未提供详细说明)"} -

+ + {editing ? ( +
+ + +
- {incoming.length > 0 && ( - ({ id: e.from, label: labelOf(e.from), edgeLabel: e.label }))} onJump={onJump} /> - )} - {outgoing.length > 0 && ( - ({ id: e.to, label: labelOf(e.to), edgeLabel: e.label }))} onJump={onJump} /> - )} -
+ ) : ( +
+
+
详细说明
+

+ {node.detail?.trim() || "(该节点未提供详细说明)"} +

+
+ {incoming.length > 0 && ( + ({ id: e.from, label: labelOf(e.from), edgeLabel: e.label }))} onJump={onJump} /> + )} + {outgoing.length > 0 && ( + ({ id: e.to, label: labelOf(e.to), edgeLabel: e.label }))} onJump={onJump} /> + )} +
+ )} + + {/* 编辑 / 删除 操作栏(仅传入 onChange 时显示) */} + {onChange && ( +
+ {editing ? ( + <> + + + + ) : ( + <> + + + + )} +
+ )}
); } +function EditField({ + label, + value, + onChange, + multiline, +}: { + label: string; + value: string; + onChange: (v: string) => void; + multiline?: boolean; +}) { + return ( +
+
{label}
+ {multiline ? ( +