【模块新增】AI:支持通义千问、文心一言、讯飞星火、智谱、DeepSeek 等国内外大模型能力

pull/126/head
YunaiV 2024-07-13 11:26:17 +08:00
parent 95936e4a0b
commit c6937cf199
181 changed files with 14204 additions and 1 deletions

18
pom.xml
View File

@ -22,6 +22,7 @@
<module>yudao-module-mall</module>
<module>yudao-module-crm</module>
<module>yudao-module-erp</module>
<module>yudao-module-ai</module>
</modules>
<name>${project.artifactId}</name>
@ -142,6 +143,23 @@
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>

View File

@ -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/ 开启]");
});
}

View File

@ -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
--- #################### 芋道相关配置 ####################

27
yudao-module-ai/pom.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<modules>
<module>yudao-module-ai-api</module>
<module>yudao-module-ai-biz</module>
<module>yudao-spring-boot-starter-ai</module>
</modules>
<packaging>pom</packaging>
<artifactId>yudao-module-ai</artifactId>
<name>${project.artifactId}</name>
<description>
ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
目前已接入各种模型,不限于:
国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description>
</project>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-module-ai</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-ai-api</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
ai 模块 API暴露给其它模块调用
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,4 @@
/**
*
*/
package cn.iocoder.yudao.module.ai.api;

View File

@ -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;
}
}

View File

@ -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"; // 写作语气
}

View File

@ -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, "写作生成异常!");
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-module-ai</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-ai-biz</artifactId>
<name>${project.artifactId}</name>
<description>
ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
目前已接入各种模型,不限于:
国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description>
<dependencies>
<!-- Spring Cloud 基础 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 依赖服务 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-module-ai-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-ai</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-rpc</artifactId>
</dependency>
<!-- Registry 注册中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Config 配置中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Job 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-job</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-monitor</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.ai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
*
* <p>
* 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/ 文章
}
}

View File

@ -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<Long> createChatConversationMy(@RequestBody @Valid AiChatConversationCreateMyReqVO createReqVO) {
return success(chatConversationService.createChatConversationMy(createReqVO, getLoginUserId()));
}
@PutMapping("/update-my")
@Operation(summary = "更新【我的】聊天对话")
public CommonResult<Boolean> updateChatConversationMy(@RequestBody @Valid AiChatConversationUpdateMyReqVO updateReqVO) {
chatConversationService.updateChatConversationMy(updateReqVO, getLoginUserId());
return success(true);
}
@GetMapping("/my-list")
@Operation(summary = "获得【我的】聊天对话列表")
public CommonResult<List<AiChatConversationRespVO>> getChatConversationMyList() {
List<AiChatConversationDO> 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<AiChatConversationRespVO> 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<Boolean> deleteChatConversationMy(@RequestParam("id") Long id) {
chatConversationService.deleteChatConversationMy(id, getLoginUserId());
return success(true);
}
@DeleteMapping("/delete-by-unpinned")
@Operation(summary = "删除未置顶的聊天对话")
public CommonResult<Boolean> deleteChatConversationMyByUnpinned() {
chatConversationService.deleteChatConversationMyByUnpinned(getLoginUserId());
return success(true);
}
// ========== 对话管理 ==========
@GetMapping("/page")
@Operation(summary = "获得对话分页", description = "用于【对话管理】菜单")
@PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')")
public CommonResult<PageResult<AiChatConversationRespVO>> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) {
PageResult<AiChatConversationDO> pageResult = chatConversationService.getChatConversationPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
// 拼接关联数据
Map<Long, Integer> 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<Boolean> deleteChatConversationByAdmin(@RequestParam("id") Long id) {
chatConversationService.deleteChatConversationByAdmin(id);
return success(true);
}
}

View File

@ -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}}

View File

@ -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<AiChatMessageSendRespVO> 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<CommonResult<AiChatMessageSendRespVO>> 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<List<AiChatMessageRespVO>> getChatMessageListByConversationId(
@RequestParam("conversationId") Long conversationId) {
AiChatConversationDO conversation = chatConversationService.getChatConversation(conversationId);
if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) {
return success(Collections.emptyList());
}
List<AiChatMessageDO> 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<Boolean> 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<Boolean> 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<PageResult<AiChatMessageRespVO>> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) {
PageResult<AiChatMessageDO> pageResult = chatMessageService.getChatMessagePage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) {
return success(PageResult.empty());
}
// 拼接数据
Map<Long, AiChatRoleDO> 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<Boolean> deleteChatMessageByAdmin(@RequestParam("id") Long id) {
chatMessageService.deleteChatMessageByAdmin(id);
return success(true);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,42 @@
### 生成图片OpenAIDALL
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"
}

View File

@ -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<PageResult<AiImageRespVO>> getImagePageMy(@Validated PageParam pageReqVO) {
PageResult<AiImageDO> 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<AiImageRespVO> 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<List<AiImageRespVO>> getImageListMyByIds(@RequestParam("ids") List<Long> ids) {
List<AiImageDO> 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<Long> 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<Boolean> deleteImageMy(@RequestParam("id") Long id) {
imageService.deleteImageMy(id, getLoginUserId());
return success(true);
}
// ================ midjourney 专属 ================
@Operation(summary = "【Midjourney】生成图片")
@PostMapping("/midjourney/imagine")
public CommonResult<Long> 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<Boolean> midjourneyNotify(@Valid @RequestBody MidjourneyApi.Notify notify) {
imageService.midjourneyNotify(notify);
return success(true);
}
@Operation(summary = "【Midjourney】Action 操作(二次生成图片)", description = "例如说放大、缩小、U1、U2 等")
@PostMapping("/midjourney/action")
public CommonResult<Long> 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<PageResult<AiImageRespVO>> getImagePage(@Valid AiImagePageReqVO pageReqVO) {
PageResult<AiImageDO> pageResult = imageService.getImagePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
}
@PutMapping("/update")
@Operation(summary = "更新绘画")
@PreAuthorize("@ss.hasPermission('ai:image:update')")
public CommonResult<Boolean> 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<Boolean> deleteImage(@RequestParam("id") Long id) {
imageService.deleteImage(id);
return success(true);
}
}

View File

@ -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 256x256512x5121024x1024
* 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<String, String> options;
}

View File

@ -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;
}

View File

@ -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<String, String> options;
@Schema(description = "mj buttons 按钮")
private List<MidjourneyApi.Button> buttons;
@Schema(description = "完成时间")
private LocalDateTime finishTime;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<CommonResult<String>> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) {
return mindMapService.generateMindMap(generateReqVO, getLoginUserId());
}
}

View File

@ -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;
}

View File

@ -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<Long> createApiKey(@Valid @RequestBody AiApiKeySaveReqVO createReqVO) {
return success(apiKeyService.createApiKey(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新 API 密钥")
@PreAuthorize("@ss.hasPermission('ai:api-key:update')")
public CommonResult<Boolean> 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<Boolean> 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<AiApiKeyRespVO> 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<PageResult<AiApiKeyRespVO>> getApiKeyPage(@Valid AiApiKeyPageReqVO pageReqVO) {
PageResult<AiApiKeyDO> pageResult = apiKeyService.getApiKeyPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiApiKeyRespVO.class));
}
@GetMapping("/simple-list")
@Operation(summary = "获得 API 密钥分页列表")
public CommonResult<List<AiChatModelRespVO>> getApiKeySimpleList() {
List<AiApiKeyDO> list = apiKeyService.getApiKeyList();
return success(convertList(list, key -> new AiChatModelRespVO().setId(key.getId()).setName(key.getName())));
}
}

View File

