diff --git a/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/FileController.java b/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/FileController.java new file mode 100644 index 00000000..8888feae --- /dev/null +++ b/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/FileController.java @@ -0,0 +1,189 @@ +package com.alops.web.controller.LlmKnowledgeController; + +import java.io.File; +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.alops.chat.entity.UploadedFile; +import com.alops.chat.service.UploadedFileService; +import com.alops.common.config.RuoYiConfig; +import com.alops.common.core.controller.BaseController; +import com.alops.common.utils.file.FileUtils; +import com.alops.common.utils.file.FileTypeUtils; +import com.alops.common.utils.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 文件下载和预览控制器 + * + * @author lingma + */ +@RestController +@RequestMapping("/QandA/knowledge/file") +public class FileController extends BaseController { + private static final Logger log = LoggerFactory.getLogger(FileController.class); + + @Autowired + private UploadedFileService uploadedFileService; + + /** + * 下载文件 + * + * @param fileId 文件ID + * @param response 响应对象 + */ + @PreAuthorize("@ss.hasPermi('QandA:knowledge:filedownload')") + @GetMapping("/download/{fileId}") + public void download(@PathVariable Long fileId, HttpServletResponse response, HttpServletRequest request) { + try { + UploadedFile uploadedFile = uploadedFileService.findById(fileId); + if (uploadedFile == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件未找到"); + return; + } + + // 获取文件路径 + String filePath = uploadedFile.getFilePath(); + // 获取实际文件路径(处理配置文件前缀) + String realPath = RuoYiConfig.getProfile() + filePath.replaceFirst("/profile", ""); + + // 检查文件是否存在 + File file = new File(realPath); + if (!file.exists()) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件未找到"); + return; + } + + // 检查文件是否允许下载 + if (!FileUtils.checkAllowDownload(realPath)) { + throw new Exception(StringUtils.format("文件({})非法,不允许下载。", realPath)); + } + + // 设置下载文件名(从文件路径中提取真实文件名) + String fileName = FileUtils.getName(realPath); + if (StringUtils.isEmpty(fileName)) { + fileName = uploadedFile.getSource(); + // 如果source中也不包含扩展名,则尝试添加扩展名 + String extension = uploadedFile.getFormat(); + if (StringUtils.isNotEmpty(extension) && !fileName.endsWith("." + extension)) { + fileName = fileName + "." + extension; + } + } + + // 对文件名进行编码以支持中文 + String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()) + .replaceAll("\\+", "%20"); + + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName); + FileUtils.writeBytes(realPath, response.getOutputStream()); + } catch (Exception e) { + log.error("下载文件失败", e); + try { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "下载文件失败: " + e.getMessage()); + } catch (IOException ioException) { + log.error("设置错误响应失败", ioException); + } + } + } + + /** + * 预览文件 + * + * @param fileId 文件ID + * @param response 响应对象 + */ + @PreAuthorize("@ss.hasPermi('QandA:knowledge:filepreview')") + @GetMapping("/preview/{fileId}") + public void preview(@PathVariable Long fileId, HttpServletResponse response, HttpServletRequest request) { + try { + UploadedFile uploadedFile = uploadedFileService.findById(fileId); + if (uploadedFile == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件未找到"); + return; + } + + // 获取文件路径 + String filePath = uploadedFile.getFilePath(); + // 获取实际文件路径(处理配置文件前缀) + String realPath = RuoYiConfig.getProfile() + filePath.replaceFirst("/profile", ""); + + // 检查文件是否存在 + File file = new File(realPath); + if (!file.exists()) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件未找到"); + return; + } + + // 检查文件是否允许预览 + if (!FileUtils.checkAllowDownload(realPath)) { + throw new Exception(StringUtils.format("文件({})非法,不允许预览。", realPath)); + } + + // 设置Content-Type以便浏览器预览 + String fileExtension = FileTypeUtils.getFileType(uploadedFile.getSource()); + if (StringUtils.isEmpty(fileExtension)) { + fileExtension = uploadedFile.getFormat(); + } + String contentType = getContentTypeByExtension(fileExtension); + if (contentType != null) { + response.setContentType(contentType); + } else { + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + } + + FileUtils.writeBytes(realPath, response.getOutputStream()); + } catch (Exception e) { + log.error("预览文件失败", e); + try { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "预览文件失败: " + e.getMessage()); + } catch (IOException ioException) { + log.error("设置错误响应失败", ioException); + } + } + } + + /** + * 根据文件扩展名获取对应的Content-Type + * + * @param extension 文件扩展名 + * @return Content-Type字符串 + */ + private String getContentTypeByExtension(String extension) { + if (StringUtils.isEmpty(extension)) { + return MediaType.APPLICATION_OCTET_STREAM_VALUE; + } + + switch (extension.toLowerCase()) { + case "txt": + return "text/plain;charset=UTF-8"; + case "doc": + return "application/msword"; + case "docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case "pdf": + return "application/pdf"; + case "jpg": + case "jpeg": + return "image/jpeg"; + case "png": + return "image/png"; + case "gif": + return "image/gif"; + default: + return MediaType.APPLICATION_OCTET_STREAM_VALUE; + } + } +} \ No newline at end of file diff --git a/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/KnowLedgeController.java b/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/KnowLedgeController.java index 875f86c1..51431126 100644 --- a/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/KnowLedgeController.java +++ b/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/KnowLedgeController.java @@ -4,13 +4,11 @@ import com.alops.chat.entity.KnowledgeEntity; import com.alops.chat.entity.UploadedFile; import com.alops.chat.service.KnowledgeService; import com.alops.chat.service.UploadedFileService; -import com.alops.common.annotation.Log; import com.alops.common.constant.HttpStatus; import com.alops.common.core.controller.BaseController; import com.alops.common.core.domain.AjaxResult; import com.alops.common.core.page.TableDataInfo; import com.alops.common.config.RuoYiConfig; -import com.alops.common.enums.BusinessType; import com.alops.common.utils.file.FileUploadUtils; import com.alops.common.utils.file.MimeTypeUtils; import io.swagger.annotations.ApiOperation; @@ -26,14 +24,9 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * 文件上传控制器 @@ -53,6 +46,36 @@ public class KnowLedgeController extends BaseController { // 文件存储目录 private static final String UPLOAD_DIR = "uploads/"; + /** + * 根据文件ID获取文件路径,供前端下载和预览使用 + * + * @param id 文件ID + * @return 文件路径信息 + */ + @PreAuthorize("@ss.hasPermi('QandA:knowledge:filedownload')") + @GetMapping("/file-path/{id}") + @ApiOperation("获取文件路径") + public AjaxResult getFilePath(@PathVariable Long id) { + try { + UploadedFile uploadedFile = uploadedFileService.findById(id); + if (uploadedFile == null) { + return AjaxResult.error(HttpStatus.NOT_FOUND, "未找到指定的文件"); + } + + // 返回文件路径信息 + Map result = new HashMap<>(); + result.put("filePath", uploadedFile.getFilePath()); + result.put("fileName", uploadedFile.getSource()); + result.put("fileFormat", uploadedFile.getFormat()); + result.put("uploadTime", uploadedFile.getUploadTime()); + + return AjaxResult.success(result); + } catch (Exception e) { + e.printStackTrace(); + return AjaxResult.error("获取文件路径失败: " + e.getMessage()); + } + } + /** * 上传并处理文本文档 * @@ -63,7 +86,6 @@ public class KnowLedgeController extends BaseController { @PreAuthorize("@ss.hasPermi('QandA:knowledge:uploadfile')") @PostMapping("/upload") - @Log(title = "上传并处理文本文档", businessType = BusinessType.INSERT) @ApiOperation("上传并处理文本文档") public AjaxResult uploadFile( @RequestPart("file") @@ -84,14 +106,14 @@ public class KnowLedgeController extends BaseController { // 使用优化后的文件上传工具保存文件到本地并记录信息到数据库 // 上传文件并保持原始文件名,只允许 TEXT_EXTENSION 类型的文件 String filePath = FileUploadUtils.uploadWithOriginalFilename( - RuoYiConfig.getProfile(), - file, + RuoYiConfig.getProfile(), + file, MimeTypeUtils.TEXT_EXTENSION); - + String fileExtension = getFileExtension(file.getOriginalFilename()); UploadedFile uploadedFile = new UploadedFile(filePath, source, fileExtension, null); uploadedFile = uploadedFileService.save(uploadedFile); - + // 获取保存后的文件ID Long fileId = uploadedFile.getId(); @@ -171,7 +193,6 @@ public class KnowLedgeController extends BaseController { */ @PreAuthorize("@ss.hasPermi('QandA:knowledge:upload')") @PostMapping("/add-knowledge") - @Log(title = "添加知识条目接口", businessType = BusinessType.INSERT) @ApiOperation("添加知识条目接口") public AjaxResult addKnowledge( @RequestParam String content, @@ -193,7 +214,6 @@ public class KnowLedgeController extends BaseController { */ @PreAuthorize("@ss.hasPermi('QandA:knowledge:permquery')") @GetMapping("/permanent-knowledge") - @Log(title = "获取所有正式知识条目", businessType = BusinessType.SELECT) @ApiOperation("获取所有正式知识条目") public TableDataInfo getPermanentKnowledge() { startPage(); // 开启分页 @@ -208,7 +228,6 @@ public class KnowLedgeController extends BaseController { */ @PreAuthorize("@ss.hasPermi('QandA:knowledge:filelist')") @GetMapping("/uploaded-files") - @Log(title = "获取所有上传文档", businessType = BusinessType.SELECT) @ApiOperation("获取所有上传文档") public TableDataInfo getUploadedFiles() { startPage(); // 开启分页 @@ -226,7 +245,6 @@ public class KnowLedgeController extends BaseController { */ @PreAuthorize("@ss.hasPermi('QandA:knowledge:update')") @PutMapping("/update-knowledge") - @Log(title = "更新知识条目", businessType = BusinessType.INSERT) @ApiOperation("更新知识条目") public AjaxResult updateKnowledge( @RequestParam Long id, @@ -253,7 +271,6 @@ public class KnowLedgeController extends BaseController { */ @PreAuthorize("@ss.hasPermi('QandA:knowledge:del')") @DeleteMapping("/knowledge/{id}") - @Log(title = "删除知识条目接口", businessType = BusinessType.DELETE) @ApiOperation("删除知识条目接口") public AjaxResult deleteKnowledge(@PathVariable Long id) { try { @@ -276,13 +293,12 @@ public class KnowLedgeController extends BaseController { */ @ApiOperation("删除上传文件信息") @PreAuthorize("@ss.hasPermi('QandA:knowledge:filedel')") - @Log(title = "删除上传文件信息", businessType = BusinessType.DELETE) @DeleteMapping("/uploaded-files/{id}") public AjaxResult deleteUploadedFile(@PathVariable Long id) { try { // 先删除关联的知识条目 knowledgeService.deleteKnowledgeByFileId(id); - + // 再删除文件信息 uploadedFileService.deleteById(id); return AjaxResult.success("上传文件信息删除成功"); diff --git a/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/QAController.java b/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/QAController.java index bed26bb8..85062fed 100644 --- a/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/QAController.java +++ b/ALOps_sys_backend/alops-admin/src/main/java/com/alops/web/controller/LlmKnowledgeController/QAController.java @@ -1,17 +1,17 @@ package com.alops.web.controller.LlmKnowledgeController; + import com.alops.chat.DTO.AnswerResponse; import com.alops.chat.DTO.QuestionRequest; import com.alops.chat.entity.KnowledgeEntity; import com.alops.chat.service.AItokenService; import com.alops.chat.service.KnowledgeService; -import com.alops.common.annotation.Log; import com.alops.common.core.domain.AjaxResult; -import com.alops.common.enums.BusinessType; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -49,7 +49,6 @@ public class QAController { @PreAuthorize("@ss.hasPermi('QandA:Aiassistant:ask')") @PostMapping("/ask") @ApiOperation("智能问答提问") - @Log(title = "智能问答提问", businessType = BusinessType.INSERT) public AjaxResult askQuestion(@RequestBody QuestionRequest request) { try { // 检查是否使用假数据模式 @@ -88,12 +87,12 @@ public class QAController { List> messages = new ArrayList<>(); Map systemMessage = new HashMap<>(); systemMessage.put("role", "system"); - systemMessage.put("content", "你是一个智能运维助手,根据提供的上下文回答问题。如果上下文中没有相关信息,请说明无法基于提供的上下文回答问题。"); + systemMessage.put("content", "你是一个智能运维助手,根据提供的上下文回答问题。如果上下文中没有相关信息,请结合自身知识回答用户问题。"); messages.add(systemMessage); Map userMessage = new HashMap<>(); userMessage.put("role", "user"); - userMessage.put("content", "上下文信息:\n" + context + "\n\n请根据以上上下文信息回答问题:" + request.getQuestion()); + userMessage.put("content", "上下文信息:\n" + context + "\n\n请根据以上上下文信息并且结合自身知识回答问题:" + request.getQuestion()); messages.add(userMessage); String answer = AItokenService.generateAnswer(messages); diff --git a/ALOps_sys_backend/alops-chat/src/main/java/com/alops/chat/service/AItokenService.java b/ALOps_sys_backend/alops-chat/src/main/java/com/alops/chat/service/AItokenService.java index 195dba1e..9134580b 100644 --- a/ALOps_sys_backend/alops-chat/src/main/java/com/alops/chat/service/AItokenService.java +++ b/ALOps_sys_backend/alops-chat/src/main/java/com/alops/chat/service/AItokenService.java @@ -30,7 +30,7 @@ public class AItokenService { // 使用的模型 private static final String EMBEDDING_MODEL = "nomic-embed-text:v1.5"; - private static final String GENERATION_MODEL = "qwen3:4b"; + private static final String GENERATION_MODEL = "qwen3:8b"; // 是否使用假数据 @Value("${mock.enabled:false}") diff --git a/ALOps_sys_backend/alops-system/src/main/resources/mapper/system/EquBaseMapper.xml b/ALOps_sys_backend/alops-system/src/main/resources/mapper/system/EquBaseMapper.xml index feae526a..dacce666 100644 --- a/ALOps_sys_backend/alops-system/src/main/resources/mapper/system/EquBaseMapper.xml +++ b/ALOps_sys_backend/alops-system/src/main/resources/mapper/system/EquBaseMapper.xml @@ -278,7 +278,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + AND b.status = #{status}