瀏覽代碼

【代码优化】AI:优化 xinghuo 的接入,复用 OpenAI,同时将 ws 替换成 htt 调用

YunaiV 9 月之前
父節點
當前提交
2d36ec4858
共有 19 個文件被更改,包括 259 次插入951 次删除
  1. 2 7
      yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
  2. 9 17
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
  3. 22 34
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java
  4. 1 4
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java
  5. 119 102
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatClient.java
  6. 0 53
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java
  7. 55 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java
  8. 0 77
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoOptions.java
  9. 0 149
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoApi.java
  10. 0 48
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletion.java
  11. 0 8
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionMessage.java
  12. 0 107
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionRequest.java
  13. 2 4
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
  14. 0 118
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java
  15. 25 43
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java
  16. 0 131
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java
  17. 3 3
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java
  18. 3 10
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java
  19. 18 36
      yudao-server/src/main/resources/application.yaml

+ 2 - 7
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml

@@ -33,11 +33,6 @@
             <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId>
             <version>${spring-ai.version}</version>
         </dependency>
-<!--        <dependency>-->
-<!--            <groupId>org.springframework.ai</groupId>-->
-<!--            <artifactId>spring-ai-vertex-ai-gemini</artifactId>-->
-<!--            <version>1.0.3</version>-->
-<!--        </dependency>-->
 
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
@@ -61,8 +56,8 @@
 
         <!-- Test 测试相关 -->
         <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
     </dependencies>

+ 9 - 17
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java

@@ -5,8 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl;
 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.ai.core.model.xinghuo.XingHuoChatClient;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
+import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -36,21 +35,14 @@ public class YudaoAiAutoConfiguration {
     @Bean
     @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
     public XingHuoChatClient xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
-        YudaoAiProperties.XingHuoProperties xingHuoProperties = yudaoAiProperties.getXinghuo();
-        // 转换配置
-        XingHuoOptions xingHuoOptions = new XingHuoOptions();
-        xingHuoOptions.setChatModel(xingHuoProperties.getModel());
-        xingHuoOptions.setTopK(xingHuoProperties.getTopK());
-        xingHuoOptions.setTemperature(xingHuoProperties.getTemperature());
-        xingHuoOptions.setMaxTokens(xingHuoProperties.getMaxTokens());
-        return new XingHuoChatClient(
-                new XingHuoApi(
-                        xingHuoProperties.getAppId(),
-                        xingHuoProperties.getAppKey(),
-                        xingHuoProperties.getSecretKey()
-                ),
-                xingHuoOptions
-        );
+        YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo();
+        XingHuoChatOptions options = XingHuoChatOptions.builder()
+                .model(properties.getModel())
+                .temperature(properties.getTemperature())
+                .maxTokens(properties.getMaxTokens())
+                .topK(properties.getTopK())
+                .build();
+        return new XingHuoChatClient(properties.getAppKey(), properties.getSecretKey(), options);
     }
 
     @Bean

+ 22 - 34
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java

@@ -1,55 +1,45 @@
 package cn.iocoder.yudao.framework.ai.config;
 
-import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
 import lombok.Data;
-import lombok.experimental.Accessors;
-import org.springframework.ai.autoconfigure.openai.OpenAiImageProperties;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 
 /**
- * ai 自动配置
+ * 芋道 AI 配置类
  *
  * @author fansili
- * @time 2024/4/12 16:29
  * @since 1.0
  */
-@Data
-@Accessors(chain = true)
 @ConfigurationProperties(prefix = "yudao.ai")
