feat: add conversation management — sessionId + X-Hermes-Session-Id + CRUD
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 37s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 37s
This commit is contained in:
parent
3b42a8618a
commit
f20bdc0d7a
@ -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;
|
||||
@ -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])
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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<string, string> = {
|
||||
'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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@IsString()
|
||||
conversationId?: string;
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
11
src/modules/admin-conversation/admin-conversation.module.ts
Normal file
11
src/modules/admin-conversation/admin-conversation.module.ts
Normal file
@ -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 {}
|
||||
46
src/modules/admin-conversation/admin-conversation.service.ts
Normal file
46
src/modules/admin-conversation/admin-conversation.service.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
7
src/modules/admin-conversation/dto/index.ts
Normal file
7
src/modules/admin-conversation/dto/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class CreateConversationDto {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export class UpdateConversationDto {
|
||||
title?: string;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user