@ -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<Long> createChatModel(@Valid @RequestBody AiChatModelSaveReqVO createReqVO) {
return success(chatModelService.createChatModel(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新聊天模型")
@PreAuthorize("@ss.hasPermission('ai:chat-model:update')")
public CommonResult<Boolean> 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<Boolean> 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<AiChatModelRespVO> 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<PageResult<AiChatModelRespVO>> getChatModelPage(@Valid AiChatModelPageReqVO pageReqVO) {
PageResult<AiChatModelDO> 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<List<AiChatModelRespVO>> getChatModelSimpleList(@RequestParam("status") Integer status) {
List<AiChatModelDO> list = chatModelService.getChatModelListByStatus(status);
return success(convertList(list, model -> new AiChatModelRespVO().setId(model.getId())
.setName(model.getName()).setModel(model.getModel())));
}
}

View File

@ -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<PageResult<AiChatRoleRespVO>> getChatRoleMyPage(@Valid AiChatRolePageReqVO pageReqVO) {
PageResult<AiChatRoleDO> 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<AiChatRoleRespVO> 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<Long> createChatRoleMy(@Valid @RequestBody AiChatRoleSaveMyReqVO createReqVO) {
return success(chatRoleService.createChatRoleMy(createReqVO, getLoginUserId()));
}
@PutMapping("/update-my")
@Operation(summary = "更新【我的】聊天角色")
public CommonResult<Boolean> 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<Boolean> deleteChatRoleMy(@RequestParam("id") Long id) {
chatRoleService.deleteChatRoleMy(id, getLoginUserId());
return success(true);
}
@GetMapping("/category-list")
@Operation(summary = "获得聊天角色的分类列表")
public CommonResult<List<String>> getChatRoleCategoryList() {
return success(chatRoleService.getChatRoleCategoryList());
}
// ========== 角色管理 ==========
@PostMapping("/create")
@Operation(summary = "创建聊天角色")
@PreAuthorize("@ss.hasPermission('ai:chat-role:create')")
public CommonResult<Long> createChatRole(@Valid @RequestBody AiChatRoleSaveReqVO createReqVO) {
return success(chatRoleService.createChatRole(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新聊天角色")
@PreAuthorize("@ss.hasPermission('ai:chat-role:update')")
public CommonResult<Boolean> 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<Boolean> 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<AiChatRoleRespVO> 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<PageResult<AiChatRoleRespVO>> getChatRolePage(@Valid AiChatRolePageReqVO pageReqVO) {
PageResult<AiChatRoleDO> pageResult = chatRoleService.getChatRolePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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<PageResult<AiMusicRespVO>> getMusicMyPage(@Valid AiMusicPageReqVO pageReqVO) {
PageResult<AiMusicDO> pageResult = musicService.getMusicMyPage(pageReqVO, getLoginUserId());
return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
}
@PostMapping("/generate")
@Operation(summary = "音乐生成")
public CommonResult<List<Long>> 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<Boolean> 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<AiMusicRespVO> 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<Boolean> updateMy(AiMusicUpdateMyReqVO updateReqVO) {
musicService.updateMyMusic(updateReqVO, getLoginUserId());
return success(true);
}
// ================ 音乐管理 ================
@GetMapping("/page")
@Operation(summary = "获得音乐分页")
@PreAuthorize("@ss.hasPermission('ai:music:query')")
public CommonResult<PageResult<AiMusicRespVO>> getMusicPage(@Valid AiMusicPageReqVO pageReqVO) {
PageResult<AiMusicDO> 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<Boolean> deleteMusic(@RequestParam("id") Long id) {
musicService.deleteMusic(id);
return success(true);
}
@PutMapping("/update")
@Operation(summary = "更新音乐")
@PreAuthorize("@ss.hasPermission('ai:music:update')")
public CommonResult<Boolean> updateMusic(@Valid @RequestBody AiMusicUpdateReqVO updateReqVO) {
musicService.updateMusic(updateReqVO);
return success(true);
}
}

View File

@ -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;
}

View File

@ -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<String> 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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<String> tags;
@Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星")
private String title;
}

View File

@ -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<CommonResult<String>> 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<Boolean> deleteWrite(@RequestParam("id") Long id) {
writeService.deleteWrite(id);
return success(true);
}
@GetMapping("/page")
@Operation(summary = "获得写作分页")
@PreAuthorize("@ss.hasPermission('ai:write:query')")
public CommonResult<PageResult<AiWriteRespVO>> getWritePage(@Valid AiWritePageReqVO pageReqVO) {
PageResult<AiWriteDO> pageResult = writeService.getWritePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiWriteRespVO.class));
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
/**
* TODO
*/
package cn.iocoder.yudao.module.ai.controller.app;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<String, Object> options;
/**
* mj buttons
*/
@TableField(typeHandler = ButtonTypeHandler.class)
private List<MidjourneyApi.Button> buttons;
/**
*
*
* 1. midjourney proxy task id
*/
private String taskId;
public static class ButtonTypeHandler extends AbstractJsonTypeHandler<Object> {
@Override
protected Object parse(String json) {
return JsonUtils.parseArray(json, MidjourneyApi.Button.class);
}
@Override
protected String toJson(Object obj) {
return JsonUtils.toJsonString(obj);
}
}
}

View File

@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.mindmap;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* AI DO
*
* @author xiaoxin
*/
@TableName(value = "ai_mind_map")
@Data
public class AiMindMapDO extends BaseDO {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
*
* <p>
* AdminUserDO userId
*/
private Long userId;
/**
*
* <p>
* {@link AiPlatformEnum}
*/
private String platform;
/**
*
*/
private String model;
/**
*
*/
private String prompt;
/**
*
*/
private String generatedContent;
/**
*
*/
private String errorMessage;
}

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* AI API DO
*
* @author
*/
@TableName("ai_api_key")
@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiApiKeyDO extends BaseDO {
/**
*
*/
@TableId
private Long id;
/**
*
*/
private String name;
/**
*
*/
private String apiKey;
/**
*
*
* {@link AiPlatformEnum}
*/
private String platform;
/**
* API
*/
private String url;
/**
*
*
* {@link CommonStatusEnum}
*/
private Integer status;
}

View File

@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* AI DO
*
* {@link #status} {@link #sort}
*
* @author fansili
* @since 2024/4/24 19:39
*/
@TableName("ai_chat_model")
@KeySequence("ai_chat_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiChatModelDO extends BaseDO {
/**
*
*/
@TableId
private Long id;
/**
* API
*
* {@link AiApiKeyDO#getId()}
*/
private Long keyId;
/**
*
*/
private String name;
/**
*
*/
private String model;
/**
*
*
* {@link AiPlatformEnum}
*/
private String platform;
/**
*
*/
private Integer sort;
/**
*
*
* {@link CommonStatusEnum}
*/
private Integer status;
// ========== 对话配置 ==========
/**
*
*
* 使
*/
private Double temperature;
/**
* Token
*/
private Integer maxTokens;
/**
* Message
*/
private Integer maxContexts;
}

View File

@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
/**
* AI DO
*
* @author fansili
* @since 2024/4/24 19:39
*/
@TableName(value = "ai_chat_role", autoResultMap = true)
@KeySequence("ai_chat_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiChatRoleDO extends BaseDO {
/**
*
*/
@TableId
private Long id;
/**
*
*/
private String name;
/**
*
*/
private String avatar;
/**
*
*/
private String category;
/**
*
*/
private String description;
/**
*
*/
private String systemMessage;
/**
*
*
* AdminUserDO userId
*/
private Long userId;
/**
*
*
* {@link AiChatModelDO#getId()}
*/
private Long modelId;
/**
*
*
* 1. true -
* 2. false -
*/
private Boolean publicStatus;
/**
*
*/
private Integer sort;
/**
*
*
* {@link CommonStatusEnum}
*/
private Integer status;
}

View File

@ -0,0 +1,117 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.music;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
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.JacksonTypeHandler;
import lombok.Data;
import java.util.List;
/**
* AI DO
*
* @author xiaoxin
*/
@TableName(value = "ai_music", autoResultMap = true)
@Data
public class AiMusicDO extends BaseDO {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
*
* <p>
* AdminUserDO userId
*/
private Long userId;
/**
*
*/
private String title;
/**
*
*/
private String lyric;
/**
*
*/
private String imageUrl;
/**
*
*/
private String audioUrl;
/**
*
*/
private String videoUrl;
/**
*
* <p>
* {@link AiMusicStatusEnum}
*/
private Integer status;
/**
*
* <p>
* {@link AiMusicGenerateModeEnum}
*/
private Integer generateMode;
/**
*
*/
private String description;
/**
*
* <p>
* {@link AiPlatformEnum}
*/
private String platform;
/**
*
*/
private String model;
/**
*
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> tags;
/**
*
*/
private Double duration;
/**
*
*/
private Boolean publicStatus;
/**
*
*/
private String taskId;
/**
*
*/
private String errorMessage;
}

View File

@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.write;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* AI DO
*
* @author xiaoxin
*/
@TableName("ai_write")
@Data
public class AiWriteDO extends BaseDO {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
*
*
* AdminUserDO userId
*/
private Long userId;
/**
*
* <p>
* {@link AiWriteTypeEnum}
*/
private Integer type;
/**
*
*
* {@link AiPlatformEnum}
*/
private String platform;
/**
*
*/
private String model;
/**
*
*/
private String prompt;
/**
*
*/
private String generatedContent;
/**
*
*/
private String originalContent;
/**
*
*
* {@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_LENGTH}
*/
private Integer length;
/**
*
*
* {@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_FORMAT}
*/
private Integer format;
/**
*
*
* {@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_TONE}
*/
private Integer tone;
/**
*
*
* {@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_LANGUAGE}
*/
private Integer language;
/**
*
*/
private String errorMessage;
}

View File

@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.ai.dal.mysql.chat;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* AI Mapper
*
* @author
*/
@Mapper
public interface AiChatConversationMapper extends BaseMapperX<AiChatConversationDO> {
default List<AiChatConversationDO> selectListByUserId(Long userId) {
return selectList(AiChatConversationDO::getUserId, userId);
}
default List<AiChatConversationDO> selectListByUserIdAndPinned(Long userId, boolean pinned) {
return selectList(new LambdaQueryWrapperX<AiChatConversationDO>()
.eq(AiChatConversationDO::getUserId, userId)
.eq(AiChatConversationDO::getPinned, pinned));
}
default PageResult<AiChatConversationDO> selectChatConversationPage(AiChatConversationPageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiChatConversationDO>()
.eqIfPresent(AiChatConversationDO::getUserId, pageReqVO.getUserId())
.likeIfPresent(AiChatConversationDO::getTitle, pageReqVO.getTitle())
.betweenIfPresent(AiChatConversationDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(AiChatConversationDO::getId));
}
}

View File

@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.ai.dal.mysql.chat;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* AI Mapper
*
* @author fansili
*/
@Mapper
public interface AiChatMessageMapper extends BaseMapperX<AiChatMessageDO> {
default List<AiChatMessageDO> selectListByConversationId(Long conversationId) {
return selectList(new LambdaQueryWrapperX<AiChatMessageDO>()
.eq(AiChatMessageDO::getConversationId, conversationId)
.orderByAsc(AiChatMessageDO::getId));
}
default Map<Long, Integer> selectCountMapByConversationId(Collection<Long> conversationIds) {
// SQL count 查询
List<Map<String, Object>> result = selectMaps(new QueryWrapper<AiChatMessageDO>()
.select("COUNT(id) AS count, conversation_id AS conversationId")
.in("conversation_id", conversationIds)
.groupBy("conversation_id"));
if (CollUtil.isEmpty(result)) {
return Collections.emptyMap();
}
// 转换数据
return CollectionUtils.convertMap(result,
record -> MapUtil.getLong(record, "conversationId"),
record -> MapUtil.getInt(record, "count" ));
}
default PageResult<AiChatMessageDO> selectPage(AiChatMessagePageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiChatMessageDO>()
.eqIfPresent(AiChatMessageDO::getConversationId, pageReqVO.getConversationId())
.eqIfPresent(AiChatMessageDO::getUserId, pageReqVO.getUserId())
.likeIfPresent(AiChatMessageDO::getContent, pageReqVO.getContent())
.betweenIfPresent(AiChatMessageDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(AiChatMessageDO::getId));
}
}

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.ai.dal.mysql.image;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* AI Mapper
*
* @author fansili
*/
@Mapper
public interface AiImageMapper extends BaseMapperX<AiImageDO> {
default AiImageDO selectByTaskId(String taskId) {
return this.selectOne(AiImageDO::getTaskId, taskId);
}
default PageResult<AiImageDO> selectPage(AiImagePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiImageDO>()
.eqIfPresent(AiImageDO::getUserId, reqVO.getUserId())
.eqIfPresent(AiImageDO::getPlatform, reqVO.getPlatform())
.eqIfPresent(AiImageDO::getStatus, reqVO.getStatus())
.eqIfPresent(AiImageDO::getPublicStatus, reqVO.getPublicStatus())
.betweenIfPresent(AiImageDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(AiImageDO::getId));
}
default PageResult<AiImageDO> selectPage(Long userId, PageParam pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiImageDO>()
.eq(AiImageDO::getUserId, userId)
.orderByDesc(AiImageDO::getId));
}
default List<AiImageDO> selectListByStatusAndPlatform(Integer status, String platform) {
return selectList(AiImageDO::getStatus, status,
AiImageDO::getPlatform, platform);
}
}

View File

@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.ai.dal.mysql.mindmap;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
import org.apache.ibatis.annotations.Mapper;
/**
* AI Mapper
*
* @author xiaoxin
*/
@Mapper
public interface AiMindMapMapper extends BaseMapperX<AiMindMapDO> {
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.ai.dal.mysql.model;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import org.apache.ibatis.annotations.Mapper;
/**
* AI API Mapper
*
* @author
*/
@Mapper
public interface AiApiKeyMapper extends BaseMapperX<AiApiKeyDO> {
default PageResult<AiApiKeyDO> selectPage(AiApiKeyPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiApiKeyDO>()
.likeIfPresent(AiApiKeyDO::getName, reqVO.getName())
.eqIfPresent(AiApiKeyDO::getPlatform, reqVO.getPlatform())
.eqIfPresent(AiApiKeyDO::getStatus, reqVO.getStatus())
.orderByDesc(AiApiKeyDO::getId));
}
default AiApiKeyDO selectFirstByPlatformAndStatus(String platform, Integer status) {
return selectOne(new QueryWrapperX<AiApiKeyDO>()
.eq("platform", platform)
.eq("status", status)
.limitN(1)
.orderByAsc("id"));
}
}

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.ai.dal.mysql.model;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
* API Mapper
*
* @author fansili
*/
@Mapper
public interface AiChatModelMapper extends BaseMapperX<AiChatModelDO> {
default AiChatModelDO selectFirstByStatus(Integer status) {
return selectOne(new QueryWrapperX<AiChatModelDO>()
.eq("status", status)
.limitN(1)
.orderByAsc("sort"));
}
default PageResult<AiChatModelDO> selectPage(AiChatModelPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiChatModelDO>()
.likeIfPresent(AiChatModelDO::getName, reqVO.getName())
.eqIfPresent(AiChatModelDO::getModel, reqVO.getModel())
.eqIfPresent(AiChatModelDO::getPlatform, reqVO.getPlatform())
.orderByAsc(AiChatModelDO::getSort));
}
default List<AiChatModelDO> selectList(Integer status) {
return selectList(new LambdaQueryWrapperX<AiChatModelDO>()
.eq(AiChatModelDO::getStatus, status)
.orderByAsc(AiChatModelDO::getSort));
}
}

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.ai.dal.mysql.model;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* AI Mapper
*
* @author fansili
*/
@Mapper
public interface AiChatRoleMapper extends BaseMapperX<AiChatRoleDO> {
default PageResult<AiChatRoleDO> selectPage(AiChatRolePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiChatRoleDO>()
.likeIfPresent(AiChatRoleDO::getName, reqVO.getName())
.eqIfPresent(AiChatRoleDO::getCategory, reqVO.getCategory())
.eqIfPresent(AiChatRoleDO::getPublicStatus, reqVO.getPublicStatus())
.orderByAsc(AiChatRoleDO::getSort));
}
default PageResult<AiChatRoleDO> selectPageByMy(AiChatRolePageReqVO reqVO, Long userId) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiChatRoleDO>()
.likeIfPresent(AiChatRoleDO::getName, reqVO.getName())
.eqIfPresent(AiChatRoleDO::getCategory, reqVO.getCategory())
// 情况一:公开
.eq(Boolean.TRUE.equals(reqVO.getPublicStatus()), AiChatRoleDO::getPublicStatus, reqVO.getPublicStatus())
// 情况二:私有
.eq(Boolean.FALSE.equals(reqVO.getPublicStatus()), AiChatRoleDO::getUserId, userId)
.eq(Boolean.FALSE.equals(reqVO.getPublicStatus()), AiChatRoleDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
.orderByAsc(AiChatRoleDO::getSort));
}
default List<AiChatRoleDO> selectListGroupByCategory(Integer status) {
return selectList(new LambdaQueryWrapperX<AiChatRoleDO>()
.select(AiChatRoleDO::getCategory)
.eq(AiChatRoleDO::getStatus, status)
.groupBy(AiChatRoleDO::getCategory));
}
default List<AiChatRoleDO> selectListByName(String name) {
return selectList(new LambdaQueryWrapperX<AiChatRoleDO>()
.likeIfPresent(AiChatRoleDO::getName, name)
.orderByAsc(AiChatRoleDO::getSort));
}
}

View File

@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.ai.dal.mysql.music;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* AI Mapper
*
* @author xiaoxin
*/
@Mapper
public interface AiMusicMapper extends BaseMapperX<AiMusicDO> {
default List<AiMusicDO> selectListByStatus(Integer status) {
return selectList(AiMusicDO::getStatus, status);
}
default PageResult<AiMusicDO> selectPage(AiMusicPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiMusicDO>()
.eqIfPresent(AiMusicDO::getUserId, reqVO.getUserId())
.eqIfPresent(AiMusicDO::getTitle, reqVO.getTitle())
.eqIfPresent(AiMusicDO::getStatus, reqVO.getStatus())
.eqIfPresent(AiMusicDO::getGenerateMode, reqVO.getGenerateMode())
.betweenIfPresent(AiMusicDO::getCreateTime, reqVO.getCreateTime())
.eqIfPresent(AiMusicDO::getPublicStatus, reqVO.getPublicStatus())
.orderByDesc(AiMusicDO::getId));
}
default PageResult<AiMusicDO> selectPageByMy(AiMusicPageReqVO reqVO, Long userId) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiMusicDO>()
// 情况一:公开
.eq(Boolean.TRUE.equals(reqVO.getPublicStatus()), AiMusicDO::getPublicStatus, reqVO.getPublicStatus())
// 情况二:私有
.eq(Boolean.FALSE.equals(reqVO.getPublicStatus()), AiMusicDO::getUserId, userId)
.orderByAsc(AiMusicDO::getId));
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.ai.dal.mysql.write;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO;
import org.apache.ibatis.annotations.Mapper;
/**
* AI Mapper
*
* @author xiaoxin
*/
@Mapper
public interface AiWriteMapper extends BaseMapperX<AiWriteDO> {
default PageResult<AiWriteDO> selectPage(AiWritePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<AiWriteDO>()
.eqIfPresent(AiWriteDO::getUserId, reqVO.getUserId())
.eqIfPresent(AiWriteDO::getType, reqVO.getType())
.eqIfPresent(AiWriteDO::getPlatform, reqVO.getPlatform())
.betweenIfPresent(AiWriteDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(AiWriteDO::getId));
}
}

