diff --git a/prisma/migrations/20260522103602_add_admin_conversation/migration.sql b/prisma/migrations/20260522110000_add_conversation_and_message/migration.sql similarity index 57% rename from prisma/migrations/20260522103602_add_admin_conversation/migration.sql rename to prisma/migrations/20260522110000_add_conversation_and_message/migration.sql index 0d2150f..7de2e52 100644 --- a/prisma/migrations/20260522103602_add_admin_conversation/migration.sql +++ b/prisma/migrations/20260522110000_add_conversation_and_message/migration.sql @@ -7,12 +7,24 @@ CREATE TABLE `AdminConversation` ( `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `updatedAt` DATETIME(3) NOT NULL, `deletedAt` DATETIME(3) NULL, - UNIQUE INDEX `AdminConversation_hermesSessionId_key`(`hermesSessionId`), INDEX `AdminConversation_adminUserId_idx`(`adminUserId`), INDEX `AdminConversation_hermesSessionId_idx`(`hermesSessionId`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +-- CreateTable +CREATE TABLE `AdminMessage` ( + `id` VARCHAR(191) NOT NULL, + `conversationId` VARCHAR(191) NOT NULL, + `role` VARCHAR(16) NOT NULL, + `content` LONGTEXT NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + INDEX `AdminMessage_conversationId_idx`(`conversationId`), + INDEX `AdminMessage_createdAt_idx`(`createdAt`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + -- AddForeignKey ALTER TABLE `AdminConversation` ADD CONSTRAINT `AdminConversation_adminUserId_fkey` FOREIGN KEY (`adminUserId`) REFERENCES `AdminUser`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE `AdminMessage` ADD CONSTRAINT `AdminMessage_conversationId_fkey` FOREIGN KEY (`conversationId`) REFERENCES `AdminConversation`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 352a440..e8a07c3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -803,9 +803,23 @@ model AdminConversation { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? + messages AdminMessage[] adminUser AdminUser @relation(fields: [adminUserId], references: [id]) @@index([adminUserId]) @@index([hermesSessionId]) } + +model AdminMessage { + id String @id @default(cuid()) + conversationId String + role String @db.VarChar(16) + content String @db.LongText + createdAt DateTime @default(now()) + + conversation AdminConversation @relation(fields: [conversationId], references: [id]) + + @@index([conversationId]) + @@index([createdAt]) +} diff --git a/src/modules/admin-ai-chat/admin-ai-chat.service.ts b/src/modules/admin-ai-chat/admin-ai-chat.service.ts index f85ebc8..6385c7a 100644 --- a/src/modules/admin-ai-chat/admin-ai-chat.service.ts +++ b/src/modules/admin-ai-chat/admin-ai-chat.service.ts @@ -20,8 +20,17 @@ export class AdminAiChatService { const conversationId = dto.conversationId ?? (await this.conversationService.create(adminUserId)).id; + // Save user message + const userMsg = dto.messages[dto.messages.length - 1]; + if (userMsg && userMsg.role === 'user') { + await this.conversationService.saveMessage(conversationId, 'user', userMsg.content); + } + const result = await this.callHermes(dto.messages, sessionId); + // Save assistant reply + await this.conversationService.saveMessage(conversationId, 'assistant', result.content); + return { ...result, conversationId }; } diff --git a/src/modules/admin-conversation/admin-conversation.controller.ts b/src/modules/admin-conversation/admin-conversation.controller.ts index 2c55c5f..da0d294 100644 --- a/src/modules/admin-conversation/admin-conversation.controller.ts +++ b/src/modules/admin-conversation/admin-conversation.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Post, Patch, Delete, Param, Body, Req, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { AdminConversationService } from './admin-conversation.service'; import { CreateConversationDto, UpdateConversationDto } from './dto'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; @@ -16,6 +16,12 @@ export class AdminConversationController { return this.svc.list(req.adminUser.id); } + @Get(':id/messages') + @ApiOperation({ summary: '获取对话的历史消息' }) + async messages(@Req() req: any, @Param('id') id: string) { + return this.svc.getMessages(id, req.adminUser.id); + } + @Post() async create(@Req() req: any, @Body() dto: CreateConversationDto) { return this.svc.create(req.adminUser.id, dto.title); @@ -23,7 +29,7 @@ export class AdminConversationController { @Patch(':id') async update(@Req() req: any, @Param('id') id: string, @Body() dto: UpdateConversationDto) { - await this.svc.update(id, req.adminUser.id, dto.title!); + await this.svc.updateTitle(id, req.adminUser.id, dto.title); return { success: true }; } diff --git a/src/modules/admin-conversation/admin-conversation.service.ts b/src/modules/admin-conversation/admin-conversation.service.ts index 85ee780..fef59f1 100644 --- a/src/modules/admin-conversation/admin-conversation.service.ts +++ b/src/modules/admin-conversation/admin-conversation.service.ts @@ -14,6 +14,19 @@ export class AdminConversationService { }); } + async getMessages(conversationId: string, adminUserId: string) { + const conv = await this.prisma.adminConversation.findFirst({ + where: { id: conversationId, adminUserId, deletedAt: null }, + }); + if (!conv) return []; + + return this.prisma.adminMessage.findMany({ + where: { conversationId }, + orderBy: { createdAt: 'asc' }, + select: { id: true, role: true, content: true, createdAt: true }, + }); + } + async create(adminUserId: string, title?: string) { const hermesSessionId = randomUUID(); return this.prisma.adminConversation.create({ @@ -22,7 +35,7 @@ export class AdminConversationService { }); } - async update(id: string, adminUserId: string, title: string) { + async updateTitle(id: string, adminUserId: string, title: string) { return this.prisma.adminConversation.updateMany({ where: { id, adminUserId, deletedAt: null }, data: { title }, @@ -36,6 +49,29 @@ export class AdminConversationService { }); } + async saveMessage(conversationId: string, role: string, content: string) { + await this.prisma.adminMessage.create({ + data: { conversationId, role, content }, + }); + // Update conversation timestamp and auto-set title from first user message + if (role === 'user') { + const conv = await this.prisma.adminConversation.findUnique({ + where: { id: conversationId }, + select: { title: true, _count: { select: { messages: true } } }, + }); + // Auto-title: use first user message (truncated) + const isFirstMessage = (conv?._count?.messages ?? 0) <= 1; + const updateData: any = { updatedAt: new Date() }; + if (isFirstMessage && conv?.title === '新对话') { + updateData.title = content.slice(0, 40); + } + await this.prisma.adminConversation.update({ + where: { id: conversationId }, + data: updateData, + }); + } + } + async getSessionId(conversationId: string, adminUserId: string) { const conv = await this.prisma.adminConversation.findFirst({ where: { id: conversationId, adminUserId, deletedAt: null },