@
feat(圆桌): Step3 三产物并行 + 大屏多页模式 + 后台挂起查看恢复流程 后台挂起 Step3: - engine 总结报告/结果绘制/大屏数据 asyncio.gather 三流并行,故障隔离 - request.state 回归修复(进程内虚拟 request 补 state),恢复后台报告 - ROUNDTABLE_JOB_GATEWAY 默认 inprocess;产物自愈重试(截断/缺失纠偏) - 大屏后端只产 report-json(dashboardJson),前端按当前主题渲染 结果绘制大屏多页模式(模式二): - report-template 模板渲染(report-json→大屏HTML),零截断、主题跟随 - 与模式一同款交互(流式+沙箱+可修改);沙箱新窗口打开按钮 最终方案展示:左侧席位栏+总控头像、交付/文件合并、收展动画、内联沙箱 后台挂起查看=恢复实时流程: - handleLoadDraft 返回 Promise,await hydrate 后订阅 streamJob SSE 跟随 - saveDraftNow 强制存(发起前+查看前各一次)防丢 第二步优化:总控判断串/并行(并行单列可写)、席位上下文+交付模板、 peerDeliveries、失败重试、共识标记、流程图小窗(缩放/拖动/并行批) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> @
This commit is contained in:
1 parent
a7596001c5
commit
d5f319d9cb
51 files changed
+5016
-1186
No files matched your search
@@ -40,7 +40,7 @@ import type {
|
||||
Step2Dialogue,
|
||||
StubAiMessage,
|
||||
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||
import { detectFlowIntent } from "@/roundtable-planning/lib/flow-graph";
|
||||
import { detectFlowIntent, hasFlowFence } from "@/roundtable-planning/lib/flow-graph";
|
||||
import { nowTime } from "@/roundtable-planning/lib/roundtable-constants";
|
||||
|
||||
/** 报告输出文件的固定虚拟路径(恢复历史快照时用它重建 stub)。 */
|
||||
@@ -87,6 +87,8 @@ export interface UseStep3SummaryReturn {
|
||||
pause: () => void;
|
||||
/** 中断在途请求(卸载 / 切回普通视图时调用)。 */
|
||||
abort: () => void;
|
||||
/** 本流是否允许**自动**打开沙箱预览(视图挂载=true / 卸载=false;并行流防抢沙箱)。 */
|
||||
setPreviewEnabled: (enabled: boolean) => void;
|
||||
openArtifactFromStep: (
|
||||
dialogue: Step2Dialogue,
|
||||
step: NonNullable<Step2Dialogue["steps"]>[number],
|
||||
@@ -164,7 +166,8 @@ function buildSummaryPrompt(opts: {
|
||||
"请基于以下圆桌多智能体研讨的成果,产出一份**结构化的方案总结**。" +
|
||||
"用户已明确要求结构化输出/流程图,因此本轮:" +
|
||||
"①**不要**调用 write_file,**不要**生成任何 .md 文件;" +
|
||||
"②把精炼的文字版总结报告直接写在对话回复里(供用户直接阅读,结构遵循编排提示的要求);" +
|
||||
"②把精炼的文字版总结报告直接写在对话回复里(供用户直接阅读,结构遵循编排提示的要求;" +
|
||||
"编排提示未指定结构时,默认对各席位交付内容逐席位格式化输出:席位名→核心结论→关键交付要点→风险/建议);" +
|
||||
"③在回复最末尾**必须**输出一个完整的 ```flow-json 代码块(前端据此实时绘制可交互流程图)。",
|
||||
);
|
||||
} else {
|
||||
@@ -201,9 +204,35 @@ function buildSummaryPrompt(opts: {
|
||||
*/
|
||||
const FLOW_ASK_INSTRUCTION =
|
||||
"(系统指令:用户要求结构化输出/流程图——本轮不要写或修改任何 .md 报告文件," +
|
||||
"在回复正文给出简短的文字说明/总结即可,并在回复最末尾输出一个**完整**的 ```flow-json 代码块" +
|
||||
"在回复正文给出简短的文字说明/总结(用户未指定结构时,默认对各席位交付内容逐席位格式化输出:" +
|
||||
"席位名→核心结论→关键交付要点),并在回复最末尾输出一个**完整**的 ```flow-json 代码块" +
|
||||
"(按你职责设定的「结构化流程数据」契约;输出全量数据而非增量)。)";
|
||||
|
||||
/**
|
||||
* 上一轮主产物是流程图、但本条消息没显式提「结构化/流程图」时的意图引导指令。
|
||||
* 历史痛点:用户画完流程图后说「把验收环节加上」「改一下」,关键词不命中 →
|
||||
* 智能体把它当改报告/问答处理,用户被迫说「重新结构化输出」才能改图。
|
||||
* 这里把「上一轮主产物」上下文显式告诉智能体,让它按语义判断、太模糊就反问。
|
||||
*/
|
||||
const FLOW_CONTEXT_ASK_INSTRUCTION =
|
||||
"(系统指令:上一轮的主产物是结构化流程图。请按语义理解用户这条消息、不要求任何关键词:" +
|
||||
"若是修改/调整类意图且没明确说要改文字报告,默认视为**修改流程图**——不要写 .md 文件," +
|
||||
"在回复末尾输出**完整修订版** ```flow-json 代码块(全量节点+连线,非增量);" +
|
||||
"若明确要改文字报告才用 write_file 整文件重写;若只是提问就只用文字回答;" +
|
||||
"若意图太模糊、无法判断要改什么,不要修改任何产物,在正文向用户提一个简短的澄清问题(给出可选方向)。)";
|
||||
|
||||
/**
|
||||
* 普通报告语境下的意图引导指令(每轮续问都附上):
|
||||
* 按语义理解修改/提问/结构化意图,太模糊就在正文反问,不要乱改。
|
||||
*/
|
||||
const SEMANTIC_ASK_INSTRUCTION =
|
||||
"(系统指令:请按语义理解用户这条消息的意图、不要求任何关键词:" +
|
||||
"修改类意见(哪怕表述笼统)就在原报告基础上增量修改并用 write_file 整文件重写;" +
|
||||
"要求结构化/格式化/可视化(或语义等价说法)就进入流程图模式——不写 .md 文件," +
|
||||
"正文默认对各席位交付内容逐席位格式化输出,末尾输出完整 ```flow-json 块;" +
|
||||
"只是提问就只用文字回答、不改文件;" +
|
||||
"若意图太模糊、无法判断要改什么,不要修改任何产物,在正文向用户提一个简短的澄清问题(给出可选方向)。)";
|
||||
|
||||
/** 恢复草稿后线程已不在内存(无 live thread)时,新建线程续问用的 prompt:带上旧报告做底。 */
|
||||
function buildResumeAskPrompt(opts: {
|
||||
intentReady: ReportIntent | null;
|
||||
@@ -212,10 +241,10 @@ function buildResumeAskPrompt(opts: {
|
||||
coordinatorPrompt?: string | null;
|
||||
previousMd: string;
|
||||
userMessage: string;
|
||||
/** 用户消息命中结构化/流程图意图 → 指示不写文件、直出 flow-json。 */
|
||||
flowIntent?: boolean;
|
||||
/** 按本轮语境选好的意图引导指令(流程图显式 / 流程图上下文 / 普通语义),追加在消息后。 */
|
||||
askInstruction?: string;
|
||||
}): string {
|
||||
const { previousMd, userMessage, flowIntent, ...materials } = opts;
|
||||
const { previousMd, userMessage, askInstruction, ...materials } = opts;
|
||||
const parts: string[] = [];
|
||||
parts.push(
|
||||
"这是一次对已有「方案总结报告」的继续对话。下面先给出研讨原料与你之前已生成的报告," +
|
||||
@@ -234,9 +263,9 @@ function buildResumeAskPrompt(opts: {
|
||||
}
|
||||
parts.push("# 用户的新消息");
|
||||
parts.push(userMessage.trim());
|
||||
if (flowIntent) {
|
||||
if (askInstruction) {
|
||||
parts.push("");
|
||||
parts.push(FLOW_ASK_INSTRUCTION);
|
||||
parts.push(askInstruction);
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
@@ -283,7 +312,12 @@ export function useStep3Summary(
|
||||
setArtifacts,
|
||||
select: selectArtifact,
|
||||
setOpen: setArtifactsOpen,
|
||||
open: artifactsOpen,
|
||||
} = useArtifacts();
|
||||
const artifactsOpenRef = useRef(artifactsOpen);
|
||||
useEffect(() => {
|
||||
artifactsOpenRef.current = artifactsOpen;
|
||||
}, [artifactsOpen]);
|
||||
|
||||
const [phase, setPhase] = useState<SummaryPhase>("idle");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -303,6 +337,10 @@ export function useStep3Summary(
|
||||
const liveThreadRef = useRef<string | null>(null);
|
||||
// 当前报告 md 全文(续问写底 / 存草稿用)。
|
||||
const currentMdRef = useRef<string>("");
|
||||
// 上一轮主产物是否为流程图(flow-json)——续问意图判断的上下文锚点:
|
||||
// 为 true 时用户的笼统修改意见(「加个环节」「改一下」)默认按改流程图引导,
|
||||
// 不再要求用户说出「结构化输出/流程图」等关键词。
|
||||
const flowActiveRef = useRef(false);
|
||||
const lastAutoOpenedUrlRef = useRef<string | null>(null);
|
||||
|
||||
// ── 对话气泡三段式(append → delta → finalize)+ 工具步骤(与第 2 步同款)──
|
||||
@@ -483,8 +521,36 @@ export function useStep3Summary(
|
||||
[selectArtifact, setArtifactsOpen],
|
||||
);
|
||||
|
||||
// 预览开关(2026-06-13,hook 提升页面层后并行流共用全局沙箱):只有用户当前所在
|
||||
// 视图的流允许**自动**打开沙箱,否则总结/绘制并行时互相抢预览(选中产物与当前视图
|
||||
// 线程对不上 → 空沙箱)。显式用户点击(openArtifactFromStep 等)不受限。
|
||||
const previewEnabledRef = useRef(false);
|
||||
const lastAutoThreadRef = useRef<string | null>(null);
|
||||
const setPreviewEnabled = useCallback(
|
||||
(enabled: boolean) => {
|
||||
previewEnabledRef.current = enabled;
|
||||
// 进入视图接管:**仅当沙箱面板已打开**时把选中产物换成本流的(修串台);
|
||||
// 面板关着保持关闭(尊重"进第三步默认关沙箱")。
|
||||
if (
|
||||
enabled &&
|
||||
lastAutoOpenedUrlRef.current &&
|
||||
lastAutoThreadRef.current &&
|
||||
artifactsOpenRef.current
|
||||
) {
|
||||
openArtifact(lastAutoThreadRef.current, lastAutoOpenedUrlRef.current);
|
||||
}
|
||||
},
|
||||
[openArtifact],
|
||||
);
|
||||
|
||||
const autoOpenArtifact = useCallback(
|
||||
(threadId: string, virtualUrl: string) => {
|
||||
lastAutoThreadRef.current = threadId;
|
||||
if (!previewEnabledRef.current) {
|
||||
// 视图不在前台:只记录,不抢沙箱(回到视图时 setPreviewEnabled 再接管)。
|
||||
lastAutoOpenedUrlRef.current = virtualUrl;
|
||||
return;
|
||||
}
|
||||
if (lastAutoOpenedUrlRef.current === virtualUrl) return;
|
||||
openArtifact(threadId, virtualUrl);
|
||||
},
|
||||
@@ -618,6 +684,7 @@ export function useStep3Summary(
|
||||
|
||||
const openRealArtifactFallback = useCallback(
|
||||
async (threadId: string) => {
|
||||
if (!previewEnabledRef.current) return; // 不在前台不抢沙箱
|
||||
try {
|
||||
const res = await listSessionArtifacts(threadId);
|
||||
const file = res.files.find((f) => /\.md$/i.test(f.name)) ?? res.files[0];
|
||||
@@ -697,7 +764,7 @@ export function useStep3Summary(
|
||||
path: SUMMARY_OUTPUT_PATH,
|
||||
content: md,
|
||||
});
|
||||
if (opts?.open === false) return;
|
||||
if (opts?.open === false || !previewEnabledRef.current) return;
|
||||
const virtualUrl = buildWriteFileVirtualUrl(SUMMARY_OUTPUT_PATH, messageId, toolCallId);
|
||||
openArtifact(threadId, virtualUrl);
|
||||
},
|
||||
@@ -720,6 +787,8 @@ export function useStep3Summary(
|
||||
flowRound?: boolean;
|
||||
}): Promise<void> => {
|
||||
const { bubbleId, prompt, aborter, seedThreadId, forceReconcile, files, flowRound } = args;
|
||||
// 本轮开始前的报告底稿——结束后对比判断「这一轮的主产物是报告还是流程图」。
|
||||
const mdBefore = currentMdRef.current;
|
||||
|
||||
let threadId = seedThreadId ?? null;
|
||||
let agentName = SUMMARY_AGENT_NAME;
|
||||
@@ -771,6 +840,14 @@ export function useStep3Summary(
|
||||
});
|
||||
if (aborter.signal.aborted) return;
|
||||
|
||||
// 跟踪本轮主产物:回复带 flow-json → 流程图轮;md 有实质更新 → 报告轮;
|
||||
// 都没有(纯问答轮)→ 维持上一轮状态。供下一次 ask() 选意图引导指令。
|
||||
if (hasFlowFence(res.content)) {
|
||||
flowActiveRef.current = true;
|
||||
} else if (md.trim() && md !== mdBefore) {
|
||||
flowActiveRef.current = false;
|
||||
}
|
||||
|
||||
finalizeDialogue(bubbleId, res.content, "总结完成");
|
||||
setPhase("done");
|
||||
|
||||
@@ -825,6 +902,7 @@ export function useStep3Summary(
|
||||
try {
|
||||
// 编排提示里明确要求了结构化输出/流程图 → 首轮就走流程图模式(不写 md,直出 flow-json)。
|
||||
const flowMode = detectFlowIntent(coordinatorPrompt);
|
||||
flowActiveRef.current = flowMode; // 流式结束后会按真实产物校正
|
||||
const prompt = buildSummaryPrompt({
|
||||
intentReady,
|
||||
seatDeliveries,
|
||||
@@ -865,8 +943,19 @@ export function useStep3Summary(
|
||||
abort();
|
||||
if (wasBusy) freezeStreaming("已打断");
|
||||
|
||||
// 输入框消息命中结构化/流程图意图 → 本轮直出 flow-json、不写 md(md 留在沙箱可切回)。
|
||||
// 意图引导分三档(智能体按语义最终裁决,前端只给上下文,不做硬分类):
|
||||
// ① 显式命中结构化/流程图关键词 → 直出 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;
|
||||
|
||||
runningRef.current = true;
|
||||
setError(null);
|
||||
@@ -910,11 +999,11 @@ export function useStep3Summary(
|
||||
setPhase("streaming");
|
||||
|
||||
if (hadLiveThread) {
|
||||
// 真线程仍在 → 续问,发原始消息,靠线程上下文 + SOUL 自判改/答。
|
||||
// 命中流程图意图时附上系统指令(不写文件、末尾输出全量 flow-json)。
|
||||
// 真线程仍在 → 续问,发原始消息 + 按语境选好的意图引导指令,
|
||||
// 靠线程上下文 + SOUL 让智能体按语义自判改图 / 改报告 / 问答 / 反问澄清。
|
||||
const base =
|
||||
text || "我上传了参考文件,请先阅读文件内容,再据此更新报告或回答。";
|
||||
const prompt = flowIntent ? `${base}\n\n${FLOW_ASK_INSTRUCTION}` : base;
|
||||
const prompt = `${base}\n\n${askInstruction}`;
|
||||
await streamOnce({
|
||||
bubbleId,
|
||||
prompt,
|
||||
@@ -922,7 +1011,7 @@ export function useStep3Summary(
|
||||
seedThreadId: liveThreadRef.current,
|
||||
forceReconcile: true,
|
||||
files: fileRefs,
|
||||
flowRound: flowIntent,
|
||||
flowRound,
|
||||
});
|
||||
} else {
|
||||
// 无 live thread(恢复草稿 / 首轮没建好就被打断)→ prompt 带研讨原料 + 旧报告做底。
|
||||
@@ -935,7 +1024,7 @@ export function useStep3Summary(
|
||||
previousMd: currentMdRef.current,
|
||||
userMessage:
|
||||
text || "请阅读我上传的参考文件后,据此更新报告或回答。",
|
||||
flowIntent,
|
||||
askInstruction,
|
||||
});
|
||||
await streamOnce({
|
||||
bubbleId,
|
||||
@@ -944,7 +1033,7 @@ export function useStep3Summary(
|
||||
seedThreadId: threadId,
|
||||
forceReconcile: true,
|
||||
files: fileRefs,
|
||||
flowRound: flowIntent,
|
||||
flowRound,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1021,7 +1110,20 @@ export function useStep3Summary(
|
||||
stubRef.current = stub;
|
||||
currentMdRef.current = snap.md;
|
||||
liveThreadRef.current = realThreadId;
|
||||
lastAutoOpenedUrlRef.current = null;
|
||||
// 快照收尾语带 flow-json → 上次主产物是流程图,恢复后续问沿用流程图语境。
|
||||
flowActiveRef.current = hasFlowFence(snap.closing);
|
||||
// 记录预览锚点(不打开):进入本视图且沙箱已开着时可立即接管显示自家 md(防串台)。
|
||||
if (hasMd) {
|
||||
lastAutoOpenedUrlRef.current = buildWriteFileVirtualUrl(
|
||||
SUMMARY_OUTPUT_PATH,
|
||||
messageId,
|
||||
toolCallId,
|
||||
);
|
||||
lastAutoThreadRef.current = synthThreadId;
|
||||
} else {
|
||||
lastAutoOpenedUrlRef.current = null;
|
||||
lastAutoThreadRef.current = null;
|
||||
}
|
||||
setSeatStubMessages(stub);
|
||||
setSummaryThreadId(synthThreadId);
|
||||
setActiveArtifactThreadId(synthThreadId);
|
||||
@@ -1052,13 +1154,11 @@ export function useStep3Summary(
|
||||
]);
|
||||
setError(null);
|
||||
setPhase("done");
|
||||
|
||||
if (hasMd) {
|
||||
const virtualUrl = buildWriteFileVirtualUrl(SUMMARY_OUTPUT_PATH, messageId, toolCallId);
|
||||
openArtifact(synthThreadId, virtualUrl);
|
||||
}
|
||||
// 不再自动打开沙箱预览(2026-06-12):进入第三步默认关闭沙箱,只有开始
|
||||
// 写文件(live write_file 流式)时才自动打开;恢复历史时用户点气泡里的
|
||||
// 「点击预览」步骤卡随时可开(stub 已种好)。
|
||||
},
|
||||
[abort, buildWriteFileVirtualUrl, openArtifact],
|
||||
[abort, buildWriteFileVirtualUrl],
|
||||
);
|
||||
|
||||
const busy = phase === "creating" || phase === "streaming";
|
||||
@@ -1077,6 +1177,7 @@ export function useStep3Summary(
|
||||
ask,
|
||||
pause,
|
||||
abort,
|
||||
setPreviewEnabled,
|
||||
openArtifactFromStep,
|
||||
openArtifactPreview,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user