feat(backend): implement knowledge base management backend API

This commit is contained in:
AI Clinical Dev Team
2025-10-11 11:32:47 +08:00
parent 8a4c703128
commit b26700a7d5
9 changed files with 1346 additions and 0 deletions

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^9.2.1",
"@prisma/client": "^6.17.0",
"@types/form-data": "^2.2.1",
"axios": "^1.12.2",
@@ -507,6 +508,12 @@
"fast-uri": "^3.0.0"
}
},
"node_modules/@fastify/busboy": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/@fastify/busboy/-/busboy-3.2.0.tgz",
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
"license": "MIT"
},
"node_modules/@fastify/cors": {
"version": "11.1.0",
"resolved": "https://registry.npmmirror.com/@fastify/cors/-/cors-11.1.0.tgz",
@@ -527,6 +534,22 @@
"toad-cache": "^3.7.0"
}
},
"node_modules/@fastify/deepmerge": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/@fastify/deepmerge/-/deepmerge-3.1.0.tgz",
"integrity": "sha512-lCVONBQINyNhM6LLezB6+2afusgEYR4G8xenMsfe+AT+iZ7Ca6upM5Ha8UkZuYSnuMw3GWl/BiPXnLMi/gSxuQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@fastify/error": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/@fastify/error/-/error-4.2.0.tgz",
@@ -620,6 +643,29 @@
"dequal": "^2.0.3"
}
},
"node_modules/@fastify/multipart": {
"version": "9.2.1",
"resolved": "https://registry.npmmirror.com/@fastify/multipart/-/multipart-9.2.1.tgz",
"integrity": "sha512-U4221XDMfzCUtfzsyV1/PkR4MNgKI0158vUUyn/oF2Tl6RxMc+N7XYLr5fZXQiEC+Fmw5zFaTjxsTGTgtDtK+g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^3.0.0",
"@fastify/deepmerge": "^3.0.0",
"@fastify/error": "^4.0.0",
"fastify-plugin": "^5.0.0",
"secure-json-parse": "^4.0.0"
}
},
"node_modules/@fastify/proxy-addr": {
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz",

View File

@@ -24,6 +24,7 @@
"dependencies": {
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^9.2.1",
"@prisma/client": "^6.17.0",
"@types/form-data": "^2.2.1",
"axios": "^1.12.2",

View File

@@ -0,0 +1,246 @@
import type { FastifyRequest, FastifyReply } from 'fastify';
import * as documentService from '../services/documentService.js';
// Mock用户ID实际应从JWT token中获取
const MOCK_USER_ID = 'user-mock-001';
/**
* 上传文档
*/
export async function uploadDocument(
request: FastifyRequest<{
Params: {
kbId: string;
};
}>,
reply: FastifyReply
) {
try {
const { kbId } = request.params;
// 获取上传的文件
const data = await request.file();
if (!data) {
return reply.status(400).send({
success: false,
message: 'No file uploaded',
});
}
const file = await data.toBuffer();
const filename = data.filename;
const fileType = data.mimetype;
const fileSizeBytes = file.length;
// 文件大小限制10MB
if (fileSizeBytes > 10 * 1024 * 1024) {
return reply.status(400).send({
success: false,
message: 'File size exceeds 10MB limit',
});
}
// 文件类型限制
const allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'text/markdown',
];
if (!allowedTypes.includes(fileType)) {
return reply.status(400).send({
success: false,
message: 'File type not supported. Allowed: PDF, DOC, DOCX, TXT, MD',
});
}
// 上传文档这里fileUrl暂时为空实际应该上传到对象存储
const document = await documentService.uploadDocument(
MOCK_USER_ID,
kbId,
file,
filename,
fileType,
fileSizeBytes,
'' // fileUrl - 可以上传到OSS后填入
);
return reply.status(201).send({
success: true,
data: document,
});
} catch (error: any) {
console.error('Failed to upload document:', error);
if (error.message.includes('not found') || error.message.includes('access denied')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
if (error.message.includes('limit exceeded')) {
return reply.status(400).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to upload document',
});
}
}
/**
* 获取文档列表
*/
export async function getDocuments(
request: FastifyRequest<{
Params: {
kbId: string;
};
}>,
reply: FastifyReply
) {
try {
const { kbId } = request.params;
const documents = await documentService.getDocuments(MOCK_USER_ID, kbId);
return reply.send({
success: true,
data: documents,
});
} catch (error: any) {
console.error('Failed to get documents:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to get documents',
});
}
}
/**
* 获取文档详情
*/
export async function getDocumentById(
request: FastifyRequest<{
Params: {
id: string;
};
}>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const document = await documentService.getDocumentById(MOCK_USER_ID, id);
return reply.send({
success: true,
data: document,
});
} catch (error: any) {
console.error('Failed to get document:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to get document',
});
}
}
/**
* 删除文档
*/
export async function deleteDocument(
request: FastifyRequest<{
Params: {
id: string;
};
}>,
reply: FastifyReply
) {
try {
const { id } = request.params;
await documentService.deleteDocument(MOCK_USER_ID, id);
return reply.send({
success: true,
message: 'Document deleted successfully',
});
} catch (error: any) {
console.error('Failed to delete document:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to delete document',
});
}
}
/**
* 重新处理文档
*/
export async function reprocessDocument(
request: FastifyRequest<{
Params: {
id: string;
};
}>,
reply: FastifyReply
) {
try {
const { id } = request.params;
await documentService.reprocessDocument(MOCK_USER_ID, id);
return reply.send({
success: true,
message: 'Document reprocessing started',
});
} catch (error: any) {
console.error('Failed to reprocess document:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to reprocess document',
});
}
}

