From 229830ee891f355dc52254bd7b76bff4ba498797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Fri, 12 Jun 2026 16:37:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BD=E4=BD=93=20md=20?= =?UTF-8?q?=E4=BA=A7=E7=89=A9=E4=B8=80=E9=94=AE=E5=8F=91=E5=B8=83=E5=88=B0?= =?UTF-8?q?=E5=A4=96=E9=83=A8=E7=9F=A5=E8=AF=86=E5=85=A5=E5=BA=93=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 智能体新增「入库模板名」(template_name) 配置,持久化到 config.yaml 并经 AgentCreate/Update/Response 回读(NewAgentPage / AgentDetailPanel) - 新增 POST /api/knowledge-ingest/publish:把 md 标题+全文发到 knowes, 标题字段先「名称」失败再用同一 nodeName 换「标题」重试一次 - md 产物预览右上角新增「发布入库」按钮(仅智能体会话且配置了模板名时显示) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace/agents/agent-detail-panel.tsx | 27 +++ .../artifacts/artifact-file-detail.tsx | 64 ++++++ frontend-web/src/core/agents/api.ts | 38 ++++ frontend-web/src/core/agents/types.ts | 6 + frontend-web/src/pages/NewAgentPage.tsx | 19 ++ offline-backend-20260512/backend/CLAUDE.md | 1 + .../backend/app/gateway/app.py | 4 + .../backend/app/gateway/routers/agents.py | 21 +- .../app/gateway/routers/knowledge_ingest.py | 191 ++++++++++++++++++ .../harness/deerflow/config/agents_config.py | 3 + 10 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 offline-backend-20260512/backend/app/gateway/routers/knowledge_ingest.py diff --git a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx index 917fc787..261c32ff 100644 --- a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx +++ b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx @@ -15,6 +15,7 @@ import { SparklesIcon, SquarePenIcon, Trash2Icon, + UploadCloudIcon, UserRoundIcon, XIcon, } from "lucide-react"; @@ -96,6 +97,7 @@ interface AgentDraft { skills: string[]; published: boolean; featured: boolean; + templateName: string; } function draftFromAgent(agent: Agent): AgentDraft { @@ -107,6 +109,7 @@ function draftFromAgent(agent: Agent): AgentDraft { skills: agent.skills ?? [], published: agent.published, featured: agent.featured_order != null, + templateName: agent.template_name ?? "", }; } @@ -305,6 +308,8 @@ export function AgentDetailPanel({ if (draft.description !== (agent.description ?? "")) request.description = draft.description; if (draft.soul !== (agent.soul ?? "")) request.soul = draft.soul; + if (draft.templateName.trim() !== (agent.template_name ?? "")) + request.template_name = draft.templateName.trim(); if (nextPublished !== agent.published) request.published = nextPublished; if (JSON.stringify(draft.skills) !== JSON.stringify(agent.skills ?? [])) request.skills = draft.skills; @@ -546,9 +551,31 @@ export function AgentDetailPanel({ )} + {/* 入库模板名 — view mode shows a hint row when configured. */} + {!editing && agent.template_name?.trim() && ( +
+ + 入库模板:{desensitize(agent.template_name)} +
+ )} + {/* Publish / featured toggles — only meaningful in edit mode. */} {editing && draft && (
+
+

入库模板名

+ + setDraft({ ...draft, templateName: e.currentTarget.value }) + } + placeholder="例如:临时策略库(留空表示不支持入库)" + className="h-8 text-sm" + /> +

+ 配置后,对话生成的 Markdown 文件可一键发布到知识入库接口。 +

+

公开发布

diff --git a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx index 43839827..d3df711e 100644 --- a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -11,8 +11,10 @@ import { PackageIcon, SaveIcon, SquareArrowOutUpRightIcon, + UploadCloudIcon, XIcon, } from "lucide-react"; +import { useParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react"; import { toast } from "sonner"; @@ -35,6 +37,8 @@ import { } from "@/components/ui/select"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { CodeEditor } from "@/components/workspace/code-editor"; +import { publishArtifactToKnowledge } from "@/core/agents/api"; +import { useAgent } from "@/core/agents/hooks"; import { apiFetch } from "@/core/api"; import { downloadArtifactFile, @@ -86,6 +90,13 @@ function extractTextFromChildren(children: ReactNode): string { return ""; } +/** 取 md 标题用于入库:优先文中第一个标题行,否则退回文件名(去扩展名)。 */ +function extractMarkdownTitle(content: string, filepath: string): string { + const heading = /^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/m.exec(content); + if (heading?.[1]) return heading[1].trim(); + return getFileName(filepath).replace(/\.(md|markdown)$/i, ""); +} + function findScrollableArtifactElement(root: HTMLElement | null) { if (!root) { return null; @@ -439,6 +450,50 @@ export function ArtifactFileDetail({ updateArtifact, ]); + // 发布入库:仅当当前路由是智能体会话(agents/:agent_id/chats/…)且该智能体 + // 配置了入库模板名(template_name)时,md 产物右上角才出现发布按钮。 + const { agent_id: routeAgentId } = useParams<{ agent_id?: string }>(); + const { agent: routeAgent } = useAgent(routeAgentId ?? null); + const ingestTemplateName = routeAgent?.template_name?.trim() ?? ""; + const canPublishToKnowledge = + !!routeAgentId && + !!ingestTemplateName && + language === "markdown" && + !isSkillFile && + !isMock; + const [isPublishingToKnowledge, setIsPublishingToKnowledge] = useState(false); + const handlePublishToKnowledge = useCallback(async () => { + if (!routeAgentId || isPublishingToKnowledge) { + return; + } + const source = (displayContent ?? "").trim(); + if (!source) { + toast.error("Markdown 内容为空,无法发布入库"); + return; + } + setIsPublishingToKnowledge(true); + try { + const result = await publishArtifactToKnowledge({ + agent_id: routeAgentId, + title: extractMarkdownTitle(source, filepath), + content: source, + }); + if (result.success) { + toast.success(`已发布入库:${result.node_name}`); + } else { + const last = result.attempts[result.attempts.length - 1]; + toast.error( + `发布入库失败:${last?.error ?? last?.response_text ?? "接口返回失败"}`, + ); + } + } catch (error) { + console.error("Failed to publish artifact to knowledge base:", error); + toast.error(error instanceof Error ? error.message : "发布入库失败"); + } finally { + setIsPublishingToKnowledge(false); + } + }, [displayContent, filepath, isPublishingToKnowledge, routeAgentId]); + const [isExportingWord, setIsExportingWord] = useState(false); const handleDownloadAsWord = useCallback(async () => { const source = (displayContent ?? "").trim(); @@ -591,6 +646,15 @@ export function ArtifactFileDetail({
+ {canPublishToKnowledge && ( + void handlePublishToKnowledge()} + /> + )} {canEditArtifact && ( { return res.json() as Promise; } +export interface KnowledgeIngestAttempt { + title_field: string; + status_code: number | null; + response_text: string; + success: boolean; + error: string | null; +} + +export interface KnowledgeIngestPublishResult { + success: boolean; + template_name: string; + node_name: string; + batch_number: string; + attempts: KnowledgeIngestAttempt[]; +} + +/** 把一份 md 产物(标题 + 全文)发布到外部知识入库接口。后端会先按 + * 「名称」字段映射发送,失败后用同一 nodeName 换「标题」字段重试一次。 */ +export async function publishArtifactToKnowledge(request: { + agent_id: string; + title: string; + content: string; +}): Promise { + const res = await apiFetch( + `${getBackendBaseURL()}/api/knowledge-ingest/publish`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }, + ); + if (!res.ok) { + const err = (await res.json().catch(() => ({}))) as { detail?: string }; + throw new Error(err.detail ?? `发布入库失败: ${res.statusText}`); + } + return res.json() as Promise; +} + export async function checkAgentName( name: string, ): Promise<{ available: boolean; name: string }> { diff --git a/frontend-web/src/core/agents/types.ts b/frontend-web/src/core/agents/types.ts index 1290a2f2..0af55b85 100644 --- a/frontend-web/src/core/agents/types.ts +++ b/frontend-web/src/core/agents/types.ts @@ -55,6 +55,9 @@ export interface Agent { * New agents store `{title, prompt}`; legacy agents may still return `string[]`. * Read via {@link normalizeAgentQuestions}. */ common_questions?: (string | AgentQuestion)[] | null; + /** 入库模板名(外部知识入库接口的 templateName)。配置后该智能体会话中的 + * Markdown 产物右上角出现"发布入库"按钮。空/缺省 = 不支持入库。 */ + template_name?: string | null; tags?: Tag[]; featured_order?: number | null; /** Whether the current user has favorited (收藏) this agent. */ @@ -87,6 +90,7 @@ export interface CreateAgentRequest { model?: string | null; tool_groups?: string[] | null; common_questions?: AgentQuestion[] | null; + template_name?: string | null; } export interface UpdateAgentRequest { @@ -100,4 +104,6 @@ export interface UpdateAgentRequest { model?: string | null; tool_groups?: string[] | null; common_questions?: AgentQuestion[] | null; + /** 入库模板名;传空字符串表示清除。 */ + template_name?: string | null; } diff --git a/frontend-web/src/pages/NewAgentPage.tsx b/frontend-web/src/pages/NewAgentPage.tsx index 61295a5b..4ab2564a 100644 --- a/frontend-web/src/pages/NewAgentPage.tsx +++ b/frontend-web/src/pages/NewAgentPage.tsx @@ -82,6 +82,7 @@ export default function NewAgentPage() { const [soulInput, setSoulInput] = useState(""); const [modelInput, setModelInput] = useState(""); const [chatModelInput, setChatModelInput] = useState(""); + const [templateNameInput, setTemplateNameInput] = useState(""); const [formError, setFormError] = useState(""); const [agentId, setAgentId] = useState(""); const [agent, setAgent] = useState(null); @@ -228,6 +229,7 @@ export default function NewAgentPage() { soul: soulInput, skills: selectedSkills, model: modelOverride, + template_name: templateNameInput.trim(), }); setAgent(updated); return updated; @@ -248,6 +250,7 @@ export default function NewAgentPage() { soul: soulInput, skills: selectedSkills, model: modelOverride, + template_name: templateNameInput.trim(), }); setAgentId(created.id); setAgent(created); @@ -277,6 +280,7 @@ export default function NewAgentPage() { modelInput, selectedSkills, soulInput, + templateNameInput, t.agents.nameStepCheckError, t.agents.nameStepNetworkError, validateForm, @@ -549,6 +553,21 @@ export default function NewAgentPage() {

+
+ + + setTemplateNameInput(event.currentTarget.value) + } + /> +

+ 配置后,该智能体对话中生成的 Markdown + 文件右上角会出现"发布入库"按钮,可一键发送到知识入库接口。 +

+
+