feat: add general chat feature (without project/agent concept)
This commit is contained in:
303
backend/src/controllers/chatController.ts
Normal file
303
backend/src/controllers/chatController.ts
Normal 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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
14
backend/src/routes/chatRoutes.ts
Normal file
14
backend/src/routes/chatRoutes.ts
Normal 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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user