+@Data
 public class YudaoAiProperties {
 
+    /**
+     * 讯飞星火
+     */
     private XingHuoProperties xinghuo;
-    private OpenAiImageProperties openAiImage;
-    private MidjourneyProperties midjourney;
-    private SunoProperties suno;
-
-    @Data
-    @Accessors(chain = true)
-    public static class ChatProperties {
 
-        private boolean enable = false;
-
-        private AiPlatformEnum aiPlatform;
-
-        private Float temperature;
-
-        private Float topP;
+    /**
+     * Midjourney 绘图
+     */
+    private MidjourneyProperties midjourney;
 
-        private Integer topK;
-        /**
-         * 用于限制模型生成token的数量,max_tokens设置的是生成上限,并不表示一定会生成这么多的token数量
-         */
-        private Integer maxTokens;
-    }
+    /**
+     * Suno 音乐
+     */
+    private SunoProperties suno;
 
     @Data
-    public static class XingHuoProperties extends ChatProperties {
+    public static class XingHuoProperties {
 
+        private String enable;
         private String appId;
         private String appKey;
         private String secretKey;
-        private XingHuoChatModel model;
+
+        private String model;
+        private Float temperature;
+        private Integer maxTokens;
+        private Integer topK;
 
     }
 
@@ -57,8 +47,9 @@ public class YudaoAiProperties {
     public static class MidjourneyProperties {
 
         private String enable;
-        private String apiKey;
         private String baseUrl;
+
+        private String apiKey;
         private String notifyUrl;
 
     }
@@ -68,9 +59,6 @@ public class YudaoAiProperties {
 
         private boolean enable = false;
 
-        /**
-         * API 服务的基本地址
-         */
         private String baseUrl;
 
     }

+ 1 - 4
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java

@@ -12,7 +12,6 @@ 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.ai.core.model.xinghuo.XingHuoChatClient;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
 import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties;
 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel;
@@ -178,11 +177,9 @@ public class AiClientFactoryImpl implements AiClientFactory {
     private static XingHuoChatClient buildXingHuoChatClient(String key) {
         List<String> keys = StrUtil.split(key, '|');
         Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式");
-        String appId = keys.get(0);
         String appKey = keys.get(1);
         String secretKey = keys.get(2);
-        XingHuoApi xingHuoApi = new XingHuoApi(appId, appKey, secretKey);
-        return new XingHuoChatClient(xingHuoApi);
+        return new XingHuoChatClient(appKey, secretKey);
     }
 
     /**

+ 119 - 102
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatClient.java

@@ -1,145 +1,162 @@
 package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
 
-import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.exceptions.ExceptionUtil;
-import cn.iocoder.yudao.framework.ai.core.exception.ChatException;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest;
+import cn.hutool.core.lang.Assert;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
 import org.springframework.ai.chat.model.ChatModel;
 import org.springframework.ai.chat.model.ChatResponse;
 import org.springframework.ai.chat.model.Generation;
-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.model.ModelOptionsUtils;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata;
+import org.springframework.ai.retry.RetryUtils;
 import org.springframework.http.ResponseEntity;
-import org.springframework.retry.RetryCallback;
-import org.springframework.retry.RetryContext;
-import org.springframework.retry.RetryListener;
 import org.springframework.retry.support.RetryTemplate;
 import reactor.core.publisher.Flux;
 
-import java.time.Duration;
+import java.util.HashMap;
 import java.util.List;
-import java.util.stream.Collectors;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions.MODEL_DEFAULT;
 
-// TODO @fan:参考 yiyan 的修改建议,调整下 xinghuo 的实现;可以等 yiyan 修改完建议,然后我 review 完,再改这个哈;
 /**
- * 讯飞星火 client
- * <p>
- * author: fansili
- * time: 2024/3/11 10:19
+ * 讯飞星火 {@link ChatModel} 实现类
+ *
+ * @author fansili
  */
 @Slf4j
-public class XingHuoChatClient implements ChatModel, StreamingChatModel {
-
-    private XingHuoApi xingHuoApi;
-
-    private XingHuoOptions xingHuoOptions;
-
-    public final RetryTemplate retryTemplate = RetryTemplate.builder()
-            // 最大重试次数 10
-            .maxAttempts(3)
-            .retryOn(ChatException.class)
-            // 最大重试5次,第一次间隔3000ms,第二次3000ms * 2,第三次3000ms * 3,以此类推,最大间隔3 * 60000ms
-            .exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000))
-            .withListener(new RetryListener() {
-                @Override
-                public <T extends Object, E extends Throwable> void onError(RetryContext context,
-                                                                            RetryCallback<T, E> callback, Throwable throwable) {
-                    System.err.println("正在重试... " + ExceptionUtil.getMessage(throwable));
-                    log.warn("重试异常:" + context.getRetryCount(), throwable);
-                }
+public class XingHuoChatClient implements ChatModel {
+
+    private static final String BASE_URL = "https://spark-api-open.xf-yun.com";
 
-                ;
-            })
-            .build();
+    private final XingHuoChatOptions defaultOptions;
+    private final RetryTemplate retryTemplate;
 
-    public XingHuoChatClient(XingHuoApi xingHuoApi) {
-        this.xingHuoApi = xingHuoApi;
+    /**
+     * 星火兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本
+     *
+     * 不过要注意,星火没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它
+     */
+    private final OpenAiApi openAiApi;
+
+    public XingHuoChatClient(String apiKey, String secretKey) {
+        this(apiKey, secretKey,
+                XingHuoChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build());
     }
 
-    public XingHuoChatClient(XingHuoApi xingHuoApi, XingHuoOptions xingHuoOptions) {
-        this.xingHuoApi = xingHuoApi;
-        this.xingHuoOptions = xingHuoOptions;
+    public XingHuoChatClient(String apiKey, String secretKey, XingHuoChatOptions options) {
+       this(apiKey, secretKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE);
+    }
+
+    public XingHuoChatClient(String apiKey, String secretKey, XingHuoChatOptions options, RetryTemplate retryTemplate) {
+        Assert.notEmpty(apiKey, "apiKey 不能为空");
+        Assert.notEmpty(secretKey, "secretKey 不能为空");
+        Assert.notNull(options, "options 不能为空");
+        Assert.notNull(retryTemplate, "retryTemplate 不能为空");
+        this.openAiApi = new OpenAiApi(BASE_URL, apiKey + ":" + secretKey);
+        this.defaultOptions = options;
+        this.retryTemplate = retryTemplate;
     }
 
     @Override
     public ChatResponse call(Prompt prompt) {
+        OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false);
         return this.retryTemplate.execute(ctx -> {
-            // ctx 会有重试的信息
-            // 获取 chatOptions 属性
-            XingHuoOptions chatOptions = this.getChatOptions(prompt);
-            // 创建 request 请求,stream模式需要供应商支持
-            XingHuoChatCompletionRequest request = this.createRequest(prompt, chatOptions);
-            // 调用 callWithFunctionSupport 发送请求
-            ResponseEntity<XingHuoChatCompletion> response = xingHuoApi.chatCompletionEntity(request, chatOptions.getChatModel());
-            // 获取结果封装 ChatResponse
-            return new ChatResponse(List.of(new Generation(response.getBody().getPayload().getChoices().getText().get(0).getContent())));
+            // 1.1 发起调用
+            ResponseEntity<OpenAiApi.ChatCompletion> completionEntity = openAiApi.chatCompletionEntity(request);
+            // 1.2 校验结果
+            OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();
+            if (chatCompletion == null) {
+                log.warn("No chat completion returned for prompt: {}", prompt);
+                return new ChatResponse(List.of());
+            }
+            List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();
+            if (choices == null) {
+                log.warn("No choices returned for prompt: {}", prompt);
+                return new ChatResponse(List.of());
+            }
+
+            // 2. 转换 ChatResponse 返回
+            List<Generation> generations = choices.stream().map(choice -> {
+                Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice));
+                if (choice.finishReason() != null) {
+                    generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null));
+                }
+                return generation;
+            }).toList();
+            return new ChatResponse(generations,
+                    OpenAiChatResponseMetadata.from(completionEntity.getBody()));
         });
     }
 
-    @Override
-    public ChatOptions getDefaultOptions() {
-        // TODO 芋艿:需要跟进下
-        throw new UnsupportedOperationException();
+    private Map<String, Object> toMap(String id, OpenAiApi.ChatCompletion.Choice choice) {
+        Map<String, Object> map = new HashMap<>();
+        OpenAiApi.ChatCompletionMessage message = choice.message();
+        if (message.role() != null) {
+            map.put("role", message.role().name());
+        }
+        if (choice.finishReason() != null) {
+            map.put("finishReason", choice.finishReason().name());
+        }
+        map.put("id", id);
+        return map;
     }
 
     @Override
     public Flux<ChatResponse> stream(Prompt prompt) {
-        // 获取 chatOptions 属性
-        XingHuoOptions chatOptions = this.getChatOptions(prompt);
-        // 创建 request 请求,stream模式需要供应商支持
-        XingHuoChatCompletionRequest request = this.createRequest(prompt, chatOptions);
-        // 发送请求
-        Flux<XingHuoChatCompletion> response = this.xingHuoApi.chatCompletionStream(request, chatOptions.getChatModel());
-        return response.map(res -> {
-            String content = res.getPayload().getChoices().getText().stream()
-                    .map(item -> item.getContent()).collect(Collectors.joining());
-            return new ChatResponse(List.of(new Generation(content)));
+        OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true);
+        return this.retryTemplate.execute(ctx -> {
+            // 1. 发起调用
+            Flux<OpenAiApi.ChatCompletionChunk> response = this.openAiApi.chatCompletionStream(request);
+            return response.map(chatCompletion -> {
+                String id = chatCompletion.id();
+                // 2. 转换 ChatResponse 返回
+                List<Generation> generations = chatCompletion.choices().stream().map(choice -> {
+                    String finish = (choice.finishReason() != null ? choice.finishReason().name() : "");
+                    Generation generation = new Generation(choice.delta().content(),
+                            Map.of("id", id, "role", choice.delta().role().name(), "finishReason", finish));
+                    if (choice.finishReason() != null) {
+                        generation = generation.withGenerationMetadata(
+                                ChatGenerationMetadata.from(choice.finishReason().name(), null));
+                    }
+                    return generation;
+                }).toList();
+                return new ChatResponse(generations);
+            });
         });
     }
 
-    private XingHuoOptions getChatOptions(Prompt prompt) {
-        // 两个都为null 则没有配置文件
-        if (xingHuoOptions == null && prompt.getOptions() == null) {
-            throw new ChatException("ChatOptions 未配置参数!");
-        }
-        // 优先使用 Prompt 里面的 ChatOptions
-        ChatOptions options = xingHuoOptions;
+    OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
+        // 1. 构建 ChatCompletionMessage 对象
+        List<OpenAiApi.ChatCompletionMessage> chatCompletionMessages = prompt.getInstructions().stream().map(m ->
+                new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList();
+        OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream);
+
+        // 2.1 补充 prompt 内置的 options
         if (prompt.getOptions() != null) {
-            options = (ChatOptions) prompt.getOptions();
+            if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {
+                OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,
+                        ChatOptions.class, OpenAiChatOptions.class);
+                request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class);
+            } else {
+                throw new IllegalArgumentException("Prompt options are not of type ChatOptions: "
+                        + prompt.getOptions().getClass().getSimpleName());
+            }
         }
-        // Prompt 里面是一个 ChatOptions,用户可以随意传入,这里做一下判断
-        if (!(options instanceof XingHuoOptions)) {
-            throw new ChatException("Prompt 传入的不是 XingHuoOptions!");
+        // 2.2 补充默认 options
+        if (this.defaultOptions != null) {
+            request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class);
         }
-        return (XingHuoOptions) options;
+        return request;
     }
 
-    private XingHuoChatCompletionRequest createRequest(Prompt prompt, XingHuoOptions xingHuoOptions) {
-        // 创建 header
-        XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id(xingHuoApi.getAppId());
-        // 创建 params
-        XingHuoChatCompletionRequest.Parameter.Chat chatParameter = new XingHuoChatCompletionRequest.Parameter.Chat();
-        BeanUtil.copyProperties(xingHuoOptions, chatParameter);
-        chatParameter.setDomain(xingHuoOptions.getChatModel().getModel());
-        XingHuoChatCompletionRequest.Parameter parameter = new XingHuoChatCompletionRequest.Parameter().setChat(chatParameter);
-        // 创建 payload text 信息
-        List<XingHuoChatCompletionRequest.Payload.Message.Text> texts = prompt.getInstructions().stream().map(message -> {
-            XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text();
-            text.setContent(message.getContent());
-            text.setRole(message.getMessageType().getValue());
-            return text;
-        }).collect(Collectors.toList());
-        // 创建 payload
-        XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload()
-                .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(texts));
-        // 创建 request
-        return new XingHuoChatCompletionRequest()
-                .setHeader(header)
-                .setParameter(parameter)
-                .setPayload(payload);
+    @Override
+    public ChatOptions getDefaultOptions() {
+         return XingHuoChatOptions.fromOptions(defaultOptions);
     }
+
 }

