diff --git a/frontend-web/src/roundtable-planning/api/drafts.ts b/frontend-web/src/roundtable-planning/api/drafts.ts index fb09ab0a..7641baba 100644 --- a/frontend-web/src/roundtable-planning/api/drafts.ts +++ b/frontend-web/src/roundtable-planning/api/drafts.ts @@ -173,6 +173,11 @@ export interface DraftStep3SummarySnapshot { dialogues?: unknown[]; /** 各席位 write_file 调用重建的 stub(threadId → [stub ai message]),供加载后点开产物的虚拟预览。 */ seatStubMessages?: Record; + /** + * 入库状态(idle/asking/running/done/later/declined)。存它使重载草稿后不重复询问 + * 已入库/已选稍后的会商;恢复展示用。Optional —— 旧草稿无该字段。 + */ + ingestStatus?: string; } /** Lightweight metadata row returned by the list endpoint. */ diff --git a/frontend-web/src/roundtable-planning/api/flow-extract.ts b/frontend-web/src/roundtable-planning/api/flow-extract.ts index 503832d3..44c735de 100644 --- a/frontend-web/src/roundtable-planning/api/flow-extract.ts +++ b/frontend-web/src/roundtable-planning/api/flow-extract.ts @@ -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 { /** 报告 Markdown(优先数据来源)。 */ reportMd?: string | null; diff --git a/frontend-web/src/roundtable-planning/api/ingest.ts b/frontend-web/src/roundtable-planning/api/ingest.ts new file mode 100644 index 00000000..c0cc44ed --- /dev/null +++ b/frontend-web/src/roundtable-planning/api/ingest.ts @@ -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 { + try { + const skills = await loadSkills(); + return skills.some((s) => s.name === INGEST_SKILL_NAME && s.enabled); + } catch { + return false; + } +} diff --git a/frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx b/frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx index e5c31fae..b6e7ce03 100644 --- a/frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx +++ b/frontend-web/src/roundtable-planning/components/Step3SummaryMode.tsx @@ -15,6 +15,7 @@ import remarkGfm from "remark-gfm"; import { ArrowRight, CheckCircle, + Database, Download, Eye, FileText, @@ -414,6 +415,12 @@ export function Step3SummaryMode({ onRegenerateFlow={summary.regenerateFlow} pendingClarification={summary.pendingFlowClarification} onResolveClarification={summary.resolveFlowClarification} + ingestAvailable={summary.ingestAvailable} + ingestStatus={summary.ingestStatus} + pendingIngestClarification={summary.pendingIngestClarification} + onResolveIngestChoice={summary.resolveIngestChoice} + onResolveIngestClarification={summary.resolveIngestClarification} + onManualIngest={summary.runIngest} onRetry={redraw} onAsk={handleAsk} onPause={pause} @@ -560,6 +567,12 @@ function AgentProgressPanel({ onRegenerateFlow, pendingClarification, onResolveClarification, + ingestAvailable, + ingestStatus, + pendingIngestClarification, + onResolveIngestChoice, + onResolveIngestClarification, + onManualIngest, onRetry, onAsk, onPause, @@ -583,6 +596,13 @@ function AgentProgressPanel({ /** 意图模糊待澄清的原始消息(非空 → 展示选项让用户定)。 */ pendingClarification: string | null; onResolveClarification: UseStep3SummaryReturn["resolveFlowClarification"]; + /** 入库技能是否可用(决定入库询问卡 + 手动入库按钮是否出现)。 */ + ingestAvailable: boolean; + ingestStatus: UseStep3SummaryReturn["ingestStatus"]; + pendingIngestClarification: string | null; + onResolveIngestChoice: UseStep3SummaryReturn["resolveIngestChoice"]; + onResolveIngestClarification: UseStep3SummaryReturn["resolveIngestClarification"]; + onManualIngest: UseStep3SummaryReturn["runIngest"]; onRetry: () => void; onAsk: (message: string, files?: File[]) => void; onPause: () => void; @@ -784,6 +804,60 @@ function AgentProgressPanel({ )} + {/* 流程图已生成 → 询问是否入库(入库技能可用时)。 */} + {ingestAvailable && ingestStatus === "asking" && ( +
+
+ 流程图已生成,是否把本方案入库到知识库? +
+
+ {( + [ + { k: "now", t: "现在入库" }, + { k: "later", t: "稍后入库" }, + { k: "never", t: "不入库" }, + ] as const + ).map((o) => ( + + ))} +
+
+ )} + + {/* 「稍后入库」自由文字意图模糊 → 让用户在入库语境下自己定。 */} + {ingestAvailable && pendingIngestClarification && ( +
+
+ 没太理解你的入库意思,你是想—— +
+
+ {( + [ + { k: "start_ingest", t: "开始入库" }, + { k: "add_data", t: "补充内容后再入库" }, + { k: "other", t: "只是提问 / 改报告" }, + ] as const + ).map((o) => ( + + ))} +
+
+ )} +
{/* 已选附件预览(发送时随指令上传到总结线程,供智能体读取) */} {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" /> + {/* 手动入库(入库技能可用、且已过流程图询问阶段时显示):直接在总结线程跑入库技能。 */} + {ingestAvailable && + ingestStatus !== "asking" && + ingestStatus !== "running" && + ingestStatus !== "idle" && ( + + )} + {/* 上传文件(参考第一/二步:本地选取,发送时上传到总结线程) */}