import React, { useState, useEffect, useRef } from 'react'; import { adminAPI } from '@/api'; interface KnowledgeBaseItem { id: string; name: string; user_id: string; owner_name?: string; description?: string; directory_path: string; document_count: number; knowledge_graph_count: number; is_official: boolean; status: string; external_service_id?: string; external_id?: string; external_sync_status: string; external_config?: any; total_storage_bytes?: number; total_storage_mb?: number; created_at: string; updated_at: string; doc_stats?: Record; graph_stats?: { nodes: number; edges: number }; } interface ExternalService { id: string; name: string; service_type: string; api_url: string; api_key?: string; config?: any; is_enabled: boolean; status: string; created_at?: string; updated_at?: string; } interface Stats { total_knowledge_bases: number; total_documents: number; total_storage_bytes: number; total_storage_display: string; official_kb: number; user_kb: number; active_kb: number; kb_with_graph: number; external_kb: number; recent_kb_7d: number; total_graph_nodes: number; total_graph_edges: number; doc_status_distribution: Record; file_type_distribution: Record; } const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => ( {children} ); const formatSize = (bytes: number) => { if (bytes >= 1024 * 1024 * 1024) return (bytes / 1024 / 1024 / 1024).toFixed(1) + ' GB'; if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB'; if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'; return bytes + ' B'; }; const Knowledge: React.FC = () => { const [knowledgeBases, setKnowledgeBases] = useState([]); const [stats, setStats] = useState(null); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize] = useState(15); const [keyword, setKeyword] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [filterOfficial, setFilterOfficial] = useState(''); // External Services const [extServices, setExtServices] = useState([]); const [showExtModal, setShowExtModal] = useState(false); const [extForm, setExtForm] = useState({ name: 'Dify 知识库', service_type: 'dify', api_url: 'https://dify.qihuyun.com/v1', api_key: '', dataset_name: 'qihuai' }); const [extMsg, setExtMsg] = useState(''); const [ingesting, setIngesting] = useState(false); const [ingestResult, setIngestResult] = useState(null); // AnySearch global config const [anysearchForm, setAnysearchForm] = useState<{ api_key: string; is_enabled: boolean }>({ api_key: '', is_enabled: false }); const [anysearchLoading, setAnysearchLoading] = useState(false); const [anysearchMsg, setAnysearchMsg] = useState(''); // KB Modal const [showModal, setShowModal] = useState(false); const [editingKB, setEditingKB] = useState(null); const [kbForm, setKBForm] = useState({ name: '', description: '', is_official: false }); // Detail drawer const [showDetail, setShowDetail] = useState(false); const [selectedKB, setSelectedKB] = useState(null); const [detailDocs, setDetailDocs] = useState([]); const [detailDocTotal, setDetailDocTotal] = useState(0); // Document preview const [previewDoc, setPreviewDoc] = useState(null); const [previewContent, setPreviewContent] = useState(''); // File upload const [uploading, setUploading] = useState(false); const [uploadMsg, setUploadMsg] = useState(''); const fileInputRef = useRef(null); const [loading, setLoading] = useState(false); useEffect(() => { fetchStats(); fetchKBs(); fetchExtServices(); }, []); useEffect(() => { fetchKBs(); }, [page, keyword, filterStatus, filterOfficial]); const fetchStats = async () => { try { const res = await adminAPI.getAdminKnowledgeStats(); if (res.code === 0) setStats(res.data); } catch (e) {} }; const fetchKBs = async () => { setLoading(true); try { const params: any = { page, page_size: pageSize }; if (keyword) params.keyword = keyword; if (filterStatus) params.status = filterStatus; if (filterOfficial) params.is_official = filterOfficial === 'true'; const res = await adminAPI.getAdminKnowledge(params); if (res.code === 0) { setKnowledgeBases(res.data.items || []); setTotal(res.data.total || 0); } } catch (e) {} setLoading(false); }; const fetchExtServices = async () => { try { const res = await adminAPI.getAdminKnowledgeExtServices(); if (res.code === 0) setExtServices(res.data.items || []); } catch (e) {} }; const fetchAnySearchConfig = async () => { try { const res = await adminAPI.getAdminAnySearchConfig(); if (res.code === 0) { const d = res.data || {}; setAnysearchForm({ api_key: d.api_key || '', is_enabled: !!d.is_enabled }); } } catch (e) {} }; const handleSaveAnySearch = async () => { setAnysearchLoading(true); setAnysearchMsg(''); try { const res = await adminAPI.saveAdminAnySearchConfig(anysearchForm); if (res.code === 0) { const conn = res.data?.connection || {}; setAnysearchMsg(conn.connected ? '✅ 已保存并连接成功' : `⚠️ 已保存但连接失败:${conn.detail || '未知错误'}`); } else { setAnysearchMsg(`❌ 保存失败:${res.message || '未知错误'}`); } } catch (e: any) { setAnysearchMsg(`❌ 保存失败:${e?.message || '网络错误'}`); } finally { setAnysearchLoading(false); } }; const handleCreateKB = async () => { try { const res = await adminAPI.createAdminKnowledge(kbForm); if (res.code === 0) { setShowModal(false); setKBForm({ name: '', description: '', is_official: false }); fetchKBs(); fetchStats(); } } catch (e) {} }; const handleUpdateKB = async () => { if (!editingKB) return; try { const res = await adminAPI.updateAdminKnowledge(editingKB.id, kbForm); if (res.code === 0) { setShowModal(false); fetchKBs(); fetchStats(); } } catch (e) {} }; const handleDeleteKB = async (id: string) => { if (!confirm('确认删除该知识库及其所有文档?此操作不可恢复!')) return; try { const res = await adminAPI.deleteAdminKnowledge(id); if (res.code === 0) { fetchKBs(); fetchStats(); } } catch (e) {} }; const handleToggleKB = async (id: string, isActive: boolean) => { try { const res = await adminAPI.toggleAdminKnowledge(id, isActive); if (res.code === 0) { fetchKBs(); fetchStats(); } } catch (e) {} }; const handleViewDetail = async (kb: KnowledgeBaseItem) => { setSelectedKB(kb); setShowDetail(true); try { const res = await adminAPI.getAdminKnowledgeDetail(kb.id); if (res.code === 0) setSelectedKB(res.data); const docRes = await adminAPI.getAdminKnowledgeDocs(kb.id, { page: 1, page_size: 50 }); if (docRes.code === 0) { setDetailDocs(docRes.data.items || []); setDetailDocTotal(docRes.data.total || 0); } } catch (e) {} }; // Document upload const handleFileUpload = async (kbId: string, file: File) => { setUploading(true); setUploadMsg(`正在上传 ${file.name}...`); try { const formData = new FormData(); formData.append('file', file); // Use raw fetch since adminAPI may not support FormData const token = localStorage.getItem('aiflow_token'); const res = await fetch(`/app/api/admin/knowledge/${kbId}/documents/upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); const data = await res.json(); if (data.code === 0) { setUploadMsg(`✅ ${file.name} 上传成功!`); handleViewDetail(selectedKB!); fetchStats(); } else { setUploadMsg(`❌ 上传失败: ${data.message || '未知错误'}`); } } catch (e: any) { setUploadMsg(`❌ 上传失败: ${e.message || '网络错误'}`); } finally { setUploading(false); setTimeout(() => setUploadMsg(''), 5000); } }; // Document preview const handlePreviewDoc = async (kbId: string, doc: any) => { setPreviewDoc(doc); setPreviewContent('加载中...'); try { const token = localStorage.getItem('aiflow_token'); const res = await fetch(`/app/api/admin/knowledge/${kbId}/documents/${doc.id}/content`, { headers: { 'Authorization': `Bearer ${token}` }, }); const data = await res.json(); if (data.code === 0) { setPreviewContent(data.data?.content || data.data?.text || '(文档内容为空)'); } else { setPreviewContent(data.message || '无法加载文档内容'); } } catch (e: any) { setPreviewContent(`加载失败: ${e.message}`); } }; const handleDeleteDoc = async (kbId: string, docId: string) => { if (!confirm('确认删除该文档?')) return; try { const res = await adminAPI.deleteAdminKnowledgeDoc(kbId, docId); if (res.code === 0) { handleViewDetail(selectedKB!); fetchStats(); } } catch (e) {} }; const handleCreateExtService = async () => { try { const payload = { ...extForm, config: { dataset_name: extForm.dataset_name || 'qihuai' }, }; const res = await adminAPI.createAdminKnowledgeExtService(payload); if (res.code === 0) { setShowExtModal(false); setExtForm({ name: 'Dify 知识库', service_type: 'dify', api_url: 'https://dify.qihuyun.com/v1', api_key: '', dataset_name: 'qihuai' }); setExtMsg(''); fetchExtServices(); } } catch (e: any) { setExtMsg(`添加失败: ${e?.message || '未知错误'}`); } }; const handleDeleteExtService = async (id: string) => { if (!confirm('确认删除该外部知识库服务配置?')) return; try { const res = await adminAPI.deleteAdminKnowledgeExtService(id); if (res.code === 0) fetchExtServices(); } catch (e) {} }; const handleCheckExtStatus = async (id: string) => { try { const res = await adminAPI.checkAdminKnowledgeExtStatus(id); if (res.code === 0) { const d = res.data || {}; const statusText = d.connected ? (d.usable ? '可通讯且可用 ✅' : '可通讯但不可用 ⚠️') : '不可通讯 ❌'; alert(`连接状态: ${statusText}\n\n详情: ${d.detail || '无'}`); fetchExtServices(); } } catch (e) {} }; const handleIngestPlatform = async () => { if (!confirm('将把平台「AI聊天回答 + 用户上传文档」中有价值的内容批量入库到 Dify 知识库(量化后可被 AI 聊天检索复用)。确认执行?')) return; setIngesting(true); setIngestResult(null); try { const token = localStorage.getItem('aiflow_token'); const res = await fetch(`/app/api/admin/knowledge/external/dify/ingest`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, }); const data = await res.json(); if (data.code === 0) { setIngestResult(data.data); setExtMsg(`✅ ${data.message}`); } else { setExtMsg(`❌ ${data.message || '入库失败'}`); } } catch (e: any) { setExtMsg(`❌ 入库失败: ${e?.message || '网络错误'}`); } finally { setIngesting(false); } }; const statusBadge = (status: string) => { const map: Record = { active: { color: '#16A34A', text: '活跃' }, inactive: { color: '#6B7280', text: '禁用' }, archived: { color: '#F59E0B', text: '归档' }, published: { color: '#16A34A', text: '已发布' }, pending: { color: '#F59E0B', text: '待审核' }, completed: { color: '#16A34A', text: '完成' }, processing: { color: '#3B82F6', text: '处理中' }, failed: { color: '#DC2626', text: '失败' }, uploading: { color: '#3B82F6', text: '上传中' }, }; const s = map[status] || { color: '#6B7280', text: status }; return {s.text}; }; const syncStatusBadge = (status: string) => { const map: Record = { none: { color: '#9CA3AF', text: '未同步' }, syncing: { color: '#3B82F6', text: '同步中' }, synced: { color: '#16A34A', text: '已同步' }, error: { color: '#DC2626', text: '同步错误' }, }; const s = map[status] || { color: '#9CA3AF', text: status }; return {s.text}; }; const totalPages = Math.ceil(total / pageSize); const statCardDefs = stats ? [ { label: '知识库总数', value: stats.total_knowledge_bases, color: '#6C63FF' }, { label: '文档总数', value: stats.total_documents, color: '#219B5E' }, { label: '存储占用', value: stats.total_storage_display, color: '#E8922C' }, { label: '官方知识库', value: stats.official_kb, color: '#8B5CF6' }, { label: '含图谱KB', value: stats.kb_with_graph, color: '#3B82F6' }, { label: '外部集成', value: stats.external_kb, color: '#EC4899' }, ] : []; return (

知识库管理

{/* Stats */} {stats && (
{statCardDefs.map((s, i) => (
{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}
{s.label}
))}
)} {/* Toolbar */}
setKeyword(e.target.value)} style={inputStyle} />
{/* Table */}
{loading ? ( ) : knowledgeBases.length === 0 ? ( ) : knowledgeBases.map(kb => ( ))}
名称来源文档数存储状态同步创建时间操作
加载中...
暂无知识库数据
{kb.name}
{kb.description &&
{kb.description.substring(0, 60)}
}
{kb.is_official ? 官方 : {kb.owner_name || '用户'}} {kb.document_count} {kb.total_storage_mb || 0} MB {statusBadge(kb.status)} {syncStatusBadge(kb.external_sync_status)} {kb.created_at?.substring(0, 10)}
{/* Pagination */} {totalPages > 1 && (
第 {page}/{totalPages} 页 (共 {total} 条)
)} {/* KB Modal */} {showModal && (
setShowModal(false)}>
e.stopPropagation()}>

{editingKB ? '编辑知识库' : '创建知识库'}

setKBForm({ ...kbForm, name: e.target.value })} style={fullInput} />