feat(roundtable): 第三步流程图后入库(示例技能 · 仅深链 taskId 模式)
- 流程图生成完成后,发起用户协助询问是否入库:现在 / 稍后 / 不入库。 - 仅**深链(taskId)模式**(会商 rwfx-A / 3qfx-A / xdfx-A 等)且入库技能启用时才出现入库 询问 / 协助卡 / 手动入库按钮;普通会商不受影响。 - 「稍后入库」后输入框自由文字做意图识别(后端 /api/flow-extract/ingest-intent: start_ingest / add_data / other / unclear),模糊则发起入库协助;另提供手动入库按钮。 - 入库执行复用总结智能体的同一线程,在消息列表出一条「知识库入库」气泡并显示步骤条 (调度 → 调用入库技能 →(补充内容)→ 提交 → 完成),与第二步同款。 - 入库数据以**用户最后修改后的流程图**为准;并把深链 **taskId 一并传给入库技能**(技能必需)。 - 入库状态(asking/later/done 等)随第三步草稿存取,重载不重复询问。 - 入库技能先用示例(演示用):skills/public/knowledge-base-ingest/SKILL.md(force-add, skills/ 目录默认 gitignore),后续替换为真实入库技能时沿用同名即无缝切换。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b04b830039
commit
19028ba627
8 files changed
+458
-7
No files matched your search
@@ -41,7 +41,12 @@ import type {
|
||||
StubAiMessage,
|
||||
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||
import { detectFlowIntent, extractFlowGraph, hasFlowFence, serializeFlowGraph, type FlowGraphData } from "@/roundtable-planning/lib/flow-graph";
|
||||
import { classifyFlowIntent, streamFlowExtract, type FlowIntent } from "@/roundtable-planning/api/flow-extract";
|
||||
import {
|
||||
classifyFlowIntent,
|
||||
classifyIngestIntent,
|
||||
streamFlowExtract,
|
||||
type FlowIntent,
|
||||
} from "@/roundtable-planning/api/flow-extract";
|
||||
import { nowTime } from "@/roundtable-planning/lib/roundtable-constants";
|
||||
|
||||
/** 报告输出文件的固定虚拟路径(恢复历史快照时用它重建 stub)。 */
|
||||
@@ -73,8 +78,22 @@ interface UseStep3SummaryOptions {
|
||||
savedSummary?: DraftStep3SummarySnapshot | null;
|
||||
/** 一次总结成功完成时回调(传出 md/thread/model/时间/收尾语),供上层存草稿。 */
|
||||
onSummarySaved?: (snapshot: DraftStep3SummarySnapshot) => void;
|
||||
/**
|
||||
* 入库技能是否可用(示例/真实入库技能存在且启用)。入库功能**仅在深链(taskId)模式**
|
||||
* (会商 rwfx-A 等)且本项为 true 时启用:流程图生成完成后自动询问是否入库,并启用
|
||||
* 「稍后入库」自由文字意图识别 + 手动入库按钮。
|
||||
*/
|
||||
ingestAvailable?: boolean;
|
||||
/**
|
||||
* 深链外部任务 id(rwfx-A 等)。入库功能仅深链模式才有;入库时该 taskId 会传给入库
|
||||
* 技能(技能必需信息)。非深链(无 taskId)时入库功能整体不出现。
|
||||
*/
|
||||
taskId?: string | null;
|
||||
}
|
||||
|
||||
/** 第三步入库状态机。 */
|
||||
export type IngestStatus = "idle" | "asking" | "running" | "done" | "later" | "declined";
|
||||
|
||||
export interface UseStep3SummaryReturn {
|
||||
phase: SummaryPhase;
|
||||
error: string | null;
|
||||
@@ -120,6 +139,24 @@ export interface UseStep3SummaryReturn {
|
||||
step: NonNullable<Step2Dialogue["steps"]>[number],
|
||||
) => void;
|
||||
openArtifactPreview: (threadId: string, path: string) => void;
|
||||
// ── 入库(生成流程图后询问 / 稍后入库意图 / 手动入库)──
|
||||
/** 入库技能是否可用(回显 option,组件据此决定是否显示入库按钮/卡片)。 */
|
||||
ingestAvailable: boolean;
|
||||
/** 入库状态机。 */
|
||||
ingestStatus: IngestStatus;
|
||||
/** 「稍后入库」自由文字意图模糊时挂起的原始消息(非空 → 组件展示入库协助选项)。 */
|
||||
pendingIngestClarification: string | null;
|
||||
/** 流程图后入库询问的三选一(现在/稍后/不入库)。 */
|
||||
resolveIngestChoice: (choice: "now" | "later" | "never") => void;
|
||||
/** 入库协助选项点选(开始入库/补充内容后再入库/只是提问→转交普通对话)。 */
|
||||
resolveIngestClarification: (choice: "start_ingest" | "add_data" | "other") => void;
|
||||
/** 手动入库(按钮 / 直接触发):在总结线程跑入库技能并显示步骤条。 */
|
||||
runIngest: (opts?: { note?: string }) => void;
|
||||
}
|
||||
|
||||
/** 演示用:步骤条之间的小停顿,让「调用入库技能」的过程看得见。 */
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/** 把研讨原料拼成一段(首轮 prompt 和「恢复后无线程续问」都用得到)。 */
|
||||
@@ -393,6 +430,8 @@ export function useStep3Summary(
|
||||
autoFlow,
|
||||
savedSummary,
|
||||
onSummarySaved,
|
||||
ingestAvailable,
|
||||
taskId: ingestTaskId,
|
||||
} = options;
|
||||
|
||||
const onSummarySavedRef = useRef(onSummarySaved);
|
||||
@@ -400,6 +439,28 @@ export function useStep3Summary(
|
||||
onSummarySavedRef.current = onSummarySaved;
|
||||
}, [onSummarySaved]);
|
||||
|
||||
// ── 入库状态 ──
|
||||
const [ingestStatus, setIngestStatus] = useState<IngestStatus>("idle");
|
||||
const [pendingIngestClarification, setPendingIngestClarification] = useState<string | null>(null);
|
||||
// 镜像供 render 外的回调(runFlowExtract 完成、ask 拦截)同步读到最新值。
|
||||
const ingestStatusRef = useRef<IngestStatus>("idle");
|
||||
useEffect(() => {
|
||||
ingestStatusRef.current = ingestStatus;
|
||||
}, [ingestStatus]);
|
||||
// 入库功能「有效可用」= 技能可用 **且** 处于深链(taskId)模式。
|
||||
const ingestEnabled = !!ingestAvailable && !!ingestTaskId;
|
||||
const ingestAvailableRef = useRef<boolean>(ingestEnabled);
|
||||
useEffect(() => {
|
||||
ingestAvailableRef.current = ingestEnabled;
|
||||
}, [ingestEnabled]);
|
||||
// 深链 taskId(入库必传给技能)。
|
||||
const ingestTaskIdRef = useRef<string | null>(ingestTaskId ?? null);
|
||||
useEffect(() => {
|
||||
ingestTaskIdRef.current = ingestTaskId ?? null;
|
||||
}, [ingestTaskId]);
|
||||
// 入库演示运行序号:新一轮入库 / 中断会自增,旧循环据此自停(防重复入库)。
|
||||
const ingestRunRef = useRef(0);
|
||||
|
||||
const {
|
||||
setArtifacts,
|
||||
select: selectArtifact,
|
||||
@@ -1046,6 +1107,11 @@ export function useStep3Summary(
|
||||
model: selectedModel || "",
|
||||
closing: serializeFlowGraph(graph),
|
||||
});
|
||||
// 流程图(实时生成完成)后,若入库技能可用且尚未处理过入库 → 询问是否入库。
|
||||
// 仅实时生成走这里;hydrate 恢复不触发(不打扰已看历史的用户)。
|
||||
if (ingestAvailableRef.current && ingestStatusRef.current === "idle") {
|
||||
setIngestStatus("asking");
|
||||
}
|
||||
} catch (err) {
|
||||
if (aborter.signal.aborted) return;
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
@@ -1165,8 +1231,94 @@ export function useStep3Summary(
|
||||
runFlowExtract,
|
||||
]);
|
||||
|
||||
/**
|
||||
* 触发一次草稿落库:重发当前总结快照(md + 流程图 closing 原样),让上层 step3SavedTick
|
||||
* 自增 → autoSave 把最新对话(含入库气泡)+ ingestStatus 一并存进草稿。入库/选择稍后等
|
||||
* 不经 onSummarySaved 的状态变化靠它落库。closing 用当前流程图序列化保持与流程图收尾一致。
|
||||
*/
|
||||
const persistSummary = useCallback(() => {
|
||||
onSummarySavedRef.current?.({
|
||||
md: currentMdRef.current,
|
||||
threadId: liveThreadRef.current ?? "",
|
||||
generatedAt: new Date().toISOString(),
|
||||
model: selectedModel || "",
|
||||
closing: flowGraphRef.current ? serializeFlowGraph(flowGraphRef.current) : "",
|
||||
});
|
||||
}, [selectedModel]);
|
||||
|
||||
/**
|
||||
* 入库(示例 · 演示用):在**总结线程**上出一条「知识库入库」气泡,按
|
||||
* 调度 → 调用入库技能 →(补充内容)→ 提交 → 完成 的步骤条演示调用过程,与第二步同款。
|
||||
* 真实入库技能后续替换这里(改为真的跑技能 / 调入库接口)。
|
||||
*/
|
||||
const runIngest = useCallback(
|
||||
async (opts?: { note?: string }) => {
|
||||
setPendingIngestClarification(null);
|
||||
setIngestStatus("running");
|
||||
const myRun = ++ingestRunRef.current;
|
||||
const bubbleId = appendStreamingDialogue({
|
||||
sender: "知识库入库",
|
||||
avatarType: "blue",
|
||||
agentThreadId: liveThreadRef.current ?? undefined,
|
||||
});
|
||||
const title = intentReady?.objective?.trim() || "会商方案";
|
||||
// 入库数据**以最后修改后的为准**:读 flowGraphRef.current(画布手动改图也会同步进来)
|
||||
// 与当前报告 md。taskId 是入库技能必需信息,必须随入库一起传给技能。
|
||||
const flow = flowGraphRef.current;
|
||||
const md = currentMdRef.current;
|
||||
const taskId = ingestTaskIdRef.current;
|
||||
const submitArgs: Record<string, unknown> = {
|
||||
title,
|
||||
taskId, // 技能必需:深链外部任务 id
|
||||
hasReport: !!md.trim(),
|
||||
flowNodes: flow?.nodes?.length ?? 0,
|
||||
flowEdges: flow?.edges?.length ?? 0,
|
||||
...(opts?.note ? { note: opts.note } : {}),
|
||||
};
|
||||
const steps: Array<{ label: string; key: string; args?: Record<string, unknown> }> = [
|
||||
{ label: "调度入库技能", key: "tool_calling:ingest_dispatch", args: { taskId } },
|
||||
{ label: "调用「知识库入库」技能", key: "tool_calling:ingest_skill", args: { skill: "knowledge-base-ingest", taskId } },
|
||||
...(opts?.note
|
||||
? [{ label: "并入补充内容", key: "tool_calling:ingest_append", args: { note: opts.note } }]
|
||||
: []),
|
||||
{ label: "提交方案数据入库(以最新流程图为准)", key: "tool_calling:ingest_submit", args: submitArgs },
|
||||
];
|
||||
for (const s of steps) {
|
||||
if (ingestRunRef.current !== myRun) return; // 被新一轮入库 / 中断取代
|
||||
setDialogueConfidence(bubbleId, s.label);
|
||||
appendStepIfNew(bubbleId, s.label, s.key, s.args);
|
||||
await sleep(700);
|
||||
}
|
||||
if (ingestRunRef.current !== myRun) return;
|
||||
finalizeDialogue(
|
||||
bubbleId,
|
||||
`✅ 已将「${title}」入库到知识库(示例技能 · 演示用,后续接入真实入库技能)。`,
|
||||
"入库完成",
|
||||
);
|
||||
setIngestStatus("done");
|
||||
persistSummary(); // 把入库气泡 + ingestStatus 落进草稿
|
||||
},
|
||||
[appendStreamingDialogue, appendStepIfNew, setDialogueConfidence, finalizeDialogue, intentReady, persistSummary],
|
||||
);
|
||||
|
||||
/** 流程图后入库询问三选一。 */
|
||||
const resolveIngestChoice = useCallback(
|
||||
(choice: "now" | "later" | "never") => {
|
||||
if (choice === "now") {
|
||||
void runIngest();
|
||||
} else if (choice === "later") {
|
||||
setIngestStatus("later");
|
||||
persistSummary();
|
||||
} else {
|
||||
setIngestStatus("declined");
|
||||
persistSummary();
|
||||
}
|
||||
},
|
||||
[runIngest, persistSummary],
|
||||
);
|
||||
|
||||
const ask = useCallback(
|
||||
async (message: string, files?: File[], opts?: { forcedIntent?: FlowIntent }) => {
|
||||
async (message: string, files?: File[], opts?: { forcedIntent?: FlowIntent; skipIngest?: boolean }) => {
|
||||
const text = message.trim();
|
||||
const hasFiles = !!(files && files.length > 0);
|
||||
if (!text && !hasFiles) return;
|
||||
@@ -1184,6 +1336,29 @@ export function useStep3Summary(
|
||||
appendUserDialogue((text || "(上传了参考文件)") + fileNote);
|
||||
}
|
||||
|
||||
// 入库优先意图:流程图后入库询问中 / 选了「稍后入库」待命时,先判断用户是否想
|
||||
// 开始入库 / 补充内容后入库;模糊则发起入库协助。其它(other)落到下方报告/流程图分流。
|
||||
const ingestPending =
|
||||
!opts?.skipIngest &&
|
||||
!opts?.forcedIntent &&
|
||||
(ingestStatusRef.current === "later" || ingestStatusRef.current === "asking");
|
||||
if (text && !hasFiles && ingestPending) {
|
||||
const ir = await classifyIngestIntent({ message: text, model: selectedModel || undefined });
|
||||
if (ir.intent === "start_ingest") {
|
||||
await runIngest();
|
||||
return;
|
||||
}
|
||||
if (ir.intent === "add_data") {
|
||||
await runIngest({ note: text });
|
||||
return;
|
||||
}
|
||||
if (ir.intent === "unclear") {
|
||||
setPendingIngestClarification(text);
|
||||
return;
|
||||
}
|
||||
// ir.intent === "other" → 继续走下方报告/流程图意图分流。
|
||||
}
|
||||
|
||||
// 意图识别(参考 ai_writing 一次性 JSON 范式):先判断用户想 重做报告 / 改流程图 /
|
||||
// 重画流程图 / 提问 / 模糊待澄清,再分流——不再用关键词硬路由(避免「说重做报告却出流程图」)。
|
||||
// 带附件时跳过分类(附件多用于报告 Q&A/改报告);澄清后用户点选则用 forcedIntent 跳过分类。
|
||||
@@ -1343,9 +1518,22 @@ export function useStep3Summary(
|
||||
finalizeDialogue,
|
||||
streamOnce,
|
||||
runFlowExtract,
|
||||
runIngest,
|
||||
],
|
||||
);
|
||||
|
||||
/** 入库协助点选:开始入库 / 补充内容后入库 / 只是提问(转交普通对话,跳过入库拦截)。 */
|
||||
const resolveIngestClarification = useCallback(
|
||||
(choice: "start_ingest" | "add_data" | "other") => {
|
||||
const msg = pendingIngestClarification;
|
||||
setPendingIngestClarification(null);
|
||||
if (choice === "start_ingest") void runIngest();
|
||||
else if (choice === "add_data") void runIngest({ note: msg ?? undefined });
|
||||
else if (msg) void ask(msg, undefined, { skipIngest: true });
|
||||
},
|
||||
[pendingIngestClarification, runIngest, ask],
|
||||
);
|
||||
|
||||
/** 澄清后用户点选意图 → 用 forcedIntent 复跑(跳过意图分类)。 */
|
||||
const resolveFlowClarification = useCallback(
|
||||
(choice: FlowIntent) => {
|
||||
@@ -1474,6 +1662,9 @@ export function useStep3Summary(
|
||||
}
|
||||
setError(null);
|
||||
setPhase("done");
|
||||
// 恢复入库状态(done/later/declined 不再重复询问;旧草稿无该字段 → idle 且不自动询问)。
|
||||
setIngestStatus((snap.ingestStatus as IngestStatus | undefined) ?? "idle");
|
||||
setPendingIngestClarification(null);
|
||||
// 不再自动打开沙箱预览(2026-06-12):进入第三步默认关闭沙箱,只有开始
|
||||
// 写文件(live write_file 流式)时才自动打开;恢复历史时用户点气泡里的
|
||||
// 「点击预览」步骤卡随时可开(stub 已种好)。
|
||||
@@ -1507,5 +1698,12 @@ export function useStep3Summary(
|
||||
setPreviewEnabled,
|
||||
openArtifactFromStep,
|
||||
openArtifactPreview,
|
||||
// 入库(仅深链 taskId 模式 + 技能可用时有效)
|
||||
ingestAvailable: ingestEnabled,
|
||||
ingestStatus,
|
||||
pendingIngestClarification,
|
||||
resolveIngestChoice,
|
||||
resolveIngestClarification,
|
||||
runIngest: (opts?: { note?: string }) => void runIngest(opts),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user