5 天以前 ea5a55deffa6d33048a1f7e03b71424c8add5e31
feat(ai): 实现采购多文件分析附件存储与历史回显功能

- 在AiChatMessageDto中添加filePaths字段支持文件路径传递
- 扩展ChatMessages实体类增加analyzeUserQuestions、analyzeFilePaths和analyzeFilePathGroups字段
- 实现MongoChatMemoryStore的文件上下文存储和检索功能
- 集成StorageBlobService完成文件上传和访问路径解析
- 改造PurchaseAiController支持多文件分析和路径存储
- 配置文件上传相关参数包括临时目录正式目录和压缩设置
- 实现历史消息回传时按消息维度附加文件路径的功能
- 提供完整的前后端联调文档说明接口变更和改造建议
已添加1个文件
已修改6个文件
464 ■■■■■ 文件已修改
doc/20260508_采购多文件分析附件存储与历史回显联调说明.md 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260508_²É¹º¶àÎļþ·ÖÎö¸½¼þ´æ´¢ÓëÀúÊ·»ØÏÔÁªµ÷˵Ã÷.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,149 @@
# é‡‡è´­å¤šæ–‡ä»¶åˆ†æžé™„件存储与历史回显联调说明
> æ›´æ–°æ—¶é—´ï¼š2026-05-08
> é€‚用范围:采购智能体多文件分析 + åŽ†å²ä¼šè¯é™„ä»¶å›žæ˜¾
## 1. å˜æ›´èƒŒæ™¯
后端已补齐以下能力:
1. `POST /purchase-ai/analyze-files` ä¸Šä¼ æ—¶å…ˆå­˜é™„件到服务器(公共访问)。
2. æŒ‰æ–‡ä»¶ç±»åž‹è¿”回可访问路径:
   - å›¾ç‰‡ / PDF:优先预览路径
   - å…¶å®ƒæ–‡ä»¶ï¼šä¼˜å…ˆä¸‹è½½è·¯å¾„
