From 81e4fd92cee15d640afd9ed906d8711b49510614 Mon Sep 17 00:00:00 2001 From: Administrator Date: Mon, 8 Jun 2026 18:31:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(knowledge):=20=E9=98=B6=E6=AE=B5=E4=B8=89?= =?UTF-8?q?=20=E7=9F=A5=E8=AF=86=E6=B2=BB=E7=90=86=20+=20=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E5=9B=9B=20=E5=9B=BE=E8=B0=B1=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 阶段三(治理): - sensitive.py:写入前脱敏 token/密钥/手机号/身份证/邮箱(sensitive_redaction_enabled) - 版本快照:更新前写 knowledge_note_versions;GET /notes/{id}/versions - dedup.py:标题/摘要 Jaccard + 内容哈希查重;GET /duplicates、POST /merge(来源并入主条目、重复项归档) - auto_ingest.py:独立后台队列/线程沉淀,不阻塞问答;中间件 after_agent 在 auto_ingest_enabled 时入队;价值判断(最小字符)+ skip_existing 去重;自动沉淀落 draft 待审 阶段四(图谱): - LLM 抽取 entities/relations;service 存 knowledge_entities/knowledge_relations 并连边 - exporter.build_graph 支持实体节点 + note→entity/entity→entity 边;graph.html/json 增强 - GET /api/knowledge/graph 返回产品图谱数据 前端:知识库页「查重合并」对话框 + 相关 api/hooks 配置:sensitive_redaction_enabled / dedup_similarity_threshold / entity_extraction_enabled 测试:tests/test_knowledge.py 20 passed Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend-web/src/core/knowledge/api.ts | 58 ++++++++ frontend-web/src/core/knowledge/hooks.ts | 16 +++ frontend-web/src/pages/KnowledgeBasePage.tsx | 84 ++++++++++++ .../backend/app/gateway/deps.py | 25 ++++ .../backend/app/gateway/routers/knowledge.py | 32 +++++ offline-backend-20260512/backend/config.yaml | 3 + .../middlewares/knowledge_rag_middleware.py | 56 ++++++++ .../deerflow/config/knowledge_config.py | 3 + .../harness/deerflow/knowledge/auto_ingest.py | 125 ++++++++++++++++++ .../harness/deerflow/knowledge/dedup.py | 75 +++++++++++ .../harness/deerflow/knowledge/exporter.py | 33 ++++- .../deerflow/knowledge/llm_extractor.py | 7 + .../deerflow/knowledge/obsidian_adapter.py | 18 ++- .../harness/deerflow/knowledge/schemas.py | 4 + .../harness/deerflow/knowledge/sensitive.py | 54 ++++++++ .../harness/deerflow/knowledge/service.py | 105 ++++++++++++++- 16 files changed, 691 insertions(+), 7 deletions(-) create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/knowledge/auto_ingest.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/knowledge/dedup.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/knowledge/sensitive.py diff --git a/frontend-web/src/core/knowledge/api.ts b/frontend-web/src/core/knowledge/api.ts index 7dfe9d20..6f9c9307 100644 --- a/frontend-web/src/core/knowledge/api.ts +++ b/frontend-web/src/core/knowledge/api.ts @@ -131,3 +131,61 @@ export async function exportKnowledgeGraph() { export function knowledgeExportFileUrl(filename: string) { return `${baseUrl()}/export/files/${encodeURIComponent(filename)}`; } + +// --- phase 2/3/4 --- + +export interface KnowledgeVersion { + id: string; + note_id: string; + title: string; + summary?: string | null; + content_md: string; + status: string; + tags: string[]; + edited_by?: string | null; + created_at?: string | null; +} + +export async function listNoteVersions(noteId: string) { + const res = await parseResponse<{ items: KnowledgeVersion[] }>( + await apiFetch(`${baseUrl()}/notes/${encodeURIComponent(noteId)}/versions`), + ); + return res.items; +} + +export interface DuplicateGroup { + primary: { id: string; title: string | null }; + duplicates: Array<{ id: string; title: string | null; score: number }>; +} + +export async function listDuplicates() { + const res = await parseResponse<{ groups: DuplicateGroup[] }>( + await apiFetch(`${baseUrl()}/duplicates`), + ); + return res.groups; +} + +export async function mergeKnowledgeNotes(primaryId: string, duplicateIds: string[]) { + return parseResponse<{ note: KnowledgeNote }>( + await apiFetch(`${baseUrl()}/merge`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ primary_id: primaryId, duplicate_ids: duplicateIds }), + }), + ); +} + +export async function reindexKnowledge() { + return parseResponse<{ reindexed: number; embedding_enabled: number }>( + await apiFetch(`${baseUrl()}/reindex`, { method: "POST" }), + ); +} + +export interface KnowledgeGraph { + nodes: Array<{ id: string; type: string; label: string; category?: string | null }>; + edges: Array<{ id: string; source: string; target: string; label: string; weight: number }>; +} + +export async function getKnowledgeGraph() { + return parseResponse(await apiFetch(`${baseUrl()}/graph`)); +} diff --git a/frontend-web/src/core/knowledge/hooks.ts b/frontend-web/src/core/knowledge/hooks.ts index 5e304a60..22b27c5e 100644 --- a/frontend-web/src/core/knowledge/hooks.ts +++ b/frontend-web/src/core/knowledge/hooks.ts @@ -12,7 +12,9 @@ import { getKnowledgeNote, ingestThread, ingestThreadsBatch, + listDuplicates, listKnowledgeNotes, + mergeKnowledgeNotes, searchKnowledge, updateKnowledgeNote, } from "./api"; @@ -85,5 +87,19 @@ export function useKnowledgeActions() { onSuccess: invalidate, }), exportGraph: useMutation({ mutationFn: exportKnowledgeGraph }), + merge: useMutation({ + mutationFn: ({ primaryId, duplicateIds }: { primaryId: string; duplicateIds: string[] }) => + mergeKnowledgeNotes(primaryId, duplicateIds), + onSuccess: invalidate, + }), }; } + +export function useKnowledgeDuplicates(enabled: boolean) { + return useQuery({ + queryKey: [...ROOT_KEY, "duplicates"], + queryFn: listDuplicates, + enabled, + refetchOnWindowFocus: false, + }); +} diff --git a/frontend-web/src/pages/KnowledgeBasePage.tsx b/frontend-web/src/pages/KnowledgeBasePage.tsx index aa7c24a3..4b7315bd 100644 --- a/frontend-web/src/pages/KnowledgeBasePage.tsx +++ b/frontend-web/src/pages/KnowledgeBasePage.tsx @@ -1,5 +1,6 @@ import { BookOpen, + Copy, ExternalLink, Loader2, Network, @@ -37,6 +38,7 @@ import { import { Textarea } from "@/components/ui/textarea"; import { useKnowledgeActions, + useKnowledgeDuplicates, useKnowledgeNote, useKnowledgeNotes, } from "@/core/knowledge/hooks"; @@ -270,6 +272,83 @@ function BatchImportDialog({ ); } +function DedupDialog({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; +}) { + const { data: groups = [], isLoading } = useKnowledgeDuplicates(open); + const { merge } = useKnowledgeActions(); + + const handleMerge = async (primaryId: string, duplicateIds: string[]) => { + try { + await merge.mutateAsync({ primaryId, duplicateIds }); + toast.success("已合并重复知识"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "合并失败"); + } + }; + + return ( + + + + 查重与合并 + + 基于标题/摘要相似度与内容哈希检测重复知识;合并会把重复项归档并把来源并入主条目。 + + +
+ {isLoading ? ( +
检测中...
+ ) : groups.length === 0 ? ( +
+ 未发现明显重复的知识。 +
+ ) : ( +
    + {groups.map((g) => ( +
  • +
    + 保留:{g.primary.title || g.primary.id} +
    +
      + {g.duplicates.map((d) => ( +
    • + 合并:{d.title || d.id}(相似度 {Math.round(d.score * 100)}%) +
    • + ))} +
    + +
  • + ))} +
+ )} +
+ + + +
+
+ ); +} + function NoteDetail({ noteId }: { noteId: string }) { const navigate = useNavigate(); const { data: note, isLoading } = useKnowledgeNote(noteId); @@ -479,6 +558,7 @@ export default function KnowledgeBasePage() { const [status, setStatus] = useState("all"); const [createOpen, setCreateOpen] = useState(false); const [batchOpen, setBatchOpen] = useState(false); + const [dedupOpen, setDedupOpen] = useState(false); const params = useMemo( () => ({ @@ -535,6 +615,9 @@ export default function KnowledgeBasePage() { +