diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 183079d0c..d94b74d5b 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -45,4 +45,9 @@ public interface ErrorCodeConstants { // ========== API 音乐 1-040-006-000 ========== ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!"); + + // ========== API 写作 1-022-007-000 ========== + ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!"); + ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "Stream 对话异常!"); + } diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java index ad4b81b36..651731b60 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * AI 音乐状态的枚举 + * AI 音乐生成模式的枚举 * * @author xiaoxin */ diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiLanguageEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiLanguageEnum.java new file mode 100644 index 000000000..9dfe69302 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiLanguageEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.ai.enums.write; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@AllArgsConstructor +@Getter +public enum AiLanguageEnum implements IntArrayValuable { + + AUTO(1, "自动"), + CHINESE(2, "中文"), + ENGLISH(3, "英文"), + KOREAN(4, "韩语"), + JAPANESE(5, "日语"); + + /** + * Language code + */ + private final Integer language; + /** + * Language name + */ + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiLanguageEnum::getLanguage).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + + public static AiLanguageEnum valueOfLanguage(Integer language) { + for (AiLanguageEnum languageEnum : AiLanguageEnum.values()) { + if (languageEnum.getLanguage().equals(language)) { + return languageEnum; + } + } + throw new IllegalArgumentException("未知语言: " + language); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteFormatEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteFormatEnum.java new file mode 100644 index 000000000..d77e08fcc --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteFormatEnum.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.ai.enums.write; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * AI 写作类型的枚举 + * + * @author xiaoxin + */ +@AllArgsConstructor +@Getter +public enum AiWriteFormatEnum implements IntArrayValuable { + + AUTO(1, "自动"), + EMAIL(2, "电子邮件"), + MESSAGE(3, "消息"), + COMMENT(4, "评论"), + PARAGRAPH(5, "段落"), + ARTICLE(6, "文章"), + BLOG_POST(7, "博客文章"), + IDEA(8, "想法"), + OUTLINE(9, "大纲"); + + /** + * 格式 + */ + private final Integer format; + /** + * 格式名 + */ + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteFormatEnum::getFormat).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + + public static AiWriteFormatEnum valueOfFormat(Integer format) { + for (AiWriteFormatEnum formatEnum : AiWriteFormatEnum.values()) { + if (formatEnum.getFormat().equals(format)) { + return formatEnum; + } + } + throw new IllegalArgumentException("未知格式: " + format); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteLengthEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteLengthEnum.java new file mode 100644 index 000000000..2c6a9c5c1 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteLengthEnum.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.ai.enums.write; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * AI 写作类型的枚举 + * + * @author xiaoxin + */ +@AllArgsConstructor +@Getter +public enum AiWriteLengthEnum implements IntArrayValuable { + + AUTO(1, "自动"), + SHORT(2, "短"), + MEDIUM(3, "中"), + LONG(4, "长"); + + /** + * 长度 + */ + private final Integer length; + /** + * 长度名 + */ + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteLengthEnum::getLength).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + + public static AiWriteLengthEnum valueOfLength(Integer length) { + for (AiWriteLengthEnum lengthEnum : AiWriteLengthEnum.values()) { + if (lengthEnum.getLength().equals(length)) { + return lengthEnum; + } + } + throw new IllegalArgumentException("未知长度: " + length); + } +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteToneEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteToneEnum.java new file mode 100644 index 000000000..181682fd9 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteToneEnum.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.ai.enums.write; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@AllArgsConstructor +@Getter +public enum AiWriteToneEnum implements IntArrayValuable { + + AUTO(1, "自动"), + FRIENDLY(2, "友善"), + CASUAL(3, "随意"), + KIND(4, "友好"), + PROFESSIONAL(5, "专业"), + HUMOROUS(6, "谈谐"), + INTERESTING(7, "有趣"), + FORMAL(8, "正式"); + + /** + * 语气 + */ + private final Integer tone; + /** + * 语气名 + */ + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteToneEnum::getTone).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + + public static AiWriteToneEnum valueOfTone(Integer tone) { + for (AiWriteToneEnum toneEnum : AiWriteToneEnum.values()) { + if (toneEnum.getTone().equals(tone)) { + return toneEnum; + } + } + throw new IllegalArgumentException("未知语气: " + tone); + } +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java new file mode 100644 index 000000000..3a62e1626 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.ai.enums.write; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * AI 写作类型的枚举 + * + * @author xiaoxin + */ +@AllArgsConstructor +@Getter +public enum AiWriteTypeEnum implements IntArrayValuable { + + WRITING(1, "撰写"), + REPLY(2, "回复"); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteTypeEnum::getType).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java index ce88c6ad9..6c09e4b30 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java @@ -63,7 +63,7 @@ public class AiMusicController { @PostMapping("/update-my") @Operation(summary = "修改【我的】音乐 目前只支持修改标题") @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星") - public CommonResult updateMy(AiMusicUpdateReqVO updateReqVO) { + public CommonResult updateMy(AiMusicUpdateMyReqVO updateReqVO) { musicService.updateMyMusic(updateReqVO, getLoginUserId()); return success(true); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java new file mode 100644 index 000000000..457670115 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 修改我的音乐 Request VO") +@Data +public class AiMusicUpdateMyReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "音乐名称", example = "夜空中最亮的星") + private String title; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java index 6a75892e8..447bc9765 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java @@ -15,8 +15,4 @@ public class AiMusicUpdateReqVO { @Schema(description = "是否发布", example = "true") private Boolean publicStatus; - // TODO @xin:得单独一个 vo。因为万一。。。模拟请求,就可以改 publicStatus 拉 - @Schema(description = "音乐名称", example = "夜空中最亮的星") - private String title; - } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java new file mode 100644 index 000000000..a032998ed --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.ai.controller.admin.write; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; +import cn.iocoder.yudao.module.ai.service.write.AiWriteService; +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/write") +public class AiWriteController { + + @Resource + private AiWriteService writeService; + + @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @PermitAll + @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快") + public Flux> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) { + return writeService.generateWriteContent(generateReqVO, getLoginUserId()); + } +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java new file mode 100644 index 000000000..59283c91c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.ai.controller.admin.write.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 AiWriteGenerateReqVO { + + @Schema(description = "写作内容提示", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1.撰写:田忌赛马;2.回复:不批") + private String prompt; + + @Schema(description = "原文", requiredMode = Schema.RequiredMode.NOT_REQUIRED, 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; + + + @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; //参见 AiWriteTypeEnum 枚举 +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java new file mode 100644 index 000000000..347c2143d --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.write; + +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; +import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; + +/** + * AI 写作 DO + * + * @author xiaoxin + */ +@TableName(value = "ai_write", autoResultMap = true) +@Data +public class AiWriteDO extends BaseDO { + + /** + * 编号 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户编号 + */ + private Long userId; + + /** + * 写作类型 + *

+ * 枚举 {@link AiWriteTypeEnum} + */ + private Integer type; + + /** + * 生成内容提示 + */ + private String prompt; + + /** + * 生成的内容 + */ + private String generatedContent; + + /** + * 原文 + */ + private String originalContent; + + /** + * 长度提示词 + */ + private Integer length; + + /** + * 格式提示词 + */ + private Integer format; + + /** + * 语气提示词 + */ + private Integer tone; + + /** + * 语言提示词 + */ + private Integer language; + + /** + * 模型 + */ + private String model; + + /** + * 平台 + */ + private String platform; + + /** + * 错误信息 + */ + private String errorMessage; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java new file mode 100644 index 000000000..9564466eb --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.write; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +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 { +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java index 7d1541ccb..49f8332de 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java @@ -42,7 +42,7 @@ public interface AiMusicService { * * @param updateReqVO 更新信息 */ - void updateMyMusic(@Valid AiMusicUpdateReqVO updateReqVO, Long userId); + void updateMyMusic(@Valid AiMusicUpdateMyReqVO updateReqVO, Long userId); /** * 删除AI 音乐 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java index a17fc19b5..591778a55 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java @@ -9,6 +9,7 @@ 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; @@ -111,7 +112,7 @@ public class AiMusicServiceImpl implements AiMusicService { } @Override - public void updateMyMusic(AiMusicUpdateReqVO updateReqVO, Long userId) { + public void updateMyMusic(AiMusicUpdateMyReqVO updateReqVO, Long userId) { // 校验音乐是否存在 AiMusicDO musicDO = validateMusicExists(updateReqVO.getId()); if (ObjUtil.notEqual(musicDO.getUserId(), userId)) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java new file mode 100644 index 000000000..0dc349cba --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.ai.service.write; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; +import reactor.core.publisher.Flux; + +/** + * AI 写作 Service 接口 + * + * @author xiaoxin + */ +public interface AiWriteService { + + + /** + * 生成写作内容 + * + * @param generateReqVO 作文生成请求参数 + * @param userId 用户编号 + * @return 生成结果 + */ + Flux> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId); + + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java new file mode 100644 index 000000000..8a3cdfa9e --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.ai.service.write; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +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.dal.dataobject.model.AiChatModelDO; +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.ErrorCodeConstants; +import cn.iocoder.yudao.module.ai.enums.write.*; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +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.ai.ollama.api.OllamaOptions; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.qianfan.QianFanChatOptions; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.Objects; + +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 AiWriteServiceImpl implements AiWriteService { + + @Resource + private AiApiKeyService apiKeyService; + @Resource + private AiChatModelService chatModalService; + @Resource + private AiWriteMapper writeMapper; + + + @Override + public Flux> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) { + //TODO 芋艿 写作的模型配置放哪好 先用千问测试 + // 1.1 校验模型 + AiChatModelDO model = chatModalService.validateChatModel(14L); + StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId()); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); + ChatOptions chatOptions = buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens()); + + //1.2 插入写作信息 + AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class); + writeMapper.insert(writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform())); + + //2.1 构建提示词 + Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions); + Flux streamResponse = chatClient.stream(prompt); + // 2.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(() -> { + writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setGeneratedContent(contentBuffer.toString())); + }).doOnError(throwable -> { + log.error("[AI Write][generateReqVO({}) 发生异常]", generateReqVO, throwable); + writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setErrorMessage(throwable.getMessage())); + }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR))); + } + + + private String buildWritingPrompt(AiWriteGenerateReqVO generateReqVO) { + String template; + Integer writeType = generateReqVO.getType(); + String format = AiWriteFormatEnum.valueOfFormat(generateReqVO.getFormat()).getName(); + String tone = AiWriteToneEnum.valueOfTone(generateReqVO.getTone()).getName(); + String language = AiLanguageEnum.valueOfLanguage(generateReqVO.getLanguage()).getName(); + String length = AiWriteLengthEnum.valueOfLength(generateReqVO.getLength()).getName(); + if (Objects.equals(writeType, AiWriteTypeEnum.WRITING.getType())) { + template = "请撰写一篇关于 [{}] 的文章。文章的内容格式为:[{}],语气为:[{}],语言为:[{}],长度为:[{}]。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。"; + return StrUtil.format(template, generateReqVO.getPrompt(), format, tone, language, length); + } else if (Objects.equals(writeType, AiWriteTypeEnum.REPLY.getType())) { + template = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复的内容格式为:[{}],语气为:[{}],语言为:[{}],长度为:[{}]。不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。"; + return StrUtil.format(template, generateReqVO.getOriginalContent(), generateReqVO.getPrompt(), format, tone, language, length); + } else { + throw new IllegalArgumentException(StrUtil.format("未知写作类型({})", writeType)); + } + } + + // TODO 芋艿:复用 + private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { + Float temperatureF = temperature != null ? temperature.floatValue() : null; + //noinspection EnhancedSwitchMigration + switch (platform) { + case OPENAI: + return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + case OLLAMA: + return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens); + case YI_YAN: + // TODO 芋艿:貌似 model 只要一设置,就报错 +// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + case XING_HUO: + return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF) + .setMaxTokens(maxTokens); + case QIAN_WEN: + return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build(); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + } + +}