diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
index 8ca4bccc3..862f7fe8d 100644
--- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
@@ -100,6 +100,14 @@
4.12.0
test
+
+ io.projectreactor.netty
+ reactor-netty
+
+
+ cn.hutool
+ hutool-all
+
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoApi.java
new file mode 100644
index 000000000..58c21349b
--- /dev/null
+++ b/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
+ *
+ * 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
+ *
+ * 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 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 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 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 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 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 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;
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatClient.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatClient.java
new file mode 100644
index 000000000..a750e1c8f
--- /dev/null
+++ b/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
+ *
+ * author: fansili
+ * time: 2024/3/11 10:19
+ */
+@Slf4j
+public class XingHuoChatClient extends AbstractFunctionCallSupport>
+ 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 void onError(RetryContext context,
+ RetryCallback 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 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 stream(Prompt prompt) {
+ // 创建 request 请求,stream模式需要供应商支持
+ XingHuoChatCompletionRequest request = this.createRequest(prompt, false);
+ // 发送请求
+ Flux 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 conversationHistory) {
+ return null;
+ }
+
+ @Override
+ protected List doGetUserMessages(XingHuoChatCompletionRequest request) {
+ return null;
+ }
+
+ @Override
+ protected XingHuoChatCompletionMessage doGetToolResponseMessage(ResponseEntity response) {
+ return null;
+ }
+
+ @Override
+ protected ResponseEntity doChatCompletion(XingHuoChatCompletionRequest request) {
+ return xingHuoApi.chatCompletionEntity(request);
+ }
+
+ @Override
+ protected boolean isToolFunctionCall(ResponseEntity response) {
+ return false;
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatModel.java
new file mode 100644
index 000000000..e00a622b0
--- /dev/null
+++ b/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;
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletion.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletion.java
new file mode 100644
index 000000000..a0f965327
--- /dev/null
+++ b/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;
+ }
+
+ @Data
+ @Accessors(chain = true)
+ public static class Text {
+ private String content;
+ private String role;
+ private int index;
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionMessage.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionMessage.java
new file mode 100644
index 000000000..faa3c8c73
--- /dev/null
+++ b/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 {
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionRequest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionRequest.java
new file mode 100644
index 000000000..009789cb3
--- /dev/null
+++ b/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;
+
+
+ @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;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/exception/XingHuoApiException.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/exception/XingHuoApiException.java
new file mode 100644
index 000000000..cde6147c7
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java
new file mode 100644
index 000000000..674e4dc6d
--- /dev/null
+++ b/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 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;
+ }
+
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java
new file mode 100644
index 000000000..da8bc9d5d
--- /dev/null
+++ b/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
+ *
+ * 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 stream = xingHuoChatClient.stream(new Prompt("java和go那个性能更好!"));
+ stream.subscribe(new Consumer() {
+ @Override
+ public void accept(ChatResponse chatResponse) {
+ System.err.print(chatResponse.getResult().getOutput().getContent());
+ }
+ });
+ // 阻止退出
+ Scanner scanner = new Scanner(System.in);
+ scanner.nextLine();
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java
new file mode 100644
index 000000000..3f2903201
--- /dev/null
+++ b/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
+ *
+ * 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 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;
+ }
+
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatTests.java
new file mode 100644
index 000000000..346ffedb1
--- /dev/null
+++ b/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 fluxResponse = yiYanChatClient.stream(new Prompt("用java帮我写一个快排算法?"));
+ fluxResponse.subscribe(new Consumer() {
+ @Override
+ public void accept(ChatResponse chatResponse) {
+ System.err.print(chatResponse.getResult().getOutput().getContent());
+ }
+ });
+ // 阻止退出
+ Scanner scanner = new Scanner(System.in);
+ scanner.nextLine();
+ }
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/package-info.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/package-info.java
new file mode 100644
index 000000000..1678ee0ae
--- /dev/null
+++ b/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;
\ No newline at end of file