3. ç”¨æˆ·æé—®ä¸Žé™„件路径分开存入 Mongo。
4. åŽ†å²æ¶ˆæ¯æŽ¥å£å¯æŒ‰â€œæ¶ˆæ¯ç»´åº¦â€å›žä¼ é™„ä»¶è·¯å¾„ï¼Œå‰ç«¯å¯ç›´æŽ¥å›žæ˜¾é™„ä»¶å¡ç‰‡ã€‚
---
## 2. æŽ¥å£è¡Œä¸º
### 2.1 å¤šæ–‡ä»¶åˆ†æžæŽ¥å£
```http
POST /purchase-ai/analyze-files
Content-Type: multipart/form-data
```
请求参数:
- `files`: `MultipartFile[]`(必填)
- `message`: `string`(可选)
- `memoryId`: `string`(可选)
后端处理流程:
1. è°ƒç”¨ `StorageBlobService.upload(files, true)` ä¸Šä¼ å¹¶ä¿å­˜é™„件。
2. ç”Ÿæˆé™„件访问路径(预览 / ä¸‹è½½ï¼‰ã€‚
3. å°†â€œæœ¬æ¬¡æé—® + æœ¬æ¬¡é™„件路径列表”写入 Mongo。
4. ç»§ç»­æ‰§è¡ŒåŽŸæœ‰æ–‡ä»¶è§£æžå’Œ AI åˆ†æžæµç¨‹ã€‚
可能错误(流式文本):
- `文件上传失败`
- `会话文件信息保存失败`
### 2.2 åŽ†å²æ¶ˆæ¯æŽ¥å£ï¼ˆå‰ç«¯é‡ç‚¹ï¼‰
```http
GET /purchase-ai/history/messages/{memoryId}
```
消息对象新增可选字段 `filePaths`(仅用户消息可能有值):
```json
{
  "role": "user | assistant | system | tool | unknown",
  "content": "消息文本",
  "filePaths": [
    "/common/preview/xxx?publicKey=...",
    "/common/download/yyy?publicKey=..."
  ]
}
```
说明:
1. `filePaths` å¯èƒ½ç¼ºå¤±æˆ–为空(老会话 / æ™®é€šå¯¹è¯ï¼‰ã€‚
2. å•条用户消息可能包含多个附件路径。
3. è¯¥å­—段已按消息维度对齐,可直接用于历史回显。
---
## 3. å‰ç«¯æ”¹é€ å»ºè®®ï¼ˆæŒ‰å½“前实现落地)
### 3.1 åŽ†å²å“åº”æ¨¡åž‹
建议在历史消息原始结构中保留 `filePaths`:
```ts
type AiHistoryMessage = {
  role: string;
  content: string;
  filePaths?: string[];
};
```
### 3.2 UI æ¶ˆæ¯æ¨¡åž‹æ˜ å°„
若前端页面用的是 `localUploadFiles` æ¸²æŸ“附件卡片(如 `AIChatSidebar` å½“前实现),历史消息需做一次映射:
```ts
type LocalUploadFileItem = {
  previewId: string;
  name: string;
  size: number;
  type: string;
  isImage: boolean;
  previewUrl: string;
  rawFile: null;
};
```
映射规则建议:
1. ä»… `role === 'user'` ä¸” `filePaths?.length > 0` æ—¶ç”Ÿæˆ `localUploadFiles`。
2. `previewId` ç”¨ `${memoryId}-${messageIndex}-${fileIndex}` ç”Ÿæˆç¨³å®šå€¼ã€‚
3. `name` å¯ä»Ž URL è·¯å¾„解析;解析失败用 `file-{n}`。
4. `isImage` å¯æŒ‰æ‰©å±•名判断(`png/jpg/jpeg/gif/webp/bmp/svg`)。
5. å›¾ç‰‡çš„ `previewUrl` ç›´æŽ¥ä½¿ç”¨è·¯å¾„;非图片可置空并走图标展示。
### 3.3 æ¶ˆæ¯æ¸²æŸ“
对于用户消息:
1. æ­£å¸¸æ¸²æŸ“ `content`。
2. è‹¥å­˜åœ¨ `localUploadFiles`(或直接使用 `filePaths`),在消息下方渲染附件列表/卡片。
3. é“¾æŽ¥ç›´æŽ¥ä½¿ç”¨åŽç«¯è¿”回路径,不再拼接或二次改写。
> åŽç«¯å·²å®Œæˆâ€œé¢„览/下载路径”选择,前端只负责展示与打开。
### 3.4 å…¼å®¹è¦æ±‚
- `filePaths` ç¼ºå¤±ï¼šä¸æŠ¥é”™ï¼Œä¸æ¸²æŸ“附件区域。
- è€ä¼šè¯ï¼šç»§ç»­æŒ‰ `role/content` å±•示,不影响历史记录查看。
- å¤šé™„件:保持顺序展示,避免打乱用户上传顺序。
---
## 4. Mongo å­—段说明(后端已实现)
`chat_messages` æ–‡æ¡£æ–°å¢ž/使用字段:
- `analyzeUserQuestions: string[]`
- `analyzeFilePaths: string[]`(兼容旧字段)
- `analyzeFilePathGroups: string[][]`(推荐,按提问分组)
历史消息回传时,后端优先读取 `analyzeFilePathGroups`,并兼容 `analyzeFilePaths`。
---
## 5. è”调验收清单
1. æ–°å»ºä¼šè¯ï¼Œä¸Šä¼  1 å¼ å›¾ç‰‡ + 1 ä¸ª Excel,发送分析请求。
2. è°ƒç”¨ `GET /purchase-ai/history/messages/{memoryId}`,确认用户消息含 `filePaths` ä¸”数量正确。
3. åˆ·æ–°é¡µé¢åŽé‡æ–°è¿›å…¥åŒä¸€ä¼šè¯ï¼Œç¡®è®¤é™„件卡片可回显。
4. åˆ†åˆ«éªŒè¯ï¼š
   - å›¾ç‰‡/PDF å¯é¢„览访问
   - å…¶å®ƒæ–‡ä»¶å¯ä¸‹è½½è®¿é—®
5. éªŒè¯è€ä¼šè¯ï¼ˆæ—  `filePaths`)可正常展示,页面不报错。
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
@@ -11,8 +11,10 @@
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.service.AiFileTextExtractor;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.basic.service.StorageBlobService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
@@ -50,10 +52,14 @@
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Base64;
import java.util.Arrays;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@@ -63,9 +69,11 @@
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.nio.file.Files;
@Tag(name = "采购智能体")
@RestController
@@ -87,6 +95,7 @@
    private final IPurchaseLedgerService purchaseLedgerService;
    private final IPaymentRegistrationService paymentRegistrationService;
    private final PurchaseReturnOrdersService purchaseReturnOrdersService;
    private final StorageBlobService storageBlobService;
    private final SupplierManageMapper supplierManageMapper;
    private final StreamingChatLanguageModel purchaseVisionStreamingChatModel;
@@ -96,12 +105,13 @@
                                MongoChatMemoryStore mongoChatMemoryStore,
                                AiChatSessionService aiChatSessionService,
                                AiFileTextExtractor aiFileTextExtractor,
                                ObjectMapper objectMapper,
                                IPurchaseLedgerService purchaseLedgerService,
                                IPaymentRegistrationService paymentRegistrationService,
                                PurchaseReturnOrdersService purchaseReturnOrdersService,
                                SupplierManageMapper supplierManageMapper,
                                @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
                                 ObjectMapper objectMapper,
                                 IPurchaseLedgerService purchaseLedgerService,
                                 IPaymentRegistrationService paymentRegistrationService,
                                 PurchaseReturnOrdersService purchaseReturnOrdersService,
                                 StorageBlobService storageBlobService,
                                 SupplierManageMapper supplierManageMapper,
                                 @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
        this.purchaseAgent = purchaseAgent;
        this.purchaseIntentExecutor = purchaseIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
@@ -112,6 +122,7 @@
        this.purchaseLedgerService = purchaseLedgerService;
        this.paymentRegistrationService = paymentRegistrationService;
        this.purchaseReturnOrdersService = purchaseReturnOrdersService;
        this.storageBlobService = storageBlobService;
        this.supplierManageMapper = supplierManageMapper;
        this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel;
    }
