feat(admin): Implement System Knowledge Base management module

Features:

- Backend: SystemKbService with full CRUD (knowledge bases + documents)

- Backend: 8 RESTful API endpoints (list/detail/create/update/delete/upload/download)

- Backend: OSS storage integration (system/knowledge-bases/{kbId}/{docId})

- Backend: RAG engine integration (document parsing, chunking, vectorization)

- Frontend: SystemKbListPage with card-based layout

- Frontend: SystemKbDetailPage with document management table

- Frontend: Master-Detail UX pattern for better user experience

- Document upload (single/batch), download (preserving original filename), delete

Technical:

- Database migration for system_knowledge_bases and system_kb_documents tables

- OSSAdapter.getSignedUrl with Content-Disposition for original filename

- Reuse RAG engine from common/rag for document processing

Tested: Local environment verified, all features working
This commit is contained in:
2026-01-28 21:57:44 +08:00
parent 3a4aa9123c
commit 0d9e6b9922
28 changed files with 2827 additions and 247 deletions

View File

@@ -21,6 +21,9 @@ import { MODULES } from './framework/modules/moduleRegistry'
import UserListPage from './modules/admin/pages/UserListPage'
import UserFormPage from './modules/admin/pages/UserFormPage'
import UserDetailPage from './modules/admin/pages/UserDetailPage'
// 系统知识库管理
import SystemKbListPage from './modules/admin/pages/SystemKbListPage'
import SystemKbDetailPage from './modules/admin/pages/SystemKbDetailPage'
// 个人中心页面
import ProfilePage from './pages/user/ProfilePage'
@@ -109,6 +112,9 @@ function App() {
<Route path="users/create" element={<UserFormPage mode="create" />} />
<Route path="users/:id" element={<UserDetailPage />} />
<Route path="users/:id/edit" element={<UserFormPage mode="edit" />} />
{/* 系统知识库 */}
<Route path="system-kb" element={<SystemKbListPage />} />
<Route path="system-kb/:id" element={<SystemKbDetailPage />} />
{/* 系统配置 */}
<Route path="system" element={<div className="text-center py-20">🚧 ...</div>} />
</Route>

View File

@@ -12,6 +12,7 @@ import {
MenuFoldOutlined,
MenuUnfoldOutlined,
BellOutlined,
BookOutlined,
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import { useAuth } from '../auth'
@@ -83,6 +84,11 @@ const AdminLayout = () => {
icon: <CodeOutlined />,
label: 'Prompt管理',
},
{
key: '/admin/system-kb',
icon: <BookOutlined />,
label: '系统知识库',
},
{
key: '/admin/tenants',
icon: <TeamOutlined />,

View File

@@ -0,0 +1,125 @@
/**
* 系统知识库 API
*/
import apiClient from '@/common/api/axios';
import type {
SystemKb,
SystemKbDocument,
CreateKbRequest,
UpdateKbRequest,
UploadDocumentResponse,
} from '../types/systemKb';
const BASE_URL = '/api/v1/admin/system-kb';
/** API 响应包装 */
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
/**
* 获取知识库列表
*/
export async function listKnowledgeBases(params?: {
category?: string;
status?: string;
}): Promise<SystemKb[]> {
const response = await apiClient.get<ApiResponse<SystemKb[]>>(BASE_URL, { params });
return response.data.data;
}
/**
* 获取知识库详情
*/
export async function getKnowledgeBase(id: string): Promise<SystemKb> {
const response = await apiClient.get<ApiResponse<SystemKb>>(`${BASE_URL}/${id}`);
return response.data.data;
}
/**
* 创建知识库
*/
export async function createKnowledgeBase(data: CreateKbRequest): Promise<SystemKb> {
const response = await apiClient.post<ApiResponse<SystemKb>>(BASE_URL, data);
return response.data.data;
}
/**
* 更新知识库
*/
export async function updateKnowledgeBase(id: string, data: UpdateKbRequest): Promise<SystemKb> {
const response = await apiClient.patch<ApiResponse<SystemKb>>(`${BASE_URL}/${id}`, data);
return response.data.data;
}
/**
* 删除知识库
*/
export async function deleteKnowledgeBase(id: string): Promise<void> {
await apiClient.delete(`${BASE_URL}/${id}`);
}
/**
* 获取知识库文档列表
*/
export async function listDocuments(kbId: string): Promise<SystemKbDocument[]> {
const response = await apiClient.get<ApiResponse<SystemKbDocument[]>>(
`${BASE_URL}/${kbId}/documents`
);
return response.data.data;
}
/**
* 上传文档
*/
export async function uploadDocument(
kbId: string,
file: File,
onProgress?: (percent: number) => void
): Promise<UploadDocumentResponse> {
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post<ApiResponse<UploadDocumentResponse>>(
`${BASE_URL}/${kbId}/documents`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress(percent);
}
},
}
);
return response.data.data;
}
/**
* 删除文档
*/
export async function deleteDocument(kbId: string, docId: string): Promise<void> {
await apiClient.delete(`${BASE_URL}/${kbId}/documents/${docId}`);
}
/**
* 获取文档下载链接
*/
export async function getDocumentDownloadUrl(kbId: string, docId: string): Promise<{
url: string;
filename: string;
fileSize: number | null;
}> {
const response = await apiClient.get<ApiResponse<{
url: string;
filename: string;
fileSize: number | null;
}>>(`${BASE_URL}/${kbId}/documents/${docId}/download`);
return response.data.data;
}

