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

@@ -0,0 +1,337 @@
/**
* 系统知识库控制器
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { getSystemKbService } from './systemKbService.js';
import { prisma } from '../../../config/database.js';
import { logger } from '../../../common/logging/index.js';
// ==================== 类型定义 ====================
interface CreateKbBody {
code: string;
name: string;
description?: string;
category?: string;
}
interface UpdateKbBody {
name?: string;
description?: string;
category?: string;
}
interface ListKbQuery {
category?: string;
status?: string;
}
interface KbIdParams {
id: string;
}
interface DocIdParams {
id: string;
docId: string;
}
// ==================== 控制器函数 ====================
/**
* 获取知识库列表
*/
export async function listKnowledgeBases(
request: FastifyRequest<{ Querystring: ListKbQuery }>,
reply: FastifyReply
) {
try {
const { category, status } = request.query;
const service = getSystemKbService(prisma);
const kbs = await service.listKnowledgeBases({ category, status });
return reply.send({
success: true,
data: kbs,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取知识库列表失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 获取知识库详情
*/
export async function getKnowledgeBase(
request: FastifyRequest<{ Params: KbIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getSystemKbService(prisma);
const kb = await service.getKnowledgeBase(id);
if (!kb) {
return reply.status(404).send({
success: false,
error: '知识库不存在',
});
}
return reply.send({
success: true,
data: kb,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取知识库详情失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 创建知识库
*/
export async function createKnowledgeBase(
request: FastifyRequest<{ Body: CreateKbBody }>,
reply: FastifyReply
) {
try {
const { code, name, description, category } = request.body;
if (!code || !name) {
return reply.status(400).send({
success: false,
error: '编码和名称为必填项',
});
}
const service = getSystemKbService(prisma);
const kb = await service.createKnowledgeBase({ code, name, description, category });
return reply.status(201).send({
success: true,
data: kb,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('创建知识库失败', { error: message });
if (message.includes('已存在')) {
return reply.status(409).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 更新知识库
*/
export async function updateKnowledgeBase(
request: FastifyRequest<{ Params: KbIdParams; Body: UpdateKbBody }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const { name, description, category } = request.body;
const service = getSystemKbService(prisma);
const kb = await service.updateKnowledgeBase(id, { name, description, category });
return reply.send({
success: true,
data: kb,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('更新知识库失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 删除知识库
*/
export async function deleteKnowledgeBase(
request: FastifyRequest<{ Params: KbIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getSystemKbService(prisma);
await service.deleteKnowledgeBase(id);
return reply.send({
success: true,
message: '删除成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('删除知识库失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 获取文档列表
*/
export async function listDocuments(
request: FastifyRequest<{ Params: KbIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getSystemKbService(prisma);
const docs = await service.listDocuments(id);
return reply.send({
success: true,
data: docs,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取文档列表失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 上传文档
*/
export async function uploadDocument(
request: FastifyRequest<{ Params: KbIdParams }>,
reply: FastifyReply
) {
try {
const { id: kbId } = request.params;
// 处理 multipart 文件上传
const data = await request.file();
if (!data) {
return reply.status(400).send({
success: false,
error: '请上传文件',
});
}
const filename = data.filename;
const fileBuffer = await data.toBuffer();
// 验证文件类型
const allowedTypes = ['pdf', 'docx', 'doc', 'txt', 'md'];
const ext = filename.toLowerCase().split('.').pop();
if (!ext || !allowedTypes.includes(ext)) {
return reply.status(400).send({
success: false,
error: `不支持的文件类型: ${ext},支持: ${allowedTypes.join(', ')}`,
});
}
// 验证文件大小(最大 50MB
const maxSize = 50 * 1024 * 1024;
if (fileBuffer.length > maxSize) {
return reply.status(400).send({
success: false,
error: '文件大小超过限制(最大 50MB',
});
}
const service = getSystemKbService(prisma);
const result = await service.uploadDocument(kbId, filename, fileBuffer);
return reply.status(201).send({
success: true,
data: {
docId: result.docId,
filename,
chunkCount: result.ingestResult.chunkCount,
tokenCount: result.ingestResult.tokenCount,
duration: result.ingestResult.duration,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('上传文档失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 删除文档
*/
export async function deleteDocument(
request: FastifyRequest<{ Params: DocIdParams }>,
reply: FastifyReply
) {
try {
const { id: kbId, docId } = request.params;
const service = getSystemKbService(prisma);
await service.deleteDocument(kbId, docId);
return reply.send({
success: true,
message: '删除成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('删除文档失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 下载文档
*/
export async function downloadDocument(
request: FastifyRequest<{ Params: DocIdParams }>,
reply: FastifyReply
) {
try {
const { id: kbId, docId } = request.params;
const service = getSystemKbService(prisma);
const result = await service.getDocumentDownloadUrl(kbId, docId);
return reply.send({
success: true,
data: result,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取下载链接失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}