Commit a5a0383f10af82df8154ced0eb6d3e15e05e1fee
1 parent
3578e0bb
feat:【ai 大模型】对话增加附件的支持
Showing
6 changed files
with
133 additions
and
19 deletions
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
| @@ -20,6 +20,30 @@ tenant-id: {{adminTenantId}} | @@ -20,6 +20,30 @@ tenant-id: {{adminTenantId}} | ||
| 20 | "content": "1+1=?" | 20 | "content": "1+1=?" |
| 21 | } | 21 | } |
| 22 | 22 | ||
| 23 | +### 发送消息(流式)【带文件】 | ||
| 24 | +POST {{baseUrl}}/ai/chat/message/send-stream | ||
| 25 | +Content-Type: application/json | ||
| 26 | +Authorization: {{token}} | ||
| 27 | +tenant-id: {{adminTenantId}} | ||
| 28 | + | ||
| 29 | +{ | ||
| 30 | + "conversationId": "1781604279872581797", | ||
| 31 | + "content": "图片里有什么?", | ||
| 32 | + "attachmentUrls": ["http://test.yudao.iocoder.cn/1755531278.jpeg"] | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +### 发送消息(流式)【追问带文件】 | ||
| 36 | +POST {{baseUrl}}/ai/chat/message/send-stream | ||
| 37 | +Content-Type: application/json | ||
| 38 | +Authorization: {{token}} | ||
| 39 | +tenant-id: {{adminTenantId}} | ||
| 40 | + | ||
| 41 | +{ | ||
| 42 | + "conversationId": "1781604279872581797", | ||
| 43 | + "content": "说下图片里,有哪些字?", | ||
| 44 | + "useContext": true | ||
| 45 | +} | ||
| 46 | + | ||
| 23 | ### 获得指定对话的消息列表 | 47 | ### 获得指定对话的消息列表 |
| 24 | GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649 | 48 | GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649 |
| 25 | Authorization: {{token}} | 49 | Authorization: {{token}} |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
| @@ -49,6 +49,9 @@ public class AiChatMessageRespVO { | @@ -49,6 +49,9 @@ public class AiChatMessageRespVO { | ||
| 49 | @Schema(description = "知识库段落数组") | 49 | @Schema(description = "知识库段落数组") |
| 50 | private List<KnowledgeSegment> segments; | 50 | private List<KnowledgeSegment> segments; |
| 51 | 51 | ||
| 52 | + @Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png") | ||
| 53 | + private List<String> attachmentUrls; | ||
| 54 | + | ||
| 52 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51") | 55 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51") |
| 53 | private LocalDateTime createTime; | 56 | private LocalDateTime createTime; |
| 54 | 57 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
| @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; | @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | import jakarta.validation.constraints.NotEmpty; | 4 | import jakarta.validation.constraints.NotEmpty; |
| 5 | import jakarta.validation.constraints.NotNull; | 5 | import jakarta.validation.constraints.NotNull; |
| 6 | -import jakarta.validation.constraints.Size; | ||
| 7 | import lombok.Data; | 6 | import lombok.Data; |
| 8 | -import lombok.experimental.Accessors; | 7 | + |
| 8 | +import java.util.List; | ||
| 9 | 9 | ||
| 10 | @Schema(description = "管理后台 - AI 聊天消息发送 Request VO") | 10 | @Schema(description = "管理后台 - AI 聊天消息发送 Request VO") |
| 11 | @Data | 11 | @Data |
| @@ -22,4 +22,7 @@ public class AiChatMessageSendReqVO { | @@ -22,4 +22,7 @@ public class AiChatMessageSendReqVO { | ||
| 22 | @Schema(description = "是否携带上下文", example = "true") | 22 | @Schema(description = "是否携带上下文", example = "true") |
| 23 | private Boolean useContext; | 23 | private Boolean useContext; |
| 24 | 24 | ||
| 25 | + @Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png") | ||
| 26 | + private List<String> attachmentUrls; | ||
| 27 | + | ||
| 25 | } | 28 | } |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; | 4 | import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; |
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; | ||
| 5 | import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; | 6 | import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; |
| 6 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; |
| 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; | 8 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; |
| @@ -105,4 +106,10 @@ public class AiChatMessageDO extends BaseDO { | @@ -105,4 +106,10 @@ public class AiChatMessageDO extends BaseDO { | ||
| 105 | @TableField(typeHandler = LongListTypeHandler.class) | 106 | @TableField(typeHandler = LongListTypeHandler.class) |
| 106 | private List<Long> segmentIds; | 107 | private List<Long> segmentIds; |
| 107 | 108 | ||
| 109 | + /** | ||
| 110 | + * 附件 URL 数组 | ||
| 111 | + */ | ||
| 112 | + @TableField(typeHandler = StringListTypeHandler.class) | ||
| 113 | + private List<String> attachmentUrls; | ||
| 114 | + | ||
| 108 | } | 115 | } |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
| 1 | package cn.iocoder.yudao.module.ai.service.chat; | 1 | package cn.iocoder.yudao.module.ai.service.chat; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.codec.Base64; | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 4 | import cn.hutool.core.collection.CollUtil; |
| 5 | +import cn.hutool.core.io.file.FileNameUtil; | ||
| 4 | import cn.hutool.core.util.ObjUtil; | 6 | import cn.hutool.core.util.ObjUtil; |
| 5 | import cn.hutool.core.util.StrUtil; | 7 | import cn.hutool.core.util.StrUtil; |
| 8 | +import cn.hutool.http.HttpUtil; | ||
| 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 9 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 10 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 8 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 11 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| @@ -28,6 +31,8 @@ import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; | @@ -28,6 +31,8 @@ import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; | ||
| 28 | import cn.iocoder.yudao.module.ai.service.model.AiModelService; | 31 | import cn.iocoder.yudao.module.ai.service.model.AiModelService; |
| 29 | import cn.iocoder.yudao.module.ai.service.model.AiToolService; | 32 | import cn.iocoder.yudao.module.ai.service.model.AiToolService; |
| 30 | import cn.iocoder.yudao.module.ai.util.AiUtils; | 33 | import cn.iocoder.yudao.module.ai.util.AiUtils; |
| 34 | +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; | ||
| 35 | +import com.google.common.collect.Maps; | ||
| 31 | import jakarta.annotation.Resource; | 36 | import jakarta.annotation.Resource; |
| 32 | import lombok.extern.slf4j.Slf4j; | 37 | import lombok.extern.slf4j.Slf4j; |
| 33 | import org.springframework.ai.chat.messages.Message; | 38 | import org.springframework.ai.chat.messages.Message; |
| @@ -64,6 +69,8 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N | @@ -64,6 +69,8 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N | ||
| 64 | @Slf4j | 69 | @Slf4j |
| 65 | public class AiChatMessageServiceImpl implements AiChatMessageService { | 70 | public class AiChatMessageServiceImpl implements AiChatMessageService { |
| 66 | 71 | ||
| 72 | + // TODO @芋艿:后续优化下对话的 Prompt 整体结构 | ||
| 73 | + | ||
| 67 | /** | 74 | /** |
| 68 | * 知识库转 {@link UserMessage} 的内容模版 | 75 | * 知识库转 {@link UserMessage} 的内容模版 |
| 69 | */ | 76 | */ |
| @@ -71,6 +78,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -71,6 +78,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 71 | "%s\n\n" + // 多个 <Reference></Reference> 的拼接 | 78 | "%s\n\n" + // 多个 <Reference></Reference> 的拼接 |
| 72 | "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。"; | 79 | "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。"; |
| 73 | 80 | ||
| 81 | + /** | ||
| 82 | + * 附件转 ${@link UserMessage} 的内容模版 | ||
| 83 | + */ | ||
| 84 | + @SuppressWarnings("TextBlockMigration") | ||
| 85 | + private static final String Attachment_USER_MESSAGE_TEMPLATE = "使用 <Attachment></Attachment> 标记用户对话上传的附件内容:\n\n" + | ||
| 86 | + "%s\n\n" + // 多个 <Attachment></Attachment> 的拼接 | ||
| 87 | + "回答要求:\n- 避免提及 <Attachment></Attachment> 附件的编码格式。"; | ||
| 88 | + | ||
| 74 | @Resource | 89 | @Resource |
| 75 | private AiChatMessageMapper chatMessageMapper; | 90 | private AiChatMessageMapper chatMessageMapper; |
| 76 | 91 | ||
| @@ -101,17 +116,18 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -101,17 +116,18 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 101 | ChatModel chatModel = modalService.getChatModel(model.getId()); | 116 | ChatModel chatModel = modalService.getChatModel(model.getId()); |
| 102 | 117 | ||
| 103 | // 2. 知识库找回 | 118 | // 2. 知识库找回 |
| 104 | - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), conversation); | 119 | + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment( |
| 120 | + sendReqVO.getContent(), conversation); | ||
| 105 | 121 | ||
| 106 | // 3. 插入 user 发送消息 | 122 | // 3. 插入 user 发送消息 |
| 107 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, | 123 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, |
| 108 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), | 124 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), |
| 109 | - null); | 125 | + null, sendReqVO.getAttachmentUrls()); |
| 110 | 126 | ||
| 111 | // 3.1 插入 assistant 接收消息 | 127 | // 3.1 插入 assistant 接收消息 |
| 112 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, | 128 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, |
| 113 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), | 129 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), |
| 114 | - knowledgeSegments); | 130 | + knowledgeSegments, null); |
| 115 | 131 | ||
| 116 | // 3.2 创建 chat 需要的 Prompt | 132 | // 3.2 创建 chat 需要的 Prompt |
| 117 | Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); | 133 | Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); |
| @@ -151,18 +167,18 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -151,18 +167,18 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 151 | StreamingChatModel chatModel = modalService.getChatModel(model.getId()); | 167 | StreamingChatModel chatModel = modalService.getChatModel(model.getId()); |
| 152 | 168 | ||
| 153 | // 2. 知识库找回 | 169 | // 2. 知识库找回 |
| 154 | - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), | ||
| 155 | - conversation); | 170 | + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment( |
| 171 | + sendReqVO.getContent(), conversation); | ||
| 156 | 172 | ||
| 157 | // 3. 插入 user 发送消息 | 173 | // 3. 插入 user 发送消息 |
| 158 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, | 174 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, |
| 159 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), | 175 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), |
| 160 | - null); | 176 | + null, sendReqVO.getAttachmentUrls()); |
| 161 | 177 | ||
| 162 | // 4.1 插入 assistant 接收消息 | 178 | // 4.1 插入 assistant 接收消息 |
| 163 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, | 179 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, |
| 164 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), | 180 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), |
| 165 | - knowledgeSegments); | 181 | + knowledgeSegments, null); |
| 166 | 182 | ||
| 167 | // 4.2 构建 Prompt,并进行调用 | 183 | // 4.2 构建 Prompt,并进行调用 |
| 168 | Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); | 184 | Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); |
| @@ -243,8 +259,13 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -243,8 +259,13 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 243 | 259 | ||
| 244 | // 1.2 历史 history message 历史消息 | 260 | // 1.2 历史 history message 历史消息 |
| 245 | List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO); | 261 | List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO); |
| 246 | - contextMessages | ||
| 247 | - .forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); | 262 | + contextMessages.forEach(message -> { |
| 263 | + chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())); | ||
| 264 | + UserMessage attachmentUserMessage = buildAttachmentUserMessage(message.getAttachmentUrls()); | ||
| 265 | + if (attachmentUserMessage != null) { | ||
| 266 | + chatMessages.add(attachmentUserMessage); | ||
| 267 | + } | ||
| 268 | + }); | ||
| 248 | 269 | ||
| 249 | // 1.3 当前 user message 新发送消息 | 270 | // 1.3 当前 user message 新发送消息 |
| 250 | chatMessages.add(new UserMessage(sendReqVO.getContent())); | 271 | chatMessages.add(new UserMessage(sendReqVO.getContent())); |
| @@ -257,6 +278,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -257,6 +278,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 257 | chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); | 278 | chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); |
| 258 | } | 279 | } |
| 259 | 280 | ||
| 281 | + // 1.5 附件,通过 UserMessage 实现 | ||
| 282 | + if (CollUtil.isNotEmpty(sendReqVO.getAttachmentUrls())) { | ||
| 283 | + UserMessage attachmentUserMessage = buildAttachmentUserMessage(sendReqVO.getAttachmentUrls()); | ||
| 284 | + if (attachmentUserMessage != null) { | ||
| 285 | + chatMessages.add(attachmentUserMessage); | ||
| 286 | + } | ||
| 287 | + } | ||
| 288 | + | ||
| 260 | // 2.1 查询 tool 工具 | 289 | // 2.1 查询 tool 工具 |
| 261 | Set<String> toolNames = null; | 290 | Set<String> toolNames = null; |
| 262 | Map<String,Object> toolContext = Map.of(); | 291 | Map<String,Object> toolContext = Map.of(); |
| @@ -314,14 +343,52 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -314,14 +343,52 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 314 | return contextMessages; | 343 | return contextMessages; |
| 315 | } | 344 | } |
| 316 | 345 | ||
| 346 | + private UserMessage buildAttachmentUserMessage(List<String> attachmentUrls) { | ||
| 347 | + if (CollUtil.isEmpty(attachmentUrls)) { | ||
| 348 | + return null; | ||
| 349 | + } | ||
| 350 | + // 读取文件内容 | ||
| 351 | + Map<String, String> attachmentContents = Maps.newLinkedHashMapWithExpectedSize(attachmentUrls.size()); | ||
| 352 | + for (String attachmentUrl : attachmentUrls) { | ||
| 353 | + try { | ||
| 354 | + String name = FileNameUtil.getName(attachmentUrl); | ||
| 355 | + String mineType = FileTypeUtils.getMineType(name); | ||
| 356 | + String content; | ||
| 357 | + if (FileTypeUtils.isImage(mineType)) { | ||
| 358 | + // 特殊:图片则转为 Base64 | ||
| 359 | + byte[] bytes = HttpUtil.downloadBytes(attachmentUrl); | ||
| 360 | + content = Base64.encode(bytes); | ||
| 361 | + } else { | ||
| 362 | + content = knowledgeDocumentService.readUrl(attachmentUrl); | ||
| 363 | + } | ||
| 364 | + if (StrUtil.isNotEmpty(content)) { | ||
| 365 | + attachmentContents.put(name, content); | ||
| 366 | + } | ||
| 367 | + } catch (Exception e) { | ||
| 368 | + log.error("[buildAttachmentUserMessage][读取附件({}) 发生异常]", attachmentUrl, e); | ||
| 369 | + } | ||
| 370 | + } | ||
| 371 | + if (CollUtil.isEmpty(attachmentContents)) { | ||
| 372 | + return null; | ||
| 373 | + } | ||
| 374 | + | ||
| 375 | + // 拼接 UserMessage 消息 | ||
| 376 | + String attachment = attachmentContents.entrySet().stream() | ||
| 377 | + .map(entry -> "<Attachment name=\"" + entry.getKey() + "\">" + entry.getValue() + "</Attachment>") | ||
| 378 | + .collect(Collectors.joining("\n\n")); | ||
| 379 | + return new UserMessage(String.format(Attachment_USER_MESSAGE_TEMPLATE, attachment)); | ||
| 380 | + } | ||
| 381 | + | ||
| 317 | private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, | 382 | private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, |
| 318 | - AiModelDO model, Long userId, Long roleId, | ||
| 319 | - MessageType messageType, String content, Boolean useContext, | ||
| 320 | - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments) { | 383 | + AiModelDO model, Long userId, Long roleId, |
| 384 | + MessageType messageType, String content, Boolean useContext, | ||
| 385 | + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, | ||
| 386 | + List<String> attachmentUrls) { | ||
| 321 | AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) | 387 | AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) |
| 322 | .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) | 388 | .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) |
| 323 | .setType(messageType.getValue()).setContent(content).setUseContext(useContext) | 389 | .setType(messageType.getValue()).setContent(content).setUseContext(useContext) |
| 324 | - .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)); | 390 | + .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)) |
| 391 | + .setAttachmentUrls(attachmentUrls); | ||
| 325 | message.setCreateTime(LocalDateTime.now()); | 392 | message.setCreateTime(LocalDateTime.now()); |
| 326 | chatMessageMapper.insert(message); | 393 | chatMessageMapper.insert(message); |
| 327 | return message; | 394 | return message; |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
| @@ -80,17 +80,17 @@ public class FileTypeUtils { | @@ -80,17 +80,17 @@ public class FileTypeUtils { | ||
| 80 | */ | 80 | */ |
| 81 | public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { | 81 | public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { |
| 82 | // 设置 header 和 contentType | 82 | // 设置 header 和 contentType |
| 83 | - String contentType = getMineType(content, filename); | ||
| 84 | - response.setContentType(contentType); | 83 | + String mineType = getMineType(content, filename); |
| 84 | + response.setContentType(mineType); | ||
| 85 | // 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html | 85 | // 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html |
| 86 | - if (StrUtil.containsIgnoreCase(contentType, "image/")) { | 86 | + if (isImage(mineType)) { |
| 87 | // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 | 87 | // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 |
| 88 | response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); | 88 | response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); |
| 89 | } else { | 89 | } else { |
| 90 | response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); | 90 | response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); |
| 91 | } | 91 | } |
| 92 | // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 | 92 | // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 |
| 93 | - if (StrUtil.containsIgnoreCase(contentType, "video")) { | 93 | + if (StrUtil.containsIgnoreCase(mineType, "video")) { |
| 94 | response.setHeader("Content-Length", String.valueOf(content.length)); | 94 | response.setHeader("Content-Length", String.valueOf(content.length)); |
| 95 | response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); | 95 | response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); |
| 96 | response.setHeader("Accept-Ranges", "bytes"); | 96 | response.setHeader("Accept-Ranges", "bytes"); |
| @@ -99,4 +99,14 @@ public class FileTypeUtils { | @@ -99,4 +99,14 @@ public class FileTypeUtils { | ||
| 99 | IoUtil.write(response.getOutputStream(), false, content); | 99 | IoUtil.write(response.getOutputStream(), false, content); |
| 100 | } | 100 | } |
| 101 | 101 | ||
| 102 | + /** | ||
| 103 | + * 判断是否是图片 | ||
| 104 | + * | ||
| 105 | + * @param mineType 类型 | ||
| 106 | + * @return 是否是图片 | ||
| 107 | + */ | ||
| 108 | + public static boolean isImage(String mineType) { | ||
| 109 | + return StrUtil.startWith(mineType, "image/"); | ||
| 110 | + } | ||
| 111 | + | ||
| 102 | } | 112 | } |