+ 0 - 53
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java

@@ -1,53 +0,0 @@
-package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
-
-import lombok.Getter;
-
-/**
- * 讯飞星火 模型
- *
- * 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
- *
- * 1tokens 约等于1.5个中文汉字 或者 0.8个英文单词
- * 星火V1.5支持[搜索]内置插件;星火V2.0、V3.0和V3.5支持[搜索]、[天气]、[日期]、[诗词]、[字词]、[股票]六个内置插件
- * 星火V3.5 现已支持system、Function Call 功能。
- *
- * author: fansili
- * time: 2024/3/11 10:12
- */
-@Getter
-public enum XingHuoChatModel {
-
-//    文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
-//    general指向V1.5版本;
-//    generalv2指向V2版本;
-//    generalv3指向V3版本;
-//    generalv3.5指向V3.5版本;
-
-    XING_HUO_1_5("星火大模型1.5",  "general", "/v1.1/chat"),
-    XING_HUO_2_0("星火大模型2.0", "generalv2", "/v2.1/chat"),
-    XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"),
-    XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"),
-
-    ;
-
-    XingHuoChatModel(String name, String model, String uri) {
-        this.name = name;
-        this.model = model;
-        this.uri = uri;
-    }
-
-    private String name;
-
-    private String model;
-
-    private String uri;
-
-    public static XingHuoChatModel valueOfModel(String model) {
-        for (XingHuoChatModel itemEnum : XingHuoChatModel.values()) {
-            if (itemEnum.getModel().equals(model)) {
-                return itemEnum;
-            }
-        }
-        throw new IllegalArgumentException("Invalid MessageType value: " + model);
-    }
-}

