import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Button, Empty, Form, Input, Modal, Select, Spin, Table, Tabs, Tag, Tooltip, message, } from "antd"; import { ArrowLeftOutlined, CheckOutlined, CloudUploadOutlined, DatabaseOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, FileAddOutlined, FileTextOutlined, PictureOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, TeamOutlined, UploadOutlined, } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import ImageWithFallback from "../common/ImageWithFallback"; import { getCurrentAuth } from "../../services/core/auth"; import { AiAgentContext, AiManualComponent, addAiAgentKnowledgeComponent, enableAiAgentKnowledgeForChat, fetchAiAgentComponents, isAiKnowledgeChatSelectable, resolveAiAgentContext, } from "../../services/core/aiChat"; import { fetchPublishedWorkflows, fetchSpaces, PublishedWorkflow, SpaceInfo, } from "../../services/core/marketplace"; import { KnowledgeComponent, KnowledgeConfigSaveParams, KnowledgeDocumentInfo, KnowledgeInfo, KnowledgeModelOption, KnowledgeQaInfo, KnowledgeRawSegmentInfo, addCustomKnowledgeDocument, addKnowledgeDocuments, addKnowledgeQa, addKnowledgeRawSegment, createKnowledgeConfig, deleteKnowledgeConfig, deleteKnowledgeDocument, deleteKnowledgeQa, deleteKnowledgeRawSegment, downloadKnowledgeQaTemplate, fetchEmbeddingModels, fetchKnowledgeComponents, fetchKnowledgeDetail, fetchKnowledgeDocuments, fetchKnowledgeQaList, fetchKnowledgeRawSegments, generateKnowledgeDocumentEmbeddings, generateKnowledgeDocumentQa, getKnowledgeId, importKnowledgeQaExcel, queryKnowledgeDocumentStatus, retryKnowledgeDocumentTask, updateKnowledgeConfig, updateKnowledgeDocumentName, updateKnowledgeQa, updateKnowledgeRawSegment, uploadKnowledgeAsset, uploadKnowledgeFile, validateKnowledgeFile, } from "../../services/core/knowledge"; import styles from "../../styles/components/KnowledgePage.module.css"; const DOCUMENT_PAGE_SIZE = 48; const SEGMENT_PAGE_SIZE = 30; const QA_PAGE_SIZE = 20; const KNOWLEDGE_ICON_MAX_SIZE = 2 * 1024 * 1024; type DetailTabKey = "documents" | "segments" | "qa" | "graph"; interface KnowledgeFormValues { name: string; description?: string; embeddingModelId: number; dataParsingMethod?: "default" | "workflow"; workflowId?: number; } interface CustomDocumentValues { name: string; fileContent: string; } interface RenameDocumentValues { name: string; } interface SegmentFormValues { rawTxt: string; sortIndex?: number; } interface QaFormValues { question: string; answer: string; } function formatDateTime(value?: string): string { if (!value) return "-"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } function formatBytes(value?: number): string { if (!value) return "0 B"; if (value >= 1024 * 1024 * 1024) { return `${(value / 1024 / 1024 / 1024).toFixed(1)} GB`; } if (value >= 1024 * 1024) { return `${(value / 1024 / 1024).toFixed(1)} MB`; } if (value >= 1024) return `${(value / 1024).toFixed(1)} KB`; return `${value} B`; } function getInitial(name?: string): string { const trimmed = (name || "").trim(); return trimmed ? trimmed.slice(0, 1).toUpperCase() : "知"; } function getDocumentStatus(doc: KnowledgeDocumentInfo): { color: string; text: string; } { const code = Number(doc.docStatusCode || 0); if (code === 2) return { color: "success", text: "分析成功" }; if (code === 10) return { color: "error", text: "分析失败" }; if ([1, 3, 4, 5].includes(code)) { return { color: "processing", text: doc.docStatusDesc || "处理中" }; } return { color: "default", text: doc.docStatusDesc || doc.docStatus || "未知", }; } function isKnowledgeComponent(component: AiManualComponent): boolean { return String(component.type || "").toLowerCase() === "knowledge"; } function getPublishedWorkflowId(workflow: PublishedWorkflow): number { return Number(workflow.targetId || workflow.id); } export default function KnowledgePage() { const [currentUserId, setCurrentUserId] = useState(null); const [spaces, setSpaces] = useState([]); const [selectedSpaceId, setSelectedSpaceId] = useState(null); const [knowledges, setKnowledges] = useState([]); const [loadingKnowledges, setLoadingKnowledges] = useState(false); const [knowledgeKeyword, setKnowledgeKeyword] = useState(""); const [knowledgeError, setKnowledgeError] = useState(null); const [selectedKnowledgeId, setSelectedKnowledgeId] = useState( null, ); const [knowledgeInfo, setKnowledgeInfo] = useState( null, ); const [loadingDetail, setLoadingDetail] = useState(false); const [activeTab, setActiveTab] = useState("documents"); const [documents, setDocuments] = useState([]); const [documentTotal, setDocumentTotal] = useState(0); const [documentKeyword, setDocumentKeyword] = useState(""); const [loadingDocuments, setLoadingDocuments] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); const [uploadingDocs, setUploadingDocs] = useState(false); const [segments, setSegments] = useState([]); const [segmentTotal, setSegmentTotal] = useState(0); const [loadingSegments, setLoadingSegments] = useState(false); const [segmentModalOpen, setSegmentModalOpen] = useState(false); const [editingSegment, setEditingSegment] = useState(null); const [qaList, setQaList] = useState([]); const [qaTotal, setQaTotal] = useState(0); const [qaKeyword, setQaKeyword] = useState(""); const [loadingQa, setLoadingQa] = useState(false); const [qaModalOpen, setQaModalOpen] = useState(false); const [editingQa, setEditingQa] = useState(null); const [models, setModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); const [parsingWorkflows, setParsingWorkflows] = useState( [], ); const [loadingParsingWorkflows, setLoadingParsingWorkflows] = useState(false); const [iconUrl, setIconUrl] = useState(""); const [uploadingIcon, setUploadingIcon] = useState(false); const [knowledgeModalOpen, setKnowledgeModalOpen] = useState(false); const [editingKnowledge, setEditingKnowledge] = useState(null); const [savingKnowledge, setSavingKnowledge] = useState(false); const [customDocModalOpen, setCustomDocModalOpen] = useState(false); const [savingCustomDoc, setSavingCustomDoc] = useState(false); const [renameDocModalOpen, setRenameDocModalOpen] = useState(false); const [renamingDoc, setRenamingDoc] = useState( null, ); const [savingRename, setSavingRename] = useState(false); const [agentContext, setAgentContext] = useState(null); const [joinedKnowledgeMap, setJoinedKnowledgeMap] = useState< Map >(new Map()); const [joiningIds, setJoiningIds] = useState>(() => new Set()); const [knowledgeForm] = Form.useForm(); const [customDocForm] = Form.useForm(); const [renameDocForm] = Form.useForm(); const [segmentForm] = Form.useForm(); const [qaForm] = Form.useForm(); const dataParsingMethod = Form.useWatch("dataParsingMethod", knowledgeForm) || "default"; const iconFileInputRef = useRef(null); const documentFileInputRef = useRef(null); const qaFileInputRef = useRef(null); const filteredKnowledges = useMemo(() => { const keyword = knowledgeKeyword.trim().toLowerCase(); if (!keyword) return knowledges; return knowledges.filter((item) => [item.name, item.description] .filter(Boolean) .some((value) => String(value).toLowerCase().includes(keyword)), ); }, [knowledgeKeyword, knowledges]); const selectedKnowledgeComponent = useMemo( () => selectedKnowledgeId ? knowledges.find( (item) => getKnowledgeId(item) === selectedKnowledgeId, ) || null : null, [knowledges, selectedKnowledgeId], ); const isKnowledgeOwner = useCallback( (knowledge?: KnowledgeComponent | KnowledgeInfo | null) => { if (!knowledge) return false; const creator = (knowledge as KnowledgeComponent).creator; const creatorId = Number(knowledge.creatorId || creator?.id || 0); return Boolean(currentUserId && creatorId && currentUserId === creatorId); }, [currentUserId], ); const canManageSelectedKnowledge = isKnowledgeOwner( knowledgeInfo || selectedKnowledgeComponent, ); const ensureCanManageSelectedKnowledge = useCallback(() => { if (canManageSelectedKnowledge) return true; message.warning("该知识库由其他用户创建,仅支持查看和加入对话"); return false; }, [canManageSelectedKnowledge]); const loadAgentComponents = useCallback(async () => { try { const context = await resolveAiAgentContext(); setAgentContext(context); const components = await fetchAiAgentComponents(context.agentId); const map = new Map(); components.filter(isKnowledgeComponent).forEach((component) => { const targetId = Number(component.targetId || 0); if (targetId) map.set(targetId, component); }); setJoinedKnowledgeMap(map); } catch (error) { console.warn("[KnowledgePage] Failed to load agent components:", error); setAgentContext(null); setJoinedKnowledgeMap(new Map()); } }, []); const loadSpaces = useCallback(async (): Promise => { try { const result = await fetchSpaces(); const nextSpaces = result || []; setSpaces(nextSpaces); setSelectedSpaceId((prev) => prev || nextSpaces[0]?.id || null); return nextSpaces; } catch (error) { console.warn("[KnowledgePage] Failed to load spaces:", error); return []; } }, []); const ensureDefaultSpaceId = useCallback( async (showError = true): Promise => { if (selectedSpaceId) return selectedSpaceId; const list = spaces.length > 0 ? spaces : await loadSpaces(); const defaultSpaceId = list[0]?.id || null; if (defaultSpaceId) { setSelectedSpaceId(defaultSpaceId); return defaultSpaceId; } if (showError) { message.error("无法获取默认工作区,暂时不能创建知识库"); } return null; }, [loadSpaces, selectedSpaceId, spaces], ); const loadKnowledges = useCallback(async () => { if (!selectedSpaceId) { setKnowledges([]); return; } setLoadingKnowledges(true); setKnowledgeError(null); try { const list = await fetchKnowledgeComponents(selectedSpaceId); setKnowledges(list); } catch (error) { const errorMessage = error instanceof Error ? error.message : "知识库加载失败"; setKnowledgeError(errorMessage); setKnowledges([]); } finally { setLoadingKnowledges(false); } }, [selectedSpaceId]); const loadKnowledgeDetail = useCallback( async (knowledgeId = selectedKnowledgeId) => { if (!knowledgeId) return; setLoadingDetail(true); try { const detail = await fetchKnowledgeDetail(knowledgeId); setKnowledgeInfo(detail); } catch (error) { message.error( error instanceof Error ? error.message : "知识库详情加载失败", ); } finally { setLoadingDetail(false); } }, [selectedKnowledgeId], ); const loadDocuments = useCallback( async (keyword = documentKeyword) => { if (!selectedKnowledgeId) return; setLoadingDocuments(true); try { const result = await fetchKnowledgeDocuments({ kbId: selectedKnowledgeId, name: keyword.trim() || undefined, current: 1, pageSize: DOCUMENT_PAGE_SIZE, }); const records = result.records || []; setDocuments(records); setDocumentTotal(result.total || 0); setSelectedDocument((prev) => { if (prev && records.some((item) => item.id === prev.id)) return prev; return records[0] || null; }); } catch (error) { message.error( error instanceof Error ? error.message : "文档列表加载失败", ); setDocuments([]); setDocumentTotal(0); setSelectedDocument(null); } finally { setLoadingDocuments(false); } }, [documentKeyword, selectedKnowledgeId], ); const loadSegments = useCallback(async () => { if (!selectedDocument) { setSegments([]); setSegmentTotal(0); return; } setLoadingSegments(true); try { const result = await fetchKnowledgeRawSegments({ docId: selectedDocument.id, current: 1, pageSize: SEGMENT_PAGE_SIZE, }); setSegments(result.records || []); setSegmentTotal(result.total || 0); } catch (error) { message.error(error instanceof Error ? error.message : "分段加载失败"); setSegments([]); setSegmentTotal(0); } finally { setLoadingSegments(false); } }, [selectedDocument]); const loadQaList = useCallback( async (keyword = qaKeyword) => { if (!selectedKnowledgeId) return; setLoadingQa(true); try { const result = await fetchKnowledgeQaList({ kbId: selectedKnowledgeId, spaceId: selectedSpaceId || undefined, question: keyword.trim() || undefined, current: 1, pageSize: QA_PAGE_SIZE, }); setQaList(result.records || []); setQaTotal(result.total || 0); } catch (error) { message.error( error instanceof Error ? error.message : "QA 列表加载失败", ); setQaList([]); setQaTotal(0); } finally { setLoadingQa(false); } }, [qaKeyword, selectedKnowledgeId, selectedSpaceId], ); const loadEmbeddingModels = useCallback(async (spaceId?: number) => { setLoadingModels(true); try { const result = await fetchEmbeddingModels(spaceId); setModels(result || []); } catch (error) { message.error( error instanceof Error ? error.message : "嵌入模型加载失败", ); setModels([]); } finally { setLoadingModels(false); } }, []); const loadParsingWorkflows = useCallback(async (spaceId?: number) => { setLoadingParsingWorkflows(true); try { const result = await fetchPublishedWorkflows({ page: 1, pageSize: 80, category: "all", spaceId, }); setParsingWorkflows(result.records || []); } catch (error) { message.error( error instanceof Error ? error.message : "解析工作流加载失败", ); setParsingWorkflows([]); } finally { setLoadingParsingWorkflows(false); } }, []); useEffect(() => { loadSpaces(); loadAgentComponents(); void getCurrentAuth().then((auth) => { setCurrentUserId(auth.userInfo?.userId || null); }); }, [loadAgentComponents, loadSpaces]); useEffect(() => { loadKnowledges(); }, [loadKnowledges]); useEffect(() => { if (!selectedKnowledgeId) return; loadKnowledgeDetail(selectedKnowledgeId); loadDocuments(""); }, [loadDocuments, loadKnowledgeDetail, selectedKnowledgeId]); useEffect(() => { if (activeTab === "segments") loadSegments(); }, [activeTab, loadSegments]); useEffect(() => { if (activeTab === "qa") loadQaList(); }, [activeTab, loadQaList]); useEffect(() => { if (!knowledgeModalOpen || dataParsingMethod !== "workflow") return; if (loadingParsingWorkflows || parsingWorkflows.length > 0) return; void (async () => { const spaceId = selectedSpaceId || (await ensureDefaultSpaceId(false)); await loadParsingWorkflows(spaceId || undefined); })(); }, [ dataParsingMethod, ensureDefaultSpaceId, knowledgeModalOpen, loadParsingWorkflows, loadingParsingWorkflows, parsingWorkflows.length, selectedSpaceId, ]); const openKnowledgeForm = async ( knowledge?: KnowledgeComponent | KnowledgeInfo, ) => { if (knowledge && !isKnowledgeOwner(knowledge)) { message.warning("该知识库由其他用户创建,不能编辑"); return; } setKnowledgeModalOpen(true); void (async () => { const spaceId = (knowledge as KnowledgeInfo | undefined)?.spaceId || selectedSpaceId || (await ensureDefaultSpaceId(false)); await loadEmbeddingModels(spaceId || undefined); })(); if (knowledge) { try { const detail = "embeddingModelId" in knowledge && knowledge.embeddingModelId ? (knowledge as KnowledgeInfo) : await fetchKnowledgeDetail(getKnowledgeId(knowledge)); setEditingKnowledge(detail); setIconUrl(detail.icon || ""); knowledgeForm.setFieldsValue({ name: detail.name, description: detail.description, embeddingModelId: detail.embeddingModelId, dataParsingMethod: detail.workflowId ? "workflow" : "default", workflowId: detail.workflowId ? Number(detail.workflowId) : undefined, }); if (detail.workflowId) { await loadParsingWorkflows( detail.spaceId || selectedSpaceId || undefined, ); setParsingWorkflows((prev) => { const workflowId = Number(detail.workflowId); if ( prev.some((item) => getPublishedWorkflowId(item) === workflowId) ) { return prev; } return [ { id: workflowId, targetId: workflowId, name: detail.workflowName || `工作流 ${workflowId}`, description: detail.workflowDescription, icon: detail.workflowIcon, } as PublishedWorkflow, ...prev, ]; }); } } catch (error) { message.error( error instanceof Error ? error.message : "知识库详情加载失败", ); setKnowledgeModalOpen(false); } } else { setEditingKnowledge(null); setIconUrl(""); knowledgeForm.resetFields(); knowledgeForm.setFieldsValue({ dataParsingMethod: "default" }); } }; const saveKnowledge = async () => { if (editingKnowledge && !isKnowledgeOwner(editingKnowledge)) { message.warning("该知识库由其他用户创建,不能编辑"); return; } let values: KnowledgeFormValues; try { values = await knowledgeForm.validateFields(); } catch { return; } if (values.dataParsingMethod === "workflow" && !values.workflowId) { message.error("请选择解析工作流"); return; } const spaceId = editingKnowledge?.spaceId || selectedSpaceId || (await ensureDefaultSpaceId()); if (!spaceId) return; setSavingKnowledge(true); try { const params: KnowledgeConfigSaveParams = { id: editingKnowledge?.id, spaceId, name: values.name, description: values.description || "", embeddingModelId: values.embeddingModelId, icon: iconUrl || "", dataType: 1, workflowId: values.dataParsingMethod === "workflow" && values.workflowId ? values.workflowId : null, }; if (editingKnowledge?.id) { await updateKnowledgeConfig(params); message.success("知识库已更新"); await loadKnowledgeDetail(editingKnowledge.id); } else { const id = await createKnowledgeConfig(params); message.success("知识库已创建"); setSelectedKnowledgeId(Number(id)); setActiveTab("documents"); } setKnowledgeModalOpen(false); await loadKnowledges(); } catch (error) { if (error instanceof Error) message.error(error.message); } finally { setSavingKnowledge(false); } }; const confirmDeleteKnowledge = (knowledge: KnowledgeComponent) => { if (!isKnowledgeOwner(knowledge)) { message.warning("该知识库由其他用户创建,不能删除"); return; } const knowledgeId = getKnowledgeId(knowledge); Modal.confirm({ title: "删除知识库", content: `确认删除「${knowledge.name}」吗?该操作不可恢复。`, okText: "删除", okButtonProps: { danger: true }, cancelText: "取消", onOk: async () => { await deleteKnowledgeConfig(knowledgeId); message.success("知识库已删除"); if (selectedKnowledgeId === knowledgeId) { setSelectedKnowledgeId(null); setKnowledgeInfo(null); } await loadKnowledges(); }, }); }; const joinKnowledgeToChat = async ( knowledge: KnowledgeComponent | KnowledgeInfo, ) => { const knowledgeId = getKnowledgeId(knowledge); const joinedComponent = joinedKnowledgeMap.get(knowledgeId); if (joinedComponent && isAiKnowledgeChatSelectable(joinedComponent)) { message.info("该知识库已加入对话"); return; } const context = agentContext || (await resolveAiAgentContext()); setAgentContext(context); setJoiningIds((prev) => new Set(prev).add(knowledgeId)); try { let componentId = joinedComponent?.id; if (!componentId) { componentId = await addAiAgentKnowledgeComponent({ agentId: context.agentId, knowledgeId, }); } const resolvedComponentId = componentId; const components = await fetchAiAgentComponents(context.agentId); const component = components.find((item) => item.id === resolvedComponentId) || components.find( (item) => String(item.type || "").toLowerCase() === "knowledge" && Number(item.targetId || 0) === knowledgeId, ) || ({ id: resolvedComponentId, type: "Knowledge", targetId: knowledgeId, name: knowledge.name || "知识库", icon: knowledge.icon, description: knowledge.description, } as AiManualComponent); const enabledComponent = await enableAiAgentKnowledgeForChat(component); setJoinedKnowledgeMap((prev) => { const next = new Map(prev); next.set(knowledgeId, enabledComponent); return next; }); message.success("知识库已加入 AI 对话"); await loadAgentComponents(); } catch (error) { message.error(error instanceof Error ? error.message : "加入对话失败"); } finally { setJoiningIds((prev) => { const next = new Set(prev); next.delete(knowledgeId); return next; }); } }; const openKnowledgeDetail = (knowledge: KnowledgeComponent) => { setSelectedKnowledgeId(getKnowledgeId(knowledge)); setKnowledgeInfo(null); setSelectedDocument(null); setDocuments([]); setSegments([]); setQaList([]); setDocumentKeyword(""); setQaKeyword(""); setActiveTab("documents"); }; const handleIconFile = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; const isValidImage = file.type === "image/jpeg" || file.type === "image/jpg" || file.type === "image/png"; if (!isValidImage) { message.error("图标仅支持 JPG、JPEG、PNG 格式"); return; } if (file.size > KNOWLEDGE_ICON_MAX_SIZE) { message.error("图标不能超过 2MB"); return; } setUploadingIcon(true); try { const url = await uploadKnowledgeAsset(file); setIconUrl(url); message.success("图标已上传"); } catch (error) { message.error(error instanceof Error ? error.message : "图标上传失败"); } finally { setUploadingIcon(false); } }; const handleDocumentFiles = async ( event: React.ChangeEvent, ) => { const files = Array.from(event.target.files || []); event.target.value = ""; if (!files.length || !selectedKnowledgeId) return; if (!ensureCanManageSelectedKnowledge()) return; const validationError = files.map(validateKnowledgeFile).find(Boolean); if (validationError) { message.error(validationError); return; } setUploadingDocs(true); try { const uploaded = await Promise.all(files.map(uploadKnowledgeFile)); await addKnowledgeDocuments({ kbId: selectedKnowledgeId, files: uploaded, }); message.success("文档已添加,后台将继续分析处理"); await loadDocuments(); } catch (error) { message.error(error instanceof Error ? error.message : "文档上传失败"); } finally { setUploadingDocs(false); } }; const saveCustomDocument = async () => { if (!selectedKnowledgeId) return; if (!ensureCanManageSelectedKnowledge()) return; const values = await customDocForm.validateFields(); setSavingCustomDoc(true); try { await addCustomKnowledgeDocument({ kbId: selectedKnowledgeId, name: values.name, fileContent: values.fileContent, }); message.success("自定义文本已添加"); setCustomDocModalOpen(false); customDocForm.resetFields(); await loadDocuments(); } catch (error) { message.error(error instanceof Error ? error.message : "添加文本失败"); } finally { setSavingCustomDoc(false); } }; const openRenameDocument = (doc: KnowledgeDocumentInfo) => { if (!ensureCanManageSelectedKnowledge()) return; setRenamingDoc(doc); renameDocForm.setFieldsValue({ name: doc.name }); setRenameDocModalOpen(true); }; const saveDocumentName = async () => { if (!renamingDoc) return; if (!ensureCanManageSelectedKnowledge()) return; const values = await renameDocForm.validateFields(); setSavingRename(true); try { await updateKnowledgeDocumentName({ docId: renamingDoc.id, name: values.name, }); message.success("文档名称已更新"); setRenameDocModalOpen(false); await loadDocuments(); } catch (error) { message.error(error instanceof Error ? error.message : "文档重命名失败"); } finally { setSavingRename(false); } }; const refreshDocumentStatus = async () => { if (documents.length === 0) return; try { const statusList = await queryKnowledgeDocumentStatus( documents.map((doc) => doc.id), ); const statusMap = new Map(statusList.map((item) => [item.id, item])); setDocuments((prev) => prev.map((doc) => ({ ...doc, ...(statusMap.get(doc.id) || {}), })), ); message.success("文档状态已刷新"); } catch { await loadDocuments(); } }; const confirmDeleteDocument = (doc: KnowledgeDocumentInfo) => { if (!ensureCanManageSelectedKnowledge()) return; Modal.confirm({ title: "删除文档", content: `确认删除「${doc.name}」吗?`, okText: "删除", okButtonProps: { danger: true }, cancelText: "取消", onOk: async () => { await deleteKnowledgeDocument(doc.id); message.success("文档已删除"); await loadDocuments(); }, }); }; const runDocumentAction = async ( doc: KnowledgeDocumentInfo, action: "qa" | "embedding" | "retry", ) => { if (!ensureCanManageSelectedKnowledge()) return; try { if (action === "qa") { await generateKnowledgeDocumentQa(doc.id); message.success("已提交生成 QA 任务"); } else if (action === "embedding") { await generateKnowledgeDocumentEmbeddings(doc.id); message.success("已提交向量化任务"); } else { await retryKnowledgeDocumentTask(doc.id); message.success("已提交重试任务"); } await loadDocuments(); } catch (error) { message.error(error instanceof Error ? error.message : "操作失败"); } }; const openSegmentModal = (segment?: KnowledgeRawSegmentInfo) => { if (!ensureCanManageSelectedKnowledge()) return; setEditingSegment(segment || null); segmentForm.setFieldsValue({ rawTxt: segment?.rawTxt || "", sortIndex: segment?.sortIndex || segments.length, }); setSegmentModalOpen(true); }; const saveSegment = async () => { if (!ensureCanManageSelectedKnowledge()) return; const spaceId = selectedDocument?.spaceId || knowledgeInfo?.spaceId || selectedSpaceId; if (!selectedDocument || !spaceId) return; const values = await segmentForm.validateFields(); try { if (editingSegment) { await updateKnowledgeRawSegment({ spaceId, rawSegmentId: editingSegment.id, docId: selectedDocument.id, rawTxt: values.rawTxt, sortIndex: values.sortIndex, }); message.success("分段已更新"); } else { await addKnowledgeRawSegment({ spaceId, docId: selectedDocument.id, rawTxt: values.rawTxt, sortIndex: values.sortIndex, }); message.success("分段已新增"); } setSegmentModalOpen(false); await loadSegments(); } catch (error) { message.error(error instanceof Error ? error.message : "保存分段失败"); } }; const confirmDeleteSegment = (segment: KnowledgeRawSegmentInfo) => { if (!ensureCanManageSelectedKnowledge()) return; Modal.confirm({ title: "删除分段", content: "确认删除该分段吗?", okText: "删除", okButtonProps: { danger: true }, cancelText: "取消", onOk: async () => { await deleteKnowledgeRawSegment(segment.id); message.success("分段已删除"); await loadSegments(); }, }); }; const openQaModal = (qa?: KnowledgeQaInfo) => { if (!ensureCanManageSelectedKnowledge()) return; setEditingQa(qa || null); qaForm.setFieldsValue({ question: qa?.question || "", answer: qa?.answer || "", }); setQaModalOpen(true); }; const saveQa = async () => { if (!selectedKnowledgeId) return; if (!ensureCanManageSelectedKnowledge()) return; const values = await qaForm.validateFields(); try { if (editingQa) { await updateKnowledgeQa({ id: editingQa.id, question: values.question, answer: values.answer, }); message.success("QA 已更新"); } else { await addKnowledgeQa({ kbId: selectedKnowledgeId, spaceId: selectedSpaceId || undefined, question: values.question, answer: values.answer, }); message.success("QA 已新增"); } setQaModalOpen(false); await loadQaList(); } catch (error) { message.error(error instanceof Error ? error.message : "保存 QA 失败"); } }; const confirmDeleteQa = (qa: KnowledgeQaInfo) => { if (!ensureCanManageSelectedKnowledge()) return; Modal.confirm({ title: "删除 QA", content: "确认删除该问答吗?", okText: "删除", okButtonProps: { danger: true }, cancelText: "取消", onOk: async () => { await deleteKnowledgeQa(qa.id); message.success("QA 已删除"); await loadQaList(); }, }); }; const handleQaImport = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file || !selectedKnowledgeId) return; if (!ensureCanManageSelectedKnowledge()) return; try { await importKnowledgeQaExcel({ kbId: selectedKnowledgeId, file }); message.success("QA 已导入"); await loadQaList(); } catch (error) { message.error(error instanceof Error ? error.message : "QA 导入失败"); } }; const handleDownloadQaTemplate = async () => { try { const blob = await downloadKnowledgeQaTemplate(); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = "知识库QA导入模板.xlsx"; anchor.click(); URL.revokeObjectURL(url); } catch (error) { message.error(error instanceof Error ? error.message : "模板下载失败"); } }; const documentColumns: ColumnsType = [ { title: "文档", dataIndex: "name", ellipsis: true, render: (name: string, record) => ( ), }, { title: "状态", width: 120, render: (_, record) => { const status = getDocumentStatus(record); return {status.text}; }, }, { title: "QA", width: 82, render: (_, record) => (record.hasQa ? "已生成" : "未生成"), }, { title: "向量", width: 82, render: (_, record) => (record.hasEmbedding ? "已完成" : "未完成"), }, { title: "更新时间", width: 170, render: (_, record) => formatDateTime(record.modified || record.created), }, { title: "操作", width: canManageSelectedKnowledge ? 260 : 120, fixed: "right", render: (_, record) => (
{canManageSelectedKnowledge && ( <> )} {record.docUrl && ( )} {canManageSelectedKnowledge && ( )}
), }, ]; const segmentColumns: ColumnsType = [ { title: "序号", dataIndex: "sortIndex", width: 90, render: (value, _, index) => value ?? index + 1, }, { title: "分段内容", dataIndex: "rawTxt", render: (value: string) =>

{value}

, }, { title: "更新时间", width: 170, render: (_, record) => formatDateTime(record.modified || record.created), }, ...(canManageSelectedKnowledge ? [ { title: "操作", width: 120, render: (_: unknown, record: KnowledgeRawSegmentInfo) => (
), }, ] : []), ]; const qaColumns: ColumnsType = [ { title: "问题", dataIndex: "question", width: "34%", render: (value: string) =>

{value}

, }, { title: "答案", dataIndex: "answer", render: (value: string) =>

{value}

, }, { title: "更新时间", width: 170, render: (_, record) => formatDateTime(record.modified || record.created), }, ...(canManageSelectedKnowledge ? [ { title: "操作", width: 120, render: (_: unknown, record: KnowledgeQaInfo) => (
), }, ] : []), ]; const renderKnowledgeIcon = ( knowledge: KnowledgeComponent | KnowledgeInfo | null, ) => ( {getInitial(knowledge?.name)} } /> ); const renderCreateButton = (variant: "header" | "toolbar" | "empty") => ( ); const renderList = () => ( <>
工作台 / 知识库

知识库

管理文档、分段和 QA,让 AI 对话可检索企业知识。

{renderCreateButton("header")}
} placeholder="搜索知识库" value={knowledgeKeyword} onChange={(event) => setKnowledgeKeyword(event.target.value)} className={styles.search} /> {renderCreateButton("toolbar")}
{filteredKnowledges.length} 个知识库
{loadingKnowledges ? (
知识库加载中...
) : knowledgeError ? (
) : filteredKnowledges.length === 0 ? (
{renderCreateButton("empty")}
) : (
{filteredKnowledges.map((knowledge) => { const knowledgeId = getKnowledgeId(knowledge); const owned = isKnowledgeOwner(knowledge); const creatorName = knowledge.creator?.nickName || knowledge.creator?.userName || knowledge.creatorName || "未知创建人"; const joinedComponent = joinedKnowledgeMap.get(knowledgeId); const joined = !!joinedComponent && isAiKnowledgeChatSelectable(joinedComponent); return (
openKnowledgeDetail(knowledge)} >
{renderKnowledgeIcon(knowledge)}

{knowledge.name}

{owned ? "我的知识库" : "只读"} {!owned && ( 创建人:{creatorName} )}

{knowledge.description || "暂无描述"}

容量 {formatBytes(Number(knowledge.fileSize || 0))} 更新{" "} {formatDateTime( knowledge.modified || knowledge.created, )}
{owned && (
)}
); })}
)}
); const renderDocuments = () => (
{ const value = event.target.value; setDocumentKeyword(value); if (!value) void loadDocuments(""); }} onSearch={(value) => loadDocuments(value)} className={styles.innerSearch} /> {canManageSelectedKnowledge && ( <> )}
), }} /> ); const renderSegments = () => (
), }} /> )} ); const renderQa = () => (
{ const value = event.target.value; setQaKeyword(value); if (!value) void loadQaList(""); }} onSearch={(value) => loadQaList(value)} className={styles.innerSearch} /> {canManageSelectedKnowledge && ( )} {canManageSelectedKnowledge && ( )} 共 {qaTotal} 条 QA
), }} /> ); const renderGraphPlaceholder = () => (

知识图谱后续接入

第一版先完成知识库核心管理。知识图谱和命中测试将在后续阶段迁移。

); const renderDetail = () => { const joined = selectedKnowledgeId !== null && !!joinedKnowledgeMap.get(selectedKnowledgeId) && isAiKnowledgeChatSelectable(joinedKnowledgeMap.get(selectedKnowledgeId)!); const joining = selectedKnowledgeId !== null && joiningIds.has(selectedKnowledgeId); const knowledgeForAction = knowledgeInfo || selectedKnowledgeComponent; const owned = isKnowledgeOwner(knowledgeForAction); const creator = (knowledgeForAction as KnowledgeComponent | null)?.creator; const creatorName = creator?.nickName || creator?.userName || knowledgeForAction?.creatorName || "未知创建人"; return ( <>
{renderKnowledgeIcon(knowledgeForAction)}
知识库 /{" "} {knowledgeForAction?.name || "知识库详情"}

{knowledgeForAction?.name || "知识库详情"}

{owned ? "我的知识库" : "只读知识库"} {!owned && 创建人:{creatorName}}

{knowledgeForAction?.description || "暂无描述"}

{owned && ( )}
{loadingDetail && !knowledgeInfo ? (
详情加载中...
) : ( <>
文档数 {documentTotal}
容量 {formatBytes(Number(knowledgeInfo?.fileSize || 0))}
状态 {knowledgeInfo?.pubStatus || "Published"}
更新时间 {formatDateTime( knowledgeInfo?.modified || knowledgeInfo?.created, )}
setActiveTab(key as DetailTabKey)} items={[ { key: "documents", label: "文档", children: renderDocuments(), }, { key: "segments", label: "分段", children: renderSegments(), }, { key: "qa", label: "QA", children: renderQa(), }, { key: "graph", label: "知识图谱", children: renderGraphPlaceholder(), }, ]} /> )}
); }; return (
{selectedKnowledgeId ? renderDetail() : renderList()} setKnowledgeModalOpen(false)} destroyOnClose width={620} >
{ if (value === "default") { knowledgeForm.setFieldValue("workflowId", undefined); return; } void (async () => { const spaceId = selectedSpaceId || (await ensureDefaultSpaceId(false)); await loadParsingWorkflows(spaceId || undefined); })(); }} /> {dataParsingMethod === "workflow" && (
setRenameDocModalOpen(false)} destroyOnClose >
setSegmentModalOpen(false)} destroyOnClose width={720} >
setQaModalOpen(false)} destroyOnClose width={720} >
); }