View File

@ -0,0 +1,6 @@
/**
* ai framework
*
* @author
*/
package cn.iocoder.yudao.module.ai.framework;

View File

@ -0,0 +1,11 @@
package cn.iocoder.yudao.module.ai.framework.rpc.config;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableFeignClients(clients = {DictDataApi.class, FileApi.class})
public class RpcConfiguration {
}

View File

@ -0,0 +1,4 @@
/**
*
*/
package cn.iocoder.yudao.module.ai.framework.rpc;

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.ai.framework.security.config;
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
import cn.iocoder.yudao.module.infra.enums.ApiConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
/**
* AI Security
*/
@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
public class SecurityConfiguration {
@Bean("infraAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
return new AuthorizeRequestsCustomizer() {
@Override
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
// Swagger 接口文档
registry.requestMatchers("/v3/api-docs/**").permitAll() // 元数据
.requestMatchers("/swagger-ui.html").permitAll(); // Swagger UI
// Spring Boot Actuator 的安全配置
registry.requestMatchers("/actuator").permitAll()
.requestMatchers("/actuator/**").permitAll();
// Druid 监控
registry.requestMatchers("/druid/**").permitAll();
// TODO 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案
// RPC 服务的安全配置
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
}
};
}
}

View File

@ -0,0 +1,4 @@
/**
*
*/
package cn.iocoder.yudao.module.ai.framework.security.core;

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.ai.job.image;
import cn.iocoder.yudao.module.ai.service.image.AiImageService;
import com.xxl.job.core.handler.annotation.XxlJob;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Midjourney Job midjourney
*
* @author fansili
*/
@Component
@Slf4j
public class AiMidjourneySyncJob {
@Resource
private AiImageService imageService;
@XxlJob("aiMidjourneySyncJob")
public void execute(String param) {
Integer count = imageService.midjourneySync();
log.info("[execute][同步 Midjourney ({}) 个]", count);
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.ai.job.music;
import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
import com.xxl.job.core.handler.annotation.XxlJob;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Suno Job
*
* @author xiaoxin
*/
@Component
@Slf4j
public class AiSunoSyncJob {
@Resource
private AiMusicService musicService;
@XxlJob("aiSunoSyncJob")
public void execute(String param) {
Integer count = musicService.syncMusic();
log.info("[execute][同步 Suno ({}) 个]", count);
}
}

View File

@ -0,0 +1,10 @@
/**
* ai LLM
*
* GLMDeepSeek
* OpenAIOllamaMidjourneyStableDiffusionSuno
*
* 1. Controller URL /ai/ Module
* 2. DataObject ai_ 便
*/
package cn.iocoder.yudao.module.ai;

View File

@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.ai.service.chat;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
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.AiChatConversationUpdateMyReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import java.util.List;
/**
* AI Service
*
* @author fansili
*/
public interface AiChatConversationService {
/**
*
*
* @param createReqVO
* @param userId
* @return
*/
Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId);
/**
*
*
* @param updateReqVO
* @param userId
*/
void updateChatConversationMy(AiChatConversationUpdateMyReqVO updateReqVO, Long userId);
/**
*
*
* @param userId
* @return
*/
List<AiChatConversationDO> getChatConversationListByUserId(Long userId);
/**
*
*
* @param id
* @return
*/
AiChatConversationDO getChatConversation(Long id);
/**
*
*
* @param id
* @param userId
*/
void deleteChatConversationMy(Long id, Long userId);
/**
*
*
* @param id
*/
void deleteChatConversationByAdmin(Long id);
/**
*
*
* @param id
* @return
*/
AiChatConversationDO validateChatConversationExists(Long id);
/**
* +
*
* @param userId
*/
void deleteChatConversationMyByUnpinned(Long userId);
/**
*
*
* @param pageReqVO
* @return
*/
PageResult<AiChatConversationDO> getChatConversationPage(AiChatConversationPageReqVO pageReqVO);
}

View File

@ -0,0 +1,157 @@
package cn.iocoder.yudao.module.ai.service.chat;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
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.AiChatConversationUpdateMyReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_MODEL_ERROR;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
/**
* AI Service
*
* @author fansili
*/
@Service
@Validated
@Slf4j
public class AiChatConversationServiceImpl implements AiChatConversationService {
@Resource
private AiChatConversationMapper chatConversationMapper;
@Resource
private AiChatModelService chatModalService;
@Resource
private AiChatRoleService chatRoleService;
@Override
public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) {
// 1.1 获得 AiChatRoleDO 聊天角色
AiChatRoleDO role = createReqVO.getRoleId() != null ? chatRoleService.validateChatRole(createReqVO.getRoleId()) : null;
// 1.2 获得 AiChatModelDO 聊天模型
AiChatModelDO model = role != null && role.getModelId() != null ? chatModalService.validateChatModel(role.getModelId())
: chatModalService.getRequiredDefaultChatModel();
Assert.notNull(model, "必须找到默认模型");
validateChatModel(model);
// 2. 创建 AiChatConversationDO 聊天对话
AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false)
.setModelId(model.getId()).setModel(model.getModel())
.setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts());
if (role != null) {
conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage());
} else {
conversation.setTitle(AiChatConversationDO.TITLE_DEFAULT);
}
chatConversationMapper.insert(conversation);
return conversation.getId();
}
@Override
public void updateChatConversationMy(AiChatConversationUpdateMyReqVO updateReqVO, Long userId) {
// 1.1 校验对话是否存在
AiChatConversationDO conversation = validateChatConversationExists(updateReqVO.getId());
if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
throw exception(CHAT_CONVERSATION_NOT_EXISTS);
}
// 1.2 校验模型是否存在(修改模型的情况)
AiChatModelDO model = null;
if (updateReqVO.getModelId() != null) {
model = chatModalService.validateChatModel(updateReqVO.getModelId());
}
// 2. 更新对话信息
AiChatConversationDO updateObj = BeanUtils.toBean(updateReqVO, AiChatConversationDO.class);
if (Boolean.TRUE.equals(updateReqVO.getPinned())) {
updateObj.setPinnedTime(LocalDateTime.now());
}
if (model != null) {
updateObj.setModel(model.getModel());
}
chatConversationMapper.updateById(updateObj);
}
@Override
public List<AiChatConversationDO> getChatConversationListByUserId(Long userId) {
return chatConversationMapper.selectListByUserId(userId);
}
@Override
public AiChatConversationDO getChatConversation(Long id) {
return chatConversationMapper.selectById(id);
}
@Override
public void deleteChatConversationMy(Long id, Long userId) {
// 1. 校验对话是否存在
AiChatConversationDO conversation = validateChatConversationExists(id);
if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), userId)) {
throw exception(CHAT_CONVERSATION_NOT_EXISTS);
}
// 2. 执行删除
chatConversationMapper.deleteById(id);
}
@Override
public void deleteChatConversationByAdmin(Long id) {
// 1. 校验对话是否存在
AiChatConversationDO conversation = validateChatConversationExists(id);
if (conversation == null) {
throw exception(CHAT_CONVERSATION_NOT_EXISTS);
}
// 2. 执行删除
chatConversationMapper.deleteById(id);
}
private void validateChatModel(AiChatModelDO model) {
if (ObjectUtil.isAllNotEmpty(model.getTemperature(), model.getMaxTokens(), model.getMaxContexts())) {
return;
}
throw exception(CHAT_CONVERSATION_MODEL_ERROR);
}
public AiChatConversationDO validateChatConversationExists(Long id) {
AiChatConversationDO conversation = chatConversationMapper.selectById(id);
if (conversation == null) {
throw exception(CHAT_CONVERSATION_NOT_EXISTS);
}
return conversation;
}
@Override
public void deleteChatConversationMyByUnpinned(Long userId) {
List<AiChatConversationDO> list = chatConversationMapper.selectListByUserIdAndPinned(userId, false);
if (CollUtil.isEmpty(list)) {
return;
}
chatConversationMapper.deleteBatchIds(convertList(list, AiChatConversationDO::getId));
}
@Override
public PageResult<AiChatConversationDO> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) {
return chatConversationMapper.selectChatConversationPage(pageReqVO);
}
}