+ 55 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.ai.chat.prompt.ChatOptions;
+
+/**
+ * 讯飞星火 {@link ChatOptions} 实现类
+ *
+ * 参考文档:<a href="https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html">HTTP 调用</a>
+ *
+ * @author fansili
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class XingHuoChatOptions implements ChatOptions {
+
+    public static final String MODEL_DEFAULT = "generalv3.5";
+
+    /**
+     * 模型
+     */
+    private String model;
+    /**
+     * 温度
+     */
+    private Float temperature;
+    /**
+     * 最大 Token
+     */
+    private Integer maxTokens;
+    /**
+     * K 个候选
+     */
+    private Integer topK;
+
+    @Override
+    public Float getTopP() {
+        return null;
+    }
+
+    public static XingHuoChatOptions fromOptions(XingHuoChatOptions fromOptions) {
+        return XingHuoChatOptions.builder()
+                .model(fromOptions.getModel())
+                .temperature(fromOptions.getTemperature())
+                .maxTokens(fromOptions.getMaxTokens())
+                .topK(fromOptions.getTopK())
+                .build();
+    }
+
+}

+ 0 - 77
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoOptions.java

@@ -1,77 +0,0 @@
-package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
-
-import org.springframework.ai.chat.prompt.ChatOptions;
-import lombok.Data;
-import lombok.experimental.Accessors;
-
-/**
- * 讯飞星火
- * <p>
- * author: fansili
- * time: 2024/3/16 20:29
- */
-@Data
-@Accessors(chain = true)
-public class XingHuoOptions implements ChatOptions {
-
-    // TODO @fan:这里 model 参数,然后使用 string
-    /**
-     * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
-     * <p>
-     * 指定访问的领域:
-     * general指向V1.5版本;
-     * generalv2指向V2版本;
-     * generalv3指向V3版本;
-     * generalv3.5指向V3.5版本;
-     * 注意:不同的取值对应的url也不一样!
-     */
-    private XingHuoChatModel chatModel = XingHuoChatModel.XING_HUO_3_5;
-    /**
-     * 取值范围 (0,1] ,默认值0.5
-     */
-    private Float temperature;
-    /**
-     * V1.5取值为[1,4096]
-     * V2.0、V3.0和V3.5取值为[1,8192],默认为2048。
-     */
-    private Integer maxTokens;
-    /**
-     * 取值为[1,6],默认为4
-     */
-    private Integer topK;
-    /**
-     * 需要保障用户下的唯一性,用于关联用户会话
-     */
-    private String chatId;
-
-    @Override
-    public Float getTemperature() {
-        return this.temperature;
-    }
-
-//    @Override
-//    public void setTemperature(Float temperature) {
-//        this.temperature = temperature;
-//    }
-
-    @Override
-    public Float getTopP() {
-        return null;
-    }
-
-//    @Override
-//    public void setTopP(Float topP) {
-//
-//    }
-
-    @Override
-    public Integer getTopK() {
-        return this.topK;
-    }
-
-//    @Override
-//    public void setTopK(Integer topK) {
-//        this.topK = topK;
-//    }
-
-}

+ 0 - 149
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoApi.java

