Commit 5579620140c7036c35bf270b2ca40ba98a7de7b7

Authored by cherishsince
1 parent 708d66e8

【优化】聊天 event stream 改为 flex 返回更加的优雅

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/Utf8SseEmitter.java deleted
1   -package cn.iocoder.yudao.module.ai.controller;
2   -
3   -import org.springframework.http.HttpHeaders;
4   -import org.springframework.http.MediaType;
5   -import org.springframework.http.server.ServerHttpResponse;
6   -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
7   -
8   -import java.nio.charset.StandardCharsets;
9   -
10   -/**
11   - * 解决中文乱码
12   - *
13   - * @author fansili
14   - * @time 2024/4/14 15:13
15   - * @since 1.0
16   - */
17   -public class Utf8SseEmitter extends SseEmitter {
18   -
19   - @Override
20   - protected void extendResponse(ServerHttpResponse outputMessage) {
21   - super.extendResponse(outputMessage);
22   -
23   - HttpHeaders headers = outputMessage.getHeaders();
24   - headers.setContentType(new MediaType(MediaType.TEXT_EVENT_STREAM, StandardCharsets.UTF_8));
25   - }
26   -}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.chat;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4   -import cn.iocoder.yudao.module.ai.controller.Utf8SseEmitter;
5 4 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
6   -import cn.iocoder.yudao.module.ai.service.AiChatService;
7 5 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
  6 +import cn.iocoder.yudao.module.ai.service.AiChatService;
8 7 import io.swagger.v3.oas.annotations.Operation;
9 8 import io.swagger.v3.oas.annotations.Parameter;
10 9 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -13,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
13 12 import org.springframework.http.MediaType;
14 13 import org.springframework.validation.annotation.Validated;
15 14 import org.springframework.web.bind.annotation.*;
16   -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
  15 +import reactor.core.publisher.Flux;
