feat(pkb): Complete PKB module frontend migration with V3 design
Summary: - Implement PKB Dashboard and Workspace pages based on V3 prototype - Add single-layer header with integrated Tab navigation - Implement 3 work modes: Full Text, Deep Read, Batch Processing - Integrate Ant Design X Chat component for AI conversations - Create BatchModeComplete with template selection and document processing - Add compact work mode selector with dropdown design Backend: - Migrate PKB controllers and services to /modules/pkb structure - Register v2 API routes at /api/v2/pkb/knowledge - Maintain dual API routes for backward compatibility Technical details: - Use Zustand for state management - Handle SSE streaming responses for AI chat - Support document selection for Deep Read mode - Implement batch processing with progress tracking Known issues: - Batch processing API integration pending - Knowledge assets page navigation needs optimization Status: Frontend functional, pending refinement
This commit is contained in:
428
backend/src/modules/pkb/controllers/batchController.ts
Normal file
428
backend/src/modules/pkb/controllers/batchController.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* Phase 3: 批处理模式 - 批处理控制器
|
||||
*
|
||||
* API路由:
|
||||
* - POST /api/v1/batch/execute - 执行批处理任务
|
||||
* - GET /api/v1/batch/tasks/:taskId - 获取任务状态
|
||||
* - GET /api/v1/batch/tasks/:taskId/results - 获取任务结果
|
||||
* - POST /api/v1/batch/tasks/:taskId/retry-failed - 重试失败项
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { executeBatchTask, retryFailedDocuments, BatchProgress } from '../services/batchService.js';
|
||||
import { prisma } from '../../../config/database.js';
|
||||
import { ModelType } from '../../../common/llm/adapters/types.js';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
interface ExecuteBatchBody {
|
||||
kb_id: string;
|
||||
document_ids: string[];
|
||||
template_type: 'preset' | 'custom';
|
||||
template_id?: string;
|
||||
custom_prompt?: string;
|
||||
model_type: ModelType;
|
||||
task_name?: string;
|
||||
}
|
||||
|
||||
interface TaskIdParams {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
// ==================== API处理器 ====================
|
||||
|
||||
/**
|
||||
* POST /api/v1/batch/execute
|
||||
* 执行批处理任务
|
||||
*/
|
||||
export async function executeBatch(
|
||||
request: FastifyRequest<{ Body: ExecuteBatchBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
// TODO: 从JWT获取userId
|
||||
const userId = 'user-mock-001';
|
||||
|
||||
const {
|
||||
kb_id,
|
||||
document_ids,
|
||||
template_type,
|
||||
template_id,
|
||||
custom_prompt,
|
||||
model_type,
|
||||
task_name,
|
||||
} = request.body;
|
||||
|
||||
console.log('📦 [BatchController] 收到批处理请求', {
|
||||
userId,
|
||||
kbId: kb_id,
|
||||
documentCount: document_ids.length,
|
||||
templateType: template_type,
|
||||
modelType: model_type,
|
||||
});
|
||||
|
||||
// 验证参数
|
||||
if (!kb_id || !document_ids || document_ids.length === 0) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: '缺少必要参数:kb_id 或 document_ids',
|
||||
});
|
||||
}
|
||||
|
||||
if (document_ids.length < 3) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: '文献数量不能少于3篇',
|
||||
});
|
||||
}
|
||||
|
||||
if (document_ids.length > 50) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: '文献数量不能超过50篇',
|
||||
});
|
||||
}
|
||||
|
||||
if (template_type === 'preset' && !template_id) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: '预设模板类型需要提供 template_id',
|
||||
});
|
||||
}
|
||||
|
||||
if (template_type === 'custom' && !custom_prompt) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: '自定义模板需要提供 custom_prompt',
|
||||
});
|
||||
}
|
||||
|
||||
// 验证模型类型
|
||||
const validModels: ModelType[] = ['deepseek-v3', 'qwen3-72b', 'qwen-long'];
|
||||
if (!validModels.includes(model_type)) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: `不支持的模型类型: ${model_type}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 验证知识库是否存在
|
||||
const kb = await prisma.knowledgeBase.findUnique({
|
||||
where: { id: kb_id },
|
||||
});
|
||||
|
||||
if (!kb) {
|
||||
return reply.code(404).send({
|
||||
success: false,
|
||||
message: `知识库不存在: ${kb_id}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 验证文档是否都存在
|
||||
const documents = await prisma.document.findMany({
|
||||
where: {
|
||||
id: { in: document_ids },
|
||||
kbId: kb_id,
|
||||
},
|
||||
});
|
||||
|
||||
if (documents.length !== document_ids.length) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: `部分文档不存在或不属于该知识库`,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取WebSocket实例(用于进度推送)
|
||||
const io = (request.server as any).io;
|
||||
|
||||
// 先创建任务记录获取taskId
|
||||
const taskPreview = await prisma.batchTask.create({
|
||||
data: {
|
||||
userId,
|
||||
kbId: kb_id,
|
||||
name: task_name || `批处理任务_${new Date().toLocaleString('zh-CN')}`,
|
||||
templateType: template_type,
|
||||
templateId: template_id || null,
|
||||
prompt: custom_prompt || template_id || '',
|
||||
status: 'processing',
|
||||
totalDocuments: document_ids.length,
|
||||
modelType: model_type,
|
||||
concurrency: 3,
|
||||
startedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const taskId = taskPreview.id;
|
||||
console.log(`✅ [BatchController] 创建任务: ${taskId}`);
|
||||
|
||||
// 执行批处理任务(异步)
|
||||
executeBatchTask({
|
||||
userId,
|
||||
kbId: kb_id,
|
||||
documentIds: document_ids,
|
||||
templateType: template_type,
|
||||
templateId: template_id,
|
||||
customPrompt: custom_prompt,
|
||||
modelType: model_type,
|
||||
taskName: task_name,
|
||||
existingTaskId: taskId, // 使用已创建的任务ID
|
||||
onProgress: (progress: BatchProgress) => {
|
||||
// WebSocket推送进度
|
||||
if (io) {
|
||||
io.to(userId).emit('batch-progress', progress);
|
||||
}
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
console.log(`🎉 [BatchController] 批处理任务完成: ${result.taskId}`);
|
||||
// 推送完成事件
|
||||
if (io) {
|
||||
io.to(userId).emit('batch-completed', {
|
||||
task_id: result.taskId,
|
||||
status: result.status,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`❌ [BatchController] 批处理任务失败:`, error);
|
||||
// 推送失败事件
|
||||
if (io) {
|
||||
io.to(userId).emit('batch-failed', {
|
||||
task_id: 'unknown',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 立即返回任务ID(任务在后台执行)
|
||||
reply.send({
|
||||
success: true,
|
||||
message: '批处理任务已开始',
|
||||
data: {
|
||||
task_id: taskId,
|
||||
status: 'processing',
|
||||
websocket_event: 'batch-progress',
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ [BatchController] 执行批处理失败:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
message: error.message || '执行批处理任务失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/batch/tasks/:taskId
|
||||
* 获取任务状态
|
||||
*/
|
||||
export async function getTask(
|
||||
request: FastifyRequest<{ Params: TaskIdParams }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { taskId } = request.params;
|
||||
|
||||
const task = await prisma.batchTask.findUnique({
|
||||
where: { id: taskId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
status: true,
|
||||
totalDocuments: true,
|
||||
completedCount: true,
|
||||
failedCount: true,
|
||||
modelType: true,
|
||||
startedAt: true,
|
||||
completedAt: true,
|
||||
durationSeconds: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return reply.code(404).send({
|
||||
success: false,
|
||||
message: `任务不存在: ${taskId}`,
|
||||
});
|
||||
}
|
||||
|
||||
reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
status: task.status,
|
||||
total_documents: task.totalDocuments,
|
||||
completed_count: task.completedCount,
|
||||
failed_count: task.failedCount,
|
||||
model_type: task.modelType,
|
||||
started_at: task.startedAt,
|
||||
completed_at: task.completedAt,
|
||||
duration_seconds: task.durationSeconds,
|
||||
created_at: task.createdAt,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ [BatchController] 获取任务失败:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
message: error.message || '获取任务失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/batch/tasks/:taskId/results
|
||||
* 获取任务结果
|
||||
*/
|
||||
export async function getTaskResults(
|
||||
request: FastifyRequest<{ Params: TaskIdParams }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { taskId } = request.params;
|
||||
|
||||
// 获取任务信息
|
||||
const task = await prisma.batchTask.findUnique({
|
||||
where: { id: taskId },
|
||||
include: {
|
||||
results: {
|
||||
include: {
|
||||
document: {
|
||||
select: {
|
||||
filename: true,
|
||||
tokensCount: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return reply.code(404).send({
|
||||
success: false,
|
||||
message: `任务不存在: ${taskId}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化结果
|
||||
const results = task.results.map((r, index) => ({
|
||||
id: r.id,
|
||||
index: index + 1,
|
||||
document_id: r.documentId,
|
||||
document_name: r.document.filename,
|
||||
status: r.status,
|
||||
data: r.data,
|
||||
raw_output: r.rawOutput,
|
||||
error_message: r.errorMessage,
|
||||
processing_time_ms: r.processingTimeMs,
|
||||
tokens_used: r.tokensUsed,
|
||||
created_at: r.createdAt,
|
||||
}));
|
||||
|
||||
reply.send({
|
||||
success: true,
|
||||
data: {
|
||||
task: {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
status: task.status,
|
||||
template_type: task.templateType,
|
||||
template_id: task.templateId,
|
||||
total_documents: task.totalDocuments,
|
||||
completed_count: task.completedCount,
|
||||
failed_count: task.failedCount,
|
||||
duration_seconds: task.durationSeconds,
|
||||
created_at: task.createdAt,
|
||||
completed_at: task.completedAt,
|
||||
},
|
||||
results,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ [BatchController] 获取任务结果失败:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
message: error.message || '获取任务结果失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/batch/tasks/:taskId/retry-failed
|
||||
* 重试失败的文档
|
||||
*/
|
||||
export async function retryFailed(
|
||||
request: FastifyRequest<{ Params: TaskIdParams }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { taskId } = request.params;
|
||||
const userId = 'user-mock-001'; // TODO: 从JWT获取
|
||||
|
||||
// 获取WebSocket实例
|
||||
const io = (request.server as any).io;
|
||||
|
||||
// 执行重试(异步)
|
||||
retryFailedDocuments(taskId, (progress: BatchProgress) => {
|
||||
if (io) {
|
||||
io.to(userId).emit('batch-progress', progress);
|
||||
}
|
||||
})
|
||||
.then((result) => {
|
||||
console.log(`✅ [BatchController] 重试完成: ${result.retriedCount}篇`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`❌ [BatchController] 重试失败:`, error);
|
||||
});
|
||||
|
||||
reply.send({
|
||||
success: true,
|
||||
message: '已开始重试失败的文档',
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ [BatchController] 重试失败:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
message: error.message || '重试失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/batch/templates
|
||||
* 获取所有预设模板
|
||||
*/
|
||||
export async function getTemplates(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { getAllTemplates } = await import('../templates/clinicalResearch.js');
|
||||
const templates = getAllTemplates();
|
||||
|
||||
reply.send({
|
||||
success: true,
|
||||
data: templates.map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
output_fields: t.outputFields,
|
||||
})),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ [BatchController] 获取模板失败:', error);
|
||||
reply.code(500).send({
|
||||
success: false,
|
||||
message: error.message || '获取模板失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
314
backend/src/modules/pkb/controllers/documentController.ts
Normal file
314
backend/src/modules/pkb/controllers/documentController.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
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;
|
||||
console.log(`📤 开始上传文档到知识库: ${kbId}`);
|
||||
|
||||
// 获取上传的文件
|
||||
const data = await request.file();
|
||||
|
||||
if (!data) {
|
||||
console.error('❌ 没有接收到文件');
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: 'No file uploaded',
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📄 接收到文件: ${data.filename}, 类型: ${data.mimetype}`);
|
||||
|
||||
const file = await data.toBuffer();
|
||||
const filename = data.filename;
|
||||
const fileType = data.mimetype;
|
||||
const fileSizeBytes = file.length;
|
||||
|
||||
// 文件大小限制(10MB)
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
console.log(`📊 文件大小: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB (限制: 10MB)`);
|
||||
|
||||
if (fileSizeBytes > maxSize) {
|
||||
console.error(`❌ 文件太大: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB`);
|
||||
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',
|
||||
];
|
||||
|
||||
console.log(`🔍 检查文件类型: ${fileType}`);
|
||||
if (!allowedTypes.includes(fileType)) {
|
||||
console.error(`❌ 不支持的文件类型: ${fileType}`);
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: 'File type not supported. Allowed: PDF, DOC, DOCX, TXT, MD',
|
||||
});
|
||||
}
|
||||
|
||||
// 上传文档(这里fileUrl暂时为空,实际应该上传到对象存储)
|
||||
console.log(`⚙️ 调用文档服务上传文件...`);
|
||||
const document = await documentService.uploadDocument(
|
||||
MOCK_USER_ID,
|
||||
kbId,
|
||||
file,
|
||||
filename,
|
||||
fileType,
|
||||
fileSizeBytes,
|
||||
'' // fileUrl - 可以上传到OSS后填入
|
||||
);
|
||||
|
||||
console.log(`✅ 文档上传成功: ${document.id}`);
|
||||
return reply.status(201).send({
|
||||
success: true,
|
||||
data: document,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ 文档上传失败:', error.message);
|
||||
console.error('错误详情:', 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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: 获取文档全文(用于逐篇精读模式)
|
||||
*/
|
||||
export async function getDocumentFullText(
|
||||
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: {
|
||||
documentId: document.id,
|
||||
filename: document.filename,
|
||||
fileType: document.fileType,
|
||||
fileSizeBytes: document.fileSizeBytes,
|
||||
extractedText: (document as any).extractedText || null,
|
||||
charCount: (document as any).charCount || null,
|
||||
tokensCount: document.tokensCount || null,
|
||||
extractionMethod: (document as any).extractionMethod || null,
|
||||
extractionQuality: (document as any).extractionQuality || null,
|
||||
language: (document as any).language || null,
|
||||
metadata: {
|
||||
uploadedAt: document.uploadedAt,
|
||||
processedAt: document.processedAt,
|
||||
status: document.status,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get document full text:', 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 full text',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
341
backend/src/modules/pkb/controllers/knowledgeBaseController.ts
Normal file
341
backend/src/modules/pkb/controllers/knowledgeBaseController.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
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) : 15; // Phase 1优化:默认从3增加到15
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库文档选择(Phase 2: 全文阅读模式)
|
||||
*/
|
||||
export async function getDocumentSelection(
|
||||
request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string;
|
||||
};
|
||||
Querystring: {
|
||||
max_files?: string;
|
||||
max_tokens?: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { max_files, max_tokens } = request.query;
|
||||
|
||||
const maxFiles = max_files ? parseInt(max_files, 10) : undefined;
|
||||
const maxTokens = max_tokens ? parseInt(max_tokens, 10) : undefined;
|
||||
|
||||
const selection = await knowledgeBaseService.getDocumentSelection(
|
||||
MOCK_USER_ID,
|
||||
id,
|
||||
maxFiles,
|
||||
maxTokens
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: selection,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get document selection:', 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 selection',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user