@@ -1,149 +0,0 @@
-package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
-
-import cn.hutool.http.HttpUtil;
-import cn.hutool.json.JSONUtil;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
-import lombok.Data;
-import org.springframework.http.HttpStatusCode;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.reactive.function.client.WebClient;
-import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import java.net.URI;
-import java.nio.charset.Charset;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.text.SimpleDateFormat;
-import java.util.*;
-import java.util.stream.Collectors;
-
-// TODO @fan:讯飞使用 spring websocket 接入,还是 okhttp?确认了,未使用的最好删除下,反正 git 也能找回 history
-/**
- * 讯飞星火 属性、api
- * <p>
- * 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
- * <p>
- * author: fansili
- * time: 2024/3/11 10:12
- */
-@Data
-public class XingHuoApi {
-
-    private static final String DEFAULT_BASE_URL = "wss://spark-api.xf-yun.com";
-
-    private String appId;
-    private String appKey;
-    private String secretKey;
-    private WebClient webClient;
-    // 创建 WebSocketClient 实例
-    private ReactorNettyWebSocketClient socketClient = new ReactorNettyWebSocketClient();
-
-    public XingHuoApi(String appId, String appKey, String secretKey) {
-        this.appId = appId;
-        this.appKey = appKey;
-        this.secretKey = secretKey;
-    }
-
-    public ResponseEntity<XingHuoChatCompletion> chatCompletionEntity(XingHuoChatCompletionRequest request, XingHuoChatModel xingHuoChatModel) {
-        String authUrl;
-        try {
-//            XingHuoChatModel useChatModel;
-            authUrl = getAuthorizationUrl("spark-api.xf-yun.com", xingHuoChatModel.getUri());
-        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
-            throw new RuntimeException(e);
-        }
-        // wss 请求的 URI
-        URI uri = URI.create(authUrl);
-        // 发起 wss 请求并处理响应
-        Flux<XingHuoChatCompletion> messageFlux = Flux.create(sink -> {
-            socketClient.execute(uri, session ->
-                            session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request))))
-                                    .thenMany(session.receive()
-                                            .map(WebSocketMessage -> {
-                                                return JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class);
-                                            })
-                                            .doOnNext(sink::next) // 将接收到的消息推送到 Flux 中
-                                            .doOnError(sink::error) // 处理错误
-                                            .doOnTerminate(sink::complete)) // 完成时关闭 sink
-                                    .then())
-                    .subscribe(); // 订阅以开始会话
-        });
-        // 阻塞获取所有结果
-        List<XingHuoChatCompletion> responseList = messageFlux.collectList().block();
-        // 拼接 content
-        String responseContent = responseList.stream().map(item -> {
-            // 获取 content
-            return item.getPayload().getChoices().getText().stream().map(XingHuoChatCompletion.Text::getContent).collect(Collectors.joining());
-        }).collect(Collectors.joining());
-        // 将多个合并成一个
-        XingHuoChatCompletion xingHuoChatCompletion = new XingHuoChatCompletion();
-        xingHuoChatCompletion.setPayload(new XingHuoChatCompletion.Payload().setChoices(new XingHuoChatCompletion.Choices().setText(List.of(new XingHuoChatCompletion.Text().setContent(responseContent)))));
-        return new ResponseEntity<>(xingHuoChatCompletion, HttpStatusCode.valueOf(200));
-    }
-
-
-    /**
-     * 获取验证请求url
-     *
-     * @return
-     */
-    public String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException {
-        // 获取鉴权时间 date
-        SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
-        format.setTimeZone(TimeZone.getTimeZone("GMT"));
-        String date = format.format(new Date());
-
-        // 获取signature_origin字段
-        StringBuilder builder = new StringBuilder("host: ").append(host).append("\n").
-                append("date: ").append(date).append("\n").
-                append("GET ").append(path).append(" HTTP/1.1");
-
-        // 获得signatue
-        Charset charset = Charset.forName("UTF-8");
-        Mac mac = Mac.getInstance("hmacsha256");
-        SecretKeySpec sp = new SecretKeySpec(secretKey.getBytes(charset), "hmacsha256");
-        mac.init(sp);
-        byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset));
-        String signature = Base64.getEncoder().encodeToString(basebefore);
-        //获得 authorization_origin
-        String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", appKey, "hmac-sha256", "host date request-line", signature);
-        //获得authorization
-        String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset));
-        // 获取httpUrl
-        Map<String, Object> param = new HashMap<>();
-        param.put("authorization", authorization);
-        param.put("date", date);
-        param.put("host", host);
-
-        String toParams = HttpUtil.toParams(param);
-        return "wss://" + host + path + "?" + toParams;
-    }
-
-    public Flux<XingHuoChatCompletion> chatCompletionStream(XingHuoChatCompletionRequest request, XingHuoChatModel xingHuoChatModel) {
-        String authUrl;
-        try {
-            authUrl = getAuthorizationUrl("spark-api.xf-yun.com", xingHuoChatModel.getUri());
-        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
-            throw new RuntimeException(e);
-        }
-        // wss 请求的 URI
-        URI uri = URI.create(authUrl);
-        // 发起 wss 请求并处理响应
-        // 创建一个 Flux 来处理接收到的消息
-        return Flux.create(sink -> {
-            socketClient.execute(uri, session ->
-                            session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request))))
-                                    .thenMany(session.receive()
-                                            .map(WebSocketMessage -> JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class))
-                                            .doOnNext(sink::next) // 将接收到的消息推送到 Flux 中
-                                            .doOnError(sink::error) // 处理错误
-                                            .doOnTerminate(sink::complete)) // 完成时关闭 sink
-                                    .then())
-                    .subscribe(); // 订阅以开始会话
-        });
-    }
-}

+ 0 - 48
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletion.java

@@ -1,48 +0,0 @@
-package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
-
-import lombok.Data;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-/**
- * author: fansili
- * time: 2024/3/11 10:20
- */
-@Data
-@Accessors(chain = true)
-public class XingHuoChatCompletion {
-    private Header header;
-    private Payload payload;
-
-    @Data
-    @Accessors(chain = true)
-    public static class Header {
-        private int code;
-        private String message;
-        private String sid;
-        private int status;
-    }
-
-    @Data
-    @Accessors(chain = true)
-    public static class Payload {
-        private Choices choices;
-    }
-
-    @Data
-    @Accessors(chain = true)
-    public static class Choices {
-        private int status;
-        private int seq;
-        private List<Text> text;
-    }
-
-    @Data
-    @Accessors(chain = true)
-    public static class Text {
-        private String content;
-        private String role;
-        private int index;
-    }
-}

+ 0 - 8
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionMessage.java

