diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageApi.java
new file mode 100644
index 000000000..bcc03f2ee
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageApi.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.framework.ai.imageopenai;
+
+import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest;
+import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse;
+import cn.iocoder.yudao.framework.ai.util.JacksonUtil;
+import io.netty.channel.ChannelOption;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.netty.http.client.HttpClient;
+
+import java.time.Duration;
+
+/**
+ * open ai image
+ *
+ * author: fansili
+ * time: 2024/3/17 09:53
+ */
+public class OpenAiImageApi {
+
+ private static final String DEFAULT_BASE_URL = "https://api.openai.com";
+ private String apiKey = "your-api-key";
+ // 发送请求 webClient
+ private final WebClient webClient;
+
+ public OpenAiImageApi(String apiKey) {
+ this.apiKey = apiKey;
+ // 创建一个HttpClient实例并设置超时
+ HttpClient httpClient = HttpClient.create()
+ .responseTimeout(Duration.ofSeconds(300)) // 设置响应超时时间为30秒
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000 * 100); // 设置连接超时为5秒
+ this.webClient = WebClient.builder()
+ .baseUrl(DEFAULT_BASE_URL)
+ .clientConnector(new ReactorClientHttpConnector(httpClient))
+ .build();
+ }
+
+ public OpenAiImageResponse createImage(OpenAiImageRequest request) {
+ String res = webClient.post()
+ .uri(uriBuilder -> uriBuilder.path("/v1/images/generations").build())
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + apiKey)
+ // 设置请求体(这里假设jsonStr是一个JSON格式的字符串)
+ .body(BodyInserters.fromValue(JacksonUtil.toJson(request)))
+ // 发送请求并获取响应体
+ .retrieve()
+ // 转换响应体为String类型
+ .bodyToMono(String.class)
+ .block();
+ // TODO: 2024/3/17 这里发送请求会失败!
+ return null;
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageClient.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageClient.java
new file mode 100644
index 000000000..a1bb59db1
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageClient.java
@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.framework.ai.imageopenai;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.iocoder.yudao.framework.ai.chat.ChatException;
+import cn.iocoder.yudao.framework.ai.chatyiyan.exception.YiYanApiException;
+import cn.iocoder.yudao.framework.ai.image.ImageClient;
+import cn.iocoder.yudao.framework.ai.image.ImageOptions;
+import cn.iocoder.yudao.framework.ai.image.ImagePrompt;
+import cn.iocoder.yudao.framework.ai.image.ImageResponse;
+import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest;
+import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse;
+import jdk.jfr.Frequency;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.retry.RetryCallback;
+import org.springframework.retry.RetryContext;
+import org.springframework.retry.RetryListener;
+import org.springframework.retry.support.RetryTemplate;
+
+import java.time.Duration;
+
+/**
+ * open ai 生成 image
+ *
+ * author: fansili
+ * time: 2024/3/17 09:51
+ */
+@Slf4j
+public class OpenAiImageClient implements ImageClient {
+
+ /**
+ * open image ai
+ */
+ private OpenAiImageApi openAiImageApi;
+ /**
+ * 默认使用的 ImageOptions
+ */
+ private OpenAiImageOptions defaultImageOptions;
+
+
+ public final RetryTemplate retryTemplate = RetryTemplate.builder()
+ // 最大重试次数 10
+ .maxAttempts(10)
+ .retryOn(YiYanApiException.class)
+ // 最大重试5次,第一次间隔3000ms,第二次3000ms * 2,第三次3000ms * 3,以此类推,最大间隔3 * 60000ms
+ .exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000))
+ .withListener(new RetryListener() {
+ @Override
+ public void onError(RetryContext context,
+ RetryCallback callback, Throwable throwable) {
+ log.warn("重试异常:" + context.getRetryCount(), throwable);
+ };
+ })
+ .build();
+
+ public OpenAiImageClient(OpenAiImageApi openAiImageApi, OpenAiImageOptions defaultImageOptions) {
+ this.openAiImageApi = openAiImageApi;
+ this.defaultImageOptions = defaultImageOptions;
+ }
+
+ @Override
+ public ImageResponse call(ImagePrompt imagePrompt) {
+ return this.retryTemplate.execute(ctx -> {
+ // 检查是否配置了 OpenAiImageOptions
+ if (defaultImageOptions == null && imagePrompt.getOptions() == null) {
+ throw new ChatException("OpenAiImageOptions 未配置参数!");
+ }
+ // 优先使用 request 中的 ImageOptions
+ ImageOptions useImageOptions = imagePrompt.getOptions() == null ? defaultImageOptions : imagePrompt.getOptions();
+ if (!(useImageOptions instanceof OpenAiImageOptions)) {
+ throw new ChatException("配置信息不正确,传入的必须是 OpenAiImageOptions!");
+ }
+ // 转换 OpenAiImageOptions
+ OpenAiImageOptions openAiImageOptions = (OpenAiImageOptions) useImageOptions;
+ // 创建请求
+ OpenAiImageRequest request = new OpenAiImageRequest();
+ BeanUtil.copyProperties(openAiImageOptions, request);
+ // 发送请求
+ OpenAiImageResponse response = openAiImageApi.createImage(request);
+ return null;
+ });
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageOptions.java
new file mode 100644
index 000000000..f18ccd298
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageOptions.java
@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.framework.ai.imageopenai;
+
+import cn.iocoder.yudao.framework.ai.image.ImageOptions;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * open ai 配置文件
+ *
+ * 文档地址:https://platform.openai.com/docs/api-reference/images/create
+ *
+ * author: fansili
+ * time: 2024/3/17 09:53
+ */
+@Data
+@Accessors(chain = true)
+public class OpenAiImageOptions implements ImageOptions {
+
+ // 必填字段,用于描述期望生成图像的文字说明。对于dall-e-2模型最大长度为1000个字符,对于dall-e-3模型最大长度为4000个字符。
+ private String prompt;
+
+ // 可选字段,默认为dall-e-2
+ // 指定用于生成图像的模型名称。
+ private String model = "dall-e-2";
+
+ // 可选字段,默认为1
+ // 生成图像的数量,必须在1到10之间。对于dall-e-3模型,目前仅支持n=1。
+ private Integer n = 1;
+
+ // 可选字段,默认为standard
+ // 设置生成图像的质量。hd质量将创建细节更丰富、图像整体一致性更高的图片。该参数仅对dall-e-3模型有效。
+ private String quality = "standard";
+
+ // 可选字段,默认为url
+ // 返回生成图像的格式。必须是url或b64_json中的一种。URL链接的有效期是从生成图像后开始计算的60分钟内有效。
+ private String responseFormat = "url";
+
+ // 可选字段,默认为1024x1024
+ // 生成图像的尺寸大小。对于dall-e-2模型,尺寸可为256x256, 512x512, 或 1024x1024。对于dall-e-3模型,尺寸可为1024x1024, 1792x1024, 或 1024x1792。
+ private String imageSize = "1024x1024";
+
+ // 可选字段,默认为vivid
+ // 图像生成的风格。可为vivid(生动)或natural(自然)。vivid会使模型偏向生成超现实和戏剧性的图像,而natural则会让模型产出更自然、不那么超现实的图像。该参数仅对dall-e-3模型有效。
+ private String style = "vivid";
+
+ // 可选字段
+ // 代表您的终端用户的唯一标识符,有助于OpenAI监控并检测滥用行为。了解更多信息请参考官方文档。
+ private String endUserId;
+
+ //
+ // 适配 spring ai
+
+ @Override
+ public Integer getN() {
+ return this.n;
+ }
+
+ @Override
+ public String getModel() {
+ return this.model;
+ }
+
+ @Override
+ public Integer getWidth() {
+ return null;
+ }
+
+ @Override
+ public Integer getHeight() {
+ return null;
+ }
+
+ @Override
+ public String getResponseFormat() {
+ return this.responseFormat;
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageRequest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageRequest.java
new file mode 100644
index 000000000..865dbd136
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageRequest.java
@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.framework.ai.imageopenai.api;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * open ai 配置文件
+ *
+ * 文档地址:https://platform.openai.com/docs/api-reference/images/create
+ *
+ * author: fansili
+ * time: 2024/3/17 09:53
+ */
+@Data
+@Accessors(chain = true)
+public class OpenAiImageRequest {
+
+ // 必填字段,用于描述期望生成图像的文字说明。对于dall-e-2模型最大长度为1000个字符,对于dall-e-3模型最大长度为4000个字符。
+ @JsonProperty("prompt")
+ private String prompt;
+
+ // 可选字段,默认为dall-e-2、dall-e-3
+ // 指定用于生成图像的模型名称。
+ @JsonProperty("model")
+ private String model = "dall-e-2";
+
+ // 可选字段,默认为1
+ // 生成图像的数量,必须在1到10之间。对于dall-e-3模型,目前仅支持n=1。
+ @JsonProperty("n")
+ private Integer n = 1;
+
+ // 可选字段,默认为standard
+ // 设置生成图像的质量。hd质量将创建细节更丰富、图像整体一致性更高的图片。该参数仅对dall-e-3模型有效。
+ @JsonProperty("quality")
+ private String quality = "standard";
+
+ // 可选字段,默认为url
+ // 返回生成图像的格式。必须是url或b64_json中的一种。URL链接的有效期是从生成图像后开始计算的60分钟内有效。
+ @JsonProperty("response_format")
+ private String responseFormat = "url";
+
+ // 可选字段,默认为1024x1024
+ // 生成图像的尺寸大小。对于dall-e-2模型,尺寸可为256x256, 512x512, 或 1024x1024。对于dall-e-3模型,尺寸可为1024x1024, 1792x1024, 或 1024x1792。
+ @JsonProperty("size")
+ private String imageSize = "1024x1024";
+
+ // 可选字段,默认为vivid
+ // 图像生成的风格。可为vivid(生动)或natural(自然)。vivid会使模型偏向生成超现实和戏剧性的图像,而natural则会让模型产出更自然、不那么超现实的图像。该参数仅对dall-e-3模型有效。
+ @JsonProperty("style")
+ private String style = "vivid";
+
+ // 可选字段
+ // 代表您的终端用户的唯一标识符,有助于OpenAI监控并检测滥用行为。了解更多信息请参考官方文档。
+ @JsonProperty("user")
+ private String endUserId;
+
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageResponse.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageResponse.java
new file mode 100644
index 000000000..04de1494d
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageResponse.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.ai.imageopenai.api;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+/**
+ * image 返回
+ *
+ * author: fansili
+ * time: 2024/3/17 10:27
+ */
+@Data
+@Accessors(chain = true)
+public class OpenAiImageResponse {
+
+ private long created;
+ private List- data;
+
+ @Data
+ @Accessors(chain = true)
+ public static class Item {
+
+ private String url;
+
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/util/JacksonUtil.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/util/JacksonUtil.java
new file mode 100644
index 000000000..046a481dc
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/util/JacksonUtil.java
@@ -0,0 +1,79 @@
+package cn.iocoder.yudao.framework.ai.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+
+import java.io.IOException;
+
+/**
+ * Jackson工具类
+ *
+ * author: fansili
+ * time: 2024/3/17 10:13
+ */
+public class JacksonUtil {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * 初始化 ObjectMapper 以美化输出(即格式化JSON内容)
+ */
+ static {
+ // 美化输出(缩进)
+ objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
+ // 忽略值为 null 的属性
+ objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ // 配置一个模块来将 Long 类型转换为 String 类型
+ SimpleModule module = new SimpleModule();
+ module.addSerializer(Long.class, ToStringSerializer.instance);
+ objectMapper.registerModule(module);
+ }
+
+ /**
+ * 将对象转换为 JSON 字符串
+ *
+ * @param obj 需要序列化的Java对象
+ * @return 序列化后的 JSON 字符串
+ * @throws JsonProcessingException 当 JSON 序列化过程中出现错误时抛出异常
+ */
+ public static String toJson(Object obj) {
+ try {
+ return objectMapper.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 将 JSON 字符串反序列化为指定类型的对象
+ *
+ * @param json JSON 字符串
+ * @param clazz 目标类型 Class 对象
+ * @param 泛型类型参数
+ * @return 反序列化后的 Java 对象
+ * @throws IOException 当 JSON 解析过程中出现错误时抛出异常
+ */
+ public static T fromJson(String json, Class clazz) {
+ try {
+ return objectMapper.readValue(json, clazz);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 将对象转换为格式化的 JSON 字符串(已启用 INDENT_OUTPUT 功能,所以所有方法都会返回格式化后的 JSON)
+ *
+ * @param obj 需要序列化的Java对象
+ * @return 格式化后的 JSON 字符串
+ * @throws JsonProcessingException 当 JSON 序列化过程中出现错误时抛出异常
+ */
+ public static String toFormattedJson(Object obj) {
+ // 已在类初始化时设置了 SerializationFeature.INDENT_OUTPUT,此处无需额外操作
+ return toJson(obj);
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java
index 2c2b44685..14b839747 100644
--- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java
@@ -23,9 +23,9 @@ public class QianWenChatClientTests {
@Before
public void setup() {
QianWenApi qianWenApi = new QianWenApi(
- "",
- "",
- "",
+ "LTAI5tNTVhXW4fLKUjMrr98z",
+ "ZJ0JQeyjzxxm5CfeTV6k1wNE9UsvZP",
+ "f0c1088824594f589c8f10567ccd929f_p_efm",
null
);
qianWenChatClient = new QianWenChatClient(
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java
new file mode 100644
index 000000000..83d556930
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.ai.image;
+
+import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageApi;
+import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageClient;
+import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageOptions;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * author: fansili
+ * time: 2024/3/17 10:40
+ */
+public class OpenAiImageClientTests {
+
+
+ private OpenAiImageClient openAiImageClient;
+
+ @Before
+ public void setup() {
+ // 初始化 openAiImageClient
+ this.openAiImageClient = new OpenAiImageClient(
+ new OpenAiImageApi(""),
+ new OpenAiImageOptions()
+ );
+ }
+
+ @Test
+ public void callTest() {
+ openAiImageClient.call(new ImagePrompt("我和我的小狗,一起在北极和企鹅玩排球。"));
+ }
+}