View File

@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.ai.service.chat;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
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.AiChatMessageDO;
import reactor.core.publisher.Flux;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* AI Service
*
* @author fansili
*/
public interface AiChatMessageService {
/**
*
*
* @param sendReqVO
* @param userId
* @return
*/
AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId);
/**
*
*
* @param sendReqVO
* @param userId
* @return
*/
Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId);
/**
*
*
* @param conversationId
* @return
*/
List<AiChatMessageDO> getChatMessageListByConversationId(Long conversationId);
/**
*
*
* @param id
* @param userId
*/
void deleteChatMessage(Long id, Long userId);
/**
*
*
* @param conversationId
* @param userId
*/
void deleteChatMessageByConversationId(Long conversationId, Long userId);
/**
*
*
* @param id
*/
void deleteChatMessageByAdmin(Long id);
/**
* Map
*
* @param conversationIds
* @return Map
*/
Map<Long, Integer> getChatMessageCountMap(Collection<Long> conversationIds);
/**
*
*
* @param pageReqVO
* @return
*/
PageResult<AiChatMessageDO> getChatMessagePage(AiChatMessagePageReqVO pageReqVO);
}

View File

@ -0,0 +1,259 @@
package cn.iocoder.yudao.module.ai.service.chat;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
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.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
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.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
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.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST;
/**
* AI Service
*
* @author fansili
*/
@Service
@Slf4j
public class AiChatMessageServiceImpl implements AiChatMessageService {
@Resource
private AiChatMessageMapper chatMessageMapper;
@Resource
private AiChatConversationService chatConversationService;
@Resource
private AiChatModelService chatModalService;
@Resource
private AiApiKeyService apiKeyService;
@Transactional(rollbackFor = Exception.class)
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
// 1.1 校验对话存在
AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId());
if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
throw exception(CHAT_CONVERSATION_NOT_EXISTS);
}
List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
// 1.2 校验模型
AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId());
// 2. 插入 user 发送消息
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext());
// 3.1 插入 assistant 接收消息
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
// 3.2 创建 chat 需要的 Prompt
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
ChatResponse chatResponse = chatModel.call(prompt);
// 3.3 段式返回
String newContent = chatResponse.getResult().getOutput().getContent();
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent));
}
@Override
public Flux<CommonResult<AiChatMessageSendRespVO>> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId) {
// 1.1 校验对话存在
AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId());
if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
throw exception(CHAT_CONVERSATION_NOT_EXISTS);
}
List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
// 1.2 校验模型
AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId());
// 2. 插入 user 发送消息
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext());
// 3.1 插入 assistant 接收消息
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
// 3.2 构建 Prompt并进行调用
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
// 3.3 流式返回
// TODO 注意Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
StringBuffer contentBuffer = new StringBuffer();
return streamResponse.map(chunk -> {
String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null;
newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
contentBuffer.append(newContent);
// 响应结果
return success(new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent)));
}).doOnComplete(() -> {
// 忽略租户,因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() ->
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())));
}).doOnError(throwable -> {
log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
// 忽略租户,因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() ->
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())));
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
}
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) {
// 1. 构建 Prompt Message 列表
List<Message> chatMessages = new ArrayList<>();
// 1.1 system context 角色设定
if (StrUtil.isNotBlank(conversation.getSystemMessage())) {
chatMessages.add(new SystemMessage(conversation.getSystemMessage()));
}
// 1.2 history message 历史消息
List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
// 1.3 user message 新发送消息
chatMessages.add(new UserMessage(sendReqVO.getContent()));
// 2. 构建 ChatOptions 对象
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
conversation.getTemperature(), conversation.getMaxTokens());
return new Prompt(chatMessages, chatOptions);
}
/**
* n
*
* n user + assistant
*
* @param messages
* @param conversation
* @param sendReqVO
* @return
*/
private List<AiChatMessageDO> filterContextMessages(List<AiChatMessageDO> messages,
AiChatConversationDO conversation,
AiChatMessageSendReqVO sendReqVO) {
if (conversation.getMaxContexts() == null || ObjUtil.notEqual(sendReqVO.getUseContext(), Boolean.TRUE)) {
return Collections.emptyList();
}
List<AiChatMessageDO> contextMessages = new ArrayList<>(conversation.getMaxContexts() * 2);
for (int i = messages.size() - 1; i >= 0; i--) {
AiChatMessageDO assistantMessage = CollUtil.get(messages, i);
if (assistantMessage == null || assistantMessage.getReplyId() == null) {
continue;
}
AiChatMessageDO userMessage = CollUtil.get(messages, i - 1);
if (userMessage == null || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId())
|| StrUtil.isEmpty(assistantMessage.getContent())) {
continue;
}
// 由于后续要 reverse 反转,所以先添加 assistantMessage
contextMessages.add(assistantMessage);
contextMessages.add(userMessage);
// 超过最大上下文,结束
if (contextMessages.size() >= conversation.getMaxContexts() * 2) {
break;
}
}
Collections.reverse(contextMessages);
return contextMessages;
}
private AiChatMessageDO createChatMessage(Long conversationId, Long replyId,
AiChatModelDO model, Long userId, Long roleId,
MessageType messageType, String content, Boolean useContext) {
AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId)
.setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId)
.setType(messageType.getValue()).setContent(content).setUseContext(useContext);
message.setCreateTime(LocalDateTime.now());
chatMessageMapper.insert(message);
return message;
}
@Override
public List<AiChatMessageDO> getChatMessageListByConversationId(Long conversationId) {
return chatMessageMapper.selectListByConversationId(conversationId);
}
@Override
public void deleteChatMessage(Long id, Long userId) {
// 1. 校验消息存在
AiChatMessageDO message = chatMessageMapper.selectById(id);
if (message == null || ObjUtil.notEqual(message.getUserId(), userId)) {
throw exception(CHAT_MESSAGE_NOT_EXIST);
}
// 2. 执行删除
chatMessageMapper.deleteById(id);
}
@Override
public void deleteChatMessageByConversationId(Long conversationId, Long userId) {
// 1. 校验消息存在
List<AiChatMessageDO> messages = chatMessageMapper.selectListByConversationId(conversationId);
if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) {
throw exception(CHAT_MESSAGE_NOT_EXIST);
}
// 2. 执行删除
chatMessageMapper.deleteBatchIds(convertList(messages, AiChatMessageDO::getId));
}
@Override
public void deleteChatMessageByAdmin(Long id) {
// 1. 校验消息存在
AiChatMessageDO message = chatMessageMapper.selectById(id);
if (message == null) {
throw exception(CHAT_MESSAGE_NOT_EXIST);
}
// 2. 执行删除
chatMessageMapper.deleteById(id);
}
@Override
public Map<Long, Integer> getChatMessageCountMap(Collection<Long> conversationIds) {
return chatMessageMapper.selectCountMapByConversationId(conversationIds);
}
@Override
public PageResult<AiChatMessageDO> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) {
return chatMessageMapper.selectPage(pageReqVO);
}
}