@@ -1,8 +0,0 @@
-package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
-
-/**
- * author: fansili
- * time: 2024/3/11 10:20
- */
-public class XingHuoChatCompletionMessage {
-}

+ 0 - 107
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionRequest.java

@@ -1,107 +0,0 @@
-package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
-
-import lombok.Data;
-import lombok.Getter;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-/**
- * 讯飞星火 request
- *
- * author: fansili
- * time: 2024/3/11 10:20
- */
-@Data
-@Accessors(chain = true)
-public class XingHuoChatCompletionRequest {
-
-    private Header header;
-    private Parameter parameter;
-    private Payload payload;
-
-    @Data
-    @Accessors(chain = true)
-    public static class Header {
-        private String app_id;
-        private String uid;
-    }
-
-    @Data
-    @Accessors(chain = true)
-    public static class Parameter {
-        private Chat chat;
-
-        @Data
-        @Accessors(chain = true)
-        public static class Chat {
-            /**
-             * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
-             *
-             * 指定访问的领域:
-             * general指向V1.5版本;
-             * generalv2指向V2版本;
-             * generalv3指向V3版本;
-             * generalv3.5指向V3.5版本;
-             * 注意:不同的取值对应的url也不一样!
-             */
-            private String domain = "generalv3.5";
-            /**
-             * 取值范围 (0,1] ,默认值0.5
-             */
-            private Float temperature;
-            /**
-             * V1.5取值为[1,4096]
-             * V2.0、V3.0和V3.5取值为[1,8192],默认为2048。
-             */
-            private Integer max_tokens;
-            /**
-             * 取值为[1,6],默认为4
-             */
-            private Integer top_k;
-            /**
-             * 	需要保障用户下的唯一性,用于关联用户会话
-             */
-            private String chat_id;
-        }
-    }
-
-    @Data
-    @Accessors(chain = true)
-    public static class Payload {
-        private Message message;
-
-        @Data
-        @Accessors(chain = true)
-        public static class Message {
-            private List<Text> text;
-
-
-            @Data
-            @Accessors(chain = true)
-            public static class Text {
-                /**
-                 * 角色
-                 */
-                private String role;
-                /**
-                 * 消息内容
-                 */
-                private String content;
-                private Integer index;
-
-                @Getter
-                public static enum Role {
-                    SYSTEM("system"),
-                    USER("user"),
-                    ASSISTANT("assistant");
-                    private String name;
-
-                    private Role(String name) {
-                        this.name = name;
-                    }
-                }
-            }
-        }
-    }
-}

+ 2 - 4
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java

@@ -2,8 +2,7 @@ package cn.iocoder.yudao.framework.ai.core.util;
 
 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.ai.core.model.xinghuo.XingHuoChatOptions;
 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
 import org.springframework.ai.chat.messages.*;
 import org.springframework.ai.chat.prompt.ChatOptions;
@@ -31,8 +30,7 @@ public class AiUtils {
 //                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);
+                return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build();
             case QIAN_WEN:
                 return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
             default:

+ 0 - 118
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java

@@ -1,118 +0,0 @@
-package cn.iocoder.yudao.framework.ai.chat;
-
-import cn.hutool.http.HttpUtil;
-import cn.hutool.json.JSONUtil;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest;
-import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
-import org.springframework.web.reactive.socket.client.WebSocketClient;
-import reactor.core.publisher.Flux;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.nio.charset.Charset;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.text.SimpleDateFormat;
-import java.util.*;
-
-// TODO 芋艿:整理单测
-/**
- * author: fansili
- * time: 2024/3/13 20:47
- */
-public class XingHuoChatClientMainTests {
-
-
-    private static final String HOST_URL = "http://spark-api.xf-yun.com/v3.5/chat";
-    private static final String API_KEY = "cb6415c19d6162cda07b47316fcb0416";
-    private static final String API_SECRET = "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh";
-
-    public static void main(String[] args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException {
-        String authUrl = getAuthorizationUrl("spark-api.xf-yun.com", "/v3.5/chat");
-        System.err.println(authUrl);
-
-        XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id("13c8cca6");
-        XingHuoChatCompletionRequest.Parameter parameter
-                = new XingHuoChatCompletionRequest.Parameter()
-                .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain("generalv3.5"));
-
-
-        XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text();
-        text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName());
-        text.setContent("世界上最好的开发语言是什么?");
-        XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload()
-                .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text)));
-        XingHuoChatCompletionRequest request = new XingHuoChatCompletionRequest()
-                .setHeader(header)
-                .setParameter(parameter)
-                .setPayload(payload);
-
-        System.err.println(JSONUtil.toJsonPrettyStr(request));
-
-
-        // 创建 WebSocketClient 实例
-        WebSocketClient client = new ReactorNettyWebSocketClient();
-
-        // wss 请求的 URI
-        URI uri = URI.create(authUrl);
-
-        // 发起 wss 请求并处理响应
-        client.execute(uri, session ->
-                        // 使用会话发送消息,并接收回应
-                        session.send(Flux.just(session.textMessage(JSONUtil.toJsonStr(request))))
-                                .thenMany(session.receive()
-                                        .map(WebSocketMessage -> {
-                                            System.err.println(WebSocketMessage.getPayloadAsText());
-                                            return JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class);
-                                        })
-                                        .log()) // 打印接收到的消息
-                                .then())
-                .block(); // 等待操作完成或超时
-
-        // 阻止退出
-        Scanner scanner = new Scanner(System.in);
-        scanner.nextLine();
-    }
-
-
-    /**
-     * 获取验证请求url
-     *
-     * @return
-     */
-    public static String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException {
-        // 获取鉴权时间 date
-        SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
-        format.setTimeZone(TimeZone.getTimeZone("GMT"));
-        String date = format.format(new Date());
-
-        // 获取signature_origin字段
-        StringBuilder builder = new StringBuilder("host: ").append(host).append("\n").
-                append("date: ").append(date).append("\n").
-                append("GET ").append(path).append(" HTTP/1.1");
-
-        // 获得signatue
-        Charset charset = Charset.forName("UTF-8");
-        Mac mac = Mac.getInstance("hmacsha256");
-        SecretKeySpec sp = new SecretKeySpec(API_SECRET.getBytes(charset), "hmacsha256");
-        mac.init(sp);
-        byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset));
-        String signature = Base64.getEncoder().encodeToString(basebefore);
-        //获得 authorization_origin
-        String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", API_KEY, "hmac-sha256", "host date request-line", signature);
-        //获得authorization
-        String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset));
-        // 获取httpUrl
-        Map<String, Object> param = new HashMap<>();
-        param.put("authorization", authorization);
-        param.put("date", date);
-        param.put("host", host);
-
-        String toParams = HttpUtil.toParams(param);
-        return "wss://" + host + path + "?" + toParams;
-    }
-
-}

