feat: 第三步流程图后端抽取智能体 + 意图识别 + 问答/画布编辑

- 新增专职「结构化抽取智能体」roundtable-structure(种子资产 SOUL+config,注册进 seeder
  启动 seed+sync 进 DB,可在智能体管理页编辑、未来挂入库技能)。
- /api/flow-extract/stream 改用该 agent 的 SOUL(load_agent_soul,缺失回退内置默认) +
  模型(前端所选 > agent 配置 > 默认);抽取/校验/编辑统一。SOUL 先输出简要分析过程再出
  flow-json,模型慢时也有实时反馈(含 <think> 思考流)。
- 新增 /api/flow-extract/intent 意图识别(参考 ai_writing 一次性 JSON 范式):识别
  重做报告/改流程图/重画流程图/提问/模糊;前端按意图分流,模糊则发起澄清让用户点选,
  不再用关键词硬路由(修「说重做报告却出流程图」)。
- Phase 2 问答修改:edit 模式(当前图+指令→改图→校验);Phase 3 画布手动改/删节点。
- 修:用户消息发送即上屏(不等模型/分类返回);regenerate_report 不再清空对话历史;
  全屏 portal 到 body 工具栏恢复;解析兼容独立 edges 数组。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
fengxiangdebaoziandClaude Opus 4.8 committed 2026-06-17 22:19:02 +08:00
1 parent ed887f762e
commit 0890b3c375
7 files changed
+346 -62

No files matched your search

