Browse Source

增加讯飞星火

cherishsince 1 year ago
parent
commit
de32611794
14 changed files with 861 additions and 0 deletions
  1. 1 0
      yudao-module-ai/pom.xml
  2. 8 0
      yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
  3. 154 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoApi.java
  4. 130 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatClient.java
  5. 44 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatModel.java
  6. 48 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletion.java
  7. 8 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionMessage.java
  8. 92 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionRequest.java
  9. 14 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/exception/XingHuoApiException.java
  10. 117 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java
  11. 56 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java
  12. 130 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java
  13. 54 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatTests.java
  14. 5 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/package-info.java

+ 1 - 0
yudao-module-ai/pom.xml

@@ -14,6 +14,7 @@
     <modules>
         <module>yudao-module-ai-api</module>
         <module>yudao-module-ai-biz</module>
+        <module>yudao-spring-boot-starter-ai</module>
     </modules>
 
     <properties>

+ 8 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml

@@ -100,6 +100,14 @@
             <version>4.12.0</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.projectreactor.netty</groupId>
+            <artifactId>reactor-netty</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 154 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoApi.java

@@ -0,0 +1,154 @@
+package cn.iocoder.yudao.framework.ai.chatxinghuo;
+
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletion;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletionRequest;
+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;
+
+/**
+ * 讯飞星火 属性、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;
+    private XingHuoChatModel useChatModel;
+    // 创建 WebSocketClient 实例
+    private ReactorNettyWebSocketClient socketClient = new ReactorNettyWebSocketClient();
+
+    public XingHuoApi(String appId, String appKey, String secretKey, XingHuoChatModel useChatModel) {
+        this.appId = appId;
+        this.appKey = appKey;
+        this.secretKey = secretKey;
+        this.useChatModel = useChatModel;
+
+    }
+
+    public ResponseEntity<XingHuoChatCompletion> chatCompletionEntity(XingHuoChatCompletionRequest request) {
+        String authUrl;
+        try {
+            authUrl = getAuthorizationUrl("spark-api.xf-yun.com", useChatModel.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) {
+        String authUrl;
+        try {
+            authUrl = getAuthorizationUrl("spark-api.xf-yun.com", useChatModel.getUri());
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            throw new RuntimeException(e);
+        }
+        System.err.println(authUrl);
+        System.err.println(JSONUtil.toJsonPrettyStr(request));
+        // wss 请求的 URI
+        URI uri = URI.create(authUrl);
+        // 发起 wss 请求并处理响应
+        // 创建一个 Flux 来处理接收到的消息
+        Flux<XingHuoChatCompletion> messageFlux = 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(); // 订阅以开始会话
+        });
+        return messageFlux;
+    }
+}

+ 130 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatClient.java

@@ -0,0 +1,130 @@
+package cn.iocoder.yudao.framework.ai.chatxinghuo;
+
+import cn.iocoder.yudao.framework.ai.chat.ChatClient;
+import cn.iocoder.yudao.framework.ai.chat.ChatResponse;
+import cn.iocoder.yudao.framework.ai.chat.Generation;
+import cn.iocoder.yudao.framework.ai.chat.StreamingChatClient;
+import cn.iocoder.yudao.framework.ai.chat.prompt.Prompt;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletion;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletionMessage;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletionRequest;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.exception.XingHuoApiException;
+import cn.iocoder.yudao.framework.ai.model.function.AbstractFunctionCallSupport;
+import lombok.extern.slf4j.Slf4j;
+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.List;
+import java.util.stream.Collectors;
+
+/**
+ * 讯飞星火 client
+ * <p>
+ * author: fansili
+ * time: 2024/3/11 10:19
+ */
+@Slf4j
+public class XingHuoChatClient extends AbstractFunctionCallSupport<XingHuoChatCompletionMessage, XingHuoChatCompletionRequest, ResponseEntity<XingHuoChatCompletion>>
+        implements ChatClient, StreamingChatClient {
+
+    private XingHuoApi xingHuoApi;
+
+    public final RetryTemplate retryTemplate = RetryTemplate.builder()
+            // 最大重试次数 10
+            .maxAttempts(10)
+            .retryOn(XingHuoApiException.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) {
+                    log.warn("重试异常:" + context.getRetryCount(), throwable);
+                }
+
+                ;
+            })
+            .build();
+
+    public XingHuoChatClient(XingHuoApi xingHuoApi) {
+        super(null);
+        this.xingHuoApi = xingHuoApi;
+    }
+
+    @Override
+    public ChatResponse call(Prompt prompt) {
+
+        return this.retryTemplate.execute(ctx -> {
+            // ctx 会有重试的信息
+            // 创建 request 请求,stream模式需要供应商支持
+            XingHuoChatCompletionRequest request = this.createRequest(prompt, false);
+            // 调用 callWithFunctionSupport 发送请求
+            ResponseEntity<XingHuoChatCompletion> response = this.callWithFunctionSupport(request);
+            // 获取结果封装 ChatResponse
+            return new ChatResponse(List.of(new Generation(response.getBody().getPayload().getChoices().getText().get(0).getContent())));
+        });
+    }
+
+    private XingHuoChatCompletionRequest createRequest(Prompt prompt, boolean b) {
+        // 创建 header
+        XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id(xingHuoApi.getAppId());
+        // 创建 params
+        XingHuoChatCompletionRequest.Parameter parameter = new XingHuoChatCompletionRequest.Parameter()
+                .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain(xingHuoApi.getUseChatModel().getValue()));
+        // 创建 payload text 信息
+        XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text();
+        text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName());
+        text.setContent(prompt.getContents());
+        // 创建 payload
+        XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload()
+                .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text)));
+        // 创建 request
+        return new XingHuoChatCompletionRequest()
+                .setHeader(header)
+                .setParameter(parameter)
+                .setPayload(payload);
+    }
+
+    @Override
+    public Flux<ChatResponse> stream(Prompt prompt) {
+        // 创建 request 请求,stream模式需要供应商支持
+        XingHuoChatCompletionRequest request = this.createRequest(prompt, false);
+        // 发送请求
+        Flux<XingHuoChatCompletion> response = this.xingHuoApi.chatCompletionStream(request);
+        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)));
+        });
+    }
+
+    @Override
+    protected XingHuoChatCompletionRequest doCreateToolResponseRequest(XingHuoChatCompletionRequest previousRequest, XingHuoChatCompletionMessage responseMessage, List<XingHuoChatCompletionMessage> conversationHistory) {
+        return null;
+    }
+
+    @Override
+    protected List<XingHuoChatCompletionMessage> doGetUserMessages(XingHuoChatCompletionRequest request) {
+        return null;
+    }
+
+    @Override
+    protected XingHuoChatCompletionMessage doGetToolResponseMessage(ResponseEntity<XingHuoChatCompletion> response) {
+        return null;
+    }
+
+    @Override
+    protected ResponseEntity<XingHuoChatCompletion> doChatCompletion(XingHuoChatCompletionRequest request) {
+        return xingHuoApi.chatCompletionEntity(request);
+    }
+
+    @Override
+    protected boolean isToolFunctionCall(ResponseEntity<XingHuoChatCompletion> response) {
+        return false;
+    }
+}