+ 25 - 43
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java

@@ -1,11 +1,7 @@
 package cn.iocoder.yudao.framework.ai.chat;
 
 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
-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.ai.core.model.xinghuo.api.XingHuoApi;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 import org.springframework.ai.chat.messages.Message;
 import org.springframework.ai.chat.messages.SystemMessage;
 import org.springframework.ai.chat.messages.UserMessage;
@@ -15,58 +11,44 @@ import reactor.core.publisher.Flux;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Scanner;
-import java.util.function.Consumer;
 
-// TODO 芋艿:整理单测
 /**
- * 讯飞星火 tests
- * <p>
- * author: fansili
- * time: 2024/3/11 11:00
+ * {@link XingHuoChatClient} 集成测试
+ *
+ * @author fansili
  */
 public class XingHuoChatClientTests {
 
-    private XingHuoChatClient xingHuoChatClient;
-
-    @Before
-    public void setup() {
-        // 初始化 xingHuoChatClient
-        xingHuoChatClient = new XingHuoChatClient(
-                new XingHuoApi(
-                        "13c8cca6",
-                        "cb6415c19d6162cda07b47316fcb0416",
-                        "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"
-                ),
-                new XingHuoOptions().setChatModel(XingHuoChatModel.XING_HUO_3_5)
-        );
-    }
+    private final XingHuoChatClient client = new XingHuoChatClient(
+            "cb6415c19d6162cda07b47316fcb0416",
+            "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh");
 
     @Test
-    public void callTest() {
+    public void testCall() {
+        // 准备参数
         List<Message> messages = new ArrayList<>();
         messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
-        messages.add(new UserMessage("长沙怎么样?"));
+        messages.add(new UserMessage("1 + 1 = ?"));
 
-        ChatResponse call = xingHuoChatClient.call(new Prompt(messages));
-        System.err.println(call.getResult());
+        // 调用
+        ChatResponse response = client.call(new Prompt(messages));
+        // 打印结果
+        System.err.println(response);
     }
 
     @Test
-    public void streamTest() {
+    public void testStream() {
+        // 准备参数
         List<Message> messages = new ArrayList<>();
         messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
-        messages.add(new UserMessage("长沙怎么样?"));
-
-        Flux<ChatResponse> stream = xingHuoChatClient.stream(new Prompt(messages));
-        stream.subscribe(new Consumer<ChatResponse>() {
-            @Override
-            public void accept(ChatResponse chatResponse) {
-                System.err.print(chatResponse.getResult().getOutput().getContent());
-            }
-        });
-        // 阻止退出
-        Scanner scanner = new Scanner(System.in);
-        scanner.nextLine();
+        messages.add(new UserMessage("1 + 1 = ?"));
+
+        // 调用
+        Flux<ChatResponse> flux = client.stream(new Prompt(messages));
+        // 打印结果
+        List<ChatResponse> responses = flux.collectList().block();
+        assert responses != null;
+        responses.forEach(System.err::println);
     }
+
 }

+ 0 - 131
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java

@@ -1,131 +0,0 @@
-package cn.iocoder.yudao.framework.ai.chat;
-
-import cn.hutool.http.HttpUtil;
-import cn.hutool.json.JSONUtil;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion;
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest;
-import okhttp3.*;
-import org.jetbrains.annotations.NotNull;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import java.net.MalformedURLException;
-import java.nio.charset.Charset;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.text.SimpleDateFormat;
-import java.util.*;
-
-// TODO 芋艿:整理单测
-/**
- * 讯飞星火 tests
- * <p>
- * author: fansili
- * time: 2024/3/11 11:00
- */
-public class XingHuoOkHttpTests {
-
-    private static final String HOST_URL = "http://spark-api.xf-yun.com/v3.5/chat";
-    private static final String API_KEY = "cb6415c19d6162cda07b47316fcb0416";
-    private static final String API_SECRET = "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh";
-
-    private XingHuoChatClient xingHuoChatClient;
-
-    public static void main(String[] args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException {
-        String authUrl = getAuthorizationUrl("spark-api.xf-yun.com", "/v3.5/chat");
-        System.err.println(authUrl);
-
-        XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id("13c8cca6");
-        XingHuoChatCompletionRequest.Parameter parameter
-                = new XingHuoChatCompletionRequest.Parameter()
-                .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain("generalv3.5"));
-
-
-        XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text();
-        text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName());
-        text.setContent("世界上最好的开发语言是什么?");
-        XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload()
-                .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text)));
-        XingHuoChatCompletionRequest request = new XingHuoChatCompletionRequest()
-                .setHeader(header)
-                .setParameter(parameter)
-                .setPayload(payload);
-
-        System.err.println(JSONUtil.toJsonPrettyStr(request));
-
-        OkHttpClient client = new OkHttpClient();
-        Request request2 = new Request.Builder()
-                .url(authUrl) // 替换为你的 wss URL
-                .build();
-
-        WebSocketListener webSocketListener = new WebSocketListener() {
-
-            @Override
-            public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
-                boolean send = webSocket.send(JSONUtil.toJsonStr(request));
-                System.err.println("发送 -> " + send);
-                System.err.println("链接成功!");
-            }
-
-            @Override
-            public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
-                super.onMessage(webSocket, text);
-//                System.err.println(text);
-                XingHuoChatCompletion response = JSONUtil.toBean(text, XingHuoChatCompletion.class);
-                for (XingHuoChatCompletion.Text text1 : response.getPayload().getChoices().getText()) {
-                    System.err.print(text1.getContent());
-                }
-            }
-        };
-
-        WebSocket webSocket = client.newWebSocket(request2, webSocketListener);
-//        webSocket.send(JSONUtil.toJsonStr(request));
-
-
-        // Trigger shutdown of the dispatcher's executor so this process can exit cleanly.
-        client.dispatcher().executorService().shutdown();
-        // 阻止退出
-        Scanner scanner = new Scanner(System.in);
-        scanner.nextLine();
-    }
-
-
-    /**
-     * 获取验证请求url
-     *
-     * @return
-     */
-    public static String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException {
-        // 获取鉴权时间 date
-        SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
-        format.setTimeZone(TimeZone.getTimeZone("GMT"));
-        String date = format.format(new Date());
-
-        // 获取signature_origin字段
-        StringBuilder builder = new StringBuilder("host: ").append(host).append("\n").
-                append("date: ").append(date).append("\n").
-                append("GET ").append(path).append(" HTTP/1.1");
-
-        // 获得signatue
-        Charset charset = Charset.forName("UTF-8");
-        Mac mac = Mac.getInstance("hmacsha256");
-        SecretKeySpec sp = new SecretKeySpec(API_SECRET.getBytes(charset), "hmacsha256");
-        mac.init(sp);
-        byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset));
-        String signature = Base64.getEncoder().encodeToString(basebefore);
-        //获得 authorization_origin
-        String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", API_KEY, "hmac-sha256", "host date request-line", signature);
-        //获得authorization
-        String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset));
-        // 获取httpUrl
-        Map<String, Object> param = new HashMap<>();
-        param.put("authorization", authorization);
-        param.put("date", date);
-        param.put("host", host);
-
-        String toParams = HttpUtil.toParams(param);
-        return "wss://" + host + path + "?" + toParams;
-    }
-
-}

