feat: KnowledgeSource 和 ImportCandidate 模块
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 22s
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 22s
This commit is contained in:
parent
1e7e4268ab
commit
9c161db26b
@ -26,6 +26,8 @@ import { NotificationsModule } from './modules/notifications/notifications.modul
|
|||||||
import { FeedbackModule } from './modules/feedback/feedback.module';
|
import { FeedbackModule } from './modules/feedback/feedback.module';
|
||||||
import { FilesModule } from './modules/files/files.module';
|
import { FilesModule } from './modules/files/files.module';
|
||||||
import { WaitlistModule } from './modules/waitlist/waitlist.module';
|
import { WaitlistModule } from './modules/waitlist/waitlist.module';
|
||||||
|
import { KnowledgeSourceModule } from './modules/knowledge-source/knowledge-source.module';
|
||||||
|
import { ImportCandidateModule } from './modules/import-candidate/import-candidate.module';
|
||||||
|
|
||||||
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
||||||
import { RolesGuard } from './common/guards/roles.guard';
|
import { RolesGuard } from './common/guards/roles.guard';
|
||||||
@ -80,6 +82,8 @@ import appleConfig from './config/apple.config';
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
KnowledgeBaseModule,
|
KnowledgeBaseModule,
|
||||||
KnowledgeItemsModule,
|
KnowledgeItemsModule,
|
||||||
|
KnowledgeSourceModule,
|
||||||
|
ImportCandidateModule,
|
||||||
DocumentImportModule,
|
DocumentImportModule,
|
||||||
LearningSessionModule,
|
LearningSessionModule,
|
||||||
ActiveRecallModule,
|
ActiveRecallModule,
|
||||||
|
|||||||
45
src/modules/import-candidate/import-candidate.controller.ts
Normal file
45
src/modules/import-candidate/import-candidate.controller.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { ImportCandidateService } from './import-candidate.service';
|
||||||
|
|
||||||
|
@ApiTags('import-candidate')
|
||||||
|
@Controller()
|
||||||
|
export class ImportCandidateController {
|
||||||
|
constructor(private readonly service: ImportCandidateService) {}
|
||||||
|
|
||||||
|
@Get('knowledge-sources/:sourceId/import-candidates')
|
||||||
|
@ApiOperation({ summary: '获取资料来源的候选知识点' })
|
||||||
|
async findBySource(@Param('sourceId') sourceId: string) {
|
||||||
|
return this.service.findBySource(sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('import-candidates/:id')
|
||||||
|
@ApiOperation({ summary: '获取候选知识点详情' })
|
||||||
|
async findOne(@Param('id') id: string) {
|
||||||
|
return this.service.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('import-candidates/:id')
|
||||||
|
@ApiOperation({ summary: '编辑候选知识点' })
|
||||||
|
async update(@Param('id') id: string, @Body() dto: any) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('import-candidates/:id/accept')
|
||||||
|
@ApiOperation({ summary: '接受候选知识点 → 生成 KnowledgeItem' })
|
||||||
|
async accept(@Param('id') id: string) {
|
||||||
|
return this.service.accept(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('import-candidates/:id/reject')
|
||||||
|
@ApiOperation({ summary: '拒绝候选知识点' })
|
||||||
|
async reject(@Param('id') id: string) {
|
||||||
|
return this.service.reject(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('import-candidates/batch-accept')
|
||||||
|
@ApiOperation({ summary: '批量接受候选知识点' })
|
||||||
|
async batchAccept(@Body() dto: { sourceId: string }) {
|
||||||
|
return this.service.batchAccept(dto.sourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/modules/import-candidate/import-candidate.module.ts
Normal file
13
src/modules/import-candidate/import-candidate.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ImportCandidateController } from './import-candidate.controller';
|
||||||
|
import { ImportCandidateService } from './import-candidate.service';
|
||||||
|
import { ImportCandidateRepository } from './import-candidate.repository';
|
||||||
|
import { KnowledgeItemsModule } from '../knowledge-items/knowledge-items.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [KnowledgeItemsModule],
|
||||||
|
controllers: [ImportCandidateController],
|
||||||
|
providers: [ImportCandidateService, ImportCandidateRepository],
|
||||||
|
exports: [ImportCandidateService, ImportCandidateRepository],
|
||||||
|
})
|
||||||
|
export class ImportCandidateModule {}
|
||||||
71
src/modules/import-candidate/import-candidate.repository.ts
Normal file
71
src/modules/import-candidate/import-candidate.repository.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImportCandidateRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async createMany(userId: string, knowledgeBaseId: string, sourceId: string, importId: string, candidates: Array<{
|
||||||
|
title: string;
|
||||||
|
summary?: string;
|
||||||
|
content?: string;
|
||||||
|
tagsJson?: any;
|
||||||
|
recallQuestionsJson?: any;
|
||||||
|
sourceTextSnippet?: string;
|
||||||
|
sourceChunkIds?: any;
|
||||||
|
confidence?: number;
|
||||||
|
difficulty?: string;
|
||||||
|
orderIndex?: number;
|
||||||
|
}>) {
|
||||||
|
const data = candidates.map((c, i) => ({
|
||||||
|
userId,
|
||||||
|
knowledgeBaseId,
|
||||||
|
sourceId,
|
||||||
|
importId,
|
||||||
|
title: c.title,
|
||||||
|
summary: c.summary,
|
||||||
|
content: c.content,
|
||||||
|
tagsJson: c.tagsJson,
|
||||||
|
recallQuestionsJson: c.recallQuestionsJson,
|
||||||
|
sourceTextSnippet: c.sourceTextSnippet,
|
||||||
|
sourceChunkIds: c.sourceChunkIds,
|
||||||
|
confidence: c.confidence ?? 0,
|
||||||
|
difficulty: c.difficulty,
|
||||||
|
orderIndex: c.orderIndex ?? i,
|
||||||
|
status: 'PENDING',
|
||||||
|
}));
|
||||||
|
return this.prisma.importCandidate.createMany({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findBySource(sourceId: string) {
|
||||||
|
return this.prisma.importCandidate.findMany({
|
||||||
|
where: { sourceId },
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string) {
|
||||||
|
return this.prisma.importCandidate.findUnique({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStatus(id: string, status: string) {
|
||||||
|
return this.prisma.importCandidate.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchAccept(ids: string[]) {
|
||||||
|
return this.prisma.importCandidate.updateMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
data: { status: 'ACCEPTED' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: { title?: string; summary?: string; content?: string; tagsJson?: any; recallQuestionsJson?: any }) {
|
||||||
|
return this.prisma.importCandidate.update({
|
||||||
|
where: { id },
|
||||||
|
data: { ...data, status: 'EDITED' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/modules/import-candidate/import-candidate.service.ts
Normal file
65
src/modules/import-candidate/import-candidate.service.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { ImportCandidateRepository } from './import-candidate.repository';
|
||||||
|
import { KnowledgeItemsRepository } from '../knowledge-items/knowledge-items.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImportCandidateService {
|
||||||
|
constructor(
|
||||||
|
private readonly repository: ImportCandidateRepository,
|
||||||
|
private readonly itemsRepo: KnowledgeItemsRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findBySource(sourceId: string) {
|
||||||
|
return this.repository.findBySource(sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const c = await this.repository.findById(id);
|
||||||
|
if (!c) throw new NotFoundException('候选知识点不存在');
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
async accept(id: string) {
|
||||||
|
const candidate = await this.repository.findById(id);
|
||||||
|
if (!candidate) throw new NotFoundException('候选知识点不存在');
|
||||||
|
|
||||||
|
await this.repository.updateStatus(id, 'ACCEPTED');
|
||||||
|
|
||||||
|
// 生成 KnowledgeItem
|
||||||
|
await this.itemsRepo.create(candidate.userId, candidate.knowledgeBaseId, {
|
||||||
|
title: candidate.title,
|
||||||
|
content: (candidate.content as string) ?? '',
|
||||||
|
itemType: 'ai_generated',
|
||||||
|
orderIndex: candidate.orderIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status: 'ACCEPTED' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async reject(id: string) {
|
||||||
|
const candidate = await this.repository.findById(id);
|
||||||
|
if (!candidate) throw new NotFoundException('候选知识点不存在');
|
||||||
|
return this.repository.updateStatus(id, 'REJECTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchAccept(sourceId: string) {
|
||||||
|
const candidates = await this.repository.findBySource(sourceId);
|
||||||
|
const pending = candidates.filter(c => c.status === 'PENDING');
|
||||||
|
|
||||||
|
for (const c of pending) {
|
||||||
|
await this.accept(c.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { accepted: pending.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: any) {
|
||||||
|
const candidate = await this.repository.findById(id);
|
||||||
|
if (!candidate) throw new NotFoundException('候选知识点不存在');
|
||||||
|
return this.repository.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCandidates(userId: string, knowledgeBaseId: string, sourceId: string, importId: string, candidates: Array<any>) {
|
||||||
|
return this.repository.createMany(userId, knowledgeBaseId, sourceId, importId, candidates);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/modules/knowledge-source/knowledge-source.controller.ts
Normal file
39
src/modules/knowledge-source/knowledge-source.controller.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Controller, Get, Post, Delete, Body, Param, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { KnowledgeSourceService } from './knowledge-source.service';
|
||||||
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
|
import type { UserPayload } from '../../common/types';
|
||||||
|
|
||||||
|
@ApiTags('knowledge-source')
|
||||||
|
@Controller('knowledge-bases/:kbId/sources')
|
||||||
|
export class KnowledgeSourceController {
|
||||||
|
constructor(private readonly service: KnowledgeSourceService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: '添加资料来源' })
|
||||||
|
async addSource(
|
||||||
|
@CurrentUser() user: UserPayload,
|
||||||
|
@Param('kbId') kbId: string,
|
||||||
|
@Body() dto: any,
|
||||||
|
) {
|
||||||
|
return this.service.addSource(user.id, kbId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取知识库的资料列表' })
|
||||||
|
async findAll(@Param('kbId') kbId: string, @Query() pagination: any) {
|
||||||
|
return this.service.findByKnowledgeBase(kbId, pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: '获取资料详情' })
|
||||||
|
async findOne(@Param('id') id: string) {
|
||||||
|
return this.service.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: '删除资料来源' })
|
||||||
|
async remove(@CurrentUser() user: UserPayload, @Param('id') id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/modules/knowledge-source/knowledge-source.module.ts
Normal file
13
src/modules/knowledge-source/knowledge-source.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { KnowledgeSourceController } from './knowledge-source.controller';
|
||||||
|
import { KnowledgeSourceService } from './knowledge-source.service';
|
||||||
|
import { KnowledgeSourceRepository } from './knowledge-source.repository';
|
||||||
|
import { DocumentImportModule } from '../document-import/document-import.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [DocumentImportModule],
|
||||||
|
controllers: [KnowledgeSourceController],
|
||||||
|
providers: [KnowledgeSourceService, KnowledgeSourceRepository],
|
||||||
|
exports: [KnowledgeSourceService, KnowledgeSourceRepository],
|
||||||
|
})
|
||||||
|
export class KnowledgeSourceModule {}
|
||||||
76
src/modules/knowledge-source/knowledge-source.repository.ts
Normal file
76
src/modules/knowledge-source/knowledge-source.repository.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KnowledgeSourceRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async create(userId: string, knowledgeBaseId: string, dto: {
|
||||||
|
fileId?: string;
|
||||||
|
type?: string;
|
||||||
|
title?: string;
|
||||||
|
originalFilename?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
sizeBytes?: number;
|
||||||
|
originalObjectKey?: string;
|
||||||
|
}) {
|
||||||
|
return this.prisma.knowledgeSource.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
knowledgeBaseId,
|
||||||
|
fileId: dto.fileId,
|
||||||
|
type: dto.type ?? 'file',
|
||||||
|
title: dto.title ?? dto.originalFilename,
|
||||||
|
originalFilename: dto.originalFilename,
|
||||||
|
mimeType: dto.mimeType,
|
||||||
|
sizeBytes: dto.sizeBytes ?? 0,
|
||||||
|
originalObjectKey: dto.originalObjectKey,
|
||||||
|
parseStatus: 'pending',
|
||||||
|
indexStatus: 'pending',
|
||||||
|
learningStatus: 'pending',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByKnowledgeBase(knowledgeBaseId: string, pagination: { page?: number; limit?: number }) {
|
||||||
|
const page = pagination.page ?? 1;
|
||||||
|
const limit = pagination.limit ?? 20;
|
||||||
|
return this.prisma.knowledgeSource.findMany({
|
||||||
|
where: { knowledgeBaseId, deletedAt: null },
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string) {
|
||||||
|
return this.prisma.knowledgeSource.findFirst({ where: { id, deletedAt: null } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(id: string) {
|
||||||
|
return this.prisma.knowledgeSource.update({
|
||||||
|
where: { id },
|
||||||
|
data: { deletedAt: new Date() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateParseStatus(id: string, parseStatus: string, data?: { textLength?: number; parsedObjectKey?: string; metadataObjectKey?: string; errorCode?: string; errorMessage?: string }) {
|
||||||
|
return this.prisma.knowledgeSource.update({
|
||||||
|
where: { id },
|
||||||
|
data: { parseStatus, ...data },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIndexStatus(id: string, indexStatus: string, errorCode?: string, errorMessage?: string) {
|
||||||
|
return this.prisma.knowledgeSource.update({
|
||||||
|
where: { id },
|
||||||
|
data: { indexStatus, errorCode, errorMessage },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async countByKnowledgeBase(knowledgeBaseId: string) {
|
||||||
|
return this.prisma.knowledgeSource.count({
|
||||||
|
where: { knowledgeBaseId, deletedAt: null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/modules/knowledge-source/knowledge-source.service.ts
Normal file
60
src/modules/knowledge-source/knowledge-source.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
|
import { KnowledgeSourceRepository } from './knowledge-source.repository';
|
||||||
|
import { DocumentImportRepository } from '../document-import/document-import.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KnowledgeSourceService {
|
||||||
|
constructor(
|
||||||
|
private readonly repository: KnowledgeSourceRepository,
|
||||||
|
private readonly importRepo: DocumentImportRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async addSource(userId: string, knowledgeBaseId: string, dto: {
|
||||||
|
fileId?: string;
|
||||||
|
type?: string;
|
||||||
|
title?: string;
|
||||||
|
originalFilename?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
sizeBytes?: number;
|
||||||
|
originalObjectKey?: string;
|
||||||
|
}) {
|
||||||
|
const source = await this.repository.create(userId, knowledgeBaseId, dto);
|
||||||
|
|
||||||
|
// 自动创建 import 任务
|
||||||
|
await this.importRepo.create({
|
||||||
|
userId,
|
||||||
|
knowledgeBaseId,
|
||||||
|
sourceId: source.id,
|
||||||
|
fileId: dto.fileId,
|
||||||
|
sourceType: dto.type ?? 'file',
|
||||||
|
sourceName: dto.originalFilename ?? dto.title ?? '',
|
||||||
|
status: 'QUEUED',
|
||||||
|
});
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByKnowledgeBase(knowledgeBaseId: string, pagination: { page?: number; limit?: number }) {
|
||||||
|
return this.repository.findByKnowledgeBase(knowledgeBaseId, pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const source = await this.repository.findById(id);
|
||||||
|
if (!source) throw new NotFoundException('资料来源不存在');
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const source = await this.repository.findById(id);
|
||||||
|
if (!source) throw new NotFoundException('资料来源不存在');
|
||||||
|
return this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateParseStatus(id: string, parseStatus: string, data?: any) {
|
||||||
|
return this.repository.updateParseStatus(id, parseStatus, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIndexStatus(id: string, indexStatus: string, errorCode?: string, errorMessage?: string) {
|
||||||
|
return this.repository.updateIndexStatus(id, indexStatus, errorCode, errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user