View File

@ -0,0 +1,121 @@
package cn.iocoder.yudao.module.ai.service.image;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
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 jakarta.validation.Valid;
import java.util.List;
/**
* AI Service
*
* @author fansili
*/
public interface AiImageService {
/**
*
*
* @param userId
* @param pageReqVO
* @return
*/
PageResult<AiImageDO> getImagePageMy(Long userId, PageParam pageReqVO);
/**
*
*
* @param id
* @return
*/
AiImageDO getImage(Long id);
/**
*
*
* @param ids
* @return
*/
List<AiImageDO> getImageList(List<Long> ids);
/**
*
*
* @param userId
* @param drawReqVO
* @return
*/
Long drawImage(Long userId, AiImageDrawReqVO drawReqVO);
/**
*
*
* @param id
* @param userId
*/
void deleteImageMy(Long id, Long userId);
/**
*
*
* @param pageReqVO
* @return
*/
PageResult<AiImageDO> getImagePage(AiImagePageReqVO pageReqVO);
/**
*
*
* @param updateReqVO
*/
void updateImage(@Valid AiImageUpdateReqVO updateReqVO);
/**
*
*
* @param id
*/
void deleteImage(Long id);
// ================ midjourney 专属 ================
/**
* Midjourney
*
* @param userId
* @param reqVO
* @return
*/
Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO);
/**
* Midjourney
*
* @return
*/
Integer midjourneySync();
/**
* Midjourney
*
* @param notify
*/
void midjourneyNotify(MidjourneyApi.Notify notify);
/**
* MidjourneyAction (U1U2...)
*
* @param userId
* @param reqVO
* @return
*/
Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO);
}

