diff --git a/.image/common/ai-feature.png b/.image/common/ai-feature.png
new file mode 100644
index 000000000..b4a55f547
Binary files /dev/null and b/.image/common/ai-feature.png differ
diff --git a/.image/common/ai-preview.gif b/.image/common/ai-preview.gif
new file mode 100644
index 000000000..5f13ac4ff
Binary files /dev/null and b/.image/common/ai-preview.gif differ
diff --git a/README.md b/README.md
index bab303e2f..070cb211c 100644
--- a/README.md
+++ b/README.md
@@ -220,12 +220,12 @@
### 商城系统
+演示地址:
+
![功能图](/.image/common/mall-feature.png)
![功能图](/.image/common/mall-preview.png)
-演示地址:
-
### 会员中心
| | 功能 | 描述 |
@@ -238,15 +238,23 @@
### ERP 系统
-![功能图](/.image/common/erp-feature.png)
-
演示地址:
-### ERP 系统
+![功能图](/.image/common/erp-feature.png)
+
+### CRM 系统
+
+演示地址:
![功能图](/.image/common/crm-feature.png)
-演示地址:
+### AI 大模型
+
+演示地址:
+
+![功能图](/.image/common/ai-feature.png)
+
+![功能图](/.image/common/ai-preview.gif)
## 🐨 技术栈
diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java
index 4c4f72585..6c01b9497 100644
--- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java
+++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java
@@ -42,7 +42,9 @@ public class BannerApplicationRunner implements ApplicationRunner {
// 微信公众号
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
// 支付平台
- System.out.println("[支付系统 yudao-module-pay - 教程][参考 https://doc.iocoder.cn/pay/build/ 开启]");
+ System.out.println("[支付系统 yudao-module-pay - 教程][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
+ // AI 大模型
+ System.out.println("[AI 大模型 yudao-module-ai - 教程][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
});
}
diff --git a/yudao-gateway/src/main/resources/application.yaml b/yudao-gateway/src/main/resources/application.yaml
index d20d17176..c400791a8 100644
--- a/yudao-gateway/src/main/resources/application.yaml
+++ b/yudao-gateway/src/main/resources/application.yaml
@@ -152,6 +152,13 @@ spring:
- Path=/admin-api/crm/**
filters:
- RewritePath=/admin-api/crm/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
+ ## ai-server 服务
+ - id: ai-admin-api # 路由的编号
+ uri: grayLb://ai-server
+ predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
+ - Path=/admin-api/ai/**
+ filters:
+ - RewritePath=/admin-api/ai/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
@@ -196,6 +203,9 @@ knife4j:
- name: crm-server
service-name: crm-server
url: /admin-api/crm/v3/api-docs
+ - name: ai-server
+ service-name: crm-server
+ url: /admin-api/crm/v3/api-docs
--- #################### 芋道相关配置 ####################
diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml
new file mode 100644
index 000000000..1631f4b48
--- /dev/null
+++ b/yudao-module-ai/pom.xml
@@ -0,0 +1,27 @@
+
+
+
+ cn.iocoder.cloud
+ yudao
+ ${revision}
+
+ 4.0.0
+
+ yudao-module-ai-api
+ yudao-module-ai-biz
+ yudao-spring-boot-starter-ai
+
+ pom
+ yudao-module-ai
+
+ ${project.artifactId}
+
+ ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
+ 目前已接入各种模型,不限于:
+ 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
+ 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
+
+
+
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-api/pom.xml b/yudao-module-ai/yudao-module-ai-api/pom.xml
new file mode 100644
index 000000000..09d27de6a
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+ cn.iocoder.cloud
+ yudao-module-ai
+ ${revision}
+
+ 4.0.0
+ yudao-module-ai-api
+ jar
+
+ ${project.artifactId}
+
+ ai 模块 API,暴露给其它模块调用
+
+
+
+
+ cn.iocoder.cloud
+ yudao-common
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ true
+
+
+
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/package-info.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/package-info.java
new file mode 100644
index 000000000..4f94f23f8
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 占位,没有特别的作用
+ */
+package cn.iocoder.yudao.module.ai.api;
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java
new file mode 100644
index 000000000..19cbc8f8f
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java
@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.ai.enums;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * AI 内置聊天角色的枚举
+ *
+ * @author xiaoxin
+ */
+@AllArgsConstructor
+@Getter
+public enum AiChatRoleEnum implements IntArrayValuable {
+
+ AI_WRITE_ROLE(1, "写作助手", """
+ 你是一位出色的写作助手,能够帮助用户生成创意和灵感,并在用户提供场景和提示词时生成对应的回复。你的任务包括:
+ 1. 撰写建议:根据用户提供的主题或问题,提供详细的写作建议、情节发展方向、角色设定以及背景描写,确保内容结构清晰、有逻辑。
+ 2. 回复生成:根据用户提供的场景和提示词,生成合适的对话或文字回复,确保语气和风格符合场景需求。
+ 除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。
+ """),
+
+ AI_MIND_MAP_ROLE(2, "脑图助手", """
+ 你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
+ # Geek-AI 助手
+ ## 完整的开源系统
+ ### 前端开源
+ ### 后端开源
+ ## 支持各种大模型
+ ### OpenAI
+ ### Azure
+ ### 文心一言
+ ### 通义千问
+ ## 集成多种收费方式
+ ### 支付宝
+ ### 微信
+ 除此之外不要任何解释性语句。
+ """);
+
+ // TODO @xin:这个 role 是不是删除掉好点哈。= = 目前主要是没做角色枚举。这里多了 role 反倒容易误解哈
+ /**
+ * 角色
+ */
+ private final Integer role;
+ /**
+ * 角色名
+ */
+ private final String name;
+
+ /**
+ * 角色设定
+ */
+ private final String systemMessage;
+
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiChatRoleEnum::getRole).toArray();
+
+ @Override
+ public int[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java
new file mode 100644
index 000000000..73782a2cb
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.ai.enums;
+
+/**
+ * AI 字典类型的枚举类
+ *
+ * @author xiaoxin
+ */
+public interface DictTypeConstants {
+
+ // ========== AI Write ==========
+ String AI_WRITE_FORMAT = "ai_write_format"; // 写作格式
+ String AI_WRITE_LENGTH = "ai_write_length"; // 写作长度
+ String AI_WRITE_LANGUAGE = "ai_write_language"; // 写作语言
+ String AI_WRITE_TONE = "ai_write_tone"; // 写作语气
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
new file mode 100644
index 000000000..ddfb489f3
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.module.ai.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * AI 错误码枚举类
+ *
+ * ai 系统,使用 1-040-000-000 段
+ */
+public interface ErrorCodeConstants {
+
+ // ========== API 密钥 1-040-000-000 ==========
+ ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在");
+ ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!");
+ ErrorCode API_KEY_MIDJOURNEY_NOT_FOUND = new ErrorCode(1_040_000_900, "Midjourney 模型不存在");
+ ErrorCode API_KEY_SUNO_NOT_FOUND = new ErrorCode(1_040_000_901, "Suno 模型不存在");
+ ErrorCode API_KEY_IMAGE_NODE_FOUND = new ErrorCode(1_040_000_902, "平台({}) 图片模型未配置");
+
+ // ========== API 聊天模型 1-040-001-000 ==========
+ ErrorCode CHAT_MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!");
+ ErrorCode CHAT_MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!");
+ ErrorCode CHAT_MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认聊天模型");
+
+ // ========== 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, "聊天角色({})已禁用!");
+
+ // ========== 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, "操作失败,该聊天模型的配置不完整");
+
+ // ========== API 聊天消息 1-040-004-000 ==========
+
+ ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!");
+ ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "对话生成异常!");
+
+ // ========== API 绘画 1-040-005-000 ==========
+
+ ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!");
+ ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}");
+ ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}");
+ ErrorCode IMAGE_FAIL = new ErrorCode(1_022_005_002, "图片绘画失败! {}");
+
+ // ========== API 音乐 1-040-006-000 ==========
+ ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
+
+
+ // ========== API 写作 1-022-007-000 ==========
+ ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!");
+ ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!");
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java
new file mode 100644
index 000000000..cf8076150
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.ai.enums.image;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * AI 绘画状态的枚举
+ *
+ * @author fansili
+ */
+@AllArgsConstructor
+@Getter
+public enum AiImageStatusEnum {
+
+ IN_PROGRESS(10, "进行中"),
+ SUCCESS(20, "已完成"),
+ FAIL(30, "已失败");
+
+ /**
+ * 状态
+ */
+ private final Integer status;
+ /**
+ * 状态名
+ */
+ private final String name;
+
+ public static AiImageStatusEnum valueOfStatus(Integer status) {
+ for (AiImageStatusEnum statusEnum : AiImageStatusEnum.values()) {
+ if (statusEnum.getStatus().equals(status)) {
+ return statusEnum;
+ }
+ }
+ throw new IllegalArgumentException("未知会话状态: " + status);
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java
new file mode 100644
index 000000000..651731b60
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.ai.enums.music;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * AI 音乐生成模式的枚举
+ *
+ * @author xiaoxin
+ */
+@AllArgsConstructor
+@Getter
+public enum AiMusicGenerateModeEnum implements IntArrayValuable {
+
+ DESCRIPTION(1, "描述模式"),
+ LYRIC(2, "歌词模式");
+
+ /**
+ * 模式
+ */
+ private final Integer mode;
+ /**
+ * 模式名
+ */
+ private final String name;
+
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicGenerateModeEnum::getMode).toArray();
+
+ @Override
+ public int[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
new file mode 100644
index 000000000..f1298cf56
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.ai.enums.music;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * AI 音乐状态的枚举
+ *
+ * @author xiaoxin
+ */
+@AllArgsConstructor
+@Getter
+public enum AiMusicStatusEnum implements IntArrayValuable {
+
+ IN_PROGRESS(10, "进行中"),
+ SUCCESS(20, "已完成"),
+ FAIL(30, "已失败");
+
+ /**
+ * 状态
+ */
+ private final Integer status;
+
+ /**
+ * 状态名
+ */
+ private final String name;
+
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicStatusEnum::getStatus).toArray();
+
+ @Override
+ public int[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java
new file mode 100644
index 000000000..49d825be8
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.ai.enums.write;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * AI 写作类型的枚举
+ *
+ * @author xiaoxin
+ */
+@AllArgsConstructor
+@Getter
+public enum AiWriteTypeEnum implements IntArrayValuable {
+
+ WRITING(1, "撰写", "请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。"),
+ REPLY(2, "回复", "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。不需要除了正文内容外的其他回复,如标题、开头、额外的解释或道歉。");
+
+ /**
+ * 类型
+ */
+ private final Integer type;
+ /**
+ * 类型名
+ */
+ private final String name;
+
+ /**
+ * 模版
+ */
+ private final String prompt;
+
+ public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteTypeEnum::getType).toArray();
+
+ @Override
+ public int[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/pom.xml b/yudao-module-ai/yudao-module-ai-biz/pom.xml
new file mode 100644
index 000000000..83c6764b1
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/pom.xml
@@ -0,0 +1,95 @@
+
+
+
+ cn.iocoder.cloud
+ yudao-module-ai
+ ${revision}
+
+ 4.0.0
+ yudao-module-ai-biz
+
+ ${project.artifactId}
+
+ ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
+ 目前已接入各种模型,不限于:
+ 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
+ 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-bootstrap
+
+
+
+
+ cn.iocoder.cloud
+ yudao-module-ai-api
+ ${revision}
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-ai
+ ${revision}
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-biz-tenant
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-security
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-mybatis
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-rpc
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-job
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-test
+
+
+
+
+ cn.iocoder.cloud
+ yudao-spring-boot-starter-monitor
+
+
+
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/AiServerApplication.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/AiServerApplication.java
new file mode 100644
index 000000000..2679dcb52
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/AiServerApplication.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.ai;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * 项目的启动类
+ *
+ * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ *
+ * @author 芋道源码
+ */
+@SpringBootApplication
+public class AiServerApplication {
+
+ public static void main(String[] args) {
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+
+ SpringApplication.run(AiServerApplication.class, args);
+
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java
new file mode 100644
index 000000000..5142cde44
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java
@@ -0,0 +1,114 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.chat.vo.conversation.AiChatConversationCreateMyReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
+import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
+import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - AI 聊天对话")
+@RestController
+@RequestMapping("/ai/chat/conversation")
+@Validated
+public class AiChatConversationController {
+
+ @Resource
+ private AiChatConversationService chatConversationService;
+ @Resource
+ private AiChatMessageService chatMessageService;
+
+ @PostMapping("/create-my")
+ @Operation(summary = "创建【我的】聊天对话")
+ public CommonResult createChatConversationMy(@RequestBody @Valid AiChatConversationCreateMyReqVO createReqVO) {
+ return success(chatConversationService.createChatConversationMy(createReqVO, getLoginUserId()));
+ }
+
+ @PutMapping("/update-my")
+ @Operation(summary = "更新【我的】聊天对话")
+ public CommonResult updateChatConversationMy(@RequestBody @Valid AiChatConversationUpdateMyReqVO updateReqVO) {
+ chatConversationService.updateChatConversationMy(updateReqVO, getLoginUserId());
+ return success(true);
+ }
+
+ @GetMapping("/my-list")
+ @Operation(summary = "获得【我的】聊天对话列表")
+ public CommonResult> getChatConversationMyList() {
+ List list = chatConversationService.getChatConversationListByUserId(getLoginUserId());
+ return success(BeanUtils.toBean(list, AiChatConversationRespVO.class));
+ }
+
+ @GetMapping("/get-my")
+ @Operation(summary = "获得【我的】聊天对话")
+ @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
+ public CommonResult getChatConversationMy(@RequestParam("id") Long id) {
+ AiChatConversationDO conversation = chatConversationService.getChatConversation(id);
+ if (conversation != null && ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) {
+ conversation = null;
+ }
+ return success(BeanUtils.toBean(conversation, AiChatConversationRespVO.class));
+ }
+
+ @DeleteMapping("/delete-my")
+ @Operation(summary = "删除聊天对话")
+ @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
+ public CommonResult deleteChatConversationMy(@RequestParam("id") Long id) {
+ chatConversationService.deleteChatConversationMy(id, getLoginUserId());
+ return success(true);
+ }
+
+ @DeleteMapping("/delete-by-unpinned")
+ @Operation(summary = "删除未置顶的聊天对话")
+ public CommonResult deleteChatConversationMyByUnpinned() {
+ chatConversationService.deleteChatConversationMyByUnpinned(getLoginUserId());
+ return success(true);
+ }
+
+ // ========== 对话管理 ==========
+
+ @GetMapping("/page")
+ @Operation(summary = "获得对话分页", description = "用于【对话管理】菜单")
+ @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')")
+ public CommonResult> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) {
+ PageResult pageResult = chatConversationService.getChatConversationPage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty());
+ }
+ // 拼接关联数据
+ Map messageCountMap = chatMessageService.getChatMessageCountMap(
+ convertList(pageResult.getList(), AiChatConversationDO::getId));
+ return success(BeanUtils.toBean(pageResult, AiChatConversationRespVO.class,
+ conversation -> conversation.setMessageCount(messageCountMap.getOrDefault(conversation.getId(), 0))));
+ }
+
+ @Operation(summary = "管理员删除对话")
+ @DeleteMapping("/delete-by-admin")
+ @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
+ @PreAuthorize("@ss.hasPermission('ai:chat-conversation:delete')")
+ public CommonResult deleteChatConversationByAdmin(@RequestParam("id") Long id) {
+ chatConversationService.deleteChatConversationByAdmin(id);
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
new file mode 100644
index 000000000..e75e0d333
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
@@ -0,0 +1,29 @@
+### 发送消息(段式)
+POST {{baseUrl}}/ai/chat/message/send
+Content-Type: application/json
+Authorization: {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+ "conversationId": "1781604279872581724",
+ "content": "你是 OpenAI 么?"
+}
+
+### 发送消息(流式)
+POST {{baseUrl}}/ai/chat/message/send-stream
+Content-Type: application/json
+Authorization: {{token}}
+tenant-id: {{adminTenentId}}
+
+{
+ "conversationId": "1781604279872581724",
+ "content": "1+1=?"
+}
+
+### 获得指定对话的消息列表
+GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649
+Authorization: {{token}}
+
+### 删除消息
+DELETE {{baseUrl}}/ai/chat/message/delete?id=50
+Authorization: {{token}}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
new file mode 100644
index 000000000..357dbec5e
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
@@ -0,0 +1,120 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
+import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
+import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService;
+import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.PermitAll;
+import jakarta.validation.Valid;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - 聊天消息")
+@RestController
+@RequestMapping("/ai/chat/message")
+@Slf4j
+public class AiChatMessageController {
+
+ @Resource
+ private AiChatMessageService chatMessageService;
+ @Resource
+ private AiChatConversationService chatConversationService;
+ @Resource
+ private AiChatRoleService chatRoleService;
+
+ @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢")
+ @PostMapping("/send")
+ public CommonResult sendMessage(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
+ return success(chatMessageService.sendMessage(sendReqVO, getLoginUserId()));
+ }
+
+ @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
+ @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
+ public Flux> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) {
+ return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId());
+ }
+
+ @Operation(summary = "获得指定对话的消息列表")
+ @GetMapping("/list-by-conversation-id")
+ @Parameter(name = "conversationId", required = true, description = "对话编号", example = "1024")
+ public CommonResult> getChatMessageListByConversationId(
+ @RequestParam("conversationId") Long conversationId) {
+ AiChatConversationDO conversation = chatConversationService.getChatConversation(conversationId);
+ if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) {
+ return success(Collections.emptyList());
+ }
+ List messageList = chatMessageService.getChatMessageListByConversationId(conversationId);
+ return success(BeanUtils.toBean(messageList, AiChatMessageRespVO.class));
+ }
+
+ @Operation(summary = "删除消息")
+ @DeleteMapping("/delete")
+ @Parameter(name = "id", required = true, description = "消息编号", example = "1024")
+ public CommonResult deleteChatMessage(@RequestParam("id") Long id) {
+ chatMessageService.deleteChatMessage(id, getLoginUserId());
+ return success(true);
+ }
+
+ @Operation(summary = "删除指定对话的消息")
+ @DeleteMapping("/delete-by-conversation-id")
+ @Parameter(name = "conversationId", required = true, description = "对话编号", example = "1024")
+ public CommonResult deleteChatMessageByConversationId(@RequestParam("conversationId") Long conversationId) {
+ chatMessageService.deleteChatMessageByConversationId(conversationId, getLoginUserId());
+ return success(true);
+ }
+
+ // ========== 对话管理 ==========
+
+ @GetMapping("/page")
+ @Operation(summary = "获得消息分页", description = "用于【对话管理】菜单")
+ @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')")
+ public CommonResult> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) {
+ PageResult pageResult = chatMessageService.getChatMessagePage(pageReqVO);
+ if (CollUtil.isEmpty(pageResult.getList())) {
+ return success(PageResult.empty());
+ }
+ // 拼接数据
+ Map roleMap = chatRoleService.getChatRoleMap(
+ convertSet(pageResult.getList(), AiChatMessageDO::getRoleId));
+ return success(BeanUtils.toBean(pageResult, AiChatMessageRespVO.class,
+ respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(), role -> respVO.setRoleName(role.getName()))));
+ }
+
+ @Operation(summary = "管理员删除消息")
+ @DeleteMapping("/delete-by-admin")
+ @Parameter(name = "id", required = true, description = "消息编号", example = "1024")
+ @PreAuthorize("@ss.hasPermission('ai:chat-message:delete')")
+ public CommonResult deleteChatMessageByAdmin(@RequestParam("id") Long id) {
+ chatMessageService.deleteChatMessageByAdmin(id);
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java
new file mode 100644
index 000000000..c13200b6a
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java
@@ -0,0 +1,13 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 聊天对话创建【我的】 Request VO")
+@Data
+public class AiChatConversationCreateMyReqVO {
+
+ @Schema(description = "聊天角色编号", example = "666")
+ private Long roleId;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java
new file mode 100644
index 000000000..967e866ea
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java
@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI 聊天对话的分页 Request VO")
+@Data
+public class AiChatConversationPageReqVO extends PageParam {
+
+ @Schema(description = "用户编号", example = "1024")
+ private Long userId;
+
+ @Schema(description = "对话标题", example = "你好")
+ private String title;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java
new file mode 100644
index 000000000..66eb24db5
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java
@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation;
+
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
+import com.fhs.core.trans.anno.Trans;
+import com.fhs.core.trans.constant.TransType;
+import com.fhs.core.trans.vo.VO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - AI 聊天对话 Response VO")
+@Data
+public class AiChatConversationRespVO implements VO {
+
+ @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long userId;
+
+ @Schema(description = "对话标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是一个标题")
+ private String title;
+
+ @Schema(description = "是否置顶", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ private Boolean pinned;
+
+ @Schema(description = "角色编号", example = "1")
+ @Trans(type = TransType.SIMPLE, target = AiChatRoleDO.class, fields = {"name", "avatar"}, refs = {"roleName", "roleAvatar"})
+ private Long roleId;
+
+ @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = "name", ref = "modelName")
+ private Long modelId;
+
+ @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "ERNIE-Bot-turbo-0922")
+ private String model;
+
+ @Schema(description = "模型名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+ private String modelName;
+
+ @Schema(description = "角色设定", example = "一个快乐的程序员")
+ private String systemMessage;
+
+ @Schema(description = "温度参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.8")
+ private Double temperature;
+
+ @Schema(description = "单条回复的最大 Token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
+ private Integer maxTokens;
+
+ @Schema(description = "上下文的最大 Message 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+ private Integer maxContexts;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+ // ========== 关联 role 信息 ==========
+
+ @Schema(description = "角色头像", example = "https://www.iocoder.cn/1.png")
+ private String roleAvatar;
+
+ @Schema(description = "角色名字", example = "小黄")
+ private String roleName;
+
+ // ========== 仅在【对话管理】时加载 ==========
+
+ @Schema(description = "消息数量", example = "20")
+ private Integer messageCount;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java
new file mode 100644
index 000000000..f9ce64bae
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 聊天对话更新【我的】 Request VO")
+@Data
+public class AiChatConversationUpdateMyReqVO {
+
+ @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "对话编号不能为空")
+ private Long id;
+
+ @Schema(description = "对话标题", example = "我是一个标题")
+ private String title;
+
+ @Schema(description = "是否置顶", example = "true")
+ private Boolean pinned;
+
+ @Schema(description = "模型编号", example = "1")
+ private Long modelId;
+
+ @Schema(description = "角色设定", example = "一个快乐的程序员")
+ private String systemMessage;
+
+ @Schema(description = "温度参数", example = "0.8")
+ private Double temperature;
+
+ @Schema(description = "单条回复的最大 Token 数量", example = "4096")
+ private Integer maxTokens;
+
+ @Schema(description = "上下文的最大 Message 数量", example = "10")
+ private Integer maxContexts;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java
new file mode 100644
index 000000000..7ccb6aa0b
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI 聊天消息的分页 Request VO")
+@Data
+public class AiChatMessagePageReqVO extends PageParam {
+
+ @Schema(description = "对话编号", example = "2048")
+ private Long conversationId;
+
+ @Schema(description = "用户编号", example = "1024")
+ private Long userId;
+
+ @Schema(description = "消息内容", example = "你好")
+ private String content;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
new file mode 100644
index 000000000..9b358df6f
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - AI 聊天消息 Response VO")
+@Data
+public class AiChatMessageRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
+ private Long conversationId;
+
+ @Schema(description = "回复消息编号", example = "1024")
+ private Long replyId;
+
+ @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "role")
+ private String type; // 参见 MessageType 枚举类
+
+ @Schema(description = "用户编号", example = "4096")
+ private Long userId;
+
+ @Schema(description = "角色编号", example = "888")
+ private Long roleId;
+
+ @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo")
+ private String model;
+
+ @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "123")
+ private Long modelId;
+
+ @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
+ private String content;
+
+ @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ private Boolean useContext;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51")
+ private LocalDateTime createTime;
+
+ // ========== 仅在【对话管理】时加载 ==========
+
+ @Schema(description = "角色名字", example = "小黄")
+ private String roleName;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
new file mode 100644
index 000000000..89a84bcbd
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Schema(description = "管理后台 - AI 聊天消息发送 Request VO")
+@Data
+public class AiChatMessageSendReqVO {
+
+ @Schema(description = "聊天对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @NotNull(message = "聊天对话编号不能为空")
+ private Long conversationId;
+
+ @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "帮我写个 Java 算法")
+ @NotEmpty(message = "聊天内容不能为空")
+ private String content;
+
+ @Schema(description = "是否携带上下文", example = "true")
+ private Boolean useContext;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
new file mode 100644
index 000000000..58ba05659
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - AI 聊天消息发送 Response VO")
+@Data
+public class AiChatMessageSendRespVO {
+
+ @Schema(description = "发送消息", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Message send;
+
+ @Schema(description = "接收消息", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Message receive;
+
+ @Schema(description = "消息")
+ @Data
+ public static class Message {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "role")
+ private String type; // 参见 MessageType 枚举类
+
+ @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
+ private String content;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http
new file mode 100644
index 000000000..9047610c0
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http
@@ -0,0 +1,42 @@
+### 生成图片:OpenAI(DALL)
+POST {{baseUrl}}/ai/image/draw
+Content-Type: application/json
+Authorization: {{token}}
+
+{
+ "platform": "OpenAI",
+ "prompt": "可爱的小喵星人",
+ "model": "dall-e-3",
+ "height": "1024",
+ "width": "1024",
+ "options": {
+ "style": "vivid"
+ }
+}
+
+### 生成图片:StableDiffusion
+POST {{baseUrl}}/ai/image/draw
+Content-Type: application/json
+Authorization: {{token}}
+
+{
+ "platform": "StableDiffusion",
+ "prompt": "中国长城",
+ "model": "stable-diffusion-v1-6",
+ "height": "1024",
+ "width": "1024",
+ "style": "vivid"
+}
+
+### 生成图片:生成图片(Midjourney)
+POST {{baseUrl}}/ai/image/midjourney/imagine
+Content-Type: application/json
+Authorization: {{token}}
+
+{
+ "prompt": "中国旗袍",
+ "model": "midjourney",
+ "width": "1",
+ "height": "1",
+ "version": "6.0"
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
new file mode 100644
index 000000000..de12ee1e0
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
@@ -0,0 +1,134 @@
+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.AiImageDrawReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
+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;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.PermitAll;
+import jakarta.validation.Valid;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - AI 绘画")
+@RestController
+@RequestMapping("/ai/image")
+@Slf4j
+public class AiImageController {
+
+ @Resource
+ private AiImageService imageService;
+
+ @GetMapping("/my-page")
+ @Operation(summary = "获取【我的】绘图分页")
+ public CommonResult> getImagePageMy(@Validated PageParam pageReqVO) {
+ PageResult pageResult = imageService.getImagePageMy(getLoginUserId(), pageReqVO);
+ return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
+ }
+
+ @GetMapping("/get-my")
+ @Operation(summary = "获取【我的】绘图记录")
+ @Parameter(name = "id", required = true, description = "绘画编号", example = "1024")
+ public CommonResult getImageMy(@RequestParam("id") Long id) {
+ AiImageDO image = imageService.getImage(id);
+ if (image == null || ObjUtil.notEqual(getLoginUserId(), image.getUserId())) {
+ return success(null);
+ }
+ return success(BeanUtils.toBean(image, AiImageRespVO.class));
+ }
+
+ @GetMapping("/my-list-by-ids")
+ @Operation(summary = "获取【我的】绘图记录列表")
+ @Parameter(name = "ids", required = true, description = "绘画编号数组", example = "1024,2048")
+ public CommonResult> getImageListMyByIds(@RequestParam("ids") List ids) {
+ List imageList = imageService.getImageList(ids);
+ imageList.removeIf(item -> !ObjUtil.equal(getLoginUserId(), item.getUserId()));
+ return success(BeanUtils.toBean(imageList, AiImageRespVO.class));
+ }
+
+ @Operation(summary = "生成图片")
+ @PostMapping("/draw")
+ public CommonResult drawImage(@Valid @RequestBody AiImageDrawReqVO drawReqVO) {
+ return success(imageService.drawImage(getLoginUserId(), drawReqVO));
+ }
+
+ @Operation(summary = "删除【我的】绘画记录")
+ @DeleteMapping("/delete-my")
+ @Parameter(name = "id", required = true, description = "绘画编号", example = "1024")
+ public CommonResult deleteImageMy(@RequestParam("id") Long id) {
+ imageService.deleteImageMy(id, getLoginUserId());
+ return success(true);
+ }
+
+ // ================ midjourney 专属 ================
+
+ @Operation(summary = "【Midjourney】生成图片")
+ @PostMapping("/midjourney/imagine")
+ public CommonResult midjourneyImagine(@Valid @RequestBody AiMidjourneyImagineReqVO reqVO) {
+ Long imageId = imageService.midjourneyImagine(getLoginUserId(), reqVO);
+ return success(imageId);
+ }
+
+ @Operation(summary = "【Midjourney】通知图片进展", description = "由 Midjourney Proxy 回调")
+ @PostMapping("/midjourney/notify") // 必须是 POST 方法,否则会报错
+ @PermitAll
+ public CommonResult midjourneyNotify(@Valid @RequestBody MidjourneyApi.Notify notify) {
+ imageService.midjourneyNotify(notify);
+ return success(true);
+ }
+
+ @Operation(summary = "【Midjourney】Action 操作(二次生成图片)", description = "例如说:放大、缩小、U1、U2 等")
+ @PostMapping("/midjourney/action")
+ public CommonResult midjourneyAction(@Valid @RequestBody AiMidjourneyActionReqVO reqVO) {
+ Long imageId = imageService.midjourneyAction(getLoginUserId(), reqVO);
+ return success(imageId);
+ }
+
+ // ================ 绘图管理 ================
+
+ @GetMapping("/page")
+ @Operation(summary = "获得绘画分页")
+ @PreAuthorize("@ss.hasPermission('ai:image:query')")
+ public CommonResult> getImagePage(@Valid AiImagePageReqVO pageReqVO) {
+ PageResult pageResult = imageService.getImagePage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新绘画")
+ @PreAuthorize("@ss.hasPermission('ai:image:update')")
+ public CommonResult updateImage(@Valid @RequestBody AiImageUpdateReqVO updateReqVO) {
+ imageService.updateImage(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除绘画")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('ai:image:delete')")
+ public CommonResult deleteImage(@RequestParam("id") Long id) {
+ imageService.deleteImage(id);
+ return success(true);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java
new file mode 100644
index 000000000..a38935ef7
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springframework.ai.openai.OpenAiImageOptions;
+import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
+
+import java.util.Map;
+
+@Schema(description = "管理后台 - AI 绘画 Request VO")
+@Data
+public class AiImageDrawReqVO {
+
+ @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
+ private String platform; // 参见 AiPlatformEnum 枚举
+
+ @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "画一个长城")
+ @NotEmpty(message = "提示词不能为空")
+ @Size(max = 1200, message = "提示词最大 1200")
+ private String prompt;
+
+ @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "stable-diffusion-v1-6")
+ @NotEmpty(message = "模型不能为空")
+ private String model;
+
+ /**
+ * 1. dall-e-2 模型:256x256、512x512、1024x1024
+ * 2. dall-e-3 模型:1024x1024, 1792x1024, 或 1024x1792
+ */
+ @Schema(description = "图片高度")
+ @NotNull(message = "图片高度不能为空")
+ private Integer height;
+
+ @Schema(description = "图片宽度")
+ @NotNull(message = "图片宽度不能为空")
+ private Integer width;
+
+ // ========== 各平台绘画的拓展参数 ==========
+
+ /**
+ * 绘制参数,不同 platform 的不同参数
+ *
+ * 1. {@link OpenAiImageOptions}
+ * 2. {@link StabilityAiImageOptions}
+ */
+ @Schema(description = "绘制参数")
+ private Map options;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java
new file mode 100644
index 000000000..bdf329c61
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI 绘画分页 Request VO")
+@Data
+public class AiImagePageReqVO extends PageParam {
+
+ @Schema(description = "用户编号", example = "28987")
+ private Long userId;
+
+ @Schema(description = "平台", example = "OpenAI")
+ private String platform;
+
+ @Schema(description = "绘画状态", example = "1")
+ private Integer status;
+
+ @Schema(description = "是否发布", example = "1")
+ private Boolean publicStatus;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java
new file mode 100644
index 000000000..f73d05aaa
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java
@@ -0,0 +1,60 @@
+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;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Schema(description = "管理后台 - AI 绘画 Response VO")
+@Data
+public class AiImageRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long id;
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Long userId;
+
+ @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
+ private String platform; // 参见 AiPlatformEnum 枚举
+
+ @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "stable-diffusion-v1-6")
+ private String model;
+
+ @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "南极的小企鹅")
+ private String prompt;
+
+ @Schema(description = "图片宽度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Integer width;
+
+ @Schema(description = "图片高度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Integer height;
+
+ @Schema(description = "绘画状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+ private Integer status;
+
+ @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "public")
+ private Boolean publicStatus;
+
+ @Schema(description = "图片地址", example = "https://www.iocoder.cn/1.png")
+ private String picUrl;
+
+ @Schema(description = "绘画错误信息", example = "图片错误信息")
+ private String errorMessage;
+
+ @Schema(description = "绘制参数")
+ private Map options;
+
+ @Schema(description = "mj buttons 按钮")
+ private List buttons;
+
+ @Schema(description = "完成时间")
+ private LocalDateTime finishTime;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java
new file mode 100644
index 000000000..45df01015
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java
@@ -0,0 +1,18 @@
+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;
+
+@Schema(description = "管理后台 - AI 绘画修改 Request VO")
+@Data
+public class AiImageUpdateReqVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
+ @NotNull(message = "编号不能为空")
+ private Long id;
+
+ @Schema(description = "是否发布", example = "true")
+ private Boolean publicStatus;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java
new file mode 100644
index 000000000..28803a051
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java
@@ -0,0 +1,20 @@
+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;
+
+@Schema(description = "管理后台 - AI 绘图操作(Midjourney) Request VO")
+@Data
+public class AiMidjourneyActionReqVO {
+
+ @Schema(description = "图片编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "图片编号不能为空")
+ private Long id;
+
+ @Schema(description = "操作按钮编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "MJ::JOB::variation::4::06aa3e66-0e97-49cc-8201-e0295d883de4")
+ @NotEmpty(message = "操作按钮编号不能为空")
+ private String customId;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java
new file mode 100644
index 000000000..b90882639
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java
@@ -0,0 +1,35 @@
+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;
+
+@Schema(description = "管理后台 - AI 绘画生成(Midjourney) Request VO")
+@Data
+public class AiMidjourneyImagineReqVO {
+
+ @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;
+
+ @Schema(description = "参考图", example = "https://www.iocoder.cn/x.png")
+ private String referImageUrl;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
new file mode 100644
index 000000000..015180265
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.ai.controller.admin.mindmap;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
+import cn.iocoder.yudao.module.ai.service.mindmap.AiMindMapService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.PermitAll;
+import jakarta.validation.Valid;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - AI 思维导图")
+@RestController
+@RequestMapping("/ai/mind-map")
+public class AiMindMapController {
+
+ @Resource
+ private AiMindMapService mindMapService;
+
+ @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ @Operation(summary = "脑图生成(流式)", description = "流式返回,响应较快")
+ @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
+ public Flux> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) {
+ return mindMapService.generateMindMap(generateReqVO, getLoginUserId());
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java
new file mode 100644
index 000000000..08404bb0f
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 思维导图生成 Request VO")
+@Data
+public class AiMindMapGenerateReqVO {
+
+ @Schema(description = "思维导图内容提示", example = "Java 学习路线")
+ @NotBlank(message = "思维导图内容提示不能为空")
+ private String prompt;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java
new file mode 100644
index 000000000..2bc190051
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.model.vo.apikey.AiApiKeyPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelRespVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+@Tag(name = "管理后台 - AI API 密钥")
+@RestController
+@RequestMapping("/ai/api-key")
+@Validated
+public class AiApiKeyController {
+
+ @Resource
+ private AiApiKeyService apiKeyService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建 API 密钥")
+ @PreAuthorize("@ss.hasPermission('ai:api-key:create')")
+ public CommonResult createApiKey(@Valid @RequestBody AiApiKeySaveReqVO createReqVO) {
+ return success(apiKeyService.createApiKey(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新 API 密钥")
+ @PreAuthorize("@ss.hasPermission('ai:api-key:update')")
+ public CommonResult updateApiKey(@Valid @RequestBody AiApiKeySaveReqVO updateReqVO) {
+ apiKeyService.updateApiKey(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除 API 密钥")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('ai:api-key:delete')")
+ public CommonResult deleteApiKey(@RequestParam("id") Long id) {
+ apiKeyService.deleteApiKey(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得 API 密钥")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('ai:api-key:query')")
+ public CommonResult getApiKey(@RequestParam("id") Long id) {
+ AiApiKeyDO apiKey = apiKeyService.getApiKey(id);
+ return success(BeanUtils.toBean(apiKey, AiApiKeyRespVO.class));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得 API 密钥分页")
+ @PreAuthorize("@ss.hasPermission('ai:api-key:query')")
+ public CommonResult> getApiKeyPage(@Valid AiApiKeyPageReqVO pageReqVO) {
+ PageResult pageResult = apiKeyService.getApiKeyPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, AiApiKeyRespVO.class));
+ }
+
+ @GetMapping("/simple-list")
+ @Operation(summary = "获得 API 密钥分页列表")
+ public CommonResult> getApiKeySimpleList() {
+ List list = apiKeyService.getApiKeyList();
+ return success(convertList(list, key -> new AiChatModelRespVO().setId(key.getId()).setName(key.getName())));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java
new file mode 100644
index 000000000..08a53b286
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.model.vo.chatModel.AiChatModelPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+
+@Tag(name = "管理后台 - AI 聊天模型")
+@RestController
+@RequestMapping("/ai/chat-model")
+@Validated
+public class AiChatModelController {
+
+ @Resource
+ private AiChatModelService chatModelService;
+
+ @PostMapping("/create")
+ @Operation(summary = "创建聊天模型")
+ @PreAuthorize("@ss.hasPermission('ai:chat-model:create')")
+ public CommonResult createChatModel(@Valid @RequestBody AiChatModelSaveReqVO createReqVO) {
+ return success(chatModelService.createChatModel(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新聊天模型")
+ @PreAuthorize("@ss.hasPermission('ai:chat-model:update')")
+ public CommonResult updateChatModel(@Valid @RequestBody AiChatModelSaveReqVO updateReqVO) {
+ chatModelService.updateChatModel(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除聊天模型")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('ai:chat-model:delete')")
+ public CommonResult deleteChatModel(@RequestParam("id") Long id) {
+ chatModelService.deleteChatModel(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得聊天模型")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('ai:chat-model:query')")
+ public CommonResult getChatModel(@RequestParam("id") Long id) {
+ AiChatModelDO chatModel = chatModelService.getChatModel(id);
+ return success(BeanUtils.toBean(chatModel, AiChatModelRespVO.class));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得聊天模型分页")
+ @PreAuthorize("@ss.hasPermission('ai:chat-model:query')")
+ public CommonResult> getChatModelPage(@Valid AiChatModelPageReqVO pageReqVO) {
+ PageResult pageResult = chatModelService.getChatModelPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, AiChatModelRespVO.class));
+ }
+
+ @GetMapping("/simple-list")
+ @Operation(summary = "获得聊天模型列表")
+ @Parameter(name = "status", description = "状态", required = true, example = "1")
+ public CommonResult> getChatModelSimpleList(@RequestParam("status") Integer status) {
+ List list = chatModelService.getChatModelListByStatus(status);
+ return success(convertList(list, model -> new AiChatModelRespVO().setId(model.getId())
+ .setName(model.getName()).setModel(model.getModel())));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java
new file mode 100644
index 000000000..02f698b94
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java
@@ -0,0 +1,124 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.model.vo.chatRole.AiChatRolePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleRespVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveMyReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
+import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - AI 聊天角色")
+@RestController
+@RequestMapping("/ai/chat-role")
+@Validated
+public class AiChatRoleController {
+
+ @Resource
+ private AiChatRoleService chatRoleService;
+
+ @GetMapping("/my-page")
+ @Operation(summary = "获得【我的】聊天角色分页")
+ public CommonResult> getChatRoleMyPage(@Valid AiChatRolePageReqVO pageReqVO) {
+ PageResult pageResult = chatRoleService.getChatRoleMyPage(pageReqVO, getLoginUserId());
+ return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class));
+ }
+
+ @GetMapping("/get-my")
+ @Operation(summary = "获得【我的】聊天角色")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ public CommonResult getChatRoleMy(@RequestParam("id") Long id) {
+ AiChatRoleDO chatRole = chatRoleService.getChatRole(id);
+ if (ObjUtil.notEqual(chatRole.getUserId(), getLoginUserId())) {
+ return success(null);
+ }
+ return success(BeanUtils.toBean(chatRole, AiChatRoleRespVO.class));
+ }
+
+ @PostMapping("/create-my")
+ @Operation(summary = "创建【我的】聊天角色")
+ public CommonResult createChatRoleMy(@Valid @RequestBody AiChatRoleSaveMyReqVO createReqVO) {
+ return success(chatRoleService.createChatRoleMy(createReqVO, getLoginUserId()));
+ }
+
+ @PutMapping("/update-my")
+ @Operation(summary = "更新【我的】聊天角色")
+ public CommonResult updateChatRoleMy(@Valid @RequestBody AiChatRoleSaveMyReqVO updateReqVO) {
+ chatRoleService.updateChatRoleMy(updateReqVO, getLoginUserId());
+ return success(true);
+ }
+
+ @DeleteMapping("/delete-my")
+ @Operation(summary = "删除【我的】聊天角色")
+ @Parameter(name = "id", description = "编号", required = true)
+ public CommonResult deleteChatRoleMy(@RequestParam("id") Long id) {
+ chatRoleService.deleteChatRoleMy(id, getLoginUserId());
+ return success(true);
+ }
+
+ @GetMapping("/category-list")
+ @Operation(summary = "获得聊天角色的分类列表")
+ public CommonResult> getChatRoleCategoryList() {
+ return success(chatRoleService.getChatRoleCategoryList());
+ }
+
+ // ========== 角色管理 ==========
+
+ @PostMapping("/create")
+ @Operation(summary = "创建聊天角色")
+ @PreAuthorize("@ss.hasPermission('ai:chat-role:create')")
+ public CommonResult createChatRole(@Valid @RequestBody AiChatRoleSaveReqVO createReqVO) {
+ return success(chatRoleService.createChatRole(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新聊天角色")
+ @PreAuthorize("@ss.hasPermission('ai:chat-role:update')")
+ public CommonResult updateChatRole(@Valid @RequestBody AiChatRoleSaveReqVO updateReqVO) {
+ chatRoleService.updateChatRole(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除聊天角色")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('ai:chat-role:delete')")
+ public CommonResult deleteChatRole(@RequestParam("id") Long id) {
+ chatRoleService.deleteChatRole(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "获得聊天角色")
+ @Parameter(name = "id", description = "编号", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('ai:chat-role:query')")
+ public CommonResult getChatRole(@RequestParam("id") Long id) {
+ AiChatRoleDO chatRole = chatRoleService.getChatRole(id);
+ return success(BeanUtils.toBean(chatRole, AiChatRoleRespVO.class));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得聊天角色分页")
+ @PreAuthorize("@ss.hasPermission('ai:chat-role:query')")
+ public CommonResult> getChatRolePage(@Valid AiChatRolePageReqVO pageReqVO) {
+ PageResult pageResult = chatRoleService.getChatRolePage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class));
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java
new file mode 100644
index 000000000..063696244
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java
@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI API 密钥分页 Request VO")
+@Data
+public class AiApiKeyPageReqVO extends PageParam {
+
+ @Schema(description = "名称", example = "文心一言")
+ private String name;
+
+ @Schema(description = "平台", example = "OpenAI")
+ private String platform;
+
+ @Schema(description = "状态", example = "1")
+ private Integer status;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java
new file mode 100644
index 000000000..55d6d802b
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+@Schema(description = "管理后台 - AI API 密钥 Response VO")
+@Data
+public class AiApiKeyRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23538")
+ private Long id;
+
+ @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "文心一言")
+ private String name;
+
+ @Schema(description = "密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC")
+ private String apiKey;
+
+ @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
+ private String platform;
+
+ @Schema(description = "自定义 API 地址", example = "https://aip.baidubce.com")
+ private String url;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer status;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java
new file mode 100644
index 000000000..8fbc8fde7
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java
@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import jakarta.validation.constraints.*;
+
+@Schema(description = "管理后台 - AI API 密钥新增/修改 Request VO")
+@Data
+public class AiApiKeySaveReqVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23538")
+ private Long id;
+
+ @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "文心一言")
+ @NotEmpty(message = "名称不能为空")
+ private String name;
+
+ @Schema(description = "密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC")
+ @NotEmpty(message = "密钥不能为空")
+ private String apiKey;
+
+ @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
+ @NotEmpty(message = "平台不能为空")
+ private String platform;
+
+ @Schema(description = "自定义 API 地址", example = "https://aip.baidubce.com")
+ private String url;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "状态不能为空")
+ private Integer status;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java
new file mode 100644
index 000000000..ce2f83b4b
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel;
+
+import lombok.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+@Schema(description = "管理后台 - API 聊天模型分页 Request VO")
+@Data
+public class AiChatModelPageReqVO extends PageParam {
+
+ @Schema(description = "模型名字", example = "张三")
+ private String name;
+
+ @Schema(description = "模型标识", example = "gpt-3.5-turbo-0125")
+ private String model;
+
+ @Schema(description = "模型平台", example = "OpenAI")
+ private String platform;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java
new file mode 100644
index 000000000..681dabe68
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - AI 聊天模型 Response VO")
+@Data
+public class AiChatModelRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2630")
+ private Long id;
+
+ @Schema(description = "API 秘钥编号", example = "22042")
+ private Long keyId;
+
+ @Schema(description = "模型名字", example = "张三")
+ private String name;
+
+ @Schema(description = "模型标识", example = "gpt-3.5-turbo-0125")
+ private String model;
+
+ @Schema(description = "模型平台", example = "OpenAI")
+ private String platform;
+
+ @Schema(description = "排序", example = "1")
+ private Integer sort;
+
+ @Schema(description = "状态", example = "2")
+ private Integer status;
+
+ @Schema(description = "温度参数", example = "1")
+ private Double temperature;
+
+ @Schema(description = "单条回复的最大 Token 数量", example = "4096")
+ private Integer maxTokens;
+
+ @Schema(description = "上下文的最大 Message 数量", example = "8192")
+ private Integer maxContexts;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java
new file mode 100644
index 000000000..4fad5a1fc
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java
@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import jakarta.validation.constraints.*;
+
+@Schema(description = "管理后台 - API 聊天模型新增/修改 Request VO")
+@Data
+public class AiChatModelSaveReqVO {
+
+ @Schema(description = "编号", example = "2630")
+ private Long id;
+
+ @Schema(description = "API 秘钥编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22042")
+ @NotNull(message = "API 秘钥编号不能为空")
+ private Long keyId;
+
+ @Schema(description = "模型名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+ @NotEmpty(message = "模型名字不能为空")
+ private String name;
+
+ @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo-0125")
+ @NotEmpty(message = "模型标识不能为空")
+ private String model;
+
+ @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
+ @NotEmpty(message = "模型平台不能为空")
+ private String platform;
+
+ @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "排序不能为空")
+ private Integer sort;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @InEnum(CommonStatusEnum.class)
+ @NotNull(message = "状态不能为空")
+ private Integer status;
+
+ @Schema(description = "温度参数", example = "1")
+ private Double temperature;
+
+ @Schema(description = "单条回复的最大 Token 数量", example = "4096")
+ private Integer maxTokens;
+
+ @Schema(description = "上下文的最大 Message 数量", example = "8192")
+ private Integer maxContexts;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java
new file mode 100644
index 000000000..0a9d08de5
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole;
+
+import lombok.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+@Schema(description = "管理后台 - AI 聊天角色分页 Request VO")
+@Data
+public class AiChatRolePageReqVO extends PageParam {
+
+ @Schema(description = "角色名称", example = "李四")
+ private String name;
+
+ @Schema(description = "角色类别", example = "创作")
+ private String category;
+
+ @Schema(description = "是否公开", example = "1")
+ private Boolean publicStatus;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java
new file mode 100644
index 000000000..eb34da274
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole;
+
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
+import com.fhs.core.trans.anno.Trans;
+import com.fhs.core.trans.constant.TransType;
+import com.fhs.core.trans.vo.VO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - AI 聊天角色 Response VO")
+@Data
+public class AiChatRoleRespVO implements VO {
+
+ @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32746")
+ private Long id;
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9442")
+ private Long userId;
+
+ @Schema(description = "模型编号", example = "17640")
+ @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = {"name", "model"}, refs = {"modelName", "model"})
+ private Long modelId;
+ @Schema(description = "模型名字", example = "张三")
+ private String modelName;
+ @Schema(description = "模型标识", example = "gpt-3.5-turbo-0125")
+ private String model;
+
+ @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+ private String name;
+
+ @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+ private String avatar;
+
+ @Schema(description = "角色类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "创作")
+ private String category;
+
+ @Schema(description = "角色排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer sort;
+
+ @Schema(description = "角色描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对")
+ private String description;
+
+ @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String systemMessage;
+
+ @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Boolean publicStatus;
+
+ @Schema(description = "状态", example = "1")
+ private Integer status;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java
new file mode 100644
index 000000000..4673901d3
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+import org.hibernate.validator.constraints.URL;
+
+@Schema(description = "管理后台 - AI 聊天角色新增/修改【我的】 Request VO")
+@Data
+public class AiChatRoleSaveMyReqVO {
+
+ @Schema(description = "角色编号", example = "32746")
+ private Long id;
+
+ @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+ @NotEmpty(message = "角色名称不能为空")
+ private String name;
+
+ @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+ @NotEmpty(message = "角色头像不能为空")
+ @URL(message = "角色头像必须是 URL 格式")
+ private String avatar;
+
+ @Schema(description = "角色描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对")
+ @NotEmpty(message = "角色描述不能为空")
+ private String description;
+
+ @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED, example = "现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题")
+ @NotEmpty(message = "角色设定不能为空")
+ private String systemMessage;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java
new file mode 100644
index 000000000..bdda027ef
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import jakarta.validation.constraints.*;
+import org.hibernate.validator.constraints.URL;
+
+@Schema(description = "管理后台 - AI 聊天角色新增/修改 Request VO")
+@Data
+public class AiChatRoleSaveReqVO {
+
+ @Schema(description = "角色编号", example = "32746")
+ private Long id;
+
+ @Schema(description = "模型编号", example = "17640")
+ private Long modelId;
+
+ @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
+ @NotEmpty(message = "角色名称不能为空")
+ private String name;
+
+ @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+ @NotEmpty(message = "角色头像不能为空")
+ @URL(message = "角色头像必须是 URL 格式")
+ private String avatar;
+
+ @Schema(description = "角色类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "创作")
+ @NotEmpty(message = "角色类别不能为空")
+ private String category;
+
+ @Schema(description = "角色排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "角色排序不能为空")
+ private Integer sort;
+
+ @Schema(description = "角色描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对")
+ @NotEmpty(message = "角色描述不能为空")
+ private String description;
+
+ @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED, example = "现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题")
+ @NotEmpty(message = "角色设定不能为空")
+ private String systemMessage;
+
+ @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "是否公开不能为空")
+ private Boolean publicStatus;
+
+ @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "状态不能为空")
+ @InEnum(CommonStatusEnum.class)
+ private Integer status;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
new file mode 100644
index 000000000..ae68c82ea
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
@@ -0,0 +1,26 @@
+### 生成音乐:Suno + 歌词模式
+POST {{baseUrl}}/ai/music/generate
+Content-Type: application/json
+Authorization: {{token}}
+
+{
+ "platform": "Suno",
+ "generateMode": 2,
+ "prompt": "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。",
+ "model": "chirp-v3.5",
+ "tags": ["Happy"],
+ "title": "Happy Song"
+}
+
+### 生成音乐:Suno + 描述模式
+POST {{baseUrl}}/ai/music/generate
+Content-Type: application/json
+Authorization: {{token}}
+
+{
+ "platform": "Suno",
+ "generateMode": 1,
+ "model": "chirp-v3.5",
+ "prompt": "happy music",
+ "makeInstrumental": false
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
new file mode 100644
index 000000000..6c09e4b30
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
@@ -0,0 +1,98 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.music.vo.*;
+import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
+import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - AI 音乐")
+@RestController
+@RequestMapping("/ai/music")
+public class AiMusicController {
+
+ @Resource
+ private AiMusicService musicService;
+
+ @GetMapping("/my-page")
+ @Operation(summary = "获得【我的】音乐分页")
+ public CommonResult> getMusicMyPage(@Valid AiMusicPageReqVO pageReqVO) {
+ PageResult pageResult = musicService.getMusicMyPage(pageReqVO, getLoginUserId());
+ return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
+ }
+
+ @PostMapping("/generate")
+ @Operation(summary = "音乐生成")
+ public CommonResult> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) {
+ return success(musicService.generateMusic(getLoginUserId(), reqVO));
+ }
+
+ @Operation(summary = "删除【我的】音乐记录")
+ @DeleteMapping("/delete-my")
+ @Parameter(name = "id", required = true, description = "音乐编号", example = "1024")
+ public CommonResult deleteMusicMy(@RequestParam("id") Long id) {
+ musicService.deleteMusicMy(id, getLoginUserId());
+ return success(true);
+ }
+
+ @GetMapping("/get-my")
+ @Operation(summary = "获取【我的】音乐")
+ @Parameter(name = "id", required = true, description = "音乐编号", example = "1024")
+ public CommonResult getMusicMy(@RequestParam("id") Long id) {
+ AiMusicDO music = musicService.getMusic(id);
+ if (music == null || ObjUtil.notEqual(getLoginUserId(), music.getUserId())) {
+ return success(null);
+ }
+ return success(BeanUtils.toBean(music, AiMusicRespVO.class));
+ }
+
+ @PostMapping("/update-my")
+ @Operation(summary = "修改【我的】音乐 目前只支持修改标题")
+ @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星")
+ public CommonResult updateMy(AiMusicUpdateMyReqVO updateReqVO) {
+ musicService.updateMyMusic(updateReqVO, getLoginUserId());
+ return success(true);
+ }
+
+ // ================ 音乐管理 ================
+
+ @GetMapping("/page")
+ @Operation(summary = "获得音乐分页")
+ @PreAuthorize("@ss.hasPermission('ai:music:query')")
+ public CommonResult> getMusicPage(@Valid AiMusicPageReqVO pageReqVO) {
+ PageResult pageResult = musicService.getMusicPage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除音乐")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('ai:music:delete')")
+ public CommonResult deleteMusic(@RequestParam("id") Long id) {
+ musicService.deleteMusic(id);
+ return success(true);
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "更新音乐")
+ @PreAuthorize("@ss.hasPermission('ai:music:update')")
+ public CommonResult updateMusic(@Valid @RequestBody AiMusicUpdateReqVO updateReqVO) {
+ musicService.updateMusic(updateReqVO);
+ return success(true);
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java
new file mode 100644
index 000000000..678edae3d
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java
@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
+import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI 音乐分页 Request VO")
+@Data
+public class AiMusicPageReqVO extends PageParam {
+
+ @Schema(description = "用户编号", example = "12212")
+ private Long userId;
+
+ @Schema(description = "音乐名称", example = "夜空中最亮的星")
+ private String title;
+
+ @Schema(description = "音乐状态", example = "20")
+ @InEnum(AiMusicStatusEnum.class)
+ private Integer status;
+
+ @Schema(description = "生成模式", example = "1")
+ @InEnum(AiMusicGenerateModeEnum.class)
+ private Integer generateMode;
+
+ @Schema(description = "是否发布", example = "true")
+ private Boolean publicStatus;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java
new file mode 100644
index 000000000..05044a4e7
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - AI 音乐 Response VO")
+@Data
+public class AiMusicRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
+ private Long id;
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12212")
+ private Long userId;
+
+ @Schema(description = "音乐名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "夜空中最亮的星")
+ private String title;
+
+ @Schema(description = "歌词", example = "oh~卖糕的")
+ private String lyric;
+
+ @Schema(description = "图片地址", example = "https://www.iocoder.cn")
+ private String imageUrl;
+
+ @Schema(description = "音频地址", example = "https://www.iocoder.cn")
+ private String audioUrl;
+
+ @Schema(description = "视频地址", example = "https://www.iocoder.cn")
+ private String videoUrl;
+
+ @Schema(description = "音乐状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
+ private Integer status;
+
+ @Schema(description = "描述词", example = "一首轻快的歌曲")
+ private String gptDescriptionPrompt;
+
+ @Schema(description = "提示词", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
+ private String prompt;
+
+ @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno")
+ private String platform;
+
+ @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
+ private String model;
+
+ @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ private Integer generateMode;
+
+ @Schema(description = "音乐风格标签")
+ private List tags;
+
+ @Schema(description = "音乐时长", example = "[\"pop\",\"jazz\",\"punk\"]")
+ private Double duration;
+
+ @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+ private Boolean publicStatus;
+
+ @Schema(description = "任务编号", example = "11369")
+ private String taskId;
+
+ @Schema(description = "错误信息")
+ private String errorMessage;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java
new file mode 100644
index 000000000..457670115
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 修改我的音乐 Request VO")
+@Data
+public class AiMusicUpdateMyReqVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
+ @NotNull(message = "编号不能为空")
+ private Long id;
+
+ @Schema(description = "音乐名称", example = "夜空中最亮的星")
+ private String title;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java
new file mode 100644
index 000000000..447bc9765
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 音乐修改 Request VO")
+@Data
+public class AiMusicUpdateReqVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
+ @NotNull(message = "编号不能为空")
+ private Long id;
+
+ @Schema(description = "是否发布", example = "true")
+ private Boolean publicStatus;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
new file mode 100644
index 000000000..f72d2b54a
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "管理后台 - AI 音乐生成 Request VO")
+@Data
+public class AiSunoGenerateReqVO {
+
+ @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno")
+ @NotBlank(message = "平台不能为空")
+ private String platform; // 参见 AiPlatformEnum 枚举
+
+ /**
+ * 1. 描述模式:描述词 + 是否纯音乐 + 模型
+ * 2. 歌词模式:歌词 + 音乐风格 + 标题 + 模型
+ */
+ @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+ @NotNull(message = "生成模式不能为空")
+ private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举
+
+ @Schema(description = "用于生成音乐音频的歌词提示",
+ example = """
+ 1.描述模式:创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。
+ 2.歌词模式:
+ [Verse]
+ 阳光下奔跑 多么欢快
+ 假期就要来 心都飞起来
+ 朋友在一旁 笑声又灿烂
+ 无忧无虑的 每一天甜蜜
+ [Chorus]
+ 马上放假了 快来庆祝
+ 一起去旅行 快去冒险
+ 日子太短暂 别再等待
+ 马上放假了 梦想起飞
+ """)
+ private String prompt;
+
+ @Schema(description = "是否纯音乐", example = "true")
+ private Boolean makeInstrumental;
+
+ @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
+ @NotEmpty(message = "模型不能为空")
+ private String model;
+
+ @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]")
+ private List tags;
+
+ @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星")
+ private String title;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java
new file mode 100644
index 000000000..d27204d21
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java
@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.ai.controller.admin.write;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+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.write.vo.AiWriteGenerateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteRespVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO;
+import cn.iocoder.yudao.module.ai.service.write.AiWriteService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.PermitAll;
+import jakarta.validation.Valid;
+import org.springframework.http.MediaType;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+
+@Tag(name = "管理后台 - AI 写作")
+@RestController
+@RequestMapping("/ai/write")
+public class AiWriteController {
+
+ @Resource
+ private AiWriteService writeService;
+
+ @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+ @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快")
+ @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
+ public Flux> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) {
+ return writeService.generateWriteContent(generateReqVO, getLoginUserId());
+ }
+
+ // ================ 写作管理 ================
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "删除写作")
+ @Parameter(name = "id", description = "编号", required = true)
+ @PreAuthorize("@ss.hasPermission('ai:write:delete')")
+ public CommonResult deleteWrite(@RequestParam("id") Long id) {
+ writeService.deleteWrite(id);
+ return success(true);
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "获得写作分页")
+ @PreAuthorize("@ss.hasPermission('ai:write:query')")
+ public CommonResult> getWritePage(@Valid AiWritePageReqVO pageReqVO) {
+ PageResult pageResult = writeService.getWritePage(pageReqVO);
+ return success(BeanUtils.toBean(pageResult, AiWriteRespVO.class));
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java
new file mode 100644
index 000000000..21c60420d
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java
@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.ai.controller.admin.write.vo;
+
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 写作生成 Request VO")
+@Data
+public class AiWriteGenerateReqVO {
+
+ @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @InEnum(value = AiWriteTypeEnum.class, message = "写作类型必须是 {value}")
+ private Integer type;
+
+ @Schema(description = "写作内容提示", example = "1.撰写:田忌赛马;2.回复:不批")
+ private String prompt;
+
+ @Schema(description = "原文", example = "领导我要辞职")
+ private String originalContent;
+
+ @Schema(description = "长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "长度不能为空")
+ private Integer length;
+
+ @Schema(description = "格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "格式不能为空")
+ private Integer format;
+
+ @Schema(description = "语气", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "语气不能为空")
+ private Integer tone;
+
+ @Schema(description = "语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "语言不能为空")
+ private Integer language;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java
new file mode 100644
index 000000000..047380e42
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java
@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.ai.controller.admin.write.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI 写作分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AiWritePageReqVO extends PageParam {
+
+ @Schema(description = "用户编号", example = "28404")
+ private Long userId;
+
+ @Schema(description = "写作类型", example = "1")
+ private Integer type;
+
+ @Schema(description = "平台", example = "TongYi")
+ private String platform;
+
+ @Schema(description = "创建时间")
+ @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+ private LocalDateTime[] createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java
new file mode 100644
index 000000000..4160de9ad
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.ai.controller.admin.write.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - AI 写作 Response VO")
+@Data
+public class AiWriteRespVO {
+
+ @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5311")
+ private Long id;
+
+ @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28404")
+ private Long userId;
+
+ @Schema(description = "写作类型", example = "1")
+ private Integer type;
+
+ @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "TongYi")
+ private String platform;
+
+ @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen")
+ private String model;
+
+ @Schema(description = "生成内容提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "撰写:田忌赛马")
+ private String prompt;
+
+ @Schema(description = "生成的内容", example = "你非常不错")
+ private String generatedContent;
+
+ @Schema(description = "原文", example = "真的么?")
+ private String originalContent;
+
+ @Schema(description = "长度提示词", example = "1")
+ private Integer length;
+
+ @Schema(description = "格式提示词", example = "2")
+ private Integer format;
+
+ @Schema(description = "语气提示词", example = "3")
+ private Integer tone;
+
+ @Schema(description = "语言提示词", example = "4")
+ private Integer language;
+
+ @Schema(description = "错误信息")
+ private String errorMessage;
+
+ @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDateTime createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java
new file mode 100644
index 000000000..05b1ce233
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * TODO 芋艿:站位,无特殊作用
+ */
+package cn.iocoder.yudao.module.ai.controller.app;
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java
new file mode 100644
index 000000000..68dfddfa3
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 提供 RESTful API 给前端:
+ * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目
+ * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分
+ */
+package cn.iocoder.yudao.module.ai.controller;
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java
new file mode 100644
index 000000000..0b7eb0233
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java
@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI Chat 对话 DO
+ *
+ * 用户每次发起 Chat 聊天时,会创建一个 {@link AiChatConversationDO} 对象,将它的消息关联在一起
+ *
+ * @author fansili
+ * @since 2024/4/14 17:35
+ */
+@TableName("ai_chat_conversation")
+@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AiChatConversationDO extends BaseDO {
+
+ public static final String TITLE_DEFAULT = "新对话";
+
+ /**
+ * ID 编号,自增
+ */
+ @TableId
+ private Long id;
+
+ /**
+ * 用户编号
+ *
+ * 关联 AdminUserDO 的 userId 字段
+ */
+ private Long userId;
+
+ /**
+ * 对话标题
+ *
+ * 默认由系统自动生成,可用户手动修改
+ */
+ private String title;
+ /**
+ * 是否置顶
+ */
+ private Boolean pinned;
+ /**
+ * 置顶时间
+ */
+ private LocalDateTime pinnedTime;
+
+ /**
+ * 角色编号
+ *
+ * 关联 {@link AiChatRoleDO#getId()}
+ */
+ private Long roleId;
+
+ /**
+ * 模型编号
+ *
+ * 关联 {@link AiChatModelDO#getId()} 字段
+ */
+ private Long modelId;
+ /**
+ * 模型标志
+ */
+ private String model;
+
+ // ========== 对话配置 ==========
+
+ /**
+ * 角色设定
+ */
+ private String systemMessage;
+ /**
+ * 温度参数
+ *
+ * 用于调整生成回复的随机性和多样性程度:较低的温度值会使输出更收敛于高频词汇,较高的则增加多样性
+ */
+ private Double temperature;
+ /**
+ * 单条回复的最大 Token 数量
+ */
+ private Integer maxTokens;
+ /**
+ * 上下文的最大 Message 数量
+ */
+ private Integer maxContexts;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
new file mode 100644
index 000000000..973c593ce
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import org.springframework.ai.chat.messages.MessageType;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * AI Chat 消息 DO
+ *
+ * @since 2024/4/14 17:35
+ * @since 2024/4/14 17:35
+ */
+@TableName("ai_chat_message")
+@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AiChatMessageDO extends BaseDO {
+
+ /**
+ * 编号,作为每条聊天记录的唯一标识符
+ */
+ @TableId
+ private Long id;
+
+ /**
+ * 对话编号
+ *
+ * 关联 {@link AiChatConversationDO#getId()} 字段
+ */
+ private Long conversationId;
+ /**
+ * 回复消息编号
+ *
+ * 关联 {@link #id} 字段
+ *
+ * 大模型回复的消息编号,用于“问答”的关联
+ */
+ private Long replyId;
+
+ /**
+ * 消息类型
+ *
+ * 也等价于 OpenAPI 的 role 字段
+ *
+ * 枚举 {@link MessageType}
+ */
+ private String type;
+ /**
+ * 用户编号
+ *
+ * 关联 AdminUserDO 的 userId 字段
+ */
+ private Long userId;
+ /**
+ * 角色编号
+ *
+ * 关联 {@link AiChatRoleDO#getId()} 字段
+ */
+ private Long roleId;
+
+ /**
+ * 模型标志
+ */
+ private String model;
+ /**
+ * 模型编号
+ *
+ * 关联 {@link AiChatModelDO#getId()} 字段
+ */
+ private Long modelId;
+
+ /**
+ * 聊天内容
+ */
+ private String content;
+
+ /**
+ * 是否携带上下文
+ */
+ private Boolean useContext;
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java
new file mode 100644
index 000000000..6768d904b
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java
@@ -0,0 +1,135 @@
+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.dal.dataobject.model.AiChatModelDO;
+import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.Data;
+import org.springframework.ai.openai.OpenAiImageOptions;
+import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI 绘画 DO
+ *
+ * @author fansili
+ */
+@TableName(value = "ai_image", autoResultMap = true)
+@Data
+public class AiImageDO extends BaseDO {
+
+ /**
+ * 编号
+ */
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ /**
+ * 用户编号
+ *
+ * 关联 {@link AdminUserRespDTO#getId()}
+ */
+ private Long userId;
+
+ /**
+ * 提示词
+ */
+ private String prompt;
+
+ /**
+ * 平台
+ *
+ * 枚举 {@link cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum}
+ */
+ private String platform;
+ /**
+ * 模型
+ *
+ * 冗余 {@link AiChatModelDO#getModel()}
+ */
+ private String model;
+
+ /**
+ * 图片宽度
+ */
+ private Integer width;
+ /**
+ * 图片高度
+ */
+ private Integer height;
+
+ /**
+ * 生成状态
+ *
+ * 枚举 {@link AiImageStatusEnum}
+ */
+ private Integer status;
+
+ /**
+ * 完成时间
+ */
+ private LocalDateTime finishTime;
+
+ /**
+ * 绘画错误信息
+ */
+ private String errorMessage;
+
+ /**
+ * 图片地址
+ */
+ private String picUrl;
+ /**
+ * 是否公开
+ */
+ private Boolean publicStatus;
+
+ /**
+ * 绘制参数,不同 platform 的不同参数
+ *
+ * 1. {@link OpenAiImageOptions}
+ * 2. {@link StabilityAiImageOptions}
+ */
+ @TableField(typeHandler = JacksonTypeHandler.class)
+ private Map options;
+
+ /**
+ * mj buttons 按钮
+ */
+ @TableField(typeHandler = ButtonTypeHandler.class)
+ private List buttons;
+
+ /**
+ * 任务编号
+ *
+ * 1. midjourney proxy:关联的 task id
+ */
+ private String taskId;
+
+ public static class ButtonTypeHandler extends AbstractJsonTypeHandler