From 8c2e5763efd74957a89ae33859a32ce59bfc9d57 Mon Sep 17 00:00:00 2001 From: qyyfw Date: Thu, 2 Jul 2026 23:03:53 +0800 Subject: [PATCH] Phase 2.4 frontend: Knowledge admin page + API methods + routes + nav --- frontend/src/App.tsx | 251 +++---- frontend/src/api/index.ts | 19 + .../src/components/layout/AdminLayout.tsx | 254 +++---- frontend/src/pages/admin/Knowledge.tsx | 625 ++++++++++++++++++ 4 files changed, 900 insertions(+), 249 deletions(-) create mode 100644 frontend/src/pages/admin/Knowledge.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 32905ad..2749a65 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,123 +1,128 @@ -import React, { useState, useEffect } from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; -import type { User } from '@/types'; -import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; -import { authAPI } from '@/api'; - -// Layouts -import FrontLayout from '@/components/layout/FrontLayout'; -import AdminLayout from '@/components/layout/AdminLayout'; - -// Front Pages -import LandingPage from '@/pages/front/LandingPage'; -import LoginPage from '@/pages/front/LoginPage'; -import ChatPage from '@/pages/front/ChatPage'; -import ChannelPage from '@/pages/front/ChannelPage'; -import KnowledgePage from '@/pages/front/KnowledgePage'; -import MarketPage from '@/pages/front/MarketPage'; -import TokenDashboardPage from '@/pages/front/TokenDashboardPage'; -import PricingPage from '@/pages/front/PricingPage'; -import ProfilePage from '@/pages/front/ProfilePage'; -import NotificationPage from '@/pages/front/NotificationPage'; -import AssetsPage from '@/pages/front/AssetsPage'; -import NovelPreviewPage from '@/pages/front/NovelPreviewPage'; -import EvolutionDashboard from '@/pages/front/EvolutionDashboard'; - -// Admin Pages -import AdminDashboard from '@/pages/admin/Dashboard'; -import AdminUsers from '@/pages/admin/Users'; -import AdminModels from '@/pages/admin/Models'; -import AdminSiteSettings from '@/pages/admin/SiteSettings'; -import AdminPayment from '@/pages/admin/Payment'; -import AdminServices from '@/pages/admin/Services'; -import AdminMembership from '@/pages/admin/Membership'; -import AdminAds from '@/pages/admin/Ads'; -import AdminMarket from '@/pages/admin/Market'; -import AdminLogs from '@/pages/admin/Logs'; -import AdminSkills from '@/pages/admin/Skills'; - -const App: React.FC = () => { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - // 初始化:从localStorage恢复登录状态 - useEffect(() => { - const token = localStorage.getItem('aiflow_token'); - const cachedUser = localStorage.getItem('aiflow_user'); - if (token && cachedUser) { - try { - setUser(JSON.parse(cachedUser)); - } catch { - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - } - } - setLoading(false); - }, []); - - const login = (u: User) => { - setUser(u); - localStorage.setItem('aiflow_user', JSON.stringify(u)); - }; - const logout = () => { - setUser(null); - localStorage.removeItem('aiflow_token'); - localStorage.removeItem('aiflow_user'); - }; - const isAdmin = user?.role === 'admin'; - - if (loading) { - return
加载中...
; - } - - return ( - - - {/* Login */} - : ) : - } /> - - {/* Root - Landing for guests, redirect for authenticated users */} - : ) : - } /> - - {/* Front Routes (authenticated only) */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - {/* Admin Routes */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - - } /> - - - ); -}; - -export default App; +import React, { useState, useEffect } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import type { User } from '@/types'; +import { AuthContext, AuthContextType } from '@/contexts/AuthContext'; +import { authAPI } from '@/api'; + +// Layouts +import FrontLayout from '@/components/layout/FrontLayout'; +import AdminLayout from '@/components/layout/AdminLayout'; + +// Front Pages +import LandingPage from '@/pages/front/LandingPage'; +import LoginPage from '@/pages/front/LoginPage'; +import ChatPage from '@/pages/front/ChatPage'; +import ChannelPage from '@/pages/front/ChannelPage'; +import KnowledgePage from '@/pages/front/KnowledgePage'; +import MarketPage from '@/pages/front/MarketPage'; +import TokenDashboardPage from '@/pages/front/TokenDashboardPage'; +import PricingPage from '@/pages/front/PricingPage'; +import ProfilePage from '@/pages/front/ProfilePage'; +import NotificationPage from '@/pages/front/NotificationPage'; +import AssetsPage from '@/pages/front/AssetsPage'; +import NovelPreviewPage from '@/pages/front/NovelPreviewPage'; +import EvolutionDashboard from '@/pages/front/EvolutionDashboard'; + +// Admin Pages +import AdminDashboard from '@/pages/admin/Dashboard'; +import AdminUsers from '@/pages/admin/Users'; +import AdminModels from '@/pages/admin/Models'; +import AdminSiteSettings from '@/pages/admin/SiteSettings'; +import AdminPayment from '@/pages/admin/Payment'; +import AdminServices from '@/pages/admin/Services'; +import AdminMembership from '@/pages/admin/Membership'; +import AdminAds from '@/pages/admin/Ads'; +import AdminMarket from '@/pages/admin/Market'; +import AdminLogs from '@/pages/admin/Logs'; +import AdminSkills from '@/pages/admin/Skills' +import AdminAgents from '@/pages/admin/Agents' +import AdminKnowledge from '@/pages/admin/Knowledge';; + +const App: React.FC = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // 初始化:从localStorage恢复登录状态 + useEffect(() => { + const token = localStorage.getItem('aiflow_token'); + const cachedUser = localStorage.getItem('aiflow_user'); + if (token && cachedUser) { + try { + setUser(JSON.parse(cachedUser)); + } catch { + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + } + } + setLoading(false); + }, []); + + const login = (u: User) => { + setUser(u); + localStorage.setItem('aiflow_user', JSON.stringify(u)); + }; + const logout = () => { + setUser(null); + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + }; + const isAdmin = user?.role === 'admin'; + + if (loading) { + return
加载中...
; + } + + return ( + + + {/* Login */} + : ) : + } /> + + {/* Root - Landing for guests, redirect for authenticated users */} + : ) : + } /> + + {/* Front Routes (authenticated only) */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Admin Routes */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + + } /> + + + ); +}; + +export default App; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 13a9a34..ab7e15f 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -455,6 +455,25 @@ export const adminAPI = { publishAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/publish`, data), unpublishAdminSkill: (id: string) => post(`/admin/skills/${id}/unpublish`), getAdminSkillsStats: () => get('/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(`/admin/agents/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getAdminAgent: (id: string) => get(`/admin/agents/${id}`), + createAdminAgent: (data: any) => post('/admin/agents/', data), + updateAdminAgent: (id: string, data: any) => put(`/admin/agents/${id}`, data), + deleteAdminAgent: (id: string) => del(`/admin/agents/${id}`), + toggleAdminAgent: (id: string, isActive: boolean) => put(`/admin/agents/${id}/toggle`, { is_active: isActive }), + getAdminAgentsStats: () => get('/admin/agents/stats/overview'), + getAdminAgentReviews: (id: string) => get(`/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(`/admin/agents/available/skills${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getAdminAvailableModels: () => get('/admin/agents/available/models'), }; // ===== AI自进化API ===== export const evolutionAPI = { diff --git a/frontend/src/components/layout/AdminLayout.tsx b/frontend/src/components/layout/AdminLayout.tsx index e3cedbd..8963bc0 100644 --- a/frontend/src/components/layout/AdminLayout.tsx +++ b/frontend/src/components/layout/AdminLayout.tsx @@ -1,126 +1,128 @@ -import React, { useState } from 'react'; -import { Outlet, NavLink, useNavigate } from 'react-router-dom'; -import { useAuth } from '@/contexts/AuthContext'; -import './AdminLayout.css'; - -const adminNavItems = [ - { path: '/admin', icon: 'dashboard', label: '平台驾驶舱', exact: true }, - { path: '/admin/users', icon: 'users', label: '用户管理' }, - { path: '/admin/models', icon: 'models', label: '大模型库管理' }, - { path: '/admin/settings', icon: 'settings', label: '网站基础设置' }, - { path: '/admin/payment', icon: 'payment', label: '支付渠道配置' }, - { path: '/admin/services', icon: 'services', label: '增值服务管理' }, - { path: '/admin/membership', icon: 'membership', label: '会员订阅体系' }, - { path: '/admin/ads', icon: 'ads', label: '广告位管理' }, - { path: '/admin/market', icon: 'market', label: '应用市场审核' }, - { path: '/admin/logs', icon: 'logs', label: '日志与风控' }, - - { path: '/admin/skills', icon: 'skills', label: '技能管理' },]; - -const adminIconPaths: Record = { - dashboard: , - users: , - models: , - settings: , - payment: , - services: , - membership: , - ads: , - market: , - logs: , - - skills: ,}; - -const AdminLayout: React.FC = () => { - const { user, logout } = useAuth(); - const navigate = useNavigate(); - const [collapsed, setCollapsed] = useState(false); - - if (!user || user.role !== 'admin') { - navigate('/login'); - return null; - } - - return ( -
- {/* Admin Sidebar */} - - - {/* Admin Main Content */} -
-
-
- 欢迎,{user.username} -
-
- - - 返回前台 - -
-
- {user.username.charAt(0).toUpperCase()} -
- {user.username} -
-
-
-
- -
-
-
- ); -}; - -export default AdminLayout; +import React, { useState } from 'react'; +import { Outlet, NavLink, useNavigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import './AdminLayout.css'; + +const adminNavItems = [ + { path: '/admin', icon: 'dashboard', label: '平台驾驶舱', exact: true }, + { path: '/admin/users', icon: 'users', label: '用户管理' }, + { path: '/admin/models', icon: 'models', label: '大模型库管理' }, + { path: '/admin/settings', icon: 'settings', label: '网站基础设置' }, + { path: '/admin/payment', icon: 'payment', label: '支付渠道配置' }, + { path: '/admin/services', icon: 'services', label: '增值服务管理' }, + { path: '/admin/membership', icon: 'membership', label: '会员订阅体系' }, + { path: '/admin/ads', icon: 'ads', label: '广告位管理' }, + { path: '/admin/market', icon: 'market', label: '应用市场审核' }, + { path: '/admin/logs', icon: 'logs', label: '日志与风控' }, + + { path: '/admin/agents', icon: 'agents', label: 'Agent管理' }, + { path: '/admin/knowledge', icon: 'knowledge', label: '知识库管理' }, + { path: '/admin/skills', icon: 'skills', label: '技能管理' },]; + +const adminIconPaths: Record = { + dashboard: , + users: , + models: , + settings: , + payment: , + services: , + membership: , + ads: , + market: , + logs: , + + skills: ,}; + +const AdminLayout: React.FC = () => { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const [collapsed, setCollapsed] = useState(false); + + if (!user || user.role !== 'admin') { + navigate('/login'); + return null; + } + + return ( +
+ {/* Admin Sidebar */} + + + {/* Admin Main Content */} +
+
+
+ 欢迎,{user.username} +
+
+ + + 返回前台 + +
+
+ {user.username.charAt(0).toUpperCase()} +
+ {user.username} +
+
+
+
+ +
+
+
+ ); +}; + +export default AdminLayout; diff --git a/frontend/src/pages/admin/Knowledge.tsx b/frontend/src/pages/admin/Knowledge.tsx new file mode 100644 index 0000000..aa46c1c --- /dev/null +++ b/frontend/src/pages/admin/Knowledge.tsx @@ -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; + 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 Knowledge: React.FC = () => { + // Data + const [knowledgeBases, setKnowledgeBases] = useState([]); + const [stats, setStats] = useState(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([]); + const [showExtModal, setShowExtModal] = useState(false); + const [editingExtService, setEditingExtService] = useState(null); + const [extForm, setExtForm] = useState({ + name: '', + service_type: 'fastgpt', + api_url: '', + api_key: '', + }); + + // 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); + + // 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 = { + 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 {s.text}; + }; + + const syncStatusBadge = (status: string) => { + const map: Record = { + 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 {s.text}; + }; + + const extTypeLabel = (type: string) => { + const map: Record = { fastgpt: 'FastGPT', dify: 'Dify', ragflow: 'RAGFlow' }; + return map[type] || type; + }; + + return ( +
+

📚 知识库管理

+ + {/* Stats Dashboard */} + {stats && ( +
+
+
{stats.total_knowledge_bases}
+
知识库总数
+
+
+
{stats.total_documents}
+
文档总数
+
+
+
{stats.total_storage_display}
+
存储占用
+
+
+
{stats.official_kb}
+
官方知识库
+
+
+
{stats.kb_with_graph}
+
含图谱KB
+
+
+
{stats.external_kb}
+
外部集成
+
+
+ )} + + {/* Toolbar */} +
+ setKeyword(e.target.value)} + style={{ padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: '4px', width: '200px' }} + /> + + + + +
+ + {/* Knowledge Base Table */} + + + + + + + + + + + + + + + {loading ? ( + + ) : knowledgeBases.length === 0 ? ( + + ) : knowledgeBases.map(kb => ( + + + + + + + + + + + ))} + +
名称来源文档数存储状态同步创建时间操作
加载中...
暂无知识库数据
+
{kb.name}
+
{kb.description?.substring(0, 50)}
+
+ {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 Create/Edit Modal */} + {showModal && ( +
+
+

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

+
+ + setKBForm({ ...kbForm, name: e.target.value })} + style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} /> +
+
+ +