View File

@ -0,0 +1,350 @@
package cn.iocoder.yudao.module.ai.service.image;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.common.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.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.dal.mysql.image.AiImageMapper;
import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.infra.api.file.FileApi;
import com.alibaba.cloud.ai.tongyi.image.TongYiImagesOptions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImageOptions;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.qianfan.QianFanImageOptions;
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
/**
* AI Service
*
* @author fansili
*/
@Service
@Slf4j
public class AiImageServiceImpl implements AiImageService {
@Resource
private AiImageMapper imageMapper;
@Resource
private FileApi fileApi;
@Resource
private AiApiKeyService apiKeyService;
@Override
public PageResult<AiImageDO> getImagePageMy(Long userId, PageParam pageReqVO) {
return imageMapper.selectPage(userId, pageReqVO);
}
@Override
public AiImageDO getImage(Long id) {
return imageMapper.selectById(id);
}
@Override
public List<AiImageDO> getImageList(List<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return Collections.emptyList();
}
return imageMapper.selectBatchIds(ids);
}
@Override
public Long drawImage(Long userId, AiImageDrawReqVO drawReqVO) {
// 1. 保存数据库
AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false)
.setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus());
imageMapper.insert(image);
// 2. 异步绘制,后续前端通过返回的 id 进行轮询结果
getSelf().executeDrawImage(image, drawReqVO);
return image.getId();
}
@Async
public void executeDrawImage(AiImageDO image, AiImageDrawReqVO req) {
try {
// 1.1 构建请求
ImageOptions request = buildImageOptions(req);
// 1.2 执行请求
ImageModel imageModel = apiKeyService.getImageModel(AiPlatformEnum.validatePlatform(req.getPlatform()));
ImageResponse response = imageModel.call(new ImagePrompt(req.getPrompt(), request));
// 2. 上传到文件服务
String b64Json = response.getResult().getOutput().getB64Json();
byte[] fileContent = StrUtil.isNotEmpty(b64Json) ? Base64.decode(b64Json)
: HttpUtil.downloadBytes(response.getResult().getOutput().getUrl());
String filePath = fileApi.createFile(fileContent);
// 3. 更新数据库
imageMapper.updateById(new AiImageDO().setId(image.getId()).setStatus(AiImageStatusEnum.SUCCESS.getStatus())
.setPicUrl(filePath).setFinishTime(LocalDateTime.now()));
} catch (Exception ex) {
log.error("[doDall][image({}) 生成异常]", image, ex);
imageMapper.updateById(new AiImageDO().setId(image.getId())
.setStatus(AiImageStatusEnum.FAIL.getStatus())
.setErrorMessage(ex.getMessage()).setFinishTime(LocalDateTime.now()));
}
}
private static ImageOptions buildImageOptions(AiImageDrawReqVO draw) {
if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) {
// https://platform.openai.com/docs/api-reference/images/create
return OpenAiImageOptions.builder().withModel(draw.getModel())
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
.withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格
.withResponseFormat("b64_json")
.build();
} else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) {
// https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage
// https://platform.stability.ai/docs/api-reference#tag/Text-to-Image/operation/textToImage
return StabilityAiImageOptions.builder().withModel(draw.getModel())
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
.withSeed(Long.valueOf(draw.getOptions().get("seed")))
.withCfgScale(Float.valueOf(draw.getOptions().get("scale")))
.withSteps(Integer.valueOf(draw.getOptions().get("steps")))
.withSampler(String.valueOf(draw.getOptions().get("sampler")))
.withStylePreset(String.valueOf(draw.getOptions().get("stylePreset")))
.withClipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset")))
.build();
} else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) {
return TongYiImagesOptions.builder()
.withModel(draw.getModel()).withN(1)
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
.build();
} else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {
return QianFanImageOptions.builder()
.withModel(draw.getModel()).withN(1)
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
.build();
} else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) {
return ZhiPuAiImageOptions.builder()
.withModel(draw.getModel())
.build();
}
throw new IllegalArgumentException("不支持的 AI 平台:" + draw.getPlatform());
}
@Override
public void deleteImageMy(Long id, Long userId) {
// 1. 校验是否存在
AiImageDO image = validateImageExists(id);
if (ObjUtil.notEqual(image.getUserId(), userId)) {
throw exception(IMAGE_NOT_EXISTS);
}
// 2. 删除记录
imageMapper.deleteById(id);
}
@Override
public PageResult<AiImageDO> getImagePage(AiImagePageReqVO pageReqVO) {
return imageMapper.selectPage(pageReqVO);
}
@Override
public void updateImage(AiImageUpdateReqVO updateReqVO) {
// 1. 校验存在
validateImageExists(updateReqVO.getId());
// 2. 更新发布状态
imageMapper.updateById(BeanUtils.toBean(updateReqVO, AiImageDO.class));
}
@Override
public void deleteImage(Long id) {
// 1. 校验存在
validateImageExists(id);
// 2. 删除
imageMapper.deleteById(id);
}
private AiImageDO validateImageExists(Long id) {
AiImageDO image = imageMapper.selectById(id);
if (image == null) {
throw exception(IMAGE_NOT_EXISTS);
}
return image;
}
// ================ midjourney 专属 ================
@Override
@Transactional(rollbackFor = Exception.class)
public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) {
MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
// 1. 保存数据库
AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false)
.setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus())
.setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform());
imageMapper.insert(image);
// 2. 调用 Midjourney Proxy 提交任务
List<String> base64Array = StrUtil.isBlank(reqVO.getReferImageUrl()) ? null :
Collections.singletonList("data:image/jpeg;base64,".concat(Base64.encode(HttpUtil.downloadBytes(reqVO.getReferImageUrl()))));
MidjourneyApi.ImagineRequest imagineRequest = new MidjourneyApi.ImagineRequest(
base64Array, reqVO.getPrompt(),null,
MidjourneyApi.ImagineRequest.buildState(reqVO.getWidth(),
reqVO.getHeight(), reqVO.getVersion(), reqVO.getModel()));
MidjourneyApi.SubmitResponse imagineResponse = midjourneyApi.imagine(imagineRequest);
// 3. 情况一【失败】:抛出业务异常
if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(imagineResponse.code())) {
String description = imagineResponse.description().contains("quota_not_enough") ?
"账户余额不足" : imagineResponse.description();
throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
}
// 4. 情况二【成功】:更新 taskId 和参数
imageMapper.updateById(new AiImageDO().setId(image.getId())
.setTaskId(imagineResponse.result()).setOptions(BeanUtil.beanToMap(reqVO)));
return image.getId();
}
@Override
public Integer midjourneySync() {
MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
// 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image
List<AiImageDO> imageList = imageMapper.selectListByStatusAndPlatform(
AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform());
if (CollUtil.isEmpty(imageList)) {
return 0;
}
// 1.2 调用 Midjourney Proxy 获取任务进展
List<MidjourneyApi.Notify> taskList = midjourneyApi.getTaskList(convertSet(imageList, AiImageDO::getTaskId));
Map<String, MidjourneyApi.Notify> taskMap = convertMap(taskList, MidjourneyApi.Notify::id);
// 2. 逐个处理,更新进展
int count = 0;
for (AiImageDO image : imageList) {
MidjourneyApi.Notify notify = taskMap.get(image.getTaskId());
if (notify == null) {
log.error("[midjourneySync][image({}) 查询不到进展]", image);
continue;
}
count++;
updateMidjourneyStatus(image, notify);
}
return count;
}
@Override
public void midjourneyNotify(MidjourneyApi.Notify notify) {
// 1. 校验 image 存在
AiImageDO image = imageMapper.selectByTaskId(notify.id());
if (image == null) {
log.warn("[midjourneyNotify][回调任务({}) 不存在]", notify.id());
return;
}
// 2. 更新状态
updateMidjourneyStatus(image, notify);
}
private void updateMidjourneyStatus(AiImageDO image, MidjourneyApi.Notify notify) {
// 1. 转换状态
Integer status = null;
LocalDateTime finishTime = null;
if (StrUtil.isNotBlank(notify.status())) {
MidjourneyApi.TaskStatusEnum taskStatusEnum = MidjourneyApi.TaskStatusEnum.valueOf(notify.status());
if (MidjourneyApi.TaskStatusEnum.SUCCESS == taskStatusEnum) {
status = AiImageStatusEnum.SUCCESS.getStatus();
finishTime = LocalDateTime.now();
} else if (MidjourneyApi.TaskStatusEnum.FAILURE == taskStatusEnum) {
status = AiImageStatusEnum.FAIL.getStatus();
finishTime = LocalDateTime.now();
}
}
// 2. 上传图片
String picUrl = null;
if (StrUtil.isNotBlank(notify.imageUrl())) {
try {
picUrl = fileApi.createFile(HttpUtil.downloadBytes(notify.imageUrl()));
} catch (Exception e) {
picUrl = notify.imageUrl();
log.warn("[updateMidjourneyStatus][图片({}) 地址({}) 上传失败]", image.getId(), notify.imageUrl(), e);
}
}
// 3. 更新 image 状态
imageMapper.updateById(new AiImageDO().setId(image.getId()).setStatus(status)
.setPicUrl(picUrl).setButtons(notify.buttons()).setErrorMessage(notify.failReason())
.setFinishTime(finishTime));
}
@Override
public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) {
MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
// 1.1 检查 image
AiImageDO image = validateImageExists(reqVO.getId());
if (ObjUtil.notEqual(userId, image.getUserId())) {
throw exception(IMAGE_NOT_EXISTS);
}
// 1.2 检查 customId
MidjourneyApi.Button button = CollUtil.findOne(image.getButtons(),
buttonX -> buttonX.customId().equals(reqVO.getCustomId()));
if (button == null) {
throw exception(IMAGE_CUSTOM_ID_NOT_EXISTS);
}
// 2. 调用 Midjourney Proxy 提交任务
MidjourneyApi.SubmitResponse actionResponse = midjourneyApi.action(
new MidjourneyApi.ActionRequest(button.customId(), image.getTaskId(), null));
if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(actionResponse.code())) {
String description = actionResponse.description().contains("quota_not_enough") ?
"账户余额不足" : actionResponse.description();
throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
}
// 3. 新增 image 记录
AiImageDO newImage = new AiImageDO().setUserId(image.getUserId()).setPublicStatus(false).setPrompt(image.getPrompt())
.setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus())
.setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform())
.setModel(image.getModel()).setWidth(image.getWidth()).setHeight(image.getHeight())
.setOptions(image.getOptions()).setTaskId(actionResponse.result());
imageMapper.insert(newImage);
return newImage.getId();
}
/**
* AOP
*
* @return
*/
private AiImageServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.ai.service.mindmap;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
import reactor.core.publisher.Flux;
/**
* AI Service
*
* @author xiaoxin
*/
public interface AiMindMapService {
/**
*
*
* @param generateReqVO
* @param userId
* @return
*/
Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId);
}

