Commit 9ddd2eddf890f7e1d130d13e0a964a67341cf862

Authored by 芋道源码
Committed by Gitee
2 parents f78d13a8 6dfbfe51

!9 【增加】AI 写作:初版

Merge pull request !9 from 小新/master-jdk21-ai
Showing 18 changed files with 577 additions and 8 deletions
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 { @@ -45,4 +45,9 @@ public interface ErrorCodeConstants {
45 // ========== API 音乐 1-040-006-000 ========== 45 // ========== API 音乐 1-040-006-000 ==========
46 ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!"); 46 ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
47 47
  48 +
  49 + // ========== API 写作 1-022-007-000 ==========
  50 + ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!");
  51 + ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "Stream 对话异常!");
  52 +
48 } 53 }
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; @@ -7,7 +7,7 @@ import lombok.Getter;
7 import java.util.Arrays; 7 import java.util.Arrays;
8 8
9 /** 9 /**
10 - * AI 音乐状态的枚举 10 + * AI 音乐生成模式的枚举
11 * 11 *
12 * @author xiaoxin 12 * @author xiaoxin
13 */ 13 */
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiLanguageEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.enums.write;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +@AllArgsConstructor
  10 +@Getter
  11 +public enum AiLanguageEnum implements IntArrayValuable {
  12 +
  13 + AUTO(1, "自动"),
  14 + CHINESE(2, "中文"),
  15 + ENGLISH(3, "英文"),
  16 + KOREAN(4, "韩语"),
  17 + JAPANESE(5, "日语");
  18 +
  19 + /**
  20 + * Language code
  21 + */
  22 + private final Integer language;
  23 + /**
  24 + * Language name
  25 + */
  26 + private final String name;
  27 +
  28 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiLanguageEnum::getLanguage).toArray();
  29 +
  30 + @Override
  31 + public int[] array() {
  32 + return ARRAYS;
  33 + }
  34 +
  35 + public static AiLanguageEnum valueOfLanguage(Integer language) {
  36 + for (AiLanguageEnum languageEnum : AiLanguageEnum.values()) {
  37 + if (languageEnum.getLanguage().equals(language)) {
  38 + return languageEnum;
  39 + }
  40 + }
  41 + throw new IllegalArgumentException("未知语言: " + language);
  42 + }
  43 +
  44 +}
