feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { AIGatewayService, ChatOptions } from '../ai/ai-gateway.service';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
@Injectable()
|
||||
export class SandboxService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private aiGateway: AIGatewayService,
|
||||
) {}
|
||||
|
||||
async chat(userId: number, conversationId: string | undefined, model: string, messages: { role: string; content: string }[], options?: ChatOptions) {
|
||||
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
if (!user || user.status !== 'ACTIVE') {
|
||||
throw new HttpException('用户不可用', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const convId = conversationId || randomUUID();
|
||||
|
||||
// 今日配额:按 conversationId 去重计数
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const existing = await this.prisma.sandboxSession.findUnique({
|
||||
where: { userId_conversationId: { userId, conversationId: convId } },
|
||||
});
|
||||
if (!existing) {
|
||||
const todayCount = await this.prisma.sandboxSession.count({
|
||||
where: { userId, createdAt: { gte: today } },
|
||||
});
|
||||
if (todayCount >= (user.sandboxDaily || 10)) {
|
||||
throw new HttpException('今日沙箱使用次数已用完', HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
}
|
||||
|
||||
let reply = await this.aiGateway.chat(model, messages as any, options);
|
||||
if (typeof reply !== 'string') {
|
||||
reply = '抱歉,AI 返回了无效的回复,请重试。';
|
||||
}
|
||||
|
||||
const allMessages = messages.concat({ role: 'assistant', content: reply });
|
||||
const firstUserMsg = messages.find(m => m.role === 'user');
|
||||
const title = firstUserMsg ? firstUserMsg.content.slice(0, 80) : 'AI 对话';
|
||||
|
||||
const session = await this.prisma.sandboxSession.upsert({
|
||||
where: { userId_conversationId: { userId, conversationId: convId } },
|
||||
create: {
|
||||
userId,
|
||||
conversationId: convId,
|
||||
model,
|
||||
title,
|
||||
messages: JSON.stringify(allMessages),
|
||||
tokens: Math.ceil(reply.length / 2),
|
||||
},
|
||||
update: {
|
||||
model,
|
||||
title,
|
||||
messages: JSON.stringify(allMessages),
|
||||
tokens: Math.ceil(reply.length / 2),
|
||||
},
|
||||
});
|
||||
|
||||
return { reply, conversationId: convId, sessionId: session.id };
|
||||
}
|
||||
|
||||
async getSessions(userId: number, params: { page?: number; pageSize?: number; search?: string }) {
|
||||
const page = Number(params.page ?? 1);
|
||||
const pageSize = Number(params.pageSize ?? 50);
|
||||
const where: any = { userId };
|
||||
if (params.search) {
|
||||
where.title = { contains: params.search };
|
||||
}
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.sandboxSession.findMany({
|
||||
where,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { id: true, conversationId: true, model: true, title: true, feedback: true, createdAt: true, tokens: true },
|
||||
}),
|
||||
this.prisma.sandboxSession.count({ where }),
|
||||
]);
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async getSession(userId: number, id: number) {
|
||||
const session = await this.prisma.sandboxSession.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
if (!session) {
|
||||
throw new HttpException('会话不存在', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return {
|
||||
id: session.id,
|
||||
conversationId: session.conversationId,
|
||||
model: session.model,
|
||||
title: session.title,
|
||||
feedback: session.feedback,
|
||||
createdAt: session.createdAt,
|
||||
messages: JSON.parse(session.messages),
|
||||
};
|
||||
}
|
||||
|
||||
async setFeedback(userId: number, id: number, feedback: string | null) {
|
||||
const session = await this.prisma.sandboxSession.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
if (!session) {
|
||||
throw new HttpException('会话不存在', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
await this.prisma.sandboxSession.update({
|
||||
where: { id },
|
||||
data: { feedback: feedback || null },
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async deleteSession(userId: number, id: number) {
|
||||
const session = await this.prisma.sandboxSession.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
if (!session) {
|
||||
throw new HttpException('会话不存在', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
await this.prisma.sandboxSession.delete({ where: { id } });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getHistory(userId: number, params: { page?: number; pageSize?: number }) {
|
||||
const page = Number(params.page ?? 1);
|
||||
const pageSize = Number(params.pageSize ?? 20);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.sandboxSession.findMany({
|
||||
where: { userId },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { id: true, conversationId: true, model: true, title: true, feedback: true, createdAt: true, tokens: true },
|
||||
}),
|
||||
this.prisma.sandboxSession.count({ where: { userId } }),
|
||||
]);
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async getQuota(userId: number) {
|
||||
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const used = await this.prisma.sandboxSession.count({
|
||||
where: { userId, createdAt: { gte: today } },
|
||||
});
|
||||
return { dailyLimit: user?.sandboxDaily || 10, used, remaining: (user?.sandboxDaily || 10) - used };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user