View File

@ -0,0 +1,134 @@
package cn.iocoder.yudao.module.ai.service.mindmap;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import cn.iocoder.yudao.module.ai.dal.mysql.mindmap.AiMindMapMapper;
import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum;
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* AI Service
*
* @author xiaoxin
*/
@Service
@Slf4j
public class AiMindMapServiceImpl implements AiMindMapService {
@Resource
private AiApiKeyService apiKeyService;
@Resource
private AiChatModelService chatModalService;
@Resource
private AiChatRoleService chatRoleService;
@Resource
private AiMindMapMapper mindMapMapper;
@Override
public Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId) {
// 1. 获取脑图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
AiChatRoleDO role = CollUtil.getFirst(
chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName()));
// 1.1 获取脑图执行模型
AiChatModelDO model = getModel(role);
// 1.2 获取角色设定消息
String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage())
? role.getSystemMessage() : AiChatRoleEnum.AI_MIND_MAP_ROLE.getSystemMessage();
// 1.3 校验平台
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId());
// 2. 插入思维导图信息
AiMindMapDO mindMapDO = BeanUtils.toBean(generateReqVO, AiMindMapDO.class,
mindMap -> mindMap.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()));
mindMapMapper.insert(mindMapDO);
// 3.1 构建 Prompt并进行调用
Prompt prompt = buildPrompt(generateReqVO, model, systemMessage);
Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
// 3.2 流式返回
StringBuffer contentBuffer = new StringBuffer();
return streamResponse.map(chunk -> {
String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null;
newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
contentBuffer.append(newContent);
// 响应结果
return success(newContent);
}).doOnComplete(() -> {
// 忽略租户,因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() ->
mindMapMapper.updateById(new AiMindMapDO().setId(mindMapDO.getId()).setGeneratedContent(contentBuffer.toString())));
}).doOnError(throwable -> {
log.error("[generateWriteContent][generateReqVO({}) 发生异常]", generateReqVO, throwable);
// 忽略租户,因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() ->
mindMapMapper.updateById(new AiMindMapDO().setId(mindMapDO.getId()).setErrorMessage(throwable.getMessage())));
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR)));
}
private Prompt buildPrompt(AiMindMapGenerateReqVO generateReqVO, AiChatModelDO model, String systemMessage) {
// 1. 构建 message 列表
List<Message> chatMessages = buildMessages(generateReqVO, systemMessage);
// 2. 构建 options 对象
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
ChatOptions options = AiUtils.buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
return new Prompt(chatMessages, options);
}
private static List<Message> buildMessages(AiMindMapGenerateReqVO generateReqVO, String systemMessage) {
List<Message> chatMessages = new ArrayList<>();
// 1. 角色设定
if (StrUtil.isNotBlank(systemMessage)) {
chatMessages.add(new SystemMessage(systemMessage));
}
// 2. 用户输入
chatMessages.add(new UserMessage(generateReqVO.getPrompt()));
return chatMessages;
}
private AiChatModelDO getModel(AiChatRoleDO role) {
AiChatModelDO model = null;
if (role != null && role.getModelId() != null) {
model = chatModalService.getChatModel(role.getModelId());
}
if (model != null) {
model = chatModalService.getRequiredDefaultChatModel();
}
Assert.notNull(model, "[AI] 获取不到模型");
return model;
}
}

