refactor(backend): incremental architecture evolution (Task 19)

- Add common/ layer for shared capabilities (LLM, RAG, document, middleware)
- Add legacy/ layer for existing business code
- Move files to new structure (controllers, routes, services)
- Update index.ts for new route registration
- System remains fully functional
This commit is contained in:
2025-11-16 15:42:44 +08:00
parent 8a17dc80ae
commit 0c5310fb77
39 changed files with 3904 additions and 353 deletions

View 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 || '获取模板失败',
});
}
}