View File

@@ -0,0 +1,291 @@
import type { FastifyRequest, FastifyReply } from 'fastify';
import * as knowledgeBaseService from '../services/knowledgeBaseService.js';
// Mock用户ID实际应从JWT token中获取
const MOCK_USER_ID = 'user-mock-001';
/**
* 创建知识库
*/
export async function createKnowledgeBase(
request: FastifyRequest<{
Body: {
name: string;
description?: string;
};
}>,
reply: FastifyReply
) {
try {
const { name, description } = request.body;
if (!name || name.trim().length === 0) {
return reply.status(400).send({
success: false,
message: 'Knowledge base name is required',
});
}
const knowledgeBase = await knowledgeBaseService.createKnowledgeBase(
MOCK_USER_ID,
name,
description
);
return reply.status(201).send({
success: true,
data: knowledgeBase,
});
} catch (error: any) {
console.error('Failed to create knowledge base:', error);
return reply.status(500).send({
success: false,
message: error.message || 'Failed to create knowledge base',
});
}
}
/**
* 获取知识库列表
*/
export async function getKnowledgeBases(
_request: FastifyRequest,
reply: FastifyReply
) {
try {
const knowledgeBases = await knowledgeBaseService.getKnowledgeBases(
MOCK_USER_ID
);
return reply.send({
success: true,
data: knowledgeBases,
});
} catch (error: any) {
console.error('Failed to get knowledge bases:', error);
return reply.status(500).send({
success: false,
message: error.message || 'Failed to get knowledge bases',
});
}
}
/**
* 获取知识库详情
*/
export async function getKnowledgeBaseById(
request: FastifyRequest<{
Params: {
id: string;
};
}>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const knowledgeBase = await knowledgeBaseService.getKnowledgeBaseById(
MOCK_USER_ID,
id
);
return reply.send({
success: true,
data: knowledgeBase,
});
} catch (error: any) {
console.error('Failed to get knowledge base:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to get knowledge base',
});
}
}
/**
* 更新知识库
*/
export async function updateKnowledgeBase(
request: FastifyRequest<{
Params: {
id: string;
};
Body: {
name?: string;
description?: string;
};
}>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const updateData = request.body;
const knowledgeBase = await knowledgeBaseService.updateKnowledgeBase(
MOCK_USER_ID,
id,
updateData
);
return reply.send({
success: true,
data: knowledgeBase,
});
} catch (error: any) {
console.error('Failed to update knowledge base:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to update knowledge base',
});
}
}
/**
* 删除知识库
*/
export async function deleteKnowledgeBase(
request: FastifyRequest<{
Params: {
id: string;
};
}>,
reply: FastifyReply
) {
try {
const { id } = request.params;
await knowledgeBaseService.deleteKnowledgeBase(MOCK_USER_ID, id);
return reply.send({
success: true,
message: 'Knowledge base deleted successfully',
});
} catch (error: any) {
console.error('Failed to delete knowledge base:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to delete knowledge base',
});
}
}
/**
* 检索知识库
*/
export async function searchKnowledgeBase(
request: FastifyRequest<{
Params: {
id: string;
};
Querystring: {
query: string;
top_k?: string;
};
}>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const { query, top_k } = request.query;
if (!query || query.trim().length === 0) {
return reply.status(400).send({
success: false,
message: 'Query parameter is required',
});
}
const topK = top_k ? parseInt(top_k, 10) : 3;
const results = await knowledgeBaseService.searchKnowledgeBase(
MOCK_USER_ID,
id,
query,
topK
);
return reply.send({
success: true,
data: results,
});
} catch (error: any) {
console.error('Failed to search knowledge base:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to search knowledge base',
});
}
}
/**
* 获取知识库统计信息
*/
export async function getKnowledgeBaseStats(
request: FastifyRequest<{
Params: {
id: string;
};
}>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const stats = await knowledgeBaseService.getKnowledgeBaseStats(
MOCK_USER_ID,
id
);
return reply.send({
success: true,
data: stats,
});
} catch (error: any) {
console.error('Failed to get knowledge base stats:', error);
if (error.message.includes('not found')) {
return reply.status(404).send({
success: false,
message: error.message,
});
}
return reply.status(500).send({
success: false,
message: error.message || 'Failed to get knowledge base stats',
});
}
}

