feat: add general chat feature (without project/agent concept)

This commit is contained in:
AI Clinical Dev Team
2025-10-12 09:48:49 +08:00
parent 07704817ba
commit 348af7fee9
9 changed files with 688 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
import { FastifyRequest, FastifyReply } from 'fastify';
import { ModelType } from '../adapters/types.js';
import { LLMFactory } from '../adapters/LLMFactory.js';
import * as knowledgeBaseService from '../services/knowledgeBaseService.js';
import { prisma } from '../config/database.js';
interface SendChatMessageBody {
content: string;
modelType: ModelType;
knowledgeBaseIds?: string[];
conversationId?: string; // 可选:续接已有对话
}
/**
* 通用聊天Controller
* 无需项目和智能体,纯大模型对话
*/
export class ChatController {
/**
* 发送消息(流式输出)
*/
async sendMessageStream(
request: FastifyRequest<{ Body: SendChatMessageBody }>,
reply: FastifyReply
) {
try {
// TODO: 从JWT token获取userId
const userId = 'user-mock-001';
const { content, modelType, knowledgeBaseIds, conversationId } = request.body;
console.log('💬 [ChatController] 收到通用对话请求', {
content,
modelType,
knowledgeBaseIds: knowledgeBaseIds || [],
conversationId,
});
// 验证modelType
if (modelType !== 'deepseek-v3' && modelType !== 'qwen3-72b' && modelType !== 'gemini-pro') {
reply.code(400).send({
success: false,
message: `不支持的模型类型: ${modelType}`,
});
return;
}
// 获取或创建对话记录
let conversation;
if (conversationId) {
// 验证对话是否存在且属于当前用户
conversation = await prisma.generalConversation.findFirst({
where: {
id: conversationId,
userId,
deletedAt: null,
},
});
if (!conversation) {
reply.code(404).send({
success: false,
message: '对话不存在',
});
return;
}
} else {
// 创建新对话
conversation = await prisma.generalConversation.create({
data: {
userId,
title: content.substring(0, 50), // 用第一条消息的前50字作为标题
modelName: modelType,
},
});
console.log('✅ [ChatController] 创建新对话', { conversationId: conversation.id });
}
// 检索知识库上下文
let knowledgeBaseContext = '';
if (knowledgeBaseIds && knowledgeBaseIds.length > 0) {
console.log('📚 [ChatController] 开始检索知识库');
const knowledgeResults: string[] = [];
for (const kbId of knowledgeBaseIds) {
try {
const searchResult = await knowledgeBaseService.searchKnowledgeBase(
userId,
kbId,
content,
3
);
if (searchResult.records && searchResult.records.length > 0) {
const kbInfo = await prisma.knowledgeBase.findUnique({
where: { id: kbId },
select: { name: true },
});
knowledgeResults.push(
`【知识库:${kbInfo?.name || '未命名'}\n` +
searchResult.records
.map((record: any, index: number) => {
const score = (record.score * 100).toFixed(1);
return `${index + 1}. [相关度${score}%] ${record.segment.content}`;
})
.join('\n\n')
);
}
} catch (error) {
console.error(`❌ [ChatController] 检索知识库失败 ${kbId}:`, error);
}
}
if (knowledgeResults.length > 0) {
knowledgeBaseContext = knowledgeResults.join('\n\n---\n\n');
console.log(`💾 [ChatController] 知识库上下文: ${knowledgeBaseContext.length} 字符`);
}
}
// 获取历史消息最近20条
const historyMessages = await prisma.generalMessage.findMany({
where: {
conversationId: conversation.id,
},
orderBy: {
createdAt: 'desc',
},
take: 20,
});
historyMessages.reverse();
console.log(`📜 [ChatController] 历史消息数: ${historyMessages.length}`);
// 组装消息上下文
const messages: any[] = [
{
role: 'system',
content: '你是一个专业、友好的AI助手。当用户提供参考资料时请优先基于参考资料回答。',
},
];
// 添加历史消息
for (const msg of historyMessages) {
messages.push({
role: msg.role,
content: msg.content,
});
}
// 添加当前用户消息
let userContent = content;
if (knowledgeBaseContext) {
userContent = `${content}\n\n## 参考资料(来自知识库)\n${knowledgeBaseContext}`;
}
messages.push({
role: 'user',
content: userContent,
});
// 设置SSE响应头
reply.raw.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
// 保存用户消息
await prisma.generalMessage.create({
data: {
conversationId: conversation.id,
role: 'user',
content,
metadata: {
knowledgeBaseIds,
},
},
});
// 流式输出
const adapter = LLMFactory.getAdapter(modelType);
let fullContent = '';
let usage: any = null;
for await (const chunk of adapter.chatStream(messages, {
temperature: 0.7,
maxTokens: 2000,
})) {
fullContent += chunk.content;
if (chunk.usage) {
usage = chunk.usage;
}
// 发送SSE数据
reply.raw.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
// 保存助手消息
await prisma.generalMessage.create({
data: {
conversationId: conversation.id,
role: 'assistant',
content: fullContent,
model: modelType,
tokens: usage?.totalTokens,
metadata: {
usage,
},
},
});
// 更新对话
await prisma.generalConversation.update({
where: { id: conversation.id },
data: {
updatedAt: new Date(),
},
});
// 发送完成信号
reply.raw.write(`data: [DONE]\n\n`);
reply.raw.end();
console.log('✅ [ChatController] 对话完成');
} catch (error: any) {
console.error('❌ [ChatController] 错误:', error);
reply.code(500).send({
success: false,
message: error.message || '服务器错误',
});
}
}
/**
* 获取对话列表
*/
async getConversations(
request: FastifyRequest,
reply: FastifyReply
) {
try {
const userId = 'user-mock-001';
const conversations = await prisma.generalConversation.findMany({
where: {
userId,
deletedAt: null,
},
orderBy: {
updatedAt: 'desc',
},
take: 50,
});
reply.send({
success: true,
data: conversations,
});
} catch (error: any) {
reply.code(500).send({
success: false,
message: error.message || '获取对话列表失败',
});
}
}
/**
* 删除对话
*/
async deleteConversation(
request: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
try {
const userId = 'user-mock-001';
const { id } = request.params;
await prisma.generalConversation.update({
where: {
id,
userId,
},
data: {
deletedAt: new Date(),
},
});
reply.send({
success: true,
message: '删除成功',
});
} catch (error: any) {
reply.code(500).send({
success: false,
message: error.message || '删除失败',
});
}
}
}
export const chatController = new ChatController();

View File

@@ -7,6 +7,7 @@ import { projectRoutes } from './routes/projects.js';
import { agentRoutes } from './routes/agents.js';
import { conversationRoutes } from './routes/conversations.js';
import knowledgeBaseRoutes from './routes/knowledgeBases.js';
import { chatRoutes } from './routes/chatRoutes.js';
// 全局处理BigInt序列化
@@ -89,6 +90,9 @@ await fastify.register(conversationRoutes, { prefix: '/api/v1' });
// 注册知识库管理路由
await fastify.register(knowledgeBaseRoutes, { prefix: '/api/v1' });
// 注册通用对话路由
await fastify.register(chatRoutes, { prefix: '/api/v1' });
// 启动服务器
const start = async () => {
try {

View File

@@ -0,0 +1,14 @@
import { FastifyInstance } from 'fastify';
import { chatController } from '../controllers/chatController.js';
export async function chatRoutes(fastify: FastifyInstance) {
// 发送消息(流式输出)
fastify.post('/chat/stream', chatController.sendMessageStream.bind(chatController));
// 获取对话列表
fastify.get('/chat/conversations', chatController.getConversations.bind(chatController));
// 删除对话
fastify.delete('/chat/conversations/:id', chatController.deleteConversation.bind(chatController));
}