0 \ No newline at end of file 45 \ No newline at end of file
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteFormatEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.enums.write;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * AI 写作类型的枚举
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +@AllArgsConstructor
  15 +@Getter
  16 +public enum AiWriteFormatEnum implements IntArrayValuable {
  17 +
  18 + AUTO(1, "自动"),
  19 + EMAIL(2, "电子邮件"),
  20 + MESSAGE(3, "消息"),
  21 + COMMENT(4, "评论"),
  22 + PARAGRAPH(5, "段落"),
  23 + ARTICLE(6, "文章"),
  24 + BLOG_POST(7, "博客文章"),
  25 + IDEA(8, "想法"),
  26 + OUTLINE(9, "大纲");
  27 +
  28 + /**
  29 + * 格式
  30 + */
  31 + private final Integer format;
  32 + /**
  33 + * 格式名
  34 + */
  35 + private final String name;
  36 +
  37 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteFormatEnum::getFormat).toArray();
  38 +
  39 + @Override
  40 + public int[] array() {
  41 + return ARRAYS;
  42 + }
  43 +
  44 + public static AiWriteFormatEnum valueOfFormat(Integer format) {
  45 + for (AiWriteFormatEnum formatEnum : AiWriteFormatEnum.values()) {
  46 + if (formatEnum.getFormat().equals(format)) {
  47 + return formatEnum;
  48 + }
  49 + }
  50 + throw new IllegalArgumentException("未知格式: " + format);
  51 + }
  52 +
  53 +}
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteLengthEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.enums.write;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * AI 写作类型的枚举
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +@AllArgsConstructor
  15 +@Getter
  16 +public enum AiWriteLengthEnum implements IntArrayValuable {
  17 +
  18 + AUTO(1, "自动"),
  19 + SHORT(2, "短"),
  20 + MEDIUM(3, "中"),
  21 + LONG(4, "长");
  22 +
  23 + /**
  24 + * 长度
  25 + */
  26 + private final Integer length;
  27 + /**
  28 + * 长度名
  29 + */
  30 + private final String name;
  31 +
  32 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteLengthEnum::getLength).toArray();
  33 +
  34 + @Override
  35 + public int[] array() {
  36 + return ARRAYS;
  37 + }
  38 +
  39 + public static AiWriteLengthEnum valueOfLength(Integer length) {
  40 + for (AiWriteLengthEnum lengthEnum : AiWriteLengthEnum.values()) {
  41 + if (lengthEnum.getLength().equals(length)) {
  42 + return lengthEnum;
  43 + }
  44 + }
  45 + throw new IllegalArgumentException("未知长度: " + length);
  46 + }
  47 +}
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteToneEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.enums.write;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +@AllArgsConstructor
  10 +@Getter
  11 +public enum AiWriteToneEnum implements IntArrayValuable {
  12 +
  13 + AUTO(1, "自动"),
  14 + FRIENDLY(2, "友善"),
  15 + CASUAL(3, "随意"),
  16 + KIND(4, "友好"),
  17 + PROFESSIONAL(5, "专业"),
  18 + HUMOROUS(6, "谈谐"),
  19 + INTERESTING(7, "有趣"),
  20 + FORMAL(8, "正式");
  21 +
  22 + /**
  23 + * 语气
  24 + */
  25 + private final Integer tone;
  26 + /**
  27 + * 语气名
  28 + */
  29 + private final String name;
  30 +
  31 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteToneEnum::getTone).toArray();
  32 +
  33 + @Override
  34 + public int[] array() {
  35 + return ARRAYS;
  36 + }
  37 +
  38 + public static AiWriteToneEnum valueOfTone(Integer tone) {
  39 + for (AiWriteToneEnum toneEnum : AiWriteToneEnum.values()) {
  40 + if (toneEnum.getTone().equals(tone)) {
  41 + return toneEnum;
  42 + }
  43 + }
  44 + throw new IllegalArgumentException("未知语气: " + tone);
  45 + }
  46 +}
