import { Body, Controller, Delete, Get, Param, Post, Query, Request, UseGuards, Res, NotFoundException, InternalServerErrorException, } from '@nestjs/common'; import { Response } from 'express'; import * as path from 'path'; import { Logger } from '@nestjs/common'; import { KnowledgeBaseService } from './knowledge-base.service'; import { CombinedAuthGuard } from '../auth/combined-auth.guard'; import { RolesGuard } from '../auth/roles.guard'; import { Roles } from '../auth/roles.decorator'; import { UserRole } from '../user/user-role.enum'; import { Public } from '../auth/public.decorator'; import { KnowledgeBase } from './knowledge-base.entity'; import { ChunkConfigService } from './chunk-config.service'; import { KnowledgeGroupService } from '../knowledge-group/knowledge-group.service'; import { I18nService } from '../i18n/i18n.service'; @Controller('knowledge-bases') @UseGuards(CombinedAuthGuard, RolesGuard) export class KnowledgeBaseController { private readonly logger = new Logger(KnowledgeBaseController.name); constructor( private readonly knowledgeBaseService: KnowledgeBaseService, private readonly chunkConfigService: ChunkConfigService, private readonly knowledgeGroupService: KnowledgeGroupService, private readonly i18nService: I18nService, ) {} @Get() @UseGuards(CombinedAuthGuard) async findAll( @Request() req, @Query('page') page?: number, @Query('limit') limit?: number, ) { return this.knowledgeBaseService.findAll( req.user.id, req.user.tenantId, page ? Number(page) : undefined, limit ? Number(limit) : undefined, ); } @Get('stats') @UseGuards(CombinedAuthGuard) async getStats( @Request() req, ): Promise<{ total: number; uncategorized: number }> { return this.knowledgeBaseService.getStats(req.user.id, req.user.tenantId); } @Delete('clear') @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN) async clearAll(@Request() req): Promise<{ message: string }> { await this.knowledgeBaseService.clearAll(req.user.id, req.user.tenantId); return { message: this.i18nService.getMessage('kbCleared') }; } @Post('search') async search(@Request() req, @Body() body: { query: string; topK?: number }) { return this.knowledgeBaseService.searchKnowledge( req.user.id, req.user.tenantId, // New body.query, body.topK || 5, ); } @Post('rag-search') async ragSearch( @Request() req, @Body() body: { query: string; settings: any }, ) { return this.knowledgeBaseService.ragSearch( req.user.id, req.user.tenantId, // New body.query, body.settings, ); } @Delete(':id') @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN) async deleteFile( @Request() req, @Param('id') fileId: string, ): Promise<{ message: string }> { await this.knowledgeBaseService.deleteFile( fileId, req.user.id, req.user.tenantId, ); return { message: this.i18nService.getMessage('fileDeleted') }; } @Post(':id/retry') @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN) async retryFile( @Request() req, @Param('id') fileId: string, ): Promise { return this.knowledgeBaseService.retryFailedFile( fileId, req.user.id, req.user.tenantId, ); } @Get(':id/chunks') async getFileChunks(@Request() req, @Param('id') fileId: string) { return this.knowledgeBaseService.getFileChunks( fileId, req.user.id, req.user.tenantId, ); } /** * Get chunk config limits (for frontend slider settings) * Query parameter: embeddingModelId - embedding model ID */ @Get('chunk-config/limits') async getChunkConfigLimits( @Request() req, @Query('embeddingModelId') embeddingModelId: string, ) { if (!embeddingModelId) { return { maxChunkSize: parseInt(process.env.MAX_CHUNK_SIZE || '8191'), maxOverlapSize: parseInt(process.env.MAX_OVERLAP_SIZE || '2000'), minOverlapSize: 25, defaultChunkSize: 200, defaultOverlapSize: 40, modelInfo: { name: this.i18nService.getMessage('modelNotConfigured'), maxInputTokens: parseInt(process.env.MAX_CHUNK_SIZE || '8191'), maxBatchSize: 2048, expectedDimensions: parseInt( process.env.DEFAULT_VECTOR_DIMENSIONS || '2560', ), }, }; } return await this.chunkConfigService.getFrontendLimits( embeddingModelId, req.user.id, req.user.tenantId, ); } // File group management - requires admin permission @Post(':id/groups') @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN) async addFileToGroups( @Param('id') fileId: string, @Body() body: { groupIds: string[] }, @Request() req, ) { await this.knowledgeGroupService.addFilesToGroup( fileId, body.groupIds, req.user.id, req.user.tenantId, ); return { message: this.i18nService.getMessage('groupSyncSuccess') }; } @Delete(':id/groups/:groupId') @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN) async removeFileFromGroup( @Param('id') fileId: string, @Param('groupId') groupId: string, @Request() req, ) { await this.knowledgeGroupService.removeFileFromGroup( fileId, groupId, req.user.id, req.user.tenantId, ); return { message: this.i18nService.getMessage('fileDeletedFromGroup') }; } // PDF preview - public access @Public() @Get(':id/pdf') async getPDFPreview( @Param('id') fileId: string, @Query('token') token: string, @Res() res: Response, ) { try { if (!token) { throw new NotFoundException( this.i18nService.getMessage('accessDeniedNoToken'), ); } const jwt = await import('jsonwebtoken'); const secret = process.env.JWT_SECRET; if (!secret) { throw new InternalServerErrorException( this.i18nService.getMessage('jwtSecretRequired'), ); } let decoded; try { decoded = jwt.verify(token, secret) as any; } catch { throw new NotFoundException( this.i18nService.getMessage('invalidToken'), ); } if (decoded.type !== 'pdf-access' || decoded.fileId !== fileId) { throw new NotFoundException( this.i18nService.getMessage('invalidToken'), ); } const pdfPath = await this.knowledgeBaseService.ensurePDFExists( fileId, decoded.userId, decoded.tenantId, // New ); const fs = await import('fs'); const path = await import('path'); if (!fs.existsSync(pdfPath)) { throw new NotFoundException( this.i18nService.getMessage('pdfFileNotFound'), ); } const stat = fs.statSync(pdfPath); const fileName = path.basename(pdfPath); if (stat.size === 0) { this.logger.warn(`PDF file is empty: ${pdfPath}`); try { fs.unlinkSync(pdfPath); // Delete empty file } catch (e) {} throw new NotFoundException( this.i18nService.getMessage('pdfFileEmpty'), ); } res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Length', stat.size); const stream = fs.createReadStream(pdfPath); stream.pipe(res); } catch (error) { if (error instanceof NotFoundException) { throw error; } this.logger.error(`PDF preview error: ${error.message}`); throw new NotFoundException( this.i18nService.getMessage('pdfConversionFailed'), ); } } // Get PDF preview URL @Get(':id/pdf-url') async getPDFUrl( @Param('id') fileId: string, @Query('force') force: string, @Request() req, ) { try { // Trigger PDF conversion await this.knowledgeBaseService.ensurePDFExists( fileId, req.user.id, req.user.tenantId, force === 'true', ); // Generate temporary access token const jwt = await import('jsonwebtoken'); const secret = process.env.JWT_SECRET; if (!secret) { throw new InternalServerErrorException( this.i18nService.getMessage('jwtSecretRequired'), ); } const token = jwt.sign( { fileId, userId: req.user.id, tenantId: req.user.tenantId, type: 'pdf-access', }, secret, { expiresIn: '1h' }, ); return { url: `/api/knowledge-bases/${fileId}/pdf?token=${token}`, }; } catch (error) { if (error.message.includes('LibreOffice')) { throw new InternalServerErrorException( this.i18nService.formatMessage('pdfServiceUnavailable', { message: error.message, }), ); } throw new InternalServerErrorException(error.message); } } @Get(':id/pdf-status') async getPDFStatus(@Param('id') fileId: string, @Request() req) { return await this.knowledgeBaseService.getPDFStatus( fileId, req.user.id, req.user.tenantId, ); } // Get specific page of PDF as image @Get(':id/page/:index') async getPageImage( @Param('id') fileId: string, @Param('index') index: number, @Request() req, @Res() res: Response, ) { try { const imagePath = await this.knowledgeBaseService.getPageAsImage( fileId, Number(index), req.user.id, req.user.tenantId, ); const fs = await import('fs'); if (!fs.existsSync(imagePath)) { throw new NotFoundException( this.i18nService.getMessage('pageImageNotFound'), ); } res.sendFile(path.resolve(imagePath)); } catch (error) { this.logger.error(`Failed to get PDF page image: ${error.message}`); throw new NotFoundException( this.i18nService.getMessage('pdfPageImageFailed'), ); } } }