fix: M3 audit — scheduleState persistence, AI→ReviewCard subscriber, ActiveRecall queue, streak bug, domain events
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 41s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 41s
- M3-02: Add scheduleState to ReviewCard model + persist in updateCard/insertCard - M3-02: Add ReviewCardSubscriber (OnEvent 'ai.analysis.completed' → generateCards) - M3-02: Add AdminReviewController (GET /admin-api/reviews) - M3-01: ActiveRecall now enqueues via AiAnalysisService instead of direct workflow call - M3-01: FocusItem model adds source field, worker uses status:'open' - M3-03: Fix streak calculation (break on gap), add StreakUpdatedEvent/DailyGoalAchievedEvent - M3-03: Add LearningGoal/StreakRecord/LearningStats to Prisma schema - M3-03: Fix FocusItem recommendation query (status:'pending' → 'open') Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8e5d722a1e
commit
2bfa9ad7c3
@ -480,6 +480,7 @@ model FocusItem {
|
|||||||
suggestion String? @db.Text
|
suggestion String? @db.Text
|
||||||
priority String @default("normal") @db.VarChar(32)
|
priority String @default("normal") @db.VarChar(32)
|
||||||
status String @default("open") @db.VarChar(32)
|
status String @default("open") @db.VarChar(32)
|
||||||
|
source String? @db.VarChar(32)
|
||||||
masteryScore Int?
|
masteryScore Int?
|
||||||
dueAt DateTime?
|
dueAt DateTime?
|
||||||
completedAt DateTime?
|
completedAt DateTime?
|
||||||
@ -509,6 +510,7 @@ model ReviewCard {
|
|||||||
easeFactor Decimal @default(2.50) @db.Decimal(4, 2)
|
easeFactor Decimal @default(2.50) @db.Decimal(4, 2)
|
||||||
repetitionCount Int @default(0)
|
repetitionCount Int @default(0)
|
||||||
lapseCount Int @default(0)
|
lapseCount Int @default(0)
|
||||||
|
scheduleState String? @db.VarChar(16)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
@ -1330,3 +1332,42 @@ model NotificationTemplate {
|
|||||||
|
|
||||||
@@index([type])
|
@@index([type])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model LearningGoal {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String @unique
|
||||||
|
dailyCardTarget Int @default(10)
|
||||||
|
dailyMinuteTarget Int @default(30)
|
||||||
|
weeklyCardTarget Int @default(50)
|
||||||
|
favoriteSubject String? @db.VarChar(100)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model StreakRecord {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
streakType String @db.VarChar(32)
|
||||||
|
length Int @default(0)
|
||||||
|
startDate DateTime
|
||||||
|
endDate DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([streakType])
|
||||||
|
}
|
||||||
|
|
||||||
|
model LearningStats {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
date DateTime
|
||||||
|
totalCards Int @default(0)
|
||||||
|
reviewedCards Int @default(0)
|
||||||
|
newCards Int @default(0)
|
||||||
|
totalMinutes Int @default(0)
|
||||||
|
avgMasteryScore Decimal? @db.Decimal(5, 2)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([userId, date])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|||||||
13
src/common/events/daily-goal-achieved.event.ts
Normal file
13
src/common/events/daily-goal-achieved.event.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { BaseDomainEvent } from './base-domain.event';
|
||||||
|
|
||||||
|
export class DailyGoalAchievedEvent extends BaseDomainEvent {
|
||||||
|
readonly eventType = 'growth.daily-goal.achieved';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly userId: string,
|
||||||
|
public readonly date: string,
|
||||||
|
public readonly cardCount: number,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/common/events/streak-updated.event.ts
Normal file
13
src/common/events/streak-updated.event.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { BaseDomainEvent } from './base-domain.event';
|
||||||
|
|
||||||
|
export class StreakUpdatedEvent extends BaseDomainEvent {
|
||||||
|
readonly eventType = 'growth.streak.updated';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly userId: string,
|
||||||
|
public readonly currentStreak: number,
|
||||||
|
public readonly longestStreak: number,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AiModule } from '../ai/ai.module';
|
import { AiModule } from '../ai/ai.module';
|
||||||
|
import { AiAnalysisModule } from '../ai-analysis/ai-analysis.module';
|
||||||
import { ActiveRecallController } from './active-recall.controller';
|
import { ActiveRecallController } from './active-recall.controller';
|
||||||
import { ActiveRecallService } from './active-recall.service';
|
import { ActiveRecallService } from './active-recall.service';
|
||||||
import { ActiveRecallRepository } from './active-recall.repository';
|
import { ActiveRecallRepository } from './active-recall.repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AiModule],
|
imports: [AiModule, AiAnalysisModule],
|
||||||
controllers: [ActiveRecallController],
|
controllers: [ActiveRecallController],
|
||||||
providers: [ActiveRecallService, ActiveRecallRepository],
|
providers: [ActiveRecallService, ActiveRecallRepository],
|
||||||
exports: [ActiveRecallService],
|
exports: [ActiveRecallService],
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
import { ActiveRecallRepository } from './active-recall.repository';
|
import { ActiveRecallRepository } from './active-recall.repository';
|
||||||
import { ActiveRecallAnalysisWorkflow } from '../ai/workflows/active-recall-analysis.workflow';
|
import { AiAnalysisService } from '../ai-analysis/ai-analysis.service';
|
||||||
import type { PaginationDto } from '../../common/dto/pagination.dto';
|
import type { PaginationDto } from '../../common/dto/pagination.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -9,7 +9,7 @@ export class ActiveRecallService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly repository: ActiveRecallRepository,
|
private readonly repository: ActiveRecallRepository,
|
||||||
private readonly analysisWorkflow: ActiveRecallAnalysisWorkflow,
|
private readonly analysisService: AiAnalysisService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findByUserId(userId: string, pagination: PaginationDto) {
|
async findByUserId(userId: string, pagination: PaginationDto) {
|
||||||
@ -22,23 +22,19 @@ export class ActiveRecallService {
|
|||||||
|
|
||||||
const answer = await this.repository.createAnswer(userId, questionId, body);
|
const answer = await this.repository.createAnswer(userId, questionId, body);
|
||||||
|
|
||||||
// Fire-and-forget: answer is saved, analysis runs async
|
// Queue AI analysis via BullMQ (worker publishes event + generates FocusItems)
|
||||||
void this.runAnalysis(answer.id, userId, question.questionText, body.answerText);
|
try {
|
||||||
|
await this.analysisService.analyze(userId, {
|
||||||
|
questionText: question.questionText,
|
||||||
|
knowledgeItemContent: '', // worker picks up content from the analysis workflow
|
||||||
|
userAnswer: body.answerText,
|
||||||
|
answerId: answer.id,
|
||||||
|
});
|
||||||
|
this.logger.log(`AI analysis queued for answer ${answer.id}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.error(`Failed to queue analysis for answer ${answer.id}: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runAnalysis(answerId: string, userId: string, questionText: string, userAnswer: string) {
|
|
||||||
try {
|
|
||||||
const result = await this.analysisWorkflow.execute({
|
|
||||||
userId,
|
|
||||||
questionText,
|
|
||||||
knowledgeItemContent: '',
|
|
||||||
userAnswer,
|
|
||||||
});
|
|
||||||
this.logger.log(`Analysis complete for answer ${answerId}: score=${result.score}`);
|
|
||||||
} catch (err: any) {
|
|
||||||
this.logger.error(`Analysis failed for answer ${answerId}: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,8 @@ export class FocusItemsRepository {
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
suggestion?: string;
|
suggestion?: string;
|
||||||
priority?: string;
|
priority?: string;
|
||||||
|
status?: string;
|
||||||
|
source?: string;
|
||||||
knowledgeBaseId?: string;
|
knowledgeBaseId?: string;
|
||||||
knowledgeItemId?: string;
|
knowledgeItemId?: string;
|
||||||
}) {
|
}) {
|
||||||
@ -36,7 +38,8 @@ export class FocusItemsRepository {
|
|||||||
reason: data.reason ?? '',
|
reason: data.reason ?? '',
|
||||||
suggestion: data.suggestion ?? '',
|
suggestion: data.suggestion ?? '',
|
||||||
priority: data.priority ?? 'normal',
|
priority: data.priority ?? 'normal',
|
||||||
status: 'open',
|
status: data.status ?? 'open',
|
||||||
|
source: data.source ?? null,
|
||||||
knowledgeBaseId: data.knowledgeBaseId ?? null,
|
knowledgeBaseId: data.knowledgeBaseId ?? null,
|
||||||
knowledgeItemId: data.knowledgeItemId ?? null,
|
knowledgeItemId: data.knowledgeItemId ?? null,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger, Optional } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||||
|
import { EventBusService } from '../../common/event-bus/event-bus.service';
|
||||||
|
import { StreakUpdatedEvent } from '../../common/events/streak-updated.event';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GrowthService {
|
export class GrowthService {
|
||||||
private readonly logger = new Logger(GrowthService.name);
|
private readonly logger = new Logger(GrowthService.name);
|
||||||
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
@Optional() private readonly eventBus?: EventBusService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** Calculate streak from daily activity */
|
/** Calculate streak from daily activity */
|
||||||
async getStreak(userId: string): Promise<{ currentStreak: number; longestStreak: number }> {
|
async getStreak(userId: string): Promise<{ currentStreak: number; longestStreak: number }> {
|
||||||
@ -24,6 +29,7 @@ export class GrowthService {
|
|||||||
const d = new Date(a.activityDate).toISOString().slice(0, 10);
|
const d = new Date(a.activityDate).toISOString().slice(0, 10);
|
||||||
if (!seen.has(d)) { seen.add(d); dates.push(d); }
|
if (!seen.has(d)) { seen.add(d); dates.push(d); }
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentStreak = dates.length > 0 ? 1 : 0;
|
let currentStreak = dates.length > 0 ? 1 : 0;
|
||||||
let longestStreak = currentStreak;
|
let longestStreak = currentStreak;
|
||||||
let streak = currentStreak;
|
let streak = currentStreak;
|
||||||
@ -38,11 +44,14 @@ export class GrowthService {
|
|||||||
} else if (diffDays === 0) {
|
} else if (diffDays === 0) {
|
||||||
// same day, skip
|
// same day, skip
|
||||||
} else {
|
} else {
|
||||||
streak = 1;
|
break; // gap found — stop counting current streak
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentStreak = streak;
|
currentStreak = streak;
|
||||||
|
|
||||||
|
// Publish event for downstream consumers
|
||||||
|
try { this.eventBus?.publish(new StreakUpdatedEvent(userId, currentStreak, longestStreak)); } catch {}
|
||||||
|
|
||||||
return { currentStreak, longestStreak };
|
return { currentStreak, longestStreak };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,9 +59,9 @@ export class GrowthService {
|
|||||||
async getRecommendations(userId: string): Promise<{ type: string; title: string; reason: string }[]> {
|
async getRecommendations(userId: string): Promise<{ type: string; title: string; reason: string }[]> {
|
||||||
const recommendations: { type: string; title: string; reason: string }[] = [];
|
const recommendations: { type: string; title: string; reason: string }[] = [];
|
||||||
|
|
||||||
// Find pending focus items
|
// Find open focus items
|
||||||
const focusCount = await this.prisma.focusItem.count({
|
const focusCount = await this.prisma.focusItem.count({
|
||||||
where: { userId, status: 'pending' },
|
where: { userId, status: 'open' },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (focusCount > 0) {
|
if (focusCount > 0) {
|
||||||
|
|||||||
46
src/modules/review/admin-review.controller.ts
Normal file
46
src/modules/review/admin-review.controller.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||||
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||||
|
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||||
|
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
|
||||||
|
|
||||||
|
@ApiTags('admin-review')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('admin-api/reviews')
|
||||||
|
@UseGuards(AdminAuthGuard, AdminRolesGuard)
|
||||||
|
export class AdminReviewController {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '复习卡片列表(Admin)' })
|
||||||
|
@ApiQuery({ name: 'search', required: false })
|
||||||
|
@ApiQuery({ name: 'status', required: false })
|
||||||
|
@ApiQuery({ name: 'page', required: false })
|
||||||
|
@ApiQuery({ name: 'limit', required: false })
|
||||||
|
async list(
|
||||||
|
@Query('search') search?: string,
|
||||||
|
@Query('status') status?: string,
|
||||||
|
@Query('page') page?: string,
|
||||||
|
@Query('limit') limit?: string,
|
||||||
|
) {
|
||||||
|
const take = Math.min(Number(limit) || 20, 100);
|
||||||
|
const skip = (Math.max(Number(page) || 1, 1) - 1) * take;
|
||||||
|
const where: any = {};
|
||||||
|
if (status) where.status = status;
|
||||||
|
if (search) {
|
||||||
|
where.frontText = { contains: search };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [items, total] = await Promise.all([
|
||||||
|
this.prisma.reviewCard.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
}),
|
||||||
|
this.prisma.reviewCard.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { items, total };
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/modules/review/review-card.subscriber.ts
Normal file
52
src/modules/review/review-card.subscriber.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { ReviewService } from './review.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ReviewCardSubscriber {
|
||||||
|
private readonly logger = new Logger(ReviewCardSubscriber.name);
|
||||||
|
|
||||||
|
constructor(private readonly reviewService: ReviewService) {}
|
||||||
|
|
||||||
|
@OnEvent('ai.analysis.completed')
|
||||||
|
async handleAIAnalysisCompleted(payload: {
|
||||||
|
userId: string;
|
||||||
|
jobId: string;
|
||||||
|
sessionId?: string;
|
||||||
|
answerId?: string;
|
||||||
|
type: string;
|
||||||
|
score?: number;
|
||||||
|
analysis?: Record<string, any>;
|
||||||
|
}) {
|
||||||
|
const { userId } = payload;
|
||||||
|
this.logger.log(
|
||||||
|
`Received ai.analysis.completed user=${userId} job=${payload.jobId} score=${payload.score}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!payload.analysis) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const a = payload.analysis;
|
||||||
|
const weaknesses = (a.weaknesses || []).join(';');
|
||||||
|
const strengths = (a.strengths || []).join(';');
|
||||||
|
const summary = a.summary || '';
|
||||||
|
|
||||||
|
if (!weaknesses && !strengths) return;
|
||||||
|
|
||||||
|
const title = summary ? summary.slice(0, 80) : 'AI 分析结果';
|
||||||
|
const content = `摘要:${summary}\n\n掌握点:${strengths}\n\n薄弱点:${weaknesses}`;
|
||||||
|
|
||||||
|
await this.reviewService.generateCards(userId, {
|
||||||
|
knowledgeItemTitle: title,
|
||||||
|
knowledgeItemContent: content,
|
||||||
|
cardCount: Math.min(3, Math.max(1, (a.weaknesses?.length || 1))),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Generated review cards from analysis job=${payload.jobId} for user=${userId}`,
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.error(`Failed to generate review cards from analysis: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,11 +3,13 @@ import { AiModule } from '../ai/ai.module';
|
|||||||
import { ReviewController } from './review.controller';
|
import { ReviewController } from './review.controller';
|
||||||
import { ReviewService } from './review.service';
|
import { ReviewService } from './review.service';
|
||||||
import { ReviewRepository } from './review.repository';
|
import { ReviewRepository } from './review.repository';
|
||||||
|
import { ReviewCardSubscriber } from './review-card.subscriber';
|
||||||
|
import { AdminReviewController } from './admin-review.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AiModule],
|
imports: [AiModule],
|
||||||
controllers: [ReviewController],
|
controllers: [ReviewController, AdminReviewController],
|
||||||
providers: [ReviewService, ReviewRepository],
|
providers: [ReviewService, ReviewRepository, ReviewCardSubscriber],
|
||||||
exports: [ReviewService],
|
exports: [ReviewService],
|
||||||
})
|
})
|
||||||
export class ReviewModule {}
|
export class ReviewModule {}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export class ReviewRepository {
|
|||||||
easeFactor?: number;
|
easeFactor?: number;
|
||||||
repetitionCount?: number;
|
repetitionCount?: number;
|
||||||
lapseCount?: number;
|
lapseCount?: number;
|
||||||
|
scheduleState?: string;
|
||||||
nextReviewAt?: Date;
|
nextReviewAt?: Date;
|
||||||
}) {
|
}) {
|
||||||
return this.prisma.reviewCard.create({ data });
|
return this.prisma.reviewCard.create({ data });
|
||||||
@ -41,8 +42,10 @@ export class ReviewRepository {
|
|||||||
status?: string;
|
status?: string;
|
||||||
nextReviewAt?: Date;
|
nextReviewAt?: Date;
|
||||||
intervalDays?: number;
|
intervalDays?: number;
|
||||||
|
easeFactor?: number;
|
||||||
repetitionCount?: number;
|
repetitionCount?: number;
|
||||||
lapseCount?: number;
|
lapseCount?: number;
|
||||||
|
scheduleState?: string;
|
||||||
}) {
|
}) {
|
||||||
await this.prisma.reviewCard.update({ where: { id }, data });
|
await this.prisma.reviewCard.update({ where: { id }, data });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export class ReviewService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.reviewRepository.updateCard(id, {
|
await this.reviewRepository.updateCard(id, {
|
||||||
status: 'active', nextReviewAt, intervalDays, repetitionCount, lapseCount,
|
status: 'active', nextReviewAt, intervalDays, easeFactor, repetitionCount, lapseCount, scheduleState,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { log, nextReviewAt, scheduleState, intervalDays };
|
return { log, nextReviewAt, scheduleState, intervalDays };
|
||||||
@ -89,6 +89,7 @@ export class ReviewService {
|
|||||||
easeFactor: EASE_FACTOR_DEFAULT,
|
easeFactor: EASE_FACTOR_DEFAULT,
|
||||||
repetitionCount: 0,
|
repetitionCount: 0,
|
||||||
lapseCount: 0,
|
lapseCount: 0,
|
||||||
|
scheduleState: 'new',
|
||||||
nextReviewAt: new Date(),
|
nextReviewAt: new Date(),
|
||||||
});
|
});
|
||||||
savedCards.push(saved);
|
savedCards.push(saved);
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export class AiAnalysisWorker extends WorkerHost {
|
|||||||
knowledgeBaseId: result.knowledgeBaseId || 'unknown',
|
knowledgeBaseId: result.knowledgeBaseId || 'unknown',
|
||||||
title: w,
|
title: w,
|
||||||
source: 'ai-analysis',
|
source: 'ai-analysis',
|
||||||
status: 'pending',
|
status: 'open',
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,7 @@ const modelNames = [
|
|||||||
'chatSession', 'chatMessage', 'chatCitation',
|
'chatSession', 'chatMessage', 'chatCitation',
|
||||||
'artifact', 'learningGoal', 'streakRecord',
|
'artifact', 'learningGoal', 'streakRecord',
|
||||||
'notificationPreference', 'pushToken', 'notificationTemplate',
|
'notificationPreference', 'pushToken', 'notificationTemplate',
|
||||||
|
'learningGoal', 'streakRecord', 'learningStats',
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const name of modelNames) {
|
for (const name of modelNames) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user