+ 44 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatModel.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.framework.ai.chatxinghuo;
+
+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 value, String uri) {
+        this.name = name;
+        this.value = value;
+        this.uri = uri;
+    }
+
+    private String name;
+
+    private String value;
+
+    private String uri;
+}

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

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.framework.ai.chatxinghuo.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;
+    }
+}

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

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

+ 92 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionRequest.java

@@ -0,0 +1,92 @@
+package cn.iocoder.yudao.framework.ai.chatxinghuo.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 = "general";
+            private Double temperature = 0.5;
+            private Integer max_tokens = 2048;
+        }
+    }
+
+    @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;
+                    }
+                }
+            }
+        }
+    }
+}

+ 14 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/exception/XingHuoApiException.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.framework.ai.chatxinghuo.exception;
+
+/**
+ * 讯飞星火 exception
+ *
+ * author: fansili
+ * time: 2024/3/11 10:22
+ */
+public class XingHuoApiException extends RuntimeException {
+
+    public XingHuoApiException(String message) {
+        super(message);
+    }
+}

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

@@ -0,0 +1,117 @@
+package cn.iocoder.yudao.framework.ai.chat;
+
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletion;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.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.*;
+
+/**
+ * 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;
+    }
+
+}

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

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.framework.ai.chat;
+
+import cn.iocoder.yudao.framework.ai.chat.prompt.Prompt;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.XingHuoApi;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.XingHuoChatClient;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.XingHuoChatModel;
+import org.junit.Before;
+import org.junit.Test;
+import reactor.core.publisher.Flux;
+
+import java.util.Scanner;
+import java.util.function.Consumer;
+
+/**
+ * 讯飞星火 tests
+ * <p>
+ * author: fansili
+ * time: 2024/3/11 11:00
+ */
+public class XingHuoChatClientTests {
+
+    private XingHuoChatClient xingHuoChatClient;
+
+    @Before
+    public void setup() {
+        // 初始化 xingHuoChatClient
+        xingHuoChatClient = new XingHuoChatClient(
+                new XingHuoApi(
+                        "13c8cca6",
+                        "cb6415c19d6162cda07b47316fcb0416",
+                        "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh",
+                        XingHuoChatModel.XING_HUO_3_5
+                )
+        );
+    }
+
+    @Test
+    public void callTest() {
+        ChatResponse call = xingHuoChatClient.call(new Prompt("java和go那个性能更好!"));
+        System.err.println(call.getResult());
+    }
+
+    @Test
+    public void streamTest() {
+        Flux<ChatResponse> stream = xingHuoChatClient.stream(new Prompt("java和go那个性能更好!"));
+        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();
+    }
+}

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

