feat: 流程图连线后端确定性推导(弱模型也连对) + 校验不删边/不简化理由 + 调度中即时反馈

- 连线质量:新增 _derive_layered_edges——连线不再信任模型,后端按节点 group 分层 +
  层级对应关系确定性推导(任务→各目的扇出、相邻等量层按下标一一对应、不规则层对回退模型边)。
  模型只需 group 标对 + 各层内按相同对应顺序排列,连线就一定对。SOUL/_STRUCTURE 据此调整。
- 校验不删边:validate 要求节点数≥原图 + gen() 兜底「校验后节点/边变少则回退抽取结果」。
- 不简化理由:_SHAPE + SOUL detail 改为完整理由原样保留、不简化/精简。
- 调度中即时反馈:ask() 意图分类前立刻建「调度中」气泡,定意图后各分支复用(relabelDialogue
  + runFlowExtract.bubbleId),不再等 intent 接口返回才出内容。
- 测试 tests/test_flow_extract_parse.py 加层级连线推导 3 例(共 10 过)。

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:56:04 +08:00
1 parent 0890b3c375
commit cc06d3f4d2
3 files changed
+112 -21

No files matched your search

@@ -501,6 +501,11 @@ export function useStep3Summary(
);
}, []);
/** 改某气泡的发送方名称(意图分类期间先建中性「调度中」气泡,定了意图再改成对应智能体名复用)。 */
const relabelDialogue = useCallback((id: number, sender: string) => {
setDialogues((prev) => prev.map((d) => (d.id === id ? { ...d, sender } : d)));
}, []);
const appendStepIfNew = useCallback(
(
bubbleId: number,
@@ -989,17 +994,22 @@ export function useStep3Summary(
const runFlowExtract = useCallback(
async (
aborter: AbortController,
opts?: { instruction?: string; currentGraph?: FlowGraphData | null },
opts?: { instruction?: string; currentGraph?: FlowGraphData | null; bubbleId?: number },
): Promise<void> => {
const isEdit = !!opts?.instruction && !!opts?.currentGraph;
setFlowPhase("extracting");
if (!isEdit) setFlowGraphState(null); // 编辑模式保留旧图直到新图就绪(防画布闪空)
// 两个气泡:抽取/修改 + 校验——把过程(思考 `<think>` + 文字 + flow-json 代码块)都留着展示,
// 由 Step2Message 渲染(思考折叠块 + markdown 代码块),不折叠成一句话。
const extractBubbleId = appendStreamingDialogue({
sender: isEdit ? "结构化流程图 · 修改" : "结构化流程图 · 抽取",
avatarType: "blue",
});
// 传入 bubbleId(意图分类期间已建的「调度中」气泡)则复用并改名,避免空窗 + 不重复建气泡。
const extractSender = isEdit ? "结构化流程图 · 修改" : "结构化流程图 · 抽取";
let extractBubbleId: number;
if (opts?.bubbleId != null) {
extractBubbleId = opts.bubbleId;
relabelDialogue(extractBubbleId, extractSender);
} else {
extractBubbleId = appendStreamingDialogue({ sender: extractSender, avatarType: "blue" });
}
let validateBubbleId: number | null = null;
try {
const graph = await streamFlowExtract({
@@ -1055,6 +1065,7 @@ export function useStep3Summary(
appendStreamingDialogue,
appendToDialogue,
finalizeDialogue,
relabelDialogue,
],
);
@@ -1176,9 +1187,14 @@ export function useStep3Summary(
// 意图识别(参考 ai_writing 一次性 JSON 范式):先判断用户想 重做报告 / 改流程图 /
// 重画流程图 / 提问 / 模糊待澄清,再分流——不再用关键词硬路由(避免「说重做报告却出流程图」)。
// 带附件时跳过分类(附件多用于报告 Q&A/改报告);澄清后用户点选则用 forcedIntent 跳过分类。
// reuseBubbleId:意图分类期间先建的「调度中」气泡,定了意图后由对应分支复用(不等接口返回、像第一步)。
let reuseBubbleId: number | null = null;
if (text && !hasFiles) {
let intent: FlowIntent | undefined = opts?.forcedIntent;
let dispatchBubbleId: number | null = null;
if (!intent) {
// **立刻**显示「智能体调度中」气泡(不等意图接口返回),消除连接卡顿时的"卡住"感。
dispatchBubbleId = appendStreamingDialogue({ sender: "智能体", avatarType: "blue" });
const r = await classifyFlowIntent({
message: text,
hasReport: !!currentMdRef.current.trim(),
@@ -1188,6 +1204,9 @@ export function useStep3Summary(
intent = r.intent;
}
if (intent === "unclear") {
if (dispatchBubbleId != null) {
finalizeDialogue(dispatchBubbleId, "意图不太明确,请在下方选择你想做的:", "待确认");
}
setPendingFlowClarification(text); // 组件渲染选项让用户自己定
return;
}
@@ -1195,13 +1214,16 @@ export function useStep3Summary(
const flowAborter = new AbortController();
aborterRef.current = flowAborter;
const cur = flowGraphRef.current;
const reuse = dispatchBubbleId ?? undefined;
if (intent === "modify_flow" && cur) {
await runFlowExtract(flowAborter, { instruction: text, currentGraph: cur });
await runFlowExtract(flowAborter, { instruction: text, currentGraph: cur, bubbleId: reuse });
} else {
await runFlowExtract(flowAborter);
await runFlowExtract(flowAborter, { bubbleId: reuse });
}
return;
}
// regenerate_report | qa → 复用调度气泡走下方总结线程路径。
reuseBubbleId = dispatchBubbleId;
// intent === "regenerate_report" | "qa" → 落到下方总结线程续问路径:
// **追加**用户气泡 + 在原线程上让智能体重写报告 / 增量改 / 回答(**不清空历史**)。
// regenerate_report 不再走 run()(run 会 setDialogues([]) 清空,导致"覆盖聊天记录、
@@ -1248,11 +1270,15 @@ export function useStep3Summary(
}));
}
// 用户气泡已在 ask 开头立刻追加(不等模型/分类返回),这里不再重复。
const bubbleId = appendStreamingDialogue({
sender: "方案总结报告智能体",
avatarType: "blue",
});
// 用户气泡已在 ask 开头立刻追加。智能体气泡:复用意图分类期间的「调度中」气泡(改名),
// 没有则新建——这样提问/重做报告时分类阶段就有「调度中」反馈、不等模型返回(像第一步)。
let bubbleId: number;
if (reuseBubbleId != null) {
bubbleId = reuseBubbleId;
relabelDialogue(bubbleId, "方案总结报告智能体");
} else {
bubbleId = appendStreamingDialogue({ sender: "方案总结报告智能体", avatarType: "blue" });
}
setPhase("streaming");
if (hadLiveThread) {
@@ -1313,6 +1339,8 @@ export function useStep3Summary(
freezeStreaming,
appendUserDialogue,
appendStreamingDialogue,
relabelDialogue,
finalizeDialogue,
streamOnce,
runFlowExtract,
],