diff --git a/pom.xml b/pom.xml
index 92a7cde..8eb8a05 100644
--- a/pom.xml
+++ b/pom.xml
@@ -113,7 +113,18 @@
org.springframework.ai
spring-ai-tika-document-reader
-
+
+
+ cn.hutool
+ hutool-all
+ 5.8.40
+
+
+
+ com.qcloud
+ cos_api
+ 5.6.227
+
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/config/ChatClientConfiguration.java b/snails-chat/src/main/java/com/hanserwei/chat/config/ChatClientConfiguration.java
index 9154f57..05143c9 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/config/ChatClientConfiguration.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/config/ChatClientConfiguration.java
@@ -3,7 +3,6 @@ package com.hanserwei.chat.config;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import com.hanserwei.chat.tools.AiDBTools;
-import com.hanserwei.chat.tools.SaveDocumentsTools;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
@@ -14,7 +13,6 @@ import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.cassandra.CassandraChatMemoryRepository;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
-import org.springframework.aot.hint.annotation.RegisterReflection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -52,14 +50,13 @@ public class ChatClientConfiguration {
}
@Bean
- public ChatClient dashScopeChatClient(ChatMemory chatMemory,
- SaveDocumentsTools saveDocumentsTools
+ public ChatClient dashScopeChatClient(ChatMemory chatMemory
) {
PromptChatMemoryAdvisor chatMemoryAdvisor = PromptChatMemoryAdvisor.builder(chatMemory).build();
SimpleLoggerAdvisor simpleLoggerAdvisor = new SimpleLoggerAdvisor();
QuestionAnswerAdvisor questionAnswerAdvisor = new QuestionAnswerAdvisor(vectorStore);
return ChatClient.builder(dashScopeChatModel)
- .defaultTools(aiDBTools, saveDocumentsTools)
+ .defaultTools(aiDBTools)
.defaultSystem(aiAssistantResource)
.defaultAdvisors(chatMemoryAdvisor,simpleLoggerAdvisor,questionAnswerAdvisor)
.build();
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/config/cos/CosConfig.java b/snails-chat/src/main/java/com/hanserwei/chat/config/cos/CosConfig.java
new file mode 100644
index 0000000..78447eb
--- /dev/null
+++ b/snails-chat/src/main/java/com/hanserwei/chat/config/cos/CosConfig.java
@@ -0,0 +1,48 @@
+package com.hanserwei.chat.config.cos;
+
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.ClientConfig;
+import com.qcloud.cos.auth.BasicCOSCredentials;
+import com.qcloud.cos.auth.COSCredentials;
+import com.qcloud.cos.endpoint.EndpointBuilder;
+import com.qcloud.cos.region.Region;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class CosConfig {
+
+ @Resource
+ private CosProperties cosProperties;
+
+ @Bean
+ public COSClient cosClient() {
+ // 1. 初始化用户身份信息(SecretId, SecretKey)
+ COSCredentials cred = new BasicCOSCredentials(
+ cosProperties.getSecretId(),
+ cosProperties.getSecretKey()
+ );
+
+ // 2. 设置 bucket 的地域
+ Region region = new Region(cosProperties.getRegion());
+ ClientConfig clientConfig = new ClientConfig(region);
+ if (cosProperties.getEndpoint() != null && !cosProperties.getEndpoint().isEmpty()) {
+ clientConfig.setEndpointBuilder(new EndpointBuilder() {
+ @Override
+ public String buildGeneralApiEndpoint(String bucketName) {
+ // 所有 API 请求都会使用自定义域名
+ return cosProperties.getEndpoint();
+ }
+
+ @Override
+ public String buildGetServiceApiEndpoint() {
+ return cosProperties.getEndpoint();
+ }
+ });
+ }
+
+ // 3. 构建 COSClient
+ return new COSClient(cred, clientConfig);
+ }
+}
\ No newline at end of file
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/config/cos/CosProperties.java b/snails-chat/src/main/java/com/hanserwei/chat/config/cos/CosProperties.java
new file mode 100644
index 0000000..d0539bf
--- /dev/null
+++ b/snails-chat/src/main/java/com/hanserwei/chat/config/cos/CosProperties.java
@@ -0,0 +1,16 @@
+package com.hanserwei.chat.config.cos;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@Data
+@ConfigurationProperties(prefix = "storage.cos")
+public class CosProperties {
+ private String endpoint;
+ private String secretId;
+ private String secretKey;
+ private String appId;
+ private String region;
+}
\ No newline at end of file
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/controller/AiChatController.java b/snails-chat/src/main/java/com/hanserwei/chat/controller/AiChatController.java
index e53ed3a..ea4718b 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/controller/AiChatController.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/controller/AiChatController.java
@@ -7,10 +7,21 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.content.Media;
import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.util.MimeTypeUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
+import java.net.URI;
+import java.util.List;
+import java.util.Objects;
+
@Slf4j
@RestController
@RequestMapping("/ai")
@@ -23,9 +34,20 @@ public class AiChatController {
public Flux chatWithAi(@RequestBody ChatMessageDTO chatMessageDTO) {
log.info("会话ID:{}", chatMessageDTO.getConversionId());
ConversationContext.setConversationId(chatMessageDTO.getConversionId());
-
- return dashScopeChatClient.prompt()
- .user(chatMessageDTO.getMessage())
+ Media media;
+ UserMessage userMessage;
+ if (Objects.nonNull(chatMessageDTO.getImage())) {
+ media = new Media(MimeTypeUtils.IMAGE_PNG, URI.create(chatMessageDTO.getImage()));
+ userMessage = UserMessage.builder()
+ .media(media)
+ .text(chatMessageDTO.getMessage())
+ .build();
+ } else {
+ userMessage = UserMessage.builder()
+ .text(chatMessageDTO.getMessage())
+ .build();
+ }
+ return dashScopeChatClient.prompt(new Prompt(List.of(userMessage)))
.advisors(p -> p.param(ChatMemory.CONVERSATION_ID, chatMessageDTO.getConversionId()))
.stream()
.chatResponse()
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/controller/DocumentController.java b/snails-chat/src/main/java/com/hanserwei/chat/controller/DocumentController.java
index 5505bdc..4281058 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/controller/DocumentController.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/controller/DocumentController.java
@@ -22,8 +22,8 @@ public class DocumentController {
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity upload(@RequestParam("file") MultipartFile file) {
- int documentCount = documentIngestionService.ingest(file);
+ String result = documentIngestionService.ingest(file);
log.info("文件 {} 上传成功。", file.getOriginalFilename());
- return ResponseEntity.ok(new DocumentUploadResponse(file.getOriginalFilename(), documentCount, "ok"));
+ return ResponseEntity.ok(new DocumentUploadResponse(file.getOriginalFilename(), result));
}
}
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/model/dto/ChatMessageDTO.java b/snails-chat/src/main/java/com/hanserwei/chat/model/dto/ChatMessageDTO.java
index 56090d5..d8e6671 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/model/dto/ChatMessageDTO.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/model/dto/ChatMessageDTO.java
@@ -5,6 +5,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.util.List;
+
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -13,5 +15,7 @@ public class ChatMessageDTO {
private String message;
+ private String image;
+
private Long conversionId;
}
\ No newline at end of file
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/model/vo/DocumentUploadResponse.java b/snails-chat/src/main/java/com/hanserwei/chat/model/vo/DocumentUploadResponse.java
index aafc3e9..80eb31e 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/model/vo/DocumentUploadResponse.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/model/vo/DocumentUploadResponse.java
@@ -1,4 +1,4 @@
package com.hanserwei.chat.model.vo;
-public record DocumentUploadResponse(String filename, int documentCount, String message) {
+public record DocumentUploadResponse(String filename, String message) {
}
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/reader/MyHtmlReader.java b/snails-chat/src/main/java/com/hanserwei/chat/reader/MyHtmlReader.java
index 50aaef5..fad2014 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/reader/MyHtmlReader.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/reader/MyHtmlReader.java
@@ -7,6 +7,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
+import java.util.Objects;
@Component
public class MyHtmlReader implements DocumentParser {
@@ -19,7 +20,7 @@ public class MyHtmlReader implements DocumentParser {
.charset("UTF-8") // 使用 UTF-8 编码
.includeLinkUrls(true) // 在元数据中包含链接 URL(绝对链接)
.metadataTags(List.of("author", "date")) // 提取 author 和 date 元标签
- .additionalMetadata("source", file.getOriginalFilename()) // 添加自定义元数据
+ .additionalMetadata("source", Objects.requireNonNull(file.getOriginalFilename())) // 添加自定义元数据
.build();
// 新建 JsoupDocumentReader 阅读器
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/reader/MyMarkdownReader.java b/snails-chat/src/main/java/com/hanserwei/chat/reader/MyMarkdownReader.java
index 9c4a702..09849e6 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/reader/MyMarkdownReader.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/reader/MyMarkdownReader.java
@@ -7,6 +7,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
+import java.util.Objects;
@Component
public class MyMarkdownReader implements DocumentParser {
@@ -18,7 +19,7 @@ public class MyMarkdownReader implements DocumentParser {
.withHorizontalRuleCreateDocument(true) // 遇到水平线 ---,则创建新文档
.withIncludeCodeBlock(false) // 排除代码块(代码块生成单独文档)
.withIncludeBlockquote(false) // 排除块引用(块引用生成单独文档)
- .withAdditionalMetadata("filename", file.getOriginalFilename()) // 添加自定义元数据,如文件名称
+ .withAdditionalMetadata("filename", Objects.requireNonNull(file.getOriginalFilename())) // 添加自定义元数据,如文件名称
.build();
// 新建 MarkdownDocumentReader 阅读器
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/service/DocumentIngestionService.java b/snails-chat/src/main/java/com/hanserwei/chat/service/DocumentIngestionService.java
index b90192b..b2e4521 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/service/DocumentIngestionService.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/service/DocumentIngestionService.java
@@ -4,5 +4,5 @@ import org.springframework.web.multipart.MultipartFile;
public interface DocumentIngestionService {
- int ingest(MultipartFile file);
+ String ingest(MultipartFile file);
}
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/service/impl/DocumentIngestionServiceImpl.java b/snails-chat/src/main/java/com/hanserwei/chat/service/impl/DocumentIngestionServiceImpl.java
index ee57329..9394fba 100644
--- a/snails-chat/src/main/java/com/hanserwei/chat/service/impl/DocumentIngestionServiceImpl.java
+++ b/snails-chat/src/main/java/com/hanserwei/chat/service/impl/DocumentIngestionServiceImpl.java
@@ -1,43 +1,111 @@
package com.hanserwei.chat.service.impl;
+import cn.hutool.core.io.FileUtil;
+import com.hanserwei.chat.config.cos.CosProperties;
import com.hanserwei.chat.reader.DocumentParser;
import com.hanserwei.chat.service.DocumentIngestionService;
-import lombok.RequiredArgsConstructor;
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.model.ObjectMetadata;
+import jakarta.annotation.Resource;
+import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
+import java.io.InputStream;
import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
@Slf4j
@Service
-@RequiredArgsConstructor
+
public class DocumentIngestionServiceImpl implements DocumentIngestionService {
- private final List documentParsers;
- private final VectorStore vectorStore;
+ @Resource
+ private List documentParsers;
+ @Resource
+ private VectorStore vectorStore;
+ @Resource
+ private COSClient cosClient;
+ @Resource
+ private CosProperties cosProperties;
@Override
- public int ingest(MultipartFile file) {
+ public String ingest(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
+ // 如果是图片,则不进行向量化,选择上传到图片存储桶
+ String originalFilename = file.getOriginalFilename();
+ String extName = Objects.requireNonNull(FileUtil.extName(originalFilename)).toLowerCase();
+
+ // 判断是否为图片类型
+ boolean isImage = extName.matches("jpg|jpeg|png|gif|bmp|webp");
+ if (isImage) {
+ log.info("上传文件 {} 为图片,不进行向量化。", originalFilename);
+ return uploadFile(file);
+ }
+
+
DocumentParser parser = documentParsers.stream()
- .filter(candidate -> candidate.supports(file.getOriginalFilename(), file.getContentType()))
+ .filter(candidate -> candidate.supports(originalFilename, file.getContentType()))
.findFirst()
- .orElseThrow(() -> new IllegalArgumentException("不支持的文件类型:" + file.getOriginalFilename()));
+ .orElseThrow(() -> new IllegalArgumentException("不支持的文件类型:" + originalFilename));
List documents = parser.parse(file);
if (documents.isEmpty()) {
- log.warn("文件 {} 解析后未生成任何文档,跳过入库。", file.getOriginalFilename());
- return 0;
+ log.warn("文件 {} 解析后未生成任何文档,跳过入库。", originalFilename);
+ return "";
}
vectorStore.add(documents);
- log.info("文件 {} 入库成功,共写入 {} 条向量。", file.getOriginalFilename(), documents.size());
- return documents.size();
+ log.info("文件 {} 入库成功,共写入 {} 条向量。", originalFilename, documents.size());
+ return documents.size() + "";
+ }
+
+ @SneakyThrows
+ public String uploadFile(MultipartFile file) {
+ log.info("## 上传文件至腾讯云Cos ...");
+
+ // 判断文件是否为空
+ if (file == null || file.getSize() == 0) {
+ log.error("==> 上传文件异常:文件大小为空 ...");
+ throw new RuntimeException("文件大小不能为空");
+ }
+
+ // 文件的原始名称
+ String originalFileName = file.getOriginalFilename();
+
+ // 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串)
+ String key = UUID.randomUUID().toString().replace("-", "");
+ // 获取文件的后缀,如 .jpg
+ String suffix = null;
+ if (originalFileName != null) {
+ suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
+ }
+
+ // 拼接上文件后缀,即为要存储的文件名
+ String objectName = String.format("%s%s", key, suffix);
+
+ log.info("==> 开始上传文件至腾讯云Cos, ObjectName: {}", objectName);
+
+ // 设置元数据
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(file.getSize());
+ metadata.setContentType(file.getContentType());
+
+ // 执行上传
+ try (InputStream inputStream = file.getInputStream()) {
+ cosClient.putObject("snails-ai-1308845726", objectName, inputStream, metadata);
+ }
+
+ // 返回文件的访问链接
+ String url = String.format("https://%s/%s", cosProperties.getEndpoint(), objectName);
+ log.info("==> 上传文件至腾讯云 Cos 成功,访问路径: {}", url);
+ return url;
}
}
diff --git a/snails-chat/src/main/java/com/hanserwei/chat/tools/SaveDocumentsTools.java b/snails-chat/src/main/java/com/hanserwei/chat/tools/SaveDocumentsTools.java
deleted file mode 100644
index fdd3837..0000000
--- a/snails-chat/src/main/java/com/hanserwei/chat/tools/SaveDocumentsTools.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.hanserwei.chat.tools;
-
-import jakarta.annotation.Resource;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.ai.document.Document;
-import org.springframework.ai.tool.annotation.Tool;
-import org.springframework.ai.tool.annotation.ToolParam;
-import org.springframework.ai.vectorstore.SimpleVectorStore;
-import org.springframework.stereotype.Component;
-
-import java.io.File;
-import java.util.List;
-
-@Slf4j
-@Component
-public class SaveDocumentsTools {
-
- @Resource
- private SimpleVectorStore simpleVectorStore;
-
- @Tool(name = "SaveDocuments",
- description = "保存文档为本地的向量化知识库。")
- public boolean saveDocuments(
- @ToolParam(description = "文档内容") String documents
- ) {
- simpleVectorStore.add(List.of(new Document(documents)));
- //把内存中的向量数据,持久化到磁盘
- File file = new File("/home/hanserwei/IdeaProjects/snails-ai-backend/snails-chat/src/main/resources/documents/vector.json");
- simpleVectorStore.save(file);
-
- return true;
- }
-}
diff --git a/snails-chat/src/main/resources/config/application.yml b/snails-chat/src/main/resources/config/application.yml
index 740b3b2..0e3d064 100644
--- a/snails-chat/src/main/resources/config/application.yml
+++ b/snails-chat/src/main/resources/config/application.yml
@@ -5,6 +5,10 @@ server:
charset: utf-8
force: true
spring:
+ servlet:
+ multipart:
+ max-file-size: 20MB
+ max-request-size: 100MB
application:
name: snails-ai
banner:
@@ -60,8 +64,9 @@ spring:
api-key: ENC(cMgcKZkFllyE88DIbGwLKot9Vg02co+gsmY8L8o4/o3UjhcmqO4lJzFU35Sx0n+qFG8pDL0wBjoWrT8X6BuRw9vNlQhY1LgRWHaF9S1zzyM=)
chat:
options:
- model: qwen-plus
+ model: qwen3-omni-flash
temperature: 0.5
+ multi-model: true
embedding:
options:
model: text-embedding-v4
@@ -83,4 +88,11 @@ jasypt:
encryptor:
password: ${jasypt.encryptor.password}
algorithm: PBEWithHMACSHA512AndAES_256
- iv-generator-classname: org.jasypt.iv.RandomIvGenerator
\ No newline at end of file
+ iv-generator-classname: org.jasypt.iv.RandomIvGenerator
+storage:
+ cos:
+ endpoint: snails-ai-1308845726.cos.ap-chengdu.myqcloud.com
+ appId: 1308845726
+ region: ap-chengdu
+ secretId: ENC(nbQSc/HpYon4HMw0sVHOB/mIkC3Z0r2bZ2ndI/V0GahiBN1Hc3Ki4+CDzch6dn+2AksNzvdazHyoiaozaUIpC9t/QGiAJ7Mdbdwpl6/F6S4=)
+ secretKey: ENC(f6Rkk1rf6DwEYCyW0xV584+fShN7c/fGvWbRdWnp46/MHk/EmOIJ5HHxhT+VtvdM6XXNSIprDmggPCdaOwdmOpdWzpoMnidnTsmSUsRw5NA=)
\ No newline at end of file