@@ -0,0 +1,130 @@
+package cn.iocoder.yudao.framework.ai.chat;
+
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.XingHuoChatClient;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletion;
+import cn.iocoder.yudao.framework.ai.chatxinghuo.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.*;
+
+/**
+ * 讯飞星火 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;
+    }
+
+}

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

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.framework.ai.chat;
+
+import cn.iocoder.yudao.framework.ai.chat.prompt.Prompt;
+import cn.iocoder.yudao.framework.ai.chatyiyan.YiYanApi;
+import cn.iocoder.yudao.framework.ai.chatyiyan.YiYanChatClient;
+import cn.iocoder.yudao.framework.ai.chatyiyan.YiYanChatModel;
+import org.junit.Before;
+import org.junit.Test;
+import reactor.core.publisher.Flux;
+
+import java.util.Scanner;
+import java.util.function.Consumer;
+
+/**
+ * chat 文心一言
+ *
+ * author: fansili
+ * time: 2024/3/12 20:59
+ */
+public class YiYanChatTests {
+
+    private YiYanChatClient yiYanChatClient;
+
+    @Before
+    public void setup() {
+        YiYanApi yiYanApi = new YiYanApi(
+                "x0cuLZ7XsaTCU08vuJWO87Lg",
+                "R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK",
+                YiYanChatModel.ERNIE4_3_5_8K,
+                86400
+        );
+        yiYanChatClient = new YiYanChatClient(yiYanApi);
+    }
+
+    @Test
+    public void callTest() {
+        ChatResponse call = yiYanChatClient.call(new Prompt("什么编程语言最好?"));
+        System.err.println(call.getResult());
+    }
+
+    @Test
+    public void streamTest() {
+        Flux<ChatResponse> fluxResponse = yiYanChatClient.stream(new Prompt("用java帮我写一个快排算法?"));
+        fluxResponse.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();
+    }
+}

+ 5 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/package-info.java

@@ -0,0 +1,5 @@
+/**
+ * author: fansili
+ * time: 2024/3/12 21:03
+ */
+package cn.iocoder.yudao.framework.ai;