@@ -172,6 +183,19 @@
                ? message
                : "请分析这些采购文件,提取可用于业务处理的数据,并整理成待客户确认的格式";
        List<String> filePaths;
        try {
            List<StorageBlobVO> uploadedFiles = storageBlobService.upload(copyFilesForUpload(files), true);
            filePaths = resolveFileAccessPaths(uploadedFiles);
        } catch (Exception ex) {
            return Flux.just("文件上传失败");
        }
        try {
            mongoChatMemoryStore.appendAnalyzeFileContext(finalMemoryId, finalMessage, filePaths);
        } catch (Exception ex) {
            return Flux.just("会话文件信息保存失败");
        }
        String fileContent;
        try {
            fileContent = buildMultiFileContent(files);
@@ -189,7 +213,7 @@
        aiChatSessionService.touchSession(finalMemoryId, loginUser, "采购多文件分析: " + finalMessage);
        if (containsImageFile(files)) {
            return chatWithPurchaseVisionModel(finalMemoryId, userPrompt, files)
            return chatWithPurchaseVisionModel(finalMemoryId, finalMessage, userPrompt, files)
                    .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
                    .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
        }
@@ -282,8 +306,121 @@
        return false;
    }
    private Flux<String> chatWithPurchaseVisionModel(String memoryId, String userPrompt, MultipartFile[] files) {
    private List<String> resolveFileAccessPaths(List<StorageBlobVO> uploadedFiles) {
        if (StringUtils.isEmpty(uploadedFiles)) {
            return Collections.emptyList();
        }
        List<String> filePaths = new ArrayList<>();
        for (StorageBlobVO uploadedFile : uploadedFiles) {
            if (uploadedFile == null) {
                continue;
            }
            String selectedPath;
            if (shouldUsePreviewPath(uploadedFile)) {
                selectedPath = StringUtils.hasText(uploadedFile.getPreviewURL())
                        ? uploadedFile.getPreviewURL()
                        : uploadedFile.getDownloadURL();
            } else {
                selectedPath = StringUtils.hasText(uploadedFile.getDownloadURL())
                        ? uploadedFile.getDownloadURL()
                        : uploadedFile.getPreviewURL();
            }
            if (StringUtils.hasText(selectedPath)) {
                filePaths.add(selectedPath);
            }
        }
        return filePaths;
    }
    private boolean shouldUsePreviewPath(StorageBlobVO uploadedFile) {
        String contentType = uploadedFile.getContentType();
        if (StringUtils.hasText(contentType)) {
            String normalized = contentType.toLowerCase(Locale.ROOT);
            if (normalized.startsWith("image/") || "application/pdf".equals(normalized)) {
                return true;
            }
        }
        String filename = uploadedFile.getOriginalFilename();
        if (!StringUtils.hasText(filename) || !filename.contains(".")) {
            return false;
        }
        String ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
        return StringUtils.inStringIgnoreCase(ext, "png", "jpg", "jpeg", "webp", "bmp", "pdf");
    }
    private List<MultipartFile> copyFilesForUpload(MultipartFile[] files) throws IOException {
        List<MultipartFile> copies = new ArrayList<>();
        for (MultipartFile file : files) {
            copies.add(new InMemoryMultipartFile(
                    file.getName(),
                    file.getOriginalFilename(),
                    file.getContentType(),
                    file.getBytes()
            ));
        }
        return copies;
    }
    private static final class InMemoryMultipartFile implements MultipartFile {
        private final String name;
        private final String originalFilename;
        private final String contentType;
        private final byte[] bytes;
        private InMemoryMultipartFile(String name, String originalFilename, String contentType, byte[] bytes) {
            this.name = name;
            this.originalFilename = originalFilename;
            this.contentType = contentType;
            this.bytes = bytes == null ? new byte[0] : bytes;
        }
        @Override
        public String getName() {
            return name;
        }
        @Override
        public String getOriginalFilename() {
            return originalFilename;
        }
        @Override
        public String getContentType() {
            return contentType;
        }
        @Override
        public boolean isEmpty() {
            return bytes.length == 0;
        }
        @Override
        public long getSize() {
            return bytes.length;
        }
        @Override
        public byte[] getBytes() {
            return bytes.clone();
        }
        @Override
        public InputStream getInputStream() {
            return new ByteArrayInputStream(bytes);
        }
        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            Files.write(dest.toPath(), bytes);
        }
    }
    private Flux<String> chatWithPurchaseVisionModel(String memoryId,
                                                     String userMessage,
                                                     String userPrompt,
                                                     MultipartFile[] files) {
        return Flux.create(sink -> {
            StringBuilder assistantReply = new StringBuilder();
            try {
                List<Content> contents = new ArrayList<>();
                contents.add(TextContent.from(userPrompt));
@@ -302,14 +439,21 @@
                        SystemMessage.from("你是采购业务文件分析助手。请从文本和图片中识别采购台账、采购产品明细、付款或退货信息,只输出合法 JSON。"),
                        UserMessage.from(contents)
                );
                safeAppendMessages(memoryId, List.of(UserMessage.from("采购多文件分析: " + userMessage)));
                purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() {
                    @Override
                    public void onPartialResponse(String partialResponse) {
                        sink.next(partialResponse);
                        if (partialResponse != null) {
                            assistantReply.append(partialResponse);
                            sink.next(partialResponse);
                        }
                    }
                    @Override
                    public void onCompleteResponse(ChatResponse completeResponse) {
                        if (StringUtils.hasText(assistantReply.toString())) {
                            safeAppendMessages(memoryId, List.of(AiMessage.from(assistantReply.toString())));
                        }
                        sink.complete();
                    }
@@ -325,6 +469,16 @@
        });
    }
    private void safeAppendMessages(String memoryId, List<ChatMessage> messages) {
        if (!StringUtils.hasText(memoryId) || StringUtils.isEmpty(messages)) {
            return;
        }
        try {
            mongoChatMemoryStore.appendMessages(memoryId, messages);
        } catch (Exception ignored) {
        }
    }
    private String resolveImageMimeType(MultipartFile file) {
        String contentType = file.getContentType();
        if (StringUtils.hasText(contentType) && contentType.startsWith("image/")) {
src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
@@ -1,15 +1,28 @@
package com.ruoyi.ai.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AiChatMessageDto {
    private String role;
    private String content;
    private List<String> filePaths;
    public AiChatMessageDto(String role, String content) {
        this.role = role;
        this.content = content;
    }
    public AiChatMessageDto(String role, String content, List<String> filePaths) {
        this.role = role;
        this.content = content;
        this.filePaths = filePaths;
    }
}
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
@@ -9,6 +9,7 @@
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@@ -24,6 +25,21 @@
    private String content;
    /**
     * å¤šæ–‡ä»¶åˆ†æžç”¨æˆ·æé—®ä¿¡æ¯ï¼ˆä¸Žæ–‡ä»¶è·¯å¾„分开存储)
     */
    private List<String> analyzeUserQuestions;
    /**
     * å¤šæ–‡ä»¶åˆ†æžä¸Šä¼ æ–‡ä»¶è·¯å¾„(图片和 pdf ä½¿ç”¨é¢„览地址,其他使用下载地址)
     */
    private List<String> analyzeFilePaths;
    /**
     * å¤šæ–‡ä»¶åˆ†æžæ¯æ¬¡æé—®å¯¹åº”的文件路径分组
     */
    private List<List<String>> analyzeFilePathGroups;
    private Date createTime;
    private Date updateTime;
src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
@@ -114,7 +114,10 @@
            return new LinkedList<>();
        }
        List<ChatMessage> messages = mongoChatMemoryStore.getMessages(memoryId);
        return messages.stream().map(this::convertMessage).collect(Collectors.toList());
        List<AiChatMessageDto> messageDtos = messages.stream().map(this::convertMessage).collect(Collectors.toList());
        List<List<String>> analyzeFilePathGroups = mongoChatMemoryStore.getAnalyzeFilePathGroups(memoryId);
        attachAnalyzeFilePaths(messageDtos, analyzeFilePathGroups);
        return messageDtos;
    }
    @Override
@@ -188,4 +191,22 @@
        }
        return new AiChatMessageDto("unknown", String.valueOf(message));
    }
    private void attachAnalyzeFilePaths(List<AiChatMessageDto> messages,
                                        List<List<String>> analyzeFilePathGroups) {
        if (StringUtils.isEmpty(messages) || StringUtils.isEmpty(analyzeFilePathGroups)) {
            return;
        }
        int analyzeIndex = 0;
        for (AiChatMessageDto message : messages) {
            if (!"user".equals(message.getRole()) || analyzeIndex >= analyzeFilePathGroups.size()) {
                continue;
            }
            List<String> filePaths = analyzeFilePathGroups.get(analyzeIndex);
            if (!StringUtils.isEmpty(filePaths)) {
                message.setFilePaths(filePaths);
            }
            analyzeIndex++;
        }
    }
}
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
@@ -11,6 +11,8 @@
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.LinkedList;
@@ -24,8 +26,7 @@
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId)));
        ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
        ChatMessages chatMessages = findChatMessages(memoryId);
        if (chatMessages == null || chatMessages.getContent() == null) {
            return new LinkedList<>();
        }