View File

@ -0,0 +1,114 @@
package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import jakarta.validation.Valid;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.image.ImageModel;
import java.util.List;
/**
* AI API Service
*
* @author
*/
public interface AiApiKeyService {
/**
* API
*
* @param createReqVO
* @return
*/
Long createApiKey(@Valid AiApiKeySaveReqVO createReqVO);
/**
* API
*
* @param updateReqVO
*/
void updateApiKey(@Valid AiApiKeySaveReqVO updateReqVO);
/**
* API
*
* @param id
*/
void deleteApiKey(Long id);
/**
* API
*
* @param id
* @return API
*/
AiApiKeyDO getApiKey(Long id);
/**
* API
*
* @param id
* @return API
*/
AiApiKeyDO validateApiKey(Long id);
/**
* API
*
* @param pageReqVO
* @return API
*/
PageResult<AiApiKeyDO> getApiKeyPage(AiApiKeyPageReqVO pageReqVO);
/**
* API
*
* @return API
*/
List<AiApiKeyDO> getApiKeyList();
// ========== 与 spring-ai 集成 ==========
/**
* ChatModel
*
* @param id
* @return ChatModel
*/
ChatModel getChatModel(Long id);
/**
* ImageModel
*
* TODO platform
*
* @param platform
* @return ImageModel
*/
ImageModel getImageModel(AiPlatformEnum platform);
/**
* MidjourneyApi
*
* TODO Midjourney
*
* @return MidjourneyApi
*/
MidjourneyApi getMidjourneyApi();
/**
* SunoApi
*
* TODO Suno
*
* @return SunoApi
*/
SunoApi getSunoApi();
}

View File

@ -0,0 +1,135 @@
package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.AiApiKeySaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
/**
* AI API Service
*
* @author
*/
@Service
@Validated
public class AiApiKeyServiceImpl implements AiApiKeyService {
@Resource
private AiApiKeyMapper apiKeyMapper;
@Resource
private AiModelFactory modelFactory;
@Override
public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
// 插入
AiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);
apiKeyMapper.insert(apiKey);
// 返回
return apiKey.getId();
}
@Override
public void updateApiKey(AiApiKeySaveReqVO updateReqVO) {
// 校验存在
validateApiKeyExists(updateReqVO.getId());
// 更新
AiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);
apiKeyMapper.updateById(updateObj);
}
@Override
public void deleteApiKey(Long id) {
// 校验存在
validateApiKeyExists(id);
// 删除
apiKeyMapper.deleteById(id);
}
private AiApiKeyDO validateApiKeyExists(Long id) {
AiApiKeyDO apiKey = apiKeyMapper.selectById(id);
if (apiKey == null) {
throw exception(API_KEY_NOT_EXISTS);
}
return apiKey;
}
@Override
public AiApiKeyDO getApiKey(Long id) {
return apiKeyMapper.selectById(id);
}
@Override
public AiApiKeyDO validateApiKey(Long id) {
AiApiKeyDO apiKey = validateApiKeyExists(id);
if (CommonStatusEnum.isDisable(apiKey.getStatus())) {
throw exception(API_KEY_DISABLE);
}
return apiKey;
}
@Override
public PageResult<AiApiKeyDO> getApiKeyPage(AiApiKeyPageReqVO pageReqVO) {
return apiKeyMapper.selectPage(pageReqVO);
}
@Override
public List<AiApiKeyDO> getApiKeyList() {
return apiKeyMapper.selectList();
}
// ========== 与 spring-ai 集成 ==========
@Override
public ChatModel getChatModel(Long id) {
AiApiKeyDO apiKey = validateApiKey(id);
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl());
}
@Override
public ImageModel getImageModel(AiPlatformEnum platform) {
AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
if (apiKey == null) {
throw exception(API_KEY_IMAGE_NODE_FOUND, platform.getName());
}
return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl());
}
@Override
public MidjourneyApi getMidjourneyApi() {
AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
if (apiKey == null) {
throw exception(API_KEY_MIDJOURNEY_NOT_FOUND);
}
return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl());
}
@Override
public SunoApi getSunoApi() {
AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
if (apiKey == null) {
throw exception(API_KEY_SUNO_NOT_FOUND);
}
return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl());
}
}

View File

@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* AI Service
*
* @author fansili
* @since 2024/4/24 19:42
*/
public interface AiChatModelService {
/**
*
*
* @param createReqVO
* @return
*/
Long createChatModel(@Valid AiChatModelSaveReqVO createReqVO);
/**
*
*
* @param updateReqVO
*/
void updateChatModel(@Valid AiChatModelSaveReqVO updateReqVO);
/**
*
*
* @param id
*/
void deleteChatModel(Long id);
/**
*
*
* @param id
* @return
*/
AiChatModelDO getChatModel(Long id);
/**
*
*
* {@link cn.iocoder.yudao.framework.common.exception.ServiceException}
*
* @return
*/
AiChatModelDO getRequiredDefaultChatModel();
/**
*
*
* @param pageReqVO
* @return
*/
PageResult<AiChatModelDO> getChatModelPage(AiChatModelPageReqVO pageReqVO);
/**
*
*
* @param id
* @return
*/
AiChatModelDO validateChatModel(Long id);
/**
*
*
* @param status
* @return
*/
List<AiChatModelDO> getChatModelListByStatus(Integer status);
/**
*
*
* @param ids
* @return
*/
List<AiChatModelDO> getChatModelList(Collection<Long> ids);
}

View File

@ -0,0 +1,114 @@
package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.AiChatModelSaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatModelMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
/**
* AI Service
*
* @author fansili
*/
@Service
@Validated
public class AiChatModelServiceImpl implements AiChatModelService {
@Resource
private AiApiKeyService apiKeyService;
@Resource
private AiChatModelMapper chatModelMapper;
@Override
public Long createChatModel(AiChatModelSaveReqVO createReqVO) {
// 1. 校验
AiPlatformEnum.validatePlatform(createReqVO.getPlatform());
apiKeyService.validateApiKey(createReqVO.getKeyId());
// 2. 插入
AiChatModelDO chatModel = BeanUtils.toBean(createReqVO, AiChatModelDO.class);
chatModelMapper.insert(chatModel);
return chatModel.getId();
}
@Override
public void updateChatModel(AiChatModelSaveReqVO updateReqVO) {
// 1. 校验
validateChatModelExists(updateReqVO.getId());
AiPlatformEnum.validatePlatform(updateReqVO.getPlatform());
apiKeyService.validateApiKey(updateReqVO.getKeyId());
// 2. 更新
AiChatModelDO updateObj = BeanUtils.toBean(updateReqVO, AiChatModelDO.class);
chatModelMapper.updateById(updateObj);
}
@Override
public void deleteChatModel(Long id) {
// 校验存在
validateChatModelExists(id);
// 删除
chatModelMapper.deleteById(id);
}
private AiChatModelDO validateChatModelExists(Long id) {
AiChatModelDO model = chatModelMapper.selectById(id);
if (chatModelMapper.selectById(id) == null) {
throw exception(CHAT_MODEL_NOT_EXISTS);
}
return model;
}
@Override
public AiChatModelDO getChatModel(Long id) {
return chatModelMapper.selectById(id);
}
@Override
public AiChatModelDO getRequiredDefaultChatModel() {
AiChatModelDO model = chatModelMapper.selectFirstByStatus(CommonStatusEnum.ENABLE.getStatus());
if (model == null) {
throw exception(CHAT_MODEL_DEFAULT_NOT_EXISTS);
}
return model;
}
@Override
public PageResult<AiChatModelDO> getChatModelPage(AiChatModelPageReqVO pageReqVO) {
return chatModelMapper.selectPage(pageReqVO);
}
@Override
public AiChatModelDO validateChatModel(Long id) {
AiChatModelDO model = validateChatModelExists(id);
if (CommonStatusEnum.isDisable(model.getStatus())) {
throw exception(CHAT_MODEL_DISABLE);
}
return model;
}
@Override
public List<AiChatModelDO> getChatModelListByStatus(Integer status) {
return chatModelMapper.selectList(status);
}
@Override
public List<AiChatModelDO> getChatModelList(Collection<Long> ids) {
return chatModelMapper.selectBatchIds(ids);
}
}

Some files were not shown because too many files have changed in this diff Show More