View File

@@ -6,6 +6,7 @@
* - 用户管理
* - 租户管理(已有)
* - Prompt管理已有
* - 系统知识库管理
*/
import React from 'react';
@@ -14,6 +15,8 @@ import UserListPage from './pages/UserListPage';
import UserFormPage from './pages/UserFormPage';
import UserDetailPage from './pages/UserDetailPage';
import StatsDashboardPage from './pages/StatsDashboardPage';
import SystemKbListPage from './pages/SystemKbListPage';
import SystemKbDetailPage from './pages/SystemKbDetailPage';
const AdminModule: React.FC = () => {
return (
@@ -28,6 +31,10 @@ const AdminModule: React.FC = () => {
<Route path="users/create" element={<UserFormPage mode="create" />} />
<Route path="users/:id" element={<UserDetailPage />} />
<Route path="users/:id/edit" element={<UserFormPage mode="edit" />} />
{/* 系统知识库管理 */}
<Route path="system-kb" element={<SystemKbListPage />} />
<Route path="system-kb/:id" element={<SystemKbDetailPage />} />
</Routes>
);
};

View File

@@ -0,0 +1,451 @@
/**
* 系统知识库详情页
*
* 管理知识库中的文档
*/
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Card,
Button,
Table,
Space,
Upload,
message,
Popconfirm,
Tag,
Typography,
Empty,
Spin,
Progress,
Breadcrumb,
Descriptions,
Tooltip,
} from 'antd';
import {
ArrowLeftOutlined,
UploadOutlined,
DeleteOutlined,
DownloadOutlined,
FileTextOutlined,
FilePdfOutlined,
FileWordOutlined,
FileUnknownOutlined,
ClockCircleOutlined,
DatabaseOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import * as systemKbApi from '../api/systemKbApi';
import type { SystemKb, SystemKbDocument } from '../types/systemKb';
const { Title, Text } = Typography;
/** 文件图标映射 */
const getFileIcon = (fileType: string | null) => {
switch (fileType) {
case 'pdf':
return <FilePdfOutlined className="text-red-500" />;
case 'doc':
case 'docx':
return <FileWordOutlined className="text-blue-500" />;
case 'txt':
case 'md':
return <FileTextOutlined className="text-gray-500" />;
default:
return <FileUnknownOutlined className="text-gray-400" />;
}
};
/** 格式化文件大小 */
const formatFileSize = (bytes: number | null) => {
if (!bytes) return '-';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
/** 格式化日期 */
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
const SystemKbDetailPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [kb, setKb] = useState<SystemKb | null>(null);
const [documents, setDocuments] = useState<SystemKbDocument[]>([]);
const [loading, setLoading] = useState(true);
const [docsLoading, setDocsLoading] = useState(false);
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
// 加载知识库详情
const loadKnowledgeBase = async () => {
if (!id) return;
setLoading(true);
try {
const data = await systemKbApi.getKnowledgeBase(id);
setKb(data);
} catch (error) {
message.error('加载知识库失败');
navigate('/admin/system-kb');
} finally {
setLoading(false);
}
};
// 加载文档列表
const loadDocuments = async () => {
if (!id) return;
setDocsLoading(true);
try {
const data = await systemKbApi.listDocuments(id);
setDocuments(data);
} catch (error) {
message.error('加载文档列表失败');
} finally {
setDocsLoading(false);
}
};
useEffect(() => {
loadKnowledgeBase();
loadDocuments();
}, [id]);
// 上传文档
const handleUpload = async (file: File) => {
if (!id) return;
setUploadProgress(0);
try {
const result = await systemKbApi.uploadDocument(
id,
file,
(percent) => setUploadProgress(percent)
);
message.success(`上传成功:${result.chunkCount} 个分块,${result.tokenCount.toLocaleString()} tokens`);
await loadDocuments();
await loadKnowledgeBase(); // 刷新统计
} catch (error: any) {
message.error(error.response?.data?.error || '上传失败');
} finally {
setUploadProgress(null);
}
};
// 删除单个文档
const handleDeleteDoc = async (doc: SystemKbDocument) => {
if (!id) return;
try {
await systemKbApi.deleteDocument(id, doc.id);
message.success('删除成功');
await loadDocuments();
await loadKnowledgeBase();
} catch (error) {
message.error('删除失败');
}
};
// 批量删除
const handleBatchDelete = async () => {
if (!id || selectedRowKeys.length === 0) return;
try {
for (const docId of selectedRowKeys) {
await systemKbApi.deleteDocument(id, docId as string);
}
message.success(`成功删除 ${selectedRowKeys.length} 个文档`);
setSelectedRowKeys([]);
await loadDocuments();
await loadKnowledgeBase();
} catch (error) {
message.error('删除失败');
}
};
// 下载文档
const handleDownload = async (doc: SystemKbDocument) => {
if (!id) return;
try {
const result = await systemKbApi.getDocumentDownloadUrl(id, doc.id);
// 创建临时链接并触发下载
const link = document.createElement('a');
link.href = result.url;
link.download = result.filename;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
message.error('获取下载链接失败');
}
};
// 表格列定义
const columns: ColumnsType<SystemKbDocument> = [
{
title: '文件名',
dataIndex: 'filename',
key: 'filename',
render: (text, record) => (
<Space>
{getFileIcon(record.fileType)}
<span className="font-medium">{text}</span>
</Space>
),
},
{
title: '大小',
dataIndex: 'fileSize',
key: 'fileSize',
width: 100,
render: (val) => formatFileSize(val),
},
{
title: 'Tokens',
dataIndex: 'tokenCount',
key: 'tokenCount',
width: 100,
align: 'right',
render: (val) => val?.toLocaleString() || '-',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status, record) => {
const statusConfig: Record<string, { color: string; text: string }> = {
ready: { color: 'success', text: '就绪' },
processing: { color: 'processing', text: '处理中' },
failed: { color: 'error', text: '失败' },
pending: { color: 'warning', text: '等待' },
};
const config = statusConfig[status] || { color: 'default', text: status };
return (
<Tooltip title={record.errorMessage}>
<Tag color={config.color}>{config.text}</Tag>
</Tooltip>
);
},
},
{
title: '上传时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (val) => formatDate(val),
},
{
title: '操作',
key: 'action',
width: 120,
render: (_, record) => (
<Space size="small">
<Tooltip title="下载">
<Button
type="text"
icon={<DownloadOutlined />}
onClick={() => handleDownload(record)}
disabled={record.status !== 'ready'}
/>
</Tooltip>
<Popconfirm
title="确定删除此文档?"
onConfirm={() => handleDeleteDoc(record)}
>
<Tooltip title="删除">
<Button type="text" danger icon={<DeleteOutlined />} />
</Tooltip>
</Popconfirm>
</Space>
),
},
];
if (loading) {
return (
<div className="h-96 flex items-center justify-center">
<Spin size="large" />
</div>
);
}
if (!kb) {
return (
<div className="p-6">
<Empty description="知识库不存在" />
</div>
);
}
return (
<div className="p-6">
{/* 面包屑 */}
<Breadcrumb
className="mb-4"
items={[
{ title: <a onClick={() => navigate('/admin/system-kb')}></a> },
{ title: kb.name },
]}
/>
{/* 返回按钮和标题 */}
<div className="flex items-center gap-4 mb-6">
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/admin/system-kb')}
>
</Button>
<div>
<Title level={4} className="mb-0">{kb.name}</Title>
<Text code>{kb.code}</Text>
</div>
</div>
{/* 知识库信息卡片 */}
<Card className="mb-6">
<Descriptions
column={{ xs: 1, sm: 2, md: 4 }}
items={[
{
key: 'documents',
label: (
<Space>
<FileTextOutlined />
<span></span>
</Space>
),
children: <span className="text-xl font-semibold">{kb.documentCount}</span>,
},
{
key: 'tokens',
label: (
<Space>
<DatabaseOutlined />
<span> Tokens</span>
</Space>
),
children: <span className="text-xl font-semibold">{kb.totalTokens.toLocaleString()}</span>,
},
{
key: 'created',
label: (
<Space>
<ClockCircleOutlined />
<span></span>
</Space>
),
children: formatDate(kb.createdAt),
},
{
key: 'description',
label: '描述',
children: kb.description || <Text type="secondary"></Text>,
},
]}
/>
</Card>
{/* 文档管理卡片 */}
<Card
title={
<Space>
<FileTextOutlined />
<span></span>
<Tag>{documents.length} </Tag>
</Space>
}
extra={
<Space>
{selectedRowKeys.length > 0 && (
<Popconfirm
title={`确定删除选中的 ${selectedRowKeys.length} 个文档?`}
onConfirm={handleBatchDelete}
>
<Button danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
)}
<Upload
accept=".pdf,.doc,.docx,.txt,.md"
showUploadList={false}
multiple
beforeUpload={(file) => {
handleUpload(file);
return false;
}}
>
<Button type="primary" icon={<UploadOutlined />}>
</Button>
</Upload>
</Space>
}
>
{/* 上传进度 */}
{uploadProgress !== null && (
<div className="mb-4 p-4 bg-blue-50 rounded-lg">
<div className="flex items-center gap-4">
<Spin size="small" />
<div className="flex-1">
<Progress percent={uploadProgress} status="active" />
<Text type="secondary">...</Text>
</div>
</div>
</div>
)}
{/* 文档表格 */}
<Spin spinning={docsLoading}>
{documents.length > 0 ? (
<Table
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys,
}}
columns={columns}
dataSource={documents}
rowKey="id"
pagination={documents.length > 10 ? { pageSize: 10 } : false}
/>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无文档"
>
<Upload
accept=".pdf,.doc,.docx,.txt,.md"
showUploadList={false}
beforeUpload={(file) => {
handleUpload(file);
return false;
}}
>
<Button type="primary" icon={<UploadOutlined />}>
</Button>
</Upload>
</Empty>
)}
</Spin>
</Card>
</div>
);
};
export default SystemKbDetailPage;