View File

@@ -1,10 +1,17 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import { config, validateEnv } from './config/env.js';
import { testDatabaseConnection, prisma } from './config/database.js';
import { projectRoutes } from './routes/projects.js';
import { agentRoutes } from './routes/agents.js';
import { conversationRoutes } from './routes/conversations.js';
import knowledgeBaseRoutes from './routes/knowledgeBases.js';
// 全局处理BigInt序列化
(BigInt.prototype as any).toJSON = function() {
return Number(this);
};
const fastify = Fastify({
logger: {
@@ -26,6 +33,13 @@ await fastify.register(cors, {
credentials: true,
});
// 注册文件上传插件
await fastify.register(multipart, {
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
},
});
// 健康检查路由
fastify.get('/health', async () => {
// 检查数据库连接
@@ -63,6 +77,9 @@ await fastify.register(agentRoutes, { prefix: '/api/v1' });
// 注册对话管理路由
await fastify.register(conversationRoutes, { prefix: '/api/v1' });
// 注册知识库管理路由
await fastify.register(knowledgeBaseRoutes, { prefix: '/api/v1' });
// 启动服务器
const start = async () => {
try {

View File

@@ -0,0 +1,46 @@
import type { FastifyInstance } from 'fastify';
import * as knowledgeBaseController from '../controllers/knowledgeBaseController.js';
import * as documentController from '../controllers/documentController.js';
export default async function knowledgeBaseRoutes(fastify: FastifyInstance) {
// ==================== 知识库管理 API ====================
// 创建知识库
fastify.post('/knowledge-bases', knowledgeBaseController.createKnowledgeBase);
// 获取知识库列表
fastify.get('/knowledge-bases', knowledgeBaseController.getKnowledgeBases);
// 获取知识库详情
fastify.get('/knowledge-bases/:id', knowledgeBaseController.getKnowledgeBaseById);
// 更新知识库
fastify.put('/knowledge-bases/:id', knowledgeBaseController.updateKnowledgeBase);
// 删除知识库
fastify.delete('/knowledge-bases/:id', knowledgeBaseController.deleteKnowledgeBase);
// 检索知识库
fastify.get('/knowledge-bases/:id/search', knowledgeBaseController.searchKnowledgeBase);
// 获取知识库统计信息
fastify.get('/knowledge-bases/:id/stats', knowledgeBaseController.getKnowledgeBaseStats);
// ==================== 文档管理 API ====================
// 上传文档
fastify.post('/knowledge-bases/:kbId/documents', documentController.uploadDocument);
// 获取文档列表
fastify.get('/knowledge-bases/:kbId/documents', documentController.getDocuments);
// 获取文档详情
fastify.get('/documents/:id', documentController.getDocumentById);
// 删除文档
fastify.delete('/documents/:id', documentController.deleteDocument);
// 重新处理文档
fastify.post('/documents/:id/reprocess', documentController.reprocessDocument);
}

View File

@@ -0,0 +1,327 @@
import { prisma } from '../config/database.js';
import { difyClient } from '../clients/DifyClient.js';
/**
* 文档服务
*/
/**
* 上传文档到知识库
*/
export async function uploadDocument(
userId: string,
kbId: string,
file: Buffer,
filename: string,
fileType: string,
fileSizeBytes: number,
fileUrl: string
) {
// 1. 验证知识库权限
const knowledgeBase = await prisma.knowledgeBase.findFirst({
where: {
id: kbId,
userId,
},
});
if (!knowledgeBase) {
throw new Error('Knowledge base not found or access denied');
}
// 2. 检查文档数量限制每个知识库最多50个文档
const documentCount = await prisma.document.count({
where: { kbId },
});
if (documentCount >= 50) {
throw new Error('Document limit exceeded. Maximum 50 documents per knowledge base');
}
// 3. 在数据库中创建文档记录状态uploading
const document = await prisma.document.create({
data: {
kbId,
userId,
filename,
fileType,
fileSizeBytes,
fileUrl,
difyDocumentId: '', // 暂时为空,稍后更新
status: 'uploading',
progress: 0,
},
});
try {
// 4. 上传到Dify
const difyResult = await difyClient.uploadDocumentDirectly(
knowledgeBase.difyDatasetId,
file,
filename
);
// 5. 更新文档记录更新difyDocumentId和状态
const updatedDocument = await prisma.document.update({
where: { id: document.id },
data: {
difyDocumentId: difyResult.document.id,
status: difyResult.document.indexing_status,
progress: 50,
},
});
// 6. 启动后台轮询任务,等待处理完成
pollDocumentStatus(userId, kbId, document.id, difyResult.document.id).catch(error => {
console.error('Failed to poll document status:', error);
});
// 7. 更新知识库统计
await updateKnowledgeBaseStats(kbId);
// 8. 转换BigInt为Number
return {
...updatedDocument,
fileSizeBytes: Number(updatedDocument.fileSizeBytes),
};
} catch (error) {
// 上传失败更新状态为error
await prisma.document.update({
where: { id: document.id },
data: {
status: 'error',
errorMessage: error instanceof Error ? error.message : 'Upload failed',
},
});
throw error;
}
}
/**
* 轮询文档处理状态
*/
async function pollDocumentStatus(
userId: string,
kbId: string,
documentId: string,
difyDocumentId: string,
maxAttempts: number = 30
) {
const knowledgeBase = await prisma.knowledgeBase.findFirst({
where: { id: kbId, userId },
});
if (!knowledgeBase) {
return;
}
for (let i = 0; i < maxAttempts; i++) {
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒
try {
// 查询Dify中的文档状态
const difyDocument = await difyClient.getDocument(
knowledgeBase.difyDatasetId,
difyDocumentId
);
// 更新数据库中的状态
await prisma.document.update({
where: { id: documentId },
data: {
status: difyDocument.indexing_status,
progress: difyDocument.indexing_status === 'completed' ? 100 : 50 + (i * 2),
segmentsCount: difyDocument.indexing_status === 'completed' ? difyDocument.word_count : null,
tokensCount: difyDocument.indexing_status === 'completed' ? difyDocument.tokens : null,
processedAt: difyDocument.indexing_status === 'completed' ? new Date() : null,
errorMessage: difyDocument.error || null,
},
});
// 如果完成或失败,退出轮询
if (difyDocument.indexing_status === 'completed') {
await updateKnowledgeBaseStats(kbId);
break;
}
if (difyDocument.indexing_status === 'error') {
break;
}
} catch (error) {
console.error(`Polling attempt ${i + 1} failed:`, error);
}
}
}
/**
* 获取文档列表
*/
export async function getDocuments(userId: string, kbId: string) {
// 1. 验证权限
const knowledgeBase = await prisma.knowledgeBase.findFirst({
where: {
id: kbId,
userId,
},
});
if (!knowledgeBase) {
throw new Error('Knowledge base not found or access denied');
}
// 2. 查询文档列表
const documents = await prisma.document.findMany({
where: { kbId },
orderBy: { uploadedAt: 'desc' },
});
// 3. 转换BigInt为Number
return documents.map(doc => ({
...doc,
fileSizeBytes: Number(doc.fileSizeBytes),
}));
}
/**
* 获取文档详情
*/
export async function getDocumentById(userId: string, documentId: string) {
const document = await prisma.document.findFirst({
where: {
id: documentId,
userId, // 确保只能访问自己的文档
},
include: {
knowledgeBase: true,
},
});
if (!document) {
throw new Error('Document not found or access denied');
}
// 转换BigInt为Number
return {
...document,
fileSizeBytes: Number(document.fileSizeBytes),
knowledgeBase: {
...document.knowledgeBase,
totalSizeBytes: Number(document.knowledgeBase.totalSizeBytes),
},
};
}
/**
* 删除文档
*/
export async function deleteDocument(userId: string, documentId: string) {
// 1. 查询文档信息
const document = await prisma.document.findFirst({
where: {
id: documentId,
userId,
},
include: {
knowledgeBase: true,
},
});
if (!document) {
throw new Error('Document not found or access denied');
}
// 2. 删除Dify中的文档
if (document.difyDocumentId) {
try {
await difyClient.deleteDocument(
document.knowledgeBase.difyDatasetId,
document.difyDocumentId
);
} catch (error) {
console.error('Failed to delete Dify document:', error);
// 继续删除本地记录
}
}
// 3. 删除数据库记录
await prisma.document.delete({
where: { id: documentId },
});
// 4. 更新知识库统计
await updateKnowledgeBaseStats(document.kbId);
}
/**
* 重新处理文档
*/
export async function reprocessDocument(userId: string, documentId: string) {
// 1. 查询文档信息
const document = await prisma.document.findFirst({
where: {
id: documentId,
userId,
},
include: {
knowledgeBase: true,
},
});
if (!document) {
throw new Error('Document not found or access denied');
}
// 2. 触发Dify重新索引
if (document.difyDocumentId) {
try {
await difyClient.updateDocument(
document.knowledgeBase.difyDatasetId,
document.difyDocumentId
);
// 3. 更新状态为processing
await prisma.document.update({
where: { id: documentId },
data: {
status: 'parsing',
progress: 0,
errorMessage: null,
},
});
// 4. 启动轮询
pollDocumentStatus(
userId,
document.kbId,
documentId,
document.difyDocumentId
).catch(error => {
console.error('Failed to poll document status:', error);
});
} catch (error) {
throw new Error('Failed to reprocess document');
}
}
}
/**
* 更新知识库统计信息
*/
async function updateKnowledgeBaseStats(kbId: string) {
const documents = await prisma.document.findMany({
where: { kbId },
});
const totalSizeBytes = documents.reduce((sum, d) => sum + Number(d.fileSizeBytes), 0);
const fileCount = documents.length;
await prisma.knowledgeBase.update({
where: { id: kbId },
data: {
fileCount,
totalSizeBytes: BigInt(totalSizeBytes),
},
});
}

View File

@@ -0,0 +1,261 @@
import { prisma } from '../config/database.js';
import { difyClient } from '../clients/DifyClient.js';
/**
* 知识库服务
*/
/**
* 创建知识库
*/
export async function createKnowledgeBase(
userId: string,
name: string,
description?: string
) {
// 1. 检查用户知识库配额
const user = await prisma.user.findUnique({
where: { id: userId },
select: { kbQuota: true, kbUsed: true }
});
if (!user) {
throw new Error('User not found');
}
if (user.kbUsed >= user.kbQuota) {
throw new Error(`Knowledge base quota exceeded. Maximum: ${user.kbQuota}`);
}
// 2. 在Dify中创建Dataset
const difyDataset = await difyClient.createDataset({
name: `${userId}_${name}_${Date.now()}`,
description: description || `Knowledge base for user ${userId}`,
indexing_technique: 'high_quality',
});
// 3. 在数据库中创建记录
const knowledgeBase = await prisma.knowledgeBase.create({
data: {
userId,
name,
description,
difyDatasetId: difyDataset.id,
},
});
// 4. 更新用户的知识库使用计数
await prisma.user.update({
where: { id: userId },
data: {
kbUsed: { increment: 1 },
},
});
// 5. 转换BigInt为Number
return {
...knowledgeBase,
totalSizeBytes: Number(knowledgeBase.totalSizeBytes),
};
}
/**
* 获取用户的知识库列表
*/
export async function getKnowledgeBases(userId: string) {
const knowledgeBases = await prisma.knowledgeBase.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
include: {
_count: {
select: { documents: true },
},
},
});
// 转换BigInt为Number
return knowledgeBases.map(kb => ({
...kb,
totalSizeBytes: Number(kb.totalSizeBytes),
}));
}
/**
* 获取知识库详情
*/
export async function getKnowledgeBaseById(userId: string, kbId: string) {
const knowledgeBase = await prisma.knowledgeBase.findFirst({
where: {
id: kbId,
userId, // 确保只能访问自己的知识库
},
include: {
documents: {
orderBy: { uploadedAt: 'desc' },
},
_count: {
select: { documents: true },
},
},
});
if (!knowledgeBase) {
throw new Error('Knowledge base not found or access denied');
}
// 转换BigInt为Number
const result = {
...knowledgeBase,
totalSizeBytes: Number(knowledgeBase.totalSizeBytes),
documents: knowledgeBase.documents.map(doc => ({
...doc,
fileSizeBytes: Number(doc.fileSizeBytes),
})),
};
return result;
}
/**
* 更新知识库
*/
export async function updateKnowledgeBase(
userId: string,
kbId: string,
data: { name?: string; description?: string }
) {
// 1. 验证权限
const existingKb = await prisma.knowledgeBase.findFirst({
where: {
id: kbId,
userId,
},
});
if (!existingKb) {
throw new Error('Knowledge base not found or access denied');
}
// 2. 更新数据库
const knowledgeBase = await prisma.knowledgeBase.update({
where: { id: kbId },
data,
});
// 3. 转换BigInt为Number
return {
...knowledgeBase,
totalSizeBytes: Number(knowledgeBase.totalSizeBytes),
};
}
/**
* 删除知识库
*/
export async function deleteKnowledgeBase(userId: string, kbId: string) {
// 1. 验证权限
const knowledgeBase = await prisma.knowledgeBase.findFirst({
where: {
id: kbId,
userId,
},
});
if (!knowledgeBase) {
throw new Error('Knowledge base not found or access denied');
}
// 2. 删除Dify中的Dataset
try {
await difyClient.deleteDataset(knowledgeBase.difyDatasetId);
} catch (error) {
console.error('Failed to delete Dify dataset:', error);
// 继续删除本地记录即使Dify删除失败
}
// 3. 删除数据库记录会级联删除documents
await prisma.knowledgeBase.delete({
where: { id: kbId },
});
// 4. 更新用户的知识库使用计数
await prisma.user.update({
where: { id: userId },
data: {
kbUsed: { decrement: 1 },
},
});
}
/**
* 检索知识库
*/
export async function searchKnowledgeBase(
userId: string,
kbId: string,
query: string,
topK: number = 3
) {
// 1. 验证权限
const knowledgeBase = await prisma.knowledgeBase.findFirst({
where: {
id: kbId,
userId,
},
});
if (!knowledgeBase) {
throw new Error('Knowledge base not found or access denied');
}
// 2. 调用Dify检索API
const results = await difyClient.retrieveKnowledge(
knowledgeBase.difyDatasetId,
query,
{
retrieval_model: {
search_method: 'semantic_search',
top_k: topK,
score_threshold_enabled: true,
score_threshold: 0.3,
},
}
);
return results;
}
/**
* 获取知识库统计信息
*/
export async function getKnowledgeBaseStats(userId: string, kbId: string) {
// 1. 验证权限
const knowledgeBase = await prisma.knowledgeBase.findFirst({
where: {
id: kbId,
userId,
},
include: {
documents: true,
},
});
if (!knowledgeBase) {
throw new Error('Knowledge base not found or access denied');
}
// 2. 统计信息
const stats = {
totalDocuments: knowledgeBase.documents.length,
completedDocuments: knowledgeBase.documents.filter(d => d.status === 'completed').length,
processingDocuments: knowledgeBase.documents.filter(d =>
['uploading', 'parsing', 'indexing'].includes(d.status)
).length,
errorDocuments: knowledgeBase.documents.filter(d => d.status === 'error').length,
totalSizeBytes: knowledgeBase.totalSizeBytes,
totalTokens: knowledgeBase.documents.reduce((sum, d) => sum + (d.tokensCount || 0), 0),
};
return stats;
}

111
backend/test-kb-api.ps1 Normal file
View File

@@ -0,0 +1,111 @@
# 测试知识库API脚本
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " 测试知识库API" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# 测试1: 创建知识库
Write-Host "测试1: 创建知识库..." -ForegroundColor Yellow
$createBody = @{
name = "Test Knowledge Base"
description = "This is a test knowledge base"
} | ConvertTo-Json
try {
$result1 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases" `
-Method Post `
-Body $createBody `
-ContentType "application/json"
Write-Host "✓ 创建成功" -ForegroundColor Green
Write-Host " 知识库ID: $($result1.data.id)" -ForegroundColor Gray
Write-Host " 名称: $($result1.data.name)" -ForegroundColor Gray
$kbId = $result1.data.id
} catch {
Write-Host "✗ 创建失败: $_" -ForegroundColor Red
exit 1
}
Write-Host ""
# 测试2: 获取知识库列表
Write-Host "测试2: 获取知识库列表..." -ForegroundColor Yellow
try {
$result2 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases" `
-Method Get
Write-Host "✓ 获取成功" -ForegroundColor Green
Write-Host " 知识库数量: $($result2.data.Count)" -ForegroundColor Gray
} catch {
Write-Host "✗ 获取失败: $_" -ForegroundColor Red
}
Write-Host ""
# 测试3: 获取知识库详情
Write-Host "测试3: 获取知识库详情..." -ForegroundColor Yellow
try {
$result3 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases/$kbId" `
-Method Get
Write-Host "✓ 获取成功" -ForegroundColor Green
Write-Host " 名称: $($result3.data.name)" -ForegroundColor Gray
Write-Host " 文档数: $($result3.data._count.documents)" -ForegroundColor Gray
} catch {
Write-Host "✗ 获取失败: $_" -ForegroundColor Red
}
Write-Host ""
# 测试4: 更新知识库
Write-Host "测试4: 更新知识库..." -ForegroundColor Yellow
$updateBody = @{
name = "Updated Test KB"
description = "Updated description"
} | ConvertTo-Json
try {
$result4 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases/$kbId" `
-Method Put `
-Body $updateBody `
-ContentType "application/json"
Write-Host "✓ 更新成功" -ForegroundColor Green
Write-Host " 新名称: $($result4.data.name)" -ForegroundColor Gray
} catch {
Write-Host "✗ 更新失败: $_" -ForegroundColor Red
}
Write-Host ""
# 测试5: 检索知识库
Write-Host "测试5: 检索知识库..." -ForegroundColor Yellow
try {
$result5 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases/$kbId/search?query=test" `
-Method Get
Write-Host "✓ 检索成功" -ForegroundColor Green
Write-Host " 结果数: $($result5.data.records.Count)" -ForegroundColor Gray
} catch {
Write-Host "✗ 检索失败: $_" -ForegroundColor Red
}
Write-Host ""
# 测试6: 删除知识库
Write-Host "测试6: 删除知识库..." -ForegroundColor Yellow
try {
$result6 = Invoke-RestMethod -Uri "http://localhost:3001/api/v1/knowledge-bases/$kbId" `
-Method Delete
Write-Host "✓ 删除成功" -ForegroundColor Green
} catch {
Write-Host "✗ 删除失败: $_" -ForegroundColor Red
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " 测试完成!" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan