Phase 2.4 frontend: Knowledge admin page + API methods + routes + nav
This commit is contained in:
1 parent
855e1cb9c6
commit
8c2e5763ef
4 files changed
+654
-3
No files matched your search
@@ -34,7 +34,9 @@ import AdminMembership from '@/pages/admin/Membership';
|
|||||||
import AdminAds from '@/pages/admin/Ads';␍
|
import AdminAds from '@/pages/admin/Ads';␍
|
||||||
import AdminMarket from '@/pages/admin/Market';␍
|
import AdminMarket from '@/pages/admin/Market';␍
|
||||||
import AdminLogs from '@/pages/admin/Logs';␍
|
import AdminLogs from '@/pages/admin/Logs';␍
|
||||||
import AdminSkills from '@/pages/admin/Skills';
|
import AdminSkills from '@/pages/admin/Skills'
|
||||||
|
import AdminAgents from '@/pages/admin/Agents'
|
||||||
|
import AdminKnowledge from '@/pages/admin/Knowledge';;␍
|
||||||
␍
|
␍
|
||||||
const App: React.FC = () => {␍
|
const App: React.FC = () => {␍
|
||||||
const [user, setUser] = useState<User | null>(null);␍
|
const [user, setUser] = useState<User | null>(null);␍
|
||||||
@@ -112,7 +114,10 @@ const App: React.FC = () => {
|
|||||||
<Route path="market" element={<AdminMarket />} />␍
|
<Route path="market" element={<AdminMarket />} />␍
|
||||||
<Route path="logs" element={<AdminLogs />} />␍
|
<Route path="logs" element={<AdminLogs />} />␍
|
||||||
␍
|
␍
|
||||||
<Route path="skills" element={<AdminSkills />} /> </Route>
|
<Route path="skills" element={<AdminSkills />} />
|
||||||
|
<Route path="agents" element={<AdminAgents />} />
|
||||||
|
<Route path="knowledge" element={<AdminKnowledge />} />
|
||||||
|
</Route>␍
|
||||||
␍
|
␍
|
||||||
<Route path="*" element={<Navigate to="/" />} />␍
|
<Route path="*" element={<Navigate to="/" />} />␍
|
||||||
</Routes>␍
|
</Routes>␍
|
||||||
|
|||||||
@@ -455,6 +455,25 @@ export const adminAPI = {
|
|||||||
publishAdminSkill: (id: string, data: any) => post<any>(`/admin/skills/${id}/publish`, data),
|
publishAdminSkill: (id: string, data: any) => post<any>(`/admin/skills/${id}/publish`, data),
|
||||||
unpublishAdminSkill: (id: string) => post<any>(`/admin/skills/${id}/unpublish`),
|
unpublishAdminSkill: (id: string) => post<any>(`/admin/skills/${id}/unpublish`),
|
||||||
getAdminSkillsStats: () => get<any>('/admin/skills/stats/overview'),
|
getAdminSkillsStats: () => get<any>('/admin/skills/stats/overview'),
|
||||||
|
// Agent管理 (Phase 2.3)
|
||||||
|
getAdminAgents: (params?: any) => {
|
||||||
|
const sp = new URLSearchParams();
|
||||||
|
if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); });
|
||||||
|
return get<any>(`/admin/agents/${sp.toString() ? `?${sp.toString()}` : ''}`);
|
||||||
|
},
|
||||||
|
getAdminAgent: (id: string) => get<any>(`/admin/agents/${id}`),
|
||||||
|
createAdminAgent: (data: any) => post<any>('/admin/agents/', data),
|
||||||
|
updateAdminAgent: (id: string, data: any) => put<any>(`/admin/agents/${id}`, data),
|
||||||
|
deleteAdminAgent: (id: string) => del<any>(`/admin/agents/${id}`),
|
||||||
|
toggleAdminAgent: (id: string, isActive: boolean) => put<any>(`/admin/agents/${id}/toggle`, { is_active: isActive }),
|
||||||
|
getAdminAgentsStats: () => get<any>('/admin/agents/stats/overview'),
|
||||||
|
getAdminAgentReviews: (id: string) => get<any>(`/admin/agents/${id}/reviews`),
|
||||||
|
getAdminAvailableSkills: (params?: any) => {
|
||||||
|
const sp = new URLSearchParams();
|
||||||
|
if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); });
|
||||||
|
return get<any>(`/admin/agents/available/skills${sp.toString() ? `?${sp.toString()}` : ''}`);
|
||||||
|
},
|
||||||
|
getAdminAvailableModels: () => get<any>('/admin/agents/available/models'),
|
||||||
};
|
};
|
||||||
// ===== AI自进化API =====
|
// ===== AI自进化API =====
|
||||||
export const evolutionAPI = {
|
export const evolutionAPI = {
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ const adminNavItems = [
|
|||||||
{ path: '/admin/market', icon: 'market', label: '应用市场审核' },␍
|
{ path: '/admin/market', icon: 'market', label: '应用市场审核' },␍
|
||||||
{ path: '/admin/logs', icon: 'logs', label: '日志与风控' },␍
|
{ path: '/admin/logs', icon: 'logs', label: '日志与风控' },␍
|
||||||
␍
|
␍
|
||||||
{ path: '/admin/skills', icon: 'skills', label: '技能管理' },];
|
{ path: '/admin/agents', icon: 'agents', label: 'Agent管理' },
|
||||||
|
{ path: '/admin/knowledge', icon: 'knowledge', label: '知识库管理' },
|
||||||
|
{ path: '/admin/skills', icon: 'skills', label: '技能管理' },];␍
|
||||||
␍
|
␍
|
||||||
const adminIconPaths: Record<string, JSX.Element> = {␍
|
const adminIconPaths: Record<string, JSX.Element> = {␍
|
||||||
dashboard: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>,␍
|
dashboard: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>,␍
|
||||||
|
|||||||
@@ -0,0 +1,625 @@
|
|||||||
|
import React, { useState, useEffect } 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<string, { count: number; size: number }>;
|
||||||
|
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<string, number>;
|
||||||
|
file_type_distribution: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Knowledge: React.FC = () => {
|
||||||
|
// Data
|
||||||
|
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBaseItem[]>([]);
|
||||||
|
const [stats, setStats] = useState<Stats | null>(null);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize] = useState(15);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
|
const [filterStatus, setFilterStatus] = useState('');
|
||||||
|
const [filterOfficial, setFilterOfficial] = useState('');
|
||||||
|
|
||||||
|
// External Services
|
||||||
|
const [extServices, setExtServices] = useState<ExternalService[]>([]);
|
||||||
|
const [showExtModal, setShowExtModal] = useState(false);
|
||||||
|
const [editingExtService, setEditingExtService] = useState<ExternalService | null>(null);
|
||||||
|
const [extForm, setExtForm] = useState({
|
||||||
|
name: '',
|
||||||
|
service_type: 'fastgpt',
|
||||||
|
api_url: '',
|
||||||
|
api_key: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// KB Modal
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [editingKB, setEditingKB] = useState<KnowledgeBaseItem | null>(null);
|
||||||
|
const [kbForm, setKBForm] = useState({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
is_official: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detail drawer
|
||||||
|
const [showDetail, setShowDetail] = useState(false);
|
||||||
|
const [selectedKB, setSelectedKB] = useState<KnowledgeBaseItem | null>(null);
|
||||||
|
const [detailDocs, setDetailDocs] = useState<any[]>([]);
|
||||||
|
const [detailDocTotal, setDetailDocTotal] = useState(0);
|
||||||
|
|
||||||
|
// Loading
|
||||||
|
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.data?.code === 0) setStats(res.data.data);
|
||||||
|
} catch (e) { console.error(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.data?.code === 0) {
|
||||||
|
setKnowledgeBases(res.data.data.items || []);
|
||||||
|
setTotal(res.data.data.total || 0);
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchExtServices = async () => {
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.getAdminKnowledgeExtServices();
|
||||||
|
if (res.data?.code === 0) setExtServices(res.data.data.items || []);
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateKB = async () => {
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.createAdminKnowledge(kbForm);
|
||||||
|
if (res.data?.code === 0) {
|
||||||
|
setShowModal(false);
|
||||||
|
setKBForm({ name: '', description: '', is_official: false });
|
||||||
|
fetchKBs();
|
||||||
|
fetchStats();
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateKB = async () => {
|
||||||
|
if (!editingKB) return;
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.updateAdminKnowledge(editingKB.id, kbForm);
|
||||||
|
if (res.data?.code === 0) {
|
||||||
|
setShowModal(false);
|
||||||
|
fetchKBs();
|
||||||
|
fetchStats();
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteKB = async (id: string) => {
|
||||||
|
if (!confirm('确认删除该知识库及其所有文档?此操作不可恢复!')) return;
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.deleteAdminKnowledge(id);
|
||||||
|
if (res.data?.code === 0) {
|
||||||
|
fetchKBs();
|
||||||
|
fetchStats();
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleKB = async (id: string, isActive: boolean) => {
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.toggleAdminKnowledge(id, isActive);
|
||||||
|
if (res.data?.code === 0) {
|
||||||
|
fetchKBs();
|
||||||
|
fetchStats();
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDetail = async (kb: KnowledgeBaseItem) => {
|
||||||
|
setSelectedKB(kb);
|
||||||
|
setShowDetail(true);
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.getAdminKnowledgeDetail(kb.id);
|
||||||
|
if (res.data?.code === 0) {
|
||||||
|
setSelectedKB(res.data.data);
|
||||||
|
}
|
||||||
|
const docRes = await adminAPI.getAdminKnowledgeDocs(kb.id, { page: 1, page_size: 20 });
|
||||||
|
if (docRes.data?.code === 0) {
|
||||||
|
setDetailDocs(docRes.data.data.items || []);
|
||||||
|
setDetailDocTotal(docRes.data.data.total || 0);
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateExtService = async () => {
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.createAdminKnowledgeExtService(extForm);
|
||||||
|
if (res.data?.code === 0) {
|
||||||
|
setShowExtModal(false);
|
||||||
|
setExtForm({ name: '', service_type: 'fastgpt', api_url: '', api_key: '' });
|
||||||
|
fetchExtServices();
|
||||||
|
fetchStats();
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteExtService = async (id: string) => {
|
||||||
|
if (!confirm('确认删除该外部知识库服务配置?')) return;
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.deleteAdminKnowledgeExtService(id);
|
||||||
|
if (res.data?.code === 0) fetchExtServices();
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckExtStatus = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.checkAdminKnowledgeExtStatus(id);
|
||||||
|
if (res.data?.code === 0) {
|
||||||
|
alert(`连接状态: ${res.data.data.connected ? '✅ 已连接' : '❌ 未连接'}\n详情: ${res.data.data.detail || '无'}`);
|
||||||
|
fetchExtServices();
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteDoc = async (kbId: string, docId: string) => {
|
||||||
|
if (!confirm('确认删除该文档?')) return;
|
||||||
|
try {
|
||||||
|
const res = await adminAPI.deleteAdminKnowledgeDoc(kbId, docId);
|
||||||
|
if (res.data?.code === 0) {
|
||||||
|
handleViewDetail(selectedKB!);
|
||||||
|
fetchStats();
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (kb: KnowledgeBaseItem) => {
|
||||||
|
setEditingKB(kb);
|
||||||
|
setKBForm({
|
||||||
|
name: kb.name,
|
||||||
|
description: kb.description || '',
|
||||||
|
is_official: kb.is_official,
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
setEditingKB(null);
|
||||||
|
setKBForm({ name: '', description: '', is_official: false });
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
|
|
||||||
|
// Status badge
|
||||||
|
const statusBadge = (status: string) => {
|
||||||
|
const map: Record<string, { color: string; text: string }> = {
|
||||||
|
active: { color: '#28a745', text: '活跃' },
|
||||||
|
inactive: { color: '#6c757d', text: '禁用' },
|
||||||
|
archived: { color: '#ffc107', text: '归档' },
|
||||||
|
published: { color: '#28a745', text: '已发布' },
|
||||||
|
pending: { color: '#ffc107', text: '待审核' },
|
||||||
|
};
|
||||||
|
const s = map[status] || { color: '#6c757d', text: status };
|
||||||
|
return <span style={{ color: s.color, fontWeight: 600 }}>{s.text}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncStatusBadge = (status: string) => {
|
||||||
|
const map: Record<string, { color: string; text: string }> = {
|
||||||
|
none: { color: '#adb5bd', text: '未同步' },
|
||||||
|
syncing: { color: '#17a2b8', text: '同步中' },
|
||||||
|
synced: { color: '#28a745', text: '已同步' },
|
||||||
|
error: { color: '#dc3545', text: '同步错误' },
|
||||||
|
};
|
||||||
|
const s = map[status] || { color: '#adb5bd', text: status };
|
||||||
|
return <span style={{ color: s.color, fontSize: '0.85em' }}>{s.text}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extTypeLabel = (type: string) => {
|
||||||
|
const map: Record<string, string> = { fastgpt: 'FastGPT', dify: 'Dify', ragflow: 'RAGFlow' };
|
||||||
|
return map[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px', maxWidth: '1400px' }}>
|
||||||
|
<h2 style={{ marginBottom: '24px', fontSize: '1.5em' }}>📚 知识库管理</h2>
|
||||||
|
|
||||||
|
{/* Stats Dashboard */}
|
||||||
|
{stats && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '12px', marginBottom: '24px' }}>
|
||||||
|
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '2em', fontWeight: 700, color: '#1890ff' }}>{stats.total_knowledge_bases}</div>
|
||||||
|
<div style={{ fontSize: '0.85em', color: '#666' }}>知识库总数</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '2em', fontWeight: 700, color: '#52c41a' }}>{stats.total_documents}</div>
|
||||||
|
<div style={{ fontSize: '0.85em', color: '#666' }}>文档总数</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '1.3em', fontWeight: 700, color: '#faad14' }}>{stats.total_storage_display}</div>
|
||||||
|
<div style={{ fontSize: '0.85em', color: '#666' }}>存储占用</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '2em', fontWeight: 700, color: '#722ed1' }}>{stats.official_kb}</div>
|
||||||
|
<div style={{ fontSize: '0.85em', color: '#666' }}>官方知识库</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '2em', fontWeight: 700, color: '#13c2c2' }}>{stats.kb_with_graph}</div>
|
||||||
|
<div style={{ fontSize: '0.85em', color: '#666' }}>含图谱KB</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', padding: '16px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '2em', fontWeight: 700, color: '#eb2f96' }}>{stats.external_kb}</div>
|
||||||
|
<div style={{ fontSize: '0.85em', color: '#666' }}>外部集成</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索知识库名称/描述..."
|
||||||
|
value={keyword}
|
||||||
|
onChange={e => setKeyword(e.target.value)}
|
||||||
|
style={{ padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: '4px', width: '200px' }}
|
||||||
|
/>
|
||||||
|
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)}
|
||||||
|
style={{ padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
|
||||||
|
<option value="">全部状态</option>
|
||||||
|
<option value="active">活跃</option>
|
||||||
|
<option value="inactive">禁用</option>
|
||||||
|
<option value="archived">归档</option>
|
||||||
|
</select>
|
||||||
|
<select value={filterOfficial} onChange={e => setFilterOfficial(e.target.value)}
|
||||||
|
style={{ padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
|
||||||
|
<option value="">全部来源</option>
|
||||||
|
<option value="true">官方</option>
|
||||||
|
<option value="false">用户</option>
|
||||||
|
</select>
|
||||||
|
<button onClick={openCreateModal}
|
||||||
|
style={{ padding: '8px 16px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
|
||||||
|
+ 创建知识库
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShowExtModal(true)}
|
||||||
|
style={{ padding: '8px 16px', background: '#722ed1', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
|
||||||
|
🔗 外部服务配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Knowledge Base Table */}
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', background: '#fff', borderRadius: '8px' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: '#f5f5f5', borderBottom: '2px solid #e0e0e0' }}>
|
||||||
|
<th style={{ padding: '12px', textAlign: 'left' }}>名称</th>
|
||||||
|
<th style={{ padding: '12px', textAlign: 'left' }}>来源</th>
|
||||||
|
<th style={{ padding: '12px', textAlign: 'left' }}>文档数</th>
|
||||||
|
<th style={{ padding: '12px', textAlign: 'left' }}>存储</th>
|
||||||
|
<th style={{ padding: '12px', textAlign: 'left' }}>状态</th>
|
||||||
|
<th style={{ padding: '12px', textAlign: 'left' }}>同步</th>
|
||||||
|
<th style={{ padding: '12px', textAlign: 'left' }}>创建时间</th>
|
||||||
|
<th style={{ padding: '12px', textAlign: 'center' }}>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{loading ? (
|
||||||
|
<tr><td colSpan={8} style={{ textAlign: 'center', padding: '24px' }}>加载中...</td></tr>
|
||||||
|
) : knowledgeBases.length === 0 ? (
|
||||||
|
<tr><td colSpan={8} style={{ textAlign: 'center', padding: '24px', color: '#999' }}>暂无知识库数据</td></tr>
|
||||||
|
) : knowledgeBases.map(kb => (
|
||||||
|
<tr key={kb.id} style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||||
|
<td style={{ padding: '12px' }}>
|
||||||
|
<div style={{ fontWeight: 600 }}>{kb.name}</div>
|
||||||
|
<div style={{ fontSize: '0.8em', color: '#999' }}>{kb.description?.substring(0, 50)}</div>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px' }}>
|
||||||
|
{kb.is_official ? (
|
||||||
|
<span style={{ color: '#722ed1', fontWeight: 600 }}>⭐ 官方</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: '#666' }}>👤 {kb.owner_name || '用户'}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '12px' }}>{kb.document_count}</td>
|
||||||
|
<td style={{ padding: '12px' }}>{kb.total_storage_mb || 0} MB</td>
|
||||||
|
<td style={{ padding: '12px' }}>{statusBadge(kb.status)}</td>
|
||||||
|
<td style={{ padding: '12px' }}>{syncStatusBadge(kb.external_sync_status)}</td>
|
||||||
|
<td style={{ padding: '12px', fontSize: '0.85em' }}>{kb.created_at?.substring(0, 10)}</td>
|
||||||
|
<td style={{ padding: '12px', textAlign: 'center' }}>
|
||||||
|
<button onClick={() => handleViewDetail(kb)}
|
||||||
|
style={{ padding: '4px 8px', margin: '2px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
|
||||||
|
详情
|
||||||
|
</button>
|
||||||
|
<button onClick={() => openEditModal(kb)}
|
||||||
|
style={{ padding: '4px 8px', margin: '2px', background: '#faad14', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleToggleKB(kb.id, kb.status !== 'active')}
|
||||||
|
style={{ padding: '4px 8px', margin: '2px', background: kb.status === 'active' ? '#ff4d4f' : '#52c41a', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
|
||||||
|
{kb.status === 'active' ? '禁用' : '启用'}
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleDeleteKB(kb.id)}
|
||||||
|
style={{ padding: '4px 8px', margin: '2px', background: '#ff4d4f', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '16px' }}>
|
||||||
|
<button disabled={page <= 1} onClick={() => setPage(page - 1)}
|
||||||
|
style={{ padding: '6px 12px', border: '1px solid #d9d9d9', borderRadius: '4px', cursor: page <= 1 ? 'not-allowed' : 'pointer' }}>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<span style={{ padding: '6px 12px' }}>第 {page} / {totalPages} 页 (共 {total} 条)</span>
|
||||||
|
<button disabled={page >= totalPages} onClick={() => setPage(page + 1)}
|
||||||
|
style={{ padding: '6px 12px', border: '1px solid #d9d9d9', borderRadius: '4px', cursor: page >= totalPages ? 'not-allowed' : 'pointer' }}>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* KB Create/Edit Modal */}
|
||||||
|
{showModal && (
|
||||||
|
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', background: 'rgba(0,0,0,0.5)', zIndex: 1000, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<div style={{ background: '#fff', borderRadius: '8px', padding: '24px', width: '480px', maxWidth: '90vw' }}>
|
||||||
|
<h3>{editingKB ? '编辑知识库' : '创建知识库'}</h3>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>名称</label>
|
||||||
|
<input value={kbForm.name} onChange={e => setKBForm({ ...kbForm, name: e.target.value })}
|
||||||
|
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>描述</label>
|
||||||
|
<textarea value={kbForm.description} onChange={e => setKBForm({ ...kbForm, description: e.target.value })}
|
||||||
|
rows={3} style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" checked={kbForm.is_official} onChange={e => setKBForm({ ...kbForm, is_official: e.target.checked })} />
|
||||||
|
<span style={{ marginLeft: '8px' }}>官方平台知识库</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '12px' }}>
|
||||||
|
<button onClick={editingKB ? handleUpdateKB : handleCreateKB}
|
||||||
|
style={{ padding: '8px 24px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
|
||||||
|
{editingKB ? '保存' : '创建'}
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShowModal(false)}
|
||||||
|
style={{ padding: '8px 24px', background: '#f5f5f5', border: '1px solid #d9d9d9', borderRadius: '4px', cursor: 'pointer' }}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Detail Drawer */}
|
||||||
|
{showDetail && selectedKB && (
|
||||||
|
<div style={{ position: 'fixed', top: 0, right: 0, width: '480px', height: '100%', background: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.1)', zIndex: 1000, padding: '24px', overflowY: 'auto' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
|
||||||
|
<h3>{selectedKB.name}</h3>
|
||||||
|
<button onClick={() => setShowDetail(false)} style={{ background: 'none', border: 'none', fontSize: '1.5em', cursor: 'pointer' }}>✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '16px', padding: '12px', background: '#f5f5f5', borderRadius: '8px' }}>
|
||||||
|
<div>来源: {selectedKB.is_official ? '⭐ 官方' : `👤 ${selectedKB.owner_name || '用户'}`}</div>
|
||||||
|
<div>描述: {selectedKB.description || '无'}</div>
|
||||||
|
<div>状态: {statusBadge(selectedKB.status)}</div>
|
||||||
|
<div>文档数: {selectedKB.document_count}</div>
|
||||||
|
<div>存储: {selectedKB.total_storage_mb || 0} MB</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Graph Stats */}
|
||||||
|
{selectedKB.graph_stats && (
|
||||||
|
<div style={{ marginBottom: '16px', padding: '12px', background: '#e6f7ff', borderRadius: '8px' }}>
|
||||||
|
<h4 style={{ margin: '0 0 8px 0' }}>知识图谱</h4>
|
||||||
|
<div>节点数: {selectedKB.graph_stats.nodes}</div>
|
||||||
|
<div>边数: {selectedKB.graph_stats.edges}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* External Integration */}
|
||||||
|
{selectedKB.external_service_id && (
|
||||||
|
<div style={{ marginBottom: '16px', padding: '12px', background: '#f9f0ff', borderRadius: '8px' }}>
|
||||||
|
<h4 style={{ margin: '0 0 8px 0' }}>外部集成</h4>
|
||||||
|
<div>同步状态: {syncStatusBadge(selectedKB.external_sync_status)}</div>
|
||||||
|
<div>外部ID: {selectedKB.external_id || '未关联'}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Documents */}
|
||||||
|
<h4 style={{ margin: '16px 0 8px 0' }}>文档列表 ({detailDocTotal})</h4>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: '#f5f5f5' }}>
|
||||||
|
<th style={{ padding: '8px', textAlign: 'left' }}>文件名</th>
|
||||||
|
<th style={{ padding: '8px', textAlign: 'left' }}>类型</th>
|
||||||
|
<th style={{ padding: '8px', textAlign: 'left' }}>大小</th>
|
||||||
|
<th style={{ padding: '8px', textAlign: 'left' }}>状态</th>
|
||||||
|
<th style={{ padding: '8px', textAlign: 'center' }}>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{detailDocs.map(doc => (
|
||||||
|
<tr key={doc.id} style={{ borderBottom: '1px solid #f0f0f0' }}>
|
||||||
|
<td style={{ padding: '8px' }}>{doc.filename}</td>
|
||||||
|
<td style={{ padding: '8px' }}>{doc.file_type}</td>
|
||||||
|
<td style={{ padding: '8px' }}>{doc.file_size > 1024 * 1024 ? `${(doc.file_size / 1024 / 1024).toFixed(1)} MB` : `${(doc.file_size / 1024).toFixed(1)} KB`}</td>
|
||||||
|
<td style={{ padding: '8px' }}>{statusBadge(doc.status)}</td>
|
||||||
|
<td style={{ padding: '8px', textAlign: 'center' }}>
|
||||||
|
<button onClick={() => handleDeleteDoc(selectedKB.id, doc.id)}
|
||||||
|
style={{ padding: '4px 8px', background: '#ff4d4f', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{detailDocs.length === 0 && (
|
||||||
|
<tr><td colSpan={5} style={{ textAlign: 'center', padding: '12px', color: '#999' }}>暂无文档</td></tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* External Service Modal */}
|
||||||
|
{showExtModal && (
|
||||||
|
<div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', background: 'rgba(0,0,0,0.5)', zIndex: 1000, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<div style={{ background: '#fff', borderRadius: '8px', padding: '24px', width: '600px', maxWidth: '90vw' }}>
|
||||||
|
<h3>🔗 外部知识库服务配置</h3>
|
||||||
|
|
||||||
|
{/* Existing Services */}
|
||||||
|
{extServices.length > 0 && (
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<h4>已配置的服务</h4>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: '#f5f5f5' }}>
|
||||||
|
<th style={{ padding: '8px' }}>名称</th>
|
||||||
|
<th style={{ padding: '8px' }}>类型</th>
|
||||||
|
<th style={{ padding: '8px' }}>URL</th>
|
||||||
|
<th style={{ padding: '8px' }}>状态</th>
|
||||||
|
<th style={{ padding: '8px' }}>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{extServices.map(svc => (
|
||||||
|
<tr key={svc.id}>
|
||||||
|
<td style={{ padding: '8px' }}>{svc.name}</td>
|
||||||
|
<td style={{ padding: '8px' }}>{extTypeLabel(svc.service_type)}</td>
|
||||||
|
<td style={{ padding: '8px', fontSize: '0.8em' }}>{svc.api_url}</td>
|
||||||
|
<td style={{ padding: '8px' }}>{statusBadge(svc.status)}</td>
|
||||||
|
<td style={{ padding: '8px' }}>
|
||||||
|
<button onClick={() => handleCheckExtStatus(svc.id)}
|
||||||
|
style={{ padding: '4px 8px', background: '#1890ff', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
|
||||||
|
检测
|
||||||
|
</button>
|
||||||
|
<button onClick={() => handleDeleteExtService(svc.id)}
|
||||||
|
style={{ padding: '4px 8px', background: '#ff4d4f', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontSize: '0.85em' }}>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* New Service Form */}
|
||||||
|
<div style={{ borderTop: '1px solid #e0e0e0', paddingTop: '16px' }}>
|
||||||
|
<h4>添加新服务</h4>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>服务名称</label>
|
||||||
|
<input value={extForm.name} onChange={e => setExtForm({ ...extForm, name: e.target.value })}
|
||||||
|
placeholder="如: 生产环境 FastGPT"
|
||||||
|
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>服务类型</label>
|
||||||
|
<select value={extForm.service_type} onChange={e => setExtForm({ ...extForm, service_type: e.target.value })}
|
||||||
|
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
|
||||||
|
<option value="fastgpt">FastGPT - 开源 AI 知识库平台</option>
|
||||||
|
<option value="dify">Dify - LLM 应用开发平台</option>
|
||||||
|
<option value="ragflow">RAGFlow - 基于深度文档理解的 RAG 引擎</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>API URL</label>
|
||||||
|
<input value={extForm.api_url} onChange={e => setExtForm({ ...extForm, api_url: e.target.value })}
|
||||||
|
placeholder="https://your-fastgpt-instance.com/api/v1"
|
||||||
|
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '16px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 600 }}>API Key</label>
|
||||||
|
<input type="password" value={extForm.api_key} onChange={e => setExtForm({ ...extForm, api_key: e.target.value })}
|
||||||
|
placeholder="Bearer Token 或 API Key"
|
||||||
|
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '12px' }}>
|
||||||
|
<button onClick={handleCreateExtService}
|
||||||
|
style={{ padding: '8px 24px', background: '#722ed1', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
|
||||||
|
添加服务
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShowExtModal(false)}
|
||||||
|
style={{ padding: '8px 24px', background: '#f5f5f5', border: '1px solid #d9d9d9', borderRadius: '4px', cursor: 'pointer' }}>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Knowledge;
|
||||||
Reference in new issue
Block a user