浏览代码

【代码优化】AI:MJ 生成图片的优化

YunaiV 10 月之前
父节点
当前提交
098483d2be
共有 23 个文件被更改,包括 322 次插入642 次删除
  1. 3 12
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
  2. 5 5
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java
  3. 0 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/config/YudaoMidjourneyConfiguration.java
  4. 8 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http
  5. 14 9
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
  6. 0 40
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageMidjourneyImagineReqVO.java
  7. 0 30
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageMidjourneyOperationsVO.java
  8. 2 4
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java
  9. 0 33
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/MidjourneyImagineReqVO.java
  10. 0 75
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/MidjourneyNotifyReqVO.java
  11. 0 30
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/MidjourneySubmitRespVO.java
  12. 38 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiImageMidjourneyImagineReqVO.java
  13. 4 19
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java
  14. 5 41
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java
  15. 0 79
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/MidjourneyJob.java
  16. 28 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/image/MidjourneySyncJob.java
  17. 18 18
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
  18. 100 113
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
  19. 0 28
      yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
  20. 86 89
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java
  21. 4 6
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/package-info.java
  22. 0 7
      yudao-server/src/main/resources/application-local.yaml
  23. 7 0
      yudao-server/src/main/resources/application.yaml

+ 3 - 12
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java

@@ -21,30 +21,21 @@ public interface ErrorCodeConstants {
     // ========== API 聊天模型 1-040-002-000 ==========
     ErrorCode CHAT_ROLE_NOT_EXISTS = new ErrorCode(1_040_002_000, "聊天角色不存在");
     ErrorCode CHAT_ROLE_DISABLE = new ErrorCode(1_040_001_001, "聊天角色({})已禁用!");
-    ErrorCode CHAT_ROLE_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认聊天角色");
 
     // ========== API 聊天会话 1-040-003-000 ==========
 
     ErrorCode CHAT_CONVERSATION_NOT_EXISTS = new ErrorCode(1_040_003_000, "对话不存在!");
     ErrorCode CHAT_CONVERSATION_MODEL_ERROR = new ErrorCode(1_040_003_001, "操作失败,该聊天模型的配置不完整");
-    ErrorCode CHAT_CONVERSATION_UPDATE_MAX_TOKENS_ERROR = new ErrorCode(1_040_003_002, "更新对话失败,最大 Token 超过上限");
-    ErrorCode CHAT_CONVERSATION_UPDATE_MAX_CONTEXTS_ERROR = new ErrorCode(1_040_003_002, "更新对话失败,最大 Context 超过上限");
 
     // ========== API 聊天消息 1-040-004-000 ==========
+
     ErrorCode AI_CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!");
     ErrorCode AI_CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "Stream 对话异常!");
 
-    // midjourney
-
-    ErrorCode AI_MIDJOURNEY_IMAGINE_FAIL = new ErrorCode(1_022_000_040, "midjourney imagine 操作失败!");
-    ErrorCode AI_MIDJOURNEY_OPERATION_NOT_EXISTS = new ErrorCode(1_022_000_040, "midjourney 操作不存在!");
-    ErrorCode AI_MIDJOURNEY_MESSAGE_ID_INCORRECT = new ErrorCode(1_022_000_040, "midjourney message id 不正确!");
-
     // ========== API 绘画 1-040-005-000 ==========
 
-    ErrorCode AI_IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "image 不存在!");
-    ErrorCode AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败! {}");
+    ErrorCode AI_IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!");
+    ErrorCode AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}");
     ErrorCode AI_IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}");
-    ErrorCode AI_IMAGE_SYSTEM_ACCOUNT_INSUFFICIENT_BALANCE = new ErrorCode(1_022_005_003, "Midjourney 系统账户余额不足!");
 
 }

+ 5 - 5
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java

