knowledge-base.controller.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import {
  2. Body,
  3. Controller,
  4. Delete,
  5. Get,
  6. Param,
  7. Post,
  8. Query,
  9. Request,
  10. UseGuards,
  11. Res,
  12. NotFoundException,
  13. InternalServerErrorException,
  14. } from '@nestjs/common';
  15. import { Response } from 'express';
  16. import * as path from 'path';
  17. import { Logger } from '@nestjs/common';
  18. import { KnowledgeBaseService } from './knowledge-base.service';
  19. import { CombinedAuthGuard } from '../auth/combined-auth.guard';
  20. import { RolesGuard } from '../auth/roles.guard';
  21. import { Roles } from '../auth/roles.decorator';
  22. import { UserRole } from '../user/user-role.enum';
  23. import { Public } from '../auth/public.decorator';
  24. import { KnowledgeBase } from './knowledge-base.entity';
  25. import { ChunkConfigService } from './chunk-config.service';
  26. import { KnowledgeGroupService } from '../knowledge-group/knowledge-group.service';
  27. import { I18nService } from '../i18n/i18n.service';
  28. @Controller('knowledge-bases')
  29. @UseGuards(CombinedAuthGuard, RolesGuard)
  30. export class KnowledgeBaseController {
  31. private readonly logger = new Logger(KnowledgeBaseController.name);
  32. constructor(
  33. private readonly knowledgeBaseService: KnowledgeBaseService,
  34. private readonly chunkConfigService: ChunkConfigService,
  35. private readonly knowledgeGroupService: KnowledgeGroupService,
  36. private readonly i18nService: I18nService,
  37. ) { }
  38. @Get()
  39. @UseGuards(CombinedAuthGuard)
  40. async findAll(@Request() req): Promise<KnowledgeBase[]> {
  41. return this.knowledgeBaseService.findAll(req.user.id, req.user.tenantId);
  42. }
  43. @Delete('clear')
  44. @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
  45. async clearAll(@Request() req): Promise<{ message: string }> {
  46. await this.knowledgeBaseService.clearAll(req.user.id, req.user.tenantId);
  47. return { message: this.i18nService.getMessage('kbCleared') };
  48. }
  49. @Post('search')
  50. async search(@Request() req, @Body() body: { query: string; topK?: number }) {
  51. return this.knowledgeBaseService.searchKnowledge(
  52. req.user.id,
  53. req.user.tenantId, // New
  54. body.query,
  55. body.topK || 5,
  56. );
  57. }
  58. @Post('rag-search')
  59. async ragSearch(
  60. @Request() req,
  61. @Body() body: { query: string; settings: any },
  62. ) {
  63. return this.knowledgeBaseService.ragSearch(
  64. req.user.id,
  65. req.user.tenantId, // New
  66. body.query,
  67. body.settings,
  68. );
  69. }
  70. @Delete(':id')
  71. @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
  72. async deleteFile(
  73. @Request() req,
  74. @Param('id') fileId: string,
  75. ): Promise<{ message: string }> {
  76. await this.knowledgeBaseService.deleteFile(fileId, req.user.id, req.user.tenantId);
  77. return { message: this.i18nService.getMessage('fileDeleted') };
  78. }
  79. @Post(':id/retry')
  80. @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
  81. async retryFile(
  82. @Request() req,
  83. @Param('id') fileId: string,
  84. ): Promise<KnowledgeBase> {
  85. return this.knowledgeBaseService.retryFailedFile(fileId, req.user.id, req.user.tenantId);
  86. }
  87. @Get(':id/chunks')
  88. async getFileChunks(
  89. @Request() req,
  90. @Param('id') fileId: string,
  91. ) {
  92. return this.knowledgeBaseService.getFileChunks(fileId, req.user.id, req.user.tenantId);
  93. }
  94. /**
  95. * チャンク設定の制限を取得(フロントエンドのスライダー設定用)
  96. * クエリパラメータ: embeddingModelId - 埋め込みモデルID
  97. */
  98. @Get('chunk-config/limits')
  99. async getChunkConfigLimits(
  100. @Request() req,
  101. @Query('embeddingModelId') embeddingModelId: string,
  102. ) {
  103. if (!embeddingModelId) {
  104. return {
  105. maxChunkSize: parseInt(process.env.MAX_CHUNK_SIZE || '8191'),
  106. maxOverlapSize: parseInt(process.env.MAX_OVERLAP_SIZE || '2000'),
  107. minOverlapSize: 25,
  108. defaultChunkSize: 200,
  109. defaultOverlapSize: 40,
  110. modelInfo: {
  111. name: this.i18nService.getMessage('modelNotConfigured'),
  112. maxInputTokens: parseInt(process.env.MAX_CHUNK_SIZE || '8191'),
  113. maxBatchSize: 2048,
  114. expectedDimensions: parseInt(process.env.DEFAULT_VECTOR_DIMENSIONS || '2560'),
  115. },
  116. };
  117. }
  118. return await this.chunkConfigService.getFrontendLimits(
  119. embeddingModelId,
  120. req.user.id,
  121. req.user.tenantId,
  122. );
  123. }
  124. // 文件分组管理 - 需要管理员权限
  125. @Post(':id/groups')
  126. @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
  127. async addFileToGroups(
  128. @Param('id') fileId: string,
  129. @Body() body: { groupIds: string[] },
  130. @Request() req,
  131. ) {
  132. await this.knowledgeGroupService.addFilesToGroup(
  133. fileId,
  134. body.groupIds,
  135. req.user.id,
  136. req.user.tenantId,
  137. );
  138. return { message: this.i18nService.getMessage('groupSyncSuccess') };
  139. }
  140. @Delete(':id/groups/:groupId')
  141. @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
  142. async removeFileFromGroup(
  143. @Param('id') fileId: string,
  144. @Param('groupId') groupId: string,
  145. @Request() req,
  146. ) {
  147. await this.knowledgeGroupService.removeFileFromGroup(
  148. fileId,
  149. groupId,
  150. req.user.id,
  151. req.user.tenantId,
  152. );
  153. return { message: this.i18nService.getMessage('fileDeletedFromGroup') };
  154. }
  155. // PDF プレビュー - 公開アクセス
  156. @Public()
  157. @Get(':id/pdf')
  158. async getPDFPreview(
  159. @Param('id') fileId: string,
  160. @Query('token') token: string,
  161. @Res() res: Response,
  162. ) {
  163. try {
  164. if (!token) {
  165. throw new NotFoundException(this.i18nService.getMessage('accessDeniedNoToken'));
  166. }
  167. const jwt = await import('jsonwebtoken');
  168. const secret = process.env.JWT_SECRET;
  169. if (!secret) {
  170. throw new InternalServerErrorException('JWT_SECRET environment variable is required but not set');
  171. }
  172. let decoded;
  173. try {
  174. decoded = jwt.verify(token, secret) as any;
  175. } catch {
  176. throw new NotFoundException(this.i18nService.getMessage('invalidToken'));
  177. }
  178. if (decoded.type !== 'pdf-access' || decoded.fileId !== fileId) {
  179. throw new NotFoundException(this.i18nService.getMessage('invalidToken'));
  180. }
  181. const pdfPath = await this.knowledgeBaseService.ensurePDFExists(
  182. fileId,
  183. decoded.userId,
  184. decoded.tenantId, // New
  185. );
  186. const fs = await import('fs');
  187. const path = await import('path');
  188. if (!fs.existsSync(pdfPath)) {
  189. throw new NotFoundException(this.i18nService.getMessage('pdfFileNotFound'));
  190. }
  191. const stat = fs.statSync(pdfPath);
  192. const fileName = path.basename(pdfPath);
  193. if (stat.size === 0) {
  194. this.logger.warn(`PDF file is empty: ${pdfPath}`);
  195. try {
  196. fs.unlinkSync(pdfPath); // 空のファイルを削除
  197. } catch (e) { }
  198. throw new NotFoundException(this.i18nService.getMessage('pdfFileEmpty'));
  199. }
  200. res.setHeader('Content-Type', 'application/pdf');
  201. res.setHeader('Content-Length', stat.size);
  202. const stream = fs.createReadStream(pdfPath);
  203. stream.pipe(res);
  204. } catch (error) {
  205. if (error instanceof NotFoundException) {
  206. throw error;
  207. }
  208. this.logger.error(`PDF preview error: ${error.message}`);
  209. throw new NotFoundException(this.i18nService.getMessage('pdfConversionFailed'));
  210. }
  211. }
  212. // PDF プレビューアドレスを取得
  213. @Get(':id/pdf-url')
  214. async getPDFUrl(
  215. @Param('id') fileId: string,
  216. @Query('force') force: string,
  217. @Request() req,
  218. ) {
  219. try {
  220. // PDF 変換をトリガー
  221. await this.knowledgeBaseService.ensurePDFExists(fileId, req.user.id, req.user.tenantId, force === 'true');
  222. // 一時的なアクセストークンを生成
  223. const jwt = await import('jsonwebtoken');
  224. const secret = process.env.JWT_SECRET;
  225. if (!secret) {
  226. throw new InternalServerErrorException('JWT_SECRET environment variable is required but not set');
  227. }
  228. const token = jwt.sign(
  229. { fileId, userId: req.user.id, tenantId: req.user.tenantId, type: 'pdf-access' },
  230. secret,
  231. { expiresIn: '1h' }
  232. );
  233. return {
  234. url: `/api/knowledge-bases/${fileId}/pdf?token=${token}`
  235. };
  236. } catch (error) {
  237. if (error.message.includes('LibreOffice')) {
  238. throw new InternalServerErrorException(this.i18nService.formatMessage('pdfServiceUnavailable', { message: error.message }));
  239. }
  240. throw new InternalServerErrorException(error.message);
  241. }
  242. }
  243. @Get(':id/pdf-status')
  244. async getPDFStatus(
  245. @Param('id') fileId: string,
  246. @Request() req,
  247. ) {
  248. return await this.knowledgeBaseService.getPDFStatus(fileId, req.user.id, req.user.tenantId);
  249. }
  250. // PDF の特定ページの画像を取得
  251. @Get(':id/page/:index')
  252. async getPageImage(
  253. @Param('id') fileId: string,
  254. @Param('index') index: number,
  255. @Request() req,
  256. @Res() res: Response,
  257. ) {
  258. try {
  259. const imagePath = await this.knowledgeBaseService.getPageAsImage(
  260. fileId,
  261. Number(index),
  262. req.user.id,
  263. req.user.tenantId,
  264. );
  265. const fs = await import('fs');
  266. if (!fs.existsSync(imagePath)) {
  267. throw new NotFoundException(this.i18nService.getMessage('pageImageNotFound'));
  268. }
  269. res.sendFile(path.resolve(imagePath));
  270. } catch (error) {
  271. this.logger.error(`PDF ページの画像取得に失敗しました: ${error.message}`);
  272. throw new NotFoundException(this.i18nService.getMessage('pdfPageImageFailed'));
  273. }
  274. }
  275. }