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?
|
deletedAt DateTime?
|
||||||
|
|
||||||
sessions AdminSession[]
|
sessions AdminSession[]
|
||||||
|
conversations AdminConversation[]
|
||||||
auditLogs AdminAuditLog[]
|
auditLogs AdminAuditLog[]
|
||||||
|
|
||||||
@@index([email])
|
@@index([email])
|
||||||
@ -793,3 +794,18 @@ model MembershipPlan {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
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 { AdminAuthModule } from './modules/admin-auth/admin-auth.module';
|
||||||
import { AdminDashboardModule } from './modules/admin-dashboard/admin-dashboard.module';
|
import { AdminDashboardModule } from './modules/admin-dashboard/admin-dashboard.module';
|
||||||
import { AdminUsersModule } from './modules/admin-users/admin-users.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 { AdminAiChatModule } from './modules/admin-ai-chat/admin-ai-chat.module';
|
||||||
import { AdminAuditLogModule } from './modules/admin-audit-log/admin-audit-log.module';
|
import { AdminAuditLogModule } from './modules/admin-audit-log/admin-audit-log.module';
|
||||||
import { UsersModule } from './modules/users/users.module';
|
import { UsersModule } from './modules/users/users.module';
|
||||||
@ -88,6 +89,7 @@ import appleConfig from './config/apple.config';
|
|||||||
AdminAuthModule,
|
AdminAuthModule,
|
||||||
AdminDashboardModule,
|
AdminDashboardModule,
|
||||||
AdminUsersModule,
|
AdminUsersModule,
|
||||||
|
AdminConversationModule,
|
||||||
AdminAiChatModule,
|
AdminAiChatModule,
|
||||||
AdminAuditLogModule,
|
AdminAuditLogModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { Controller, Post, Get, Body, UseGuards } from '@nestjs/common';
|
import { Controller, Post, Get, Body, Req, UseGuards } from '@nestjs/common';
|
||||||
import { AdminAiChatService } from './admin-ai-chat.service';
|
import { AdminAiChatService } from './admin-ai-chat.service';
|
||||||
import { AiChatDto } from './dto/ai-chat.dto';
|
import { AiChatDto } from './dto/ai-chat.dto';
|
||||||
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||||
@ -17,8 +17,8 @@ export class AdminAiChatController {
|
|||||||
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'AI 对话(仅超级管理员)' })
|
@ApiOperation({ summary: 'AI 对话(仅超级管理员)' })
|
||||||
async chat(@Body() dto: AiChatDto) {
|
async chat(@Body() dto: AiChatDto, @Req() req: any) {
|
||||||
return this.aiChatService.chat(dto);
|
return this.aiChatService.chat(dto, req.adminUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('dashboard')
|
@Get('dashboard')
|
||||||
|
|||||||
@ -3,8 +3,10 @@ import { AdminAiChatController } from './admin-ai-chat.controller';
|
|||||||
import { AdminAiChatService } from './admin-ai-chat.service';
|
import { AdminAiChatService } from './admin-ai-chat.service';
|
||||||
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||||
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
|
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
|
||||||
|
import { AdminConversationModule } from '../admin-conversation/admin-conversation.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [AdminConversationModule],
|
||||||
controllers: [AdminAiChatController],
|
controllers: [AdminAiChatController],
|
||||||
providers: [AdminAiChatService, AdminAuthGuard, AdminRolesGuard],
|
providers: [AdminAiChatService, AdminAuthGuard, AdminRolesGuard],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import type { AiChatDto } from './dto/ai-chat.dto';
|
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_URL = 'http://10.2.0.7:8642/v1/chat/completions';
|
||||||
const HERMES_API_KEY = 'zhixi-hermes-key-2026';
|
const HERMES_API_KEY = 'zhixi-hermes-key-2026';
|
||||||
@ -8,20 +9,38 @@ const HERMES_API_KEY = 'zhixi-hermes-key-2026';
|
|||||||
export class AdminAiChatService {
|
export class AdminAiChatService {
|
||||||
private readonly logger = new Logger(AdminAiChatService.name);
|
private readonly logger = new Logger(AdminAiChatService.name);
|
||||||
|
|
||||||
constructor() {}
|
constructor(private readonly conversationService: AdminConversationService) {}
|
||||||
|
|
||||||
async chat(dto: AiChatDto) {
|
async chat(dto: AiChatDto, adminUserId: string) {
|
||||||
return await this.callHermes(dto.messages);
|
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 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, {
|
const resp = await fetch(HERMES_API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: 'Bearer ' + HERMES_API_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'hermes-agent',
|
model: 'hermes-agent',
|
||||||
messages,
|
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 { Type } from 'class-transformer';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
@ -21,7 +21,8 @@ export class AiChatDto {
|
|||||||
@Type(() => ChatMessageDto)
|
@Type(() => ChatMessageDto)
|
||||||
messages: ChatMessageDto[];
|
messages: ChatMessageDto[];
|
||||||
|
|
||||||
@ApiProperty({ required: false, default: false })
|
@ApiProperty({ required: false })
|
||||||
@IsOptional()
|
@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