View File

@@ -0,0 +1,316 @@
/**
* 系统知识库列表页
*
* 卡片式展示所有知识库
*/
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Card,
Button,
Modal,
Form,
Input,
Select,
message,
Popconfirm,
Typography,
Empty,
Spin,
Row,
Col,
Statistic,
} from 'antd';
import {
PlusOutlined,
DeleteOutlined,
FolderOutlined,
FileTextOutlined,
ReloadOutlined,
RightOutlined,
} from '@ant-design/icons';
import * as systemKbApi from '../api/systemKbApi';
import type { SystemKb, CreateKbRequest } from '../types/systemKb';
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
/** 知识库分类选项 */
const CATEGORY_OPTIONS = [
{ value: 'guidelines', label: '临床指南' },
{ value: 'methodology', label: '方法学' },
{ value: 'regulations', label: '法规政策' },
{ value: 'templates', label: '模板文档' },
{ value: 'other', label: '其他' },
];
/** 分类标签颜色 */
const CATEGORY_COLORS: Record<string, string> = {
guidelines: '#1890ff',
methodology: '#52c41a',
regulations: '#faad14',
templates: '#722ed1',
other: '#8c8c8c',
};
const SystemKbListPage: React.FC = () => {
const navigate = useNavigate();
const [knowledgeBases, setKnowledgeBases] = useState<SystemKb[]>([]);
const [loading, setLoading] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [form] = Form.useForm();
// 加载知识库列表
const loadKnowledgeBases = async () => {
setLoading(true);
try {
const data = await systemKbApi.listKnowledgeBases();
setKnowledgeBases(data);
} catch (error) {
message.error('加载知识库列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadKnowledgeBases();
}, []);
// 创建知识库
const handleCreate = async (values: CreateKbRequest) => {
try {
const newKb = await systemKbApi.createKnowledgeBase(values);
message.success('创建成功');
setCreateModalOpen(false);
form.resetFields();
// 直接进入详情页
navigate(`/admin/system-kb/${newKb.id}`);
} catch (error: any) {
message.error(error.response?.data?.error || '创建失败');
}
};
// 删除知识库
const handleDelete = async (e: React.MouseEvent, kb: SystemKb) => {
e.stopPropagation();
try {
await systemKbApi.deleteKnowledgeBase(kb.id);
message.success('删除成功');
await loadKnowledgeBases();
} catch (error) {
message.error('删除失败');
}
};
// 进入详情页
const handleCardClick = (kb: SystemKb) => {
navigate(`/admin/system-kb/${kb.id}`);
};
return (
<div className="p-6">
{/* 页面标题 */}
<div className="flex justify-between items-center mb-6">
<div>
<Title level={3} className="mb-1"></Title>
<Text type="secondary"> Prompt </Text>
</div>
<div className="flex gap-2">
<Button icon={<ReloadOutlined />} onClick={loadKnowledgeBases}>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setCreateModalOpen(true)}
>
</Button>
</div>
</div>
{/* 知识库卡片列表 */}
<Spin spinning={loading}>
{knowledgeBases.length > 0 ? (
<Row gutter={[16, 16]}>
{knowledgeBases.map((kb) => (
<Col key={kb.id} xs={24} sm={12} md={8} lg={6}>
<Card
hoverable
className="h-full cursor-pointer transition-all hover:shadow-lg"
onClick={() => handleCardClick(kb)}
actions={[
<Popconfirm
key="delete"
title="确定删除此知识库?"
description="将同时删除所有文档,此操作不可恢复"
onConfirm={(e) => handleDelete(e as React.MouseEvent, kb)}
onCancel={(e) => e?.stopPropagation()}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
>
</Button>
</Popconfirm>,
<Button
key="manage"
type="text"
icon={<RightOutlined />}
onClick={() => handleCardClick(kb)}
>
</Button>,
]}
>
{/* 卡片头部 */}
<div className="flex items-start gap-3 mb-4">
<div
className="w-12 h-12 rounded-lg flex items-center justify-center"
style={{
backgroundColor: `${CATEGORY_COLORS[kb.category || 'other']}15`,
color: CATEGORY_COLORS[kb.category || 'other'],
}}
>
<FolderOutlined className="text-2xl" />
</div>
<div className="flex-1 min-w-0">
<Title level={5} className="mb-0 truncate" title={kb.name}>
{kb.name}
</Title>
<Text code className="text-xs">{kb.code}</Text>
</div>
</div>
{/* 描述 */}
{kb.description && (
<Paragraph
type="secondary"
className="text-sm mb-4"
ellipsis={{ rows: 2 }}
>
{kb.description}
</Paragraph>
)}
{/* 统计数据 */}
<Row gutter={16}>
<Col span={12}>
<Statistic
title={<span className="text-xs"></span>}
value={kb.documentCount}
prefix={<FileTextOutlined />}
valueStyle={{ fontSize: 18 }}
/>
</Col>
<Col span={12}>
<Statistic
title={<span className="text-xs">Tokens</span>}
value={kb.totalTokens}
valueStyle={{ fontSize: 18 }}
formatter={(val) => {
const num = Number(val);
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num;
}}
/>
</Col>
</Row>
</Card>
</Col>
))}
{/* 新建卡片 */}
<Col xs={24} sm={12} md={8} lg={6}>
<Card
hoverable
className="h-full cursor-pointer border-dashed flex items-center justify-center"
style={{ minHeight: 260 }}
onClick={() => setCreateModalOpen(true)}
>
<div className="text-center py-8">
<PlusOutlined className="text-4xl text-gray-400 mb-4" />
<div className="text-gray-500"></div>
</div>
</Card>
</Col>
</Row>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无知识库"
>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setCreateModalOpen(true)}
>
</Button>
</Empty>
)}
</Spin>
{/* 创建知识库弹窗 */}
<Modal
title="创建知识库"
open={createModalOpen}
onCancel={() => {
setCreateModalOpen(false);
form.resetFields();
}}
onOk={() => form.submit()}
okText="创建"
cancelText="取消"
width={480}
>
<Form
form={form}
layout="vertical"
onFinish={handleCreate}
className="mt-4"
>
<Form.Item
name="code"
label="知识库编码"
rules={[
{ required: true, message: '请输入编码' },
{ pattern: /^[A-Z][A-Z0-9_]*$/, message: '编码只能包含大写字母、数字和下划线,且以字母开头' },
]}
extra="唯一标识符,用于 Prompt 引用"
>
<Input placeholder="如CLINICAL_GUIDELINES" />
</Form.Item>
<Form.Item
name="name"
label="知识库名称"
rules={[{ required: true, message: '请输入名称' }]}
>
<Input placeholder="如:临床指南知识库" />
</Form.Item>
<Form.Item name="category" label="分类">
<Select
placeholder="选择分类"
options={CATEGORY_OPTIONS}
allowClear
/>
</Form.Item>
<Form.Item name="description" label="描述">
<TextArea rows={3} placeholder="知识库描述(可选)" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default SystemKbListPage;

View File

@@ -0,0 +1,55 @@
/**
* 系统知识库类型定义
*/
/** 知识库 */
export interface SystemKb {
id: string;
code: string;
name: string;
description: string | null;
category: string | null;
documentCount: number;
totalTokens: number;
status: string;
createdAt: string;
updatedAt: string;
}
/** 知识库文档 */
export interface SystemKbDocument {
id: string;
kbId: string;
filename: string;
filePath: string | null;
fileSize: number | null;
fileType: string | null;
tokenCount: number;
status: string;
errorMessage: string | null;
createdAt: string;
}
/** 创建知识库请求 */
export interface CreateKbRequest {
code: string;
name: string;
description?: string;
category?: string;
}
/** 更新知识库请求 */
export interface UpdateKbRequest {
name?: string;
description?: string;
category?: string;
}
/** 上传文档响应 */
export interface UploadDocumentResponse {
docId: string;
filename: string;
chunkCount: number;
tokenCount: number;
duration: number;
}