Commit c98495dfcb615e028fda95d0c92ed5d70ac5c16f

Authored by cherishsince
2 parents 3d2b0dec 266b6faa

Merge remote-tracking branch 'origin/master-jdk21-ai' into master-jdk21-ai

Showing 29 changed files with 189 additions and 470 deletions
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.enums;
  2 +
  3 +/**
  4 + * AI 字典类型的枚举类
  5 + *
  6 + * @author xiaoxin
  7 + */
  8 +public interface DictTypeConstants {
  9 +
  10 + // ========== AI Write ==========
  11 + String AI_WRITE_FORMAT = "ai_write_format"; // 写作格式
  12 + String AI_WRITE_LENGTH = "ai_write_length"; // 写作长度
  13 + String AI_WRITE_LANGUAGE = "ai_write_language"; // 写作语言
  14 + String AI_WRITE_TONE = "ai_write_tone"; // 写作语气
  15 +
  16 +}
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java deleted
1   -package cn.iocoder.yudao.module.ai.enums.model;
2   -
3   -import lombok.AllArgsConstructor;
4   -import lombok.Getter;
5   -
6   -// TODO @芋艿:可以考虑清理掉
7   -/**
8   - * ai 模型
9   - *
10   - * @author: fansili
11   - * @time: 2024/3/4 12:36
12   - */
13   -@Getter
14   -@AllArgsConstructor
15   -public enum AiModelEnum {
16   -
17   - // open ai
18   - OPEN_AI_GPT_3_5( "GPT3.5", "gpt-3.5-turbo",null),
19   - OPEN_AI_GPT_4("GPT4", "gpt-4-turbo",null),
20   -
21   - // 千问付费模型
22   - QWEN_TURBO("通义千问超大规模语言模型", "qwen-turbo", null),
23   - QWEN_PLUS("通义千问超大规模语言模型增强版", "qwen-plus", null),
24   - QWEN_MAX("通义千问千亿级别超大规模语言模型", "qwen-max", null),
25   - QWEN_MAX_0403("通义千问千亿级别超大规模语言模型-0403", "qwen-max-0403", null),
26   - QWEN_MAX_0107("通义千问千亿级别超大规模语言模型-0107", "qwen-max-0107", null),
27   - QWEN_MAX_1201("通义千问千亿级别超大规模语言模型-1201", "qwen-max-1201", null),
28   - QWEN_MAX_LONGCONTEXT("通义千问千亿级别超大规模语言模型-28k tokens", "qwen-max-longcontext", null),
29   -
30   - // 千问开源模型
31   - // https://help.aliyun.com/document_detail/2666503.html?spm=a2c4g.2701795.0.0.26eb34dfKzcWN4
32   - QWEN_72B_CHAT("通义千问1.5对外开源的72B规模参数量的经过人类指令对齐的chat模型", "qwen-72b-chat", null),
33   -
34   - // 一言模型
35   - ERNIE4_0("ERNIE 4.0", "ERNIE 4.0", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro"),
36   - ERNIE4_3_5_8K("ERNIE-3.5-8K", "ERNIE-3.5-8K", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions"),
37   - ERNIE4_3_5_8K_0205("ERNIE-3.5-8K-0205", "ERNIE-3.5-8K-0205", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-8k-0205"),
38   -
39   - ERNIE4_3_5_8K_1222("ERNIE-3.5-8K-1222", "ERNIE-3.5-8K-1222", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-8k-1222"),
40   - ERNIE4_BOT_8K("ERNIE-Bot-8K", "ERNIE-Bot-8K", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_bot_8k"),
41   - ERNIE4_3_5_4K_0205("ERNIE-3.5-4K-0205", "ERNIE-3.5-4K-0205", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-4k-0205"),
42   -
43   -// 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
44   -// general指向V1.5版本;
45   -// generalv2指向V2版本;
46   -// generalv3指向V3版本;
47   -// generalv3.5指向V3.5版本;
48   -
49   - XING_HUO_1_5("星火大模型1.5", "general", "/v1.1/chat"),
50   - XING_HUO_2_0("星火大模型2.0", "generalv2", "/v2.1/chat"),
51   - XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"),
52   - XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"),
53   -
54   - // Suno 模型
55   - SUNO_2( "SUNO-2", "chirp-v2-xxl-alpha",null),
56   - SUNO_3_0( "SUNO-3.0", "chirp-v3-0",null),
57   - SUNO_3_5( "SUNO-3.5", "chirp-v3.5",null),
58   - ;
59   -
60   - /**
61   - * 模型名字 - 用于展示
62   - */
63   - private final String name;
64   - /**
65   - * 模型标志 - 用于参数传递
66   - */
67   - private final String model;
68   - /**
69   - * uri地址
70   - */
71   - private final String uri;
72   -
73   -}
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiLanguageEnum.java deleted
1   -package cn.iocoder.yudao.module.ai.enums.write;
2   -
3   -import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
4   -import lombok.AllArgsConstructor;
5   -import lombok.Getter;
6   -
7   -import java.util.Arrays;
8   -
9   -// TODO @xin:写作的几个,不用枚举类哈。直接搞字段就好了。AiWriteTypeEnum 还是需要的哈
10   -@AllArgsConstructor
11   -@Getter
12   -public enum AiLanguageEnum implements IntArrayValuable {
13   -
14   - AUTO(1, "自动"),
15   - CHINESE(2, "中文"),
16   - ENGLISH(3, "英文"),
17   - KOREAN(4, "韩语"),
18   - JAPANESE(5, "日语");
19   -
20   - /**
21   - * Language code
22   - */
23   - private final Integer language;
24   - /**
25   - * Language name
26   - */
27   - private final String name;
28   -
29   - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiLanguageEnum::getLanguage).toArray();
30   -
31   - @Override
32   - public int[] array() {
33   - return ARRAYS;
34   - }
35   -
36   - public static AiLanguageEnum valueOfLanguage(Integer language) {
37   - for (AiLanguageEnum languageEnum : AiLanguageEnum.values()) {
38   - if (languageEnum.getLanguage().equals(language)) {
39   - return languageEnum;
40   - }
41   - }
42   - throw new IllegalArgumentException("未知语言: " + language);
43   - }
44   -
45   -}
46 0 \ No newline at end of file
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteFormatEnum.java deleted
1   -package cn.iocoder.yudao.module.ai.enums.write;
2   -
3   -import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
4   -import lombok.AllArgsConstructor;
5   -import lombok.Getter;
6   -
7   -import java.util.Arrays;
8   -
9   -/**
10   - * AI 写作类型的枚举
11   - *
12   - * @author xiaoxin
13   - */
14   -@AllArgsConstructor
15   -@Getter
16   -public enum AiWriteFormatEnum implements IntArrayValuable {
17   -
18   - AUTO(1, "自动"),
19   - EMAIL(2, "电子邮件"),
20   - MESSAGE(3, "消息"),
21   - COMMENT(4, "评论"),
22   - PARAGRAPH(5, "段落"),
23   - ARTICLE(6, "文章"),
24   - BLOG_POST(7, "博客文章"),
25   - IDEA(8, "想法"),
26   - OUTLINE(9, "大纲");
27   -
28   - /**
29   - * 格式
30   - */
31   - private final Integer format;
32   - /**
33   - * 格式名
34   - */
35   - private final String name;
36   -
37   - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteFormatEnum::getFormat).toArray();
38   -
39   - @Override
40   - public int[] array() {
41   - return ARRAYS;
42   - }
43   -
44   - public static AiWriteFormatEnum valueOfFormat(Integer format) {
45   - for (AiWriteFormatEnum formatEnum : AiWriteFormatEnum.values()) {
46   - if (formatEnum.getFormat().equals(format)) {
47   - return formatEnum;
48   - }
49   - }
50   - throw new IllegalArgumentException("未知格式: " + format);
51   - }
52   -
53   -}
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteLengthEnum.java deleted
1   -package cn.iocoder.yudao.module.ai.enums.write;
2   -
3   -import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
4   -import lombok.AllArgsConstructor;
5   -import lombok.Getter;
6   -
7   -import java.util.Arrays;
8   -
9   -/**
10   - * AI 写作类型的枚举
11   - *
12   - * @author xiaoxin
13   - */
14   -@AllArgsConstructor
15   -@Getter
16   -public enum AiWriteLengthEnum implements IntArrayValuable {
17   -
18   - AUTO(1, "自动"),
19   - SHORT(2, "短"),
20   - MEDIUM(3, "中"),
21   - LONG(4, "长");
22   -
23   - /**
24   - * 长度
25   - */
26   - private final Integer length;
27   - /**
28   - * 长度名
29   - */
30   - private final String name;
31   -
32   - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteLengthEnum::getLength).toArray();
33   -
34   - @Override
35   - public int[] array() {
36   - return ARRAYS;
37   - }
38   -
39   - public static AiWriteLengthEnum valueOfLength(Integer length) {
40   - for (AiWriteLengthEnum lengthEnum : AiWriteLengthEnum.values()) {
41   - if (lengthEnum.getLength().equals(length)) {
42   - return lengthEnum;
43   - }
44   - }
45   - throw new IllegalArgumentException("未知长度: " + length);
46   - }
47   -}
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteToneEnum.java deleted
1   -package cn.iocoder.yudao.module.ai.enums.write;
2   -
3   -import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
4   -import lombok.AllArgsConstructor;
5   -import lombok.Getter;
6   -
7   -import java.util.Arrays;
8   -
9   -@AllArgsConstructor
10   -@Getter
11   -public enum AiWriteToneEnum implements IntArrayValuable {
12   -
13   - AUTO(1, "自动"),
14   - FRIENDLY(2, "友善"),
15   - CASUAL(3, "随意"),
16   - KIND(4, "友好"),
17   - PROFESSIONAL(5, "专业"),
18   - HUMOROUS(6, "谈谐"),
19   - INTERESTING(7, "有趣"),
20   - FORMAL(8, "正式");
21   -
22   - /**
23   - * 语气
24   - */
25   - private final Integer tone;
26   - /**
27   - * 语气名
28   - */
29   - private final String name;
30   -
31   - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteToneEnum::getTone).toArray();
32   -
33   - @Override
34   - public int[] array() {
35   - return ARRAYS;
36   - }
37   -
38   - public static AiWriteToneEnum valueOfTone(Integer tone) {
39   - for (AiWriteToneEnum toneEnum : AiWriteToneEnum.values()) {
40   - if (toneEnum.getTone().equals(tone)) {
41   - return toneEnum;
42   - }
43   - }
44   - throw new IllegalArgumentException("未知语气: " + tone);
45   - }
46   -}
47 0 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java
... ... @@ -23,7 +23,6 @@ import org.springframework.web.bind.annotation.*;
23 23  
24 24 import java.util.List;
25 25 import java.util.Map;
26   -import java.util.function.Consumer;
27 26  
28 27 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
29 28 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
... ... @@ -79,9 +78,8 @@ public class AiChatConversationController {
79 78 return success(true);
80 79 }
81 80  
82   - // TODO 芋艿:这个 url 可以改下
83   - @DeleteMapping("/delete-my-all-except-pinned")
84   - @Operation(summary = "删除所有对话(置顶除外)")
  81 + @DeleteMapping("/delete-by-unpinned")
  82 + @Operation(summary = "删除未置顶的聊天对话")
85 83 public CommonResult<Boolean> deleteChatConversationMyByUnpinned() {
86 84 chatConversationService.deleteChatConversationMyByUnpinned(getLoginUserId());
87 85 return success(true);
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
1   -
2   -### chat call
3   -POST {{baseUrl}}/admin-api/ai/chat/message/send
  1 +### 发送消息(段式)
  2 +POST {{baseUrl}}/ai/chat/message/send
4 3 Content-Type: application/json
5 4 Authorization: {{token}}
6 5  
7 6 {
8   - "conversationId": "1781604279872581649",
  7 + "conversationId": "1781604279872581724",
9 8 "content": "你是 OpenAI 么?"
10 9 }
11 10  
12   -
13 11 ### 发送消息(流式)
14 12 POST {{baseUrl}}/ai/chat/message/send-stream
15 13 Content-Type: application/json
... ... @@ -20,11 +18,10 @@ Authorization: {{token}}
20 18 "content": "1+1=?"
21 19 }
22 20  
23   -### message list
24   -GET {{baseUrl}}/admin-api/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649
  21 +### 获得指定对话的消息列表
  22 +GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649
25 23 Authorization: {{token}}
26 24  
27   -
28   -### message delete
29   -DELETE {{baseUrl}}/admin-api/ai/chat/message/delete?id=50
30   -Authorization: {{token}}
  25 +### 删除消息
  26 +DELETE {{baseUrl}}/ai/chat/message/delete?id=50
  27 +Authorization: {{token}}
31 28 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
... ... @@ -21,10 +21,10 @@ import io.swagger.v3.oas.annotations.Parameter;
21 21 import io.swagger.v3.oas.annotations.tags.Tag;
22 22 import jakarta.annotation.Resource;
23 23 import jakarta.annotation.security.PermitAll;
  24 +import jakarta.validation.Valid;
24 25 import lombok.extern.slf4j.Slf4j;
25 26 import org.springframework.http.MediaType;
26 27 import org.springframework.security.access.prepost.PreAuthorize;
27   -import org.springframework.validation.annotation.Validated;
28 28 import org.springframework.web.bind.annotation.*;
29 29 import reactor.core.publisher.Flux;
30 30  
... ... @@ -51,14 +51,14 @@ public class AiChatMessageController {
51 51  
52 52 @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢")
53 53 @PostMapping("/send")
54   - public CommonResult<AiChatMessageRespVO> sendMessage(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
55   - return success(chatMessageService.sendMessage(sendReqVO));
  54 + public CommonResult<AiChatMessageSendRespVO> sendMessage(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
  55 + return success(chatMessageService.sendMessage(sendReqVO, getLoginUserId()));
56 56 }
57 57  
58 58 @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
59 59 @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
60 60 @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
61   - public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
  61 + public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
62 62 return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId());
63 63 }
64 64  
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
... ... @@ -28,7 +28,7 @@ public class AiChatMessageRespVO {
28 28 private Long roleId;
29 29  
30 30 @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo")
31   - private String model; // 参见 AiOpenAiModelEnum 枚举类
  31 + private String model;
32 32  
33 33 @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "123")
34 34 private Long modelId;
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
... ... @@ -31,14 +31,6 @@ public class AiChatMessageSendRespVO {
31 31 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
32 32 private LocalDateTime createTime;
33 33  
34   - // ========== 扩展字段 ==========
35   -
36   - @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://iocoder.cn/1.png")
37   - private String userAvatar;
38   -
39   - @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://iocoder.cn/2.png")
40   - private String roleAvatar;
41   -
42 34 }
43 35  
44 36 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http
1   -
2 1 ### 生成图片:OpenAI(DALL)
3 2 POST {{baseUrl}}/ai/image/draw
4 3 Content-Type: application/json
... ... @@ -29,8 +28,7 @@ Authorization: {{token}}
29 28 "style": "vivid"
30 29 }
31 30  
32   -### 生成图片:生成图片
33   -
  31 +### 生成图片:生成图片(Midjourney)
34 32 POST {{baseUrl}}/ai/image/midjourney/imagine
35 33 Content-Type: application/json
36 34 Authorization: {{token}}
... ... @@ -40,6 +38,5 @@ Authorization: {{token}}
40 38 "model": "midjourney",
41 39 "width": "1",
42 40 "height": "1",
43   - "version": "6.0",
44   - "base64Array": []
  41 + "version": "6.0"
45 42 }
46 43 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
... ... @@ -68,7 +68,7 @@ public class AiImageController {
68 68  
69 69 @Operation(summary = "生成图片")
70 70 @PostMapping("/draw")
71   - public CommonResult<Long> drawImage(@Validated @RequestBody AiImageDrawReqVO drawReqVO) {
  71 + public CommonResult<Long> drawImage(@Valid @RequestBody AiImageDrawReqVO drawReqVO) {
72 72 return success(imageService.drawImage(getLoginUserId(), drawReqVO));
73 73 }
74 74  
... ... @@ -84,7 +84,7 @@ public class AiImageController {
84 84  
85 85 @Operation(summary = "【Midjourney】生成图片")
86 86 @PostMapping("/midjourney/imagine")
87   - public CommonResult<Long> midjourneyImagine(@Validated @RequestBody AiMidjourneyImagineReqVO reqVO) {
  87 + public CommonResult<Long> midjourneyImagine(@Valid @RequestBody AiMidjourneyImagineReqVO reqVO) {
88 88 Long imageId = imageService.midjourneyImagine(getLoginUserId(), reqVO);
89 89 return success(imageId);
90 90 }
... ... @@ -92,14 +92,14 @@ public class AiImageController {
92 92 @Operation(summary = "【Midjourney】通知图片进展", description = "由 Midjourney Proxy 回调")
93 93 @PostMapping("/midjourney/notify") // 必须是 POST 方法,否则会报错
94 94 @PermitAll
95   - public CommonResult<Boolean> midjourneyNotify(@Validated @RequestBody MidjourneyApi.Notify notify) {
  95 + public CommonResult<Boolean> midjourneyNotify(@Valid @RequestBody MidjourneyApi.Notify notify) {
96 96 imageService.midjourneyNotify(notify);
97 97 return success(true);
98 98 }
99 99  
100 100 @Operation(summary = "【Midjourney】Action 操作(二次生成图片)", description = "例如说:放大、缩小、U1、U2 等")
101 101 @PostMapping("/midjourney/action")
102   - public CommonResult<Long> midjourneyAction(@Validated @RequestBody AiMidjourneyActionReqVO reqVO) {
  102 + public CommonResult<Long> midjourneyAction(@Valid @RequestBody AiMidjourneyActionReqVO reqVO) {
103 103 Long imageId = imageService.midjourneyAction(getLoginUserId(), reqVO);
104 104 return success(imageId);
105 105 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
... ... @@ -6,7 +6,7 @@ Authorization: {{token}}
6 6 {
7 7 "platform": "Suno",
8 8 "generateMode": 2,
9   - "prompt": "周末啦!",
  9 + "prompt": "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。",
10 10 "model": "chirp-v3.5",
11 11 "tags": ["Happy"],
12 12 "title": "Happy Song"
... ... @@ -21,6 +21,6 @@ Authorization: {{token}}
21 21 "platform": "Suno",
22 22 "generateMode": 1,
23 23 "model": "chirp-v3.5",
24   - "gptDescriptionPrompt": "今天是星球六,结果是个下雨天,希望心情很美丽",
  24 + "prompt": "happy music",
25 25 "makeInstrumental": false
26 26 }
27 27 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
... ... @@ -46,7 +46,7 @@ public class AiSunoGenerateReqVO {
46 46  
47 47 @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
48 48 @NotEmpty(message = "模型不能为空")
49   - private String model; // 参见 AiModelEnum 枚举
  49 + private String model;
50 50  
51 51 @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]")
52 52 private List<String> tags;
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java
... ... @@ -26,9 +26,10 @@ public class AiWriteController {
26 26 private AiWriteService writeService;
27 27  
28 28 @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
29   - @PermitAll
30 29 @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快")
  30 + @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
31 31 public Flux<CommonResult<String>> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) {
32 32 return writeService.generateWriteContent(generateReqVO, getLoginUserId());
33 33 }
  34 +
34 35 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java
... ... @@ -14,11 +14,10 @@ public class AiWriteGenerateReqVO {
14 14 @InEnum(AiWriteTypeEnum.class)
15 15 private Integer type;
16 16  
17   - // TODO @xin:如果非必填,可以不用写 requiredMode
18   - @Schema(description = "写作内容提示", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1.撰写:田忌赛马;2.回复:不批")
  17 + @Schema(description = "写作内容提示", example = "1.撰写:田忌赛马;2.回复:不批")
19 18 private String prompt;
20 19  
21   - @Schema(description = "原文", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "领导我要辞职")
  20 + @Schema(description = "原文", example = "领导我要辞职")
22 21 private String originalContent;
23 22  
24 23 @Schema(description = "长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 4 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
5 5 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
6   -import cn.iocoder.yudao.module.ai.enums.model.AiModelEnum;
7 6 import com.baomidou.mybatisplus.annotation.KeySequence;
8 7 import com.baomidou.mybatisplus.annotation.TableId;
9 8 import com.baomidou.mybatisplus.annotation.TableName;
... ... @@ -73,8 +72,6 @@ public class AiChatConversationDO extends BaseDO {
73 72 private Long modelId;
74 73 /**
75 74 * 模型标志
76   - *
77   - * 枚举 {@link AiModelEnum}
78 75 */
79 76 private String model;
80 77  
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
... ... @@ -5,7 +5,6 @@ import org.springframework.ai.chat.messages.MessageType;
5 5 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
6 6 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
7 7 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
8   -import cn.iocoder.yudao.module.ai.enums.model.AiModelEnum;
9 8 import com.baomidou.mybatisplus.annotation.KeySequence;
10 9 import com.baomidou.mybatisplus.annotation.TableName;
11 10 import lombok.*;
... ... @@ -69,8 +68,6 @@ public class AiChatMessageDO extends BaseDO {
69 68  
70 69 /**
71 70 * 模型标志
72   - *
73   - * 枚举 {@link AiModelEnum}
74 71 */
75 72 private String model;
76 73 /**
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java
... ... @@ -2,8 +2,9 @@ package cn.iocoder.yudao.module.ai.service.chat;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5   -import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO;
6   -import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.*;
  5 +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
7 8 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
8 9 import reactor.core.publisher.Flux;
9 10  
... ... @@ -22,9 +23,10 @@ public interface AiChatMessageService {
22 23 * 发送消息
23 24 *
24 25 * @param sendReqVO 发送信息
  26 + * @param userId 用户编号
25 27 * @return 发送结果
26 28 */
27   - AiChatMessageRespVO sendMessage(AiChatMessageSendReqVO sendReqVO);
  29 + AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId);
28 30  
29 31 /**
30 32 * 发送消息
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
... ... @@ -4,35 +4,28 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.hutool.core.util.StrUtil;
6 6 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
7   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
8   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
  7 +import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
9 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
10 9 import cn.iocoder.yudao.framework.common.pojo.PageResult;
11 10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
12 11 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
13   -import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
14 12 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
15 13 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
16 14 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
17 15 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
18 16 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
19   -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
20 17 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
21 18 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
22 19 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
23 20 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
24   -import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
25   -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
26 21 import jakarta.annotation.Resource;
27 22 import lombok.extern.slf4j.Slf4j;
28 23 import org.springframework.ai.chat.messages.*;
  24 +import org.springframework.ai.chat.model.ChatModel;
29 25 import org.springframework.ai.chat.model.ChatResponse;
30 26 import org.springframework.ai.chat.model.StreamingChatModel;
31 27 import org.springframework.ai.chat.prompt.ChatOptions;
32 28 import org.springframework.ai.chat.prompt.Prompt;
33   -import org.springframework.ai.ollama.api.OllamaOptions;
34   -import org.springframework.ai.openai.OpenAiChatOptions;
35   -import org.springframework.ai.qianfan.QianFanChatOptions;
36 29 import org.springframework.stereotype.Service;
37 30 import org.springframework.transaction.annotation.Transactional;
38 31 import reactor.core.publisher.Flux;
... ... @@ -64,47 +57,37 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
64 57 @Resource
65 58 private AiChatModelService chatModalService;
66 59 @Resource
67   - private AiChatRoleService chatRoleService;
68   - @Resource
69 60 private AiApiKeyService apiKeyService;
70 61  
71 62 @Transactional(rollbackFor = Exception.class)
72   - public AiChatMessageRespVO sendMessage(AiChatMessageSendReqVO req) {
73   - return null; // TODO 芋艿:一起改
74   -// Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
75   -// // 查询对话
76   -// AiChatConversationDO conversation = chatConversationService.validateExists(req.getConversationId());
77   -// // 获取对话模型
78   -// AiChatModelDO chatModel = chatModalService.validateChatModel(conversation.getModelId());
79   -// // 获取角色信息
80   -// AiChatRoleDO chatRoleDO = conversation.getRoleId() != null ? chatRoleService.validateChatRole(conversation.getRoleId()) : null;
81   -// // 获取 client 类型
82   -// AiPlatformEnum platformEnum = AiPlatformEnum.validatePlatform(chatModel.getPlatform());
83   -// // 保存 chat message
84   -// createChatMessage(conversation.getId(), MessageType.USER, loginUserId, conversation.getRoleId(),
85   -// chatModel.getModel(), chatModel.getId(), req.getContent());
86   -// String content = null;
87   -// int tokens = 0;
88   -// try {
89   -// // 创建 chat 需要的 Prompt
90   -// Prompt prompt = new Prompt(req.getContent());
91   -// // TODO @芋艿 @范 看要不要支持这些
92   -//// req.setTopK(req.getTopK());
93   -//// req.setTopP(req.getTopP());
94   -//// req.setTemperature(req.getTemperature());
95   -// // 发送 call 调用
96   -// ChatClient chatClient = chatClientFactory.getChatClient(platformEnum);
97   -// ChatResponse call = chatClient.call(prompt);
98   -// content = call.getResult().getOutput().getContent();
99   -// // 更新 conversation
100   -// } catch (Exception e) {
101   -// content = ExceptionUtil.getMessage(e);
102   -// } finally {
103   -// // 保存 chat message
104   -// createChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(),
105   -// chatModel.getModel(), chatModel.getId(), content);
106   -// }
107   -// return new AiChatMessageRespVO().setContent(content);
  63 + public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
  64 + // 1.1 校验对话存在
  65 + AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId());
  66 + if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
  67 + throw exception(CHAT_CONVERSATION_NOT_EXISTS);
  68 + }
  69 + List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
  70 + // 1.2 校验模型
  71 + AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
  72 + ChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
  73 +
  74 + // 2. 插入 user 发送消息
  75 + AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
  76 + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext());
  77 +
  78 + // 3.1 插入 assistant 接收消息
  79 + AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
  80 + userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
  81 +
  82 + // 3.2 创建 chat 需要的 Prompt
  83 + Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
  84 + ChatResponse chatResponse = chatClient.call(prompt);
  85 +
  86 + // 3.3 段式返回
  87 + String newContent = chatResponse.getResult().getOutput().getContent();
  88 + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
  89 + return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
  90 + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent));
108 91 }
109 92  
110 93 @Override
... ... @@ -112,14 +95,12 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
112 95 // 1.1 校验对话存在
113 96 AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId());
114 97 if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
115   - throw exception(CHAT_CONVERSATION_NOT_EXISTS); // TODO 芋艿:异常情况的对接;
  98 + throw exception(CHAT_CONVERSATION_NOT_EXISTS);
116 99 }
117 100 List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
118 101 // 1.2 校验模型
119 102 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
120   - StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
121   - // 1.3 获取用户头像、角色头像
122   - AiChatRoleDO role = conversation.getRoleId() != null ? chatRoleService.getChatRole(conversation.getRoleId()) : null;
  103 + StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
123 104  
124 105 // 2. 插入 user 发送消息
125 106 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
... ... @@ -149,9 +130,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
149 130 // TODO @芋艿:失败的情况下,要不要删除消息
150 131 log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
151 132 chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()));
152   - }).onErrorResume(error -> {
153   - return Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR));
154   - });
  133 + }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
155 134 }
156 135  
157 136 private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
... ... @@ -164,46 +143,17 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
164 143 }
165 144 // 1.2 history message 历史消息
166 145 List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
167   - contextMessages.forEach(message -> {
168   - // TODO @芋艿:看看有没优化空间
169   - if (MessageType.USER.getValue().equals(message.getType())) {
170   - chatMessages.add(new UserMessage(message.getContent()));
171   - } else {
172   - chatMessages.add(new AssistantMessage(message.getContent()));
173   - }
174   - });
  146 + contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
175 147 // 1.3 user message 新发送消息
176 148 chatMessages.add(new UserMessage(sendReqVO.getContent()));
177 149  
178 150 // 2. 构建 ChatOptions 对象
179 151 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
180   - ChatOptions chatOptions = buildChatOptions(platform, model.getModel(),
  152 + ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
181 153 conversation.getTemperature(), conversation.getMaxTokens());
182 154 return new Prompt(chatMessages, chatOptions);
183 155 }
184 156  
185   - private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
186   - Float temperatureF = temperature != null ? temperature.floatValue() : null;
187   - //noinspection EnhancedSwitchMigration
188   - switch (platform) {
189   - case OPENAI:
190   - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
191   - case OLLAMA:
192   - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
193   - case YI_YAN:
194   - // TODO 芋艿:貌似 model 只要一设置,就报错
195   -// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
196   - return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
197   - case XING_HUO:
198   - return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
199   - .setMaxTokens(maxTokens);
200   - case QIAN_WEN:
201   - return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
202   - default:
203   - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
204   - }
205   - }
206   -
207 157 /**
208 158 * 从历史消息中,获得倒序的 n 组消息作为消息上下文
209 159 *
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
... ... @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageR
8 8 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
9 9 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
10 10 import jakarta.validation.Valid;
11   -import org.springframework.ai.chat.model.StreamingChatModel;
  11 +import org.springframework.ai.chat.model.ChatModel;
12 12 import org.springframework.ai.image.ImageModel;
13 13  
14 14 import java.util.List;
... ... @@ -76,12 +76,12 @@ public interface AiApiKeyService {
76 76 // ========== 与 spring-ai 集成 ==========
77 77  
78 78 /**
79   - * 获得 StreamingChatClient 对象
  79 + * 获得 ChatModel 对象
80 80 *
81 81 * @param id 编号
82   - * @return StreamingChatClient 对象
  82 + * @return ChatModel 对象
83 83 */
84   - StreamingChatModel getStreamingChatClient(Long id);
  84 + ChatModel getChatClient(Long id);
85 85  
86 86 /**
87 87 * 获得 ImageClient 对象
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
... ... @@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR
12 12 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
13 13 import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
14 14 import jakarta.annotation.Resource;
15   -import org.springframework.ai.chat.model.StreamingChatModel;
  15 +import org.springframework.ai.chat.model.ChatModel;
16 16 import org.springframework.ai.image.ImageModel;
17 17 import org.springframework.stereotype.Service;
18 18 import org.springframework.validation.annotation.Validated;
... ... @@ -98,10 +98,10 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
98 98 // ========== 与 spring-ai 集成 ==========
99 99  
100 100 @Override
101   - public StreamingChatModel getStreamingChatClient(Long id) {
  101 + public ChatModel getChatClient(Long id) {
102 102 AiApiKeyDO apiKey = validateApiKey(id);
103 103 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
104   - return clientFactory.getOrCreateStreamingChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
  104 + return clientFactory.getOrCreateChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
105 105 }
106 106  
107 107 @Override
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
... ... @@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.infra.api.file.FileApi;
21 21 import jakarta.annotation.Resource;
22 22 import lombok.extern.slf4j.Slf4j;
23 23 import org.springframework.stereotype.Service;
  24 +import org.springframework.transaction.annotation.Transactional;
24 25  
25 26 import java.util.*;
26 27  
... ... @@ -49,10 +50,10 @@ public class AiMusicServiceImpl implements AiMusicService {
49 50 private FileApi fileApi;
50 51  
51 52 @Override
  53 + @Transactional(rollbackFor = Exception.class)
52 54 public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) {
53 55 // 1. 调用 Suno 生成音乐
54 56 SunoApi sunoApi = apiKeyService.getSunoApi();
55   - // TODO 芋艿:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的 --xin:大部分ok的,补充了error_message
56 57 List<SunoApi.MusicData> musicDataList;
57 58 if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
58 59 // 1.1 描述模式
... ... @@ -164,14 +165,9 @@ public class AiMusicServiceImpl implements AiMusicService {
164 165 */
165 166 private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
166 167 return convertList(musicList, musicData -> {
167   - Integer status;
168   - if (Objects.equals("complete", musicData.status())) {
169   - status = AiMusicStatusEnum.SUCCESS.getStatus();
170   - } else if (Objects.equals("error", musicData.status())) {
171   - status = AiMusicStatusEnum.FAIL.getStatus();
172   - } else {
173   - status = AiMusicStatusEnum.IN_PROGRESS.getStatus();
174   - }
  168 + Integer status = Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.SUCCESS.getStatus()
  169 + : Objects.equals("error", musicData.status()) ? AiMusicStatusEnum.FAIL.getStatus()
  170 + : AiMusicStatusEnum.IN_PROGRESS.getStatus();
175 171 return new AiMusicDO()
176 172 .setTaskId(musicData.id()).setModel(musicData.modelName())
177 173 .setDescription(musicData.gptDescriptionPrompt())
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
... ... @@ -2,28 +2,25 @@ package cn.iocoder.yudao.module.ai.service.write;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
5   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
6   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
  5 +import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
7 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
9 8 import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO;
10 9 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
11 10 import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO;
12 11 import cn.iocoder.yudao.module.ai.dal.mysql.write.AiWriteMapper;
  12 +import cn.iocoder.yudao.module.ai.enums.DictTypeConstants;
13 13 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
14   -import cn.iocoder.yudao.module.ai.enums.write.*;
  14 +import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum;
15 15 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
16 16 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
17   -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
  17 +import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
18 18 import jakarta.annotation.Resource;
19 19 import lombok.extern.slf4j.Slf4j;
20 20 import org.springframework.ai.chat.model.ChatResponse;
21 21 import org.springframework.ai.chat.model.StreamingChatModel;
22 22 import org.springframework.ai.chat.prompt.ChatOptions;
23 23 import org.springframework.ai.chat.prompt.Prompt;
24   -import org.springframework.ai.ollama.api.OllamaOptions;
25   -import org.springframework.ai.openai.OpenAiChatOptions;
26   -import org.springframework.ai.qianfan.QianFanChatOptions;
27 24 import org.springframework.stereotype.Service;
28 25 import reactor.core.publisher.Flux;
29 26  
... ... @@ -45,25 +42,30 @@ public class AiWriteServiceImpl implements AiWriteService {
45 42 private AiApiKeyService apiKeyService;
46 43 @Resource
47 44 private AiChatModelService chatModalService;
  45 +
  46 + @Resource
  47 + private DictDataApi dictDataApi;
  48 +
48 49 @Resource
49   - private AiWriteMapper writeMapper; // TODO @xin:上面空一行;因为同类之间不要空行,非同类空行;
  50 + private AiWriteMapper writeMapper;
50 51  
51 52 @Override
52 53 public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) {
53   - // 1.1 校验模型
54   - // TODO @xin:可以约定大于配置先,查询某个名字。例如说,写作助手!然后写作助手,上面是有个 model 的,可以使用它。
55   - AiChatModelDO model = chatModalService.validateChatModel(14L);
56   - StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
  54 + // 1.1 校验模型 TODO 芋艿 是不是取默认的模型也ok?;那可以,有限拿 chatRole 的角色;如果没有,则获取默认的;
  55 + AiChatModelDO model = chatModalService.getRequiredDefaultChatModel();
  56 + StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
57 57 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
58   - ChatOptions chatOptions = buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
59 58  
60 59 // 1.2 插入写作信息
  60 + // TODO @xin:建议把 writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()),写在 toBean 的 consumer 里;原因是,让这个 set 保持完整性
61 61 AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class);
62 62 writeMapper.insert(writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()));
63 63  
64 64 // 2.1 构建提示词
  65 + ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
65 66 Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions);
66 67 Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
  68 +
67 69 // 2.2 流式返回
68 70 StringBuffer contentBuffer = new StringBuffer();
69 71 return streamResponse.map(chunk -> {
... ... @@ -83,11 +85,13 @@ public class AiWriteServiceImpl implements AiWriteService {
83 85 private String buildWritingPrompt(AiWriteGenerateReqVO generateReqVO) {
84 86 String template;
85 87 Integer writeType = generateReqVO.getType();
86   - String format = AiWriteFormatEnum.valueOfFormat(generateReqVO.getFormat()).getName();
87   - String tone = AiWriteToneEnum.valueOfTone(generateReqVO.getTone()).getName();
88   - String language = AiLanguageEnum.valueOfLanguage(generateReqVO.getLanguage()).getName();
89   - String length = AiWriteLengthEnum.valueOfLength(generateReqVO.getLength()).getName();
  88 + String format = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_FORMAT, generateReqVO.getFormat());
  89 + String tone = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_TONE, generateReqVO.getFormat());
  90 + String language = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LANGUAGE, generateReqVO.getFormat());
  91 + String length = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LENGTH, generateReqVO.getFormat());
  92 + // TODO @xin:建议改成 if return 哈;更简洁;
90 93 if (Objects.equals(writeType, AiWriteTypeEnum.WRITING.getType())) {
  94 + // TODO @xin:写成静态枚举哈
91 95 template = "请撰写一篇关于 [{}] 的文章。文章的内容格式为:[{}],语气为:[{}],语言为:[{}],长度为:[{}]。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。";
92 96 return StrUtil.format(template, generateReqVO.getPrompt(), format, tone, language, length);
93 97 } else if (Objects.equals(writeType, AiWriteTypeEnum.REPLY.getType())) {
... ... @@ -98,27 +102,4 @@ public class AiWriteServiceImpl implements AiWriteService {
98 102 }
99 103 }
100 104  
101   - // TODO 芋艿:复用
102   - private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
103   - Float temperatureF = temperature != null ? temperature.floatValue() : null;
104   - //noinspection EnhancedSwitchMigration
105   - switch (platform) {
106   - case OPENAI:
107   - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
108   - case OLLAMA:
109   - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
110   - case YI_YAN:
111   - // TODO 芋艿:貌似 model 只要一设置,就报错
112   -// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
113   - return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
114   - case XING_HUO:
115   - return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
116   - .setMaxTokens(maxTokens);
117   - case QIAN_WEN:
118   - return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
119   - default:
120   - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
121   - }
122   - }
123   -
124 105 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.ai.core.factory;
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4 4 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
5 5 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
6   -import org.springframework.ai.chat.model.StreamingChatModel;
  6 +import org.springframework.ai.chat.model.ChatModel;
7 7 import org.springframework.ai.image.ImageModel;
8 8  
9 9 /**
... ... @@ -14,26 +14,26 @@ import org.springframework.ai.image.ImageModel;
14 14 public interface AiClientFactory {
15 15  
16 16 /**
17   - * 基于指定配置,获得 StreamingChatClient 对象
  17 + * 基于指定配置,获得 ChatModel 对象
18 18 *
19 19 * 如果不存在,则进行创建
20 20 *
21 21 * @param platform 平台
22 22 * @param apiKey API KEY
23 23 * @param url API URL
24   - * @return StreamingChatClient 对象
  24 + * @return ChatModel 对象
25 25 */
26   - StreamingChatModel getOrCreateStreamingChatClient(AiPlatformEnum platform, String apiKey, String url);
  26 + ChatModel getOrCreateChatClient(AiPlatformEnum platform, String apiKey, String url);
27 27  
28 28 /**
29   - * 基于默认配置,获得 StreamingChatClient 对象
  29 + * 基于默认配置,获得 ChatModel 对象
30 30 *
31 31 * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置
32 32 *
33 33 * @param platform 平台
34   - * @return StreamingChatClient 对象
  34 + * @return ChatModel 对象
35 35 */
36   - StreamingChatModel getDefaultStreamingChatClient(AiPlatformEnum platform);
  36 + ChatModel getDefaultChatClient(AiPlatformEnum platform);
37 37  
38 38 /**
39 39 * 基于默认配置,获得 ImageClient 对象
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java
... ... @@ -23,7 +23,7 @@ import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
23 23 import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
24 24 import org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties;
25 25 import org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties;
26   -import org.springframework.ai.chat.model.StreamingChatModel;
  26 +import org.springframework.ai.chat.model.ChatModel;
27 27 import org.springframework.ai.image.ImageModel;
28 28 import org.springframework.ai.ollama.OllamaChatModel;
29 29 import org.springframework.ai.ollama.api.OllamaApi;
... ... @@ -50,9 +50,9 @@ import java.util.List;
50 50 public class AiClientFactoryImpl implements AiClientFactory {
51 51  
52 52 @Override
53   - public StreamingChatModel getOrCreateStreamingChatClient(AiPlatformEnum platform, String apiKey, String url) {
54   - String cacheKey = buildClientCacheKey(StreamingChatModel.class, platform, apiKey, url);
55   - return Singleton.get(cacheKey, (Func0<StreamingChatModel>) () -> {
  53 + public ChatModel getOrCreateChatClient(AiPlatformEnum platform, String apiKey, String url) {
  54 + String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url);
  55 + return Singleton.get(cacheKey, (Func0<ChatModel>) () -> {
56 56 //noinspection EnhancedSwitchMigration
57 57 switch (platform) {
58 58 case OPENAI:
... ... @@ -74,7 +74,7 @@ public class AiClientFactoryImpl implements AiClientFactory {
74 74 }
75 75  
76 76 @Override
77   - public StreamingChatModel getDefaultStreamingChatClient(AiPlatformEnum platform) {
  77 + public ChatModel getDefaultChatClient(AiPlatformEnum platform) {
78 78 //noinspection EnhancedSwitchMigration
79 79 switch (platform) {
80 80 case OPENAI:
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.core.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  5 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
  6 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
  7 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
  8 +import org.springframework.ai.chat.messages.*;
  9 +import org.springframework.ai.chat.prompt.ChatOptions;
  10 +import org.springframework.ai.ollama.api.OllamaOptions;
  11 +import org.springframework.ai.openai.OpenAiChatOptions;
  12 +import org.springframework.ai.qianfan.QianFanChatOptions;
  13 +
  14 +/**
  15 + * Spring AI 工具类
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +public class AiUtils {
  20 +
  21 + public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
  22 + Float temperatureF = temperature != null ? temperature.floatValue() : null;
  23 + //noinspection EnhancedSwitchMigration
  24 + switch (platform) {
  25 + case OPENAI:
  26 + return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  27 + case OLLAMA:
  28 + return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
  29 + case YI_YAN:
  30 + // TODO @xin:貌似 model 只要一设置,就报错;可以排查下
  31 +// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  32 + return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  33 + case XING_HUO:
  34 + return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
  35 + .setMaxTokens(maxTokens);
  36 + case QIAN_WEN:
  37 + return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
  38 + default:
  39 + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
  40 + }
  41 + }
  42 +
  43 + public static Message buildMessage(String type, String content) {
  44 + if (MessageType.USER.getValue().equals(type)) {
  45 + return new UserMessage(content);
  46 + }
  47 + if (MessageType.ASSISTANT.getValue().equals(type)) {
  48 + return new AssistantMessage(content);
  49 + }
  50 + if (MessageType.SYSTEM.getValue().equals(type)) {
  51 + return new SystemMessage(content);
  52 + }
  53 + if (MessageType.FUNCTION.getValue().equals(type)) {
  54 + return new FunctionMessage(content);
  55 + }
  56 + throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type));
  57 + }
  58 +
  59 +}
0 60 \ No newline at end of file
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java
... ... @@ -236,6 +236,7 @@ public class TongYiChatModel extends
236 236 .model(Generation.Models.QWEN_TURBO)
237 237 // {@link GenerationOutput}
238 238 .resultFormat(ConversationParam.ResultFormat.MESSAGE)
  239 + .incrementalOutput(true)
239 240  
240 241 .build();
241 242  
... ...