diff --git a/src/App.tsx b/src/App.tsx index 69259d4..53db1b3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import ProfilePage from './pages/ProfilePage'; import PricingPage from './pages/PricingPage'; import SharedSessionPage from './pages/SharedSessionPage'; import WorkflowsPage from './pages/WorkflowsPage'; +import KnowledgeBasePage from './pages/KnowledgeBase'; import { useAuth } from './store/auth'; import { AgentAPI } from './api'; import { useIsMobile } from './hooks/useIsMobile'; @@ -64,6 +65,7 @@ export default function App() { } /> } /> } /> + } /> } /> ); diff --git a/src/api/index.ts b/src/api/index.ts index be93997..2535ef4 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -16,4 +16,5 @@ export * from './llmProviders'; export * from './streamChat'; export * from './workflows'; export * from './membership'; +export * from './knowledgeBase'; diff --git a/src/api/knowledgeBase.ts b/src/api/knowledgeBase.ts new file mode 100644 index 0000000..d9a5877 --- /dev/null +++ b/src/api/knowledgeBase.ts @@ -0,0 +1,55 @@ +import { api } from './http'; + +export interface KBDTO { + id: string; + name: string; + description: string; + ownerId: string; + isPublic: boolean; + createdAt: number; + updatedAt: number; + access: 'owner' | 'view' | 'none'; +} + +export interface KBFileDTO { + id: string; + originalName: string; + filename: string; + size: number; + status: 'indexing' | 'ready' | 'error'; + chunkCount?: number; + createdAt?: number; +} + +export const KnowledgeBaseAPI = { + // 核心管理 + listKBs: () => api.get<{ data: KBDTO[] }>('/kbs').then((r) => r.data.data), + getKB: (id: string) => api.get<{ data: KBDTO }>(`/kbs/${id}`).then((r) => r.data.data), + createKB: (data: { name: string; description: string; isPublic: boolean }) => + api.post<{ data: KBDTO }>('/kbs', data).then((r) => r.data.data), + updateKB: (id: string, data: Partial<{ name: string; description: string; isPublic: boolean }>) => + api.patch<{ data: KBDTO }>(`/kbs/${id}`, data).then((r) => r.data.data), + deleteKB: (id: string) => api.delete(`/kbs/${id}`).then((r) => r.data), + + // 文件管理 + listFiles: (id: string) => api.get<{ data: KBFileDTO[] }>(`/kbs/${id}/files`).then((r) => r.data.data), + uploadFiles: (id: string, files: File[]) => { + const formData = new FormData(); + files.forEach((file) => formData.append('files', file)); + return api.post<{ data: KBFileDTO[] }>(`/kbs/${id}/files`, formData).then((r) => r.data.data); + }, + removeFile: (kbId: string, fileId: string) => + api.delete(`/kbs/${kbId}/files/${fileId}`).then((r) => r.data), + + // 授权与共享 + shareKB: (id: string, data: { subjectType: 'user' | 'team'; subjectId: string }) => + api.post(`/kbs/${id}/shares`, data).then((r) => r.data), + unshareKB: (id: string, data: { subjectType: 'user' | 'team'; subjectId: string }) => + api.delete(`/kbs/${id}/shares`, { data }).then((r) => r.data), + + // 智能体关联 + mountToAgent: (agentId: string, kbId: string) => + api.post(`/agents/${agentId}/kbs/${kbId}`).then((r) => r.data), + unmountFromAgent: (agentId: string, kbId: string) => + api.delete(`/agents/${agentId}/kbs/${kbId}`).then((r) => r.data), +}; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index fc19aab..492f0dc 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -14,7 +14,8 @@ import { RightOutlined, UserOutlined, CreditCardOutlined, - ShoppingCartOutlined + ShoppingCartOutlined, + DatabaseOutlined } from '@ant-design/icons'; import { useAuth } from '../store/auth'; import kaiwuIcon from '../assets/brand/kaiwu-icon-gradient-transparent.png'; @@ -42,6 +43,7 @@ const NAV_GROUPS: Array<{ { label: '资源', items: [ + { to: '/knowledge', icon: , label: '知识库' }, { to: '/prompts', icon: , label: 'Prompt 模板库' }, { to: '/workflows', icon: , label: '工作流' } ] diff --git a/src/pages/KnowledgeBase/KnowledgeBaseLogic.ts b/src/pages/KnowledgeBase/KnowledgeBaseLogic.ts new file mode 100644 index 0000000..a69a33c --- /dev/null +++ b/src/pages/KnowledgeBase/KnowledgeBaseLogic.ts @@ -0,0 +1,150 @@ +import { useState, useEffect, useCallback } from 'react'; +import { message } from 'antd'; +import { KnowledgeBaseAPI, KBDTO, KBFileDTO } from '../../api/knowledgeBase'; +import { useAuth } from '../../store/auth'; + +export function useKnowledgeBaseLogic() { + const { user } = useAuth(); + const [loading, setLoading] = useState(false); + const [kbs, setKbs] = useState([]); + const [activeKb, setActiveKb] = useState(null); + const [files, setFiles] = useState([]); + const [filesLoading, setFilesLoading] = useState(false); + + // 弹窗状态 + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); + + // 获取知识库列表 + const loadKBs = useCallback(async () => { + setLoading(true); + try { + const data = await KnowledgeBaseAPI.listKBs(); + setKbs(data); + // 如果没有选中的,默认选第一个 + if (data.length > 0 && !activeKb) { + setActiveKb(data[0]); + } else if (activeKb) { + // 刷新当前选中的 KB 信息 + const updated = data.find(kb => kb.id === activeKb.id); + if (updated) setActiveKb(updated); + } + } catch (err) { + message.error('加载知识库失败'); + } finally { + setLoading(false); + } + }, [activeKb]); + + // 获取当前 KB 的文件列表 + const loadFiles = useCallback(async (kbId: string) => { + setFilesLoading(true); + try { + const data = await KnowledgeBaseAPI.listFiles(kbId); + setFiles(data); + } catch (err) { + message.error('加载文件列表失败'); + } finally { + setFilesLoading(false); + } + }, []); + + useEffect(() => { + loadKBs(); + }, []); + + useEffect(() => { + if (activeKb) { + loadFiles(activeKb.id); + } else { + setFiles([]); + } + }, [activeKb?.id, loadFiles]); + + // CRUD 操作 + const handleCreate = async (values: { name: string; description: string; isPublic: boolean }) => { + try { + const newKb = await KnowledgeBaseAPI.createKB(values); + message.success('创建成功'); + setIsCreateModalOpen(false); + await loadKBs(); + setActiveKb(newKb); + } catch (err) { + message.error('创建失败'); + } + }; + + const handleUpdate = async (values: Partial) => { + if (!activeKb) return; + try { + await KnowledgeBaseAPI.updateKB(activeKb.id, values); + message.success('更新成功'); + setIsEditModalOpen(false); + await loadKBs(); + } catch (err) { + message.error('更新失败'); + } + }; + + const handleDelete = async (id: string) => { + try { + await KnowledgeBaseAPI.deleteKB(id); + message.success('删除成功'); + if (activeKb?.id === id) { + setActiveKb(null); + } + await loadKBs(); + } catch (err) { + message.error('删除失败'); + } + }; + + const handleUpload = async (fileList: File[]) => { + if (!activeKb) return; + try { + await KnowledgeBaseAPI.uploadFiles(activeKb.id, fileList); + message.success('上传成功'); + setIsUploadModalOpen(false); + await loadFiles(activeKb.id); + } catch (err) { + message.error('上传失败'); + } + }; + + const handleRemoveFile = async (fileId: string) => { + if (!activeKb) return; + try { + await KnowledgeBaseAPI.removeFile(activeKb.id, fileId); + message.success('文件已移除'); + await loadFiles(activeKb.id); + } catch (err) { + message.error('移除失败'); + } + }; + + return { + loading, + kbs, + activeKb, + setActiveKb, + files, + filesLoading, + isCreateModalOpen, + setIsCreateModalOpen, + isEditModalOpen, + setIsEditModalOpen, + isShareModalOpen, + setIsShareModalOpen, + isUploadModalOpen, + setIsUploadModalOpen, + handleCreate, + handleUpdate, + handleDelete, + handleUpload, + handleRemoveFile, + refreshKBs: loadKBs, + refreshFiles: () => activeKb && loadFiles(activeKb.id), + }; +} diff --git a/src/pages/KnowledgeBase/components/KBDetail.tsx b/src/pages/KnowledgeBase/components/KBDetail.tsx new file mode 100644 index 0000000..d6a7625 --- /dev/null +++ b/src/pages/KnowledgeBase/components/KBDetail.tsx @@ -0,0 +1,196 @@ +import { useState, useEffect } from 'react'; +import { Button, Table, Space, Tag, Popconfirm, Tooltip, Dropdown, MenuProps } from 'antd'; +import { + UploadOutlined, + ShareAltOutlined, + DeleteOutlined, + EditOutlined, + FileTextOutlined, + EllipsisOutlined, + RobotOutlined +} from '@ant-design/icons'; +import { KBFileDTO } from '../../../api/knowledgeBase'; +import { AgentAPI, Agent } from '../../../api/agents'; +import { useAuth } from '../../../store/auth'; +import dayjs from 'dayjs'; + +interface Props { + logic: any; // 简化类型,实际开发中建议定义完整接口 +} + +export default function KBDetail({ logic }: Props) { + const { user } = useAuth(); + const { + activeKb, + files, + filesLoading, + setIsEditModalOpen, + setIsShareModalOpen, + setIsUploadModalOpen, + handleDelete, + handleRemoveFile + } = logic; + + const [agents, setAgents] = useState([]); + + useEffect(() => { + // 获取智能体列表,用于挂载 + const fetchAgents = async () => { + if (!user?.phone) return; + try { + const data = await AgentAPI.mine(user.phone); + setAgents(data); + } catch (err) { + console.error('获取智能体列表失败', err); + } + }; + fetchAgents(); + }, [user?.phone]); + + const columns = [ + { + title: '文件名', + dataIndex: 'originalName', + key: 'originalName', + render: (text: string) => ( + + + {text} + + ), + }, + { + title: '大小', + dataIndex: 'size', + key: 'size', + width: 100, + render: (size: number) => { + if (size < 1024) return `${size} B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; + return `${(size / (1024 * 1024)).toFixed(1)} MB`; + }, + }, + { + title: '分块数', + dataIndex: 'chunkCount', + key: 'chunkCount', + width: 80, + render: (count: number) => count ?? '-', + }, + { + title: '上传时间', + dataIndex: 'createdAt', + key: 'createdAt', + width: 160, + render: (time: number) => time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '-', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: (status: KBFileDTO['status']) => { + const config = { + indexing: { color: 'processing', text: '索引中' }, + ready: { color: 'success', text: '就绪' }, + error: { color: 'error', text: '失败' }, + }; + const item = config[status] || config.indexing; + return {item.text}; + }, + }, + { + title: '操作', + key: 'action', + width: 80, + render: (_: any, record: KBFileDTO) => ( + handleRemoveFile(record.id)} + okText="确定" + cancelText="取消" + > + + + + +