feat: taskId 深链会商按任务隔离草稿 + 第三步自动结构化流程图

- 深链 ?taskId=&goPath=*-A 透传 taskId 进会商,按 taskId 隔离存取草稿(一个 taskId 多条、可新建);
  后端新增 GET /api/roundtable-drafts/by-task/{id} 列表 + RoundtableJobRepository.status_map_by_drafts 富化。
- 第三步生成 md 报告后自动结构化输出流程图(仅 taskId 模式):有报告基于报告、无报告基于各席位交付,
  整段内联数据来源逐条抽取;层级优先用业务链总控提示词写明的层级,否则用 stages 的 goal。
- 流程图节点加 group(层级名)标签 + detail(条目名称+理由);强 prompt 压制 coordinatorPrompt 报告锚定,
  禁阶段编号节点;FLOW_STRUCTURE_RULE 精确画法(根=任务、每层每条成节点、按对应关系串分支、排除汇总段落)。
- flow-graph 解析容错:JSON.parse 失败退回正则逐字段抽取,兼容裸换行/尾逗号,避免节点被整体丢弃。
- 修:重开第二步后第三步基于新一轮分析重新生成(复位 step3 闸门 + 中断旧三流)。

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 19:45:35 +08:00
1 parent e6a41dbc97
commit fca39eeaee
17 files changed
+472 -37

No files matched your search

