| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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 |
| | |
| | | private final IPurchaseLedgerService purchaseLedgerService; |
| | | private final IPaymentRegistrationService paymentRegistrationService; |
| | | private final PurchaseReturnOrdersService purchaseReturnOrdersService; |
| | | private final StorageBlobService storageBlobService; |
| | | private final SupplierManageMapper supplierManageMapper; |
| | | private final StreamingChatLanguageModel purchaseVisionStreamingChatModel; |
| | | |
| | |
| | | IPurchaseLedgerService purchaseLedgerService, |
| | | IPaymentRegistrationService paymentRegistrationService, |
| | | PurchaseReturnOrdersService purchaseReturnOrdersService, |
| | | StorageBlobService storageBlobService, |
| | | SupplierManageMapper supplierManageMapper, |
| | | @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) { |
| | | this.purchaseAgent = purchaseAgent; |
| | |
| | | this.purchaseLedgerService = purchaseLedgerService; |
| | | this.paymentRegistrationService = paymentRegistrationService; |
| | | this.purchaseReturnOrdersService = purchaseReturnOrdersService; |
| | | this.storageBlobService = storageBlobService; |
| | | this.supplierManageMapper = supplierManageMapper; |
| | | this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel; |
| | | } |
| | |
| | | ? 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); |
| | |
| | | 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)); |
| | | } |
| | |
| | | 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)); |
| | |
| | | SystemMessage.from("你是采购业务文件分析助手。请从文本和图片中识别采购台账、采购产品明细、付款或退货信息,只输出合法 JSON。"), |
| | | UserMessage.from(contents) |
| | | ); |
| | | safeAppendMessages(memoryId, List.of(UserMessage.from("采购多文件分析: " + userMessage))); |
| | | purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() { |
| | | @Override |
| | | public void onPartialResponse(String 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(); |
| | | } |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | 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/")) { |