feat(knowledge): 支持文件导入沉淀知识,知识图谱可视化随主题切换

- 后端新增 /api/knowledge/ingest/file:上传 md/txt/pdf/word/ppt/excel,
  转 Markdown 后走 wiki-capture LLM 蒸馏为陈述式知识笔记,失败回退逐字草稿
- service.capture_file + 文档专用蒸馏提示词,按主题拆分 1~4 页
- 知识图谱导出 HTML 支持 ?theme=light|dark,布局稳定后冻结,
  按度数缩放节点,悬停/点击聚焦邻居
- 前端知识库页新增「导入文件」对话框与 ingestKnowledgeFile API
- 智能体问答页与简介侧边栏 UI 优化;批量人物蒸馏脚本提取正文逻辑修正

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-11 10:25:27 +08:00
1 parent 17bb3715d1
commit 3845d5090b
14 files changed
+1040 -352

No files matched your search

@@ -11,6 +11,7 @@ import {
Search,
Sparkles,
Trash2,
Upload,
X,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
@@ -323,6 +324,119 @@ function BatchImportDialog({
);
}
function ImportFileDialog({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (v: boolean) => void;
}) {
const { ingestFile } = useKnowledgeActions();
const navigate = useNavigate();
const [file, setFile] = useState<File | null>(null);
const [title, setTitle] = useState("");
const [tags, setTags] = useState("");
const [model, setModel] = useState<string | null>(null);
const reset = () => {
setFile(null);
setTitle("");
setTags("");
setModel(null);
};
const handleImport = async () => {
if (!file) {
toast.error("请选择文件");
return;
}
try {
const result = await ingestFile.mutateAsync({
file,
opts: {
title: title.trim() || undefined,
tags: tags.trim() || undefined,
status: "approved",
model_name: model,
},
});
const count = result.notes?.length ?? 1;
if (result.llm_fallback) {
// The model produced nothing usable — the file was stored verbatim as a
// draft instead of distilled into a wiki note.
toast.warning(
`已导入 ${count} 条(大模型异常,改用规则抽取并存为草稿)`,
);
} else {
toast.success(`已从文件生成 ${count} 条知识`);
}
onOpenChange(false);
reset();
if (result.note?.id) {
navigate(`/page/workspace/knowledge/${result.note.id}`);
}
} catch (error) {
toast.error(error instanceof Error ? error.message : "文件导入失败");
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>导入文件生成 Wiki</DialogTitle>
<DialogDescription>
上传文档(Markdown / TXT / PDF / Word / PPT / Excel),自动提炼为知识库 wiki 词条并写入 vault。
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4">
<label className="flex flex-col gap-1.5 text-sm">
选择文件
<Input
type="file"
accept=".md,.markdown,.txt,.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
className="cursor-pointer"
/>
{file ? (
<span className="text-muted-foreground truncate text-xs">
{file.name} · {(file.size / 1024).toFixed(0)} KB
</span>
) : null}
</label>
<Input
placeholder="标题(可选,留空则由模型生成)"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Input
placeholder="标签,逗号分隔(可选)"
value={tags}
onChange={(e) => setTags(e.target.value)}
/>
<label className="flex items-center justify-between gap-2 text-sm">
提炼大模型
<ModelNameSelect
value={model}
onChange={setModel}
disabled={ingestFile.isPending}
triggerClassName="h-9 w-[11rem]"
/>
</label>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
取消
</Button>
<Button onClick={() => void handleImport()} disabled={ingestFile.isPending || !file}>
{ingestFile.isPending ? "导入中..." : "导入并生成"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function DedupDialog({
open,
onOpenChange,
@@ -698,6 +812,7 @@ export default function KnowledgeBasePage() {
const [status, setStatus] = useState("all");
const [createOpen, setCreateOpen] = useState(false);
const [batchOpen, setBatchOpen] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [dedupOpen, setDedupOpen] = useState(false);
const params = useMemo(
@@ -752,6 +867,9 @@ export default function KnowledgeBasePage() {
</SelectContent>
</Select>
<div className="ml-auto flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setImportOpen(true)}>
<Upload className="size-4" /> 导入文件
</Button>
<Button variant="outline" size="sm" onClick={() => setBatchOpen(true)}>
<RefreshCw className="size-4" /> 批量导入
</Button>
@@ -814,6 +932,7 @@ export default function KnowledgeBasePage() {
</div>
<CreateNoteDialog open={createOpen} onOpenChange={setCreateOpen} />
<ImportFileDialog open={importOpen} onOpenChange={setImportOpen} />
<BatchImportDialog open={batchOpen} onOpenChange={setBatchOpen} />
<DedupDialog open={dedupOpen} onOpenChange={setDedupOpen} />
</div>