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 { FilesModule } from './modules/files/files.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 { RolesGuard } from './common/guards/roles.guard';
|
||||
@ -80,6 +82,8 @@ import appleConfig from './config/apple.config';
|
||||
UsersModule,
|
||||
KnowledgeBaseModule,
|
||||
KnowledgeItemsModule,
|
||||
KnowledgeSourceModule,
|
||||
ImportCandidateModule,
|
||||
DocumentImportModule,
|
||||
LearningSessionModule,
|
||||
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