feat: 智能体 md 产物一键发布到外部知识入库接口
- 智能体新增「入库模板名」(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) <noreply@anthropic.com>
This commit is contained in:
1 parent
d9d5780ea7
commit
229830ee89
10 files changed
+371
-3
No files matched your search
@@ -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({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 入库模板名 — view mode shows a hint row when configured. */}
|
||||
{!editing && agent.template_name?.trim() && (
|
||||
<div className="text-muted-foreground mt-2 flex items-center gap-1 text-xs">
|
||||
<UploadCloudIcon className="size-3 shrink-0" />
|
||||
入库模板:{desensitize(agent.template_name)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Publish / featured toggles — only meaningful in edit mode. */}
|
||||
{editing && draft && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="bg-background/60 space-y-1 rounded-lg border px-3 py-2">
|
||||
<p className="text-sm font-medium">入库模板名</p>
|
||||
<Input
|
||||
value={draft.templateName}
|
||||
onChange={(e) =>
|
||||
setDraft({ ...draft, templateName: e.currentTarget.value })
|
||||
}
|
||||
placeholder="例如:临时策略库(留空表示不支持入库)"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
配置后,对话生成的 Markdown 文件可一键发布到知识入库接口。
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-background/60 flex items-center justify-between rounded-lg border px-3 py-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium">公开发布</p>
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<ArtifactActions className="bg-muted/45 rounded-full border border-black/6 p-1 dark:border-white/8">
|
||||
{canPublishToKnowledge && (
|
||||
<ArtifactAction
|
||||
icon={isPublishingToKnowledge ? LoaderIcon : UploadCloudIcon}
|
||||
label="发布入库"
|
||||
tooltip={`发布到知识库(模板:${ingestTemplateName})`}
|
||||
disabled={isPublishingToKnowledge || !displayContent}
|
||||
onClick={() => void handlePublishToKnowledge()}
|
||||
/>
|
||||
)}
|
||||
{canEditArtifact && (
|
||||
<ArtifactAction
|
||||
icon={updateArtifact.isPending ? LoaderIcon : SaveIcon}
|
||||
|
||||
Reference in new issue
Block a user