+ 3 - 3
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java

@@ -1,9 +1,9 @@
 package cn.iocoder.yudao.framework.ai.image;
 
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.springframework.ai.image.ImagePrompt;
 import org.springframework.ai.image.ImageResponse;
-import org.junit.Before;
-import org.junit.Test;
 import org.springframework.ai.openai.OpenAiImageModel;
 import org.springframework.ai.openai.api.OpenAiImageApi;
 
@@ -25,7 +25,7 @@ public class OpenAiImageClientTests {
 
     private OpenAiImageModel openAiImageClient;
 
-    @Before
+    @BeforeEach
     public void setup() {
         // 初始化 openAiImageClient
         this.openAiImageClient = new OpenAiImageModel(

+ 3 - 10
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java

@@ -1,8 +1,7 @@
 package cn.iocoder.yudao.framework.ai.music;
 
 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
 
 import java.util.List;
 
@@ -13,14 +12,8 @@ import java.util.List;
  */
 public class SunoTests {
 
-    private SunoApi sunoApi;
-
-    @Before
-    public void setup() {
-        String url = "https://suno-55ishh05u-status2xxs-projects.vercel.app";
-//        String url = "http://127.0.0.1:3001";
-        this.sunoApi = new SunoApi(url);
-    }
+    private final SunoApi sunoApi = new SunoApi("https://suno-55ishh05u-status2xxs-projects.vercel.app");
+//    private final SunoApi sunoApi = new SunoApi("http://127.0.0.1:3001");
 
     @Test
     public void selectById() {

+ 18 - 36
yudao-server/src/main/resources/application.yaml

@@ -162,11 +162,6 @@ spring:
       base-url: https://api.gptsapi.net
     stabilityai:
       api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
-    vertex:
-      ai:
-        gemini:
-          project-id: 1 # TODO 芋艿:缺配置
-          location: 2
     qianfan: # 文心一言
       api-key: x0cuLZ7XsaTCU08vuJWO87Lg
       secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
@@ -176,37 +171,24 @@ spring:
         tongyi:
           api-key: sk-Zsd81gZYg7
 
-yudao.ai:
-  xinghuo:
-    enable: true
-    aiPlatform: XING_HUO # TODO @fan:建议每个都独立配置属性类
-    max-tokens: 1500
-    temperature: 0.85
-    topP: 0.8
-    topK: 0
-    appId: 13c8cca6
-    appKey: cb6415c19d6162cda07b47316fcb0416
-    secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh
-    model: XING_HUO_3_5
-  qianwen:
-    enable: true
-    aiPlatform: QIAN_WEN
-    max-tokens: 1500
-    temperature: 0.85
-    topP: 0.8
-    topK: 0
-    api-key: sk-Zsd81gZYg7
-    model: QWEN_TURBO
-  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:
+  ai:
+    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
 
 --- #################### 芋道相关配置 ####################