@@ -57,6 +57,17 @@ interface UseStep3SummaryOptions {
selectedModel: string;
/** 业务链写给总控的编排提示 —— 作为最高优先级口径注入总结智能体。 */
coordinatorPrompt?: string | null;
/**
* 业务链条层级(取自 stages 的「本阶段目标」goal,有序)—— taskId 深链模式下
* 自动结构化输出时,作流程图的层级骨架(任务 → 层级1 → 层级2 → …)。
* 例:链层级 目的→行为体→驱动因素 → 流程图 任务→目的→行为体→驱动因素。
*/
chainLevels?: string[];
/**
* taskId/深链模式:生成 md 报告后**自动**续跑一轮结构化输出(流程图)。
* 普通手动会商为 false —— 结构化输出仍按需(用户输入框打「流程图」才出)。
*/
autoFlow?: boolean;
/** 草稿里已存的上次总结结果 —— 有则进入时恢复展示,不重新生成。 */
savedSummary?: DraftStep3SummarySnapshot | null;
/** 一次总结成功完成时回调(传出 md/thread/model/时间/收尾语),供上层存草稿。 */
@@ -66,6 +77,8 @@ interface UseStep3SummaryOptions {
export interface UseStep3SummaryReturn {
phase: SummaryPhase;
error: string | null;
/** taskId/深链模式:md 报告生成后自动续跑流程图(组件据此让流程图画布自动聚焦)。 */
autoFlow: boolean;
summaryThreadId: string | null;
dialogues: Step2Dialogue[];
seatStubMessages: Record<string, StubAiMessage[]>;
@@ -152,6 +165,117 @@ function buildMaterials(opts: {
return parts.join("\n");
}
/**
* flow-json 形状契约(内联)。roundtable-summary 的 runtime SOUL 为空,模型拿不到契约,
* 所有要画流程图的 prompt 都得带上它,否则模型产出的 JSON 可能解析不出节点。
*/
const FLOW_JSON_SHAPE =
"flow-json 严格用下面这个形状(一个 JSON 对象,`nodes` 是节点数组;示例仅示意结构、勿照抄内容):\n" +
"```flow-json\n" +
'{"title":"流程图标题","nodes":[\n' +
' {"id":"n1","label":"任务简短名","type":"start","group":"任务","detail":"任务目标的完整描述"},\n' +
' {"id":"n2","label":"目的核心短语","type":"task","group":"目的","detail":"该目的的完整内容:名称 + 理由全文","from":["n1"]},\n' +
' {"id":"n3","label":"行为体核心短语","type":"role","group":"行为体","detail":"该行为体的完整内容:名称 + 理由全文","from":["n2"]}\n' +
"]}\n" +
"```\n" +
"字段:`id`(n1/n2… 唯一短串)、`label`(≤12 字,节点标题,取条目核心短语)、" +
"`type`(取 start/end/stage/task/decision/risk/deliverable/role 之一,仅决定配色)、" +
"**`group`(该节点所在的层级名称,如 任务 / 目的 / 行为体 / 预期行为 / 驱动因素 / 叙事——画布按它给节点打标签,必须填)**、" +
"**`detail`(该条目的名称 + 理由 / 说明,点击节点时展示——要把要点表达完整,但控制在约 2–4 句,**不必**逐字照抄报告整段)**、" +
"`from`(前驱节点 id 列表,只引用**前面已出现**的节点)。" +
"节点按先后排列、输出**全量**数据、全程简体中文。";
/**
* 「本轮只画流程图、不写报告」的强约束。关键:有的业务链 coordinatorPrompt 自身写着
* 「输出结构化结论 / 按阶段产出报告 / 最终方案总结报告按层级结构输出」,模型在同一线程里
* 会被它锚定,把用户的「结构化输出」当成「继续写结构化的文字报告」→ 产出 Markdown 而非
* flow-json(实测 deepseek-chat 在这类链上必现)。所以必须显式声明本轮压过那些报告措辞。
*/
const FLOW_ONLY_RULE =
"【硬性·本轮只画流程图】本轮唯一产物是一张**流程图**(由节点 + 连线组成的图),不是文字、不是报告、" +
"不是章节、不是结论清单、不是 Markdown:①**不要**调用 write_file、**不要**写或改任何 .md 文件;" +
"②回复里**不要**再出现任何报告正文 / 章节标题(如「## 四…」)/ 编号结论,正文最多一句话说明;" +
"③**忽略**此前关于「方案总结报告」「按层级结构输出报告」「输出结构化结论」「分阶段产出」的一切措辞——" +
"那些是给上一轮文字报告用的,本轮一律不适用;④在回复**最末尾**输出一个**完整**的 ```flow-json 代码块。";
/**
* 流程图的层级画法(修「结构画错」):直接从已有报告/席位交付里**提取实体**,
* 根=任务名称,每层取具体条目当节点,按报告里的对应关系串成分支,排除汇总/阶段名等无关段落。
*/
const FLOW_STRUCTURE_RULE =
"【流程图画法·严格遵守】\n" +
"① 根节点(n1) = 本次任务本身(即报告「任务总览」里的任务名称 / 目标),label 用任务的简短名称(≤12 字)," +
"**不要**用「任务」「目标」这种泛称当 label;其 `group` 填「任务」;\n" +
"② 从第二层起按层级顺序逐层展开:每个层级在报告/交付里通常列了**多条具体条目**" +
"(如『目的』层有 目的1 / 目的2 / 目的3,『行为体』层有各自对应的行为体,后面还有 预期行为·驱动因素、叙事…)," +
"把**每一条具体条目各画成一个节点**,label 取该条目核心短语(≤12 字);**不要**把一整层合并成一个笼统节点;\n" +
"③ **每个节点的 `group` 必须填它所在层级的名称**(任务 / 目的 / 行为体 / 预期行为 / 驱动因素 / 叙事…,取自报告里该层级的真实名称)——画布据此给节点打分类标签;\n" +
"④ **每个节点的 `detail`** 放该条目的名称 + 理由 / 说明,把要点表达完整、约 2–4 句即可(不必逐字照抄报告整段);" +
"**`detail` 必须写成一行**——内部不要出现真实换行,需要分隔就用「;」或「/」,避免 JSON 里出现裸换行;\n" +
"⑤ 连边严格按报告里的**对应关系**串成分支:根任务 → 各『目的』节点;每个『目的』→ 它对应的『行为体』节点;" +
"每个『行为体』→ 它对应的『预期行为·驱动因素』节点 → 对应的『叙事』节点……每一组各成一条自上而下的分支" +
"(报告里有几组就有几条分支,组与组之间不要混连);\n" +
"⑦ **务必把每个层级的每一条都输出成节点、不要漏层、不要只输出开头一两个节点就停**——" +
"典型规模约 13–16 个节点(如 任务1 + 目的3 + 行为体3 + 预期行为·驱动因素3 + 叙事3);\n" +
"⑥ **只画**这些层级实体节点;**不要**把『综合分析』『核心洞察』『分层传播策略 / 执行路线 / 建议』『结论』" +
"以及『阶段①②③④』等章节名 / 阶段名 / 汇总段落画成节点。";
/** 把各席位交付拼成一段内联材料(无报告时作流程图的数据来源)。 */
function inlineSeatDeliveries(seatDeliveries?: { sender: string; content: string }[]): string {
const list = (seatDeliveries ?? []).filter((s) => (s.content ?? "").trim());
if (list.length === 0) return "";
return list
.map((s, i) => `## 席位${i + 1}:${s.sender || "(未具名席位)"}\n${s.content.trim()}`)
.join("\n\n");
}
/**
* taskId/深链模式:md 报告生成完后自动续跑的「结构化输出」prompt。
*
* 数据基准(用户拍板):**有报告就基于报告**(报告已把各席位交付清晰总结过,直接提取实体、
* 不要再自己重新总结一遍);没有报告则基于**各席位交付产物**。两种来源都**整段内联**进 prompt
* (不依赖线程上下文),让模型逐条对着抽取节点。层级顺序优先用总控提示词里写明的层级,否则 chainLevels。
*/
function buildAutoFlowPrompt(opts: {
chainLevels?: string[];
coordinatorPrompt?: string | null;
reportMd?: string;
seatDeliveries?: { sender: string; content: string }[];
}): string {
const levels = (opts.chainLevels ?? []).map((s) => s.trim()).filter(Boolean);
const cp = opts.coordinatorPrompt?.trim();
const report = opts.reportMd?.trim() ?? "";
const seatMaterial = report ? "" : inlineSeatDeliveries(opts.seatDeliveries);
const parts: string[] = [];
if (report) {
parts.push(
"本轮只画一张流程图。**数据来源 = 下面这份你已写好的《方案总结报告》**(它已把各席位交付清晰总结过)——" +
"请**逐条对着报告抽取**已有的层级条目作为流程图节点,**不要再自己重新总结 / 概括 / 提炼**,也不要凭空新增报告里没有的内容。",
);
} else {
parts.push(
"本轮只画一张流程图。**数据来源 = 下面各席位的交付产物**——请**逐条对着这些交付抽取**已有的层级条目作为流程图节点,不要自己另起总结。",
);
}
parts.push(FLOW_ONLY_RULE);
// 整段内联数据来源(报告全文 / 席位交付),用分隔线包起来便于模型对照抽取。
if (report) {
parts.push("===== 方案总结报告全文(数据来源,逐条抽取,勿改写)=====\n" + report + "\n===== 报告结束 =====");
} else if (seatMaterial) {
parts.push("===== 各席位交付产物(数据来源,逐条抽取,勿改写)=====\n" + seatMaterial + "\n===== 交付结束 =====");
}
parts.push(FLOW_JSON_SHAPE);
parts.push(FLOW_STRUCTURE_RULE);
if (cp) {
parts.push("层级顺序优先采用「业务链总控编排提示」里写明的层级链(如它写的 任务 → 目的 → 行为体 → …)。");
} else if (levels.length > 0) {
parts.push(`层级顺序(自上而下):任务 → ${levels.join(" → ")}。`);
}
// 收尾再强调(首尾夹击,对抗线程里 coordinatorPrompt 的报告锚定)。
parts.push("再次强调:本轮不写任何 Markdown 报告正文 / 章节,整个回复除一句话说明外,只有一个完整的 ```flow-json 代码块。");
return parts.join("\n");
}
/** 首轮总结 prompt。`flowMode`=编排提示里明确要求了结构化输出/流程图 → 跳过 md 文件,直出流程图。 */
function buildSummaryPrompt(opts: {
intentReady: ReportIntent | null;
@@ -203,10 +327,15 @@ function buildSummaryPrompt(opts: {
* 强调「全量 flow-json + 不写文件」——画图数据必须完整自洽,否则前端增量解析会画出半张图。
*/
const FLOW_ASK_INSTRUCTION =
"(系统指令:用户要求结构化输出/流程图——本轮不要写或修改任何 .md 报告文件," +
"在回复正文给出简短的文字说明/总结(用户未指定结构时,默认对各席位交付内容逐席位格式化输出:" +
"席位名→核心结论→关键交付要点),并在回复最末尾输出一个**完整**的 ```flow-json 代码块" +
"(按你职责设定的「结构化流程数据」契约;输出全量数据而非增量)。)";
"(系统指令:用户要求结构化输出 / 流程图——这里的「结构化输出」指的是画一张**流程图**," +
"不是写结构化的文字报告。**有报告就直接基于报告提取层级条目**(报告已总结过各席位交付,不要重复总结);" +
"没有报告则基于各席位交付。\n" +
FLOW_ONLY_RULE +
"\n" +
FLOW_STRUCTURE_RULE +
"\n" +
FLOW_JSON_SHAPE +
")";
/**
* 上一轮主产物是流程图、但本条消息没显式提「结构化/流程图」时的意图引导指令。
@@ -299,6 +428,8 @@ export function useStep3Summary(
consensusText,
selectedModel,
coordinatorPrompt,
chainLevels,
autoFlow,
savedSummary,
onSummarySaved,
} = options;
@@ -901,7 +1032,8 @@ export function useStep3Summary(
try {
// 编排提示里明确要求了结构化输出/流程图 → 首轮就走流程图模式(不写 md,直出 flow-json)。
const flowMode = detectFlowIntent(coordinatorPrompt);
// 但 autoFlow(taskId/深链)模式要「先 md 再结构化输出」,故首轮强制走 md(忽略 detectFlowIntent)。
const flowMode = !autoFlow && detectFlowIntent(coordinatorPrompt);
flowActiveRef.current = flowMode; // 流式结束后会按真实产物校正
const prompt = buildSummaryPrompt({
intentReady,
@@ -911,6 +1043,35 @@ export function useStep3Summary(
flowMode,
});
await streamOnce({ bubbleId, prompt, aborter, flowRound: flowMode });
// taskId/深链模式:报告生成完,**自动**续跑一轮结构化输出(流程图)。
// 数据来源:有报告则内联报告全文、否则内联各席位交付(buildAutoFlowPrompt 处理)。
// 故触发条件放宽为「有报告 **或** 有席位交付」——保证「无报告也能据席位交付出图」。
// 复用同一真线程,与手动「结构化输出」走同一渲染链路。
if (
autoFlow &&
!aborter.signal.aborted &&
liveThreadRef.current &&
(currentMdRef.current.trim() || seatDeliveries.length > 0)
) {
const flowBubbleId = appendStreamingDialogue({
sender: "方案总结报告智能体",
avatarType: "blue",
});
const flowPrompt = buildAutoFlowPrompt({
chainLevels,
coordinatorPrompt,
reportMd: currentMdRef.current,
seatDeliveries,
});
await streamOnce({
bubbleId: flowBubbleId,
prompt: flowPrompt,
aborter,
seedThreadId: liveThreadRef.current,
flowRound: true,
});
}
} catch (err) {
if (aborter.signal.aborted) return;
const msg = err instanceof Error ? err.message : String(err);
@@ -926,6 +1087,8 @@ export function useStep3Summary(
seatDeliveries,
consensusText,
coordinatorPrompt,
chainLevels,
autoFlow,
appendStreamingDialogue,
finalizeDialogue,
streamOnce,
@@ -1166,6 +1329,7 @@ export function useStep3Summary(
return {
phase,
error,
autoFlow: !!autoFlow,
summaryThreadId,
dialogues,
seatStubMessages,