0 \ No newline at end of file 47 \ No newline at end of file
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.enums.write;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * AI 写作类型的枚举
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +@AllArgsConstructor
  15 +@Getter
  16 +public enum AiWriteTypeEnum implements IntArrayValuable {
  17 +
  18 + WRITING(1, "撰写"),
  19 + REPLY(2, "回复");
  20 +
  21 + /**
  22 + * 类型
  23 + */
  24 + private final Integer type;
  25 + /**
  26 + * 类型名
  27 + */
  28 + private final String name;
  29 +
  30 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteTypeEnum::getType).toArray();
  31 +
  32 + @Override
  33 + public int[] array() {
  34 + return ARRAYS;
  35 + }
  36 +
  37 +}
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 { @@ -63,7 +63,7 @@ public class AiMusicController {
63 @PostMapping("/update-my") 63 @PostMapping("/update-my")
64 @Operation(summary = "修改【我的】音乐 目前只支持修改标题") 64 @Operation(summary = "修改【我的】音乐 目前只支持修改标题")
65 @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星") 65 @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星")
66 - public CommonResult<Boolean> updateMy(AiMusicUpdateReqVO updateReqVO) { 66 + public CommonResult<Boolean> updateMy(AiMusicUpdateMyReqVO updateReqVO) {
67 musicService.updateMyMusic(updateReqVO, getLoginUserId()); 67 musicService.updateMyMusic(updateReqVO, getLoginUserId());
68 return success(true); 68 return success(true);
69 } 69 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI 修改我的音乐 Request VO")
  8 +@Data
  9 +public class AiMusicUpdateMyReqVO {
  10 +
  11 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
  12 + @NotNull(message = "编号不能为空")
  13 + private Long id;
  14 +
  15 + @Schema(description = "音乐名称", example = "夜空中最亮的星")
  16 + private String title;
  17 +
  18 +}
0 \ No newline at end of file 19 \ No newline at end of file
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 { @@ -15,8 +15,4 @@ public class AiMusicUpdateReqVO {
15 @Schema(description = "是否发布", example = "true") 15 @Schema(description = "是否发布", example = "true")
16 private Boolean publicStatus; 16 private Boolean publicStatus;
17 17
18 - // TODO @xin:得单独一个 vo。因为万一。。。模拟请求,就可以改 publicStatus 拉  
19 - @Schema(description = "音乐名称", example = "夜空中最亮的星")  
20 - private String title;  
21 -  
22 } 18 }
23 \ No newline at end of file 19 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.write;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO;
  5 +import cn.iocoder.yudao.module.ai.service.write.AiWriteService;
  6 +import io.swagger.v3.oas.annotations.Operation;
  7 +import io.swagger.v3.oas.annotations.tags.Tag;
  8 +import jakarta.annotation.Resource;
  9 +import jakarta.annotation.security.PermitAll;
  10 +import jakarta.validation.Valid;
  11 +import org.springframework.http.MediaType;
  12 +import org.springframework.web.bind.annotation.PostMapping;
  13 +import org.springframework.web.bind.annotation.RequestBody;
  14 +import org.springframework.web.bind.annotation.RequestMapping;
  15 +import org.springframework.web.bind.annotation.RestController;
  16 +import reactor.core.publisher.Flux;
  17 +
  18 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  19 +
  20 +@Tag(name = "管理后台 - AI 写作")
  21 +@RestController
  22 +@RequestMapping("/ai/write")
  23 +public class AiWriteController {
  24 +
  25 + @Resource
  26 + private AiWriteService writeService;
  27 +
  28 + @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  29 + @PermitAll
  30 + @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快")
  31 + public Flux<CommonResult<String>> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) {
  32 + return writeService.generateWriteContent(generateReqVO, getLoginUserId());
  33 + }
  34 +}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.write.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI 写作生成 Request VO")
  8 +@Data
  9 +public class AiWriteGenerateReqVO {
  10 +
  11 + @Schema(description = "写作内容提示", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1.撰写:田忌赛马;2.回复:不批")
  12 + private String prompt;
  13 +
  14 + @Schema(description = "原文", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "领导我要辞职")
  15 + private String originalContent;
  16 +
  17 + @Schema(description = "长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  18 + @NotNull(message = "长度不能为空")
  19 + private Integer length;
  20 +
  21 + @Schema(description = "格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  22 + @NotNull(message = "格式不能为空")
  23 + private Integer format;
  24 +
  25 + @Schema(description = "语气", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  26 + @NotNull(message = "语气不能为空")
  27 + private Integer tone;
  28 +
  29 + @Schema(description = "语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  30 + @NotNull(message = "语言不能为空")
  31 + private Integer language;
  32 +
  33 +
  34 + @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  35 + private Integer type; //参见 AiWriteTypeEnum 枚举
  36 +}
0 \ No newline at end of file 37 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.dataobject.write;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import com.baomidou.mybatisplus.annotation.IdType;
  5 +import com.baomidou.mybatisplus.annotation.TableId;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Data;
  8 +import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum;
  9 +
  10 +/**
  11 + * AI 写作 DO
  12 + *
  13 + * @author xiaoxin
  14 + */
  15 +@TableName(value = "ai_write", autoResultMap = true)
  16 +@Data
  17 +public class AiWriteDO extends BaseDO {
  18 +
  19 + /**
  20 + * 编号
  21 + */
  22 + @TableId(type = IdType.AUTO)
  23 + private Long id;
  24 +
  25 + /**
  26 + * 用户编号
  27 + */
  28 + private Long userId;
  29 +
  30 + /**
  31 + * 写作类型
  32 + * <p>
  33 + * 枚举 {@link AiWriteTypeEnum}
  34 + */
  35 + private Integer type;
  36 +
  37 + /**
  38 + * 生成内容提示
  39 + */
  40 + private String prompt;
  41 +
  42 + /**
  43 + * 生成的内容
  44 + */
  45 + private String generatedContent;
  46 +
  47 + /**
  48 + * 原文
  49 + */
  50 + private String originalContent;
  51 +
  52 + /**
  53 + * 长度提示词
  54 + */
  55 + private Integer length;
  56 +
  57 + /**
  58 + * 格式提示词
  59 + */
  60 + private Integer format;
  61 +
  62 + /**
  63 + * 语气提示词
  64 + */
  65 + private Integer tone;
  66 +
  67 + /**
  68 + * 语言提示词
  69 + */
  70 + private Integer language;
  71 +
  72 + /**
  73 + * 模型
  74 + */
  75 + private String model;
  76 +
  77 + /**
  78 + * 平台
  79 + */
  80 + private String platform;
  81 +
  82 + /**
  83 + * 错误信息
  84 + */
  85 + private String errorMessage;
  86 +
  87 +}
0 \ No newline at end of file 88 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.mysql.write;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +/**
  8 + * AI 音乐 Mapper
  9 + *
  10 + * @author xiaoxin
  11 + */
  12 +@Mapper
  13 +public interface AiWriteMapper extends BaseMapperX<AiWriteDO> {
  14 +}
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 { @@ -42,7 +42,7 @@ public interface AiMusicService {
42 * 42 *
43 * @param updateReqVO 更新信息 43 * @param updateReqVO 更新信息
44 */ 44 */
45 - void updateMyMusic(@Valid AiMusicUpdateReqVO updateReqVO, Long userId); 45 + void updateMyMusic(@Valid AiMusicUpdateMyReqVO updateReqVO, Long userId);
46 46
47 /** 47 /**
48 * 删除AI 音乐 48 * 删除AI 音乐
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; @@ -9,6 +9,7 @@ import cn.hutool.http.HttpUtil;
9 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; 9 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
10 import cn.iocoder.yudao.framework.common.pojo.PageResult; 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
11 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO; 11 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
  12 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateMyReqVO;
12 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateReqVO; 13 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateReqVO;
13 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; 14 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
14 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; 15 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
@@ -111,7 +112,7 @@ public class AiMusicServiceImpl implements AiMusicService { @@ -111,7 +112,7 @@ public class AiMusicServiceImpl implements AiMusicService {
111 } 112 }
112 113
113 @Override 114 @Override
114 - public void updateMyMusic(AiMusicUpdateReqVO updateReqVO, Long userId) { 115 + public void updateMyMusic(AiMusicUpdateMyReqVO updateReqVO, Long userId) {
115 // 校验音乐是否存在 116 // 校验音乐是否存在
116 AiMusicDO musicDO = validateMusicExists(updateReqVO.getId()); 117 AiMusicDO musicDO = validateMusicExists(updateReqVO.getId());
117 if (ObjUtil.notEqual(musicDO.getUserId(), userId)) { 118 if (ObjUtil.notEqual(musicDO.getUserId(), userId)) {
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.write;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO;
  5 +import reactor.core.publisher.Flux;
  6 +
  7 +/**
  8 + * AI 写作 Service 接口
  9 + *
  10 + * @author xiaoxin
  11 + */
  12 +public interface AiWriteService {
  13 +
  14 +
  15 + /**
  16 + * 生成写作内容
  17 + *
  18 + * @param generateReqVO 作文生成请求参数
  19 + * @param userId 用户编号
  20 + * @return 生成结果
  21 + */
  22 + Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId);
  23 +
  24 +
  25 +}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.write;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  5 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
  6 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
  7 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  9 +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO;
  10 +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
  11 +import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO;
  12 +import cn.iocoder.yudao.module.ai.dal.mysql.write.AiWriteMapper;
  13 +import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
  14 +import cn.iocoder.yudao.module.ai.enums.write.*;
  15 +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
  16 +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
  17 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
  18 +import jakarta.annotation.Resource;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.ai.chat.model.ChatResponse;
  21 +import org.springframework.ai.chat.model.StreamingChatModel;
  22 +import org.springframework.ai.chat.prompt.ChatOptions;
  23 +import org.springframework.ai.chat.prompt.Prompt;
  24 +import org.springframework.ai.ollama.api.OllamaOptions;
  25 +import org.springframework.ai.openai.OpenAiChatOptions;
  26 +import org.springframework.ai.qianfan.QianFanChatOptions;
  27 +import org.springframework.stereotype.Service;
  28 +import reactor.core.publisher.Flux;
  29 +
  30 +import java.util.Objects;
  31 +
  32 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
  33 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  34 +
  35 +/**
  36 + * AI 写作 Service 实现类
  37 + *
  38 + * @author xiaoxin
  39 + */
  40 +@Service
  41 +@Slf4j
  42 +public class AiWriteServiceImpl implements AiWriteService {
  43 +
  44 + @Resource
  45 + private AiApiKeyService apiKeyService;
  46 + @Resource
  47 + private AiChatModelService chatModalService;
  48 + @Resource
  49 + private AiWriteMapper writeMapper;
  50 +
  51 +
  52 + @Override
  53 + public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) {
  54 + //TODO 芋艿 写作的模型配置放哪好 先用千问测试
  55 + // 1.1 校验模型
  56 + AiChatModelDO model = chatModalService.validateChatModel(14L);
  57 + StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
  58 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
  59 + ChatOptions chatOptions = buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
  60 +
  61 + //1.2 插入写作信息
  62 + AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class);
  63 + writeMapper.insert(writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()));
  64 +
  65 + //2.1 构建提示词
  66 + Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions);
  67 + Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
  68 + // 2.2 流式返回
  69 + StringBuffer contentBuffer = new StringBuffer();
  70 + return streamResponse.map(chunk -> {
  71 + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null;
  72 + newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
  73 + contentBuffer.append(newContent);
  74 + // 响应结果
  75 + return success(newContent);
  76 + }).doOnComplete(() -> {
  77 + writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setGeneratedContent(contentBuffer.toString()));
  78 + }).doOnError(throwable -> {
  79 + log.error("[AI Write][generateReqVO({}) 发生异常]", generateReqVO, throwable);
  80 + writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setErrorMessage(throwable.getMessage()));
  81 + }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR)));
  82 + }
  83 +
  84 +
  85 + private String buildWritingPrompt(AiWriteGenerateReqVO generateReqVO) {
  86 + String template;
  87 + Integer writeType = generateReqVO.getType();
  88 + String format = AiWriteFormatEnum.valueOfFormat(generateReqVO.getFormat()).getName();
  89 + String tone = AiWriteToneEnum.valueOfTone(generateReqVO.getTone()).getName();
  90 + String language = AiLanguageEnum.valueOfLanguage(generateReqVO.getLanguage()).getName();
  91 + String length = AiWriteLengthEnum.valueOfLength(generateReqVO.getLength()).getName();
  92 + if (Objects.equals(writeType, AiWriteTypeEnum.WRITING.getType())) {
  93 + template = "请撰写一篇关于 [{}] 的文章。文章的内容格式为:[{}],语气为:[{}],语言为:[{}],长度为:[{}]。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。";
  94 + return StrUtil.format(template, generateReqVO.getPrompt(), format, tone, language, length);
  95 + } else if (Objects.equals(writeType, AiWriteTypeEnum.REPLY.getType())) {
  96 + template = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复的内容格式为:[{}],语气为:[{}],语言为:[{}],长度为:[{}]。不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。";
  97 + return StrUtil.format(template, generateReqVO.getOriginalContent(), generateReqVO.getPrompt(), format, tone, language, length);
  98 + } else {
  99 + throw new IllegalArgumentException(StrUtil.format("未知写作类型({})", writeType));
  100 + }
  101 + }
  102 +
  103 + // TODO 芋艿:复用
  104 + private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
  105 + Float temperatureF = temperature != null ? temperature.floatValue() : null;
  106 + //noinspection EnhancedSwitchMigration
  107 + switch (platform) {
  108 + case OPENAI:
  109 + return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  110 + case OLLAMA:
  111 + return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
  112 + case YI_YAN:
  113 + // TODO 芋艿:貌似 model 只要一设置,就报错
  114 +// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  115 + return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  116 + case XING_HUO:
  117 + return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
  118 + .setMaxTokens(maxTokens);
  119 + case QIAN_WEN:
  120 + return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
  121 + default:
  122 + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
  123 + }
  124 + }
  125 +
  126 +}