@@ -41,7 +41,7 @@ import type {
StubAiMessage,
} from "@/roundtable-planning/hooks/useStep2Orchestration";
import { detectFlowIntent, extractFlowGraph, hasFlowFence, serializeFlowGraph, type FlowGraphData } from "@/roundtable-planning/lib/flow-graph";
import { streamFlowExtract } from "@/roundtable-planning/api/flow-extract";
import { classifyFlowIntent, streamFlowExtract, type FlowIntent } from "@/roundtable-planning/api/flow-extract";
import { nowTime } from "@/roundtable-planning/lib/roundtable-constants";
/** 报告输出文件的固定虚拟路径(恢复历史快照时用它重建 stub)。 */
@@ -88,6 +88,10 @@ export interface UseStep3SummaryReturn {
regenerateFlow: () => void;
/** 手动编辑流程图(Phase 3:画布上改/删节点后回写 + 持久化)。 */
setFlowGraph: (next: FlowGraphData) => void;
/** 意图模糊时挂起的原始消息(非空 → 组件展示「重做报告/改图/重画/提问」选项让用户定)。 */
pendingFlowClarification: string | null;
/** 用户在澄清里点选意图后执行。 */
resolveFlowClarification: (choice: FlowIntent) => void;
summaryThreadId: string | null;
dialogues: Step2Dialogue[];
seatStubMessages: Record<string, StubAiMessage[]>;
@@ -417,6 +421,8 @@ export function useStep3Summary(
useEffect(() => {
flowGraphRef.current = flowGraph;
}, [flowGraph]);
// 意图模糊时挂起待澄清的原始消息(组件渲染选项让用户自己定)。
const [pendingFlowClarification, setPendingFlowClarification] = useState<string | null>(null);
const [summaryThreadId, setSummaryThreadId] = useState<string | null>(null);
const [dialogues, setDialogues] = useState<Step2Dialogue[]>([]);
const [seatStubMessages, setSeatStubMessages] = useState<
@@ -1149,7 +1155,7 @@ export function useStep3Summary(
]);
const ask = useCallback(
async (message: string, files?: File[]) => {
async (message: string, files?: File[], opts?: { forcedIntent?: FlowIntent }) => {
const text = message.trim();
const hasFiles = !!(files && files.length > 0);
if (!text && !hasFiles) return;
@@ -1160,38 +1166,54 @@ export function useStep3Summary(
abort();
if (wasBusy) freezeStreaming("已打断");
// 流程图相关消息 → 走后端「抽取/编辑 + 校验」端点(流式显示过程 → 校验后绘制),
// 不再在总结线程吐 flow-json 做实时绘制。判定:
// · 已有流程图 且(显式 flow 关键词 或 上一轮主产物是流程图)→ **问答修改**(带当前图+指令,Phase 2);
// · 还没有流程图 但显式要流程图 → **首次生成**。
// 普通报告修改 / 问答(无 flow 上下文)仍走下方总结线程路径。
const flowIntentNow = detectFlowIntent(text);
const curGraph = flowGraphRef.current;
if (flowIntentNow || (curGraph && flowActiveRef.current)) {
appendUserDialogue(text);
const flowAborter = new AbortController();
aborterRef.current = flowAborter;
if (curGraph) {
await runFlowExtract(flowAborter, { instruction: text, currentGraph: curGraph });
} else {
await runFlowExtract(flowAborter);
}
return;
// 用户气泡**立刻上屏**(不等意图分类 / 模型返回,避免连接卡顿时显得卡)——对齐第一/二步。
// 后续各分支都不再重复追加用户气泡。forcedIntent(澄清后点选复跑)时原气泡已在、跳过避免重复。
if (!opts?.forcedIntent) {
const fileNote = hasFiles ? `(附 ${files!.length} 个文件)` : "";
appendUserDialogue((text || "(上传了参考文件)") + fileNote);
}
// 意图引导分三档(智能体按语义最终裁决,前端只给上下文,不做硬分类):
// ① 显式命中结构化/流程图关键词 → 直出 flow-json、不写 md;
// ② 没命中关键词但上一轮主产物是流程图 → 流程图上下文引导(笼统修改默认改图,太模糊则反问);
// ③ 普通报告语境 → 语义理解修改/提问/结构化意图,太模糊则反问。
const flowIntent = detectFlowIntent(text);
const flowContext = !flowIntent && flowActiveRef.current;
const askInstruction = flowIntent
? FLOW_ASK_INSTRUCTION
: flowContext
? FLOW_CONTEXT_ASK_INSTRUCTION
: SEMANTIC_ASK_INSTRUCTION;
// 流程图语境轮:不主动弹 md 沙箱预览(用户多半要看的是画布;真写了文件预览仍会自动打开)。
const flowRound = flowIntent || flowContext;
// 意图识别(参考 ai_writing 一次性 JSON 范式):先判断用户想 重做报告 / 改流程图 /
// 重画流程图 / 提问 / 模糊待澄清,再分流——不再用关键词硬路由(避免「说重做报告却出流程图」)。
// 带附件时跳过分类(附件多用于报告 Q&A/改报告);澄清后用户点选则用 forcedIntent 跳过分类。
if (text && !hasFiles) {
let intent: FlowIntent | undefined = opts?.forcedIntent;
if (!intent) {
const r = await classifyFlowIntent({
message: text,
hasReport: !!currentMdRef.current.trim(),
hasFlow: !!flowGraphRef.current,
model: selectedModel || undefined,
});
intent = r.intent;
}
if (intent === "unclear") {
setPendingFlowClarification(text); // 组件渲染选项让用户自己定
return;
}
if (intent === "regenerate_flow" || intent === "modify_flow") {
const flowAborter = new AbortController();
aborterRef.current = flowAborter;
const cur = flowGraphRef.current;
if (intent === "modify_flow" && cur) {
await runFlowExtract(flowAborter, { instruction: text, currentGraph: cur });
} else {
await runFlowExtract(flowAborter);
}
return;
}
// intent === "regenerate_report" | "qa" → 落到下方总结线程续问路径:
// **追加**用户气泡 + 在原线程上让智能体重写报告 / 增量改 / 回答(**不清空历史**)。
// regenerate_report 不再走 run()(run 会 setDialogues([]) 清空,导致"覆盖聊天记录、
// 不显示用户输入"的 bug);改报告/重写报告由总结智能体在续问里整文件重写。
// intent === "qa" → 落到下方总结线程问答路径(只答不改产物)。
}
// 走到这里只剩 regenerate_report / qa(含带附件场景)—— 流程图操作已在上面意图分流处理。
// 一律走报告/问答语义引导(让总结智能体在原线程上重写报告 / 增量改 / 回答),
// **不再**在总结线程里产 flow-json(流程图统一由后端抽取端点负责)。
const askInstruction = SEMANTIC_ASK_INSTRUCTION;
const flowRound = false;
runningRef.current = true;
setError(null);
@@ -1226,8 +1248,7 @@ export function useStep3Summary(
}));
}
const fileNote = hasFiles ? `(附 ${files!.length} 个文件)` : "";
appendUserDialogue((text || "(上传了参考文件)") + fileNote);
// 用户气泡已在 ask 开头立刻追加(不等模型/分类返回),这里不再重复。
const bubbleId = appendStreamingDialogue({
sender: "方案总结报告智能体",
avatarType: "blue",
@@ -1287,6 +1308,7 @@ export function useStep3Summary(
seatDeliveries,
consensusText,
coordinatorPrompt,
selectedModel,
abort,
freezeStreaming,
appendUserDialogue,
@@ -1296,6 +1318,17 @@ export function useStep3Summary(
],
);
/** 澄清后用户点选意图 → 用 forcedIntent 复跑(跳过意图分类)。 */
const resolveFlowClarification = useCallback(
(choice: FlowIntent) => {
const msg = pendingFlowClarification;
setPendingFlowClarification(null);
if (!msg) return;
void ask(msg, undefined, { forcedIntent: choice });
},
[pendingFlowClarification, ask],
);
/** 暂停当前生成:中断在途请求 + 把流式气泡收尾为「已暂停」+ 状态回 done。 */
const pause = useCallback(() => {
if (!runningRef.current) return;
@@ -1413,6 +1446,8 @@ export function useStep3Summary(
flowPhase,
regenerateFlow,
setFlowGraph: updateFlowGraph,
pendingFlowClarification,
resolveFlowClarification,
summaryThreadId,
dialogues,
seatStubMessages,