From f20bdc0d7abce07a777e2291e39fb37596e39710 Mon Sep 17 00:00:00 2001 From: WangDL Date: Fri, 22 May 2026 10:43:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20conversation=20management=20?= =?UTF-8?q?=E2=80=94=20sessionId=20+=20X-Hermes-Session-Id=20+=20CRUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 18 ++++++++ prisma/schema.prisma | 16 +++++++ src/app.module.ts | 2 + .../admin-ai-chat/admin-ai-chat.controller.ts | 10 ++-- .../admin-ai-chat/admin-ai-chat.module.ts | 2 + .../admin-ai-chat/admin-ai-chat.service.ts | 35 ++++++++++---- src/modules/admin-ai-chat/dto/ai-chat.dto.ts | 9 ++-- .../admin-conversation.controller.ts | 35 ++++++++++++++ .../admin-conversation.module.ts | 11 +++++ .../admin-conversation.service.ts | 46 +++++++++++++++++++ src/modules/admin-conversation/dto/index.ts | 7 +++ 11 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 prisma/migrations/20260522103602_add_admin_conversation/migration.sql create mode 100644 src/modules/admin-conversation/admin-conversation.controller.ts create mode 100644 src/modules/admin-conversation/admin-conversation.module.ts create mode 100644 src/modules/admin-conversation/admin-conversation.service.ts create mode 100644 src/modules/admin-conversation/dto/index.ts diff --git a/prisma/migrations/20260522103602_add_admin_conversation/migration.sql b/prisma/migrations/20260522103602_add_admin_conversation/migration.sql new file mode 100644 index 0000000..0d2150f --- /dev/null +++ b/prisma/migrations/20260522103602_add_admin_conversation/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE `AdminConversation` ( + `id` VARCHAR(191) NOT NULL, + `adminUserId` VARCHAR(191) NOT NULL, + `title` VARCHAR(200) NOT NULL DEFAULT '新对话', + `hermesSessionId` VARCHAR(64) NOT NULL, + `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; + +-- AddForeignKey +ALTER TABLE `AdminConversation` ADD CONSTRAINT `AdminConversation_adminUserId_fkey` FOREIGN KEY (`adminUserId`) REFERENCES `AdminUser`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 50fb85a..352a440 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -732,6 +732,7 @@ model AdminUser { deletedAt DateTime? sessions AdminSession[] + conversations AdminConversation[] auditLogs AdminAuditLog[] @@index([email]) @@ -793,3 +794,18 @@ model MembershipPlan { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model AdminConversation { + id String @id @default(cuid()) + adminUserId String + title String @default("新对话") @db.VarChar(200) + hermesSessionId String @unique @db.VarChar(64) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + adminUser AdminUser @relation(fields: [adminUserId], references: [id]) + + @@index([adminUserId]) + @@index([hermesSessionId]) +} diff --git a/src/app.module.ts b/src/app.module.ts index bd7a120..960b69c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,6 +15,7 @@ import { AuthModule } from './modules/auth/auth.module'; import { AdminAuthModule } from './modules/admin-auth/admin-auth.module'; import { AdminDashboardModule } from './modules/admin-dashboard/admin-dashboard.module'; import { AdminUsersModule } from './modules/admin-users/admin-users.module'; +import { AdminConversationModule } from './modules/admin-conversation/admin-conversation.module'; import { AdminAiChatModule } from './modules/admin-ai-chat/admin-ai-chat.module'; import { AdminAuditLogModule } from './modules/admin-audit-log/admin-audit-log.module'; import { UsersModule } from './modules/users/users.module'; @@ -88,6 +89,7 @@ import appleConfig from './config/apple.config'; AdminAuthModule, AdminDashboardModule, AdminUsersModule, + AdminConversationModule, AdminAiChatModule, AdminAuditLogModule, UsersModule, diff --git a/src/modules/admin-ai-chat/admin-ai-chat.controller.ts b/src/modules/admin-ai-chat/admin-ai-chat.controller.ts index c7c3848..6d0c990 100644 --- a/src/modules/admin-ai-chat/admin-ai-chat.controller.ts +++ b/src/modules/admin-ai-chat/admin-ai-chat.controller.ts @@ -1,5 +1,5 @@ -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; -import { Controller, Post, Get, Body, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { Controller, Post, Get, Body, Req, UseGuards } from '@nestjs/common'; import { AdminAiChatService } from './admin-ai-chat.service'; import { AiChatDto } from './dto/ai-chat.dto'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; @@ -17,8 +17,8 @@ export class AdminAiChatController { @AdminRoles('SUPER_ADMIN' as AdminRole) @ApiBearerAuth() @ApiOperation({ summary: 'AI 对话(仅超级管理员)' }) - async chat(@Body() dto: AiChatDto) { - return this.aiChatService.chat(dto); + async chat(@Body() dto: AiChatDto, @Req() req: any) { + return this.aiChatService.chat(dto, req.adminUser.id); } @Get('dashboard') @@ -28,4 +28,4 @@ export class AdminAiChatController { getDashboard() { return this.aiChatService.getDashboardConfig(); } -} \ No newline at end of file +} diff --git a/src/modules/admin-ai-chat/admin-ai-chat.module.ts b/src/modules/admin-ai-chat/admin-ai-chat.module.ts index e7a3c55..0bc9983 100644 --- a/src/modules/admin-ai-chat/admin-ai-chat.module.ts +++ b/src/modules/admin-ai-chat/admin-ai-chat.module.ts @@ -3,8 +3,10 @@ import { AdminAiChatController } from './admin-ai-chat.controller'; import { AdminAiChatService } from './admin-ai-chat.service'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; +import { AdminConversationModule } from '../admin-conversation/admin-conversation.module'; @Module({ + imports: [AdminConversationModule], controllers: [AdminAiChatController], providers: [AdminAiChatService, AdminAuthGuard, AdminRolesGuard], }) 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 8a5cb85..f85ebc8 100644 --- a/src/modules/admin-ai-chat/admin-ai-chat.service.ts +++ b/src/modules/admin-ai-chat/admin-ai-chat.service.ts @@ -1,5 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import type { AiChatDto } from './dto/ai-chat.dto'; +import { AdminConversationService } from '../admin-conversation/admin-conversation.service'; const HERMES_API_URL = 'http://10.2.0.7:8642/v1/chat/completions'; const HERMES_API_KEY = 'zhixi-hermes-key-2026'; @@ -8,20 +9,38 @@ const HERMES_API_KEY = 'zhixi-hermes-key-2026'; export class AdminAiChatService { private readonly logger = new Logger(AdminAiChatService.name); - constructor() {} + constructor(private readonly conversationService: AdminConversationService) {} - async chat(dto: AiChatDto) { - return await this.callHermes(dto.messages); + async chat(dto: AiChatDto, adminUserId: string) { + const sessionId = dto.conversationId + ? await this.conversationService.getSessionId(dto.conversationId, adminUserId) + : null; + + // Auto-create conversation if none provided + const conversationId = dto.conversationId + ?? (await this.conversationService.create(adminUserId)).id; + + const result = await this.callHermes(dto.messages, sessionId); + + return { ...result, conversationId }; } - private async callHermes(messages: Array<{ role: string; content: string }>) { + private async callHermes( + messages: Array<{ role: string; content: string }>, + sessionId: string | null, + ) { const start = Date.now(); + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + HERMES_API_KEY, + }; + if (sessionId) { + headers['X-Hermes-Session-Id'] = sessionId; + } + const resp = await fetch(HERMES_API_URL, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + HERMES_API_KEY, - }, + headers, body: JSON.stringify({ model: 'hermes-agent', messages, diff --git a/src/modules/admin-ai-chat/dto/ai-chat.dto.ts b/src/modules/admin-ai-chat/dto/ai-chat.dto.ts index aca8768..2b26d77 100644 --- a/src/modules/admin-ai-chat/dto/ai-chat.dto.ts +++ b/src/modules/admin-ai-chat/dto/ai-chat.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsArray, ValidateNested, IsOptional, IsIn, MinLength } from 'class-validator'; +import { IsString, IsArray, ValidateNested, IsOptional, IsIn, MinLength } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; @@ -21,7 +21,8 @@ export class AiChatDto { @Type(() => ChatMessageDto) messages: ChatMessageDto[]; - @ApiProperty({ required: false, default: false }) + @ApiProperty({ required: false }) @IsOptional() - stream?: boolean; -} \ No newline at end of file + @IsString() + conversationId?: string; +} diff --git a/src/modules/admin-conversation/admin-conversation.controller.ts b/src/modules/admin-conversation/admin-conversation.controller.ts new file mode 100644 index 0000000..2c55c5f --- /dev/null +++ b/src/modules/admin-conversation/admin-conversation.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, Req, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { AdminConversationService } from './admin-conversation.service'; +import { CreateConversationDto, UpdateConversationDto } from './dto'; +import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; + +@ApiTags('admin-conversation') +@Controller('admin-api/conversations') +@UseGuards(AdminAuthGuard) +@ApiBearerAuth() +export class AdminConversationController { + constructor(private readonly svc: AdminConversationService) {} + + @Get() + async list(@Req() req: any) { + return this.svc.list(req.adminUser.id); + } + + @Post() + async create(@Req() req: any, @Body() dto: CreateConversationDto) { + return this.svc.create(req.adminUser.id, dto.title); + } + + @Patch(':id') + async update(@Req() req: any, @Param('id') id: string, @Body() dto: UpdateConversationDto) { + await this.svc.update(id, req.adminUser.id, dto.title!); + return { success: true }; + } + + @Delete(':id') + async delete(@Req() req: any, @Param('id') id: string) { + await this.svc.delete(id, req.adminUser.id); + return { success: true }; + } +} diff --git a/src/modules/admin-conversation/admin-conversation.module.ts b/src/modules/admin-conversation/admin-conversation.module.ts new file mode 100644 index 0000000..2aa28e1 --- /dev/null +++ b/src/modules/admin-conversation/admin-conversation.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AdminConversationController } from './admin-conversation.controller'; +import { AdminConversationService } from './admin-conversation.service'; +import { PrismaService } from '../../infrastructure/database/prisma.service'; + +@Module({ + controllers: [AdminConversationController], + providers: [AdminConversationService, PrismaService], + exports: [AdminConversationService], +}) +export class AdminConversationModule {} diff --git a/src/modules/admin-conversation/admin-conversation.service.ts b/src/modules/admin-conversation/admin-conversation.service.ts new file mode 100644 index 0000000..85ee780 --- /dev/null +++ b/src/modules/admin-conversation/admin-conversation.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { randomUUID } from 'crypto'; + +@Injectable() +export class AdminConversationService { + constructor(private readonly prisma: PrismaService) {} + + async list(adminUserId: string) { + return this.prisma.adminConversation.findMany({ + where: { adminUserId, deletedAt: null }, + orderBy: { updatedAt: 'desc' }, + select: { id: true, title: true, createdAt: true, updatedAt: true }, + }); + } + + async create(adminUserId: string, title?: string) { + const hermesSessionId = randomUUID(); + return this.prisma.adminConversation.create({ + data: { adminUserId, hermesSessionId, title: title || '新对话' }, + select: { id: true, title: true, hermesSessionId: true, createdAt: true }, + }); + } + + async update(id: string, adminUserId: string, title: string) { + return this.prisma.adminConversation.updateMany({ + where: { id, adminUserId, deletedAt: null }, + data: { title }, + }); + } + + async delete(id: string, adminUserId: string) { + return this.prisma.adminConversation.updateMany({ + where: { id, adminUserId, deletedAt: null }, + data: { deletedAt: new Date() }, + }); + } + + async getSessionId(conversationId: string, adminUserId: string) { + const conv = await this.prisma.adminConversation.findFirst({ + where: { id: conversationId, adminUserId, deletedAt: null }, + select: { id: true, hermesSessionId: true }, + }); + return conv?.hermesSessionId ?? null; + } +} diff --git a/src/modules/admin-conversation/dto/index.ts b/src/modules/admin-conversation/dto/index.ts new file mode 100644 index 0000000..fb1be46 --- /dev/null +++ b/src/modules/admin-conversation/dto/index.ts @@ -0,0 +1,7 @@ +export class CreateConversationDto { + title?: string; +} + +export class UpdateConversationDto { + title?: string; +}