| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- 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): Promise<KnowledgeBase[]> {
- return this.knowledgeBaseService.findAll(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<KnowledgeBase> {
- 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);
- }
- /**
- * チャンク設定の制限を取得(フロントエンドのスライダー設定用)
- * クエリパラメータ: embeddingModelId - 埋め込みモデル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,
- );
- }
- // 文件分组管理 - 需要管理员权限
- @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 プレビュー - 公開アクセス
- @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('JWT_SECRET environment variable is required but not set');
- }
- 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); // 空のファイルを削除
- } 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'));
- }
- }
- // PDF プレビューアドレスを取得
- @Get(':id/pdf-url')
- async getPDFUrl(
- @Param('id') fileId: string,
- @Query('force') force: string,
- @Request() req,
- ) {
- try {
- // PDF 変換をトリガー
- await this.knowledgeBaseService.ensurePDFExists(fileId, req.user.id, req.user.tenantId, force === 'true');
- // 一時的なアクセストークンを生成
- const jwt = await import('jsonwebtoken');
- const secret = process.env.JWT_SECRET;
- if (!secret) {
- throw new InternalServerErrorException('JWT_SECRET environment variable is required but not set');
- }
- 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);
- }
- // PDF の特定ページの画像を取得
- @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(`PDF ページの画像取得に失敗しました: ${error.message}`);
- throw new NotFoundException(this.i18nService.getMessage('pdfPageImageFailed'));
- }
- }
- }
|