@@ -56,7 +57,75 @@
        updateMessages(memoryId, messages);
    }
    public void appendAnalyzeFileContext(Object memoryId, String userQuestion, List<String> filePaths) {
        String memoryIdValue = memoryIdString(memoryId);
        if (!StringUtils.hasText(memoryIdValue)) {
            return;
        }
        List<String> validFilePaths = new LinkedList<>();
        if (!CollectionUtils.isEmpty(filePaths)) {
            for (String filePath : filePaths) {
                if (StringUtils.hasText(filePath)) {
                    validFilePaths.add(filePath);
                }
            }
        }
        if (!StringUtils.hasText(userQuestion) && validFilePaths.isEmpty()) {
            return;
        }
        Query query = Query.query(Criteria.where("memoryId").is(memoryIdValue));
        Update update = new Update();
        update.set("memoryId", memoryIdValue);
        update.set("updateTime", new Date());
        update.setOnInsert("createTime", new Date());
        if (StringUtils.hasText(userQuestion)) {
            update.push("analyzeUserQuestions", userQuestion);
        }
        if (!validFilePaths.isEmpty()) {
            update.push("analyzeFilePaths").each(validFilePaths.toArray());
            update.push("analyzeFilePathGroups", validFilePaths);
        }
        mongoTemplate.upsert(query, update, ChatMessages.class);
    }
    public List<String> getAnalyzeUserQuestions(Object memoryId) {
        ChatMessages chatMessages = findChatMessages(memoryId);
        if (chatMessages == null || CollectionUtils.isEmpty(chatMessages.getAnalyzeUserQuestions())) {
            return new LinkedList<>();
        }
        return new LinkedList<>(chatMessages.getAnalyzeUserQuestions());
    }
    public List<List<String>> getAnalyzeFilePathGroups(Object memoryId) {
        ChatMessages chatMessages = findChatMessages(memoryId);
        if (chatMessages == null) {
            return new LinkedList<>();
        }
        if (CollectionUtils.isEmpty(chatMessages.getAnalyzeFilePathGroups())) {
            if (CollectionUtils.isEmpty(chatMessages.getAnalyzeFilePaths())) {
                return new LinkedList<>();
            }
            List<List<String>> fallback = new LinkedList<>();
            fallback.add(new LinkedList<>(chatMessages.getAnalyzeFilePaths()));
            return fallback;
        }
        List<List<String>> groups = new LinkedList<>();
        for (List<String> group : chatMessages.getAnalyzeFilePathGroups()) {
            if (CollectionUtils.isEmpty(group)) {
                groups.add(new LinkedList<>());
            } else {
                groups.add(new LinkedList<>(group));
            }
        }
        return groups;
    }
    private String memoryIdString(Object memoryId) {
        return memoryId == null ? "" : memoryId.toString();
    }
    private ChatMessages findChatMessages(Object memoryId) {
        Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId)));
        return mongoTemplate.findOne(query, ChatMessages.class);
    }
}
src/main/resources/application-dev.yml
@@ -254,8 +254,18 @@
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: D:/ruoyi/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: D:/ruoyi/prod/uploads # æ­£å¼ç›®å½•
  temp-dir: D:/ruoyi/temp/uploads   # ä¸´æ—¶ç›®å½• åŽæœŸåˆ é™¤
  upload-dir: D:/ruoyi/prod/uploads # æ­£å¼ç›®å½• åŽæœŸåˆ é™¤
  path: D:/ruoyi/prod/uploads # ä¸Šä¼ ç›®å½•
  urlPrefix: /common # é“¾æŽ¥å‰ç¼€
  domain: http://127.0.0.1:7005 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
knowledge:
  one: D:\新疆大罗素企业产品体系说明文档.md