feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径

This commit is contained in:
yuzhiran-dev
2026-05-18 09:48:51 +08:00
commit 11bb86854c
277 changed files with 37755 additions and 0 deletions
@@ -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 };
}
}