From 19028ba627e0b5f3e7208f3b3666b5fad114a16f Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Thu, 18 Jun 2026 16:49:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(roundtable):=20=E7=AC=AC=E4=B8=89=E6=AD=A5?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E5=9B=BE=E5=90=8E=E5=85=A5=E5=BA=93=EF=BC=88?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E6=8A=80=E8=83=BD=20=C2=B7=20=E4=BB=85?= =?UTF-8?q?=E6=B7=B1=E9=93=BE=20taskId=20=E6=A8=A1=E5=BC=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 流程图生成完成后,发起用户协助询问是否入库:现在 / 稍后 / 不入库。 - 仅**深链(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) --- .../src/roundtable-planning/api/drafts.ts | 5 + .../roundtable-planning/api/flow-extract.ts | 27 +++ .../src/roundtable-planning/api/ingest.ts | 21 ++ .../components/Step3SummaryMode.tsx | 90 ++++++++ .../hooks/useStep3Summary.ts | 202 +++++++++++++++++- .../pages/RoundtablePlanningPage.tsx | 26 ++- .../app/gateway/routers/flow_extract.py | 51 +++++ .../public/knowledge-base-ingest/SKILL.md | 43 ++++ 8 files changed, 458 insertions(+), 7 deletions(-) create mode 100644 frontend-web/src/roundtable-planning/api/ingest.ts create mode 100644 offline-backend-20260512/backend/skills/public/knowledge-base-ingest/SKILL.md 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" && ( + + )} + {/* 上传文件(参考第一/二步:本地选取,发送时上传到总结线程) */}