From ea5a55deffa6d33048a1f7e03b71424c8add5e31 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 08 五月 2026 14:51:52 +0800
Subject: [PATCH] feat(ai): 实现采购多文件分析附件存储与历史回显功能

---
 src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java       |  172 ++++++++++++++++++++-
 doc/20260508_采购多文件分析附件存储与历史回显联调说明.md                                  |  149 ++++++++++++++++++
 src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java              |   16 ++
 src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java |   23 ++
 src/main/resources/application-dev.yml                                |   14 +
 src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java                  |   17 +
 src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java            |   73 ++++++++
 7 files changed, 448 insertions(+), 16 deletions(-)

diff --git "a/doc/20260508_\351\207\207\350\264\255\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\351\231\204\344\273\266\345\255\230\345\202\250\344\270\216\345\216\206\345\217\262\345\233\236\346\230\276\350\201\224\350\260\203\350\257\264\346\230\216.md" "b/doc/20260508_\351\207\207\350\264\255\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\351\231\204\344\273\266\345\255\230\345\202\250\344\270\216\345\216\206\345\217\262\345\233\236\346\230\276\350\201\224\350\260\203\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..c883a22
--- /dev/null
+++ "b/doc/20260508_\351\207\207\350\264\255\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\351\231\204\344\273\266\345\255\230\345\202\250\344\270\216\345\216\206\345\217\262\345\233\236\346\230\276\350\201\224\350\260\203\350\257\264\346\230\216.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. 缁х画鎵ц鍘熸湁鏂囦欢瑙f瀽鍜� 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 璺緞瑙f瀽锛涜В鏋愬け璐ョ敤 `file-{n}`銆�
+4. `isImage` 鍙寜鎵╁睍鍚嶅垽鏂紙`png/jpg/jpeg/gif/webp/bmp/svg`锛夈��
+5. 鍥剧墖鐨� `previewUrl` 鐩存帴浣跨敤璺緞锛涢潪鍥剧墖鍙疆绌哄苟璧板浘鏍囧睍绀恒��
+
+### 3.3 娑堟伅娓叉煋
+
+瀵逛簬鐢ㄦ埛娑堟伅锛�
+
+1. 姝e父娓叉煋 `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`锛夊彲姝e父灞曠ず锛岄〉闈笉鎶ラ敊銆�
diff --git a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
index 2bb0625..08dc357 100644
--- a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
+++ b/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/")) {
diff --git a/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java b/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
index 9242b3e..081bf12 100644
--- a/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
+++ b/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;
+    }
 }
diff --git a/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java b/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
index 0a239d8..e2944cb 100644
--- a/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
+++ b/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;
diff --git a/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java b/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
index 6d8c945..0ea436d 100644
--- a/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
+++ b/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++;
+        }
+    }
 }
diff --git a/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java b/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
index e88f0e9..e6bcadb 100644
--- a/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
+++ b/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);
+    }
 }
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index ef003c9..334e5d7 100644
--- a/src/main/resources/application-dev.yml
+++ b/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 # 姝e紡鐩綍
+  temp-dir: D:/ruoyi/temp/uploads   # 涓存椂鐩綍 鍚庢湡鍒犻櫎
+  upload-dir: D:/ruoyi/prod/uploads # 姝e紡鐩綍 鍚庢湡鍒犻櫎
+  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
+

--
Gitblit v1.9.3