feat: add AdminMessage persistence + conversation title auto-set + messages API
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 21s

This commit is contained in:
WangDL 2026-05-22 11:03:24 +08:00
parent 73e52d2201
commit f2d3f3f13f
5 changed files with 81 additions and 4 deletions

View File

@ -7,12 +7,24 @@ CREATE TABLE `AdminConversation` (
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL, `updatedAt` DATETIME(3) NOT NULL,
`deletedAt` DATETIME(3) NULL, `deletedAt` DATETIME(3) NULL,
UNIQUE INDEX `AdminConversation_hermesSessionId_key`(`hermesSessionId`), UNIQUE INDEX `AdminConversation_hermesSessionId_key`(`hermesSessionId`),
INDEX `AdminConversation_adminUserId_idx`(`adminUserId`), INDEX `AdminConversation_adminUserId_idx`(`adminUserId`),
INDEX `AdminConversation_hermesSessionId_idx`(`hermesSessionId`), INDEX `AdminConversation_hermesSessionId_idx`(`hermesSessionId`),
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) 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 -- AddForeignKey
ALTER TABLE `AdminConversation` ADD CONSTRAINT `AdminConversation_adminUserId_fkey` FOREIGN KEY (`adminUserId`) REFERENCES `AdminUser`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 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;

View File

@ -803,9 +803,23 @@ model AdminConversation {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime? deletedAt DateTime?
messages AdminMessage[]
adminUser AdminUser @relation(fields: [adminUserId], references: [id]) adminUser AdminUser @relation(fields: [adminUserId], references: [id])
@@index([adminUserId]) @@index([adminUserId])
@@index([hermesSessionId]) @@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])
}

View File

@ -20,8 +20,17 @@ export class AdminAiChatService {
const conversationId = dto.conversationId const conversationId = dto.conversationId
?? (await this.conversationService.create(adminUserId)).id; ?? (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); const result = await this.callHermes(dto.messages, sessionId);
// Save assistant reply
await this.conversationService.saveMessage(conversationId, 'assistant', result.content);
return { ...result, conversationId }; return { ...result, conversationId };
} }

View File

@ -1,5 +1,5 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Req, UseGuards } from '@nestjs/common'; 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 { AdminConversationService } from './admin-conversation.service';
import { CreateConversationDto, UpdateConversationDto } from './dto'; import { CreateConversationDto, UpdateConversationDto } from './dto';
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
@ -16,6 +16,12 @@ export class AdminConversationController {
return this.svc.list(req.adminUser.id); 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() @Post()
async create(@Req() req: any, @Body() dto: CreateConversationDto) { async create(@Req() req: any, @Body() dto: CreateConversationDto) {
return this.svc.create(req.adminUser.id, dto.title); return this.svc.create(req.adminUser.id, dto.title);
@ -23,7 +29,7 @@ export class AdminConversationController {
@Patch(':id') @Patch(':id')
async update(@Req() req: any, @Param('id') id: string, @Body() dto: UpdateConversationDto) { 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 }; return { success: true };
} }

View File

@ -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) { async create(adminUserId: string, title?: string) {
const hermesSessionId = randomUUID(); const hermesSessionId = randomUUID();
return this.prisma.adminConversation.create({ 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({ return this.prisma.adminConversation.updateMany({
where: { id, adminUserId, deletedAt: null }, where: { id, adminUserId, deletedAt: null },
data: { title }, 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) { async getSessionId(conversationId: string, adminUserId: string) {
const conv = await this.prisma.adminConversation.findFirst({ const conv = await this.prisma.adminConversation.findFirst({
where: { id: conversationId, adminUserId, deletedAt: null }, where: { id: conversationId, adminUserId, deletedAt: null },