【模块新增】AI:支持通义千问、文心一言、讯飞星火、智谱、DeepSeek 等国内外大模型能力
This commit is contained in:
95
yudao-module-ai/yudao-module-ai-biz/pom.xml
Normal file
95
yudao-module-ai/yudao-module-ai-biz/pom.xml
Normal 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>
|
||||
@@ -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/ 文章
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
### 生成图片:OpenAI(DALL)
|
||||
POST {{baseUrl}}/ai/image/draw
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
|
||||
{
|
||||
"platform": "OpenAI",
|
||||
"prompt": "可爱的小喵星人",
|
||||
"model": "dall-e-3",
|
||||
"height": "1024",
|
||||
"width": "1024",
|
||||
"options": {
|
||||
"style": "vivid"
|
||||
}
|
||||
}
|
||||
|
||||
### 生成图片:StableDiffusion
|
||||
POST {{baseUrl}}/ai/image/draw
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
|
||||
{
|
||||
"platform": "StableDiffusion",
|
||||
"prompt": "中国长城",
|
||||
"model": "stable-diffusion-v1-6",
|
||||
"height": "1024",
|
||||
"width": "1024",
|
||||
"style": "vivid"
|
||||
}
|
||||
|
||||
### 生成图片:生成图片(Midjourney)
|
||||
POST {{baseUrl}}/ai/image/midjourney/imagine
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
|
||||
{
|
||||
"prompt": "中国旗袍",
|
||||
"model": "midjourney",
|
||||
"width": "1",
|
||||
"height": "1",
|
||||
"version": "6.0"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import org.springframework.ai.openai.OpenAiImageOptions;
|
||||
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "管理后台 - AI 绘画 Request VO")
|
||||
@Data
|
||||
public class AiImageDrawReqVO {
|
||||
|
||||
@Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
|
||||
private String platform; // 参见 AiPlatformEnum 枚举
|
||||
|
||||
@Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "画一个长城")
|
||||
@NotEmpty(message = "提示词不能为空")
|
||||
@Size(max = 1200, message = "提示词最大 1200")
|
||||
private String prompt;
|
||||
|
||||
@Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "stable-diffusion-v1-6")
|
||||
@NotEmpty(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 1. dall-e-2 模型:256x256、512x512、1024x1024
|
||||
* 2. dall-e-3 模型:1024x1024, 1792x1024, 或 1024x1792
|
||||
*/
|
||||
@Schema(description = "图片高度")
|
||||
@NotNull(message = "图片高度不能为空")
|
||||
private Integer height;
|
||||
|
||||
@Schema(description = "图片宽度")
|
||||
@NotNull(message = "图片宽度不能为空")
|
||||
private Integer width;
|
||||
|
||||
// ========== 各平台绘画的拓展参数 ==========
|
||||
|
||||
/**
|
||||
* 绘制参数,不同 platform 的不同参数
|
||||
*
|
||||
* 1. {@link OpenAiImageOptions}
|
||||
* 2. {@link StabilityAiImageOptions}
|
||||
*/
|
||||
@Schema(description = "绘制参数")
|
||||
private Map<String, String> options;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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())));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* TODO 芋艿:站位,无特殊作用
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.controller.app;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 属于 ai 模块的 framework 封装
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.framework;
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.framework.rpc;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.framework.security.core;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
|
||||
* 目前已接入各种模型,不限于:
|
||||
* 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
|
||||
* 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||
*
|
||||
* 1. Controller URL:以 /ai/ 开头,避免和其它 Module 冲突
|
||||
* 2. DataObject 表名:以 ai_ 开头,方便在数据库中区分
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai;
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 【Midjourney】Action 操作(放大、缩小、U1、U2...)
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param reqVO 绘制请求
|
||||
* @return 绘画编号
|
||||
*/
|
||||
Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO);
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
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.chatRole.AiChatRolePageReqVO;
|
||||
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 jakarta.validation.Valid;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
|
||||
|
||||
/**
|
||||
* AI 聊天角色 Service 接口
|
||||
*
|
||||
* @author fansili
|
||||
*/
|
||||
public interface AiChatRoleService {
|
||||
|
||||
/**
|
||||
* 创建聊天角色
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createChatRole(@Valid AiChatRoleSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 创建【我的】聊天角色
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @param userId 用户编号
|
||||
* @return 编号
|
||||
*/
|
||||
Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 更新聊天角色
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateChatRole(@Valid AiChatRoleSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 创建【我的】聊天角色
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
* @param userId 用户编号
|
||||
*/
|
||||
void updateChatRoleMy(AiChatRoleSaveMyReqVO updateReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 删除聊天角色
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteChatRole(Long id);
|
||||
|
||||
/**
|
||||
* 删除【我的】聊天角色
|
||||
*
|
||||
* @param id 编号
|
||||
* @param userId 用户编号
|
||||
*/
|
||||
void deleteChatRoleMy(Long id, Long userId);
|
||||
|
||||
/**
|
||||
* 获得聊天角色
|
||||
*
|
||||
* @param id 编号
|
||||
* @return AI 聊天角色
|
||||
*/
|
||||
AiChatRoleDO getChatRole(Long id);
|
||||
|
||||
/**
|
||||
* 获得聊天角色列表
|
||||
*
|
||||
* @param ids 编号数组
|
||||
* @return 聊天角色列表
|
||||
*/
|
||||
List<AiChatRoleDO> getChatRoleList(Collection<Long> ids);
|
||||
|
||||
default Map<Long, AiChatRoleDO> getChatRoleMap(Collection<Long> ids) {
|
||||
return convertMap(getChatRoleList(ids), AiChatRoleDO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验聊天角色是否合法
|
||||
*
|
||||
* @param id 角色编号
|
||||
*/
|
||||
AiChatRoleDO validateChatRole(Long id);
|
||||
|
||||
/**
|
||||
* 获得聊天角色分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 聊天角色分页
|
||||
*/
|
||||
PageResult<AiChatRoleDO> getChatRolePage(AiChatRolePageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获得【我的】聊天角色分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @param userId 用户编号
|
||||
* @return 聊天角色分页
|
||||
*/
|
||||
PageResult<AiChatRoleDO> getChatRoleMyPage(AiChatRolePageReqVO pageReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 获得聊天角色的分类列表
|
||||
*
|
||||
* @return 分类列表
|
||||
*/
|
||||
List<String> getChatRoleCategoryList();
|
||||
|
||||
/**
|
||||
* 根据名字获得聊天角色
|
||||
*
|
||||
* @param name 名字
|
||||
* @return 聊天角色列表
|
||||
*/
|
||||
List<AiChatRoleDO> getChatRoleListByName(String name);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package cn.iocoder.yudao.module.ai.service.model;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.chatRole.AiChatRolePageReqVO;
|
||||
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.dal.mysql.model.AiChatRoleMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
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.*;
|
||||
|
||||
/**
|
||||
* AI 聊天角色 Service 实现类
|
||||
*
|
||||
* @author fansili
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiChatRoleServiceImpl implements AiChatRoleService {
|
||||
|
||||
@Resource
|
||||
private AiChatRoleMapper chatRoleMapper;
|
||||
|
||||
@Override
|
||||
public Long createChatRole(AiChatRoleSaveReqVO createReqVO) {
|
||||
AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class);
|
||||
chatRoleMapper.insert(chatRole);
|
||||
return chatRole.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId) {
|
||||
AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class).setUserId(userId)
|
||||
.setStatus(CommonStatusEnum.ENABLE.getStatus()).setPublicStatus(false);
|
||||
chatRoleMapper.insert(chatRole);
|
||||
return chatRole.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateChatRole(AiChatRoleSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateChatRoleExists(updateReqVO.getId());
|
||||
// 更新
|
||||
AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class);
|
||||
chatRoleMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateChatRoleMy(AiChatRoleSaveMyReqVO updateReqVO, Long userId) {
|
||||
// 校验存在
|
||||
AiChatRoleDO chatRole = validateChatRoleExists(updateReqVO.getId());
|
||||
if (ObjectUtil.notEqual(chatRole.getUserId(), userId)) {
|
||||
throw exception(CHAT_ROLE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 更新
|
||||
AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class);
|
||||
chatRoleMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChatRole(Long id) {
|
||||
// 校验存在
|
||||
validateChatRoleExists(id);
|
||||
// 删除
|
||||
chatRoleMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteChatRoleMy(Long id, Long userId) {
|
||||
// 校验存在
|
||||
AiChatRoleDO chatRole = validateChatRoleExists(id);
|
||||
if (ObjectUtil.notEqual(chatRole.getUserId(), userId)) {
|
||||
throw exception(CHAT_ROLE_NOT_EXISTS);
|
||||
}
|
||||
// 删除
|
||||
chatRoleMapper.deleteById(id);
|
||||
}
|
||||
|
||||
private AiChatRoleDO validateChatRoleExists(Long id) {
|
||||
AiChatRoleDO chatRole = chatRoleMapper.selectById(id);
|
||||
if (chatRole == null) {
|
||||
throw exception(CHAT_ROLE_NOT_EXISTS);
|
||||
}
|
||||
return chatRole;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiChatRoleDO getChatRole(Long id) {
|
||||
return chatRoleMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiChatRoleDO> getChatRoleList(Collection<Long> ids) {
|
||||
if (CollUtil.isEmpty(ids)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return chatRoleMapper.selectBatchIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiChatRoleDO validateChatRole(Long id) {
|
||||
AiChatRoleDO chatRole = validateChatRoleExists(id);
|
||||
if (CommonStatusEnum.isDisable(chatRole.getStatus())) {
|
||||
throw exception(CHAT_ROLE_DISABLE, chatRole.getName());
|
||||
}
|
||||
return chatRole;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiChatRoleDO> getChatRolePage(AiChatRolePageReqVO pageReqVO) {
|
||||
return chatRoleMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiChatRoleDO> getChatRoleMyPage(AiChatRolePageReqVO pageReqVO, Long userId) {
|
||||
return chatRoleMapper.selectPageByMy(pageReqVO, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getChatRoleCategoryList() {
|
||||
List<AiChatRoleDO> list = chatRoleMapper.selectListGroupByCategory(CommonStatusEnum.ENABLE.getStatus());
|
||||
return convertList(list, AiChatRoleDO::getCategory, role -> role != null && StrUtil.isNotBlank(role.getCategory()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AiChatRoleDO> getChatRoleListByName(String name) {
|
||||
return chatRoleMapper.selectListByName(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package cn.iocoder.yudao.module.ai.service.music;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.*;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 音乐 Service 接口
|
||||
*
|
||||
* @author xiaoxin
|
||||
*/
|
||||
public interface AiMusicService {
|
||||
|
||||
/**
|
||||
* 音乐生成
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param reqVO 请求参数
|
||||
* @return 生成的音乐ID
|
||||
*/
|
||||
List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 同步音乐任务
|
||||
*
|
||||
* @return 同步数量
|
||||
*/
|
||||
Integer syncMusic();
|
||||
|
||||
/**
|
||||
* 更新音乐发布状态
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 更新我的音乐
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateMyMusic(@Valid AiMusicUpdateMyReqVO updateReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 删除AI 音乐
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteMusic(Long id);
|
||||
|
||||
/**
|
||||
* 删除【我的】音乐记录
|
||||
*
|
||||
* @param id 音乐编号
|
||||
* @param userId 用户编号
|
||||
*/
|
||||
void deleteMusicMy(Long id, Long userId);
|
||||
|
||||
/**
|
||||
* 获得AI 音乐
|
||||
*
|
||||
* @param id 音乐编号
|
||||
* @return 音乐内容
|
||||
*/
|
||||
AiMusicDO getMusic(Long id);
|
||||
|
||||
/**
|
||||
* 获得音乐分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 音乐分页
|
||||
*/
|
||||
PageResult<AiMusicDO> getMusicPage(AiMusicPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获得【我的】音乐分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @param userId 用户编号
|
||||
* @return 音乐分页
|
||||
*/
|
||||
PageResult<AiMusicDO> getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package cn.iocoder.yudao.module.ai.service.music;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.text.StrPool;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
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.music.vo.AiMusicPageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateMyReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper;
|
||||
import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
|
||||
import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
|
||||
import cn.iocoder.yudao.module.infra.api.file.FileApi;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
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.framework.common.util.collection.CollectionUtils.convertMap;
|
||||
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.IMAGE_NOT_EXISTS;
|
||||
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXISTS;
|
||||
|
||||
/**
|
||||
* AI 音乐 Service 实现类
|
||||
*
|
||||
* @author xiaoxin
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiMusicServiceImpl implements AiMusicService {
|
||||
|
||||
@Resource
|
||||
private AiApiKeyService apiKeyService;
|
||||
|
||||
@Resource
|
||||
private AiMusicMapper musicMapper;
|
||||
|
||||
@Resource
|
||||
private FileApi fileApi;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) {
|
||||
// 1. 调用 Suno 生成音乐
|
||||
SunoApi sunoApi = apiKeyService.getSunoApi();
|
||||
List<SunoApi.MusicData> musicDataList;
|
||||
if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
|
||||
// 1.1 描述模式
|
||||
SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
|
||||
reqVO.getPrompt(), reqVO.getModel(), reqVO.getMakeInstrumental());
|
||||
musicDataList = sunoApi.generate(generateRequest);
|
||||
} else if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
|
||||
// 1.2 歌词模式
|
||||
SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
|
||||
reqVO.getPrompt(), reqVO.getModel(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
|
||||
musicDataList = sunoApi.customGenerate(generateRequest);
|
||||
} else {
|
||||
throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO));
|
||||
}
|
||||
|
||||
// 2. 插入数据库
|
||||
if (CollUtil.isEmpty(musicDataList)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<AiMusicDO> musicList = buildMusicDOList(musicDataList);
|
||||
musicList.forEach(music -> music.setUserId(userId).setPlatform(reqVO.getPlatform()).setGenerateMode(reqVO.getGenerateMode()));
|
||||
musicMapper.insertBatch(musicList);
|
||||
return convertList(musicList, AiMusicDO::getId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer syncMusic() {
|
||||
List<AiMusicDO> streamingTask = musicMapper.selectListByStatus(AiMusicStatusEnum.IN_PROGRESS.getStatus());
|
||||
if (CollUtil.isEmpty(streamingTask)) {
|
||||
return 0;
|
||||
}
|
||||
log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size());
|
||||
|
||||
// GET 请求,为避免参数过长,分批次处理
|
||||
SunoApi sunoApi = apiKeyService.getSunoApi();
|
||||
CollUtil.split(streamingTask, 36).forEach(chunkList -> {
|
||||
Map<String, Long> taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId);
|
||||
List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet()));
|
||||
if (CollUtil.isEmpty(musicTaskList)) {
|
||||
log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet());
|
||||
return;
|
||||
}
|
||||
// 更新进度
|
||||
List<AiMusicDO> updateMusicList = buildMusicDOList(musicTaskList);
|
||||
updateMusicList.forEach(music -> music.setId(taskIdMap.get(music.getTaskId())));
|
||||
musicMapper.updateBatch(updateMusicList);
|
||||
});
|
||||
return streamingTask.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMusic(AiMusicUpdateReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateMusicExists(updateReqVO.getId());
|
||||
// 更新
|
||||
musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setPublicStatus(updateReqVO.getPublicStatus()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMyMusic(AiMusicUpdateMyReqVO updateReqVO, Long userId) {
|
||||
// 校验音乐是否存在
|
||||
AiMusicDO musicDO = validateMusicExists(updateReqVO.getId());
|
||||
if (ObjUtil.notEqual(musicDO.getUserId(), userId)) {
|
||||
throw exception(MUSIC_NOT_EXISTS);
|
||||
}
|
||||
// 更新
|
||||
musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setTitle(updateReqVO.getTitle()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMusic(Long id) {
|
||||
// 校验存在
|
||||
validateMusicExists(id);
|
||||
// 删除
|
||||
musicMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMusicMy(Long id, Long userId) {
|
||||
// 1. 校验是否存在
|
||||
AiMusicDO music = validateMusicExists(id);
|
||||
if (ObjUtil.notEqual(music.getUserId(), userId)) {
|
||||
throw exception(IMAGE_NOT_EXISTS);
|
||||
}
|
||||
// 2. 删除记录
|
||||
musicMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiMusicDO getMusic(Long id) {
|
||||
return musicMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiMusicDO> getMusicPage(AiMusicPageReqVO pageReqVO) {
|
||||
return musicMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiMusicDO> getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId) {
|
||||
return musicMapper.selectPageByMy(pageReqVO, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 AiMusicDO 集合
|
||||
*
|
||||
* @param musicList suno 音乐任务列表
|
||||
* @return AiMusicDO 集合
|
||||
*/
|
||||
private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
|
||||
return convertList(musicList, musicData -> {
|
||||
Integer status = Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.SUCCESS.getStatus()
|
||||
: Objects.equals("error", musicData.status()) ? AiMusicStatusEnum.FAIL.getStatus()
|
||||
: AiMusicStatusEnum.IN_PROGRESS.getStatus();
|
||||
return new AiMusicDO()
|
||||
.setTaskId(musicData.id()).setModel(musicData.modelName())
|
||||
.setDescription(musicData.gptDescriptionPrompt())
|
||||
.setAudioUrl(downloadFile(status, musicData.audioUrl()))
|
||||
.setVideoUrl(downloadFile(status, musicData.videoUrl()))
|
||||
.setImageUrl(downloadFile(status, musicData.imageUrl()))
|
||||
.setTitle(musicData.title()).setDuration(musicData.duration())
|
||||
.setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
|
||||
.setErrorMessage(musicData.errorMessage())
|
||||
.setStatus(status);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 音乐生成好后,将音频文件上传到文件服务器
|
||||
*
|
||||
* @param status 音乐状态
|
||||
* @param url 音频文件地址
|
||||
* @return 内部文件地址
|
||||
*/
|
||||
private String downloadFile(Integer status, String url) {
|
||||
if (StrUtil.isBlank(url) || ObjectUtil.notEqual(status, AiMusicStatusEnum.SUCCESS.getStatus())) {
|
||||
return url;
|
||||
}
|
||||
try {
|
||||
byte[] bytes = HttpUtil.downloadBytes(url);
|
||||
return fileApi.createFile(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error("[downloadFile][url({}) 下载失败]", url, e);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验音乐是否存在
|
||||
*
|
||||
* @param id 音乐编号
|
||||
* @return 音乐信息
|
||||
*/
|
||||
private AiMusicDO validateMusicExists(Long id) {
|
||||
AiMusicDO music = musicMapper.selectById(id);
|
||||
if (music == null) {
|
||||
throw exception(MUSIC_NOT_EXISTS);
|
||||
}
|
||||
return music;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.iocoder.yudao.module.ai.service.write;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
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.dal.dataobject.write.AiWriteDO;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* AI 写作 Service 接口
|
||||
*
|
||||
* @author xiaoxin
|
||||
*/
|
||||
public interface AiWriteService {
|
||||
|
||||
/**
|
||||
* 生成写作内容
|
||||
*
|
||||
* @param generateReqVO 作文生成请求参数
|
||||
* @param userId 用户编号
|
||||
* @return 生成结果
|
||||
*/
|
||||
Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 删除写作
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteWrite(Long id);
|
||||
|
||||
/**
|
||||
* 获得写作分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return AI 写作分页
|
||||
*/
|
||||
PageResult<AiWriteDO> getWritePage(AiWritePageReqVO pageReqVO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package cn.iocoder.yudao.module.ai.service.write;
|
||||
|
||||
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.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.write.vo.AiWriteGenerateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO;
|
||||
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.dataobject.write.AiWriteDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.write.AiWriteMapper;
|
||||
import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum;
|
||||
import cn.iocoder.yudao.module.ai.enums.DictTypeConstants;
|
||||
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum;
|
||||
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 cn.iocoder.yudao.module.system.api.dict.DictDataApi;
|
||||
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.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 reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
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.module.ai.enums.ErrorCodeConstants.WRITE_NOT_EXISTS;
|
||||
|
||||
/**
|
||||
* AI 写作 Service 实现类
|
||||
*
|
||||
* @author xiaoxin
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiWriteServiceImpl implements AiWriteService {
|
||||
|
||||
@Resource
|
||||
private AiApiKeyService apiKeyService;
|
||||
@Resource
|
||||
private AiChatModelService chatModalService;
|
||||
@Resource
|
||||
private AiChatRoleService chatRoleService;
|
||||
|
||||
@Resource
|
||||
private DictDataApi dictDataApi;
|
||||
|
||||
@Resource
|
||||
private AiWriteMapper writeMapper;
|
||||
|
||||
@Override
|
||||
public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) {
|
||||
// 1 获取写作模型。尝试获取写作助手角色,没有则使用默认模型
|
||||
AiChatRoleDO writeRole = CollUtil.getFirst(
|
||||
chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_WRITE_ROLE.getName()));
|
||||
// 1.1 获取写作执行模型
|
||||
AiChatModelDO model = getModel(writeRole);
|
||||
// 1.2 获取角色设定消息
|
||||
String systemMessage = Objects.nonNull(writeRole) && StrUtil.isNotBlank(writeRole.getSystemMessage())
|
||||
? writeRole.getSystemMessage() : AiChatRoleEnum.AI_WRITE_ROLE.getSystemMessage();
|
||||
// 1.3 校验平台
|
||||
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
|
||||
StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId());
|
||||
|
||||
// 2. 插入写作信息
|
||||
AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class,
|
||||
write -> write.setUserId(userId).setPlatform(platform.getPlatform()).setModel(model.getModel()));
|
||||
writeMapper.insert(writeDO);
|
||||
|
||||
// 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(() ->
|
||||
writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setGeneratedContent(contentBuffer.toString())));
|
||||
}).doOnError(throwable -> {
|
||||
log.error("[generateWriteContent][generateReqVO({}) 发生异常]", generateReqVO, throwable);
|
||||
// 忽略租户,因为 Flux 异步无法透传租户
|
||||
TenantUtils.executeIgnore(() ->
|
||||
writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setErrorMessage(throwable.getMessage())));
|
||||
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR)));
|
||||
}
|
||||
|
||||
private AiChatModelDO getModel(AiChatRoleDO writeRole) {
|
||||
AiChatModelDO model = null;
|
||||
if (Objects.nonNull(writeRole) && Objects.nonNull(writeRole.getModelId())) {
|
||||
model = chatModalService.getChatModel(writeRole.getModelId());
|
||||
}
|
||||
if (Objects.isNull(model)) {
|
||||
model = chatModalService.getRequiredDefaultChatModel();
|
||||
}
|
||||
Assert.notNull(model, "[AI] 获取不到模型");
|
||||
return model;
|
||||
}
|
||||
|
||||
private Prompt buildPrompt(AiWriteGenerateReqVO 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 List<Message> buildMessages(AiWriteGenerateReqVO generateReqVO, String systemMessage) {
|
||||
List<Message> chatMessages = new ArrayList<>();
|
||||
if (StrUtil.isNotBlank(systemMessage)) {
|
||||
// 1.1 角色设定
|
||||
chatMessages.add(new SystemMessage(systemMessage));
|
||||
}
|
||||
// 1.2 用户输入
|
||||
chatMessages.add(new UserMessage(buildUserMessage(generateReqVO)));
|
||||
return chatMessages;
|
||||
}
|
||||
|
||||
private String buildUserMessage(AiWriteGenerateReqVO generateReqVO) {
|
||||
String format = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_FORMAT, generateReqVO.getFormat());
|
||||
String tone = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_TONE, generateReqVO.getTone());
|
||||
String language = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LANGUAGE, generateReqVO.getLanguage());
|
||||
String length = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LENGTH, generateReqVO.getLength());
|
||||
// 格式化 prompt
|
||||
String prompt = generateReqVO.getPrompt();
|
||||
if (Objects.equals(generateReqVO.getType(), AiWriteTypeEnum.WRITING.getType())) {
|
||||
return StrUtil.format(AiWriteTypeEnum.WRITING.getPrompt(), prompt, format, tone, language, length);
|
||||
} else {
|
||||
return StrUtil.format(AiWriteTypeEnum.REPLY.getPrompt(), generateReqVO.getOriginalContent(), prompt, format, tone, language, length);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteWrite(Long id) {
|
||||
// 校验存在
|
||||
validateWriteExists(id);
|
||||
// 删除
|
||||
writeMapper.deleteById(id);
|
||||
}
|
||||
|
||||
private void validateWriteExists(Long id) {
|
||||
if (writeMapper.selectById(id) == null) {
|
||||
throw exception(WRITE_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiWriteDO> getWritePage(AiWritePageReqVO pageReqVO) {
|
||||
return writeMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
--- #################### 数据库相关配置 ####################
|
||||
spring:
|
||||
# 数据源配置项
|
||||
autoconfigure:
|
||||
exclude:
|
||||
datasource:
|
||||
druid: # Druid 【监控】相关的全局配置
|
||||
web-stat-filter:
|
||||
enabled: true
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
allow: # 设置白名单,不填则允许所有访问
|
||||
url-pattern: /druid/*
|
||||
login-username: # 控制台管理用户名和密码
|
||||
login-password:
|
||||
filter:
|
||||
stat:
|
||||
enabled: true
|
||||
log-slow-sql: true # 慢 SQL 记录
|
||||
slow-sql-millis: 100
|
||||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
dynamic: # 多数据源配置
|
||||
druid: # Druid 【连接池】相关的全局配置
|
||||
initial-size: 5 # 初始连接数
|
||||
min-idle: 10 # 最小连接池数量
|
||||
max-active: 20 # 最大连接池数量
|
||||
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
|
||||
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
|
||||
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
|
||||
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
|
||||
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
||||
username: root
|
||||
password: 123456
|
||||
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
|
||||
lazy: true # 开启懒加载,保证启动速度
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||
data:
|
||||
redis:
|
||||
host: 400-infra.server.iocoder.cn # 地址
|
||||
port: 6379 # 端口
|
||||
database: 1 # 数据库索引
|
||||
# password: 123456 # 密码,建议生产环境开启
|
||||
|
||||
--- #################### MQ 消息队列相关配置 ####################
|
||||
|
||||
# rocketmq 配置项,对应 RocketMQProperties 配置类
|
||||
rocketmq:
|
||||
name-server: 127.0.0.1:9876 # RocketMQ Namesrv
|
||||
|
||||
spring:
|
||||
# RabbitMQ 配置项,对应 RabbitProperties 配置类
|
||||
rabbitmq:
|
||||
host: 127.0.0.1 # RabbitMQ 服务的地址
|
||||
port: 5672 # RabbitMQ 服务的端口
|
||||
username: guest # RabbitMQ 服务的账号
|
||||
password: guest # RabbitMQ 服务的密码
|
||||
# Kafka 配置项,对应 KafkaProperties 配置类
|
||||
kafka:
|
||||
bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
xxl:
|
||||
job:
|
||||
admin:
|
||||
addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址
|
||||
|
||||
--- #################### 服务保障相关配置 ####################
|
||||
|
||||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
# Actuator 监控端点的配置项
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
|
||||
exposure:
|
||||
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
|
||||
|
||||
# Spring Boot Admin 配置项
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
# Spring Boot Admin Client 客户端的相关配置
|
||||
client:
|
||||
instance:
|
||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||
# Spring Boot Admin Server 服务端的相关配置
|
||||
context-path: /admin # 配置 Spring
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
# 芋道配置项,设置当前项目所有自定义的配置
|
||||
yudao:
|
||||
xss:
|
||||
enable: false
|
||||
exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系
|
||||
- ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
|
||||
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
|
||||
demo: true # 开启演示模式
|
||||
@@ -0,0 +1,125 @@
|
||||
--- #################### 数据库相关配置 ####################
|
||||
spring:
|
||||
# 数据源配置项
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源
|
||||
- de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置
|
||||
datasource:
|
||||
druid: # Druid 【监控】相关的全局配置
|
||||
web-stat-filter:
|
||||
enabled: true
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
allow: # 设置白名单,不填则允许所有访问
|
||||
url-pattern: /druid/*
|
||||
login-username: # 控制台管理用户名和密码
|
||||
login-password:
|
||||
filter:
|
||||
stat:
|
||||
enabled: true
|
||||
log-slow-sql: true # 慢 SQL 记录
|
||||
slow-sql-millis: 100
|
||||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
dynamic: # 多数据源配置
|
||||
druid: # Druid 【连接池】相关的全局配置
|
||||
initial-size: 1 # 初始连接数
|
||||
min-idle: 1 # 最小连接池数量
|
||||
max-active: 20 # 最大连接池数量
|
||||
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
|
||||
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
|
||||
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
|
||||
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
|
||||
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例
|
||||
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||
# url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例
|
||||
# url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
||||
username: root
|
||||
password: 123456
|
||||
# username: sa # SQL Server 连接的示例
|
||||
# password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例
|
||||
# username: SYSDBA # DM 连接的示例
|
||||
# password: SYSDBA # DM 连接的示例
|
||||
slave: # 模拟从库,可根据自己需要修改
|
||||
lazy: true # 开启懒加载,保证启动速度
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||
redis:
|
||||
data:
|
||||
host: 127.0.0.1 # 地址
|
||||
port: 6379 # 端口
|
||||
database: 0 # 数据库索引
|
||||
# password: 123456 # 密码,建议生产环境开启
|
||||
|
||||
--- #################### MQ 消息队列相关配置 ####################
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
|
||||
xxl:
|
||||
job:
|
||||
admin:
|
||||
addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址
|
||||
|
||||
--- #################### 服务保障相关配置 ####################
|
||||
|
||||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
# Actuator 监控端点的配置项
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
|
||||
exposure:
|
||||
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
|
||||
|
||||
# Spring Boot Admin 配置项
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
# Spring Boot Admin Client 客户端的相关配置
|
||||
client:
|
||||
instance:
|
||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||
|
||||
# 日志文件配置
|
||||
logging:
|
||||
level:
|
||||
# 配置自己写的 MyBatis Mapper 打印日志
|
||||
cn.iocoder.yudao.module.ai.dal.mysql: debug
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
# 芋道配置项,设置当前项目所有自定义的配置
|
||||
yudao:
|
||||
env: # 多环境的配置项
|
||||
tag: ${HOSTNAME}
|
||||
security:
|
||||
mock-enable: true
|
||||
xss:
|
||||
enable: false
|
||||
exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系
|
||||
- ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
|
||||
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
|
||||
access-log: # 访问日志的配置项
|
||||
enable: false
|
||||
demo: false # 关闭演示模式
|
||||
@@ -0,0 +1,155 @@
|
||||
spring:
|
||||
main:
|
||||
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
|
||||
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
|
||||
|
||||
# Servlet 配置
|
||||
servlet:
|
||||
# 文件上传相关配置项
|
||||
multipart:
|
||||
max-file-size: 16MB # 单个文件大小
|
||||
max-request-size: 32MB # 设置总上传的文件大小
|
||||
mvc:
|
||||
pathmatch:
|
||||
matching-strategy: ANT_PATH_MATCHER # 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题,参见 SpringFoxHandlerProviderBeanPostProcessor 类
|
||||
|
||||
# Jackson 配置项
|
||||
jackson:
|
||||
serialization:
|
||||
write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
|
||||
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
|
||||
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
|
||||
fail-on-empty-beans: false # 允许序列化无属性的 Bean
|
||||
|
||||
# Cache 配置项
|
||||
cache:
|
||||
type: REDIS
|
||||
redis:
|
||||
time-to-live: 1h # 设置过期时间为 1 小时
|
||||
|
||||
server:
|
||||
servlet:
|
||||
encoding:
|
||||
enabled: true
|
||||
charset: UTF-8 # 必须设置 UTF-8,避免 WebFlux 流式返回(AI 场景)会乱码问题
|
||||
force: true
|
||||
|
||||
--- #################### 接口文档配置 ####################
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true # 1. 是否开启 Swagger 接文档的元数据
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面
|
||||
path: /swagger-ui.html
|
||||
default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档
|
||||
|
||||
knife4j:
|
||||
enable: true # 2.2 是否开启 Swagger 文档的 Knife4j UI 界面
|
||||
setting:
|
||||
language: zh_cn
|
||||
|
||||
# MyBatis Plus 的配置项
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
|
||||
# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库
|
||||
# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
|
||||
# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
|
||||
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
|
||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||
banner: false # 关闭控制台的 Banner 打印
|
||||
type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject
|
||||
encryptor:
|
||||
password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成
|
||||
|
||||
mybatis-plus-join:
|
||||
banner: false # 关闭控制台的 Banner 打印
|
||||
|
||||
# VO 转换(数据翻译)相关
|
||||
easy-trans:
|
||||
is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口
|
||||
is-enable-cloud: false # 禁用 TransType.RPC 微服务模式
|
||||
|
||||
--- #################### RPC 远程调用相关配置 ####################
|
||||
|
||||
--- #################### MQ 消息队列相关配置 ####################
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
|
||||
xxl:
|
||||
job:
|
||||
executor:
|
||||
appname: ${spring.application.name} # 执行器 AppName
|
||||
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
|
||||
accessToken: default_token # 执行器通讯TOKEN
|
||||
|
||||
--- #################### AI 相关配置 ####################
|
||||
|
||||
spring:
|
||||
ai:
|
||||
qianfan: # 文心一言
|
||||
api-key: x0cuLZ7XsaTCU08vuJWO87Lg
|
||||
secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
|
||||
zhipuai: # 智谱 AI
|
||||
api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs
|
||||
openai:
|
||||
api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z
|
||||
base-url: https://api.gptsapi.net
|
||||
ollama:
|
||||
base-url: http://127.0.0.1:11434
|
||||
chat:
|
||||
model: llama3
|
||||
stabilityai:
|
||||
api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
|
||||
cloud:
|
||||
ai:
|
||||
tongyi: # 通义千问
|
||||
tongyi:
|
||||
api-key: sk-Zsd81gZYg7
|
||||
|
||||
yudao:
|
||||
ai:
|
||||
deep-seek: # DeepSeek
|
||||
enable: true
|
||||
api-key: sk-e94db327cc7d457d99a8de8810fc6b12
|
||||
model: deepseek-chat
|
||||
xinghuo: # 讯飞星火
|
||||
enable: true
|
||||
appId: 13c8cca6
|
||||
appKey: cb6415c19d6162cda07b47316fcb0416
|
||||
secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh
|
||||
model: generalv3.5
|
||||
midjourney:
|
||||
enable: true
|
||||
# base-url: https://api.holdai.top/mj-relax/mj
|
||||
base-url: https://api.holdai.top/mj
|
||||
api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf
|
||||
notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
|
||||
suno:
|
||||
enable: true
|
||||
# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
|
||||
base-url: http://127.0.0.1:3001
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
yudao:
|
||||
info:
|
||||
version: 1.0.0
|
||||
base-package: cn.iocoder.yudao.module.ai
|
||||
web:
|
||||
admin-ui:
|
||||
url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址
|
||||
swagger:
|
||||
title: 管理后台
|
||||
description: 提供管理员管理的所有功能
|
||||
version: ${yudao.info.version}
|
||||
base-package: ${yudao.info.base-package}
|
||||
tenant: # 多租户相关配置项
|
||||
enable: true
|
||||
|
||||
debug: false
|
||||
@@ -0,0 +1,23 @@
|
||||
--- #################### 注册中心相关配置 ####################
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: 127.0.0.1:8848
|
||||
discovery:
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
metadata:
|
||||
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
|
||||
|
||||
--- #################### 配置中心相关配置 ####################
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
# Nacos Config 配置项,对应 NacosConfigProperties 配置属性类
|
||||
config:
|
||||
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
|
||||
namespace: dev # 命名空间 dev 的ID,不能直接使用 dev 名称。创建命名空间的时候需要指定ID为 dev,这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
name: ${spring.application.name} # 使用的 Nacos 配置集的 dataId,默认为 spring.application.name
|
||||
file-extension: yaml # 使用的 Nacos 配置集的 dataId 的文件拓展名,同时也是 Nacos 配置集的配置格式,默认为 properties
|
||||
@@ -0,0 +1,14 @@
|
||||
spring:
|
||||
application:
|
||||
name: ai-server
|
||||
|
||||
profiles:
|
||||
active: local
|
||||
|
||||
server:
|
||||
port: 48090
|
||||
|
||||
# 日志文件配置。注意,如果 logging.file.name 不放在 bootstrap.yaml 配置文件,而是放在 application.yaml 中,会导致出现 LOG_FILE_IS_UNDEFINED 文件
|
||||
logging:
|
||||
file:
|
||||
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
|
||||
@@ -0,0 +1,76 @@
|
||||
<configuration>
|
||||
<!-- 引用 Spring Boot 的 logback 基础配置 -->
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
|
||||
<!-- 变量 yudao.info.base-package,基础业务包 -->
|
||||
<springProperty scope="context" name="yudao.info.base-package" source="yudao.info.base-package"/>
|
||||
<!-- 格式化输出:%d 表示日期,%X{tid} SkWalking 链路追踪编号,%thread 表示线程名,%-5level:级别从左显示 5 个字符宽度,%msg:日志消息,%n是换行符 -->
|
||||
<property name="PATTERN_DEFAULT" value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} | %highlight(${LOG_LEVEL_PATTERN:-%5p} ${PID:- }) | %boldYellow(%thread [%tid]) %boldGreen(%-40.40logger{39}) | %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
|
||||
|
||||
<!-- 控制台 Appender -->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
|
||||
<pattern>${PATTERN_DEFAULT}</pattern>
|
||||
</layout>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件 Appender -->
|
||||
<!-- 参考 Spring Boot 的 file-appender.xml 编写 -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
|
||||
<pattern>${PATTERN_DEFAULT}</pattern>
|
||||
</layout>
|
||||
</encoder>
|
||||
<!-- 日志文件名 -->
|
||||
<file>${LOG_FILE}</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<!-- 滚动后的日志文件名 -->
|
||||
<fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
|
||||
<!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
|
||||
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
|
||||
<!-- 日志文件,到达多少容量,进行滚动 -->
|
||||
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
|
||||
<!-- 日志文件的总大小,0 表示不限制 -->
|
||||
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
|
||||
<!-- 日志文件的保留天数 -->
|
||||
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30}</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
<!-- 异步写入日志,提升性能 -->
|
||||
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<!-- 不丢失日志。默认的,如果队列的 80% 已满,则会丢弃 TRACT、DEBUG、INFO 级别的日志 -->
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<!-- 更改默认的队列的深度,该值会影响性能。默认值为 256 -->
|
||||
<queueSize>256</queueSize>
|
||||
<appender-ref ref="FILE"/>
|
||||
</appender>
|
||||
|
||||
<!-- SkyWalking GRPC 日志收集,实现日志中心。注意:SkyWalking 8.4.0 版本开始支持 -->
|
||||
<appender name="GRPC" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
|
||||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
|
||||
<pattern>${PATTERN_DEFAULT}</pattern>
|
||||
</layout>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 本地环境 -->
|
||||
<springProfile name="local">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
|
||||
<appender-ref ref="ASYNC"/> <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
|
||||
</root>
|
||||
</springProfile>
|
||||
<!-- 其它环境 -->
|
||||
<springProfile name="dev,test,stage,prod,default">
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="ASYNC"/>
|
||||
<appender-ref ref="GRPC"/>
|
||||
</root>
|
||||
</springProfile>
|
||||
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user