feat: KnowledgeSource 和 ImportCandidate 模块
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 22s

This commit is contained in:
WangDL 2026-05-19 22:20:29 +08:00
parent 1e7e4268ab
commit 9c161db26b
9 changed files with 386 additions and 0 deletions

View File

@ -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,

View 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);
}
}

View 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 {}

View 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' },
});
}
}

View 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);
}
}

View 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);
}
}

View 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 {}

View 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 },
});
}
}

View 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);
}
}