@@ -12,20 +12,20 @@ import lombok.Getter;
 @Getter
 public enum AiImageStatusEnum {
 
-    IN_PROGRESS("10", "进行中"),
-    SUCCESS("20", "完成"),
-    FAIL("30", "失败");
+    IN_PROGRESS(10, "进行中"),
+    SUCCESS(20, "完成"),
+    FAIL(30, "失败");
 
     /**
      * 状态
      */
-    private final String status;
+    private final Integer status;
     /**
      * 状态名
      */
     private final String name;
 
-    public static AiImageStatusEnum valueOfStatus(String status) {
+    public static AiImageStatusEnum valueOfStatus(Integer status) {
         for (AiImageStatusEnum statusEnum : AiImageStatusEnum.values()) {
             if (statusEnum.getStatus().equals(status)) {
                 return statusEnum;

+ 0 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/config/YudaoMidjourneyConfiguration.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.config;
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.MidjourneyConfig;
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;

+ 8 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http

@@ -29,12 +29,17 @@ Authorization: {{token}}
   "style": "vivid"
 }
 
-### chat midjourney
+### 生成图片:生成图片
 
-POST {{baseUrl}}/admin-api/ai/image/midjourney
+POST {{baseUrl}}/ai/image/midjourney/imagine
 Content-Type: application/json
 Authorization: {{token}}
 
 {
-  "prompt": "Cute cartoon style mobile game scene, a colorful camping car with an outdoor table and chairs next to it on the road in a spring forest, the simple structure of the camper van, soft lighting, C4D rendering, 3d model in the style of a cartoon, cute shape, a pastel color scheme, closeup view from the side angle, high resolution, bright colors, a happy atmosphere."
+  "prompt": "中国旗袍",
+  "model": "midjourney",
+  "width": "1",
+  "height": "1",
+  "version": "6.0",
+  "base64Array": []
 }

+ 14 - 9
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java

@@ -1,14 +1,14 @@
 package cn.iocoder.yudao.module.ai.controller.admin.image;
 
 import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.MidjourneyNotifyReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiImageMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
 import cn.iocoder.yudao.module.ai.service.image.AiImageService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -63,19 +63,24 @@ public class AiImageController {
         return success(true);
     }
 
-    // ================ midjourney 接口 ================
+    // ================ midjourney 专属 ================
 
-    @Operation(summary = "Midjourney imagine(绘画)")
+    @Operation(summary = "【Midjourney】生成图片")
     @PostMapping("/midjourney/imagine")
-    public CommonResult<Long> midjourneyImagine(@Validated @RequestBody AiImageMidjourneyImagineReqVO req) {
-        return success(imageService.midjourneyImagine(getLoginUserId(), req));
+    public CommonResult<Long> midjourneyImagine(@Validated @RequestBody AiImageMidjourneyImagineReqVO reqVO) {
+        if (true) {
+            imageService.midjourneySync();
+            return null;
+        }
+        Long imageId = imageService.midjourneyImagine(getLoginUserId(), reqVO);
+        return success(imageId);
     }
 
-    @Operation(summary = "Midjourney 回调通知", description = "由 Midjourney Proxy 回调")
+    @Operation(summary = "Midjourney 生成图片的回调通知", description = "由 Midjourney Proxy 回调")
     @PostMapping("/midjourney-notify")
     @PermitAll
-    public void midjourneyNotify(@RequestBody MidjourneyNotifyReqVO notifyReqVO) {
-        imageService.midjourneyNotify(notifyReqVO);
+    public void midjourneyNotify(@RequestBody MidjourneyApi.Notify notify) {
+        imageService.midjourneyNotify(notify);
     }
 
     @Operation(summary = "Midjourney Action", description = "例如说:放大、缩小、U1、U2 等")

+ 0 - 40
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageMidjourneyImagineReqVO.java

@@ -1,40 +0,0 @@
-package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotNull;
-import lombok.Data;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-// TODO @fan:待定
-/**
- * midjourney req
- *
- * @author fansili
- * @time 2024/4/28 17:42
- * @since 1.0
- */
-@Data
-@Accessors(chain = true)
-public class AiImageMidjourneyImagineReqVO {
-
-    @Schema(description = "提示词")
-    @NotNull(message = "提示词不能为空!")
-    private String prompt;
-
-    @Schema(description = "模型(midjourney、niji)")
-    private String model;
-
-    @Schema(description = "图片宽度 --ar 设置")
-    private Integer width;
-
-    @Schema(description = "图片高度 --ar 设置")
-    private Integer height;
-
-    @Schema(description = "版本号 --v 设置")
-    private String version;
-
-    @Schema(description = "垫图(参考图)base64数组")
-    private List<String> base64Array;
-}

+ 0 - 30
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageMidjourneyOperationsVO.java

@@ -1,30 +0,0 @@
-package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
-
-import lombok.Data;
-import lombok.experimental.Accessors;
-
-/**
- * mj 保存 components 记录
- *
- *  "components": [
- *           {
- *             "custom_id": "MJ::JOB::upsample::1::5d32f4e8-8d2f-4bef-82d8-bf517e3c3660",
- *             "style": 2,
- *             "label": "U1",
- *             "type": 2
- *           },
- *     ]
- *
- * @author fansili
- * @time 2024/5/8 14:44
- * @since 1.0
- */
-@Data
-@Accessors(chain = true)
-public class AiImageMidjourneyOperationsVO {
-
-    private String custom_id;
-    private String style;
-    private String label;
-    private String type;
-}

+ 2 - 4
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
 
+import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -46,14 +47,11 @@ public class AiImageRespVO {
     @Schema(description = "绘制参数")
     private Map<String, String> options;
 
-    @Schema(description = "绘画 response")
-    private MidjourneyNotifyReqVO response;
-
     // TODO @fan:进度是百分比,还是一个数字哈?感觉这个可以统一成通用字段;
     @Schema(description = "mj 进度")
     private String progress;
 
     @Schema(description = "mj buttons 按钮")
-    private List<MidjourneyNotifyReqVO.Button> buttons;
+    private List<MidjourneyApi.Button> buttons;
 
 }

+ 0 - 33
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/MidjourneyImagineReqVO.java

@@ -1,33 +0,0 @@
-package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotNull;
-import lombok.Data;
-
-import java.util.List;
-
-// TODO @fan:待定
-/**
- * Midjourney:Imagine 请求
- *
- * @author fansili
- * @time 2024/5/30 14:02
- * @since 1.0
- */
-@Data
-public class MidjourneyImagineReqVO {
-
-    @Schema(description = "垫图(参考图)base64数组", required = false)
-    private List<String> base64Array;
-
-    @Schema(description = "通知地址", required = false)
-    @NotNull(message = "回调地址不能为空!")
-    private String notifyHook;
-
-    @Schema(description = "提示词", required = true)
-    @NotNull(message = "提示词不能为空!")
-    private String prompt;
-
-    @Schema(description = "自定义参数", required = false)
-    private String state;
-}

+ 0 - 75
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/MidjourneyNotifyReqVO.java

@@ -1,75 +0,0 @@
-package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import java.util.List;
-
-/**
- * Midjourney Proxy 通知回调
- *
- *  - Midjourney Proxy:通知回调 bean 是 com.github.novicezk.midjourney.support.Task
- *  - 毫秒 api 通知回调文档地址:https://gpt-best.apifox.cn/doc-3530863
- *
- * @author fansili
- * @time 2024/5/31 10:37
- * @since 1.0
- */
-@Data
-public class MidjourneyNotifyReqVO {
-
-    @Schema(description = "job id")
-    private String id;
-
-    @Schema(description = "任务类型 MidjourneyTaskActionEnum")
-    private String action;
-    @Schema(description = "任务状态 MidjourneyTaskStatusEnum")
-    private String status;
-
-    @Schema(description = "提示词")
-    private String prompt;
-    @Schema(description = "提示词-英文")
-    private String promptEn;
-
-    @Schema(description = "任务描述")
-    private String description;
-    @Schema(description = "自定义参数")
-    private String state;
-
-    @Schema(description = "提交时间")
-    private Long submitTime;
-    @Schema(description = "开始执行时间")
-    private Long startTime;
-    @Schema(description = "结束时间")
-    private Long finishTime;
-
-    @Schema(description = "图片url")
-    private String imageUrl;
-
-    @Schema(description = "任务进度")
-    private String progress;
-    @Schema(description = "失败原因")
-    private String failReason;
-
-    @Schema(description = "任务完成后的可执行按钮")
-    private List<Button> buttons;
-
-    @Data
-    public static class Button {
-
-        @Schema(description = "MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识")
-        private String customId;
-
-        @Schema(description = "图标 emoji")
-        private String emoji;
-
-        @Schema(description = "Make Variations 文本")
-        private String label;
-
-        @Schema(description = "类型,系统内部使用")
-        private String type;
-
-        @Schema(description = "样式: 2(Primary)、3(Green)")
-        private String style;
-    }
-}

+ 0 - 30
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/MidjourneySubmitRespVO.java

@@ -1,30 +0,0 @@
-package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.Data;
-
-import java.util.Map;
-
-// TODO @fan:待定
-/**
- * Midjourney:Imagine 请求
- *
- * @author fansili
- * @time 2024/5/30 14:02
- * @since 1.0
- */
-@Data
-public class MidjourneySubmitRespVO {
-
-    @Schema(description = "状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)")
-    private String code;
-
-    @Schema(description = "描述")
-    private String description;
-
-    @Schema(description = "扩展字段")
-    private Map<String, Object> properties;
-
-    @Schema(description = "任务ID")
-    private String result;
-}

+ 38 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiImageMidjourneyImagineReqVO.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - 绘画生成(Midjourney) Request VO")
+@Data
+public class AiImageMidjourneyImagineReqVO {
+
+    @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "中国神龙")
+    @NotEmpty(message = "提示词不能为空!")
+    private String prompt;
+
+    @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "midjourney")
+    @NotEmpty(message = "模型不能为空")
+    private String model; // 参考 MidjourneyApi.ModelEnum
+
+    @Schema(description = "图片宽度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "图片宽度不能为空")
+    private Integer width;
+
+    @Schema(description = "图片高度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "图片高度不能为空")
+    private Integer height;
+
+    @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6.0")
+    @NotEmpty(message = "版本号不能为空")
+    private String version;
+
+    // TODO @fan:参考图,建议用 referImageUrl。
+    @Schema(description = "垫图(参考图)base64数组")
+    private List<String> base64Array;
+
+}

+ 4 - 19
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.ai.dal.dataobject.image;
 
+import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.MidjourneyNotifyReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
 import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
@@ -28,8 +28,6 @@ import java.util.Map;
 @Data
 public class AiImageDO extends BaseDO {
 
-    // TODO @fan:1)使用 java 注释哈,不要注解。2)关联、枚举字段,要关联到对应类,参考 AiChatMessageDO 的注释
-
     /**
      * 编号
      */
@@ -76,7 +74,7 @@ public class AiImageDO extends BaseDO {
      *
      * 枚举 {@link AiImageStatusEnum}
      */
-    private String status;
+    private Integer status;
 
     /**
      * 图片地址
@@ -96,23 +94,11 @@ public class AiImageDO extends BaseDO {
     @TableField(typeHandler = JacksonTypeHandler.class)
     private Map<String, Object> options;
 
-    /**
-     * 绘画 response
-     */
-    @TableField(typeHandler = JacksonTypeHandler.class)
-    private MidjourneyNotifyReqVO response;
-
-    // TODO @fan:这个建议 Double
-    /**
-     * mj 进度(10%、50%、100%)
-     */
-    private String progress;
-
     /**
      * mj buttons 按钮
      */
     @TableField(typeHandler = ButtonTypeHandler.class)
-    private List<MidjourneyNotifyReqVO.Button> buttons;
+    private List<MidjourneyApi.Button> buttons;
 
     /**
      * midjourney proxy 关联的 task id
@@ -124,12 +110,11 @@ public class AiImageDO extends BaseDO {
      */
     private String errorMessage;
 
-    // TODO @芋艿:看看是不是 MidjourneyNotifyReqVO.Button 搞到 MJ API 那
     public static class ButtonTypeHandler extends AbstractJsonTypeHandler<Object> {
 
         @Override
         protected Object parse(String json) {
-            return JsonUtils.parseArray(json, MidjourneyNotifyReqVO.Button.class);
+            return JsonUtils.parseArray(json, MidjourneyApi.Button.class);
         }
 
         @Override

+ 5 - 41
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java

@@ -1,13 +1,10 @@
 package cn.iocoder.yudao.module.ai.dal.mysql.image;
 
-import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
-import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.List;
@@ -20,52 +17,19 @@ import java.util.List;
 @Mapper
 public interface AiImageMapper extends BaseMapperX<AiImageDO> {
 
-    // TODO @fan:这个建议,直接使用 update,service 拼接要改的状态哈
-
-    /**
-     * 更新 - 根据 messageId
-     *
-     * @param mjNonceId
-     * @param aiImageDO
-     */
-    default void updateByMjNonce(Long mjNonceId, AiImageDO aiImageDO) {
-//        this.update(aiImageDO, new LambdaQueryWrapperX<AiImageDO>().eq(AiImageDO::getMjNonceId, mjNonceId));
-        return;
-    }
-
-    /**
-     * 查询 - 根据 job id
-     *
-     * @param id
-     * @return
-     */
-    default AiImageDO selectByJobId(String id) {
-        return this.selectOne(new LambdaQueryWrapperX<AiImageDO>().eq(AiImageDO::getTaskId, id));
+    default AiImageDO selectByTaskId(String taskId) {
+        return this.selectOne(AiImageDO::getTaskId, taskId);
     }
 
-    /**
-     * 查询 - page
-     *
-     * @param userId
-     * @param pageReqVO
-     * @return
-     */
     default PageResult<AiImageDO> selectPage(Long userId, PageParam pageReqVO) {
         return selectPage(pageReqVO, new LambdaQueryWrapperX<AiImageDO>()
                 .eq(AiImageDO::getUserId, userId)
                 .orderByDesc(AiImageDO::getId));
     }
 
-    /**
-     * 查询 - 根据 status 和 platform
-     *
-     * @return
-     */
-    default List<AiImageDO> selectByStatusAndPlatform(AiImageStatusEnum statusEnum, AiPlatformEnum platformEnum) {
-        return this.selectList(new LambdaUpdateWrapper<AiImageDO>()
-                .eq(AiImageDO::getStatus, statusEnum.getStatus())
-                .eq(AiImageDO::getPlatform, platformEnum.getPlatform())
-        );
+    default List<AiImageDO> selectListByStatusAndPlatform(Integer status, String platform) {
+        return selectList(AiImageDO::getStatus, status,
+                AiImageDO::getPlatform, platform);
     }
 
 }

+ 0 - 79
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/MidjourneyJob.java

@@ -1,79 +0,0 @@
-package cn.iocoder.yudao.module.ai.job;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
-import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.MidjourneyNotifyReqVO;
-import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
-import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper;
-import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
-import cn.iocoder.yudao.module.ai.service.image.AiImageService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * midjourney job 定时拉去 midjourney 绘制状态
- *
- * @author fansili
- * @time 2024/6/5 14:55
- * @since 1.0
- */
-@Component
-@Slf4j
-public class MidjourneyJob implements JobHandler {
-
-    // TODO @fan:@Resource
-    @Autowired(required = false)
-    private MidjourneyApi midjourneyApi;
-    @Autowired
-    private AiImageMapper imageMapper;
-    @Autowired
-    private AiImageService imageService;
-
-    // TODO @fan:这个方法,建议实现到 AiImageService,例如说 midjourneySync,返回 int 同步数量。
-    @Override
-    public String execute(String param) {
-        // 1、获取 midjourney 平台,状态在 “进行中” 的 image
-        List<AiImageDO> imageList = imageMapper.selectByStatusAndPlatform(AiImageStatusEnum.IN_PROGRESS, AiPlatformEnum.MIDJOURNEY);
-        log.info("Midjourney 同步 - 任务数量 {}!", imageList.size());
-        if (CollUtil.isEmpty(imageList)) {
-            return "Midjourney 同步 - 数量为空!";
-        }
-        log.info("Midjourney 同步 - 开始...");
-        // 2、批量拉去 task 信息
-        List<MidjourneyApi.NotifyRequest> taskList = midjourneyApi
-                .listByCondition(CollectionUtils.convertSet(imageList, AiImageDO::getTaskId));
-        Map<String, MidjourneyApi.NotifyRequest> taskIdMap
-                = CollectionUtils.convertMap(taskList, MidjourneyApi.NotifyRequest::id);
-        // 3、更新 image 状态
-        List<AiImageDO> updateImageList = new ArrayList<>();
-        for (AiImageDO aiImageDO : imageList) {
-            // 3.1 排除掉空的情况
-            if (!taskIdMap.containsKey(aiImageDO.getTaskId())) {
-                log.warn("Midjourney 同步 - {} 任务为空!", aiImageDO.getTaskId());
-                continue;
-            }
-            // TODO @ 3.1 和 3.2 是不是融合下;get,然后判空,continue;
-            // 3.2 获取通知对象
-            MidjourneyApi.NotifyRequest notifyRequest = taskIdMap.get(aiImageDO.getTaskId());
-            // 3.2 构建更新对象
-            // TODO @fan:建议 List<MidjourneyNotifyReqVO> 作为 imageService 去更新;
-            // TODO @芋艿 BeanUtils.toBean 转换为 null
-            updateImageList.add(imageService.buildUpdateImage(aiImageDO.getId(),
-                    JsonUtils.parseObject(JsonUtils.toJsonString(notifyRequest), MidjourneyNotifyReqVO.class)));
-        }
-        // 4、批了更新 updateImageList
-        imageMapper.updateBatch(updateImageList);
-        return "Midjourney 同步 - ".concat(String.valueOf(updateImageList.size())).concat(" 任务!");
-    }
-
-}

+ 28 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/image/MidjourneySyncJob.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.ai.job.image;
+
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.module.ai.service.image.AiImageService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * Midjourney 同步 Job:定时拉去 midjourney 绘制状态
+ *
+ * @author fansili
+ */
+@Component
+@Slf4j
+public class MidjourneySyncJob implements JobHandler {
+
+    @Resource
+    private AiImageService imageService;
+
+    @Override
+    public String execute(String param) {
+        Integer count = imageService.midjourneySync();
+        log.info("[execute][同步 Midjourney ({}) 个]", count);
+        return String.format("同步 Midjourney %s 个", count);
+    }
+
+}

+ 18 - 18
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java

@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.ai.service.image;
 
+import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.MidjourneyNotifyReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyImagineReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiImageMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
 
 /**
@@ -41,38 +41,37 @@ public interface AiImageService {
     Long drawImage(Long userId, AiImageDrawReqVO drawReqVO);
 
     /**
-     * Midjourney imagine(绘画)
+     * 删除【我的】绘画记录
      *
+     * @param id 绘画编号
      * @param userId 用户编号
-     * @param imagineReqVO 绘制请求
-     * @return 绘画编号
      */
-    Long midjourneyImagine(Long userId, AiImageMidjourneyImagineReqVO imagineReqVO);
+    void deleteImageMy(Long id, Long userId);
+
+    // ================ midjourney 专属 ================
 
     /**
-     * 删除【我的】绘画记录
+     * 【Midjourney】生成图片
      *
-     * @param id 绘画编号
      * @param userId 用户编号
+     * @param reqVO 绘制请求
+     * @return 绘画编号
      */
-    void deleteImageMy(Long id, Long userId);
+    Long midjourneyImagine(Long userId, AiImageMidjourneyImagineReqVO reqVO);
 
     /**
-     * midjourney proxy - 回调通知
+     * 【Midjourney】同步图片进展
      *
-     * @param notifyReqVO
-     * @return
+     * @return 同步成功数量
      */
-    void midjourneyNotify(MidjourneyNotifyReqVO notifyReqVO);
+    Integer midjourneySync();
 
     /**
-     * 构建 midjourney - 更新对象
+     * 【Midjourney】通知图片进展
      *
-     * @param imageId
-     * @param notifyReqVO
-     * @return
+     * @param notify 通知
      */
-    AiImageDO buildUpdateImage(Long imageId, MidjourneyNotifyReqVO notifyReqVO);
+    void midjourneyNotify(MidjourneyApi.Notify notify);
 
     /**
      * midjourney - action(放大、缩小、U1、U2...)
@@ -83,4 +82,5 @@ public interface AiImageService {
      * @return
      */
     void midjourneyAction(Long loginUserId, Long imageId, String customId);
+
 }

+ 100 - 113
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java

@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.ai.service.image;
 
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.codec.Base64;
-import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
@@ -14,8 +14,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyImagineReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.MidjourneyNotifyReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiImageMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
 import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper;
 import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
@@ -29,15 +28,17 @@ import org.springframework.ai.image.ImagePrompt;
 import org.springframework.ai.image.ImageResponse;
 import org.springframework.ai.openai.OpenAiImageOptions;
 import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
 
 /**
@@ -51,12 +52,16 @@ public class AiImageServiceImpl implements AiImageService {
 
     @Resource
     private AiImageMapper imageMapper;
+
     @Resource
     private FileApi fileApi;
+
     @Resource
     private AiApiKeyService apiKeyService;
-    @Autowired(required = false)
+
+    @Resource
     private MidjourneyApi midjourneyApi;
+
     @Value("${ai.midjourney-proxy.notifyUrl:http://127.0.0.1:48080/admin-api/ai/image/midjourney-notify}")
     private String midjourneyNotifyUrl;
 
@@ -74,7 +79,7 @@ public class AiImageServiceImpl implements AiImageService {
     public Long drawImage(Long userId, AiImageDrawReqVO drawReqVO) {
         // 1. 保存数据库
         AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false)
-                .setWidth(drawReqVO.getWidth()).setHeight(drawReqVO.getHeight()).setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus());
+                .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus());
         imageMapper.insert(image);
         // 2. 异步绘制,后续前端通过返回的 id 进行轮询结果
         getSelf().executeDrawImage(image, drawReqVO);
@@ -122,101 +127,121 @@ public class AiImageServiceImpl implements AiImageService {
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    public Long midjourneyImagine(Long userId, AiImageMidjourneyImagineReqVO req) {
-        // 1、构建 AiImageDO 并 保存
-        AiImageDO image = new AiImageDO()
-                .setUserId(userId)
-                .setPrompt(req.getPrompt())
-                .setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform())
-                .setModel(req.getModel())
-                .setWidth(req.getWidth())
-                .setHeight(req.getHeight())
-                .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus());
-        imageMapper.insert(image);
+    public void deleteImageMy(Long id, Long userId) {
+        // 1. 校验是否存在
+        AiImageDO image = validateImageExists(id);
+        if (ObjUtil.notEqual(image.getUserId(), userId)) {
+            throw exception(AI_IMAGE_NOT_EXISTS);
+        }
+        // 2. 删除记录
+        imageMapper.deleteById(id);
+    }
 
-        // 3、调用 MidjourneyProxy 提交任务
+    private AiImageDO validateImageExists(Long id) {
+        AiImageDO image = imageMapper.selectById(id);
+        if (image == null) {
+            throw exception(AI_IMAGE_NOT_EXISTS);
+        }
+        return image;
+    }
 
-        // 3.1、设置 midjourney 扩展参数
-        MidjourneyApi.ImagineRequest imagineRequest = new MidjourneyApi.ImagineRequest(null, midjourneyNotifyUrl, req.getPrompt(),
-                buildParams(req.getWidth(), req.getHeight(), req.getVersion(),
-                        MidjourneyApi.ModelEnum.valueOfModel(req.getModel())));
-        // 3.2、提交绘画请求
-        // TODO @fan:5 这里,失败的情况,到底抛出异常,还是 RespVO,可以参考 OpenAI 的 API 封装
-        MidjourneyApi.SubmitResponse submitResponse = midjourneyApi.imagine(imagineRequest);
+    // ================ midjourney 专属 ================
 
-        // 4、保存任务 id (状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误))
-        if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(submitResponse.code())) {
-            if (submitResponse.description().contains("quota_not_enough")) {
-                throw exception(AI_IMAGE_SYSTEM_ACCOUNT_INSUFFICIENT_BALANCE, submitResponse.description());
-            }
-            throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, submitResponse.description());
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long midjourneyImagine(Long userId, AiImageMidjourneyImagineReqVO reqVO) {
+        // 1. 保存数据库
+        AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false)
+                .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus())
+                .setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform());
+        imageMapper.insert(image);
+
+        // 2. 调用 Midjourney Proxy 提交任务
+        MidjourneyApi.ImagineRequest imagineRequest = new MidjourneyApi.ImagineRequest(
+                null, midjourneyNotifyUrl, reqVO.getPrompt(),
+                MidjourneyApi.ImagineRequest.buildState(reqVO.getWidth(), reqVO.getHeight(), reqVO.getVersion(), reqVO.getModel()));
+        MidjourneyApi.SubmitResponse imagineResponse = midjourneyApi.imagine(imagineRequest);
+
+        // 3. 情况一【失败】:抛出业务异常
+        if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(imagineResponse.code())) {
+            String description = imagineResponse.description().contains("quota_not_enough") ?
+                    "账户余额不足" : imagineResponse.description();
+            throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
         }
-        // 4.1、更新 taskId 和参数
+
+        // 4. 情况二【成功】:更新 taskId 和参数
         imageMapper.updateById(new AiImageDO()
                 .setId(image.getId())
-                .setTaskId(submitResponse.result())
-                .setOptions(BeanUtil.beanToMap(req))
+                .setTaskId(imagineResponse.result())
+                .setOptions(BeanUtil.beanToMap(reqVO))
         );
         return image.getId();
     }
 
-
     @Override
-    public void deleteImageMy(Long id, Long userId) {
-        // 1. 校验是否存在
-        AiImageDO image = validateImageExists(id);
-        if (ObjUtil.notEqual(image.getUserId(), userId)) {
-            throw exception(AI_IMAGE_NOT_EXISTS);
+    public Integer midjourneySync() {
+        // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image
+        List<AiImageDO> imageList = imageMapper.selectListByStatusAndPlatform(
+                AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform());
+        if (CollUtil.isEmpty(imageList)) {
+            return 0;
         }
-        // 2. 删除记录
-        imageMapper.deleteById(id);
+        // 1.2 调用 Midjourney Proxy 获取任务进展
+        List<MidjourneyApi.Notify> taskList = midjourneyApi.getTaskList(convertSet(imageList, AiImageDO::getTaskId));
+        Map<String, MidjourneyApi.Notify> taskMap = convertMap(taskList, MidjourneyApi.Notify::id);
+
+        // 2. 逐个处理,更新进展
+        int count = 0;
+        for (AiImageDO image : imageList) {
+            MidjourneyApi.Notify notify = taskMap.get(image.getTaskId());
+            if (notify == null) {
+                log.error("[midjourneySync][image({}) 查询不到进展]", image);
+                continue;
+            }
+            count++;
+            updateMidjourneyStatus(image, notify);
+        }
+        return count;
     }
 
     @Override
-    public void midjourneyNotify(MidjourneyNotifyReqVO notifyReqVO) {
-        // 1、根据 job id 查询关联的 image
-        AiImageDO image = imageMapper.selectByJobId(notifyReqVO.getId());
+    public void midjourneyNotify(MidjourneyApi.Notify notify) {
+        // 1. 校验 image 存在
+        AiImageDO image = imageMapper.selectByTaskId(notify.id());
         if (image == null) {
-            log.warn("midjourneyNotify 回调的 jobId 不存在! jobId: {}", notifyReqVO.getId());
+            log.warn("[midjourneyNotify][回调任务({}) 不存在]", notify.id());
+            return;
         }
-        // 2、转换状态
-        AiImageDO updateImage = buildUpdateImage(image.getId(), notifyReqVO);
-        // 3、更新 image 状态
-        imageMapper.updateById(updateImage);
+        // 2. 更新状态
+        updateMidjourneyStatus(image, notify);
     }
 
-    public AiImageDO buildUpdateImage(Long imageId, MidjourneyNotifyReqVO notifyReqVO) {
-        // 1转换状态
-        String imageStatus = null;
-        if (StrUtil.isNotBlank(notifyReqVO.getStatus())) {
-            MidjourneyApi.TaskStatusEnum taskStatusEnum = MidjourneyApi.TaskStatusEnum.valueOf(notifyReqVO.getStatus());
+    private void updateMidjourneyStatus(AiImageDO image, MidjourneyApi.Notify notify) {
+        // 1. 转换状态
+        Integer status = null;
+        if (StrUtil.isNotBlank(notify.status())) {
+            MidjourneyApi.TaskStatusEnum taskStatusEnum = MidjourneyApi.TaskStatusEnum.valueOf(notify.status());
             if (MidjourneyApi.TaskStatusEnum.SUCCESS == taskStatusEnum) {
-                imageStatus = AiImageStatusEnum.SUCCESS.getStatus();
+                status = AiImageStatusEnum.SUCCESS.getStatus();
             } else if (MidjourneyApi.TaskStatusEnum.FAILURE == taskStatusEnum) {
-                imageStatus = AiImageStatusEnum.FAIL.getStatus();
+                status = AiImageStatusEnum.FAIL.getStatus();
             }
         }
 
-        // 2上传图片
-        String filePath = null;
-        if (!StrUtil.isBlank(notifyReqVO.getImageUrl())) {
+        // 2. 上传图片
+        String picUrl = null;
+        if (StrUtil.isNotBlank(notify.imageUrl())) {
             try {
-                filePath = fileApi.createFile(HttpUtil.downloadBytes(notifyReqVO.getImageUrl()));
+                picUrl = fileApi.createFile(HttpUtil.downloadBytes(notify.imageUrl()));
             } catch (Exception e) {
-                log.warn("midjourneyNotify 图片上传失败! {} 异常:{}", notifyReqVO.getImageUrl(), ExceptionUtil.getMessage(e));
+                picUrl = notify.imageUrl();
+                log.warn("[updateMidjourneyStatus][图片({}) 地址({}) 上传失败]", image.getId(), notify.imageUrl(), e);
             }
         }
 
-        // 3、更新 image 状态
-        return new AiImageDO()
-                .setId(imageId)
-                .setStatus(imageStatus)
-                .setPicUrl(filePath)
-                .setProgress(notifyReqVO.getProgress())
-                .setResponse(notifyReqVO)
-                .setButtons(notifyReqVO.getButtons())
-                .setErrorMessage(notifyReqVO.getFailReason());
+        // 3. 更新 image 状态
+        imageMapper.updateById(new AiImageDO().setId(image.getId()).setStatus(status)
+                .setPicUrl(picUrl).setButtons(notify.buttons()).setErrorMessage(notify.failReason()));
     }
 
     @Override
@@ -236,7 +261,6 @@ public class AiImageServiceImpl implements AiImageService {
 
         // 5、新增 image 记录(根据 image 新增一个)
         AiImageDO newImage = new AiImageDO();
-        newImage.setId(null);
         newImage.setUserId(image.getUserId());
         newImage.setPrompt(image.getPrompt());
 
@@ -248,20 +272,15 @@ public class AiImageServiceImpl implements AiImageService {
         newImage.setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus());
         newImage.setPublicStatus(image.getPublicStatus());
 
-        newImage.setPicUrl(null);
-        newImage.setProgress(null);
-        newImage.setButtons(null);
         newImage.setOptions(image.getOptions());
-        newImage.setResponse(image.getResponse());
         newImage.setTaskId(submitResponse.result());
-        newImage.setErrorMessage(null);
         imageMapper.insert(newImage);
     }
 
-    private static void validateCustomId(String customId, List<MidjourneyNotifyReqVO.Button> buttons) {
+    private static void validateCustomId(String customId, List<MidjourneyApi.Button> buttons) {
         boolean isTrue = false;
-        for (MidjourneyNotifyReqVO.Button button : buttons) {
-            if (button.getCustomId().equals(customId)) {
+        for (MidjourneyApi.Button button : buttons) {
+            if (button.customId().equals(customId)) {
                 isTrue = true;
                 break;
             }
@@ -271,14 +290,6 @@ public class AiImageServiceImpl implements AiImageService {
         }
     }
 
-    private AiImageDO validateImageExists(Long id) {
-        AiImageDO image = imageMapper.selectById(id);
-        if (image == null) {
-            throw exception(AI_IMAGE_NOT_EXISTS);
-        }
-        return image;
-    }
-
     /**
      * 获得自身的代理对象,解决 AOP 生效问题
      *
@@ -288,28 +299,4 @@ public class AiImageServiceImpl implements AiImageService {
         return SpringUtil.getBean(getClass());
     }
 
-    // TODO @fan:这个是不是应该放在 MJ API 的封装里面搞哈?
-
-    /**
-     * 构建 Midjourney 自定义参数
-     *
-     * @param width
-     * @param height
-     * @param version
-     * @param model
-     * @return
-     */
-    private String buildParams(Integer width, Integer height, String version, MidjourneyApi.ModelEnum model) {
-        StringBuilder params = new StringBuilder();
-        //  --ar 来设置尺寸
-        params.append(String.format(" --ar %s:%s ", width, height));
-        // --niji 模型
-        if (MidjourneyApi.ModelEnum.NIJI == model) {
-            params.append(String.format(" --niji %s ", version));
-        } else {
-            // --v 版本
-            params.append(String.format(" --v %s ", version));
-        }
-        return params.toString();
-    }
 }

+ 0 - 28
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml

@@ -50,34 +50,6 @@
             <artifactId>junit</artifactId>
             <scope>test</scope>
         </dependency>
-
-        <!-- TODO fan:这里包,要进一步减少 -->
-
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-websocket</artifactId>
-        </dependency>
-
-        <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
-        <dependency>
-            <groupId>com.squareup.okhttp3</groupId>
-            <artifactId>okhttp</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>io.projectreactor.netty</groupId>
-            <artifactId>reactor-netty</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>net.dv8tion</groupId>
-            <artifactId>JDA</artifactId>
-            <version>5.0.0-beta.21</version>
-<!--            <exclusions>-->
-<!--                <exclusion>-->
-<!--                    <groupId>club.minnced</groupId>-->
-<!--                    <artifactId>opus-java</artifactId>-->
-<!--                </exclusion>-->
-<!--            </exclusions>-->
-        </dependency>
     </dependencies>
 
 </project>

+ 86 - 89
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java

@@ -16,66 +16,54 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * Midjourney api
+ * Midjourney API
  *
  * @author fansili
- * @time 2024/6/11 15:46
  * @since 1.0
  */
 @Slf4j
 public class MidjourneyApi {
 
-    private static final String URI_IMAGINE = "/submit/imagine";
-    private static final String URI_ACTON = "/submit/action";
-    private static final String URI_LIST_BY_CONDITION = "/task/list-by-condition";
     private final WebClient webClient;
-    private final MidjourneyConfig midjourneyConfig;
 
     public MidjourneyApi(MidjourneyConfig midjourneyConfig) {
-        this.midjourneyConfig = midjourneyConfig;
         this.webClient = WebClient.builder()
                 .baseUrl(midjourneyConfig.getUrl())
                 .defaultHeaders(ApiUtils.getJsonContentHeaders(midjourneyConfig.getKey()))
                 .build();
     }
 
-
     /**
      * imagine - 根据提示词提交绘画任务
      *
-     * @param imagineReqVO
-     * @return
+     * @param request 请求
+     * @return 提交结果
      */
-    public SubmitResponse imagine(ImagineRequest imagineReqVO) {
-        // 1、发送 post 请求
-        String res = post(URI_IMAGINE, imagineReqVO);
-        // 2、转换 resp
-        return JsonUtils.parseObject(res, SubmitResponse.class);
+    public SubmitResponse imagine(ImagineRequest request) {
+        String response = post("/submit/imagine", request);
+        return JsonUtils.parseObject(response, SubmitResponse.class);
     }
 
     /**
      * action - 放大、缩小、U1、U2...
      *
-     * @param actionReqVO
+     * @param request 请求
+     * @return 提交结果
      */
-    public SubmitResponse action(ActionRequest actionReqVO) {
-        // 1、发送 post 请求
-        String res = post(URI_ACTON, actionReqVO);
-        // 2、转换 resp
+    public SubmitResponse action(ActionRequest request) {
+        String res = post("/submit/action", request);
         return JsonUtils.parseObject(res, SubmitResponse.class);
     }
 
     /**
      * 批量查询 task 任务
      *
-     * @param taskIds
-     * @return
+     * @param ids 任务编号数组
+     * @return task 任务
      */
-    public List<NotifyRequest> listByCondition(Collection<String> taskIds) {
-        // 1、发送 post 请求
-        String res = post(URI_LIST_BY_CONDITION, ImmutableMap.of("ids", taskIds));
-        // 2、转换 对象
-        return JsonUtils.parseArray(res, NotifyRequest.class);
+    public List<Notify> getTaskList(Collection<String> ids) {
+        String res = post("/task/list-by-condition", ImmutableMap.of("ids", ids));
+        return JsonUtils.parseArray(res, Notify.class);
     }
 
     private String post(String uri, Object body) {
@@ -94,12 +82,12 @@ public class MidjourneyApi {
                 .block();
     }
 
-    // ====== record 结构
+    // ========== record 结构 ==========
 
     /**
-     * Midjourney - Imagine 请求
+     * Imagine 请求(生成图片)
      *
-     * @param base64Array 垫图(参考图)base64数组
+     * @param base64Array 垫图(参考图) base64数 
      * @param notifyHook 通知地址
      * @param prompt 提示词
      * @param state 自定义参数
@@ -108,10 +96,24 @@ public class MidjourneyApi {
                                  String notifyHook,
                                  String prompt,
                                  String state) {
+
+        public static String buildState(Integer width, Integer height, String version, String model) {
+            StringBuilder params = new StringBuilder();
+            //  --ar 来设置尺寸
+            params.append(String.format(" --ar %s:%s ", width, height));
+            // --niji 模型
+            if (MidjourneyApi.ModelEnum.NIJI.getModel().equals(model)) {
+                params.append(String.format(" --niji %s ", version));
+            } else {
+                params.append(String.format(" --v %s ", version));
+            }
+            return params.toString();
+        }
+
     }
 
     /**
-     * Midjourney - Action 请求
+     * Action 请求
      *
      * @param customId   操作按钮id
      * @param taskId     操作按钮id
@@ -124,7 +126,7 @@ public class MidjourneyApi {
     }
 
     /**
-     * Midjourney - Submit 返回
+     * Submit 统一返回
      *
      * @param code 状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)
      * @param description 描述
@@ -138,7 +140,7 @@ public class MidjourneyApi {
     }
 
     /**
-     * Midjourney - 通知 request
+     * 通知 request
      *
      * @param id job id
      * @param action 任务类型 {@link TaskActionEnum}
@@ -155,47 +157,47 @@ public class MidjourneyApi {
      * @param failReason 失败原因
      * @param buttons 任务完成后的可执行按钮
      */
-    public record NotifyRequest(String id,
-                                String action,
-                                String status,
+    public record Notify(String id,
+                         String action,
+                         String status,
 
-                                String prompt,
-                                String promptEn,
+                         String prompt,
+                         String promptEn,
 
-                                String description,
-                                String state,
+                         String description,
+                         String state,
 
-                                Long submitTime,
-                                Long startTime,
-                                Long finishTime,
+                         Long submitTime,
+                         Long startTime,
+                         Long finishTime,
 
-                                String imageUrl,
-                                String progress,
-                                String failReason,
-                                List<Button> buttons) {
+                         String imageUrl,
+                         String progress,
+                         String failReason,
+                         List<Button> buttons) {
 
-        /**
-         * button
-         *
-         * @param customId MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
-         * @param emoji 图标 emoji
-         * @param label Make Variations 文本
-         * @param type 类型,系统内部使用
-         * @param style 样式: 2(Primary)、3(Green)
-         */
-        public record Button(String customId,
-                             String emoji,
-                             String label,
-                             String type,
-                             String style) {
+    }
 
-        }
+    /**
+     * button
+     *
+     * @param customId MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
+     * @param emoji 图标 emoji
+     * @param label Make Variations 文本
+     * @param type 类型,系统内部使用
+     * @param style 样式: 2(Primary)、3(Green)
+     */
+    public record Button(String customId,
+                         String emoji,
+                         String label,
+                         String type,
+                         String style) {
     }
 
-    // ====== enums
+    // ============ enums ============
 
     /**
-     * Midjourney - 模型
+     * 模型枚举
      */
     @AllArgsConstructor
     @Getter
@@ -203,24 +205,15 @@ public class MidjourneyApi {
 
         MIDJOURNEY("midjourney", "midjourney"),
         NIJI("niji", "niji"),
-
         ;
 
-        private String model;
-        private String name;
+        private final String model;
+        private final String name;
 
-        public static ModelEnum valueOfModel(String model) {
-            for (ModelEnum itemEnum : ModelEnum.values()) {
-                if (itemEnum.getModel().equals(model)) {
-                    return itemEnum;
-                }
-            }
-            throw new IllegalArgumentException("Invalid MessageType value: " + model);
-        }
     }
 
     /**
-     * Midjourney - 提交返回的状态码
+     * 提交返回的状态码的枚举
      */
     @Getter
     @AllArgsConstructor
@@ -239,64 +232,68 @@ public class MidjourneyApi {
 
         private final String code;
         private final String name;
+
     }
 
     /**
-     * Midjourney - action
+     * Action 枚举
      */
     @Getter
     @AllArgsConstructor
     public enum TaskActionEnum {
+
         /**
-         * 生成图片.
+         * 生成图片
          */
         IMAGINE,
         /**
-         * 选中放大.
+         * 选中放大
          */
         UPSCALE,
         /**
-         * 选中其中的一张图,生成四张相似的.
+         * 选中其中的一张图,生成四张相似的
          */
         VARIATION,
         /**
-         * 重新执行.
+         * 重新执行
          */
         REROLL,
         /**
-         * 图转prompt.
+         * 图转 prompt
          */
         DESCRIBE,
         /**
-         * 多图混合.
+         * 多图混合
          */
         BLEND
+
     }
 
     /**
-     * Midjourney - 任务状态
+     * 任务状态枚举
      */
     @Getter
     @AllArgsConstructor
     public enum TaskStatusEnum {
+
         /**
-         * 未启动.
+         * 未启动
          */
         NOT_START(0),
         /**
-         * 已提交.
+         * 已提交
          */
         SUBMITTED(1),
         /**
-         * 执行中.
+         * 执行中
          */
         IN_PROGRESS(3),
         /**
-         * 失败.
+         * 失败
          */
         FAILURE(4),
         /**
-         * 成功.
+         * 成功
          */
         SUCCESS(4);
 

+ 4 - 6
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/package-info.java

@@ -2,11 +2,9 @@
  * model 包,接入各种大模型,对标 https://github.com/spring-projects/spring-ai/tree/main/models
  *
  * 1. yiyan 包:【百度】文心一言
- * 2. TODO 芋艿:
- * tongyi 包:【阿里】通义千问,对标 spring-cloud-alibaba 提供的 ai 包
- *  2.2
- *  2.3 xinghuo 包:【讯飞】星火,自己实现
- *  2.4 openai 包:【OpenAI】ChatGPT,拷贝 spring-ai 提供的 models/openai 包
- *  2.5 midjourney 包:Midjourney,参考 https://github.com/novicezk/midjourney-proxy 实现
+ * 2. tongyi 包:【阿里】通义千问,对标 spring-cloud-alibaba 提供的 ai 包 TODO 芋艿:未来直接使用它
+ * 3. xinghuo 包:【讯飞】星火,自己实现
+ * 4. midjourney 包:Midjourney,接入 https://github.com/novicezk/midjourney-proxy 实现
+ * 5. suno 包:TODO 芋艿:
  */
 package cn.iocoder.yudao.framework.ai.core.model;

+ 0 - 7
yudao-server/src/main/resources/application-local.yaml

@@ -76,13 +76,6 @@ server:
       enabled: true
       charset: UTF-8
       force: true
-# ai TODO @fan:这个融合到 yudao.ai 那好点哈
-ai:
-  midjourney-proxy:
-    enable: true
-    url: https://api.holdai.top/mj
-    notifyUrl: http://61d61685.r21.cpolar.top/admin-api/ai/image/midjourney-notify
-    key: sk-c3qxUCVKsPfdQiYU8440E3Fc8dE5424d9cB124A4Ee2489E3
 
 --- #################### 定时任务相关配置 ####################
 

+ 7 - 0
yudao-server/src/main/resources/application.yaml

@@ -201,6 +201,13 @@ yudao.ai:
     enable: true
     base-url: https://suno-imrqwwui8-status2xxs-projects.vercel.app
 
+ai:
+  midjourney-proxy:
+    enable: true
+    url: https://api.holdai.top/mj
+    notifyUrl: http://61d61685.r21.cpolar.top/admin-api/ai/image/midjourney-notify
+    key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf
+
 --- #################### 芋道相关配置 ####################
 
 yudao: