From ea3c8dec72a2c6610fd0324052cb6b36af623116 Mon Sep 17 00:00:00 2001 From: unknowIfGuestInDream Date: Fri, 2 May 2025 16:30:58 +0800 Subject: [PATCH 1/2] feat(core): Add deepseek support Close: #2100 Signed-off-by: unknowIfGuestInDream --- core/pom.xml | 1 + .../ai/deepseek/ChatCompletionChoice.java | 62 ++++ .../core/ai/deepseek/ChatCompletionDelta.java | 53 +++ .../ai/deepseek/ChatCompletionRequest.java | 114 ++++++ .../ai/deepseek/ChatCompletionResponse.java | 91 +++++ .../deepseek/ChatCompletionStreamChoice.java | 62 ++++ .../ChatCompletionStreamResponse.java | 82 +++++ .../tlcsdm/core/ai/deepseek/ChatMessage.java | 61 ++++ .../ai/deepseek/DeepSeekApiException.java | 41 +++ .../core/ai/deepseek/DeepSeekChatClient.java | 330 ++++++++++++++++++ .../com/tlcsdm/core/ai/deepseek/Usage.java | 61 ++++ core/src/main/java/module-info.java | 6 +- .../java/com/tlcsdm/core/ai/DeepseekTest.java | 153 ++++++++ 13 files changed, 1115 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionChoice.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionDelta.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionRequest.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionResponse.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionStreamChoice.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionStreamResponse.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatMessage.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekApiException.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekChatClient.java create mode 100644 core/src/main/java/com/tlcsdm/core/ai/deepseek/Usage.java create mode 100644 core/src/test/java/com/tlcsdm/core/ai/DeepseekTest.java diff --git a/core/pom.xml b/core/pom.xml index e2394e782..23aa645a7 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -471,6 +471,7 @@ **/H2Test.java **/OshiTest.java **/CompressTest.java + **/com/tlcsdm/core/ai/*.java diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionChoice.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionChoice.java new file mode 100644 index 000000000..cc0707b23 --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionChoice.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +/** + * @author unknowIfGuestInDream + */ +public class ChatCompletionChoice { + + private int index; + private ChatMessage message; + private String finishReason; + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public ChatMessage getMessage() { + return message; + } + + public void setMessage(ChatMessage message) { + this.message = message; + } + + public String getFinishReason() { + return finishReason; + } + + public void setFinishReason(String finishReason) { + this.finishReason = finishReason; + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionDelta.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionDelta.java new file mode 100644 index 000000000..37b65ffe2 --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionDelta.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +/** + * @author unknowIfGuestInDream + */ +public class ChatCompletionDelta { + + private String content; + private String role; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionRequest.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionRequest.java new file mode 100644 index 000000000..b46853cbf --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionRequest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +import java.util.List; + +/** + * @author unknowIfGuestInDream + */ +public class ChatCompletionRequest { + + private String model; + private List messages; + private Double temperature; + private Integer maxTokens; + private Boolean stream; + private Boolean webSearch; + private Boolean deepThought; + + public ChatCompletionRequest() { + } + + public ChatCompletionRequest(String model, List messages, Double temperature, + Integer maxTokens, Boolean stream, Boolean webSearch, Boolean deepThought) { + this.model = model; + this.messages = messages; + this.temperature = temperature; + this.maxTokens = maxTokens; + this.stream = stream; + this.webSearch = webSearch; + this.deepThought = deepThought; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getMessages() { + return messages; + } + + public void setMessages(List messages) { + this.messages = messages; + } + + public Double getTemperature() { + return temperature; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + public Integer getMaxTokens() { + return maxTokens; + } + + public void setMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + } + + public Boolean getStream() { + return stream; + } + + public void setStream(Boolean stream) { + this.stream = stream; + } + + public Boolean getWebSearch() { + return webSearch; + } + + public void setWebSearch(Boolean webSearch) { + this.webSearch = webSearch; + } + + public Boolean getDeepThought() { + return deepThought; + } + + public void setDeepThought(Boolean deepThought) { + this.deepThought = deepThought; + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionResponse.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionResponse.java new file mode 100644 index 000000000..46bf83daf --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionResponse.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +import java.util.List; + +/** + * @author unknowIfGuestInDream + */ +public class ChatCompletionResponse { + + private String id; + private String object; + private long created; + private String model; + private List choices; + private Usage usage; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public long getCreated() { + return created; + } + + public void setCreated(long created) { + this.created = created; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } + + public Usage getUsage() { + return usage; + } + + public void setUsage(Usage usage) { + this.usage = usage; + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionStreamChoice.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionStreamChoice.java new file mode 100644 index 000000000..4ff6819e0 --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionStreamChoice.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +/** + * @author unknowIfGuestInDream + */ +public class ChatCompletionStreamChoice { + + private int index; + private ChatCompletionDelta delta; + private String finishReason; + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public ChatCompletionDelta getDelta() { + return delta; + } + + public void setDelta(ChatCompletionDelta delta) { + this.delta = delta; + } + + public String getFinishReason() { + return finishReason; + } + + public void setFinishReason(String finishReason) { + this.finishReason = finishReason; + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionStreamResponse.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionStreamResponse.java new file mode 100644 index 000000000..d3f74f3df --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatCompletionStreamResponse.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +import java.util.List; + +/** + * @author unknowIfGuestInDream + */ +public class ChatCompletionStreamResponse { + + private String id; + private String object; + private long created; + private String model; + private List choices; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public long getCreated() { + return created; + } + + public void setCreated(long created) { + this.created = created; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatMessage.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatMessage.java new file mode 100644 index 000000000..68a6c575b --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/ChatMessage.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +/** + * @author unknowIfGuestInDream + */ +public class ChatMessage { + + private String role; + private String content; + + public ChatMessage() { + } + + public ChatMessage(String role, String content) { + this.role = role; + this.content = content; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekApiException.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekApiException.java new file mode 100644 index 000000000..580b3f8a7 --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekApiException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +/** + * @author unknowIfGuestInDream + */ +public class DeepSeekApiException extends Exception { + public DeepSeekApiException(String message) { + super(message); + } + + public DeepSeekApiException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekChatClient.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekChatClient.java new file mode 100644 index 000000000..0c4778969 --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekChatClient.java @@ -0,0 +1,330 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.tlcsdm.core.util.JacksonUtil; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * @author unknowIfGuestInDream + */ +public class DeepSeekChatClient { + private static final String API_BASE_URL = "https://api.deepseek.com/v1"; + private final HttpClient httpClient; + private final ObjectMapper objectMapper; + private final String apiKey; + //对话上下文 + private final List conversationHistory; + + public DeepSeekChatClient(String apiKey) { + this.apiKey = apiKey; + this.conversationHistory = new ArrayList<>(); + this.httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .connectTimeout(Duration.ofSeconds(30)) + .build(); + + this.objectMapper = JacksonUtil.getJsonMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + // 同步聊天方法 + public String chat(String userMessage, boolean webSearch, boolean deepThought) throws DeepSeekApiException { + return chat(userMessage, "deepseek-chat", webSearch, deepThought, 0.7, null); + } + + public String chat(String userMessage, String model, boolean webSearch, boolean deepThought, + Double temperature, Integer maxTokens) throws DeepSeekApiException { + // 添加用户消息到历史 + addUserMessage(userMessage); + + // 构建请求 + ChatCompletionRequest request = new ChatCompletionRequest( + model, + new ArrayList<>(conversationHistory), + temperature, + maxTokens, + false, // 非流式 + webSearch, + deepThought + ); + + try { + ChatCompletionResponse response = createChatCompletion(request); + + if (response.getChoices() != null && !response.getChoices().isEmpty()) { + String assistantReply = response.getChoices().get(0).getMessage().getContent(); + // 添加助手回复到历史 + addAssistantMessage(assistantReply); + return assistantReply; + } + throw new DeepSeekApiException("No response choices available"); + } catch (DeepSeekApiException e) { + // 移除最后一条用户消息,因为对话失败 + if (!conversationHistory.isEmpty() && + "user".equals(conversationHistory.get(conversationHistory.size() - 1).getRole())) { + conversationHistory.remove(conversationHistory.size() - 1); + } + throw e; + } + } + + // 异步流式聊天方法 + public CompletableFuture chatStream(String userMessage, boolean webSearch, boolean deepThought, + Consumer chunkConsumer) { + return chatStream(userMessage, "deepseek-chat", webSearch, deepThought, 0.7, null, chunkConsumer); + } + + public CompletableFuture chatStream(String userMessage, String model, boolean webSearch, boolean deepThought, + Double temperature, Integer maxTokens, Consumer chunkConsumer) { + // 添加用户消息到历史 + addUserMessage(userMessage); + + // 构建请求 + ChatCompletionRequest request = new ChatCompletionRequest( + model, + new ArrayList<>(conversationHistory), + temperature, + maxTokens, + true, // 流式 + webSearch, + deepThought + ); + + CompletableFuture future = new CompletableFuture<>(); + StringBuilder fullResponse = new StringBuilder(); + + try { + String requestBody = objectMapper.writeValueAsString(request); + + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(URI.create(API_BASE_URL + "/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + // 使用 sendAsync 并手动处理流 + httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines()) + .thenAccept(response -> { + if (response.statusCode() >= 200 && response.statusCode() < 300) { + try { + // 直接处理 Stream + response.body().forEach(line -> { + try { + if (line.startsWith("data: ") && !line.equals("data: [DONE]")) { + String data = line.substring(6); + ChatCompletionStreamResponse chunk = objectMapper.readValue( + data, ChatCompletionStreamResponse.class); + + if (chunk.getChoices() != null && !chunk.getChoices().isEmpty()) { + String content = chunk.getChoices().get(0).getDelta().getContent(); + if (content != null) { + chunkConsumer.accept(content); + fullResponse.append(content); + } + } + } + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + + // 添加助手回复到历史 + String responseText = fullResponse.toString(); + addAssistantMessage(responseText); + future.complete(responseText); + + } catch (Exception e) { + future.completeExceptionally(e); + } finally { + response.body().close(); + } + } else { + future.completeExceptionally(new DeepSeekApiException( + "API request failed with status code: " + response.statusCode())); + } + }) + .exceptionally(e -> { + future.completeExceptionally(e); + return null; + }); + + } catch (Exception e) { + future.completeExceptionally(e); + } + + return future; + } + + // 历史记录管理方法 + public void addSystemMessage(String content) { + conversationHistory.add(new ChatMessage("system", content)); + } + + public void addUserMessage(String content) { + conversationHistory.add(new ChatMessage("user", content)); + } + + public void addAssistantMessage(String content) { + conversationHistory.add(new ChatMessage("assistant", content)); + } + + public void clearConversationHistory() { + conversationHistory.clear(); + } + + public List getConversationHistory() { + return new ArrayList<>(conversationHistory); + } + + // 核心API调用方法 + private ChatCompletionResponse createChatCompletion(ChatCompletionRequest request) throws DeepSeekApiException { + try { + String requestBody = objectMapper.writeValueAsString(request); + + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(URI.create(API_BASE_URL + "/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() >= 200 && response.statusCode() < 300) { + return objectMapper.readValue(response.body(), ChatCompletionResponse.class); + } else { + throw new DeepSeekApiException("API request failed with status code: " + response.statusCode() + + ", response: " + response.body()); + } + } catch (JsonProcessingException e) { + throw new DeepSeekApiException("Failed to serialize request or deserialize response", e); + } catch (Exception e) { + throw new DeepSeekApiException("HTTP request failed", e); + } + } + + private void streamChatCompletion(ChatCompletionRequest request, Flow.Subscriber subscriber) + throws DeepSeekApiException { + try { + String requestBody = objectMapper.writeValueAsString(request); + + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(URI.create(API_BASE_URL + "/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + // 使用AtomicReference来安全地管理Subscription + AtomicReference subscriptionRef = new AtomicReference<>(); + //CompletableFuture> resp = + httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines()) + .thenAccept(response -> { + if (response.statusCode() >= 200 && response.statusCode() < 300) { + // 创建安全的Subscriber包装器 + Flow.Subscriber safeSubscriber = new Flow.Subscriber<>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscriptionRef.set(subscription); + subscriber.onSubscribe(subscription); + subscription.request(1); // 请求第一个数据项 + } + + @Override + public void onNext(String line) { + try { + if (line.startsWith("data: ") && !line.equals("data: [DONE]")) { + String data = line.substring(6); + ChatCompletionStreamResponse chunk = objectMapper.readValue(data, + ChatCompletionStreamResponse.class); + if (chunk.getChoices() != null && !chunk.getChoices().isEmpty()) { + String content = chunk.getChoices().get(0).getDelta().getContent(); + if (content != null) { + subscriber.onNext(content); + } + } + } + // 请求下一个数据项 + Flow.Subscription sub = subscriptionRef.get(); + if (sub != null) { + sub.request(1); + } + } catch (JsonProcessingException e) { + onError(e); + } + } + + @Override + public void onError(Throwable throwable) { + subscriber.onError(throwable); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }; + // 处理响应流 + response.body().forEach(safeSubscriber::onNext); + safeSubscriber.onComplete(); + } else { + subscriber.onError(new DeepSeekApiException( + "API request failed with status code: " + response.statusCode())); + } + }) + .exceptionally(e -> { + subscriber.onError(e); + return null; + }); + } catch (JsonProcessingException e) { + throw new DeepSeekApiException("Failed to serialize request", e); + } + } +} diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/Usage.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/Usage.java new file mode 100644 index 000000000..e1e37f49a --- /dev/null +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/Usage.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai.deepseek; + +/** + * @author unknowIfGuestInDream + */ +public class Usage { + private int promptTokens; + private int completionTokens; + private int totalTokens; + + public int getPromptTokens() { + return promptTokens; + } + + public void setPromptTokens(int promptTokens) { + this.promptTokens = promptTokens; + } + + public int getCompletionTokens() { + return completionTokens; + } + + public void setCompletionTokens(int completionTokens) { + this.completionTokens = completionTokens; + } + + public int getTotalTokens() { + return totalTokens; + } + + public void setTotalTokens(int totalTokens) { + this.totalTokens = totalTokens; + } +} diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 71544fff1..f61d726df 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -49,6 +49,8 @@ requires static com.fasterxml.jackson.core; requires static com.fasterxml.jackson.databind; requires static com.fasterxml.jackson.annotation; + requires static com.fasterxml.jackson.dataformat.yaml; + requires static com.fasterxml.jackson.datatype.jsr310; requires static org.dom4j; requires static org.apache.commons.jexl3; requires static org.apache.groovy; @@ -73,8 +75,6 @@ requires static com.sun.jna.platform; requires static vosk; requires static jakarta.xml.bind; - requires static com.fasterxml.jackson.dataformat.yaml; - requires static com.fasterxml.jackson.datatype.jsr310; requires static org.python.jython2; requires static com.zaxxer.hikari; requires static druid; @@ -131,6 +131,8 @@ exports com.tlcsdm.core.database; exports com.tlcsdm.core.oshi; exports com.tlcsdm.core.powershell; + exports com.tlcsdm.core.watermark; + exports com.tlcsdm.core.ai.deepseek; uses com.tlcsdm.core.freemarker.TemplateLoaderService; uses com.tlcsdm.core.groovy.GroovyLoaderService; diff --git a/core/src/test/java/com/tlcsdm/core/ai/DeepseekTest.java b/core/src/test/java/com/tlcsdm/core/ai/DeepseekTest.java new file mode 100644 index 000000000..f9fafa269 --- /dev/null +++ b/core/src/test/java/com/tlcsdm/core/ai/DeepseekTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2025 unknowIfGuestInDream. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of unknowIfGuestInDream, any associated website, nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL UNKNOWIFGUESTINDREAM BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.tlcsdm.core.ai; + +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; +import cn.hutool.crypto.symmetric.AES; +import com.tlcsdm.core.ai.deepseek.DeepSeekApiException; +import com.tlcsdm.core.ai.deepseek.DeepSeekChatClient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; + +/** + * @author unknowIfGuestInDream + */ +class DeepseekTest { + + private static final String aesKey = "3f4alpd3525678154a5e3a0183d8087b"; + private static final String encryptStr = "aac70872f21545ff2c56a590188bbe4c20035e0e130568c8dabeb699947e6cdf6df2dd72f573c55a8829ea53a2e22cd4"; + private static String token; + + @BeforeAll + static void init() { + AES aes = new AES(Mode.ECB, Padding.PKCS5Padding, aesKey.getBytes()); + token = aes.decryptStr(encryptStr); + } + + @Test + void ds() throws DeepSeekApiException { + String apiKey = token; + DeepSeekChatClient client = new DeepSeekChatClient(apiKey); + // 简单对话 + System.out.println("用户: 你好,你是谁?"); + String reply = client.chat("你好,你是谁?", false, true); + System.out.println("助手: " + reply); + + // 带上下文的后续对话 + System.out.println("用户: 我刚才问了你什么?"); + reply = client.chat("我刚才问了你什么?", false, false); + System.out.println("助手: " + reply); + + // 流式对话 + System.out.println("用户: 请用流式方式告诉我关于Java 17的新特性"); + client.chatStream("请用流式方式告诉我关于Java 17的新特性", true, false, chunk -> { + System.out.print(chunk); + System.out.flush(); + }).join(); + + // 查看对话历史 + System.out.println("\n对话历史:"); + client.getConversationHistory().forEach(msg -> { + System.out.println(msg.getRole() + ": " + msg.getContent()); + }); + } + + @Test + void use() throws DeepSeekApiException { + String apiKey = token; + DeepSeekChatClient client = new DeepSeekChatClient(apiKey); + + // 1. 简单对话 + String response = client.chat("Java 17有什么新特性?", true, false); + System.out.println(response); + + // 2. 流式对话 + client.chatStream("详细解释Java 17的密封类", true, true, chunk -> { + System.out.print(chunk); + }).join(); + + // 3. 带上下文的对话 + client.addUserMessage("记住我的名字是张三"); + client.addUserMessage("我的名字是什么?"); + String reply = client.chat("", false, false); // 空消息会使用历史上下文 + System.out.println(reply); // 应该回答"张三" + } + + /** + * 高级功能配置. + */ + @Test + void useBuilder() throws DeepSeekApiException { + String apiKey = token; + DeepSeekChatClient client = new DeepSeekChatClient(apiKey); + // 使用特定模型,设置温度和最大token数 + String reply = client.chat("你的知识截止到什么时候?", + "deepseek-chat", // 模型 + false, // 不使用联网搜索 + true, // 启用深度思考 + 0.5, // 温度参数 + 500); // 最大token数 + System.out.println(reply); + + // 在对话开始前设置系统角色消息 + client.addSystemMessage("你是一个专业的Java开发助手,回答要简洁专业"); + + // 自定义流式响应处理器 + CompletableFuture future = client.chatStream( + "解释JVM的工作原理", + "deepseek-chat", + false, + true, + 0.7, + null, + chunk -> { + // 实时打印流式响应 + System.out.print(chunk); + System.out.flush(); + }); + + //为流式响应添加超时机制 + // future.orTimeout(30, TimeUnit.SECONDS) + // .exceptionally(e -> { + // if (e instanceof TimeoutException) { + // System.err.println("响应超时"); + // } + // return null; + // }); + + future.thenAccept(fullResponse -> { + System.out.println("\n\n完整响应已接收,长度: " + fullResponse.length()); + }).exceptionally(e -> { + System.err.println("对话失败: " + e.getMessage()); + return null; + }).join(); + } +} From ea4b90f293b8baba22d92937cf919b6e2e96987e Mon Sep 17 00:00:00 2001 From: unknowIfGuestInDream Date: Fri, 2 May 2025 16:52:55 +0800 Subject: [PATCH 2/2] test: Improve deepseek test Close: #2100 Signed-off-by: unknowIfGuestInDream --- .../core/ai/deepseek/DeepSeekChatClient.java | 80 ------------------- .../java/com/tlcsdm/core/ai/DeepseekTest.java | 73 +++++++++++++++++ 2 files changed, 73 insertions(+), 80 deletions(-) diff --git a/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekChatClient.java b/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekChatClient.java index 0c4778969..1a9bff5b3 100644 --- a/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekChatClient.java +++ b/core/src/main/java/com/tlcsdm/core/ai/deepseek/DeepSeekChatClient.java @@ -43,8 +43,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; /** @@ -249,82 +247,4 @@ private ChatCompletionResponse createChatCompletion(ChatCompletionRequest reques throw new DeepSeekApiException("HTTP request failed", e); } } - - private void streamChatCompletion(ChatCompletionRequest request, Flow.Subscriber subscriber) - throws DeepSeekApiException { - try { - String requestBody = objectMapper.writeValueAsString(request); - - HttpRequest httpRequest = HttpRequest.newBuilder() - .uri(URI.create(API_BASE_URL + "/chat/completions")) - .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + apiKey) - .POST(HttpRequest.BodyPublishers.ofString(requestBody)) - .build(); - - // 使用AtomicReference来安全地管理Subscription - AtomicReference subscriptionRef = new AtomicReference<>(); - //CompletableFuture> resp = - httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines()) - .thenAccept(response -> { - if (response.statusCode() >= 200 && response.statusCode() < 300) { - // 创建安全的Subscriber包装器 - Flow.Subscriber safeSubscriber = new Flow.Subscriber<>() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscriptionRef.set(subscription); - subscriber.onSubscribe(subscription); - subscription.request(1); // 请求第一个数据项 - } - - @Override - public void onNext(String line) { - try { - if (line.startsWith("data: ") && !line.equals("data: [DONE]")) { - String data = line.substring(6); - ChatCompletionStreamResponse chunk = objectMapper.readValue(data, - ChatCompletionStreamResponse.class); - if (chunk.getChoices() != null && !chunk.getChoices().isEmpty()) { - String content = chunk.getChoices().get(0).getDelta().getContent(); - if (content != null) { - subscriber.onNext(content); - } - } - } - // 请求下一个数据项 - Flow.Subscription sub = subscriptionRef.get(); - if (sub != null) { - sub.request(1); - } - } catch (JsonProcessingException e) { - onError(e); - } - } - - @Override - public void onError(Throwable throwable) { - subscriber.onError(throwable); - } - - @Override - public void onComplete() { - subscriber.onComplete(); - } - }; - // 处理响应流 - response.body().forEach(safeSubscriber::onNext); - safeSubscriber.onComplete(); - } else { - subscriber.onError(new DeepSeekApiException( - "API request failed with status code: " + response.statusCode())); - } - }) - .exceptionally(e -> { - subscriber.onError(e); - return null; - }); - } catch (JsonProcessingException e) { - throw new DeepSeekApiException("Failed to serialize request", e); - } - } } diff --git a/core/src/test/java/com/tlcsdm/core/ai/DeepseekTest.java b/core/src/test/java/com/tlcsdm/core/ai/DeepseekTest.java index f9fafa269..212ebd2cb 100644 --- a/core/src/test/java/com/tlcsdm/core/ai/DeepseekTest.java +++ b/core/src/test/java/com/tlcsdm/core/ai/DeepseekTest.java @@ -30,11 +30,19 @@ import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.symmetric.AES; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.tlcsdm.core.ai.deepseek.DeepSeekApiException; import com.tlcsdm.core.ai.deepseek.DeepSeekChatClient; +import com.tlcsdm.core.util.JacksonUtil; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; import java.util.concurrent.CompletableFuture; /** @@ -150,4 +158,69 @@ void useBuilder() throws DeepSeekApiException { return null; }).join(); } + + /** + * 查询账户余额. + */ + @Test + void queryUsage() throws Exception { + String endpoint = "/user/balance"; + JsonNode response = makeApiRequest(endpoint, "GET", null); + + System.out.println("\n账户余额信息:"); + System.out.println("当前账户是否有余额可供 API 调用: " + response.path("is_available").asBoolean()); + response.path("balance_infos").valueStream().forEach(e -> { + System.out.println("货币: " + e.path("currency").asText()); + System.out.println("总的可用余额: " + e.path("total_balance").asText()); + System.out.println("未过期的赠金余额: " + e.path("granted_balance").asText()); + System.out.println("充值余额: " + e.path("topped_up_balance").asText()); + }); + } + + /** + * 列出可用模型. + */ + @Test + void listModels() throws Exception { + String endpoint = "/models"; + JsonNode response = makeApiRequest(endpoint, "GET", null); + + System.out.println("\n可用模型列表:"); + JsonNode models = response.path("data"); + for (JsonNode model : models) { + System.out.println("\n模型ID: " + model.path("id").asText()); + System.out.println("组织: " + model.path("owned_by").asText()); + } + } + + /** + * 通用的API请求方法. + */ + private static JsonNode makeApiRequest(String endpoint, String method, String requestBody) throws Exception { + HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .connectTimeout(Duration.ofSeconds(10)) + .build(); + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create("https://api.deepseek.com/v1" + endpoint)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + token); + + if ("GET".equalsIgnoreCase(method)) { + requestBuilder.GET(); + } else if ("POST".equalsIgnoreCase(method)) { + requestBuilder.POST(HttpRequest.BodyPublishers.ofString(requestBody)); + } else { + throw new IllegalArgumentException("不支持的HTTP方法: " + method); + } + + HttpRequest request = requestBuilder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("API请求失败: " + response.statusCode() + " - " + response.body()); + } + ObjectMapper objectMapper = JacksonUtil.getJsonMapper(); + return objectMapper.readTree(response.body()); + } }