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
@@ -173,6 +173,11 @@ export interface DraftStep3SummarySnapshot {
|
|||||||
dialogues?: unknown[];
|
dialogues?: unknown[];
|
||||||
/** 各席位 write_file 调用重建的 stub(threadId → [stub ai message]),供加载后点开产物的虚拟预览。 */
|
/** 各席位 write_file 调用重建的 stub(threadId → [stub ai message]),供加载后点开产物的虚拟预览。 */
|
||||||
seatStubMessages?: Record<string, unknown[]>;
|
seatStubMessages?: Record<string, unknown[]>;
|
||||||
|
/**
|
||||||
|
* 入库状态(idle/asking/running/done/later/declined)。存它使重载草稿后不重复询问
|
||||||
|
* 已入库/已选稍后的会商;恢复展示用。Optional —— 旧草稿无该字段。
|
||||||
|
*/
|
||||||
|
ingestStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Lightweight metadata row returned by the list endpoint. */
|
/** Lightweight metadata row returned by the list endpoint. */
|
||||||
|
|||||||
@@ -46,6 +46,33 @@ export async function classifyFlowIntent(req: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 「稍后入库」待命时输入框消息的入库意图(与后端 /ingest-intent 对应)。 */
|
||||||
|
export type IngestIntent = "start_ingest" | "add_data" | "other" | "unclear";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 识别「稍后入库」后用户在输入框的入库意图(一次模型调用)。
|
||||||
|
* 失败回退 "unclear"(前端发起协助让用户自己选)。
|
||||||
|
*/
|
||||||
|
export async function classifyIngestIntent(req: {
|
||||||
|
message: string;
|
||||||
|
model?: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}): Promise<{ intent: IngestIntent; reason: string }> {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch(`${getBackendBaseURL()}/api/flow-extract/ingest-intent`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ message: req.message, model: req.model }),
|
||||||
|
signal: req.signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) return { intent: "unclear", reason: "" };
|
||||||
|
const data = (await res.json()) as { intent?: string; reason?: string };
|
||||||
|
return { intent: (data.intent ?? "unclear") as IngestIntent, reason: data.reason ?? "" };
|
||||||
|
} catch {
|
||||||
|
return { intent: "unclear", reason: "" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface FlowExtractRequest {
|
export interface FlowExtractRequest {
|
||||||
/** 报告 Markdown(优先数据来源)。 */
|
/** 报告 Markdown(优先数据来源)。 */
|
||||||
reportMd?: string | null;
|
reportMd?: string | null;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 第三步「入库」功能门控。
|
||||||
|
*
|
||||||
|
* 入库询问 / 意图识别 / 手动入库按钮只有在**入库技能存在且启用**时才出现
|
||||||
|
* (对应需求「如果有了调用技能入库的技能后…」)。当前是示例技能 `knowledge-base-ingest`
|
||||||
|
* (演示用);后续替换为真实入库技能时只要沿用同一个 name,本门控即继续生效。
|
||||||
|
*/
|
||||||
|
import { loadSkills } from "@/core/skills/api";
|
||||||
|
|
||||||
|
/** 入库技能名(示例/真实共用同一名字,方便无缝替换)。 */
|
||||||
|
export const INGEST_SKILL_NAME = "knowledge-base-ingest";
|
||||||
|
|
||||||
|
/** 入库技能是否存在且启用。失败回退 false(不弹入库询问)。 */
|
||||||
|
export async function isIngestSkillAvailable(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const skills = await loadSkills();
|
||||||
|
return skills.some((s) => s.name === INGEST_SKILL_NAME && s.enabled);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import remarkGfm from "remark-gfm";
|
|||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
Database,
|
||||||
Download,
|
Download,
|
||||||
Eye,
|
Eye,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -414,6 +415,12 @@ export function Step3SummaryMode({
|
|||||||
onRegenerateFlow={summary.regenerateFlow}
|
onRegenerateFlow={summary.regenerateFlow}
|
||||||
pendingClarification={summary.pendingFlowClarification}
|
pendingClarification={summary.pendingFlowClarification}
|
||||||
onResolveClarification={summary.resolveFlowClarification}
|
onResolveClarification={summary.resolveFlowClarification}
|
||||||
|
ingestAvailable={summary.ingestAvailable}
|
||||||
|
ingestStatus={summary.ingestStatus}
|
||||||
|
pendingIngestClarification={summary.pendingIngestClarification}
|
||||||
|
onResolveIngestChoice={summary.resolveIngestChoice}
|
||||||
|
onResolveIngestClarification={summary.resolveIngestClarification}
|
||||||
|
onManualIngest={summary.runIngest}
|
||||||
onRetry={redraw}
|
onRetry={redraw}
|
||||||
onAsk={handleAsk}
|
onAsk={handleAsk}
|
||||||
onPause={pause}
|
onPause={pause}
|
||||||
@@ -560,6 +567,12 @@ function AgentProgressPanel({
|
|||||||
onRegenerateFlow,
|
onRegenerateFlow,
|
||||||
pendingClarification,
|
pendingClarification,
|
||||||
onResolveClarification,
|
onResolveClarification,
|
||||||
|
ingestAvailable,
|
||||||
|
ingestStatus,
|
||||||
|
pendingIngestClarification,
|
||||||
|
onResolveIngestChoice,
|
||||||
|
onResolveIngestClarification,
|
||||||
|
onManualIngest,
|
||||||
onRetry,
|
onRetry,
|
||||||
onAsk,
|
onAsk,
|
||||||
onPause,
|
onPause,
|
||||||
@@ -583,6 +596,13 @@ function AgentProgressPanel({
|
|||||||
/** 意图模糊待澄清的原始消息(非空 → 展示选项让用户定)。 */
|
/** 意图模糊待澄清的原始消息(非空 → 展示选项让用户定)。 */
|
||||||
pendingClarification: string | null;
|
pendingClarification: string | null;
|
||||||
onResolveClarification: UseStep3SummaryReturn["resolveFlowClarification"];
|
onResolveClarification: UseStep3SummaryReturn["resolveFlowClarification"];
|
||||||
|
/** 入库技能是否可用(决定入库询问卡 + 手动入库按钮是否出现)。 */
|
||||||
|
ingestAvailable: boolean;
|
||||||
|
ingestStatus: UseStep3SummaryReturn["ingestStatus"];
|
||||||
|
pendingIngestClarification: string | null;
|
||||||
|
onResolveIngestChoice: UseStep3SummaryReturn["resolveIngestChoice"];
|
||||||
|
onResolveIngestClarification: UseStep3SummaryReturn["resolveIngestClarification"];
|
||||||
|
onManualIngest: UseStep3SummaryReturn["runIngest"];
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
onAsk: (message: string, files?: File[]) => void;
|
onAsk: (message: string, files?: File[]) => void;
|
||||||
onPause: () => void;
|
onPause: () => void;
|
||||||
@@ -784,6 +804,60 @@ function AgentProgressPanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 流程图已生成 → 询问是否入库(入库技能可用时)。 */}
|
||||||
|
{ingestAvailable && ingestStatus === "asking" && (
|
||||||
|
<div className="shrink-0 border-t border-emerald-100 bg-emerald-50/60 px-3 py-2.5">
|
||||||
|
<div className="mb-1.5 text-[11px] font-bold text-emerald-800">
|
||||||
|
流程图已生成,是否把本方案入库到知识库?
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ k: "now", t: "现在入库" },
|
||||||
|
{ k: "later", t: "稍后入库" },
|
||||||
|
{ k: "never", t: "不入库" },
|
||||||
|
] as const
|
||||||
|
).map((o) => (
|
||||||
|
<button
|
||||||
|
key={o.k}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onResolveIngestChoice(o.k)}
|
||||||
|
className="rounded-lg border border-emerald-300 bg-white px-2.5 py-1.5 text-[11px] font-bold text-emerald-800 transition hover:bg-emerald-100"
|
||||||
|
>
|
||||||
|
{o.t}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 「稍后入库」自由文字意图模糊 → 让用户在入库语境下自己定。 */}
|
||||||
|
{ingestAvailable && pendingIngestClarification && (
|
||||||
|
<div className="shrink-0 border-t border-emerald-100 bg-emerald-50/60 px-3 py-2.5">
|
||||||
|
<div className="mb-1.5 text-[11px] font-bold text-emerald-800">
|
||||||
|
没太理解你的入库意思,你是想——
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{ k: "start_ingest", t: "开始入库" },
|
||||||
|
{ k: "add_data", t: "补充内容后再入库" },
|
||||||
|
{ k: "other", t: "只是提问 / 改报告" },
|
||||||
|
] as const
|
||||||
|
).map((o) => (
|
||||||
|
<button
|
||||||
|
key={o.k}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onResolveIngestClarification(o.k)}
|
||||||
|
className="rounded-lg border border-emerald-300 bg-white px-2.5 py-1.5 text-[11px] font-bold text-emerald-800 transition hover:bg-emerald-100"
|
||||||
|
>
|
||||||
|
{o.t}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleAskSubmit} className="shrink-0 border-t border-slate-100 bg-white px-3 py-3">
|
<form onSubmit={handleAskSubmit} className="shrink-0 border-t border-slate-100 bg-white px-3 py-3">
|
||||||
{/* 已选附件预览(发送时随指令上传到总结线程,供智能体读取) */}
|
{/* 已选附件预览(发送时随指令上传到总结线程,供智能体读取) */}
|
||||||
{askFiles.length > 0 && (
|
{askFiles.length > 0 && (
|
||||||
@@ -830,6 +904,22 @@ function AgentProgressPanel({
|
|||||||
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none"
|
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 手动入库(入库技能可用、且已过流程图询问阶段时显示):直接在总结线程跑入库技能。 */}
|
||||||
|
{ingestAvailable &&
|
||||||
|
ingestStatus !== "asking" &&
|
||||||
|
ingestStatus !== "running" &&
|
||||||
|
ingestStatus !== "idle" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onManualIngest()}
|
||||||
|
title="把本方案入库到知识库(调用入库技能,过程见消息列表步骤条)"
|
||||||
|
className="mb-0.5 flex h-8 shrink-0 items-center gap-1 rounded-xl border border-emerald-200 bg-emerald-50 px-2.5 text-[11px] font-bold text-emerald-700 transition hover:bg-emerald-100 active:scale-95"
|
||||||
|
>
|
||||||
|
<Database className="h-3.5 w-3.5" />
|
||||||
|
入库
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 上传文件(参考第一/二步:本地选取,发送时上传到总结线程) */}
|
{/* 上传文件(参考第一/二步:本地选取,发送时上传到总结线程) */}
|
||||||
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={pickFiles} />
|
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={pickFiles} />
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -41,7 +41,12 @@ import type {
|
|||||||
StubAiMessage,
|
StubAiMessage,
|
||||||
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||||
import { detectFlowIntent, extractFlowGraph, hasFlowFence, serializeFlowGraph, type FlowGraphData } from "@/roundtable-planning/lib/flow-graph";
|
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";
|
import { nowTime } from "@/roundtable-planning/lib/roundtable-constants";
|
||||||
|
|
||||||
/** 报告输出文件的固定虚拟路径(恢复历史快照时用它重建 stub)。 */
|
/** 报告输出文件的固定虚拟路径(恢复历史快照时用它重建 stub)。 */
|
||||||
@@ -73,8 +78,22 @@ interface UseStep3SummaryOptions {
|
|||||||
savedSummary?: DraftStep3SummarySnapshot | null;
|
savedSummary?: DraftStep3SummarySnapshot | null;
|
||||||
/** 一次总结成功完成时回调(传出 md/thread/model/时间/收尾语),供上层存草稿。 */
|
/** 一次总结成功完成时回调(传出 md/thread/model/时间/收尾语),供上层存草稿。 */
|
||||||
onSummarySaved?: (snapshot: DraftStep3SummarySnapshot) => void;
|
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 {
|
export interface UseStep3SummaryReturn {
|
||||||
phase: SummaryPhase;
|
phase: SummaryPhase;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
@@ -120,6 +139,24 @@ export interface UseStep3SummaryReturn {
|
|||||||
step: NonNullable<Step2Dialogue["steps"]>[number],
|
step: NonNullable<Step2Dialogue["steps"]>[number],
|
||||||
) => void;
|
) => void;
|
||||||
openArtifactPreview: (threadId: string, path: string) => 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 和「恢复后无线程续问」都用得到)。 */
|
/** 把研讨原料拼成一段(首轮 prompt 和「恢复后无线程续问」都用得到)。 */
|
||||||
@@ -393,6 +430,8 @@ export function useStep3Summary(
|
|||||||
autoFlow,
|
autoFlow,
|
||||||
savedSummary,
|
savedSummary,
|
||||||
onSummarySaved,
|
onSummarySaved,
|
||||||
|
ingestAvailable,
|
||||||
|
taskId: ingestTaskId,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const onSummarySavedRef = useRef(onSummarySaved);
|
const onSummarySavedRef = useRef(onSummarySaved);
|
||||||
@@ -400,6 +439,28 @@ export function useStep3Summary(
|
|||||||
onSummarySavedRef.current = onSummarySaved;
|
onSummarySavedRef.current = onSummarySaved;
|
||||||
}, [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 {
|
const {
|
||||||
setArtifacts,
|
setArtifacts,
|
||||||
select: selectArtifact,
|
select: selectArtifact,
|
||||||
@@ -1046,6 +1107,11 @@ export function useStep3Summary(
|
|||||||
model: selectedModel || "",
|
model: selectedModel || "",
|
||||||
closing: serializeFlowGraph(graph),
|
closing: serializeFlowGraph(graph),
|
||||||
});
|
});
|
||||||
|
// 流程图(实时生成完成)后,若入库技能可用且尚未处理过入库 → 询问是否入库。
|
||||||
|
// 仅实时生成走这里;hydrate 恢复不触发(不打扰已看历史的用户)。
|
||||||
|
if (ingestAvailableRef.current && ingestStatusRef.current === "idle") {
|
||||||
|
setIngestStatus("asking");
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (aborter.signal.aborted) return;
|
if (aborter.signal.aborted) return;
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
@@ -1165,8 +1231,94 @@ export function useStep3Summary(
|
|||||||
runFlowExtract,
|
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(
|
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 text = message.trim();
|
||||||
const hasFiles = !!(files && files.length > 0);
|
const hasFiles = !!(files && files.length > 0);
|
||||||
if (!text && !hasFiles) return;
|
if (!text && !hasFiles) return;
|
||||||
@@ -1184,6 +1336,29 @@ export function useStep3Summary(
|
|||||||
appendUserDialogue((text || "(上传了参考文件)") + fileNote);
|
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 范式):先判断用户想 重做报告 / 改流程图 /
|
// 意图识别(参考 ai_writing 一次性 JSON 范式):先判断用户想 重做报告 / 改流程图 /
|
||||||
// 重画流程图 / 提问 / 模糊待澄清,再分流——不再用关键词硬路由(避免「说重做报告却出流程图」)。
|
// 重画流程图 / 提问 / 模糊待澄清,再分流——不再用关键词硬路由(避免「说重做报告却出流程图」)。
|
||||||
// 带附件时跳过分类(附件多用于报告 Q&A/改报告);澄清后用户点选则用 forcedIntent 跳过分类。
|
// 带附件时跳过分类(附件多用于报告 Q&A/改报告);澄清后用户点选则用 forcedIntent 跳过分类。
|
||||||
@@ -1343,9 +1518,22 @@ export function useStep3Summary(
|
|||||||
finalizeDialogue,
|
finalizeDialogue,
|
||||||
streamOnce,
|
streamOnce,
|
||||||
runFlowExtract,
|
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 复跑(跳过意图分类)。 */
|
/** 澄清后用户点选意图 → 用 forcedIntent 复跑(跳过意图分类)。 */
|
||||||
const resolveFlowClarification = useCallback(
|
const resolveFlowClarification = useCallback(
|
||||||
(choice: FlowIntent) => {
|
(choice: FlowIntent) => {
|
||||||
@@ -1474,6 +1662,9 @@ export function useStep3Summary(
|
|||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setPhase("done");
|
setPhase("done");
|
||||||
|
// 恢复入库状态(done/later/declined 不再重复询问;旧草稿无该字段 → idle 且不自动询问)。
|
||||||
|
setIngestStatus((snap.ingestStatus as IngestStatus | undefined) ?? "idle");
|
||||||
|
setPendingIngestClarification(null);
|
||||||
// 不再自动打开沙箱预览(2026-06-12):进入第三步默认关闭沙箱,只有开始
|
// 不再自动打开沙箱预览(2026-06-12):进入第三步默认关闭沙箱,只有开始
|
||||||
// 写文件(live write_file 流式)时才自动打开;恢复历史时用户点气泡里的
|
// 写文件(live write_file 流式)时才自动打开;恢复历史时用户点气泡里的
|
||||||
// 「点击预览」步骤卡随时可开(stub 已种好)。
|
// 「点击预览」步骤卡随时可开(stub 已种好)。
|
||||||
@@ -1507,5 +1698,12 @@ export function useStep3Summary(
|
|||||||
setPreviewEnabled,
|
setPreviewEnabled,
|
||||||
openArtifactFromStep,
|
openArtifactFromStep,
|
||||||
openArtifactPreview,
|
openArtifactPreview,
|
||||||
|
// 入库(仅深链 taskId 模式 + 技能可用时有效)
|
||||||
|
ingestAvailable: ingestEnabled,
|
||||||
|
ingestStatus,
|
||||||
|
pendingIngestClarification,
|
||||||
|
resolveIngestChoice,
|
||||||
|
resolveIngestClarification,
|
||||||
|
runIngest: (opts?: { note?: string }) => void runIngest(opts),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,7 @@ import { useStep3Summary } from "@/roundtable-planning/hooks/useStep3Summary";
|
|||||||
import { useStep3Report } from "@/roundtable-planning/hooks/useStep3Report";
|
import { useStep3Report } from "@/roundtable-planning/hooks/useStep3Report";
|
||||||
import { useStep3Dashboard } from "@/roundtable-planning/hooks/useStep3Dashboard";
|
import { useStep3Dashboard } from "@/roundtable-planning/hooks/useStep3Dashboard";
|
||||||
import { isDashboardHtml } from "@/roundtable-planning/lib/report-template";
|
import { isDashboardHtml } from "@/roundtable-planning/lib/report-template";
|
||||||
|
import { isIngestSkillAvailable } from "@/roundtable-planning/api/ingest";
|
||||||
import { stripReasoning } from "@/roundtable-planning/lib/reasoning";
|
import { stripReasoning } from "@/roundtable-planning/lib/reasoning";
|
||||||
import {
|
import {
|
||||||
BUILTIN_AGENT_META,
|
BUILTIN_AGENT_META,
|
||||||
@@ -257,6 +258,11 @@ export default function RoundtablePlanningPage() {
|
|||||||
// 加载草稿时 hydrateStep3 恢复;新建任务清空。step3SavedTick 递增触发一次草稿自动保存。
|
// 加载草稿时 hydrateStep3 恢复;新建任务清空。step3SavedTick 递增触发一次草稿自动保存。
|
||||||
const [step3Report, setStep3Report] = useState<DraftStep3Snapshot | null>(null);
|
const [step3Report, setStep3Report] = useState<DraftStep3Snapshot | null>(null);
|
||||||
const [step3SavedTick, setStep3SavedTick] = useState<number>(0);
|
const [step3SavedTick, setStep3SavedTick] = useState<number>(0);
|
||||||
|
// 入库技能是否可用(决定第三步生成流程图后是否询问入库 + 显示手动入库按钮)。挂载查一次。
|
||||||
|
const [ingestAvailable, setIngestAvailable] = useState<boolean>(false);
|
||||||
|
useEffect(() => {
|
||||||
|
void isIngestSkillAvailable().then(setIngestAvailable).catch(() => setIngestAvailable(false));
|
||||||
|
}, []);
|
||||||
const step2RunsRef = useRef<DraftStep2RunSnapshot[]>([]);
|
const step2RunsRef = useRef<DraftStep2RunSnapshot[]>([]);
|
||||||
const activeStep2RunIdRef = useRef<string | null>(null);
|
const activeStep2RunIdRef = useRef<string | null>(null);
|
||||||
// 并行执行时收起左侧栏 —— 全局工作区侧栏(最左)走 useSidebarSafe(与「打开沙箱」同一路径,
|
// 并行执行时收起左侧栏 —— 全局工作区侧栏(最左)走 useSidebarSafe(与「打开沙箱」同一路径,
|
||||||
@@ -560,10 +566,15 @@ export default function RoundtablePlanningPage() {
|
|||||||
// 第三步三条生成流(总结报告 / 经典看板 / 大屏)各自生成过程的实时对话(中间文字 + 步骤条)。
|
// 第三步三条生成流(总结报告 / 经典看板 / 大屏)各自生成过程的实时对话(中间文字 + 步骤条)。
|
||||||
// 在 render 阶段镜像进 ref(这些 flow 在本 hook 之后才定义,故用 ref 跨过 TDZ;render 期赋值,
|
// 在 render 阶段镜像进 ref(这些 flow 在本 hook 之后才定义,故用 ref 跨过 TDZ;render 期赋值,
|
||||||
// 保证 autoSave effect 读到的是本次提交已 flush 的最终对话,规避 effect 执行顺序问题)。
|
// 保证 autoSave effect 读到的是本次提交已 flush 的最终对话,规避 effect 执行顺序问题)。
|
||||||
const emptyConvo = { dialogues: [] as unknown[], seatStubMessages: {} as Record<string, unknown[]> };
|
type Step3Convo = {
|
||||||
const step3SummaryConvoRef = useRef<typeof emptyConvo>({ ...emptyConvo });
|
dialogues: unknown[];
|
||||||
const step3ReportConvoRef = useRef<typeof emptyConvo>({ ...emptyConvo });
|
seatStubMessages: Record<string, unknown[]>;
|
||||||
const step3DashConvoRef = useRef<typeof emptyConvo>({ ...emptyConvo });
|
/** 仅总结流用:入库状态,随快照落库。 */
|
||||||
|
ingestStatus?: string;
|
||||||
|
};
|
||||||
|
const step3SummaryConvoRef = useRef<Step3Convo>({ dialogues: [], seatStubMessages: {} });
|
||||||
|
const step3ReportConvoRef = useRef<Step3Convo>({ dialogues: [], seatStubMessages: {} });
|
||||||
|
const step3DashConvoRef = useRef<Step3Convo>({ dialogues: [], seatStubMessages: {} });
|
||||||
|
|
||||||
// 草稿 hook —— 同时依赖 step1 + step2 的 snapshot / hydrate。
|
// 草稿 hook —— 同时依赖 step1 + step2 的 snapshot / hydrate。
|
||||||
const drafts = useDraftPersistence({
|
const drafts = useDraftPersistence({
|
||||||
@@ -590,6 +601,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
seatStubMessages: Object.keys(sumLive.seatStubMessages).length
|
seatStubMessages: Object.keys(sumLive.seatStubMessages).length
|
||||||
? sumLive.seatStubMessages
|
? sumLive.seatStubMessages
|
||||||
: sr.seatStubMessages,
|
: sr.seatStubMessages,
|
||||||
|
ingestStatus: sumLive.ingestStatus ?? sr.ingestStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const repLive = step3ReportConvoRef.current;
|
const repLive = step3ReportConvoRef.current;
|
||||||
@@ -1063,11 +1075,15 @@ export default function RoundtablePlanningPage() {
|
|||||||
autoFlow: autoSummaryFlow,
|
autoFlow: autoSummaryFlow,
|
||||||
savedSummary: step3Report?.summaryReport ?? null,
|
savedSummary: step3Report?.summaryReport ?? null,
|
||||||
onSummarySaved: handleSummarySaved,
|
onSummarySaved: handleSummarySaved,
|
||||||
|
ingestAvailable,
|
||||||
|
// 入库仅深链(taskId)模式(会商 rwfx-A 等)才有;taskId 入库时必传给技能。
|
||||||
|
taskId: prefillRef.current?.taskId ?? null,
|
||||||
});
|
});
|
||||||
// render 期镜像总结报告的实时对话(文字 + 步骤条) + stub,供 getStep3Snapshot 落库。
|
// render 期镜像总结报告的实时对话(文字 + 步骤条) + stub + 入库状态,供 getStep3Snapshot 落库。
|
||||||
step3SummaryConvoRef.current = {
|
step3SummaryConvoRef.current = {
|
||||||
dialogues: summaryFlow.dialogues,
|
dialogues: summaryFlow.dialogues,
|
||||||
seatStubMessages: summaryFlow.seatStubMessages,
|
seatStubMessages: summaryFlow.seatStubMessages,
|
||||||
|
ingestStatus: summaryFlow.ingestStatus,
|
||||||
};
|
};
|
||||||
const reportFlow = useStep3Report({
|
const reportFlow = useStep3Report({
|
||||||
intentReady: step1.intentReady,
|
intentReady: step1.intentReady,
|
||||||
|
|||||||
@@ -711,3 +711,54 @@ async def classify_flow_intent(request: Request, payload: FlowIntentRequest = Bo
|
|||||||
except Exception: # noqa: BLE001 — 分类失败不致命,回退澄清让用户自己定
|
except Exception: # noqa: BLE001 — 分类失败不致命,回退澄清让用户自己定
|
||||||
logger.warning("flow intent classify failed", exc_info=True)
|
logger.warning("flow intent classify failed", exc_info=True)
|
||||||
return {"intent": "unclear", "reason": "意图识别失败"}
|
return {"intent": "unclear", "reason": "意图识别失败"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 入库意图识别(「稍后入库」后用户在输入框的自由文字)──────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class IngestIntentRequest(BaseModel):
|
||||||
|
"""第三步「稍后入库」待命时,识别输入框消息是否要开始入库 / 补充内容后入库 / 其他。"""
|
||||||
|
|
||||||
|
message: str = ""
|
||||||
|
model: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
_INGEST_INTENTS = {"start_ingest", "add_data", "other", "unclear"}
|
||||||
|
|
||||||
|
_INGEST_INTENT_SYS = (
|
||||||
|
"你是圆桌「方案总结」第三步**入库意图识别器**。流程图已生成、并且用户此前选择了「稍后入库」,"
|
||||||
|
"现在他在输入框发了一条消息。请判断他的**真实意图**,只输出一个 JSON 对象(不要解释、不要 markdown 代码块、用英文双引号):\n"
|
||||||
|
'{"intent":"<枚举>","reason":"<一句话依据>"}\n'
|
||||||
|
"intent 只能取其一:\n"
|
||||||
|
"- start_ingest:现在就把当前方案入库(如「入库吧」「开始入库」「现在入库」「把方案存进知识库」);\n"
|
||||||
|
"- add_data:想先补充/追加一些内容再入库(如「补一段背景再入库」「加上风险说明后入库」「把这段也一起入库」);\n"
|
||||||
|
"- other:与入库无关,是改报告/改流程图/提问等其它诉求(如「报告再写细点」「流程图加个验收节点」「为什么选这个方案」);\n"
|
||||||
|
"- unclear:和入库相关但说不清是现在入库还是要补内容(如只说「入库」却又像没说完、「那个再说」之类模糊表达)。\n"
|
||||||
|
"示例:\n"
|
||||||
|
'「现在入库」→ {"intent":"start_ingest","reason":"明确要求立即入库"}\n'
|
||||||
|
'「我再补一段实施风险,然后入库」→ {"intent":"add_data","reason":"先补内容再入库"}\n'
|
||||||
|
'「报告执行部分写细一点」→ {"intent":"other","reason":"是改报告,与入库无关"}\n'
|
||||||
|
'「入库……嗯」→ {"intent":"unclear","reason":"提到入库但表达不完整"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ingest-intent")
|
||||||
|
async def classify_ingest_intent(request: Request, payload: IngestIntentRequest = Body(...)) -> dict[str, str]:
|
||||||
|
"""识别「稍后入库」待命时输入框消息的入库意图(一次模型调用)。失败/解析不出 → unclear,前端发起协助。"""
|
||||||
|
msg = (payload.message or "").strip()
|
||||||
|
if not msg:
|
||||||
|
return {"intent": "unclear", "reason": "空消息"}
|
||||||
|
try:
|
||||||
|
model = create_chat_model(name=_resolve_model(payload.model), thinking_enabled=False)
|
||||||
|
res = await model.ainvoke(
|
||||||
|
[SystemMessage(content=_INGEST_INTENT_SYS), HumanMessage(content="用户消息:" + msg + "\n\n请输出 JSON:")]
|
||||||
|
)
|
||||||
|
text = res.content if isinstance(res.content, str) else _as_str(getattr(res, "text", ""))
|
||||||
|
obj = _parse_intent_json(text) or {}
|
||||||
|
intent = _as_str(obj.get("intent")).lower()
|
||||||
|
if intent not in _INGEST_INTENTS:
|
||||||
|
intent = "unclear"
|
||||||
|
return {"intent": intent, "reason": _as_str(obj.get("reason"))}
|
||||||
|
except Exception: # noqa: BLE001 — 分类失败不致命,回退协助让用户自己定
|
||||||
|
logger.warning("ingest intent classify failed", exc_info=True)
|
||||||
|
return {"intent": "unclear", "reason": "意图识别失败"}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: knowledge-base-ingest
|
||||||
|
description: 把会商/方案的最终产物(总结报告 Markdown + 结构化流程图)入库到知识库。当用户在第三步生成流程图后表示「现在入库 / 开始入库 / 把方案入库 / 补充内容后入库」时使用本 Skill 触发入库。
|
||||||
|
allowed-tools:
|
||||||
|
- read_file
|
||||||
|
---
|
||||||
|
|
||||||
|
# 知识库入库 Skill(示例 · 演示用)
|
||||||
|
|
||||||
|
> ⚠️ **这是一个示例(演示)技能,仅用于打通第三步「生成流程图 → 入库」的交互流程。**
|
||||||
|
> 真正的入库技能(对接实际知识库写入接口)后续会替换本技能;届时只需保持
|
||||||
|
> `name: knowledge-base-ingest` 不变,前端的入库询问 / 意图识别 / 手动入库按钮即可
|
||||||
|
> 无缝切到真实实现。
|
||||||
|
|
||||||
|
## 用途
|
||||||
|
|
||||||
|
在多智能体会商的第三步,方案总结报告与结构化流程图生成完成后,把这份成果作为一条
|
||||||
|
记录沉淀进知识库,便于后续检索与复用。
|
||||||
|
|
||||||
|
## 何时触发
|
||||||
|
|
||||||
|
- 流程图生成完成后,系统会询问用户「现在入库 / 稍后入库 / 不入库」;
|
||||||
|
- 用户选择「现在入库」、点击「入库」按钮,或在输入框中表达「开始入库 / 把方案入库 /
|
||||||
|
补充 xxx 内容后再入库」时触发。
|
||||||
|
|
||||||
|
## 适用范围
|
||||||
|
|
||||||
|
仅**深链(taskId)模式**(会商 `rwfx-A` / `3qfx-A` / `xdfx-A` 等)才提供入库;普通会商
|
||||||
|
不出现入库询问/按钮。
|
||||||
|
|
||||||
|
## 入库内容
|
||||||
|
|
||||||
|
- **taskId**:深链外部任务 id —— **技能必需信息**,入库时一并传入;
|
||||||
|
- 标题:本次会商的任务目标(intent objective);
|
||||||
|
- 正文:方案总结报告(Markdown)+ 结构化流程图数据(**以用户最后修改后的流程图为准**);
|
||||||
|
- 可选:用户在「补充内容后再入库」时追加的说明。
|
||||||
|
|
||||||
|
## 调用过程(演示)
|
||||||
|
|
||||||
|
本示例技能不写真实知识库,仅在消息列表中以「调度入库技能 → 调用入库技能 → 提交方案
|
||||||
|
数据 → 入库完成」的步骤条演示调用过程。真实技能将把上述内容提交到知识库写入接口
|
||||||
|
(如 `POST /api/knowledge-ingest/publish`,按智能体 `config.yaml` 的 `template_name`
|
||||||
|
解析入库模板)。
|
||||||
Reference in new issue
Block a user