Commit 6225e18f70a385c6bfcdfcf40ee53e23d09b304c

Authored by YunaiV
1 parent 949d5a18

【新增】AI:绘图(MJ)接入 API KEY 管理

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
... ... @@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
9 9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
10 10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
11 11 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
12   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO;
  12 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
13 13 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
14 14 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
15 15 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -114,11 +114,11 @@ public class AiImageController {
114 114 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
115 115 }
116 116  
117   - @PutMapping("/update-public-status")
118   - @Operation(summary = "更新绘画发布状态")
  117 + @PutMapping("/update")
  118 + @Operation(summary = "更新绘画")
119 119 @PreAuthorize("@ss.hasPermission('ai:image:update')")
120   - public CommonResult<Boolean> updateImagePublicStatus(@Valid @RequestBody AiImageUpdatePublicStatusReqVO updateReqVO) {
121   - imageService.updateImagePublicStatus(updateReqVO);
  120 + public CommonResult<Boolean> updateImage(@Valid @RequestBody AiImageUpdateReqVO updateReqVO) {
  121 + imageService.updateImage(updateReqVO);
122 122 return success(true);
123 123 }
124 124  
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdatePublicStatusReqVO.java renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java
... ... @@ -4,15 +4,15 @@ import io.swagger.v3.oas.annotations.media.Schema;
4 4 import jakarta.validation.constraints.NotNull;
5 5 import lombok.Data;
6 6  
7   -@Schema(description = "管理后台 - AI 绘画修改发布状态 Request VO")
  7 +@Schema(description = "管理后台 - AI 绘画修改 Request VO")
8 8 @Data
9   -public class AiImageUpdatePublicStatusReqVO {
  9 +public class AiImageUpdateReqVO {
10 10  
11 11 @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
  12 + @NotNull(message = "编号不能为空")
12 13 private Long id;
13 14  
14   - @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
15   - @NotNull(message = "是否发布不能为空")
  15 + @Schema(description = "是否发布", example = "true")
16 16 private Boolean publicStatus;
17 17  
18 18 }
19 19 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
... ... @@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
7 7 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
8   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
9 9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
10 10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
11 11 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -71,11 +71,11 @@ public interface AiImageService {
71 71 PageResult<AiImageDO> getImagePage(AiImagePageReqVO pageReqVO);
72 72  
73 73 /**
74   - * 更新绘画发布状态
  74 + * 更新绘画
75 75 *
76 76 * @param updateReqVO 更新信息
77 77 */
78   - void updateImagePublicStatus(@Valid AiImageUpdatePublicStatusReqVO updateReqVO);
  78 + void updateImage(@Valid AiImageUpdateReqVO updateReqVO);
79 79  
80 80 /**
81 81 * 删除绘画
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
... ... @@ -15,7 +15,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
15 15 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
16 16 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
17 17 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
18   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO;
  18 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
19 19 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
20 20 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
21 21 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -62,9 +62,6 @@ public class AiImageServiceImpl implements AiImageService {
62 62 @Resource
63 63 private AiApiKeyService apiKeyService;
64 64  
65   - @Resource
66   - private MidjourneyApi midjourneyApi;
67   -
68 65 @Override
69 66 public PageResult<AiImageDO> getImagePageMy(Long userId, PageParam pageReqVO) {
70 67 return imageMapper.selectPage(userId, pageReqVO);
... ... @@ -151,7 +148,7 @@ public class AiImageServiceImpl implements AiImageService {
151 148 }
152 149  
153 150 @Override
154   - public void updateImagePublicStatus(AiImageUpdatePublicStatusReqVO updateReqVO) {
  151 + public void updateImage(AiImageUpdateReqVO updateReqVO) {
155 152 // 1. 校验存在
156 153 validateImageExists(updateReqVO.getId());
157 154 // 2. 更新发布状态
... ... @@ -179,6 +176,7 @@ public class AiImageServiceImpl implements AiImageService {
179 176 @Override
180 177 @Transactional(rollbackFor = Exception.class)
181 178 public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) {
  179 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
182 180 // 1. 保存数据库
183 181 AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false)
184 182 .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus())
... ... @@ -206,6 +204,7 @@ public class AiImageServiceImpl implements AiImageService {
206 204  
207 205 @Override
208 206 public Integer midjourneySync() {
  207 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
209 208 // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image
210 209 List<AiImageDO> imageList = imageMapper.selectListByStatusAndPlatform(
211 210 AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform());
... ... @@ -272,6 +271,7 @@ public class AiImageServiceImpl implements AiImageService {
272 271  
273 272 @Override
274 273 public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) {
  274 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
275 275 // 1.1 检查 image
276 276 AiImageDO image = validateImageExists(reqVO.getId());
277 277 if (ObjUtil.notEqual(userId, image.getUserId())) {
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/MidjourneyImageOptions.java deleted
1   -package cn.iocoder.yudao.module.ai.service.image;
2   -
3   -import lombok.Data;
4   -import org.springframework.ai.image.ImageOptions;
5   -
6   -/**
7   - * @author fansili
8   - * @time 2024/6/5 10:34
9   - * @since 1.0
10   - */
11   -@Data
12   -public class MidjourneyImageOptions implements ImageOptions {
13   - /**
14   - * 模型
15   - */
16   - private String model;
17   - /**
18   - * 宽度
19   - */
20   - private Integer width;
21   - /**
22   - * 高度
23   - */
24   - private Integer height;
25   - /**
26   - * 版本
27   - */
28   - private String version;
29   - /**
30   - * 参数
31   - */
32   - private String state;
33   -
34   - @Override
35   - public Integer getN() {
36   - return 0;
37   - }
38   -
39   - @Override
40   - public String getModel() {
41   - return model;
42   - }
43   -
44   - @Override
45   - public Integer getWidth() {
46   - return width;
47   - }
48   -
49   - @Override
50   - public Integer getHeight() {
51   - return height;
52   - }
53   -
54   - @Override
55   - public String getResponseFormat() {
56   - return "";
57   - }
58   -}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
1 1 package cn.iocoder.yudao.module.ai.service.model;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  4 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
4 5 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
5 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 7 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
... ... @@ -93,6 +94,15 @@ public interface AiApiKeyService {
93 94 ImageClient getImageClient(AiPlatformEnum platform);
94 95  
95 96 /**
  97 + * 获得 MidjourneyApi 对象
  98 + *
  99 + * TODO 可优化点:目前默认获取 Midjourney 对应的第一个开启的配置用于绘画;后续可以支持配置选择
  100 + *
  101 + * @return MidjourneyApi 对象
  102 + */
  103 + MidjourneyApi getMidjourneyApi();
  104 +
  105 + /**
96 106 * 获得 SunoApi 对象
97 107 *
98 108 * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4 4 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
  5 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
5 6 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
6 7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
... ... @@ -113,6 +114,16 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
113 114 }
114 115  
115 116 @Override
  117 + public MidjourneyApi getMidjourneyApi() {
  118 + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
  119 + AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
  120 + if (apiKey == null) {
  121 + return null;
  122 + }
  123 + return clientFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl());
  124 + }
  125 +
  126 + @Override
116 127 public SunoApi getSunoApi() {
117 128 AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
118 129 AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java
1 1 package cn.iocoder.yudao.framework.ai.core.factory;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  4 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
4 5 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
5 6 import org.springframework.ai.chat.StreamingChatClient;
6 7 import org.springframework.ai.image.ImageClient;
... ... @@ -57,6 +58,17 @@ public interface AiClientFactory {
57 58 ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url);
58 59  
59 60 /**
  61 + * 基于指定配置,获得 MidjourneyApi 对象
  62 + *
  63 + * 如果不存在,则进行创建
  64 + *
  65 + * @param apiKey API KEY
  66 + * @param url API URL
  67 + * @return MidjourneyApi 对象
  68 + */
  69 + MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url);
  70 +
  71 + /**
60 72 * 基于指定配置,获得 SunoApi 对象
61 73 *
62 74 * 如果不存在,则进行创建
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java
... ... @@ -9,6 +9,7 @@ import cn.hutool.extra.spring.SpringUtil;
9 9 import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration;
10 10 import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties;
11 11 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  12 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
12 13 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
13 14 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
14 15 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
... ... @@ -111,8 +112,18 @@ public class AiClientFactoryImpl implements AiClientFactory {
111 112 }
112 113  
113 114 @Override
  115 + public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) {
  116 + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url);
  117 + return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
  118 + YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class).getMidjourney();
  119 + return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
  120 + });
  121 + }
  122 +
  123 + @Override
114 124 public SunoApi getOrCreateSunoApi(String apiKey, String url) {
115   - return new SunoApi(url);
  125 + String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url);
  126 + return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
116 127 }
117 128  
118 129 private static String buildClientCacheKey(Class<?> clazz, Object... params) {
... ...