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:
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,
|
||||
|
||||
Reference in new issue
Block a user