17 16  
18 17 import java.util.List;
19 18  
... ... @@ -39,10 +38,8 @@ public class AiChatMessageController {
39 38 // TODO @fan:要不要使用 Flux 来返回;可以使用 Flux<AiChatMessageRespVO>
40 39 @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
41 40 @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
42   - public SseEmitter sendMessageStream(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
43   - Utf8SseEmitter sseEmitter = new Utf8SseEmitter();
44   - chatService.chatStream(sendReqVO, sseEmitter);
45   - return sseEmitter;
  41 + public Flux<AiChatMessageRespVO> sendMessageStream(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
  42 + return chatService.chatStream(sendReqVO);
46 43 }
47 44  
48 45 @Operation(summary = "获得指定会话的消息列表")
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.image;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4   -import cn.iocoder.yudao.module.ai.controller.Utf8SseEmitter;
5   -import cn.iocoder.yudao.module.ai.service.AiImageService;
6 4 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDallDrawingReq;
7 5 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyReq;
  6 +import cn.iocoder.yudao.module.ai.service.AiImageService;
8 7 import io.swagger.v3.oas.annotations.Operation;
9 8 import io.swagger.v3.oas.annotations.tags.Tag;
10 9 import lombok.AllArgsConstructor;
... ... @@ -14,7 +13,6 @@ import org.springframework.web.bind.annotation.PostMapping;
14 13 import org.springframework.web.bind.annotation.RequestBody;
15 14 import org.springframework.web.bind.annotation.RequestMapping;
16 15 import org.springframework.web.bind.annotation.RestController;
17   -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
18 16  
19 17 // TODO @芋艿:整理接口定义
20 18 /**
... ... @@ -35,10 +33,11 @@ public class AiImageController {
35 33  
36 34 @Operation(summary = "dall2/dall3绘画", description = "openAi dall3是付费的!")
37 35 @PostMapping("/dallDrawing")
38   - public SseEmitter dallDrawing(@Validated @RequestBody AiImageDallDrawingReq req) {
39   - Utf8SseEmitter sseEmitter = new Utf8SseEmitter();
40   - aiImageService.dallDrawing(req, sseEmitter);
41   - return sseEmitter;
  36 + public void dallDrawing(@Validated @RequestBody AiImageDallDrawingReq req) {
  37 +// Utf8SseEmitter sseEmitter = new Utf8SseEmitter();
  38 +// aiImageService.dallDrawing(req, sseEmitter);
  39 +// return sseEmitter;
  40 +
42 41 }
43 42  
44 43 @Operation(summary = "midjourney", description = "midjourney图片绘画流程:1、提交任务 2、获取完成的任务 3、选择对应功能 4、获取最终结果")
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/AiChatService.java
1 1 package cn.iocoder.yudao.module.ai.service;
2 2  
3   -import cn.iocoder.yudao.module.ai.controller.Utf8SseEmitter;
4 3 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
5 4 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
  5 +import reactor.core.publisher.Flux;
6 6  
7 7 import java.util.List;
8 8  
... ... @@ -26,11 +26,10 @@ public interface AiChatService {
26 26 /**
27 27 * chat stream
28 28 *
29   - * @param req
30   - * @param sseEmitter
  29 + * @param sendReqVO
31 30 * @return
32 31 */
33   - void chatStream(AiChatMessageSendReqVO req, Utf8SseEmitter sseEmitter);
  32 + Flux<AiChatMessageRespVO> chatStream(AiChatMessageSendReqVO sendReqVO);
34 33  
35 34 /**
36 35 * 获取 - 获取对话 message list
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/AiImageService.java
1 1 package cn.iocoder.yudao.module.ai.service;
2 2  
3   -import cn.iocoder.yudao.module.ai.controller.Utf8SseEmitter;
4 3 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDallDrawingReq;
5 4 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyReq;
6 5  
... ... @@ -17,9 +16,8 @@ public interface AiImageService {
17 16 * ai绘画 - dall2/dall3 绘画
18 17 *
19 18 * @param req
20   - * @param sseEmitter
21 19 */
22   - void dallDrawing(AiImageDallDrawingReq req, Utf8SseEmitter sseEmitter);
  20 + void dallDrawing(AiImageDallDrawingReq req);
23 21  
24 22 /**
25 23 * midjourney 图片生成
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/impl/AiChatServiceImpl.java
... ... @@ -9,7 +9,6 @@ import cn.iocoder.yudao.framework.ai.chat.messages.MessageType;
9 9 import cn.iocoder.yudao.framework.ai.chat.prompt.Prompt;
10 10 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
11 11 import cn.iocoder.yudao.module.ai.config.AiChatClientFactory;
12   -import cn.iocoder.yudao.module.ai.controller.Utf8SseEmitter;
13 12 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationRespVO;
14 13 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
15 14 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
... ... @@ -25,13 +24,12 @@ import cn.iocoder.yudao.module.ai.service.AiChatRoleService;
25 24 import cn.iocoder.yudao.module.ai.service.AiChatService;
26 25 import lombok.AllArgsConstructor;
27 26 import lombok.extern.slf4j.Slf4j;
28   -import org.springframework.http.MediaType;
29 27 import org.springframework.stereotype.Service;
30 28 import org.springframework.transaction.annotation.Transactional;
31 29 import reactor.core.publisher.Flux;
32 30  
33   -import java.io.IOException;
34 31 import java.util.List;
  32 +import java.util.concurrent.atomic.AtomicInteger;
35 33 import java.util.function.Consumer;
36 34  
37 35 /**
... ... @@ -76,6 +74,7 @@ public class AiChatServiceImpl implements AiChatService {
76 74 chatModal.getModel(), chatModal.getId(), req.getContent(),
77 75 null, conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
78 76 String content = null;
  77 + int tokens = 0;
79 78 try {
80 79 // 创建 chat 需要的 Prompt
81 80 Prompt prompt = new Prompt(req.getContent());
... ... @@ -87,6 +86,7 @@ public class AiChatServiceImpl implements AiChatService {
87 86 ChatClient chatClient = aiChatClientFactory.getChatClient(platformEnum);
88 87 ChatResponse call = chatClient.call(prompt);
89 88 content = call.getResult().getOutput().getContent();
  89 + tokens = call.getResults().size();
90 90 // 更新 conversation
91 91 } catch (Exception e) {
92 92 content = ExceptionUtil.getMessage(e);
... ... @@ -94,7 +94,7 @@ public class AiChatServiceImpl implements AiChatService {
94 94 // 保存 chat message
95 95 insertChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(),
96 96 chatModal.getModel(), chatModal.getId(), content,
97   - null, conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
  97 + tokens, conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
98 98 }
99 99 return new AiChatMessageRespVO().setContent(content);
100 100 }
... ... @@ -123,8 +123,7 @@ public class AiChatServiceImpl implements AiChatService {
123 123 return insertChatMessageDO;
124 124 }
125 125  
126   - @Override
127   - public void chatStream(AiChatMessageSendReqVO req, Utf8SseEmitter sseEmitter) {
  126 + public Flux<AiChatMessageRespVO> chatStream(AiChatMessageSendReqVO req) {
128 127 Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
129 128 // 查询对话
130 129 AiChatConversationRespVO conversation = chatConversationService.getConversationOfValidate(req.getConversationId());
... ... @@ -145,46 +144,42 @@ public class AiChatServiceImpl implements AiChatService {
145 144 // req.setTopP(req.getTopP());
146 145 // req.setTemperature(req.getTemperature());
147 146 // 保存 chat message
148   - // 保存 chat message
149 147 insertChatMessage(conversation.getId(), MessageType.USER, loginUserId, conversation.getRoleId(),
150 148 chatModal.getModel(), chatModal.getId(), req.getContent(),
151 149 null, conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
152   -
153 150 // 获取 client 类型
154 151 AiPlatformEnum platformEnum = AiPlatformEnum.valueOfPlatform(chatModal.getPlatform());
155 152 StreamingChatClient streamingChatClient = aiChatClientFactory.getStreamingChatClient(platformEnum);
156 153 Flux<ChatResponse> streamResponse = streamingChatClient.stream(prompt);
157   -
  154 + // 转换 flex AiChatMessageRespVO
158 155 StringBuffer contentBuffer = new StringBuffer();
159   - streamResponse.subscribe(
160   - new Consumer<ChatResponse>() {
161   - @Override
162   - public void accept(ChatResponse chatResponse) {
163   - String content = chatResponse.getResults().get(0).getOutput().getContent();
164   - try {
165   - contentBuffer.append(content);
166   - sseEmitter.send(new AiChatMessageRespVO().setContent(content), MediaType.APPLICATION_JSON);
167   - } catch (IOException e) {
168   - log.error("发送异常{}", ExceptionUtil.getMessage(e));
169   - // 如果不是因为关闭而抛出异常,则重新连接
170   - sseEmitter.completeWithError(e);
171   - }
172   - }
173   - },
174   - error -> {
175   - //
176   - log.error("subscribe错误 {}", ExceptionUtil.getMessage(error));
177   - },
178   - () -> {
179   - log.info("发送完成!");
180   - sseEmitter.complete();
181   - // 保存 chat message
182   - insertChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(),
183   - chatModal.getModel(), chatModal.getId(), contentBuffer.toString(),
184   - null, conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
185   -
  156 + AtomicInteger tokens = new AtomicInteger(0);
  157 + return streamResponse.map(res -> {
  158 + AiChatMessageRespVO aiChatMessageRespVO = new AiChatMessageRespVO();
  159 + aiChatMessageRespVO.setContent(res.getResult().getOutput().getContent());
  160 + contentBuffer.append(res.getResult().getOutput().getContent());
  161 + tokens.incrementAndGet();
  162 + return aiChatMessageRespVO;
186 163 }
187   - );
  164 + ).doOnComplete(new Runnable() {
  165 + @Override
  166 + public void run() {
  167 + log.info("发送完成!");
  168 + // 保存 chat message
  169 + insertChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(),
  170 + chatModal.getModel(), chatModal.getId(), contentBuffer.toString(),
  171 + tokens.get(), conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
  172 + }
  173 + }).doOnError(new Consumer<Throwable>() {
  174 + @Override
  175 + public void accept(Throwable throwable) {
  176 + log.error("发送错误 {}!", throwable.getMessage());
  177 + // 保存 chat message
  178 + insertChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(),
  179 + chatModal.getModel(), chatModal.getId(), throwable.getMessage(),
  180 + tokens.get(), conversation.getTemperature(), conversation.getMaxTokens(), conversation.getMaxContexts());
  181 + }
  182 + });
188 183 }
189 184  
190 185 @Override
... ... @@ -194,7 +189,7 @@ public class AiChatServiceImpl implements AiChatService {
194 189 // 获取对话所有 message
195 190 List<AiChatMessageDO> aiChatMessageDOList = aiChatMessageMapper.selectByConversationId(conversationId);
196 191 // 转换 AiChatMessageRespVO
197   - return AiChatMessageConvert.INSTANCE.convertAiChatMessageRespVOList(aiChatMessageDOList);
  192 + return AiChatMessageConvert.INSTANCE.convertAiChatMessageRespVOList(aiChatMessageDOList);
198 193 }
199 194  
200 195 @Override
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/impl/AiImageServiceImpl.java
... ... @@ -5,8 +5,8 @@ import cn.iocoder.yudao.framework.ai.image.ImageGeneration;
5 5 import cn.iocoder.yudao.framework.ai.image.ImagePrompt;
6 6 import cn.iocoder.yudao.framework.ai.image.ImageResponse;
7 7 import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageClient;
8   -import cn.iocoder.yudao.framework.ai.imageopenai.enums.OpenAiImageModelEnum;
9 8 import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageOptions;
  9 +import cn.iocoder.yudao.framework.ai.imageopenai.enums.OpenAiImageModelEnum;
10 10 import cn.iocoder.yudao.framework.ai.imageopenai.enums.OpenAiImageStyleEnum;
11 11 import cn.iocoder.yudao.framework.ai.midjourney.api.MidjourneyInteractionsApi;
12 12 import cn.iocoder.yudao.framework.ai.midjourney.webSocket.MidjourneyWebSocketStarter;
... ... @@ -14,22 +14,18 @@ import cn.iocoder.yudao.framework.ai.midjourney.webSocket.WssNotify;
14 14 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
15 15 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
16 16 import cn.iocoder.yudao.module.ai.ErrorCodeConstants;
17   -import cn.iocoder.yudao.module.ai.controller.Utf8SseEmitter;
  17 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDallDrawingReq;
  18 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyReq;
18 19 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
19   -import cn.iocoder.yudao.module.ai.enums.AiChatDrawingStatusEnum;
20 20 import cn.iocoder.yudao.module.ai.dal.mysql.AiImageMapper;
  21 +import cn.iocoder.yudao.module.ai.enums.AiChatDrawingStatusEnum;
21 22 import cn.iocoder.yudao.module.ai.service.AiImageService;
22   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDallDrawingReq;
23   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyReq;
24 23 import jakarta.annotation.PostConstruct;
25 24 import lombok.AllArgsConstructor;
26 25 import lombok.extern.slf4j.Slf4j;
27   -import org.springframework.http.MediaType;
28 26 import org.springframework.stereotype.Service;
29 27 import org.springframework.transaction.annotation.Transactional;
30 28  
31   -import java.io.IOException;
32   -
33 29 /**
34 30 * ai 作图
35 31 *
... ... @@ -64,7 +60,7 @@ public class AiImageServiceImpl implements AiImageService {
64 60 }
65 61  
66 62 @Override
67   - public void dallDrawing(AiImageDallDrawingReq req, Utf8SseEmitter sseEmitter) {
  63 + public void dallDrawing(AiImageDallDrawingReq req) {
68 64 // 获取 model
69 65 OpenAiImageModelEnum openAiImageModelEnum = OpenAiImageModelEnum.valueOfModel(req.getModal());
70 66 OpenAiImageStyleEnum openAiImageStyleEnum = OpenAiImageStyleEnum.valueOfStyle(req.getStyle());
... ... @@ -79,7 +75,7 @@ public class AiImageServiceImpl implements AiImageService {
79 75 // 发送
80 76 ImageGeneration imageGeneration = imageResponse.getResult();
81 77 // 发送信息
82   - sendSseEmitter(sseEmitter, imageGeneration);
  78 +// sendSseEmitter(sseEmitter, imageGeneration);
83 79 // 保存数据库
84 80 doSave(req.getPrompt(), req.getSize(), req.getModal(),
85 81 imageGeneration.getOutput().getUrl(), AiChatDrawingStatusEnum.COMPLETE, null);
... ... @@ -88,7 +84,7 @@ public class AiImageServiceImpl implements AiImageService {
88 84 doSave(req.getPrompt(), req.getSize(), req.getModal(),
89 85 null, AiChatDrawingStatusEnum.FAIL, aiException.getMessage());
90 86 // 发送错误信息
91   - sendSseEmitter(sseEmitter, aiException.getMessage());
  87 +// sendSseEmitter(sseEmitter, aiException.getMessage());
92 88 }
93 89 }
94 90  
... ... @@ -105,16 +101,16 @@ public class AiImageServiceImpl implements AiImageService {
105 101 }
106 102 }
107 103  
108   - private static void sendSseEmitter(Utf8SseEmitter sseEmitter, Object object) {
109   - try {
110   - sseEmitter.send(object, MediaType.APPLICATION_JSON);
111   - } catch (IOException e) {
112   - throw new RuntimeException(e);
113   - } finally {
114   - // 发送 complete
115   - sseEmitter.complete();
116   - }
117   - }
  104 +// private static void sendSseEmitter(Utf8SseEmitter sseEmitter, Object object) {
  105 +// try {
  106 +// sseEmitter.send(object, MediaType.APPLICATION_JSON);
  107 +// } catch (IOException e) {
  108 +// throw new RuntimeException(e);
  109 +// } finally {
  110 +// // 发送 complete
  111 +// sseEmitter.complete();
  112 +// }
  113 +// }
118 114  
119 115 private AiImageDO doSave(String prompt,
120 116 String size,
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -2,7 +2,6 @@ server:
2 2 port: 48080
3 3  
4 4 --- #################### 数据库相关配置 ####################
5   -
6 5 spring:
7 6 # 数据源配置项
8 7 autoconfigure:
... ... @@ -79,7 +78,12 @@ spring:
79 78 port: 6379 # 端口
80 79 database: 0 # 数据库索引
81 80 # password: dev # 密码,建议生产环境开启
82   -
  81 +server:
  82 + servlet:
  83 + encoding:
  84 + enabled: true
  85 + charset: UTF-8
  86 + force: true
83 87 --- #################### 定时任务相关配置 ####################
84 88  
85 89 # Quartz 配置项,对应 QuartzProperties 配置类
... ...