diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86ca143c..39648bbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,22 +16,47 @@ jobs: strategy: fail-fast: false matrix: - java-version: [ "8", "11", "17" ] - os: [ "ubuntu-latest" ] - os-label: [ "Ubuntu" ] + java-version: [ "11", "17" ] + os: [ "ubuntu-latest", "windows-latest", "macos-latest" ] + os-label: [ "Ubuntu", "Windows", "macOS" ] include: - - { java-version: "11", os: "windows-latest", os-label: "Windows" } - - { java-version: "11", os: "macos-latest", os-label: "macOS" } + - { java-version: "8", os: "macos-latest", os-label: "macOS", distribution: "zulu" } + exclude: + - os: "windows-latest" + os-label: "Ubuntu" + - os: "windows-latest" + os-label: "macOS" + - os: "macos-latest" + os-label: "Ubuntu" + - os: "macos-latest" + os-label: "Windows" + - os: "ubuntu-latest" + os-label: "Windows" + - os: "ubuntu-latest" + os-label: "macOS" steps: - uses: actions/checkout@v4 - name: Set up JDK ${{ matrix.java-version }} uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: ${{ matrix.java-version == '8' && matrix.os == 'macos-latest' && 'zulu' || 'temurin' }} java-version: ${{ matrix.java-version }} cache: 'maven' + java-package: 'jdk' + - name: Set JAVA_HOME (Windows) + if: runner.os == 'Windows' + run: | + echo "JAVA_HOME=${{ env.JAVA_HOME }}" >> $GITHUB_ENV + echo "${{ env.JAVA_HOME }}/bin" >> $GITHUB_PATH + shell: bash - name: Code style check - run: mvn spotless:check + run: | + echo "Java version:" + java -version + echo "Javac version:" + javac -version + mvn -v + mvn spotless:check - name: Build and Test with Coverage run: | mvn -pl api clean test-compile diff --git a/.gitignore b/.gitignore index 14bfd82d..1fe5b781 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ **/bin/* **/obj/* .mvn +*.wav # Compiled class file *.class diff --git a/README.md b/README.md index cf225ca0..84aa58f6 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,9 @@ dependencies { | how to handle exception | [HandlerExceptionExample.java](example/src/main/java/example/service/HandlerExceptionExample.java) | | get request log id | [GetLogExample.java](example/src/main/java/example/service/GetLogExample.java) | | set timeout | [SetRequestTimeoutExample.java](example/src/main/java/example/service/SetRequestTimeoutExample.java) | +| websocket chat | [ChatExample.java](example/src/main/java/example/websocket/chat/ChatExample.java) | +| websocket speech synthesis | [WebsocketAudioSpeechExample.java](example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java) | +| websocket transcription | [WebsocketTranscriptionsExample.java](example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java) | ### Initialize the Coze Client @@ -984,4 +987,123 @@ while (iterator.hasNext()) { ``` +### WebSocket + +The SDK provides WebSocket interfaces for real-time chat, speech synthesis and speech transcription. + +You can check the official documentation for more information: +https://www.coze.cn/open/docs/guides/websocket_openapi + +#### WebSocket Chat + +WebSocket chat allows real-time communication with bots, including text and audio interactions: + +```java +WebsocketChatClient client = coze.websocket() + .chat() + .create(new WebsocketChatCreateReq(botID, new CallbackHandler())); + +// Send audio data +String audioData = "..."; // Base64 encoded audio data +client.inputAudioBufferAppend(audioData); +client.inputAudioBufferComplete(); + +// Handle responses in callback +class CallbackHandler extends WebsocketChatCallbackHandler { + // Handle text responses + @Override + public void onConversationMessageDelta(WebsocketChatClient client, ConversationMessageDeltaEvent event) { + System.out.printf("Received: %s\n", event.getData().getContent()); + } + + // Handle audio responses + @Override + public void onConversationAudioDelta(WebsocketChatClient client, ConversationAudioDeltaEvent event) { + byte[] audioData = event.getData().getAudio(); + // Process audio data... + } +} +``` + +#### Speech Synthesis + +WebSocket speech synthesis allows real-time text-to-speech conversion: + +```java +WebsocketAudioSpeechClient client = coze.websocket() + .audio() + .speech() + .create(new WebsocketAudioSpeechCreateReq(new CallbackHandler())); + +// Configure audio output +OutputAudio outputAudio = OutputAudio.builder() + .voiceId(voiceID) + .codec("pcm") + .speechRate(50) + .pcmConfig(PCMConfig.builder().sampleRate(24000).build()) + .build(); +client.speechUpdate(new SpeechUpdateEventData(outputAudio)); + +// Send text for synthesis +client.inputTextBufferAppend("Hello world!"); +client.inputTextBufferComplete(); + +// Handle synthesized audio in callback +class CallbackHandler extends WebsocketAudioSpeechCallbackHandler { + @Override + public void onSpeechAudioUpdate(WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) { + byte[] audioData = event.getDelta(); + // Process audio data... + } +} +``` + +#### Speech Transcription + +WebSocket speech transcription provides real-time speech-to-text conversion: + +```java +WebsocketAudioTranscriptionsClient client = coze.websocket() + .audio() + .transcriptions() + .create(new WebsocketAudioTranscriptionsCreateReq(new CallbackHandler())); + +// Configure audio input +InputAudio inputAudio = InputAudio.builder() + .sampleRate(24000) + .codec("pcm") + .format("wav") + .channel(2) + .build(); +client.transcriptionsUpdate(new TranscriptionsUpdateEventData(inputAudio)); + +// Send audio for transcription +String audioData = "..."; // Base64 encoded audio data +client.inputAudioBufferAppend(audioData); +client.inputAudioBufferComplete(); + +// Handle transcription results in callback +class CallbackHandler extends WebsocketAudioTranscriptionsCallbackHandler { + @Override + public void onTranscriptionsMessageUpdate( + WebsocketAudioTranscriptionsClient client, + TranscriptionsMessageUpdateEvent event) { + System.out.println(event.getData().getContent()); + } +} +``` + +All WebSocket clients support proper resource cleanup: + +```java +try { + // Use the client... +} finally { + if (client != null) { + client.close(); + } + coze.shutdownExecutor(); +} +``` + diff --git a/api/pom.xml b/api/pom.xml index cb908ef2..b88be710 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -43,7 +43,7 @@ coze-api - 0.2.8 + 0.2.9 scm:git:git://github.com/coze-dev/coze-java.git @@ -68,7 +68,7 @@ 2.14.2 0.11.5 5.10.2 - 4.8.1 + 3.14.9 @@ -280,7 +280,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.2.5 **/*Test.java @@ -288,9 +288,12 @@ false 1 true - @{argLine} -Djava.security.egd=file:/dev/./urandom + @{argLine} - jdk + jdk-8 + file:/dev/urandom + ${java.home}/lib/security/cacerts + changeit diff --git a/api/src/main/java/com/coze/openapi/api/AudioTranscriptionAPI.java b/api/src/main/java/com/coze/openapi/api/AudioTranscriptionAPI.java new file mode 100644 index 00000000..c81373f0 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/api/AudioTranscriptionAPI.java @@ -0,0 +1,19 @@ +package com.coze.openapi.api; + +import com.coze.openapi.client.audio.transcriptions.CreateTranscriptionsResp; +import com.coze.openapi.client.common.BaseReq; +import com.coze.openapi.client.common.BaseResponse; + +import okhttp3.MultipartBody; +import retrofit2.Call; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.Part; +import retrofit2.http.Tag; + +public interface AudioTranscriptionAPI { + @Multipart + @POST("/v1/audio/transcriptions") + Call> create( + @Part MultipartBody.Part file, @Tag BaseReq baseReq); +} diff --git a/api/src/main/java/com/coze/openapi/client/audio/speech/CreateSpeechReq.java b/api/src/main/java/com/coze/openapi/client/audio/speech/CreateSpeechReq.java index 2bab6725..278b7fc1 100644 --- a/api/src/main/java/com/coze/openapi/client/audio/speech/CreateSpeechReq.java +++ b/api/src/main/java/com/coze/openapi/client/audio/speech/CreateSpeechReq.java @@ -28,4 +28,7 @@ public class CreateSpeechReq extends BaseReq { @JsonProperty("speed") @Builder.Default private float speed = 1.0f; + + @JsonProperty("sample_rate") + private int sampleRate; } diff --git a/api/src/main/java/com/coze/openapi/client/audio/transcriptions/CreateTranscriptionsReq.java b/api/src/main/java/com/coze/openapi/client/audio/transcriptions/CreateTranscriptionsReq.java new file mode 100644 index 00000000..1abde976 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/audio/transcriptions/CreateTranscriptionsReq.java @@ -0,0 +1,42 @@ +package com.coze.openapi.client.audio.transcriptions; + +import java.io.File; + +import com.coze.openapi.client.common.BaseReq; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CreateTranscriptionsReq extends BaseReq { + /** local file path */ + private String filePath; + + /* + * file byte array + */ + private byte[] fileBytes; + + /** file name */ + private String fileName; + + /** file object */ + private File file; + + public static CreateTranscriptionsReq of(String fileName, byte[] fileBytes) { + return CreateTranscriptionsReq.builder().fileName(fileName).fileBytes(fileBytes).build(); + } + + public static CreateTranscriptionsReq of(File file) { + return CreateTranscriptionsReq.builder().file(file).build(); + } + + public static CreateTranscriptionsReq of(String filePath) { + return CreateTranscriptionsReq.builder().filePath(filePath).build(); + } +} diff --git a/api/src/main/java/com/coze/openapi/client/audio/transcriptions/CreateTranscriptionsResp.java b/api/src/main/java/com/coze/openapi/client/audio/transcriptions/CreateTranscriptionsResp.java new file mode 100644 index 00000000..fa754631 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/audio/transcriptions/CreateTranscriptionsResp.java @@ -0,0 +1,17 @@ +package com.coze.openapi.client.audio.transcriptions; + +import com.coze.openapi.client.common.BaseResp; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CreateTranscriptionsResp extends BaseResp { + @JsonProperty("text") + private String text; +} diff --git a/api/src/main/java/com/coze/openapi/client/bots/CreateBotReq.java b/api/src/main/java/com/coze/openapi/client/bots/CreateBotReq.java index 846e11a0..bd069bd7 100644 --- a/api/src/main/java/com/coze/openapi/client/bots/CreateBotReq.java +++ b/api/src/main/java/com/coze/openapi/client/bots/CreateBotReq.java @@ -1,7 +1,5 @@ package com.coze.openapi.client.bots; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.client.bots.model.BotOnboardingInfo; import com.coze.openapi.client.bots.model.BotPromptInfo; import com.coze.openapi.client.common.BaseReq; @@ -11,6 +9,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.NonNull; import lombok.experimental.SuperBuilder; @Data @@ -19,11 +18,11 @@ @AllArgsConstructor @EqualsAndHashCode(callSuper = true) public class CreateBotReq extends BaseReq { - @NotNull + @NonNull @JsonProperty("space_id") String spaceID; - @NotNull + @NonNull @JsonProperty("name") String name; diff --git a/api/src/main/java/com/coze/openapi/client/bots/UpdateBotReq.java b/api/src/main/java/com/coze/openapi/client/bots/UpdateBotReq.java index 56600a35..c9115274 100644 --- a/api/src/main/java/com/coze/openapi/client/bots/UpdateBotReq.java +++ b/api/src/main/java/com/coze/openapi/client/bots/UpdateBotReq.java @@ -1,17 +1,12 @@ package com.coze.openapi.client.bots; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.client.bots.model.BotKnowledge; import com.coze.openapi.client.bots.model.BotOnboardingInfo; import com.coze.openapi.client.bots.model.BotPromptInfo; import com.coze.openapi.client.common.BaseReq; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import lombok.*; import lombok.experimental.SuperBuilder; @Data @@ -21,7 +16,7 @@ @EqualsAndHashCode(callSuper = true) public class UpdateBotReq extends BaseReq { - @NotNull + @NonNull @JsonProperty("bot_id") private String botID; diff --git a/api/src/main/java/com/coze/openapi/client/chat/model/ChatUsage.java b/api/src/main/java/com/coze/openapi/client/chat/model/ChatUsage.java index 6531ad61..f1ea7b49 100644 --- a/api/src/main/java/com/coze/openapi/client/chat/model/ChatUsage.java +++ b/api/src/main/java/com/coze/openapi/client/chat/model/ChatUsage.java @@ -23,6 +23,12 @@ public class ChatUsage { @JsonProperty("output_count") private int outputCount; + @JsonProperty("output_tokens") + private int outputTokens; + + @JsonProperty("input_tokens") + private int inputTokens; + /** The total number of Tokens consumed for the input part. */ @JsonProperty("input_count") private int inputCount; diff --git a/api/src/main/java/com/coze/openapi/client/connversations/message/model/Message.java b/api/src/main/java/com/coze/openapi/client/connversations/message/model/Message.java index 7193ce7c..c4b17c45 100644 --- a/api/src/main/java/com/coze/openapi/client/connversations/message/model/Message.java +++ b/api/src/main/java/com/coze/openapi/client/connversations/message/model/Message.java @@ -1,5 +1,6 @@ package com.coze.openapi.client.connversations.message.model; +import java.util.Base64; import java.util.List; import java.util.Map; @@ -75,6 +76,16 @@ public class Message { @JsonProperty("reasoning_content") private String reasoningContent; + public byte[] getAudio() { + if (this.contentType == MessageContentType.AUDIO) { + if (this.content == null) { + return new byte[0]; + } + return Base64.getDecoder().decode(this.content); + } + return new byte[0]; + } + public static Message buildUserQuestionText(String content) { return buildUserQuestionText(content, null); } diff --git a/api/src/main/java/com/coze/openapi/client/connversations/message/model/MessageContentType.java b/api/src/main/java/com/coze/openapi/client/connversations/message/model/MessageContentType.java index 9b2374c1..338a1904 100644 --- a/api/src/main/java/com/coze/openapi/client/connversations/message/model/MessageContentType.java +++ b/api/src/main/java/com/coze/openapi/client/connversations/message/model/MessageContentType.java @@ -3,9 +3,11 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import lombok.EqualsAndHashCode; import lombok.Getter; @Getter +@EqualsAndHashCode public class MessageContentType { public static final MessageContentType UNKNOWN = new MessageContentType("unknown"); /** Text. */ diff --git a/api/src/main/java/com/coze/openapi/client/dataset/ProcessDatasetReq.java b/api/src/main/java/com/coze/openapi/client/dataset/ProcessDatasetReq.java index a8f799ca..0e01230e 100644 --- a/api/src/main/java/com/coze/openapi/client/dataset/ProcessDatasetReq.java +++ b/api/src/main/java/com/coze/openapi/client/dataset/ProcessDatasetReq.java @@ -2,16 +2,12 @@ import java.util.List; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.client.common.BaseReq; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import lombok.*; +import lombok.NonNull; import lombok.experimental.SuperBuilder; @Data @@ -20,7 +16,7 @@ @AllArgsConstructor @EqualsAndHashCode(callSuper = true) public class ProcessDatasetReq extends BaseReq { - @NotNull @JsonIgnore private String datasetID; + @NonNull @JsonIgnore private String datasetID; /* * The IDs of the documents diff --git a/api/src/main/java/com/coze/openapi/client/dataset/document/CreateDocumentReq.java b/api/src/main/java/com/coze/openapi/client/dataset/document/CreateDocumentReq.java index 721acd18..319fe8e1 100644 --- a/api/src/main/java/com/coze/openapi/client/dataset/document/CreateDocumentReq.java +++ b/api/src/main/java/com/coze/openapi/client/dataset/document/CreateDocumentReq.java @@ -2,8 +2,6 @@ import java.util.List; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.client.common.BaseReq; import com.coze.openapi.client.dataset.document.model.DocumentBase; import com.coze.openapi.client.dataset.document.model.DocumentChunkStrategy; @@ -15,6 +13,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.NonNull; import lombok.experimental.SuperBuilder; @Data @@ -25,7 +24,7 @@ @EqualsAndHashCode(callSuper = true) public class CreateDocumentReq extends BaseReq { /** The ID of the knowledge base. */ - @NotNull + @NonNull @JsonProperty("dataset_id") private Long datasetID; @@ -34,7 +33,7 @@ public class CreateDocumentReq extends BaseReq { * meaning up to 10 files can be uploaded at a time. For detailed instructions, refer to the * DocumentBase object. */ - @NotNull + @NonNull @JsonProperty("document_bases") private List documentBases; diff --git a/api/src/main/java/com/coze/openapi/client/dataset/document/ListDocumentReq.java b/api/src/main/java/com/coze/openapi/client/dataset/document/ListDocumentReq.java index 74982c65..8aae26a4 100644 --- a/api/src/main/java/com/coze/openapi/client/dataset/document/ListDocumentReq.java +++ b/api/src/main/java/com/coze/openapi/client/dataset/document/ListDocumentReq.java @@ -1,7 +1,5 @@ package com.coze.openapi.client.dataset.document; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.client.common.BaseReq; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -11,6 +9,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.NonNull; import lombok.experimental.SuperBuilder; @Data @@ -21,7 +20,7 @@ @EqualsAndHashCode(callSuper = true) public class ListDocumentReq extends BaseReq { /** The ID of the knowledge base. */ - @NotNull + @NonNull @JsonProperty("dataset_id") private Long datasetID; diff --git a/api/src/main/java/com/coze/openapi/client/exception/CozeApiException.java b/api/src/main/java/com/coze/openapi/client/exception/CozeApiException.java index 5fd2c0f4..cae37587 100644 --- a/api/src/main/java/com/coze/openapi/client/exception/CozeApiException.java +++ b/api/src/main/java/com/coze/openapi/client/exception/CozeApiException.java @@ -1,14 +1,22 @@ package com.coze.openapi.client.exception; -import lombok.Getter; -import lombok.ToString; +import com.fasterxml.jackson.annotation.JsonProperty; -@Getter +import lombok.*; + +@Data +@Builder @ToString +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class CozeApiException extends RuntimeException { - private final int code; - private final String msg; - private final String logID; + @JsonProperty("code") + private int code; + + @JsonProperty("msg") + private String msg; + + private String logID; public CozeApiException(int code, String msg, String logID) { super(msg); diff --git a/api/src/main/java/com/coze/openapi/client/websocket/common/BaseEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/common/BaseEvent.java new file mode 100644 index 00000000..15181bcc --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/common/BaseEvent.java @@ -0,0 +1,20 @@ +package com.coze.openapi.client.websocket.common; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class BaseEvent { + @JsonProperty("id") + private String id; + + @JsonProperty("detail") + private Detail detail; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/common/Detail.java b/api/src/main/java/com/coze/openapi/client/websocket/common/Detail.java new file mode 100644 index 00000000..f43a146b --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/common/Detail.java @@ -0,0 +1,17 @@ +package com.coze.openapi.client.websocket.common; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Detail { + @JsonProperty("logid") + private String logID; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/EventType.java b/api/src/main/java/com/coze/openapi/client/websocket/event/EventType.java new file mode 100644 index 00000000..b71f0318 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/EventType.java @@ -0,0 +1,92 @@ +package com.coze.openapi.client.websocket.event; + +public class EventType { + // common + public static final String CLIENT_ERROR = "client_error"; // sdk error + public static final String CLOSED = "closed"; // connection closed + + // error + public static final String ERROR = "error"; // received error event + + // v1/audio/speech + // req + public static final String INPUT_TEXT_BUFFER_APPEND = + "input_text_buffer.append"; // send text to server + public static final String INPUT_TEXT_BUFFER_COMPLETE = + "input_text_buffer.complete"; // no text to send, after audio all received, can close + // connection + public static final String SPEECH_UPDATE = "speech.update"; // send speech config to server + + // resp + // v1/audio/speech + public static final String SPEECH_UPDATED = "speech.updated"; // send speech config to server + public static final String SPEECH_CREATED = "speech.created"; // after speech created + public static final String INPUT_TEXT_BUFFER_COMPLETED = + "input_text_buffer.completed"; // received `input_text_buffer.complete` event + public static final String SPEECH_AUDIO_UPDATE = + "speech.audio.update"; // received `speech.update` event + public static final String SPEECH_AUDIO_COMPLETED = + "speech.audio.completed"; // all audio received, can close connection + + // v1/audio/transcriptions + // req + public static final String INPUT_AUDIO_BUFFER_APPEND = + "input_audio_buffer.append"; // send audio to server + public static final String INPUT_AUDIO_BUFFER_COMPLETE = + "input_audio_buffer.complete"; // no audio to send, after text all received, can close + // connection + public static final String TRANSCRIPTIONS_UPDATE = + "transcriptions.update"; // send transcriptions config to server + + // resp + public static final String TRANSCRIPTIONS_CREATED = + "transcriptions.created"; // after transcriptions created + public static final String TRANSCRIPTIONS_UPDATED = + "transcriptions.updated"; // after transcriptions config updated + public static final String INPUT_AUDIO_BUFFER_COMPLETED = + "input_audio_buffer.completed"; // received `input_audio_buffer.complete` event + public static final String TRANSCRIPTIONS_MESSAGE_UPDATE = + "transcriptions.message.update"; // received `transcriptions.update` event + public static final String TRANSCRIPTIONS_MESSAGE_COMPLETED = + "transcriptions.message.completed"; // all audio received, can close connection + + // v1/chat + // req + public static final String CHAT_UPDATE = "chat.update"; // send chat config to server + public static final String CONVERSATION_CHAT_SUBMIT_TOOL_OUTPUTS = + "conversation.chat.submit_tool_outputs"; // send tool outputs to server + public static final String INPUT_AUDIO_BUFFER_CLEAR = "input_audio_buffer.clear"; + public static final String CONVERSATION_MESSAGE_CREATE = "conversation.message.create"; + public static final String CONVERSATION_CLEAR = "conversation.clear"; + public static final String CONVERSATION_CHAT_CANCEL = "conversation.chat.cancel"; + + // resp + public static final String CHAT_CREATED = "chat.created"; + public static final String CHAT_UPDATED = "chat.updated"; + public static final String CONVERSATION_CHAT_CREATED = + "conversation.chat.created"; // audio ast completed, chat started + public static final String CONVERSATION_CHAT_IN_PROGRESS = "conversation.chat.in_progress"; + public static final String CONVERSATION_MESSAGE_DELTA = + "conversation.message.delta"; // get agent text message update + public static final String CONVERSATION_AUDIO_DELTA = + "conversation.audio.delta"; // get agent audio message update + public static final String CONVERSATION_MESSAGE_COMPLETED = "conversation.message.completed"; + public static final String CONVERSATION_AUDIO_COMPLETED = "conversation.audio.completed"; + public static final String CONVERSATION_CHAT_COMPLETED = + "conversation.chat.completed"; // all message received, can close connection + public static final String CONVERSATION_CHAT_FAILED = "conversation.chat.failed"; // chat failed + public static final String INPUT_AUDIO_BUFFER_CLEARED = "input_audio_buffer.cleared"; + public static final String CONVERSATION_CLEARED = "conversation.cleared"; + public static final String CONVERSATION_CHAT_CANCELED = "conversation.chat.canceled"; + public static final String CONVERSATION_AUDIO_TRANSCRIPT_UPDATE = + "conversation.audio_transcript.update"; // get agent audio transcript update + public static final String CONVERSATION_AUDIO_TRANSCRIPT_COMPLETED = + "conversation.audio_transcript.completed"; // all audio transcript received, can close + // connection + public static final String CONVERSATION_CHAT_REQUIRES_ACTION = + "conversation.chat.requires_action"; // need plugin submit + public static final String INPUT_AUDIO_BUFFER_SPEECH_STARTED = + "input_audio_buffer.speech_started"; + public static final String INPUT_AUDIO_BUFFER_SPEECH_STOPPED = + "input_audio_buffer.speech_stopped"; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatCreatedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatCreatedEvent.java new file mode 100644 index 00000000..1de0291e --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatCreatedEvent.java @@ -0,0 +1,25 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话创建事件 +// event_type: chat.created +public class ChatCreatedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CHAT_CREATED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatUpdatedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatUpdatedEvent.java new file mode 100644 index 00000000..7b99a24e --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatUpdatedEvent.java @@ -0,0 +1,27 @@ +// event_type: chat.updated +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.model.ChatUpdateEventData; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话配置成功事件 +// event_type: chat.updated +public class ChatUpdatedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CHAT_UPDATED; + + @JsonProperty("data") + private ChatUpdateEventData data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioCompletedEvent.java new file mode 100644 index 00000000..60e1c816 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioCompletedEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.connversations.message.model.Message; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 语音回复完成事件 +// event_type: conversation.audio.completed +public class ConversationAudioCompletedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_AUDIO_COMPLETED; + + @JsonProperty("data") + private Message data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioDeltaEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioDeltaEvent.java new file mode 100644 index 00000000..bc40fff4 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioDeltaEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.connversations.message.model.Message; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 增量语音事件 +// event_type: conversation.audio.delta +public class ConversationAudioDeltaEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_AUDIO_DELTA; + + @JsonProperty("data") + private Message data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioTranscriptCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioTranscriptCompletedEvent.java new file mode 100644 index 00000000..2ab896ad --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioTranscriptCompletedEvent.java @@ -0,0 +1,34 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 语音转录完成事件 +// event_type: conversation.audio_transcript.completed +public class ConversationAudioTranscriptCompletedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_AUDIO_TRANSCRIPT_COMPLETED; + + @JsonProperty("data") + private ConversationAudioTranscriptUpdateEvent.Data data; + + @lombok.Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Data { + @JsonProperty("content") + private String content; + } +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioTranscriptUpdateEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioTranscriptUpdateEvent.java new file mode 100644 index 00000000..20ea5b81 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioTranscriptUpdateEvent.java @@ -0,0 +1,34 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 语音转录更新事件 +// event_type: conversation.audio_transcript.update +public class ConversationAudioTranscriptUpdateEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_AUDIO_TRANSCRIPT_UPDATE; + + @JsonProperty("data") + private Data data; + + @lombok.Data + @SuperBuilder + @NoArgsConstructor + @AllArgsConstructor + public static class Data { + @JsonProperty("content") + private String content; + } +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCanceledEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCanceledEvent.java new file mode 100644 index 00000000..c0079c9b --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCanceledEvent.java @@ -0,0 +1,21 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话取消事件 +// event_type: conversation.chat.canceled +public class ConversationChatCanceledEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CHAT_CANCELED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCompletedEvent.java new file mode 100644 index 00000000..2cfb170d --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCompletedEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.chat.model.Chat; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话完成事件 +// event_type: conversation.chat.completed +public class ConversationChatCompletedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CHAT_COMPLETED; + + @JsonProperty("data") + private Chat data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCreatedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCreatedEvent.java new file mode 100644 index 00000000..e6d8bde1 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCreatedEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.chat.model.Chat; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话创建事件 +// event_type: conversation.chat.created +public class ConversationChatCreatedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CHAT_CREATED; + + @JsonProperty("data") + private Chat data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatFailedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatFailedEvent.java new file mode 100644 index 00000000..16976227 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatFailedEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.chat.model.Chat; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话失败事件 +// event_type: conversation.chat.failed +public class ConversationChatFailedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CHAT_FAILED; + + @JsonProperty("data") + private Chat data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatInProgressEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatInProgressEvent.java new file mode 100644 index 00000000..2803d3a2 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatInProgressEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.chat.model.Chat; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话正在处理事件 +// event_type: conversation.chat.in_progress +public class ConversationChatInProgressEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CHAT_IN_PROGRESS; + + @JsonProperty("data") + private Chat data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatRequiresActionEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatRequiresActionEvent.java new file mode 100644 index 00000000..4b48fe2d --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatRequiresActionEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.chat.model.Chat; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话需要操作事件 +// event_type: conversation.chat.requires_action +public class ConversationChatRequiresActionEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CHAT_REQUIRES_ACTION; + + @JsonProperty("data") + private Chat data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationClearedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationClearedEvent.java new file mode 100644 index 00000000..8aa2c8cd --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationClearedEvent.java @@ -0,0 +1,21 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 对话开始事件 +// event_type: conversation.cleared +public class ConversationClearedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CLEARED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationMessageCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationMessageCompletedEvent.java new file mode 100644 index 00000000..1336a965 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationMessageCompletedEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.connversations.message.model.Message; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 消息完成事件 +// event_type: conversation.message.completed +public class ConversationMessageCompletedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_MESSAGE_COMPLETED; + + @JsonProperty("data") + private Message data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationMessageDeltaEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationMessageDeltaEvent.java new file mode 100644 index 00000000..16675dda --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationMessageDeltaEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.connversations.message.model.Message; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 增量消息事件 +// event_type: conversation.message.delta +public class ConversationMessageDeltaEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_MESSAGE_DELTA; + + @JsonProperty("data") + private Message data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ErrorEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ErrorEvent.java new file mode 100644 index 00000000..3be145d6 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ErrorEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.exception.CozeApiException; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 发生异常 +// event_type: error +public class ErrorEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.ERROR; + + @JsonProperty("data") + private CozeApiException data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferClearedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferClearedEvent.java new file mode 100644 index 00000000..9e93a1ce --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferClearedEvent.java @@ -0,0 +1,21 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 语音清除成功 +// event_type: input_audio_buffer.cleared +public class InputAudioBufferClearedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_AUDIO_BUFFER_CLEARED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferCompletedEvent.java new file mode 100644 index 00000000..4cebe6f9 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferCompletedEvent.java @@ -0,0 +1,21 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 语音提交成功 +// event_type: input_audio_buffer.completed +public class InputAudioBufferCompletedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_AUDIO_BUFFER_COMPLETED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferSpeechStartedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferSpeechStartedEvent.java new file mode 100644 index 00000000..acab3cd3 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferSpeechStartedEvent.java @@ -0,0 +1,21 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 语音提交成功 +// event_type: input_audio_buffer.completed +public class InputAudioBufferSpeechStartedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_AUDIO_BUFFER_SPEECH_STARTED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferSpeechStoppedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferSpeechStoppedEvent.java new file mode 100644 index 00000000..5978c166 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferSpeechStoppedEvent.java @@ -0,0 +1,19 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class InputAudioBufferSpeechStoppedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_AUDIO_BUFFER_SPEECH_STOPPED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputTextBufferCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputTextBufferCompletedEvent.java new file mode 100644 index 00000000..7c5a939a --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputTextBufferCompletedEvent.java @@ -0,0 +1,20 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: input_text_buffer.completed +public class InputTextBufferCompletedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_TEXT_BUFFER_COMPLETED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioCompletedEvent.java new file mode 100644 index 00000000..40e14b43 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioCompletedEvent.java @@ -0,0 +1,24 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: speech.audio.completed +public class SpeechAudioCompletedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.SPEECH_AUDIO_COMPLETED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioUpdateEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioUpdateEvent.java new file mode 100644 index 00000000..56ccfd81 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioUpdateEvent.java @@ -0,0 +1,42 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import java.util.Base64; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: speech.audio.update +public class SpeechAudioUpdateEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.SPEECH_AUDIO_UPDATE; + + @JsonProperty("data") + private Data data; + + @lombok.Data + @NoArgsConstructor + @AllArgsConstructor + public static class Data { + @JsonProperty("delta") + private String delta; + } + + public static SpeechAudioUpdateEvent of(String delta) { + return builder().data(new SpeechAudioUpdateEvent.Data(delta)).build(); + } + + public byte[] getDelta() { + return Base64.getDecoder().decode(this.data.delta); + } +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechCreatedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechCreatedEvent.java new file mode 100644 index 00000000..0e8d151e --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechCreatedEvent.java @@ -0,0 +1,24 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: speech.created +public class SpeechCreatedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.SPEECH_CREATED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechUpdatedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechUpdatedEvent.java new file mode 100644 index 00000000..16b64b52 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechUpdatedEvent.java @@ -0,0 +1,30 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.model.SpeechUpdateEventData; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: speech.update +public class SpeechUpdatedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.SPEECH_UPDATED; + + @JsonProperty("data") + private SpeechUpdateEventData data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsCreatedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsCreatedEvent.java new file mode 100644 index 00000000..594967e6 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsCreatedEvent.java @@ -0,0 +1,21 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 转录创建事件 +// event_type: transcriptions.created +public class TranscriptionsCreatedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.TRANSCRIPTIONS_CREATED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsMessageCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsMessageCompletedEvent.java new file mode 100644 index 00000000..8e5e8443 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsMessageCompletedEvent.java @@ -0,0 +1,22 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 转录消息完成事件 +// event_type: transcriptions.message.completed +public class TranscriptionsMessageCompletedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.TRANSCRIPTIONS_MESSAGE_COMPLETED; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsMessageUpdateEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsMessageUpdateEvent.java new file mode 100644 index 00000000..72fafefb --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsMessageUpdateEvent.java @@ -0,0 +1,34 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@lombok.Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 转录消息更新事件 +// event_type: transcriptions.message.update +public class TranscriptionsMessageUpdateEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.TRANSCRIPTIONS_MESSAGE_UPDATE; + + @JsonProperty("data") + private Data data; + + @lombok.Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Data { + @JsonProperty("content") + private String content; + } +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsUpdatedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsUpdatedEvent.java new file mode 100644 index 00000000..c68436be --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsUpdatedEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.downstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.model.TranscriptionsUpdateEventData; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 转录配置更新事件 +// event_type: transcriptions.updated +public class TranscriptionsUpdatedEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.TRANSCRIPTIONS_UPDATED; + + @JsonProperty("data") + private TranscriptionsUpdateEventData data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatConfig.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatConfig.java new file mode 100644 index 00000000..d6784fec --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatConfig.java @@ -0,0 +1,32 @@ +package com.coze.openapi.client.websocket.event.model; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class ChatConfig { + @JsonProperty("conversation_id") + private String conversationId; + + @JsonProperty("user_id") + private String userId; + + @JsonProperty("meta_data") + private Map metaData; + + @JsonProperty("custom_variables") + private Map customVariables; + + @JsonProperty("extra_params") + private Map extraParams; + + @JsonProperty("auto_save_history") + private Boolean autoSaveHistory; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatUpdateEventData.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatUpdateEventData.java new file mode 100644 index 00000000..9fad4fd9 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatUpdateEventData.java @@ -0,0 +1,27 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class ChatUpdateEventData { + + @JsonProperty("input_audio") + private InputAudio inputAudio; + + @JsonProperty("output_audio") + private OutputAudio outputAudio; + + @JsonProperty("chat_config") + private ChatConfig chatConfig; + + @JsonProperty("turn_detection") + private TurnDetection turnDetection; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/InputAudio.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/InputAudio.java new file mode 100644 index 00000000..db469b44 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/InputAudio.java @@ -0,0 +1,27 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class InputAudio { + @JsonProperty("format") + private String format; + + @JsonProperty("codec") + private String codec; + + @JsonProperty("sample_rate") + private Integer sampleRate; + + @JsonProperty("channel") + private Integer channel; + + @JsonProperty("bit_depth") + private Integer bitDepth; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/LimitConfig.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/LimitConfig.java new file mode 100644 index 00000000..11eae02a --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/LimitConfig.java @@ -0,0 +1,18 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class LimitConfig { + @JsonProperty("period") + private Integer period; + + @JsonProperty("max_frame_num") + private Integer maxFrameNum; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/OpusConfig.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/OpusConfig.java new file mode 100644 index 00000000..c61806ee --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/OpusConfig.java @@ -0,0 +1,24 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class OpusConfig { + @JsonProperty("bitrate") + private Integer bitrate; + + @JsonProperty("use_cbr") + private Boolean useCbr; + + @JsonProperty("frame_size_ms") + private Double frameSizeMs; + + @JsonProperty("limit_config") + private LimitConfig limitConfig; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/OutputAudio.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/OutputAudio.java new file mode 100644 index 00000000..a947ee18 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/OutputAudio.java @@ -0,0 +1,27 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class OutputAudio { + @JsonProperty("codec") + private String codec; + + @JsonProperty("pcm_config") + private PCMConfig pcmConfig; + + @JsonProperty("opus_config") + private OpusConfig opusConfig; + + @JsonProperty("speech_rate") + private Integer speechRate; + + @JsonProperty("voice_id") + private String voiceId; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/PCMConfig.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/PCMConfig.java new file mode 100644 index 00000000..895c0e15 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/PCMConfig.java @@ -0,0 +1,15 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class PCMConfig { + @JsonProperty("sample_rate") + private Integer sampleRate; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechUpdateEventData.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechUpdateEventData.java new file mode 100644 index 00000000..cf64e621 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechUpdateEventData.java @@ -0,0 +1,15 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class SpeechUpdateEventData { + @JsonProperty("output_audio") + private OutputAudio outputAudio; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/TranscriptionsUpdateEventData.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/TranscriptionsUpdateEventData.java new file mode 100644 index 00000000..f841a3a1 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/TranscriptionsUpdateEventData.java @@ -0,0 +1,15 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class TranscriptionsUpdateEventData { + @JsonProperty("input_audio") + private InputAudio inputAudio; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/model/TurnDetection.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/TurnDetection.java new file mode 100644 index 00000000..c3a2b13c --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/TurnDetection.java @@ -0,0 +1,22 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class TurnDetection { + @Builder.Default + @JsonProperty("type") + private String type = "client_vad"; + + @JsonProperty("prefix_padding_ms") + private Integer prefixPaddingMs; + + @JsonProperty("suffix_padding_ms") + private Integer silenceDurationMs; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ChatUpdateEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ChatUpdateEvent.java new file mode 100644 index 00000000..eb18ab3d --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ChatUpdateEvent.java @@ -0,0 +1,30 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.model.*; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: chat.update +public class ChatUpdateEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CHAT_UPDATE; + + @JsonProperty("data") + private ChatUpdateEventData data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationChatCancelEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationChatCancelEvent.java new file mode 100644 index 00000000..e63af6de --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationChatCancelEvent.java @@ -0,0 +1,21 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.Builder; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: conversation.chat.cancel +public class ConversationChatCancelEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CHAT_CANCEL; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationChatSubmitToolOutputsEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationChatSubmitToolOutputsEvent.java new file mode 100644 index 00000000..acd68d2c --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationChatSubmitToolOutputsEvent.java @@ -0,0 +1,42 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import java.util.List; + +import com.coze.openapi.client.chat.model.ToolOutput; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: conversation.chat.submit_tool_outputs +public class ConversationChatSubmitToolOutputsEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CHAT_SUBMIT_TOOL_OUTPUTS; + + @JsonProperty("data") + private Data data; + + @lombok.Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Data { + @JsonProperty("chat_id") + private String chatID; + + @JsonProperty("tool_outputs") + private List toolOutputs; + } +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationClearEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationClearEvent.java new file mode 100644 index 00000000..d19dfa23 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationClearEvent.java @@ -0,0 +1,24 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: conversation.clear +public class ConversationClearEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_CLEAR; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationMessageCreateEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationMessageCreateEvent.java new file mode 100644 index 00000000..d5078967 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationMessageCreateEvent.java @@ -0,0 +1,30 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.connversations.message.model.Message; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: conversation.message.create +public class ConversationMessageCreateEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.CONVERSATION_MESSAGE_CREATE; + + @JsonProperty("data") + private Message data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferAppendEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferAppendEvent.java new file mode 100644 index 00000000..d2208217 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferAppendEvent.java @@ -0,0 +1,35 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: input_audio_buffer.append +public class InputAudioBufferAppendEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_AUDIO_BUFFER_APPEND; + + @JsonProperty("data") + private Data data; + + @NoArgsConstructor + @AllArgsConstructor + public static class Data { + @JsonProperty("delta") + private String delta; + } + + public static InputAudioBufferAppendEvent of(String delta) { + return builder().data(new Data(delta)).build(); + } +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferClearEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferClearEvent.java new file mode 100644 index 00000000..68a320d0 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferClearEvent.java @@ -0,0 +1,21 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.Builder; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: input_audio_buffer.clear +public class InputAudioBufferClearEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_AUDIO_BUFFER_CLEAR; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferCompleteEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferCompleteEvent.java new file mode 100644 index 00000000..6ad2c47d --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferCompleteEvent.java @@ -0,0 +1,24 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: input_audio_buffer.complete +public class InputAudioBufferCompleteEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_AUDIO_BUFFER_COMPLETE; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputTextBufferAppendEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputTextBufferAppendEvent.java new file mode 100644 index 00000000..c608fadf --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputTextBufferAppendEvent.java @@ -0,0 +1,40 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: input_text_buffer.append +public class InputTextBufferAppendEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_TEXT_BUFFER_APPEND; + + @JsonProperty("data") + private Data data; + + @NoArgsConstructor + @AllArgsConstructor + public static class Data { + @JsonProperty("delta") + private String delta; + } + + public static InputTextBufferAppendEvent of(String delta) { + return builder().data(new Data(delta)).build(); + } +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputTextBufferCompleteEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputTextBufferCompleteEvent.java new file mode 100644 index 00000000..24546245 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputTextBufferCompleteEvent.java @@ -0,0 +1,24 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: input_text_buffer.complete +public class InputTextBufferCompleteEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.INPUT_TEXT_BUFFER_COMPLETE; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/SpeechUpdateEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/SpeechUpdateEvent.java new file mode 100644 index 00000000..ba715011 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/SpeechUpdateEvent.java @@ -0,0 +1,30 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.model.SpeechUpdateEventData; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// event_type: speech.update +public class SpeechUpdateEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.SPEECH_UPDATE; + + @JsonProperty("data") + private SpeechUpdateEventData data; +} diff --git a/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/TranscriptionsUpdateEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/TranscriptionsUpdateEvent.java new file mode 100644 index 00000000..a67809cc --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/TranscriptionsUpdateEvent.java @@ -0,0 +1,26 @@ +package com.coze.openapi.client.websocket.event.upstream; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.model.TranscriptionsUpdateEventData; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +// 发送转录配置更新事件 +// event_type: transcriptions.update +public class TranscriptionsUpdateEvent extends BaseEvent { + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.TRANSCRIPTIONS_UPDATE; + + @JsonProperty("data") + private TranscriptionsUpdateEventData data; +} diff --git a/api/src/main/java/com/coze/openapi/client/workflows/run/model/WorkflowRunHistory.java b/api/src/main/java/com/coze/openapi/client/workflows/run/model/WorkflowRunHistory.java index 6b8ef2eb..8e7b9946 100644 --- a/api/src/main/java/com/coze/openapi/client/workflows/run/model/WorkflowRunHistory.java +++ b/api/src/main/java/com/coze/openapi/client/workflows/run/model/WorkflowRunHistory.java @@ -75,7 +75,7 @@ public class WorkflowRunHistory { * failed. You can determine the detailed reason for the error through the error_message field. */ @JsonProperty("error_code") - private int errorCode; + private String errorCode; /** Status message. You can get detailed error information when the API call fails. */ @JsonProperty("error_message") diff --git a/api/src/main/java/com/coze/openapi/service/auth/Auth.java b/api/src/main/java/com/coze/openapi/service/auth/Auth.java index 8eee61f1..8461bbd3 100644 --- a/api/src/main/java/com/coze/openapi/service/auth/Auth.java +++ b/api/src/main/java/com/coze/openapi/service/auth/Auth.java @@ -1,19 +1,13 @@ package com.coze.openapi.service.auth; -import com.coze.openapi.client.auth.OAuthToken; - public abstract class Auth { protected String accessToken; protected String refreshToken; + protected long refreshAt; protected long expiresIn; protected OAuthClient client; - protected boolean needRefresh() { - // accessToken 为空代表第一次请求,需要刷新token - return accessToken == null || System.currentTimeMillis() / 1000 > expiresIn; - } - /** * 获取token类型 * @@ -28,15 +22,5 @@ public String tokenType() { * * @return token */ - public String token() { - if (!this.needRefresh()) { - return accessToken; - } - - OAuthToken resp = this.client.refreshToken(this.refreshToken); - this.accessToken = resp.getAccessToken(); - this.refreshToken = resp.getRefreshToken(); - this.expiresIn = resp.getExpiresIn(); - return this.accessToken; - } + public abstract String token(); } diff --git a/api/src/main/java/com/coze/openapi/service/auth/DeviceOAuthClient.java b/api/src/main/java/com/coze/openapi/service/auth/DeviceOAuthClient.java index dfed4e50..6c679a94 100644 --- a/api/src/main/java/com/coze/openapi/service/auth/DeviceOAuthClient.java +++ b/api/src/main/java/com/coze/openapi/service/auth/DeviceOAuthClient.java @@ -2,7 +2,6 @@ import java.util.concurrent.TimeUnit; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import com.coze.openapi.client.auth.*; @@ -32,7 +31,7 @@ public DeviceAuthResp getDeviceCode() { return resp; } - public DeviceAuthResp getDeviceCode(@NotNull String workspaceID) { + public DeviceAuthResp getDeviceCode(String workspaceID) { DeviceAuthReq req = DeviceAuthReq.builder().clientID(this.clientID).build(); DeviceAuthResp resp = execute(this.api.device(workspaceID, req)); resp.setVerificationURL(resp.getVerificationURI() + "?user_code=" + resp.getUserCode()); diff --git a/api/src/main/java/com/coze/openapi/service/auth/JWTOAuth.java b/api/src/main/java/com/coze/openapi/service/auth/JWTOAuth.java index acaeaa20..60e6c909 100644 --- a/api/src/main/java/com/coze/openapi/service/auth/JWTOAuth.java +++ b/api/src/main/java/com/coze/openapi/service/auth/JWTOAuth.java @@ -24,7 +24,10 @@ public JWTOAuth(JWTOAuthClient client) { this.ttl = client.getTtl(); } - @Override + protected boolean needRefresh() { + return accessToken == null || System.currentTimeMillis() / 1000 > refreshAt; + } + public String token() { if (!this.needRefresh()) { return accessToken; @@ -32,6 +35,18 @@ public String token() { OAuthToken resp = this.jwtClient.getAccessToken(this.ttl, this.scope, this.sessionName); this.accessToken = resp.getAccessToken(); this.expiresIn = resp.getExpiresIn(); + this.refreshAt = this.expiresIn - getRefreshBefore(); return this.accessToken; } + + private long getRefreshBefore() { + if (ttl >= 600) { + return 30; + } else if (ttl >= 60) { + return 10; + } else if (ttl >= 30) { + return 5; + } + return 0; + } } diff --git a/api/src/main/java/com/coze/openapi/service/auth/OAuthClient.java b/api/src/main/java/com/coze/openapi/service/auth/OAuthClient.java index 5ab37922..1f2e1cf1 100644 --- a/api/src/main/java/com/coze/openapi/service/auth/OAuthClient.java +++ b/api/src/main/java/com/coze/openapi/service/auth/OAuthClient.java @@ -14,7 +14,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import com.coze.openapi.api.CozeAuthAPI; @@ -40,7 +39,7 @@ public abstract class OAuthClient { private static final String AuthorizeHeader = "Authorization"; - private static final ObjectMapper mapper = Utils.defaultObjectMapper(); + private static final ObjectMapper mapper = Utils.getMapper(); protected final String clientSecret; protected final String clientID; @@ -77,28 +76,25 @@ protected OAuthClient(OAuthBuilder builder) { this.executorService = builder.client.dispatcher().executorService(); } - protected String getOAuthURL(@NotNull String redirectURI, String state) { + protected String getOAuthURL(String redirectURI, String state) { return this._getOAuthURL(redirectURI, state, null, null, null); } - protected String getOAuthURL(@NotNull String redirectURI, String state, String workspace) { + protected String getOAuthURL(String redirectURI, String state, String workspace) { return this._getOAuthURL(redirectURI, state, null, null, workspace); } protected String getOAuthURL( - @NotNull String redirectURI, - String state, - @NotNull String codeChallenge, - @NotNull String codeChallengeMethod) { + String redirectURI, String state, String codeChallenge, String codeChallengeMethod) { return this._getOAuthURL(redirectURI, state, codeChallenge, codeChallengeMethod, null); } protected String getOAuthURL( - @NotNull String redirectURI, + String redirectURI, String state, - @NotNull String codeChallenge, - @NotNull String codeChallengeMethod, - @NotNull String workspaceID) { + String codeChallenge, + String codeChallengeMethod, + String workspaceID) { return this._getOAuthURL(redirectURI, state, codeChallenge, codeChallengeMethod, workspaceID); } @@ -195,7 +191,7 @@ public void shutdownExecutor() { private OAuthToken request( String code, String secret, GrantType grantType, String refreshToken, String redirectURI) { - GetAccessTokenReq.GetAccessTokenReqBuilder builder = GetAccessTokenReq.builder(); + GetAccessTokenReq.GetAccessTokenReqBuilder builder = GetAccessTokenReq.builder(); builder .clientID(this.clientID) .grantType(grantType.getValue()) @@ -330,7 +326,7 @@ protected T init() { defaultClient( Duration.ofMillis(this.readTimeout), Duration.ofMillis(this.connectTimeout)); } else { - OkHttpClient.Builder builder = new OkHttpClient.Builder(client); + OkHttpClient.Builder builder = client.newBuilder(); builder.addInterceptor(new UserAgentInterceptor()); this.client = builder.build(); diff --git a/api/src/main/java/com/coze/openapi/service/auth/PKCEOAuthClient.java b/api/src/main/java/com/coze/openapi/service/auth/PKCEOAuthClient.java index afd17ff8..ce45e853 100644 --- a/api/src/main/java/com/coze/openapi/service/auth/PKCEOAuthClient.java +++ b/api/src/main/java/com/coze/openapi/service/auth/PKCEOAuthClient.java @@ -5,9 +5,6 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import com.coze.openapi.client.auth.*; import com.coze.openapi.service.utils.Utils; @@ -41,17 +38,16 @@ public static PKCEOAuthClient loadFromConfig(LoadAuthConfig loadConfig) { .build(); } - public GetPKCEAuthURLResp genOAuthURL(@NotNull String redirectURI, String state) { + public GetPKCEAuthURLResp genOAuthURL(String redirectURI, String state) { return genOAuthURL(redirectURI, state, CodeChallengeMethod.Plain); } - public GetPKCEAuthURLResp genOAuthURL( - @NotNull String redirectURI, String state, @NotNull String workspaceID) { + public GetPKCEAuthURLResp genOAuthURL(String redirectURI, String state, String workspaceID) { return genOAuthURL(redirectURI, state, CodeChallengeMethod.Plain, workspaceID); } public GetPKCEAuthURLResp genOAuthURL( - @NotNull String redirectURI, String state, @NotNull CodeChallengeMethod codeChallengeMethod) { + String redirectURI, String state, CodeChallengeMethod codeChallengeMethod) { String codeVerifier = Utils.genRandomSign(codeVerifierLen); String url = super.getOAuthURL( @@ -63,10 +59,10 @@ public GetPKCEAuthURLResp genOAuthURL( } public GetPKCEAuthURLResp genOAuthURL( - @NotNull String redirectURI, + String redirectURI, String state, - @NotNull CodeChallengeMethod codeChallengeMethod, - @NotNull String workspaceID) { + CodeChallengeMethod codeChallengeMethod, + String workspaceID) { String codeVerifier = Utils.genRandomSign(codeVerifierLen); String url = super.getOAuthURL( @@ -78,8 +74,7 @@ public GetPKCEAuthURLResp genOAuthURL( return new GetPKCEAuthURLResp(codeVerifier, url); } - private String getCode( - @NotNull String codeVerifier, @NotNull CodeChallengeMethod codeChallengeMethod) { + private String getCode(String codeVerifier, CodeChallengeMethod codeChallengeMethod) { String code = ""; try { code = @@ -92,8 +87,7 @@ private String getCode( return code; } - public OAuthToken getAccessToken( - @NotNull String code, @NotNull String redirectURI, @Nullable String codeVerifier) { + public OAuthToken getAccessToken(String code, String redirectURI, String codeVerifier) { GetAccessTokenReq req = GetAccessTokenReq.builder() .clientID(this.clientID) diff --git a/api/src/main/java/com/coze/openapi/service/auth/TokenAuth.java b/api/src/main/java/com/coze/openapi/service/auth/TokenAuth.java index 7587c403..fafc4d8e 100644 --- a/api/src/main/java/com/coze/openapi/service/auth/TokenAuth.java +++ b/api/src/main/java/com/coze/openapi/service/auth/TokenAuth.java @@ -7,7 +7,6 @@ public TokenAuth(String accessToken) { this.accessToken = accessToken; } - @Override public String token() { return accessToken; } diff --git a/api/src/main/java/com/coze/openapi/service/auth/WebOAuthClient.java b/api/src/main/java/com/coze/openapi/service/auth/WebOAuthClient.java index 43d01630..a0da86fe 100644 --- a/api/src/main/java/com/coze/openapi/service/auth/WebOAuthClient.java +++ b/api/src/main/java/com/coze/openapi/service/auth/WebOAuthClient.java @@ -1,7 +1,5 @@ package com.coze.openapi.service.auth; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.client.auth.GrantType; import com.coze.openapi.client.auth.LoadAuthConfig; import com.coze.openapi.client.auth.OAuthConfig; @@ -23,13 +21,12 @@ public static WebOAuthClient loadFromConfig(LoadAuthConfig loadConfig) { } @Override - public String getOAuthURL(@NotNull String redirectURI, String state) { + public String getOAuthURL(String redirectURI, String state) { return super.getOAuthURL(redirectURI, state); } @Override - public String getOAuthURL( - @NotNull String redirectURI, String state, @NotNull String workspaceID) { + public String getOAuthURL(String redirectURI, String state, String workspaceID) { return super.getOAuthURL(redirectURI, state, workspaceID); } diff --git a/api/src/main/java/com/coze/openapi/service/service/CozeAPI.java b/api/src/main/java/com/coze/openapi/service/service/CozeAPI.java index 2b49aee3..7c10b0da 100644 --- a/api/src/main/java/com/coze/openapi/service/service/CozeAPI.java +++ b/api/src/main/java/com/coze/openapi/service/service/CozeAPI.java @@ -18,12 +18,14 @@ import com.coze.openapi.service.service.dataset.DatasetService; import com.coze.openapi.service.service.file.FileService; import com.coze.openapi.service.service.template.TemplateService; +import com.coze.openapi.service.service.websocket.WebsocketsClient; import com.coze.openapi.service.service.workflow.WorkflowService; import com.coze.openapi.service.service.workspace.WorkspaceService; import com.coze.openapi.service.utils.UserAgentInterceptor; import com.coze.openapi.service.utils.Utils; import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.lang.Strings; import okhttp3.ConnectionPool; import okhttp3.Interceptor; import okhttp3.OkHttpClient; @@ -44,6 +46,7 @@ public class CozeAPI { private final ChatService chatAPI; private final AudioService audioAPI; private final TemplateService templateAPI; + private final WebsocketsClient websocket; private CozeAPI( String baseURL, @@ -57,7 +60,8 @@ private CozeAPI( WorkflowService workflowAPI, ChatService chatAPI, AudioService audioAPI, - TemplateService templateAPI) { + TemplateService templateAPI, + WebsocketsClient websocket) { this.baseURL = baseURL; this.executorService = executorService; this.auth = auth; @@ -70,6 +74,7 @@ private CozeAPI( this.chatAPI = chatAPI; this.audioAPI = audioAPI; this.templateAPI = templateAPI; + this.websocket = websocket; } public WorkspaceService workspaces() { @@ -108,6 +113,10 @@ public TemplateService templates() { return this.templateAPI; } + public WebsocketsClient websockets() { + return this.websocket; + } + public void shutdownExecutor() { Objects.requireNonNull( this.executorService, "executorService must be set in order to shut down"); @@ -166,7 +175,7 @@ public CozeAPI build() { this.baseURL = Consts.COZE_COM_BASE_URL; } - ObjectMapper mapper = Utils.defaultObjectMapper(); + ObjectMapper mapper = Utils.getMapper(); Retrofit retrofit = defaultRetrofit(client, mapper, this.baseURL); ExecutorService executorService = client.dispatcher().executorService(); WorkspaceService workspaceAPI = new WorkspaceService(retrofit.create(WorkspaceAPI.class)); @@ -192,8 +201,12 @@ public CozeAPI build() { new AudioService( retrofit.create(AudioVoiceAPI.class), retrofit.create(AudioRoomAPI.class), - retrofit.create(AudioSpeechAPI.class)); + retrofit.create(AudioSpeechAPI.class), + retrofit.create(AudioTranscriptionAPI.class)); TemplateService templateAPI = new TemplateService(retrofit.create(TemplateAPI.class)); + WebsocketsClient websocket = + new WebsocketsClient( + this.client, Strings.replace(this.baseURL, "https://api", "wss://ws")); return new CozeAPI( this.baseURL, executorService, @@ -206,7 +219,8 @@ public CozeAPI build() { workflowAPI, chatAPI, audioAPI, - templateAPI); + templateAPI, + websocket); } // 确保加上了 Auth 拦截器 @@ -228,7 +242,7 @@ private OkHttpClient parseClient(OkHttpClient client) { if (hasAuthInterceptor && hasTimeoutInterceptor && hasUserAgentInterceptor) { return client; } - OkHttpClient.Builder builder = new OkHttpClient.Builder(client); + OkHttpClient.Builder builder = client.newBuilder(); if (!hasAuthInterceptor) { builder.addInterceptor(new AuthenticationInterceptor(this.auth)); } diff --git a/api/src/main/java/com/coze/openapi/service/service/audio/AudioService.java b/api/src/main/java/com/coze/openapi/service/service/audio/AudioService.java index c7a4c147..0b47f10a 100644 --- a/api/src/main/java/com/coze/openapi/service/service/audio/AudioService.java +++ b/api/src/main/java/com/coze/openapi/service/service/audio/AudioService.java @@ -2,17 +2,24 @@ import com.coze.openapi.api.AudioRoomAPI; import com.coze.openapi.api.AudioSpeechAPI; +import com.coze.openapi.api.AudioTranscriptionAPI; import com.coze.openapi.api.AudioVoiceAPI; public class AudioService { private final VoiceService voiceAPI; private final RoomService roomAPI; private final SpeechService speechAPI; + private final TranscriptionService transcriptionAPI; - public AudioService(AudioVoiceAPI voiceAPI, AudioRoomAPI roomAPI, AudioSpeechAPI speechAPI) { + public AudioService( + AudioVoiceAPI voiceAPI, + AudioRoomAPI roomAPI, + AudioSpeechAPI speechAPI, + AudioTranscriptionAPI transcriptionAPI) { this.voiceAPI = new VoiceService(voiceAPI); this.roomAPI = new RoomService(roomAPI); this.speechAPI = new SpeechService(speechAPI); + this.transcriptionAPI = new TranscriptionService(transcriptionAPI); } public VoiceService voices() { @@ -26,4 +33,8 @@ public RoomService rooms() { public SpeechService speech() { return this.speechAPI; } + + public TranscriptionService transcription() { + return this.transcriptionAPI; + } } diff --git a/api/src/main/java/com/coze/openapi/service/service/audio/TranscriptionService.java b/api/src/main/java/com/coze/openapi/service/service/audio/TranscriptionService.java new file mode 100644 index 00000000..f003d2b8 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/audio/TranscriptionService.java @@ -0,0 +1,55 @@ +package com.coze.openapi.service.service.audio; + +import java.io.File; + +import com.coze.openapi.api.AudioTranscriptionAPI; +import com.coze.openapi.client.audio.transcriptions.CreateTranscriptionsReq; +import com.coze.openapi.client.audio.transcriptions.CreateTranscriptionsResp; +import com.coze.openapi.client.common.BaseReq; +import com.coze.openapi.client.common.BaseResponse; +import com.coze.openapi.service.utils.Utils; + +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +public class TranscriptionService { + private final AudioTranscriptionAPI transcriptionAPI; + + public TranscriptionService(AudioTranscriptionAPI transcriptionAPI) { + this.transcriptionAPI = transcriptionAPI; + } + + public CreateTranscriptionsResp create(CreateTranscriptionsReq req) { + if (req.getFilePath() != null) { + File file = new File(req.getFilePath()); + return uploadFile(file, file.getName(), req); + } + if (req.getFileBytes() != null) { + return uploadFile(req.getFileBytes(), req.getFileName(), req); + } + if (req.getFile() != null) { + return uploadFile(req.getFile(), req.getFileName(), req); + } + throw new IllegalArgumentException("file source is required"); + } + + /** Internal unified upload processing method */ + private CreateTranscriptionsResp uploadFile(Object fileSource, String filename, BaseReq req) { + RequestBody requestFile; + if (fileSource instanceof File) { + MediaType mediaType = MediaType.parse("multipart/form-data"); + requestFile = RequestBody.create(mediaType, (File) fileSource); + } else { + MediaType mediaType = MediaType.parse("multipart/form-data"); + requestFile = RequestBody.create(mediaType, (byte[]) fileSource); + } + + MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, requestFile); + + BaseResponse resp = Utils.execute(transcriptionAPI.create(body, req)); + CreateTranscriptionsResp data = resp.getData(); + data.setLogID(resp.getLogID()); + return data; + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/audio/VoiceService.java b/api/src/main/java/com/coze/openapi/service/service/audio/VoiceService.java index f7a4588c..1fe6d60f 100644 --- a/api/src/main/java/com/coze/openapi/service/service/audio/VoiceService.java +++ b/api/src/main/java/com/coze/openapi/service/service/audio/VoiceService.java @@ -2,8 +2,6 @@ import java.io.File; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.api.AudioVoiceAPI; import com.coze.openapi.client.audio.voices.CloneVoiceReq; import com.coze.openapi.client.audio.voices.CloneVoiceResp; @@ -29,28 +27,29 @@ public VoiceService(AudioVoiceAPI api) { } public CloneVoiceResp clone(CloneVoiceReq req) { - RequestBody voiceName = RequestBody.create(req.getVoiceName(), MediaType.parse("text/plain")); - RequestBody audioFormat = - RequestBody.create(req.getAudioFormat().getValue(), MediaType.parse("text/plain")); + MediaType textPlainType = MediaType.parse("text/plain"); + RequestBody voiceName = RequestBody.create(textPlainType, req.getVoiceName()); + RequestBody audioFormat = RequestBody.create(textPlainType, req.getAudioFormat().getValue()); RequestBody language = null; if (req.getLanguage() != null) { - language = RequestBody.create(req.getLanguage().getValue(), MediaType.parse("text/plain")); + language = RequestBody.create(textPlainType, req.getLanguage().getValue()); } RequestBody voiceID = null; if (req.getVoiceID() != null) { - voiceID = RequestBody.create(req.getVoiceID(), MediaType.parse("text/plain")); + voiceID = RequestBody.create(textPlainType, req.getVoiceID()); } RequestBody previewText = null; if (req.getPreviewText() != null) { - previewText = RequestBody.create(req.getPreviewText(), MediaType.parse("text/plain")); + previewText = RequestBody.create(textPlainType, req.getPreviewText()); } RequestBody text = null; if (req.getText() != null) { - text = RequestBody.create(req.getText(), MediaType.parse("text/plain")); + text = RequestBody.create(textPlainType, req.getText()); } File file = new File(req.getFilePath()); - RequestBody fileBody = RequestBody.create(file, MediaType.parse("multipart/form-data")); + MediaType formDataType = MediaType.parse("multipart/form-data"); + RequestBody fileBody = RequestBody.create(formDataType, file); MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), fileBody); @@ -59,7 +58,7 @@ public CloneVoiceResp clone(CloneVoiceReq req) { .getData(); } - public PageResp list(@NotNull ListVoiceReq req) { + public PageResp list(ListVoiceReq req) { if (req == null) { throw new IllegalArgumentException("req is required"); } diff --git a/api/src/main/java/com/coze/openapi/service/service/bots/BotService.java b/api/src/main/java/com/coze/openapi/service/service/bots/BotService.java index b393bc85..f2048b1d 100644 --- a/api/src/main/java/com/coze/openapi/service/service/bots/BotService.java +++ b/api/src/main/java/com/coze/openapi/service/service/bots/BotService.java @@ -1,7 +1,5 @@ package com.coze.openapi.service.service.bots; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.api.BotAPI; import com.coze.openapi.client.bots.*; import com.coze.openapi.client.bots.model.Bot; @@ -22,7 +20,7 @@ public BotService(BotAPI api) { this.api = api; } - public PageResp list(@NotNull ListBotReq req) { + public PageResp list(ListBotReq req) { if (req == null) { throw new IllegalArgumentException("req is required"); } @@ -48,8 +46,7 @@ public PageResp list(@NotNull ListBotReq req) { .build(); } - @NotNull - private PageFetcher getSimpleBotPageFetcher(@NotNull ListBotReq req) { + private PageFetcher getSimpleBotPageFetcher(ListBotReq req) { String spaceID = req.getSpaceID(); // 创建分页获取器 @@ -69,21 +66,21 @@ private PageFetcher getSimpleBotPageFetcher(@NotNull ListBotReq req) return pageFetcher; } - public RetrieveBotResp retrieve(@NotNull RetrieveBotReq req) { + public RetrieveBotResp retrieve(RetrieveBotReq req) { BaseResponse resp = Utils.execute(api.retrieve(req.getBotID(), req)); return RetrieveBotResp.builder().bot(resp.getData()).logID(resp.getLogID()).build(); } - public CreateBotResp create(@NotNull CreateBotReq req) { + public CreateBotResp create(CreateBotReq req) { return Utils.execute(api.create(req, req)).getData(); } - public UpdateBotResp update(@NotNull UpdateBotReq req) { + public UpdateBotResp update(UpdateBotReq req) { String logID = Utils.execute(api.update(req, req)).getLogID(); return UpdateBotResp.builder().logID(logID).build(); } - public PublishBotResp publish(@NotNull PublishBotReq req) { + public PublishBotResp publish(PublishBotReq req) { return Utils.execute(api.publish(req, req)).getData(); } } diff --git a/api/src/main/java/com/coze/openapi/service/service/common/AbstractEventCallback.java b/api/src/main/java/com/coze/openapi/service/service/common/AbstractEventCallback.java index a2f70208..ff2cc09a 100644 --- a/api/src/main/java/com/coze/openapi/service/service/common/AbstractEventCallback.java +++ b/api/src/main/java/com/coze/openapi/service/service/common/AbstractEventCallback.java @@ -23,7 +23,7 @@ import retrofit2.Response; public abstract class AbstractEventCallback implements Callback { - private static final ObjectMapper mapper = Utils.defaultObjectMapper(); + private static final ObjectMapper mapper = Utils.getMapper(); private static final Logger logger = CozeLoggerFactory.getLogger(); private final ExecutorService backgroundExecutor; diff --git a/api/src/main/java/com/coze/openapi/service/service/conversation/ConversationService.java b/api/src/main/java/com/coze/openapi/service/service/conversation/ConversationService.java index 821a0942..c2fb9011 100644 --- a/api/src/main/java/com/coze/openapi/service/service/conversation/ConversationService.java +++ b/api/src/main/java/com/coze/openapi/service/service/conversation/ConversationService.java @@ -1,7 +1,5 @@ package com.coze.openapi.service.service.conversation; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.api.ConversationAPI; import com.coze.openapi.api.ConversationMessageAPI; import com.coze.openapi.client.common.BaseResponse; @@ -63,7 +61,7 @@ public ClearConversationResp clear(ClearConversationReq req) { return Utils.execute(api.clear(req.getConversationID(), req)).getData(); } - public PageResp list(@NotNull ListConversationReq req) { + public PageResp list(ListConversationReq req) { if (req == null || req.getBotID() == null) { throw new IllegalArgumentException("botID is required"); } diff --git a/api/src/main/java/com/coze/openapi/service/service/conversation/MessageService.java b/api/src/main/java/com/coze/openapi/service/service/conversation/MessageService.java index 020b5e36..ea3e28e2 100644 --- a/api/src/main/java/com/coze/openapi/service/service/conversation/MessageService.java +++ b/api/src/main/java/com/coze/openapi/service/service/conversation/MessageService.java @@ -1,7 +1,5 @@ package com.coze.openapi.service.service.conversation; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.api.ConversationMessageAPI; import com.coze.openapi.client.common.BaseResponse; import com.coze.openapi.client.common.pagination.PageFetcher; @@ -63,7 +61,7 @@ public CreateMessageResp create(CreateMessageReq req) { docs en: https://www.coze.com/docs/developer_guides/list_message docs zh: https://www.coze.cn/docs/developer_guides/list_message * */ - public PageResp list(@NotNull ListMessageReq req) { + public PageResp list(ListMessageReq req) { if (req == null || req.getConversationID() == null) { throw new IllegalArgumentException("conversationID is required"); } @@ -91,9 +89,7 @@ public PageResp list(@NotNull ListMessageReq req) { .build(); } - @NotNull - private PageFetcher getMessagePageFetcher( - @NotNull ListMessageReq req, String conversationID) { + private PageFetcher getMessagePageFetcher(ListMessageReq req, String conversationID) { // 创建分页获取器 PageFetcher pageFetcher = diff --git a/api/src/main/java/com/coze/openapi/service/service/dataset/DocumentService.java b/api/src/main/java/com/coze/openapi/service/service/dataset/DocumentService.java index 4c5f1e03..b0668747 100644 --- a/api/src/main/java/com/coze/openapi/service/service/dataset/DocumentService.java +++ b/api/src/main/java/com/coze/openapi/service/service/dataset/DocumentService.java @@ -1,7 +1,5 @@ package com.coze.openapi.service.service.dataset; -import org.jetbrains.annotations.NotNull; - import com.coze.openapi.api.DatasetDocumentAPI; import com.coze.openapi.client.common.pagination.PageFetcher; import com.coze.openapi.client.common.pagination.PageNumBasedPaginator; @@ -58,7 +56,7 @@ public DeleteDocumentResp delete(DeleteDocumentReq req) { docs en: https://www.coze.com/docs/developer_guides/list_knowledge_files docs zh: https://www.coze.cn/docs/developer_guides/list_knowledge_files * */ - public PageResp list(@NotNull ListDocumentReq req) { + public PageResp list(ListDocumentReq req) { if (req == null || req.getDatasetID() == null) { throw new IllegalArgumentException("req is required"); } diff --git a/api/src/main/java/com/coze/openapi/service/service/file/FileService.java b/api/src/main/java/com/coze/openapi/service/service/file/FileService.java index e610d0fd..207fa271 100644 --- a/api/src/main/java/com/coze/openapi/service/service/file/FileService.java +++ b/api/src/main/java/com/coze/openapi/service/service/file/FileService.java @@ -51,9 +51,11 @@ public UploadFileResp upload(UploadFileReq req) { private UploadFileResp uploadFile(Object fileSource, String filename, BaseReq req) { RequestBody requestFile; if (fileSource instanceof File) { - requestFile = RequestBody.create((File) fileSource, MediaType.parse("multipart/form-data")); + MediaType mediaType = MediaType.parse("multipart/form-data"); + requestFile = RequestBody.create(mediaType, (File) fileSource); } else { - requestFile = RequestBody.create((byte[]) fileSource, MediaType.parse("multipart/form-data")); + MediaType mediaType = MediaType.parse("multipart/form-data"); + requestFile = RequestBody.create(mediaType, (byte[]) fileSource); } MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, requestFile); diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketsClient.java new file mode 100644 index 00000000..e3cffdb9 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketsClient.java @@ -0,0 +1,26 @@ +package com.coze.openapi.service.service.websocket; + +import com.coze.openapi.service.service.websocket.audio.WebsocketsAudioClient; +import com.coze.openapi.service.service.websocket.chat.WebsocketsChatClientBuilder; + +import okhttp3.OkHttpClient; + +public class WebsocketsClient { + + private final WebsocketsChatClientBuilder chat; + + private final WebsocketsAudioClient audio; + + public WebsocketsClient(OkHttpClient client, String baseURL) { + this.chat = new WebsocketsChatClientBuilder(baseURL, client); + this.audio = new WebsocketsAudioClient(baseURL, client); + } + + public WebsocketsChatClientBuilder chat() { + return chat; + } + + public WebsocketsAudioClient audio() { + return audio; + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketsAudioClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketsAudioClient.java new file mode 100644 index 00000000..d1bcfca5 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketsAudioClient.java @@ -0,0 +1,26 @@ +package com.coze.openapi.service.service.websocket.audio; + +import com.coze.openapi.service.service.websocket.audio.speech.WebsocketsAudioSpeechBuilder; +import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketsAudioTranscriptionsBuilder; + +import okhttp3.OkHttpClient; + +public class WebsocketsAudioClient { + + private final WebsocketsAudioSpeechBuilder websocketAudioSpeechBuilder; + private final WebsocketsAudioTranscriptionsBuilder websocketAudioTranscriptionsBuilder; + + public WebsocketsAudioClient(String baseUrl, OkHttpClient httpClient) { + this.websocketAudioSpeechBuilder = new WebsocketsAudioSpeechBuilder(baseUrl, httpClient); + this.websocketAudioTranscriptionsBuilder = + new WebsocketsAudioTranscriptionsBuilder(baseUrl, httpClient); + } + + public WebsocketsAudioSpeechBuilder speech() { + return websocketAudioSpeechBuilder; + } + + public WebsocketsAudioTranscriptionsBuilder transcriptions() { + return websocketAudioTranscriptionsBuilder; + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechBuilder.java new file mode 100644 index 00000000..bc0e192e --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechBuilder.java @@ -0,0 +1,14 @@ +package com.coze.openapi.service.service.websocket.audio.speech; + +import lombok.AllArgsConstructor; +import okhttp3.OkHttpClient; + +@AllArgsConstructor +public class WebsocketsAudioSpeechBuilder { + private final String baseUrl; + private final OkHttpClient httpClient; + + public WebsocketsAudioSpeechClient create(WebsocketsAudioSpeechCreateReq req) { + return new WebsocketsAudioSpeechClient(httpClient, baseUrl, req); + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCallbackHandler.java new file mode 100644 index 00000000..9bb0eb84 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCallbackHandler.java @@ -0,0 +1,31 @@ +package com.coze.openapi.service.service.websocket.audio.speech; + +import com.coze.openapi.client.websocket.event.downstream.InputTextBufferCompletedEvent; +import com.coze.openapi.client.websocket.event.downstream.SpeechAudioCompletedEvent; +import com.coze.openapi.client.websocket.event.downstream.SpeechAudioUpdateEvent; +import com.coze.openapi.client.websocket.event.downstream.SpeechCreatedEvent; +import com.coze.openapi.client.websocket.event.downstream.SpeechUpdatedEvent; +import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; + +public abstract class WebsocketsAudioSpeechCallbackHandler + extends BaseCallbackHandler { + public WebsocketsAudioSpeechCallbackHandler() {} + + // 语音创建成功事件 (speech.created) + public void onSpeechCreated(WebsocketsAudioSpeechClient client, SpeechCreatedEvent event) {} + + // 语音配置更新事件 (speech.update) + public void onSpeechUpdated(WebsocketsAudioSpeechClient client, SpeechUpdatedEvent event) {} + + // 语音数据更新事件 (speech.audio.update) + public void onSpeechAudioUpdate( + WebsocketsAudioSpeechClient client, SpeechAudioUpdateEvent event) {} + + // 语音数据完成事件 (speech.audio.completed) + public void onSpeechAudioCompleted( + WebsocketsAudioSpeechClient client, SpeechAudioCompletedEvent event) {} + + // 文本缓冲区完成事件 (input_text_buffer.completed) + public void onInputTextBufferCompleted( + WebsocketsAudioSpeechClient client, InputTextBufferCompletedEvent event) {} +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechClient.java new file mode 100644 index 00000000..d72d3fbd --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechClient.java @@ -0,0 +1,94 @@ +package com.coze.openapi.service.service.websocket.audio.speech; + +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.SpeechUpdateEventData; +import com.coze.openapi.client.websocket.event.upstream.*; +import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; +import com.coze.openapi.service.service.websocket.common.BaseWebsocketsClient; +import com.fasterxml.jackson.databind.JsonNode; + +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketsAudioSpeechClient extends BaseWebsocketsClient { + private final WebsocketsAudioSpeechCallbackHandler handler; + private static final String uri = "/v1/audio/speech"; + + protected WebsocketsAudioSpeechClient( + OkHttpClient client, String wsHost, WebsocketsAudioSpeechCreateReq req) { + super(client, buildUrl(wsHost), req.getCallbackHandler(), req); + this.handler = req.getCallbackHandler(); + } + + protected static String buildUrl(String wsHost) { + return String.format("%s%s", wsHost, uri); + } + + // 发送语音配置更新事件 + public void speechUpdate(SpeechUpdateEventData data) { + this.sendEvent(SpeechUpdateEvent.builder().data(data).build()); + } + + // 发送文本缓冲区追加事件 + public void inputTextBufferAppend(String data) { + this.sendEvent(InputTextBufferAppendEvent.of(data)); + } + + // 发送文本缓冲区完成事件 + public void inputTextBufferComplete() { + this.sendEvent(new InputTextBufferCompleteEvent()); + } + + @Override + protected BaseCallbackHandler getCallbackHandler() { + return handler; + } + + @Override + protected void handleEvent(WebSocket ws, String text) { + try { + JsonNode jsonNode = objectMapper.readTree(text); + String eventType = parseEventType(jsonNode, text); + if (eventType == null) { + // 这里在上面已经抛出异常了,直接 return 即可 + return; + } + switch (eventType) { + case EventType.SPEECH_CREATED: + SpeechCreatedEvent speechCreatedEvent = + objectMapper.treeToValue(jsonNode, SpeechCreatedEvent.class); + handler.onSpeechCreated(this, speechCreatedEvent); + break; + case EventType.SPEECH_UPDATED: + SpeechUpdatedEvent speechUpdatedEvent = + objectMapper.treeToValue(jsonNode, SpeechUpdatedEvent.class); + handler.onSpeechUpdated(this, speechUpdatedEvent); + break; + case EventType.SPEECH_AUDIO_UPDATE: + SpeechAudioUpdateEvent audioUpdateEvent = + objectMapper.treeToValue(jsonNode, SpeechAudioUpdateEvent.class); + handler.onSpeechAudioUpdate(this, audioUpdateEvent); + break; + case EventType.SPEECH_AUDIO_COMPLETED: + SpeechAudioCompletedEvent audioCompletedEvent = + objectMapper.treeToValue(jsonNode, SpeechAudioCompletedEvent.class); + handler.onSpeechAudioCompleted(this, audioCompletedEvent); + break; + case EventType.INPUT_TEXT_BUFFER_COMPLETED: + InputTextBufferCompletedEvent bufferCompletedEvent = + objectMapper.treeToValue(jsonNode, InputTextBufferCompletedEvent.class); + handler.onInputTextBufferCompleted(this, bufferCompletedEvent); + break; + case EventType.ERROR: + ErrorEvent errorEvent = objectMapper.treeToValue(jsonNode, ErrorEvent.class); + handler.onError(this, errorEvent); + break; + default: + logger.error("unknown event type: {}, event string: {}", eventType, text); + } + } catch (Exception e) { + handler.onClientException(this, new RuntimeException(e)); + } + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCreateReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCreateReq.java new file mode 100644 index 00000000..e1440b21 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCreateReq.java @@ -0,0 +1,15 @@ +package com.coze.openapi.service.service.websocket.audio.speech; + +import com.coze.openapi.client.common.BaseReq; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WebsocketsAudioSpeechCreateReq extends BaseReq { + @NonNull private WebsocketsAudioSpeechCallbackHandler callbackHandler; +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsBuilder.java new file mode 100644 index 00000000..f4be265f --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsBuilder.java @@ -0,0 +1,14 @@ +package com.coze.openapi.service.service.websocket.audio.transcriptions; + +import lombok.AllArgsConstructor; +import okhttp3.OkHttpClient; + +@AllArgsConstructor +public class WebsocketsAudioTranscriptionsBuilder { + private final String baseUrl; + private final OkHttpClient httpClient; + + public WebsocketsAudioTranscriptionsClient create(WebsocketsAudioTranscriptionsCreateReq req) { + return new WebsocketsAudioTranscriptionsClient(httpClient, baseUrl, req); + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCallbackHandler.java new file mode 100644 index 00000000..8024cd52 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCallbackHandler.java @@ -0,0 +1,38 @@ +package com.coze.openapi.service.service.websocket.audio.transcriptions; + +import com.coze.openapi.client.websocket.event.downstream.InputAudioBufferClearedEvent; +import com.coze.openapi.client.websocket.event.downstream.InputAudioBufferCompletedEvent; +import com.coze.openapi.client.websocket.event.downstream.TranscriptionsCreatedEvent; +import com.coze.openapi.client.websocket.event.downstream.TranscriptionsMessageCompletedEvent; +import com.coze.openapi.client.websocket.event.downstream.TranscriptionsMessageUpdateEvent; +import com.coze.openapi.client.websocket.event.downstream.TranscriptionsUpdatedEvent; +import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; + +public abstract class WebsocketsAudioTranscriptionsCallbackHandler + extends BaseCallbackHandler { + public WebsocketsAudioTranscriptionsCallbackHandler() {} + + // 转录创建事件 (transcriptions.created) + public void onTranscriptionsCreated( + WebsocketsAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) {} + + // 转录配置更新事件 (transcriptions.updated) + public void onTranscriptionsUpdated( + WebsocketsAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) {} + + // 转录消息更新事件 (transcriptions.message.update) + public void onTranscriptionsMessageUpdate( + WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) {} + + // 转录消息完成事件 (transcriptions.message.completed) + public void onTranscriptionsMessageCompleted( + WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) {} + + // 语音缓冲区清除事件 (input_audio_buffer.cleared) + public void onInputAudioBufferCleared( + WebsocketsAudioTranscriptionsClient client, InputAudioBufferClearedEvent event) {} + + // 语音缓冲区完成事件 (input_audio_buffer.completed) + public void onInputAudioBufferCompleted( + WebsocketsAudioTranscriptionsClient client, InputAudioBufferCompletedEvent event) {} +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsClient.java new file mode 100644 index 00000000..eed3f6c2 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsClient.java @@ -0,0 +1,112 @@ +package com.coze.openapi.service.service.websocket.audio.transcriptions; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.TranscriptionsUpdateEventData; +import com.coze.openapi.client.websocket.event.upstream.*; +import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; +import com.coze.openapi.service.service.websocket.common.BaseWebsocketsClient; +import com.fasterxml.jackson.databind.JsonNode; + +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketsAudioTranscriptionsClient extends BaseWebsocketsClient { + private final WebsocketsAudioTranscriptionsCallbackHandler handler; + private static final String uri = "/v1/audio/transcriptions"; + + protected WebsocketsAudioTranscriptionsClient( + OkHttpClient client, String wsHost, WebsocketsAudioTranscriptionsCreateReq req) { + super(client, buildUrl(wsHost), req.getCallbackHandler(), req); + this.handler = req.getCallbackHandler(); + } + + protected static String buildUrl(String wsHost) { + return String.format("%s%s", wsHost, uri); + } + + // 发送转录配置更新事件 + public void transcriptionsUpdate(TranscriptionsUpdateEventData data) { + this.sendEvent(TranscriptionsUpdateEvent.builder().data(data).build()); + } + + // 发送语音缓冲区追加事件 + public void inputAudioBufferAppend(String data) { + this.inputAudioBufferAppend(data.getBytes(StandardCharsets.UTF_8)); + } + + public void inputAudioBufferAppend(byte[] data) { + String base64Data = Base64.getEncoder().encodeToString(data); + this.sendEvent(InputAudioBufferAppendEvent.of(base64Data)); + } + + // 发送语音缓冲区清除事件 + public void inputAudioBufferClear() { + this.sendEvent(new InputAudioBufferClearEvent()); + } + + // 发送语音缓冲区完成事件 + public void inputAudioBufferComplete() { + this.sendEvent(new InputAudioBufferCompleteEvent()); + } + + @Override + protected BaseCallbackHandler getCallbackHandler() { + return handler; + } + + @Override + protected void handleEvent(WebSocket ws, String text) { + try { + JsonNode jsonNode = objectMapper.readTree(text); + String eventType = parseEventType(jsonNode, text); + if (eventType == null) { + // 这里在上面已经抛出异常了,直接 return 即可 + return; + } + switch (eventType) { + case EventType.TRANSCRIPTIONS_CREATED: + TranscriptionsCreatedEvent createdEvent = + objectMapper.treeToValue(jsonNode, TranscriptionsCreatedEvent.class); + handler.onTranscriptionsCreated(this, createdEvent); + break; + case EventType.TRANSCRIPTIONS_UPDATED: + TranscriptionsUpdatedEvent updatedEvent = + objectMapper.treeToValue(jsonNode, TranscriptionsUpdatedEvent.class); + handler.onTranscriptionsUpdated(this, updatedEvent); + break; + case EventType.TRANSCRIPTIONS_MESSAGE_UPDATE: + TranscriptionsMessageUpdateEvent messageUpdateEvent = + objectMapper.treeToValue(jsonNode, TranscriptionsMessageUpdateEvent.class); + handler.onTranscriptionsMessageUpdate(this, messageUpdateEvent); + break; + case EventType.TRANSCRIPTIONS_MESSAGE_COMPLETED: + TranscriptionsMessageCompletedEvent messageCompletedEvent = + objectMapper.treeToValue(jsonNode, TranscriptionsMessageCompletedEvent.class); + handler.onTranscriptionsMessageCompleted(this, messageCompletedEvent); + break; + case EventType.INPUT_AUDIO_BUFFER_CLEARED: + InputAudioBufferClearedEvent bufferClearedEvent = + objectMapper.treeToValue(jsonNode, InputAudioBufferClearedEvent.class); + handler.onInputAudioBufferCleared(this, bufferClearedEvent); + break; + case EventType.INPUT_AUDIO_BUFFER_COMPLETED: + InputAudioBufferCompletedEvent bufferCompletedEvent = + objectMapper.treeToValue(jsonNode, InputAudioBufferCompletedEvent.class); + handler.onInputAudioBufferCompleted(this, bufferCompletedEvent); + break; + case EventType.ERROR: + ErrorEvent errorEvent = objectMapper.treeToValue(jsonNode, ErrorEvent.class); + handler.onError(this, errorEvent); + break; + default: + logger.error("unknown event type: {}, event string: {}", eventType, text); + } + } catch (Exception e) { + handler.onClientException(this, new RuntimeException(e)); + } + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCreateReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCreateReq.java new file mode 100644 index 00000000..00f64f86 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCreateReq.java @@ -0,0 +1,15 @@ +package com.coze.openapi.service.service.websocket.audio.transcriptions; + +import com.coze.openapi.client.common.BaseReq; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WebsocketsAudioTranscriptionsCreateReq extends BaseReq { + @NonNull private WebsocketsAudioTranscriptionsCallbackHandler callbackHandler; +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCallbackHandler.java new file mode 100644 index 00000000..6f0273ed --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCallbackHandler.java @@ -0,0 +1,82 @@ +package com.coze.openapi.service.service.websocket.chat; + +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; + +public abstract class WebsocketsChatCallbackHandler + extends BaseCallbackHandler { + public WebsocketsChatCallbackHandler() {} + + // 对话连接成功事件 (chat.created) + public void onChatCreated(WebsocketsChatClient client, ChatCreatedEvent event) {} + + // 对话配置成功事件 (chat.updated) + public void onChatUpdated(WebsocketsChatClient client, ChatUpdatedEvent event) {} + + // 对话创建事件 (conversation.chat.created) + public void onConversationChatCreated( + WebsocketsChatClient client, ConversationChatCreatedEvent event) {} + + // 对话正在处理事件 (conversation.chat.in_progress) + public void onConversationChatInProgress( + WebsocketsChatClient client, ConversationChatInProgressEvent event) {} + + // 增量消息事件 (conversation.message.delta) + public void onConversationMessageDelta( + WebsocketsChatClient client, ConversationMessageDeltaEvent event) {} + + // 增量语音事件 (conversation.audio.delta) + public void onConversationAudioDelta( + WebsocketsChatClient client, ConversationAudioDeltaEvent event) {} + + // 消息完成事件 (conversation.message.completed) + public void onConversationMessageCompleted( + WebsocketsChatClient client, ConversationMessageCompletedEvent event) {} + + // 语音回复完成事件 (conversation.audio.completed) + public void onConversationAudioCompleted( + WebsocketsChatClient client, ConversationAudioCompletedEvent event) {} + + // 对话完成事件 (conversation.chat.completed) + public void onConversationChatCompleted( + WebsocketsChatClient client, ConversationChatCompletedEvent event) {} + + // 对话失败事件 (conversation.chat.failed) + public void onConversationChatFailed( + WebsocketsChatClient client, ConversationChatFailedEvent event) {} + + // 语音提交成功事件 (input_audio_buffer.completed) + public void onInputAudioBufferCompleted( + WebsocketsChatClient client, InputAudioBufferCompletedEvent event) {} + + // 语音清除成功事件 (input_audio_buffer.cleared) + public void onInputAudioBufferCleared( + WebsocketsChatClient client, InputAudioBufferClearedEvent event) {} + + // 对话清除事件 (conversation.cleared) + public void onConversationCleared(WebsocketsChatClient client, ConversationClearedEvent event) {} + + // 对话取消事件 (conversation.chat.canceled) + public void onConversationChatCanceled( + WebsocketsChatClient client, ConversationChatCanceledEvent event) {} + + // 语音转录更新事件 (conversation.audio_transcript.update) + public void onConversationAudioTranscriptUpdate( + WebsocketsChatClient client, ConversationAudioTranscriptUpdateEvent event) {} + + // 语音转录完成事件 (conversation.audio_transcript.completed) + public void onConversationAudioTranscriptCompleted( + WebsocketsChatClient client, ConversationAudioTranscriptCompletedEvent event) {} + + // 端插件事件 (conversation.chat.requires_action) + public void onConversationChatRequiresAction( + WebsocketsChatClient client, ConversationChatRequiresActionEvent event) {} + + // 服务端检测到开始说话事件(input_audio_buffer.speech_started) + public void onInputAudioBufferSpeechStarted( + WebsocketsChatClient client, InputAudioBufferSpeechStartedEvent event) {} + + // 服务端检测到停止说话事件(input_audio_buffer.speech_stopped) + public void onInputAudioBufferSpeechStopped( + WebsocketsChatClient client, InputAudioBufferSpeechStoppedEvent event) {} +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClient.java new file mode 100644 index 00000000..ed59b4dc --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClient.java @@ -0,0 +1,200 @@ +package com.coze.openapi.service.service.websocket.chat; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import com.coze.openapi.client.connversations.message.model.Message; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.ChatUpdateEventData; +import com.coze.openapi.client.websocket.event.upstream.*; +import com.coze.openapi.client.websocket.event.upstream.ChatUpdateEvent; +import com.coze.openapi.client.websocket.event.upstream.ConversationChatCancelEvent; +import com.coze.openapi.client.websocket.event.upstream.ConversationChatSubmitToolOutputsEvent; +import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; +import com.coze.openapi.service.service.websocket.common.BaseWebsocketsClient; +import com.coze.openapi.service.utils.Utils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketsChatClient extends BaseWebsocketsClient { + + private final ObjectMapper objectMapper = Utils.getMapper(); + + private final WebsocketsChatCallbackHandler handler; + + private static final String uri = "/v1/chat"; + + protected WebsocketsChatClient(OkHttpClient client, String wsHost, WebsocketsChatCreateReq req) { + super(client, buildUrl(wsHost, req), req.getCallbackHandler(), req); + this.handler = req.getCallbackHandler(); + } + + protected static String buildUrl(String wsHost, WebsocketsChatCreateReq req) { + return String.format("%s%s?bot_id=%s", wsHost, uri, req.getBotID()); + } + + public void chatUpdate(ChatUpdateEventData data) { + this.sendEvent(ChatUpdateEvent.builder().data(data).build()); + } + + public void conversationChatCancel() { + this.sendEvent(new ConversationChatCancelEvent()); + } + + public void conversationChatSubmitToolOutputs(ConversationChatSubmitToolOutputsEvent.Data data) { + this.sendEvent(ConversationChatSubmitToolOutputsEvent.builder().data(data).build()); + } + + public void conversationClear() { + this.sendEvent(new ConversationClearEvent()); + } + + public void conversationMessageCreate(Message data) { + this.sendEvent(ConversationMessageCreateEvent.builder().data(data).build()); + } + + public void inputAudioBufferAppend(String data) { + this.sendEvent( + InputAudioBufferAppendEvent.of( + Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)))); + } + + public void inputAudioBufferAppend(byte[] data) { + this.sendEvent(InputAudioBufferAppendEvent.of(Base64.getEncoder().encodeToString(data))); + } + + public void inputAudioBufferClear() { + this.sendEvent(new InputAudioBufferClearEvent()); + } + + public void inputAudioBufferComplete() { + this.sendEvent(new InputAudioBufferCompleteEvent()); + } + + @Override + protected BaseCallbackHandler getCallbackHandler() { + return handler; + } + + @Override + protected void handleEvent(WebSocket ws, String text) { + try { + // 解析 JSON + JsonNode jsonNode = objectMapper.readTree(text); + String eventType = parseEventType(jsonNode, text); + if (eventType == null) { + // 这里在上面已经抛出异常了,直接 return 即可 + return; + } + switch (eventType) { + case EventType.CHAT_CREATED: + ChatCreatedEvent chatCreatedEvent = + objectMapper.treeToValue(jsonNode, ChatCreatedEvent.class); + handler.onChatCreated(this, chatCreatedEvent); + break; + case EventType.CHAT_UPDATED: + ChatUpdatedEvent chatUpdatedEvent = + objectMapper.treeToValue(jsonNode, ChatUpdatedEvent.class); + handler.onChatUpdated(this, chatUpdatedEvent); + break; + case EventType.CONVERSATION_AUDIO_COMPLETED: + ConversationAudioCompletedEvent audioCompletedEvent = + objectMapper.treeToValue(jsonNode, ConversationAudioCompletedEvent.class); + handler.onConversationAudioCompleted(this, audioCompletedEvent); + break; + case EventType.CONVERSATION_AUDIO_DELTA: + ConversationAudioDeltaEvent audioDeltaEvent = + objectMapper.treeToValue(jsonNode, ConversationAudioDeltaEvent.class); + handler.onConversationAudioDelta(this, audioDeltaEvent); + break; + case EventType.CONVERSATION_AUDIO_TRANSCRIPT_COMPLETED: + ConversationAudioTranscriptCompletedEvent transcriptCompletedEvent = + objectMapper.treeToValue(jsonNode, ConversationAudioTranscriptCompletedEvent.class); + handler.onConversationAudioTranscriptCompleted(this, transcriptCompletedEvent); + break; + case EventType.CONVERSATION_AUDIO_TRANSCRIPT_UPDATE: + ConversationAudioTranscriptUpdateEvent transcriptUpdateEvent = + objectMapper.treeToValue(jsonNode, ConversationAudioTranscriptUpdateEvent.class); + handler.onConversationAudioTranscriptUpdate(this, transcriptUpdateEvent); + break; + case EventType.CONVERSATION_CHAT_CANCELED: + ConversationChatCanceledEvent chatCanceledEvent = + objectMapper.treeToValue(jsonNode, ConversationChatCanceledEvent.class); + handler.onConversationChatCanceled(this, chatCanceledEvent); + break; + case EventType.CONVERSATION_CHAT_COMPLETED: + ConversationChatCompletedEvent chatCompletedEvent = + objectMapper.treeToValue(jsonNode, ConversationChatCompletedEvent.class); + handler.onConversationChatCompleted(this, chatCompletedEvent); + break; + case EventType.CONVERSATION_CHAT_CREATED: + ConversationChatCreatedEvent conversationChatCreatedEvent = + objectMapper.treeToValue(jsonNode, ConversationChatCreatedEvent.class); + handler.onConversationChatCreated(this, conversationChatCreatedEvent); + break; + case EventType.CONVERSATION_CHAT_FAILED: + ConversationChatFailedEvent chatFailedEvent = + objectMapper.treeToValue(jsonNode, ConversationChatFailedEvent.class); + handler.onConversationChatFailed(this, chatFailedEvent); + break; + case EventType.CONVERSATION_CHAT_IN_PROGRESS: + ConversationChatInProgressEvent chatInProgressEvent = + objectMapper.treeToValue(jsonNode, ConversationChatInProgressEvent.class); + handler.onConversationChatInProgress(this, chatInProgressEvent); + break; + case EventType.CONVERSATION_CHAT_REQUIRES_ACTION: + ConversationChatRequiresActionEvent chatRequiresActionEvent = + objectMapper.treeToValue(jsonNode, ConversationChatRequiresActionEvent.class); + handler.onConversationChatRequiresAction(this, chatRequiresActionEvent); + break; + case EventType.CONVERSATION_CLEARED: + ConversationClearedEvent clearedEvent = + objectMapper.treeToValue(jsonNode, ConversationClearedEvent.class); + handler.onConversationCleared(this, clearedEvent); + break; + case EventType.CONVERSATION_MESSAGE_COMPLETED: + ConversationMessageCompletedEvent messageCompletedEvent = + objectMapper.treeToValue(jsonNode, ConversationMessageCompletedEvent.class); + handler.onConversationMessageCompleted(this, messageCompletedEvent); + break; + case EventType.CONVERSATION_MESSAGE_DELTA: + ConversationMessageDeltaEvent messageDeltaEvent = + objectMapper.treeToValue(jsonNode, ConversationMessageDeltaEvent.class); + handler.onConversationMessageDelta(this, messageDeltaEvent); + break; + case EventType.INPUT_AUDIO_BUFFER_CLEARED: + InputAudioBufferClearedEvent bufferClearedEvent = + objectMapper.treeToValue(jsonNode, InputAudioBufferClearedEvent.class); + handler.onInputAudioBufferCleared(this, bufferClearedEvent); + break; + case EventType.INPUT_AUDIO_BUFFER_COMPLETED: + InputAudioBufferCompletedEvent bufferCompletedEvent = + objectMapper.treeToValue(jsonNode, InputAudioBufferCompletedEvent.class); + handler.onInputAudioBufferCompleted(this, bufferCompletedEvent); + break; + case EventType.INPUT_AUDIO_BUFFER_SPEECH_STARTED: + InputAudioBufferSpeechStartedEvent speechStartedEvent = + objectMapper.treeToValue(jsonNode, InputAudioBufferSpeechStartedEvent.class); + handler.onInputAudioBufferSpeechStarted(this, speechStartedEvent); + break; + case EventType.INPUT_AUDIO_BUFFER_SPEECH_STOPPED: + InputAudioBufferSpeechStoppedEvent speechStoppedEvent = + objectMapper.treeToValue(jsonNode, InputAudioBufferSpeechStoppedEvent.class); + handler.onInputAudioBufferSpeechStopped(this, speechStoppedEvent); + break; + case EventType.ERROR: + ErrorEvent errorEvent = objectMapper.treeToValue(jsonNode, ErrorEvent.class); + handler.onError(this, errorEvent); + break; + default: + logger.error("unknown event type: {}, event string: {}", eventType, text); + } + } catch (Exception e) { + handler.onClientException(this, new RuntimeException(e)); + } + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientBuilder.java new file mode 100644 index 00000000..3b057b4b --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientBuilder.java @@ -0,0 +1,14 @@ +package com.coze.openapi.service.service.websocket.chat; + +import lombok.AllArgsConstructor; +import okhttp3.OkHttpClient; + +@AllArgsConstructor +public class WebsocketsChatClientBuilder { + private final String baseUrl; + private final OkHttpClient httpClient; + + public WebsocketsChatClient create(WebsocketsChatCreateReq req) { + return new WebsocketsChatClient(httpClient, baseUrl, req); + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCreateReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCreateReq.java new file mode 100644 index 00000000..2ea52083 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCreateReq.java @@ -0,0 +1,16 @@ +package com.coze.openapi.service.service.websocket.chat; + +import com.coze.openapi.client.common.BaseReq; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WebsocketsChatCreateReq extends BaseReq { + @NonNull private String botID; + @NonNull private WebsocketsChatCallbackHandler callbackHandler; +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseCallbackHandler.java new file mode 100644 index 00000000..2cf69f2c --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseCallbackHandler.java @@ -0,0 +1,23 @@ +package com.coze.openapi.service.service.websocket.common; + +import com.coze.openapi.client.websocket.event.downstream.ErrorEvent; + +public abstract class BaseCallbackHandler { + + public BaseCallbackHandler() {} + + // websocket closing + public void onClosing(T client, int code, String reason) {} + + // websocket closed + public void onClosed(T client, int code, String reason) {} + + // coze api exception + public void onError(T client, ErrorEvent event) {} + + // websocket connection failed + public void onFailure(T client, Throwable t) {} + + // sdk exception + public void onClientException(T client, Throwable t) {} +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClient.java new file mode 100644 index 00000000..5b7fbc69 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClient.java @@ -0,0 +1,101 @@ +package com.coze.openapi.service.service.websocket.common; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; + +import com.coze.openapi.client.common.BaseReq; +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.service.service.common.CozeLoggerFactory; +import com.coze.openapi.service.utils.Utils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.WebSocket; + +public abstract class BaseWebsocketsClient { + protected final ObjectMapper objectMapper = Utils.getMapper(); + protected static final Logger logger = CozeLoggerFactory.getLogger(); + protected final WebSocket ws; + protected final ExecutorService executorService; + protected static final int CLOSE_TIMEOUT_SECONDS = 10; + protected final BaseWebsocketsListener listener; + + protected BaseWebsocketsClient( + OkHttpClient client, String url, BaseCallbackHandler handler, BaseReq req) { + Request request = new Request.Builder().url(url).build(); + this.executorService = Executors.newSingleThreadExecutor(); + this.listener = new BaseWebsocketsListener(this::handleEvent, handler, this); + OkHttpClient.Builder builder = client.newBuilder(); + boolean hasSetTimeout = false; + if (req != null) { + if (req.getConnectTimeout() != null) { + hasSetTimeout = true; + builder.connectTimeout(req.getConnectTimeout(), TimeUnit.SECONDS); + } + if (req.getReadTimeout() != null) { + hasSetTimeout = true; + builder.readTimeout(req.getReadTimeout(), TimeUnit.SECONDS); + } + if (req.getWriteTimeout() != null) { + hasSetTimeout = true; + builder.writeTimeout(req.getWriteTimeout(), TimeUnit.SECONDS); + } + } + if (hasSetTimeout) { + this.ws = builder.build().newWebSocket(request, this.listener); + } else { + this.ws = client.newWebSocket(request, this.listener); + } + } + + protected void sendEvent(BaseEvent event) { + this.ws.send(Utils.toJson(event)); + } + + protected abstract void handleEvent(WebSocket ws, String text); + + protected abstract BaseCallbackHandler getCallbackHandler(); + + protected String parseEventType(JsonNode jsonNode, String text) { + JsonNode eventTypeNode = jsonNode.get("event_type"); + if (eventTypeNode == null) { + logger.error("Missing event_type field in event: {}", text); + getCallbackHandler() + .onClientException(this, new RuntimeException("Missing event_type field in event")); + return null; + } + return eventTypeNode.asText(); + } + + public void close() { + try { + // 先关闭 WebSocket 连接 + this.ws.close(1000, "Normal closure"); + + // 停止接收新任务并等待现有任务完成 + executorService.shutdown(); + + // 等待一段时间让任务完成 + if (!executorService.awaitTermination(CLOSE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + // 如果超时,强制关闭 + executorService.shutdownNow(); + // 再次等待,让任务响应中断 + if (!executorService.awaitTermination(CLOSE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + System.err.println("ExecutorService did not terminate"); + } + } + } catch (InterruptedException e) { + // 恢复中断状态 + Thread.currentThread().interrupt(); + // 强制关闭 + executorService.shutdownNow(); + } finally { + this.listener.shutdown(); + } + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListener.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListener.java new file mode 100644 index 00000000..4b60aa7c --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListener.java @@ -0,0 +1,62 @@ +package com.coze.openapi.service.service.websocket.common; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.BiConsumer; + +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class BaseWebsocketsListener extends WebSocketListener { + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final BiConsumer handleEvent; + private final BaseCallbackHandler callbackHandler; + private final BaseWebsocketsClient client; + + public BaseWebsocketsListener( + BiConsumer handleEvent, + BaseCallbackHandler handler, + BaseWebsocketsClient client) { + this.handleEvent = handleEvent; + this.callbackHandler = handler; + this.client = client; + } + + @Override + public void onOpen(WebSocket webSocket, okhttp3.Response response) { + System.out.println("onOpen"); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + executorService.submit(() -> this.handleEvent.accept(webSocket, text)); + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + executorService.submit(() -> this.handleEvent.accept(webSocket, bytes.utf8())); + } + + @Override + public void onClosing(WebSocket webSocket, int code, String reason) { + this.callbackHandler.onClosing(this.client, code, reason); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response response) { + this.callbackHandler.onFailure( + this.client, new RuntimeException("websocket connection error: " + t.getMessage(), t)); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + this.callbackHandler.onClosed(this.client, code, reason); + } + + public void shutdown() { + if (!executorService.isShutdown()) { + executorService.shutdown(); + } + } +} diff --git a/api/src/main/java/com/coze/openapi/service/utils/UserAgentInterceptor.java b/api/src/main/java/com/coze/openapi/service/utils/UserAgentInterceptor.java index 4dd7ccc9..44d8c2f5 100644 --- a/api/src/main/java/com/coze/openapi/service/utils/UserAgentInterceptor.java +++ b/api/src/main/java/com/coze/openapi/service/utils/UserAgentInterceptor.java @@ -24,7 +24,7 @@ public Response intercept(Chain chain) throws IOException { return chain.proceed(request); } - public static final String VERSION = "0.2.8"; + public static final String VERSION = "0.2.9"; private static final ObjectMapper objectMapper = new ObjectMapper(); /** 获取操作系统版本 */ diff --git a/api/src/main/java/com/coze/openapi/service/utils/Utils.java b/api/src/main/java/com/coze/openapi/service/utils/Utils.java index 694ddef1..1e0a52b8 100644 --- a/api/src/main/java/com/coze/openapi/service/utils/Utils.java +++ b/api/src/main/java/com/coze/openapi/service/utils/Utils.java @@ -15,7 +15,21 @@ public class Utils { public static final String LOG_HEADER = "x-tt-logid"; - private static final ObjectMapper mapper = defaultObjectMapper(); + + private static ObjectMapper defaultObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper; + } + + private static final class MapperHolder { + static final ObjectMapper mapper = defaultObjectMapper(); + } + + public static ObjectMapper getMapper() { + return MapperHolder.mapper; + } public static T execute(Call call) { try { @@ -45,20 +59,13 @@ public static T execute(Call call) { } } - public static ObjectMapper defaultObjectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - return mapper; - } - public static String getLogID(Response response) { return response.raw().headers().get(LOG_HEADER); } public static String toJson(Object obj) { try { - return mapper.writeValueAsString(obj); + return getMapper().writeValueAsString(obj); } catch (JsonProcessingException e) { throw new RuntimeException("Failed to convert object to JSON string", e); } @@ -66,7 +73,7 @@ public static String toJson(Object obj) { public static T fromJson(String json, Class clazz) { try { - return mapper.readValue(json, clazz); + return getMapper().readValue(json, clazz); } catch (JsonProcessingException e) { throw new RuntimeException("Failed to parse object from JSON string", e); } diff --git a/api/src/test/java/com/coze/openapi/service/auth/DeviceOAuthClientTest.java b/api/src/test/java/com/coze/openapi/service/auth/DeviceOAuthClientTest.java index 296abace..35564e34 100644 --- a/api/src/test/java/com/coze/openapi/service/auth/DeviceOAuthClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/auth/DeviceOAuthClientTest.java @@ -17,8 +17,10 @@ import io.reactivex.Single; import okhttp3.Headers; +import okhttp3.MediaType; import okhttp3.Protocol; import okhttp3.Request; +import okhttp3.ResponseBody; import retrofit2.Response; class DeviceOAuthClientTest { @@ -129,15 +131,15 @@ void testGetAccessTokenWithPolling() throws Exception { Response pendingResponse = Response.error( 400, - okhttp3.ResponseBody.create( - "{\"error_code\":\"authorization_pending\",\"error_message\":\"Authorization pending\"}", - okhttp3.MediaType.get("application/json"))); + ResponseBody.create( + MediaType.parse("application/json"), + "{\"error_code\":\"authorization_pending\",\"error_message\":\"Authorization pending\"}")); Response slowDownResponse = Response.error( 400, - okhttp3.ResponseBody.create( - "{\"error_code\":\"slow_down\",\"error_message\":\"Slow Down\"}", - okhttp3.MediaType.get("application/json"))); + ResponseBody.create( + MediaType.parse("application/json"), + "{\"error_code\":\"slow_down\",\"error_message\":\"Slow Down\"}")); // 设置模拟行为 - 第一次返回 pending,第二次返回 slow down,第三次成功 when(mockApi.retrieve(any(), any())) @@ -162,8 +164,8 @@ void testGetAccessTokenWithError() { Response.error( 400, okhttp3.ResponseBody.create( - "{\"error_code\":\"invalid_grant\",\"error_message\":\"Invalid grant\"}", - okhttp3.MediaType.get("application/json"))); + MediaType.parse("application/json"), + "{\"error_code\":\"invalid_grant\",\"error_message\":\"Invalid grant\"}")); // 设置模拟行为 when(mockApi.retrieve(anyMap(), any(GetAccessTokenReq.class))) diff --git a/api/src/test/java/com/coze/openapi/service/auth/JWTOAuthTest.java b/api/src/test/java/com/coze/openapi/service/auth/JWTOAuthTest.java new file mode 100644 index 00000000..033cfe3d --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/auth/JWTOAuthTest.java @@ -0,0 +1,119 @@ +package com.coze.openapi.service.auth; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.coze.openapi.client.auth.OAuthToken; + +@ExtendWith(MockitoExtension.class) +class JWTOAuthTest { + + @Mock private JWTOAuthClient jwtClient; + + @Mock private OAuthToken oAuthToken; + + private JWTOAuth jwtOAuth; + + @Test + void testConstructorWithNullClient() { + assertThrows(NullPointerException.class, () -> new JWTOAuth(null)); + } + + @Test + void testConstructorWithValidClient() { + when(jwtClient.getTtl()).thenReturn(600); + jwtOAuth = new JWTOAuth(jwtClient); + assertNotNull(jwtOAuth); + } + + @Test + void testTokenFirstTime() { + when(jwtClient.getTtl()).thenReturn(600); + when(oAuthToken.getAccessToken()).thenReturn("test-token"); + when(jwtClient.getAccessToken((Integer) any(), any(), any())).thenReturn(oAuthToken); + + jwtOAuth = new JWTOAuth(jwtClient); + String token = jwtOAuth.token(); + + assertEquals("test-token", token); + verify(jwtClient, times(1)).getAccessToken((Integer) any(), any(), any()); + } + + @Test + void testTokenCaching() { + when(jwtClient.getTtl()).thenReturn(600); + when(oAuthToken.getAccessToken()).thenReturn("test-token"); + when(oAuthToken.getExpiresIn()).thenReturn(600); + when(jwtClient.getAccessToken((Integer) any(), any(), any())).thenReturn(oAuthToken); + + jwtOAuth = new JWTOAuth(jwtClient); + + String token1 = jwtOAuth.token(); + String token2 = jwtOAuth.token(); + + assertEquals(token1, token2); + verify(jwtClient, times(2)).getAccessToken((Integer) any(), any(), any()); + } + + @Test + void testTokenWithCustomParameters() { + Integer ttl = 300; + String sessionName = "test-session"; + + when(oAuthToken.getAccessToken()).thenReturn("test-token"); + when(jwtClient.getAccessToken(eq(ttl), any(), eq(sessionName))).thenReturn(oAuthToken); + + jwtOAuth = JWTOAuth.builder().jwtClient(jwtClient).ttl(ttl).sessionName(sessionName).build(); + + String token = jwtOAuth.token(); + + assertEquals("test-token", token); + verify(jwtClient).getAccessToken(ttl, null, sessionName); + } + + @Test + void testTokenRefresh() throws InterruptedException { + when(oAuthToken.getAccessToken()).thenReturn("test-token"); + when(oAuthToken.getExpiresIn()).thenReturn(2); + when(jwtClient.getAccessToken((Integer) any(), any(), any())).thenReturn(oAuthToken); + + jwtOAuth = JWTOAuth.builder().jwtClient(jwtClient).ttl(2).build(); + + String token1 = jwtOAuth.token(); + Thread.sleep(3000); + String token2 = jwtOAuth.token(); + + assertEquals("test-token", token2); + verify(jwtClient, times(2)).getAccessToken((Integer) any(), any(), any()); + } + + @Test + void testGetRefreshBeforeWithDifferentTtls() { + when(oAuthToken.getAccessToken()).thenReturn("test-token"); + + // Test ttl >= 600 + jwtOAuth = JWTOAuth.builder().jwtClient(jwtClient).ttl(600).build(); + when(jwtClient.getAccessToken((Integer) any(), any(), any())).thenReturn(oAuthToken); + jwtOAuth.token(); + verify(jwtClient).getAccessToken(eq(600), any(), any()); + + // Test 60 <= ttl < 600 + jwtOAuth = JWTOAuth.builder().jwtClient(jwtClient).ttl(100).build(); + when(jwtClient.getAccessToken((Integer) any(), any(), any())).thenReturn(oAuthToken); + jwtOAuth.token(); + verify(jwtClient).getAccessToken(eq(100), any(), any()); + + // Test 30 <= ttl < 60 + jwtOAuth = JWTOAuth.builder().jwtClient(jwtClient).ttl(30).build(); + when(jwtClient.getAccessToken((Integer) any(), any(), any())).thenReturn(oAuthToken); + jwtOAuth.token(); + verify(jwtClient).getAccessToken(eq(30), any(), any()); + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/APIResponseCallAdapterTest.java b/api/src/test/java/com/coze/openapi/service/service/APIResponseCallAdapterTest.java index f9978978..82ef1a2c 100644 --- a/api/src/test/java/com/coze/openapi/service/service/APIResponseCallAdapterTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/APIResponseCallAdapterTest.java @@ -74,7 +74,7 @@ void testErrorResponse() { // 准备错误响应 String errorJson = "{\"code\":400,\"msg\":\"Bad Request\"}"; ResponseBody errorBody = - ResponseBody.create(errorJson, MediaType.parse("application/json")); + ResponseBody.create(MediaType.parse("application/json"), errorJson); Request request = new Request.Builder().url("https://api.test.com").build(); diff --git a/api/src/test/java/com/coze/openapi/service/service/audio/SpeechServiceTest.java b/api/src/test/java/com/coze/openapi/service/service/audio/SpeechServiceTest.java index c1585a68..1a01c329 100644 --- a/api/src/test/java/com/coze/openapi/service/service/audio/SpeechServiceTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/audio/SpeechServiceTest.java @@ -39,7 +39,7 @@ void testCreate() throws Exception { CreateSpeechReq.builder().input("Test speech").voiceID("mock voice id").build(); ResponseBody responseBody = - ResponseBody.create(new byte[] {1, 2, 3, 4}, MediaType.parse("audio/mpeg")); + ResponseBody.create(MediaType.parse("audio/mpeg"), new byte[] {1, 2, 3, 4}); // 设置 mock 行为 when(audioSpeechAPI.create(any(CreateSpeechReq.class), any(CreateSpeechReq.class))) diff --git a/api/src/test/java/com/coze/openapi/service/service/audio/TranscriptionServiceTest.java b/api/src/test/java/com/coze/openapi/service/service/audio/TranscriptionServiceTest.java new file mode 100644 index 00000000..68c1331d --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/audio/TranscriptionServiceTest.java @@ -0,0 +1,129 @@ +package com.coze.openapi.service.service.audio; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.nio.file.Files; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.coze.openapi.api.AudioTranscriptionAPI; +import com.coze.openapi.client.audio.transcriptions.CreateTranscriptionsReq; +import com.coze.openapi.client.audio.transcriptions.CreateTranscriptionsResp; +import com.coze.openapi.client.common.BaseResponse; + +import okhttp3.MultipartBody; +import retrofit2.Call; +import retrofit2.Response; + +class TranscriptionServiceTest { + + @Mock private AudioTranscriptionAPI transcriptionAPI; + @Mock private Call> mockCall; + + private TranscriptionService transcriptionService; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + transcriptionService = new TranscriptionService(transcriptionAPI); + } + + @Test + void testCreateWithFilePath(@TempDir File tempDir) throws Exception { + // 准备测试文件 + File testFile = new File(tempDir, "test-audio.wav"); + Files.write(testFile.toPath(), "test audio content".getBytes()); + + // 准备请求和预期响应 + CreateTranscriptionsReq req = + CreateTranscriptionsReq.builder().filePath(testFile.getAbsolutePath()).build(); + + CreateTranscriptionsResp expectedResp = + CreateTranscriptionsResp.builder().text("测试转录文本").build(); + + BaseResponse baseResponse = new BaseResponse<>(); + baseResponse.setData(expectedResp); + + // 配置 mock + when(transcriptionAPI.create(any(MultipartBody.Part.class), eq(req))).thenReturn(mockCall); + when(mockCall.execute()).thenReturn(Response.success(baseResponse)); + + // 执行测试 + CreateTranscriptionsResp resp = transcriptionService.create(req); + + // 验证结果 + assertEquals("测试转录文本", resp.getText()); + verify(transcriptionAPI).create(any(MultipartBody.Part.class), eq(req)); + } + + @Test + void testCreateWithFileBytes() throws Exception { + // 准备测试数据 + byte[] fileBytes = "test audio content".getBytes(); + String fileName = "test-audio.wav"; + + CreateTranscriptionsReq req = + CreateTranscriptionsReq.builder().fileBytes(fileBytes).fileName(fileName).build(); + + CreateTranscriptionsResp expectedResp = + CreateTranscriptionsResp.builder().text("测试转录文本").build(); + + BaseResponse baseResponse = new BaseResponse<>(); + baseResponse.setData(expectedResp); + + // 配置 mock + when(transcriptionAPI.create(any(MultipartBody.Part.class), eq(req))).thenReturn(mockCall); + when(mockCall.execute()).thenReturn(Response.success(baseResponse)); + + // 执行测试 + CreateTranscriptionsResp resp = transcriptionService.create(req); + + // 验证结果 + assertEquals("测试转录文本", resp.getText()); + verify(transcriptionAPI).create(any(MultipartBody.Part.class), eq(req)); + } + + @Test + void testCreateWithFile(@TempDir File tempDir) throws Exception { + // 准备测试文件 + File testFile = new File(tempDir, "test-audio.wav"); + Files.write(testFile.toPath(), "test audio content".getBytes()); + + CreateTranscriptionsReq req = + CreateTranscriptionsReq.builder().file(testFile).fileName(testFile.getName()).build(); + + CreateTranscriptionsResp expectedResp = + CreateTranscriptionsResp.builder().text("测试转录文本").build(); + + BaseResponse baseResponse = new BaseResponse<>(); + baseResponse.setData(expectedResp); + + // 配置 mock + when(transcriptionAPI.create(any(MultipartBody.Part.class), eq(req))).thenReturn(mockCall); + when(mockCall.execute()).thenReturn(Response.success(baseResponse)); + + // 执行测试 + CreateTranscriptionsResp resp = transcriptionService.create(req); + + // 验证结果 + assertEquals("测试转录文本", resp.getText()); + verify(transcriptionAPI).create(any(MultipartBody.Part.class), eq(req)); + } + + @Test + void testCreateWithNoFileSource() { + CreateTranscriptionsReq req = CreateTranscriptionsReq.builder().build(); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> transcriptionService.create(req)); + + assertEquals("file source is required", exception.getMessage()); + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClientTest.java new file mode 100644 index 00000000..d30ccddb --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClientTest.java @@ -0,0 +1,268 @@ +package com.coze.openapi.service.service.websocket.audio.speech; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.OutputAudio; +import com.coze.openapi.client.websocket.event.model.PCMConfig; +import com.coze.openapi.client.websocket.event.model.SpeechUpdateEventData; + +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketAudioSpeechClientTest { + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private WebsocketsAudioSpeechCallbackHandler mockCallbackHandler; + + @Captor private ArgumentCaptor speechCreatedEventCaptor; + @Captor private ArgumentCaptor speechUpdatedEventCaptor; + @Captor private ArgumentCaptor speechAudioUpdateEventCaptor; + @Captor private ArgumentCaptor speechAudioCompletedEventCaptor; + @Captor private ArgumentCaptor inputTextBufferCompletedEventCaptor; + @Captor private ArgumentCaptor errorEventCaptor; + + private WebsocketsAudioSpeechClient client; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); + + WebsocketsAudioSpeechCreateReq req = + WebsocketsAudioSpeechCreateReq.builder().callbackHandler(mockCallbackHandler).build(); + client = new WebsocketsAudioSpeechClient(mockOkHttpClient, "ws://test.com", req); + } + + @Test + public void testHandleSpeechCreatedEvent() { + String json = + "{\n" + + " \"id\": \"7446668538246561xxxx\",\n" + + " \"event_type\": \"speech.created\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler).onSpeechCreated(eq(client), speechCreatedEventCaptor.capture()); + + SpeechCreatedEvent event = speechCreatedEventCaptor.getValue(); + assertEquals(EventType.SPEECH_CREATED, event.getEventType()); + assertEquals("7446668538246561xxxx", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); + } + + @Test + public void testHandleSpeechUpdatedEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"speech.updated\",\n" + + " \"data\": {\n" + + " \"output_audio\": {\n" + + " \"codec\": \"pcm\",\n" + + " \"pcm_config\": {\n" + + " \"sample_rate\": 24000\n" + + " },\n" + + " \"speech_rate\": 50,\n" + + " \"voice_id\": \"音色id\"\n" + + " }\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\" }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler).onSpeechUpdated(eq(client), speechUpdatedEventCaptor.capture()); + + SpeechUpdatedEvent event = speechUpdatedEventCaptor.getValue(); + assertEquals(EventType.SPEECH_UPDATED, event.getEventType()); + assertEquals("event_id", event.getId()); + + // 验证 data + assertEquals("pcm", event.getData().getOutputAudio().getCodec()); + assertEquals(24000, event.getData().getOutputAudio().getPcmConfig().getSampleRate()); + assertEquals(50, event.getData().getOutputAudio().getSpeechRate()); + assertEquals("音色id", event.getData().getOutputAudio().getVoiceId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); + } + + @Test + public void testHandleSpeechAudioUpdateEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"speech.audio.update\",\n" + + " \"data\": {\n" + + " \"delta\": \"base64EncodedAudioDelta\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\" }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onSpeechAudioUpdate(eq(client), speechAudioUpdateEventCaptor.capture()); + + SpeechAudioUpdateEvent event = speechAudioUpdateEventCaptor.getValue(); + assertEquals(EventType.SPEECH_AUDIO_UPDATE, event.getEventType()); + assertEquals("event_id", event.getId()); + + // 验证 data + assertEquals("base64EncodedAudioDelta", event.getData().getDelta()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); + } + + @Test + public void testHandleSpeechAudioCompletedEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"speech.audio.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onSpeechAudioCompleted(eq(client), speechAudioCompletedEventCaptor.capture()); + + SpeechAudioCompletedEvent event = speechAudioCompletedEventCaptor.getValue(); + assertEquals(EventType.SPEECH_AUDIO_COMPLETED, event.getEventType()); + assertEquals("event_id", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); + } + + @Test + public void testHandleInputTextBufferCompletedEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"input_text_buffer.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onInputTextBufferCompleted(eq(client), inputTextBufferCompletedEventCaptor.capture()); + + InputTextBufferCompletedEvent event = inputTextBufferCompletedEventCaptor.getValue(); + assertEquals(EventType.INPUT_TEXT_BUFFER_COMPLETED, event.getEventType()); + assertEquals("event_id", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); + } + + @Test + public void testHandleErrorEvent() { + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"error\",\n" + + " \"data\": {\n" + + " \"code\": 123,\n" + + " \"msg\": \"error message\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler).onError(eq(client), errorEventCaptor.capture()); + + ErrorEvent event = errorEventCaptor.getValue(); + assertEquals(EventType.ERROR, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals(123, event.getData().getCode()); + assertEquals("error message", event.getData().getMsg()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); + } + + @Test + public void testHandleUnknownEvent() { + String json = "{\"event_type\":\"unknown\"}"; + + client.handleEvent(mockWebSocket, json); + + verifyNoInteractions(mockCallbackHandler); + } + + @Test + public void testHandleInvalidJson() { + String invalidJson = "invalid json"; + + client.handleEvent(mockWebSocket, invalidJson); + + verify(mockCallbackHandler).onClientException(eq(client), any(RuntimeException.class)); + } + + @Test + void testSpeechUpdate() { + SpeechUpdateEventData data = + SpeechUpdateEventData.builder() + .outputAudio( + OutputAudio.builder() + .codec("pcm") + .pcmConfig(PCMConfig.builder().sampleRate(24000).build()) + .speechRate(50) + .voiceId("test-voice-id") + .build()) + .build(); + + client.speechUpdate(data); + + verify(mockWebSocket).send(anyString()); // 验证发送了消息 + } + + @Test + void testInputTextBufferAppend() { + String textData = "测试文本内容"; + + client.inputTextBufferAppend(textData); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testInputTextBufferComplete() { + client.inputTextBufferComplete(); + + verify(mockWebSocket).send(anyString()); + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClientTest.java new file mode 100644 index 00000000..b9b8b6df --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClientTest.java @@ -0,0 +1,326 @@ +package com.coze.openapi.service.service.websocket.audio.transcriptions; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.InputAudio; +import com.coze.openapi.client.websocket.event.model.TranscriptionsUpdateEventData; + +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketAudioTranscriptionsClientTest { + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private WebsocketsAudioTranscriptionsCallbackHandler mockCallbackHandler; + + @Captor private ArgumentCaptor transcriptionsCreatedEventCaptor; + @Captor private ArgumentCaptor transcriptionsUpdatedEventCaptor; + + @Captor + private ArgumentCaptor transcriptionsMessageUpdateEventCaptor; + + @Captor + private ArgumentCaptor + transcriptionsMessageCompletedEventCaptor; + + @Captor private ArgumentCaptor inputAudioBufferClearedEventCaptor; + + @Captor + private ArgumentCaptor inputAudioBufferCompletedEventCaptor; + + @Captor private ArgumentCaptor errorEventCaptor; + + private WebsocketsAudioTranscriptionsClient client; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); + + WebsocketsAudioTranscriptionsCreateReq req = + WebsocketsAudioTranscriptionsCreateReq.builder() + .callbackHandler(mockCallbackHandler) + .build(); + client = new WebsocketsAudioTranscriptionsClient(mockOkHttpClient, "ws://test.com", req); + } + + @Test + public void testHandleTranscriptionsCreatedEvent() { + String json = + "{\n" + + " \"id\": \"7446668538246561xxxx\",\n" + + " \"event_type\": \"transcriptions.created\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onTranscriptionsCreated(eq(client), transcriptionsCreatedEventCaptor.capture()); + + TranscriptionsCreatedEvent event = transcriptionsCreatedEventCaptor.getValue(); + assertEquals(EventType.TRANSCRIPTIONS_CREATED, event.getEventType()); + assertEquals("7446668538246561xxxx", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleTranscriptionsUpdatedEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"transcriptions.updated\",\n" + + " \"data\": {\n" + + " \"input_audio\": {\n" + + " \"format\": \"pcm\",\n" + + " \"codec\": \"pcm\",\n" + + " \"sample_rate\": 24000,\n" + + " \"channel\": 1,\n" + + " \"bit_depth\": 16\n" + + " }\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onTranscriptionsUpdated(eq(client), transcriptionsUpdatedEventCaptor.capture()); + + TranscriptionsUpdatedEvent event = transcriptionsUpdatedEventCaptor.getValue(); + assertEquals(EventType.TRANSCRIPTIONS_UPDATED, event.getEventType()); + assertEquals("event_id", event.getId()); + + // 验证 data + assertEquals("pcm", event.getData().getInputAudio().getFormat()); + assertEquals("pcm", event.getData().getInputAudio().getCodec()); + assertEquals(24000, event.getData().getInputAudio().getSampleRate()); + assertEquals(1, event.getData().getInputAudio().getChannel()); + assertEquals(16, event.getData().getInputAudio().getBitDepth()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleTranscriptionsMessageUpdateEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"transcriptions.message.update\",\n" + + " \"data\": {\n" + + " \"content\": \"text\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onTranscriptionsMessageUpdate( + eq(client), transcriptionsMessageUpdateEventCaptor.capture()); + + TranscriptionsMessageUpdateEvent event = transcriptionsMessageUpdateEventCaptor.getValue(); + assertEquals(EventType.TRANSCRIPTIONS_MESSAGE_UPDATE, event.getEventType()); + assertEquals("event_id", event.getId()); + + // 验证 data + assertEquals("text", event.getData().getContent()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleTranscriptionsMessageCompletedEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"transcriptions.message.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onTranscriptionsMessageCompleted( + eq(client), transcriptionsMessageCompletedEventCaptor.capture()); + + TranscriptionsMessageCompletedEvent event = + transcriptionsMessageCompletedEventCaptor.getValue(); + assertEquals(EventType.TRANSCRIPTIONS_MESSAGE_COMPLETED, event.getEventType()); + assertEquals("event_id", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleInputAudioBufferClearedEvent() { + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"input_audio_buffer.cleared\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onInputAudioBufferCleared(eq(client), inputAudioBufferClearedEventCaptor.capture()); + + InputAudioBufferClearedEvent event = inputAudioBufferClearedEventCaptor.getValue(); + assertEquals(EventType.INPUT_AUDIO_BUFFER_CLEARED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleInputAudioBufferCompletedEvent() { + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"input_audio_buffer.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onInputAudioBufferCompleted(eq(client), inputAudioBufferCompletedEventCaptor.capture()); + + InputAudioBufferCompletedEvent event = inputAudioBufferCompletedEventCaptor.getValue(); + assertEquals(EventType.INPUT_AUDIO_BUFFER_COMPLETED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleErrorEvent() { + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"error\",\n" + + " \"data\": {\n" + + " \"code\": 123,\n" + + " \"msg\": \"error message\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler).onError(eq(client), errorEventCaptor.capture()); + + ErrorEvent event = errorEventCaptor.getValue(); + assertEquals(EventType.ERROR, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals(123, event.getData().getCode()); + assertEquals("error message", event.getData().getMsg()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleUnknownEvent() { + String json = "{\"event_type\":\"unknown\"}"; + + client.handleEvent(mockWebSocket, json); + + verifyNoInteractions(mockCallbackHandler); + } + + @Test + public void testHandleInvalidJson() { + String invalidJson = "invalid json"; + + client.handleEvent(mockWebSocket, invalidJson); + + verify(mockCallbackHandler).onClientException(eq(client), any(RuntimeException.class)); + } + + @Test + void testTranscriptionsUpdate() { + TranscriptionsUpdateEventData data = + TranscriptionsUpdateEventData.builder() + .inputAudio( + InputAudio.builder() + .format("pcm") + .codec("pcm") + .sampleRate(24000) + .channel(1) + .bitDepth(16) + .build()) + .build(); + + client.transcriptionsUpdate(data); + + verify(mockWebSocket).send(anyString()); // 验证发送了消息 + } + + @Test + void testInputAudioBufferAppendWithString() { + String audioData = "base64EncodedAudioData"; + + client.inputAudioBufferAppend(audioData); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testInputAudioBufferAppendWithData() { + client.inputAudioBufferAppend("data"); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testInputAudioBufferClear() { + client.inputAudioBufferClear(); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testInputAudioBufferComplete() { + client.inputAudioBufferComplete(); + + verify(mockWebSocket).send(anyString()); + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientTest.java new file mode 100644 index 00000000..efccb96c --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientTest.java @@ -0,0 +1,895 @@ +package com.coze.openapi.service.service.websocket.chat; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.coze.openapi.client.chat.model.ChatToolCall; +import com.coze.openapi.client.connversations.message.model.Message; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.ChatUpdateEventData; +import com.coze.openapi.client.websocket.event.upstream.ConversationChatSubmitToolOutputsEvent; + +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketsChatClientTest { + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private WebsocketsChatCallbackHandler mockCallbackHandler; + + @Captor private ArgumentCaptor chatCreatedEventCaptor; + @Captor private ArgumentCaptor messageDeltaEventCaptor; + @Captor private ArgumentCaptor requiresActionEventCaptor; + @Captor private ArgumentCaptor chatUpdatedEventCaptor; + @Captor private ArgumentCaptor audioCompletedEventCaptor; + @Captor private ArgumentCaptor audioDeltaEventCaptor; + @Captor private ArgumentCaptor conversationMessageDeltaEventCaptor; + + @Captor + private ArgumentCaptor audioTranscriptUpdateEventCaptor; + + @Captor private ArgumentCaptor inputAudioBufferClearedEventCaptor; + + @Captor + private ArgumentCaptor inputAudioBufferCompletedEventCaptor; + + @Captor private ArgumentCaptor errorEventCaptor; + + @Captor + private ArgumentCaptor + audioTranscriptCompletedEventCaptor; + + @Captor private ArgumentCaptor chatCompletedEventCaptor; + @Captor private ArgumentCaptor conversationChatCreatedEventCaptor; + @Captor private ArgumentCaptor chatFailedEventCaptor; + @Captor private ArgumentCaptor chatInProgressEventCaptor; + @Captor private ArgumentCaptor messageCompletedEventCaptor; + // ... 其他事件的 Captor + + private WebsocketsChatClient client; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); + + WebsocketsChatCreateReq req = + WebsocketsChatCreateReq.builder() + .botID("test-bot-id") + .callbackHandler(mockCallbackHandler) + .build(); + + client = new WebsocketsChatClient(mockOkHttpClient, "ws://test.com", req); + } + + @Test + public void testHandleChatCreatedEvent() { + // event_type: chat.created + String json = + "{\n" + + " \"id\": \"7446668538246561xxxx\",\n" + + " \"event_type\": \"chat.created\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\" }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler).onChatCreated(eq(client), chatCreatedEventCaptor.capture()); + + ChatCreatedEvent event = chatCreatedEventCaptor.getValue(); + assertEquals(EventType.CHAT_CREATED, event.getEventType()); + } + + @Test + public void testHandleChatUpdatedEvent() { + // event_type: chat.updated + String json = + "{\n" + + " \"id\":\"event_id\",\n" + + " \"event_type\":\"chat.updated\",\n" + + " \"data\":{\n" + + " \"chat_config\":{\n" + + " \"auto_save_history\":true,\n" + + " \"conversation_id\":\"xxxx\",\n" + + " \"user_id\":\"xxx\",\n" + + " \"meta_data\":{ },\n" + + " \"custom_variables\":{ },\n" + + " \"extra_params\":{ },\n" + + " \"parameters\":{\n" + + " \"custom_var_1\":\"测试\"\n" + + " }\n" + + " },\n" + + " \"input_audio\":{\n" + + " \"format\":\"pcm\",\n" + + " \"codec\":\"pcm\",\n" + + " \"sample_rate\":24000,\n" + + " \"channel\":1,\n" + + " \"bit_depth\":16\n" + + " },\n" + + " \"output_audio\":{\n" + + " \"codec\":\"opus\",\n" + + " \"opus_config\":{\n" + + " \"bitrate\":48000,\n" + + " \"use_cbr\":false,\n" + + " \"frame_size_ms\":10,\n" + + " \"limit_config\":{\n" + + " \"period\":2,\n" + + " \"max_frame_num\":300\n" + + " }\n" + + " },\n" + + " \"speech_rate\":50,\n" + + " \"voice_id\":\"74466752759302*****\"\n" + + " }\n" + + " },\n" + + " \"detail\":{\n" + + " \"logid\":\"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler).onChatUpdated(eq(client), chatUpdatedEventCaptor.capture()); + + ChatUpdatedEvent event = chatUpdatedEventCaptor.getValue(); + assertEquals(EventType.CHAT_UPDATED, event.getEventType()); + assertEquals("event_id", event.getId()); + + // 验证 chat_config + assertEquals(true, event.getData().getChatConfig().getAutoSaveHistory()); + assertEquals("xxxx", event.getData().getChatConfig().getConversationId()); + assertEquals("xxx", event.getData().getChatConfig().getUserId()); + + // 验证 input_audio + assertEquals("pcm", event.getData().getInputAudio().getFormat()); + assertEquals("pcm", event.getData().getInputAudio().getCodec()); + assertEquals(24000, event.getData().getInputAudio().getSampleRate()); + assertEquals(1, event.getData().getInputAudio().getChannel()); + assertEquals(16, event.getData().getInputAudio().getBitDepth()); + + // 验证 output_audio + assertEquals("opus", event.getData().getOutputAudio().getCodec()); + assertEquals(48000, event.getData().getOutputAudio().getOpusConfig().getBitrate()); + assertEquals(false, event.getData().getOutputAudio().getOpusConfig().getUseCbr()); + assertEquals(10, event.getData().getOutputAudio().getOpusConfig().getFrameSizeMs()); + assertEquals(2, event.getData().getOutputAudio().getOpusConfig().getLimitConfig().getPeriod()); + assertEquals( + 300, event.getData().getOutputAudio().getOpusConfig().getLimitConfig().getMaxFrameNum()); + assertEquals(50, event.getData().getOutputAudio().getSpeechRate()); + assertEquals("74466752759302*****", event.getData().getOutputAudio().getVoiceId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationAudioCompletedEvent() { + // event_type: conversation.audio.completed + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"conversation.audio.completed\",\n" + + " \"data\": {\n" + + " \"id\": \"msg_002\",\n" + + " \"role\": \"assistant\",\n" + + " \"type\": \"function_call\",\n" + + " \"content\": \"{\\\"name\\\":\\\"toutiaosousuo-search\\\",\\\"arguments\\\":{\\\"cursor\\\":0,\\\"input_query\\\":\\\"今天的体育新闻\\\",\\\"plugin_id\\\":7281192623887548473,\\\"api_id\\\":7288907006982012986,\\\"plugin_type\\\":1}}\",\n" + + " \"content_type\": \"audio\",\n" + + " \"chat_id\": \"123\",\n" + + " \"conversation_id\": \"123\",\n" + + " \"bot_id\": \"222\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationAudioCompleted(eq(client), audioCompletedEventCaptor.capture()); + + ConversationAudioCompletedEvent event = audioCompletedEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_AUDIO_COMPLETED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals("msg_002", event.getData().getId()); + assertEquals("assistant", event.getData().getRole().getValue()); + assertEquals("function_call", event.getData().getType().getValue()); + assertEquals("audio", event.getData().getContentType().getValue()); + assertEquals("123", event.getData().getChatId()); + assertEquals("123", event.getData().getConversationId()); + assertEquals("222", event.getData().getBotId()); + + // 验证 content 中的 function_call 数据 + String content = event.getData().getContent(); + assertTrue(content.contains("toutiaosousuo-search")); + assertTrue(content.contains("今天的体育新闻")); + assertTrue(content.contains("7281192623887548473")); + assertTrue(content.contains("7288907006982012986")); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationAudioDeltaEvent() { + // event_type: conversation.audio.delta + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"conversation.audio.delta\",\n" + + " \"data\": {\n" + + " \"id\": \"msg_006\",\n" + + " \"role\": \"assistant\",\n" + + " \"type\": \"answer\",\n" + + " \"content\": \"你好你好\",\n" + + " \"content_type\": \"text\",\n" + + " \"chat_id\": \"123\",\n" + + " \"conversation_id\": \"123\",\n" + + " \"bot_id\": \"222\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationAudioDelta(eq(client), audioDeltaEventCaptor.capture()); + + ConversationAudioDeltaEvent event = audioDeltaEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_AUDIO_DELTA, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals("msg_006", event.getData().getId()); + assertEquals("assistant", event.getData().getRole().getValue()); + assertEquals("answer", event.getData().getType().getValue()); + assertEquals("你好你好", event.getData().getContent()); + assertEquals("text", event.getData().getContentType().getValue()); + assertEquals("123", event.getData().getChatId()); + assertEquals("123", event.getData().getConversationId()); + assertEquals("222", event.getData().getBotId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationAudioTranscriptCompletedEvent() { + // event_type: conversation.audio_transcript.completed + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"conversation.audio_transcript.completed\",\n" + + " \"data\": {\n" + + " \"content\": \"今天的天气怎么样?\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationAudioTranscriptCompleted( + eq(client), audioTranscriptCompletedEventCaptor.capture()); + + ConversationAudioTranscriptCompletedEvent event = + audioTranscriptCompletedEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_AUDIO_TRANSCRIPT_COMPLETED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals("今天的天气怎么样?", event.getData().getContent()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationAudioTranscriptUpdateEvent() { + // event_type: conversation.audio_transcript.update + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"conversation.audio_transcript.update\",\n" + + " \"data\": {\n" + + " \"content\": \"今天的\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationAudioTranscriptUpdate( + eq(client), audioTranscriptUpdateEventCaptor.capture()); + + ConversationAudioTranscriptUpdateEvent event = audioTranscriptUpdateEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_AUDIO_TRANSCRIPT_UPDATE, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals("今天的", event.getData().getContent()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationChatCanceledEvent() { + // event_type: conversation.chat.canceled + String json = "{\"event_type\":\"conversation.chat.canceled\"}"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationChatCanceled(eq(client), any(ConversationChatCanceledEvent.class)); + } + + @Test + public void testHandleConversationChatCompletedEvent() { + // event_type: conversation.chat.completed + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"conversation.chat.completed\",\n" + + " \"data\": {\n" + + " \"id\": \"123\",\n" + + " \"chat_id\": \"123\",\n" + + " \"conversation_id\": \"123\",\n" + + " \"bot_id\": \"222\",\n" + + " \"created_at\": 1710348675,\n" + + " \"completed_at\": 1710348675,\n" + + " \"last_error\": null,\n" + + " \"meta_data\": {},\n" + + " \"status\": \"completed\",\n" + + " \"usage\": {\n" + + " \"token_count\": 3397,\n" + + " \"output_tokens\": 1173,\n" + + " \"input_tokens\": 2224\n" + + " }\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationChatCompleted(eq(client), chatCompletedEventCaptor.capture()); + + ConversationChatCompletedEvent event = chatCompletedEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_CHAT_COMPLETED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals("123", event.getData().getID()); + assertEquals("123", event.getData().getConversationID()); + assertEquals("222", event.getData().getBotID()); + assertEquals(1710348675, event.getData().getCreatedAt()); + assertEquals(1710348675, event.getData().getCompletedAt()); + assertNull(event.getData().getLastError()); + assertEquals("completed", event.getData().getStatus().getValue()); + + // 验证 usage + assertEquals(3397, event.getData().getUsage().getTokenCount()); + assertEquals(1173, event.getData().getUsage().getOutputTokens()); + assertEquals(2224, event.getData().getUsage().getInputTokens()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationChatCreatedEvent() { + // event_type: conversation.chat.created + String json = + "{\n" + + " \"id\": \"744666853824656xxx\",\n" + + " \"event_type\": \"conversation.chat.created\",\n" + + " \"data\": {\n" + + " \"id\": \"123\",\n" + + " \"conversation_id\": \"123\",\n" + + " \"bot_id\": \"222\",\n" + + " \"created_at\": 1710348675,\n" + + " \"completed_at\": null,\n" + + " \"last_error\": null,\n" + + " \"meta_data\": {},\n" + + " \"status\": \"created\",\n" + + " \"usage\": null\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationChatCreated(eq(client), conversationChatCreatedEventCaptor.capture()); + + ConversationChatCreatedEvent event = conversationChatCreatedEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_CHAT_CREATED, event.getEventType()); + assertEquals("744666853824656xxx", event.getId()); + + // 验证 data + assertEquals("123", event.getData().getID()); + assertEquals("123", event.getData().getConversationID()); + assertEquals("222", event.getData().getBotID()); + assertEquals(1710348675, event.getData().getCreatedAt()); + assertNull(event.getData().getCompletedAt()); + assertNull(event.getData().getLastError()); + assertEquals("created", event.getData().getStatus().getValue()); + assertNull(event.getData().getUsage()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationChatFailedEvent() { + // event_type: conversation.chat.failed + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"conversation.chat.failed\",\n" + + " \"data\": {\n" + + " \"id\": \"123\",\n" + + " \"chat_id\": \"123\",\n" + + " \"conversation_id\": \"123\",\n" + + " \"bot_id\": \"222\",\n" + + " \"created_at\": 1710348675,\n" + + " \"failed_at\": 1710348675,\n" + + " \"last_error\": {\n" + + " \"code\": 1,\n" + + " \"msg\": \"发生异常\"\n" + + " },\n" + + " \"meta_data\": { },\n" + + " \"status\": \"failed\",\n" + + " \"usage\": {\n" + + " \"token_count\": 3397,\n" + + " \"output_tokens\": 1173,\n" + + " \"input_tokens\": 2224\n" + + " }\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationChatFailed(eq(client), chatFailedEventCaptor.capture()); + + ConversationChatFailedEvent event = chatFailedEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_CHAT_FAILED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals("123", event.getData().getID()); + assertEquals("123", event.getData().getConversationID()); + assertEquals("222", event.getData().getBotID()); + assertEquals(1710348675, event.getData().getCreatedAt()); + assertEquals(1710348675, event.getData().getFailedAt()); + assertEquals(1, event.getData().getLastError().getCode()); + assertEquals("发生异常", event.getData().getLastError().getMsg()); + assertEquals("failed", event.getData().getStatus().getValue()); + + // 验证 usage + assertEquals(3397, event.getData().getUsage().getTokenCount()); + assertEquals(1173, event.getData().getUsage().getOutputTokens()); + assertEquals(2224, event.getData().getUsage().getInputTokens()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationChatInProgressEvent() { + // event_type: conversation.chat.in_progress + String json = + "{\n" + + " \"id\": \"744666853824656xxxx\",\n" + + " \"event_type\": \"conversation.chat.in_progress\",\n" + + " \"data\": {\n" + + " \"id\": \"123\",\n" + + " \"conversation_id\": \"123\",\n" + + " \"bot_id\": \"222\",\n" + + " \"created_at\": 1710348675,\n" + + " \"completed_at\": null,\n" + + " \"last_error\": null,\n" + + " \"meta_data\": {},\n" + + " \"status\": \"in_progress\",\n" + + " \"usage\": null\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationChatInProgress(eq(client), chatInProgressEventCaptor.capture()); + + ConversationChatInProgressEvent event = chatInProgressEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_CHAT_IN_PROGRESS, event.getEventType()); + assertEquals("744666853824656xxxx", event.getId()); + + // 验证 data + assertEquals("123", event.getData().getID()); + assertEquals("123", event.getData().getConversationID()); + assertEquals("222", event.getData().getBotID()); + assertEquals(1710348675, event.getData().getCreatedAt()); + assertNull(event.getData().getCompletedAt()); + assertNull(event.getData().getLastError()); + assertEquals("in_progress", event.getData().getStatus().getValue()); + assertNull(event.getData().getUsage()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleConversationChatRequiresActionEvent() { + // event_type: conversation.chat.requires_action + String json = + "{\"event_type\":\"conversation.chat.requires_action\"," + + "\"data\":{" + + "\"required_action\":{" + + "\"type\":\"submit_tool_outputs\"," + + "\"submit_tool_outputs\":{" + + "\"tool_calls\":[{" + + "\"id\":\"call-id\"," + + "\"type\":\"function\"," + + "\"function\":{" + + "\"name\":\"get_weather\"," + + "\"arguments\":\"{\\\"location\\\":\\\"深圳\\\"}\"" + + "}" + + "}]" + + "}" + + "}" + + "}}"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationChatRequiresAction(eq(client), requiresActionEventCaptor.capture()); + + ConversationChatRequiresActionEvent event = requiresActionEventCaptor.getValue(); + assertEquals("submit_tool_outputs", event.getData().getRequiredAction().getType().getValue()); + + List toolCalls = + event.getData().getRequiredAction().getSubmitToolOutputs().getToolCalls(); + assertEquals(1, toolCalls.size()); + + ChatToolCall toolCall = toolCalls.get(0); + assertEquals("call-id", toolCall.getID()); + assertEquals("function", toolCall.getType().getValue()); + assertEquals("get_weather", toolCall.getFunction().getName()); + assertTrue(toolCall.getFunction().getArguments().contains("location")); + assertTrue(toolCall.getFunction().getArguments().contains("深圳")); + } + + @Test + public void testHandleConversationClearedEvent() { + // event_type: conversation.cleared + String json = "{\"event_type\":\"conversation.cleared\"}"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationCleared(eq(client), any(ConversationClearedEvent.class)); + } + + @Test + public void testHandleConversationMessageCompletedEvent() { + // event_type: conversation.message.completed + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"conversation.message.completed\",\n" + + " \"data\": {\n" + + " \"id\": \"msg_002\",\n" + + " \"role\": \"assistant\",\n" + + " \"type\": \"function_call\",\n" + + " \"content\": \"{\\\"name\\\":\\\"toutiaosousuo-search\\\",\\\"arguments\\\":{\\\"cursor\\\":0,\\\"input_query\\\":\\\"今天的体育新闻\\\",\\\"plugin_id\\\":7281192623887548473,\\\"api_id\\\":7288907006982012986,\\\"plugin_type\\\":1}}\",\n" + + " \"content_type\": \"text\",\n" + + " \"chat_id\": \"123\",\n" + + " \"conversation_id\": \"123\",\n" + + " \"bot_id\": \"222\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationMessageCompleted(eq(client), messageCompletedEventCaptor.capture()); + + ConversationMessageCompletedEvent event = messageCompletedEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_MESSAGE_COMPLETED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals("msg_002", event.getData().getId()); + assertEquals("assistant", event.getData().getRole().getValue()); + assertEquals("function_call", event.getData().getType().getValue()); + assertEquals("text", event.getData().getContentType().getValue()); + assertEquals("123", event.getData().getChatId()); + assertEquals("123", event.getData().getConversationId()); + assertEquals("222", event.getData().getBotId()); + + // 验证 content 中的 function_call 数据 + String content = event.getData().getContent(); + assertTrue(content.contains("toutiaosousuo-search")); + assertTrue(content.contains("今天的体育新闻")); + assertTrue(content.contains("7281192623887548473")); + assertTrue(content.contains("7288907006982012986")); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleInputAudioBufferClearedEvent() { + // event_type: input_audio_buffer.cleared + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"input_audio_buffer.cleared\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onInputAudioBufferCleared(eq(client), inputAudioBufferClearedEventCaptor.capture()); + + InputAudioBufferClearedEvent event = inputAudioBufferClearedEventCaptor.getValue(); + assertEquals(EventType.INPUT_AUDIO_BUFFER_CLEARED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleInputAudioBufferCompletedEvent() { + // event_type: input_audio_buffer.completed + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"input_audio_buffer.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onInputAudioBufferCompleted(eq(client), inputAudioBufferCompletedEventCaptor.capture()); + + InputAudioBufferCompletedEvent event = inputAudioBufferCompletedEventCaptor.getValue(); + assertEquals(EventType.INPUT_AUDIO_BUFFER_COMPLETED, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleErrorEvent() { + // event_type: error + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"error\",\n" + + " \"data\": {\n" + + " \"code\": 123,\n" + + " \"msg\": \"error message\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler).onError(eq(client), errorEventCaptor.capture()); + + ErrorEvent event = errorEventCaptor.getValue(); + assertEquals(EventType.ERROR, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals(123, event.getData().getCode()); + assertEquals("error message", event.getData().getMsg()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleUnknownEvent() { + // event_type: unknown + String json = "{\"event_type\":\"unknown\"}"; + + client.handleEvent(mockWebSocket, json); + + verifyNoInteractions(mockCallbackHandler); + } + + @Test + public void testHandleConversationMessageDeltaEvent() { + String json = + "{\n" + + " \"id\": \"event_1\",\n" + + " \"event_type\": \"conversation.message.delta\",\n" + + " \"data\": {\n" + + " \"id\": \"msg_006\",\n" + + " \"role\": \"assistant\",\n" + + " \"type\": \"answer\",\n" + + " \"content\": \"你好你好\",\n" + + " \"content_type\": \"text\",\n" + + " \"chat_id\": \"123\",\n" + + " \"conversation_id\": \"123\",\n" + + " \"bot_id\": \"222\"\n" + + " },\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " }\n" + + "}\n"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onConversationMessageDelta(eq(client), conversationMessageDeltaEventCaptor.capture()); + + ConversationMessageDeltaEvent event = conversationMessageDeltaEventCaptor.getValue(); + assertEquals(EventType.CONVERSATION_MESSAGE_DELTA, event.getEventType()); + assertEquals("event_1", event.getId()); + + // 验证 data + assertEquals("msg_006", event.getData().getId()); + assertEquals("assistant", event.getData().getRole().getValue()); + assertEquals("answer", event.getData().getType().getValue()); + assertEquals("你好你好", event.getData().getContent()); + assertEquals("text", event.getData().getContentType().getValue()); + assertEquals("123", event.getData().getChatId()); + assertEquals("123", event.getData().getConversationId()); + assertEquals("222", event.getData().getBotId()); + + // 验证 detail + assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleInvalidJson() { + String invalidJson = "invalid json"; + + client.handleEvent(mockWebSocket, invalidJson); + + verify(mockCallbackHandler).onClientException(eq(client), any(RuntimeException.class)); + } + + @Test + void testChatUpdate() { + ChatUpdateEventData data = ChatUpdateEventData.builder().build(); + + client.chatUpdate(data); + + verify(mockWebSocket).send(anyString()); // 验证发送了消息 + } + + @Test + void testConversationChatCancel() { + client.conversationChatCancel(); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testConversationChatSubmitToolOutputs() { + ConversationChatSubmitToolOutputsEvent.Data data = + ConversationChatSubmitToolOutputsEvent.Data.builder().chatID("test-tool-call-id").build(); + + client.conversationChatSubmitToolOutputs(data); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testConversationClear() { + client.conversationClear(); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testConversationMessageCreate() { + client.conversationMessageCreate(Message.buildUserQuestionText("hello")); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testInputAudioBufferAppendWithData() { + + client.inputAudioBufferAppend("hello"); + + verify(mockWebSocket).send(anyString()); + } + + @Test + public void testHandleInputAudioBufferSpeechStarted() { + // event_type: conversation.cleared + String json = "{\"event_type\":\"input_audio_buffer.speech_started\"}"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onInputAudioBufferSpeechStarted(eq(client), any(InputAudioBufferSpeechStartedEvent.class)); + } + + @Test + public void testHandleInputAudioBufferSpeechStopped() { + // event_type: conversation.cleared + String json = "{\"event_type\":\"input_audio_buffer.speech_stopped\"}"; + + client.handleEvent(mockWebSocket, json); + + verify(mockCallbackHandler) + .onInputAudioBufferSpeechStopped(eq(client), any(InputAudioBufferSpeechStoppedEvent.class)); + } + + @Test + void testInputAudioBufferAppendWithString() { + String audioData = "base64EncodedAudioData"; + + client.inputAudioBufferAppend(audioData); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testInputAudioBufferClear() { + client.inputAudioBufferClear(); + + verify(mockWebSocket).send(anyString()); + } + + @Test + void testInputAudioBufferComplete() { + client.inputAudioBufferComplete(); + + verify(mockWebSocket).send(anyString()); + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClientTest.java new file mode 100644 index 00000000..ba634c83 --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClientTest.java @@ -0,0 +1,174 @@ +package com.coze.openapi.service.service.websocket.common; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.coze.openapi.client.common.BaseReq; +import com.coze.openapi.client.websocket.common.BaseEvent; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.WebSocket; + +class BaseWebsocketsClientTest { + + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private BaseCallbackHandler mockCallbackHandler; + @Mock private BaseWebsocketsListener mockListener; + + private TestBaseWebsocketClient client; + + // 创建一个测试用的具体实现类 + private static class TestBaseWebsocketClient extends BaseWebsocketsClient { + BaseCallbackHandler handler; + + public TestBaseWebsocketClient( + OkHttpClient client, String url, BaseCallbackHandler handler, BaseReq req) { + super(client, url, handler, req); + this.handler = handler; + } + + @Override + protected BaseCallbackHandler getCallbackHandler() { + return handler; + } + + @Override + protected void handleEvent(WebSocket ws, String text) { + // 测试实现 + } + + // 暴露 protected 方法用于测试 + public void testSendEvent(BaseEvent event) { + sendEvent(event); + } + } + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(Request.class), any(BaseWebsocketsListener.class))) + .thenReturn(mockWebSocket); + + client = + new TestBaseWebsocketClient(mockOkHttpClient, "ws://test.com", mockCallbackHandler, null); + } + + @Test + void testConstructor() { + // 验证构造函数是否正确初始化了 WebSocket + TestBaseWebsocketClient client = + new TestBaseWebsocketClient( + new OkHttpClient(), + "ws://test.com", + mockCallbackHandler, + BaseReq.builder().writeTimeout(1).connectTimeout(2).readTimeout(3).build()); + assertNotNull(client); + } + + @Test + void testInitialization() { + verify(mockOkHttpClient).newWebSocket(any(Request.class), any(BaseWebsocketsListener.class)); + } + + @Test + void testSendEvent() { + // 创建测试事件 + BaseEvent testEvent = new BaseEvent(); + + // 发送事件 + client.testSendEvent(testEvent); + + // 验证 WebSocket.send() 被调用 + verify(mockWebSocket).send(anyString()); + } + + @Test + void testClose() throws InterruptedException { + // 执行关闭 + client.close(); + + // 验证 WebSocket 关闭被调用 + verify(mockWebSocket).close(eq(1000), eq("Normal closure")); + + // 等待一小段时间确保关闭操作完成 + TimeUnit.MILLISECONDS.sleep(100); + + // 再次调用关闭,确保不会出错 + client.close(); + } + + @Test + void testCloseWithInterruption() throws InterruptedException { + // 创建一个会导致中断的模拟 + doAnswer( + invocation -> { + Thread.currentThread().interrupt(); + return null; + }) + .when(mockWebSocket) + .close(anyInt(), anyString()); + + // 执行关闭 + client.close(); + + // 验证 WebSocket 关闭被调用 + verify(mockWebSocket).close(eq(1000), eq("Normal closure")); + + // 清除中断状态 + Thread.interrupted(); + } + + @Test + void testExecutorServiceShutdown() throws InterruptedException { + // 发送多个事件以测试 ExecutorService + BaseEvent testEvent = new BaseEvent(); + for (int i = 0; i < 5; i++) { + client.testSendEvent(testEvent); + } + + // 关闭客户端 + client.close(); + + // 验证所有消息都被发送 + verify(mockWebSocket, times(5)).send(anyString()); + } + + @Test + void testListenerShutdown() { + // 使用反射设置模拟的 listener + try { + java.lang.reflect.Field listenerField = + BaseWebsocketsClient.class.getDeclaredField("listener"); + listenerField.setAccessible(true); + listenerField.set(client, mockListener); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // 关闭客户端 + client.close(); + + // 验证 listener 的 shutdown 被调用 + verify(mockListener).shutdown(); + } + + @Test + void testHandleEventDelegation() { + String testMessage = "test message"; + + // 调用 handleEvent + client.handleEvent(mockWebSocket, testMessage); + + // 由于是空实现,这里主要验证调用不会抛出异常 + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListenerTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListenerTest.java new file mode 100644 index 00000000..ed9690d2 --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListenerTest.java @@ -0,0 +1,155 @@ +package com.coze.openapi.service.service.websocket.common; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okio.ByteString; + +class BaseWebsocketsListenerTest { + + @Mock private WebSocket mockWebSocket; + private Response mockResponse; + @Mock private BaseCallbackHandler mockCallbackHandler; + @Mock private BaseWebsocketsClient mockClient; + @Mock private BiConsumer mockHandleEvent; + + private BaseWebsocketsListener listener; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + // 使用 Builder 创建真实的 Response 对象 + mockResponse = + new Response.Builder() + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .request(new Request.Builder().url("ws://test.com").build()) + .build(); + + listener = new BaseWebsocketsListener(mockHandleEvent, mockCallbackHandler, mockClient); + } + + @Test + void testOnOpen() { + listener.onOpen(mockWebSocket, mockResponse); + // onOpen 只是打印日志,不需要验证 + } + + @Test + void testOnMessage_String() throws InterruptedException { + String message = "test message"; + CountDownLatch latch = new CountDownLatch(1); + + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(mockHandleEvent) + .accept(any(), any()); + + listener.onMessage(mockWebSocket, message); + + // 等待异步处理完成 + latch.await(1, TimeUnit.SECONDS); + verify(mockHandleEvent).accept(mockWebSocket, message); + } + + @Test + void testOnMessage_ByteString() throws InterruptedException { + String message = "test message"; + ByteString byteString = ByteString.encodeUtf8(message); + CountDownLatch latch = new CountDownLatch(1); + + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(mockHandleEvent) + .accept(any(), any()); + + listener.onMessage(mockWebSocket, byteString); + + // 等待异步处理完成 + latch.await(1, TimeUnit.SECONDS); + verify(mockHandleEvent).accept(mockWebSocket, message); + } + + @Test + void testOnClosing() { + int code = 1000; + String reason = "normal closure"; + + listener.onClosing(mockWebSocket, code, reason); + + verify(mockCallbackHandler).onClosing(mockClient, code, reason); + } + + @Test + void testOnClosed() { + int code = 1000; + String reason = "normal closure"; + + listener.onClosed(mockWebSocket, code, reason); + + verify(mockCallbackHandler).onClosed(mockClient, code, reason); + } + + @Test + void testOnFailure() { + Throwable error = new RuntimeException("test error"); + + listener.onFailure(mockWebSocket, error, mockResponse); + + verify(mockCallbackHandler).onFailure(eq(mockClient), any(RuntimeException.class)); + } + + @Test + void testShutdown() { + listener.shutdown(); + + // 再次调用 shutdown 确保不会出错 + listener.shutdown(); + } + + @Test + void testMultipleMessages() throws InterruptedException { + int messageCount = 5; + CountDownLatch latch = new CountDownLatch(messageCount); + + doAnswer( + invocation -> { + latch.countDown(); + return null; + }) + .when(mockHandleEvent) + .accept(any(), any()); + + // 发送多条消息 + for (int i = 0; i < messageCount; i++) { + String message = "message " + i; + listener.onMessage(mockWebSocket, message); + } + + // 等待所有消息处理完成 + latch.await(2, TimeUnit.SECONDS); + verify(mockHandleEvent, times(messageCount)).accept(any(), any()); + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/workflow/WorkFlowRunServiceTest.java b/api/src/test/java/com/coze/openapi/service/service/workflow/WorkFlowRunServiceTest.java index 19064c66..70dfccc0 100644 --- a/api/src/test/java/com/coze/openapi/service/service/workflow/WorkFlowRunServiceTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/workflow/WorkFlowRunServiceTest.java @@ -88,7 +88,7 @@ void parseStreamEventTest() { // 准备 SSE 格式的响应数据 // 使用 okio 的 Buffer 创建模拟的响应流 ResponseBody responseBody = - ResponseBody.create(eventData, MediaType.parse("text/event-stream; charset=utf-8")); + ResponseBody.create(MediaType.parse("text/event-stream; charset=utf-8"), eventData); // 模拟 API 调用 Call call = Calls.response(responseBody); diff --git a/api/src/test/java/com/coze/openapi/service/utils/UtilsTest.java b/api/src/test/java/com/coze/openapi/service/utils/UtilsTest.java index ff265cf0..9f2432c1 100644 --- a/api/src/test/java/com/coze/openapi/service/utils/UtilsTest.java +++ b/api/src/test/java/com/coze/openapi/service/utils/UtilsTest.java @@ -13,9 +13,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.Headers; +import okhttp3.MediaType; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response.Builder; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Response; @@ -25,7 +27,7 @@ public class UtilsTest { @BeforeEach public void setUp() { - objectMapper = Utils.defaultObjectMapper(); + objectMapper = Utils.getMapper(); } public static class TestResponse extends BaseResp {} @@ -104,7 +106,7 @@ public void testExecuteWithHttpError() throws IOException { .message("Bad Request") .build(); - Response response = Response.error(400, okhttp3.ResponseBody.create("", null)); + Response response = Response.error(400, ResponseBody.create(MediaType.parse(""), "")); when(call.execute()).thenReturn(response); // 执行测试,应该抛出异常 @@ -131,6 +133,7 @@ public void testJsonConversion() { @Test public void testGenRandomSign() { + // 测试生成16位随机字符串 String sign = Utils.genRandomSign(16); assertNotNull(sign); diff --git a/example/pom.xml b/example/pom.xml index 4101de0f..c94f595d 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -16,7 +16,7 @@ com.coze coze-api - 0.2.8 + 0.2.9 diff --git a/example/src/main/java/example/audio/transcriptions/CreateTranscriptionExample.java b/example/src/main/java/example/audio/transcriptions/CreateTranscriptionExample.java new file mode 100644 index 00000000..c3e02ffb --- /dev/null +++ b/example/src/main/java/example/audio/transcriptions/CreateTranscriptionExample.java @@ -0,0 +1,25 @@ +package example.audio.transcriptions; + +import com.coze.openapi.client.audio.transcriptions.CreateTranscriptionsReq; +import com.coze.openapi.client.audio.transcriptions.CreateTranscriptionsResp; +import com.coze.openapi.service.auth.TokenAuth; +import com.coze.openapi.service.service.CozeAPI; + +public class CreateTranscriptionExample { + public static void main(String[] args) { + String token = System.getenv("COZE_API_TOKEN"); + TokenAuth authCli = new TokenAuth(token); + CozeAPI coze = + new CozeAPI.Builder() + .baseURL(System.getenv("COZE_API_BASE")) + .auth(authCli) + .readTimeout(10000) + .connectTimeout(10000) + .build(); + String voiceFilePath = System.getenv("VOICE_FILE_PATH"); + + CreateTranscriptionsReq req = CreateTranscriptionsReq.of(voiceFilePath); + CreateTranscriptionsResp resp = coze.audio().transcription().create(req); + System.out.println(resp); + } +} diff --git a/example/src/main/java/example/auth/JWTOAuthExample.java b/example/src/main/java/example/auth/JWTOAuthExample.java index 8af0c006..37e31dfd 100644 --- a/example/src/main/java/example/auth/JWTOAuthExample.java +++ b/example/src/main/java/example/auth/JWTOAuthExample.java @@ -36,7 +36,7 @@ public static void main(String[] args) { String jwtOauthPrivateKey = System.getenv("COZE_JWT_OAUTH_PRIVATE_KEY"); String jwtOauthPrivateKeyFilePath = System.getenv("COZE_JWT_OAUTH_PRIVATE_KEY_FILE_PATH"); String jwtOauthPublicKeyID = System.getenv("COZE_JWT_OAUTH_PUBLIC_KEY_ID"); - + // jwtOauthPublicKeyID+="123"; JWTOAuthClient oauth = null; try { jwtOauthPrivateKey = @@ -78,7 +78,11 @@ public static void main(String[] args) { The jwt oauth process does not support refreshing tokens. When the token expires, just directly call get_access_token to generate a new token. * */ - CozeAPI coze = new CozeAPI.Builder().auth(new JWTOAuth(oauth)).baseURL(cozeAPIBase).build(); + CozeAPI coze = + new CozeAPI.Builder() + .auth(JWTOAuth.builder().jwtClient(oauth).build()) + .baseURL(cozeAPIBase) + .build(); // you can also specify the scope and session for it } } diff --git a/example/src/main/java/example/utils/ExampleUtils.java b/example/src/main/java/example/utils/ExampleUtils.java new file mode 100644 index 00000000..da328496 --- /dev/null +++ b/example/src/main/java/example/utils/ExampleUtils.java @@ -0,0 +1,50 @@ +package example.utils; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class ExampleUtils { + public static void writePcmToWavFile(byte[] pcmData, String filepath) throws IOException { + try (FileOutputStream fos = new FileOutputStream(filepath)) { + int channels = 1; // 单声道 + int sampleWidth = 2; + int frameRate = 24000; + // RIFF header + writeString(fos, "RIFF"); + writeInt(fos, 36 + pcmData.length); + writeString(fos, "WAVE"); + + // Format chunk + writeString(fos, "fmt "); + writeInt(fos, 16); // Subchunk1Size + writeShort(fos, (short) 1); // AudioFormat (PCM) + writeShort(fos, (short) channels); + writeInt(fos, frameRate); + writeInt(fos, frameRate * channels * sampleWidth); // ByteRate + writeShort(fos, (short) (channels * sampleWidth)); // BlockAlign + writeShort(fos, (short) (sampleWidth * 8)); // BitsPerSample + + // Data chunk + writeString(fos, "data"); + writeInt(fos, pcmData.length); + fos.write(pcmData); + } + } + + private static void writeString(FileOutputStream fos, String value) throws IOException { + fos.write(value.getBytes(StandardCharsets.US_ASCII)); + } + + private static void writeInt(FileOutputStream fos, int value) throws IOException { + fos.write(value & 0xFF); + fos.write((value >> 8) & 0xFF); + fos.write((value >> 16) & 0xFF); + fos.write((value >> 24) & 0xFF); + } + + private static void writeShort(FileOutputStream fos, short value) throws IOException { + fos.write(value & 0xFF); + fos.write((value >> 8) & 0xFF); + } +} diff --git a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java new file mode 100644 index 00000000..17e27b47 --- /dev/null +++ b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java @@ -0,0 +1,130 @@ +package example.websocket.audio.speech; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.OutputAudio; +import com.coze.openapi.client.websocket.event.model.PCMConfig; +import com.coze.openapi.client.websocket.event.model.SpeechUpdateEventData; +import com.coze.openapi.service.auth.TokenAuth; +import com.coze.openapi.service.service.CozeAPI; +import com.coze.openapi.service.service.websocket.audio.speech.WebsocketsAudioSpeechCallbackHandler; +import com.coze.openapi.service.service.websocket.audio.speech.WebsocketsAudioSpeechClient; +import com.coze.openapi.service.service.websocket.audio.speech.WebsocketsAudioSpeechCreateReq; + +import example.utils.ExampleUtils; + +/* +This example demonstrates how to use the WebSocket audio speech interface to: +- Create a speech session +- Configure audio parameters +- Send text input +- Process and save the resulting audio output +*/ +public class WebsocketAudioSpeechExample { + + private static boolean isDone = false; + + private static class CallbackHandler extends WebsocketsAudioSpeechCallbackHandler { + private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 + + public CallbackHandler() { + super(); + } + + // 语音创建成功事件 (speech.created) + @Override + public void onSpeechCreated(WebsocketsAudioSpeechClient client, SpeechCreatedEvent event) { + System.out.println("==== Speech Created ===="); + System.out.println(event); + } + + // 语音配置更新事件 (speech.update) + @Override + public void onSpeechUpdated(WebsocketsAudioSpeechClient client, SpeechUpdatedEvent event) { + System.out.println("==== Speech Updated ===="); + System.out.println(event); + } + + // 语音数据更新事件 (speech.audio.update) + @Override + public void onSpeechAudioUpdate( + WebsocketsAudioSpeechClient client, SpeechAudioUpdateEvent event) { + buffer.put(event.getDelta()); + } + + // 语音数据完成事件 (speech.audio.completed) + @Override + public void onSpeechAudioCompleted( + WebsocketsAudioSpeechClient client, SpeechAudioCompletedEvent event) { + try { + ExampleUtils.writePcmToWavFile(buffer.array(), "output_speech.wav"); + System.out.println("========= On Speech Audio Completed ========="); + isDone = true; + } catch (IOException e) { + e.printStackTrace(); + } + } + + // 文本缓冲区完成事件 (input_text_buffer.completed) + @Override + public void onInputTextBufferCompleted( + WebsocketsAudioSpeechClient client, InputTextBufferCompletedEvent event) { + System.out.println("==== Input Text Buffer Completed ===="); + System.out.println(event); + } + + @Override + public void onError(WebsocketsAudioSpeechClient client, ErrorEvent event) { + System.out.println(event); + } + } + + // For non-streaming chat API, it is necessary to create a chat first and then poll the chat + // results. + public static void main(String[] args) throws Exception { + // Get an access_token through personal access token or oauth. + String token = System.getenv("COZE_API_TOKEN"); + String voiceID = System.getenv("COZE_VOICE_ID"); + TokenAuth authCli = new TokenAuth(token); + + // Init the Coze client through the access_token. + CozeAPI coze = + new CozeAPI.Builder() + .baseURL(System.getenv("COZE_API_BASE")) + .auth(authCli) + .readTimeout(10000) + .build(); + + WebsocketsAudioSpeechClient client = null; + try { + client = + coze.websockets() + .audio() + .speech() + .create(new WebsocketsAudioSpeechCreateReq(new CallbackHandler())); + OutputAudio outputAudio = + OutputAudio.builder() + .voiceId(voiceID) + .codec("pcm") + .speechRate(50) + .pcmConfig(PCMConfig.builder().sampleRate(24000).build()) + .build(); + client.speechUpdate(new SpeechUpdateEventData(outputAudio)); + client.inputTextBufferAppend("hello world, nice to meet you!"); + client.inputTextBufferComplete(); + while (!isDone) { + TimeUnit.MILLISECONDS.sleep(100); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (client != null) { + client.close(); + } + coze.shutdownExecutor(); + } + } +} diff --git a/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java new file mode 100644 index 00000000..afdfd51f --- /dev/null +++ b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java @@ -0,0 +1,152 @@ +package example.websocket.audio.transcriptions; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import com.coze.openapi.client.audio.common.AudioFormat; +import com.coze.openapi.client.audio.speech.CreateSpeechReq; +import com.coze.openapi.client.audio.speech.CreateSpeechResp; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.InputAudio; +import com.coze.openapi.client.websocket.event.model.TranscriptionsUpdateEventData; +import com.coze.openapi.service.auth.TokenAuth; +import com.coze.openapi.service.service.CozeAPI; +import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketsAudioTranscriptionsCallbackHandler; +import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketsAudioTranscriptionsClient; +import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketsAudioTranscriptionsCreateReq; + +/* +This example demonstrates how to use the WebSocket transcription API to transcribe audio data, +process transcription events, and handle the results through callback methods. + */ +public class WebsocketTranscriptionsExample { + + public static boolean isDone = false; + + private static class CallbackHandler extends WebsocketsAudioTranscriptionsCallbackHandler { + private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 + + public CallbackHandler() { + super(); + } + + @Override + public void onError(WebsocketsAudioTranscriptionsClient client, ErrorEvent event) { + System.out.println(event); + } + + @Override + public void onClientException(WebsocketsAudioTranscriptionsClient client, Throwable e) { + e.printStackTrace(); + } + + // 转录配置更新事件 (transcriptions.updated) + @Override + public void onTranscriptionsUpdated( + WebsocketsAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) { + System.out.println("=== Transcriptions Updated ==="); + System.out.println(event); + } + + // 转录创建事件 (transcriptions.created) + @Override + public void onTranscriptionsCreated( + WebsocketsAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) { + System.out.println("=== Transcriptions Created ==="); + System.out.println(event); + } + + // 转录消息更新事件 (transcriptions.message.update) + @Override + public void onTranscriptionsMessageUpdate( + WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) { + System.out.println(event.getData().getContent()); + } + + // 转录消息完成事件 (transcriptions.message.completed) + @Override + public void onTranscriptionsMessageCompleted( + WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) { + System.out.println("=== Transcriptions Message Completed ==="); + System.out.println(event); + isDone = true; + } + + // 语音缓冲区完成事件 (input_audio_buffer.completed) + @Override + public void onInputAudioBufferCompleted( + WebsocketsAudioTranscriptionsClient client, InputAudioBufferCompletedEvent event) { + System.out.println("=== Input Audio Buffer Completed ==="); + System.out.println(event); + } + } + + // For non-streaming chat API, it is necessary to create a chat first and then poll the chat + // results. + public static void main(String[] args) throws Exception { + // Get an access_token through personal access token or oauth. + String token = System.getenv("COZE_API_TOKEN"); + String voiceID = System.getenv("COZE_VOICE_ID"); + TokenAuth authCli = new TokenAuth(token); + + // Init the Coze client through the access_token. + CozeAPI coze = + new CozeAPI.Builder() + .baseURL(System.getenv("COZE_API_BASE")) + .auth(authCli) + .readTimeout(10000) + .build(); + + WebsocketsAudioTranscriptionsClient client = null; + try { + client = + coze.websockets() + .audio() + .transcriptions() + .create(new WebsocketsAudioTranscriptionsCreateReq(new CallbackHandler())); + CreateSpeechResp speechResp = + coze.audio() + .speech() + .create( + CreateSpeechReq.builder() + .input("今天深圳的天气怎么样?") + .voiceID(voiceID) + .responseFormat(AudioFormat.WAV) + .sampleRate(24000) + .build()); + + InputAudio inputAudio = + InputAudio.builder().sampleRate(24000).codec("pcm").format("wav").channel(2).build(); + client.transcriptionsUpdate(new TranscriptionsUpdateEventData(inputAudio)); + + try (InputStream inputStream = speechResp.getResponse().byteStream()) { + byte[] buffer = new byte[1024]; + int bytesRead; + + while ((bytesRead = inputStream.read(buffer)) != -1) { + client.inputAudioBufferAppend(Arrays.copyOf(buffer, bytesRead)); + // 模拟人说话的间隔 + TimeUnit.MILLISECONDS.sleep(100); + } + client.inputAudioBufferComplete(); + } catch (IOException e) { + e.printStackTrace(); + } + + while (!isDone) { + TimeUnit.MILLISECONDS.sleep(100); + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (client != null) { + client.close(); + } + coze.shutdownExecutor(); + } + } +} diff --git a/example/src/main/java/example/websocket/chat/ChatExample.java b/example/src/main/java/example/websocket/chat/ChatExample.java new file mode 100644 index 00000000..d3a3a581 --- /dev/null +++ b/example/src/main/java/example/websocket/chat/ChatExample.java @@ -0,0 +1,191 @@ +package example.websocket.chat; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.coze.openapi.client.audio.common.AudioFormat; +import com.coze.openapi.client.audio.speech.CreateSpeechReq; +import com.coze.openapi.client.audio.speech.CreateSpeechResp; +import com.coze.openapi.client.chat.model.ChatToolCall; +import com.coze.openapi.client.chat.model.ToolOutput; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.upstream.ConversationChatSubmitToolOutputsEvent; +import com.coze.openapi.service.auth.TokenAuth; +import com.coze.openapi.service.service.CozeAPI; +import com.coze.openapi.service.service.websocket.chat.WebsocketsChatCallbackHandler; +import com.coze.openapi.service.service.websocket.chat.WebsocketsChatClient; +import com.coze.openapi.service.service.websocket.chat.WebsocketsChatCreateReq; +import com.coze.openapi.service.utils.Utils; +import com.fasterxml.jackson.annotation.JsonProperty; + +import example.utils.ExampleUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/* +This example demonstrates how to use the WebSocket chat interface to: +- Create a speech session +- Configure audio parameters +- Send text input +- Receive and process local plugin event +- Process and save the resulting audio output +*/ +public class ChatExample { + + private static boolean isDone; + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + private static class Weather { + @JsonProperty("weather") + private String weather; + } + + private static class CallbackHandler extends WebsocketsChatCallbackHandler { + private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 + + public CallbackHandler() { + super(); + } + + @Override + public void onChatCreated(WebsocketsChatClient client, ChatCreatedEvent event) { + System.out.println(event); + // client.sendEvent(new BaseEvent()); + } + + @Override + public void onConversationMessageDelta( + WebsocketsChatClient client, ConversationMessageDeltaEvent event) { + System.out.printf("Revieve: %s\n", event.getData().getContent()); + } + + @Override + public void onError(WebsocketsChatClient client, ErrorEvent event) { + System.out.println(event); + } + + @Override + public void onInputAudioBufferCompleted( + WebsocketsChatClient client, InputAudioBufferCompletedEvent event) { + System.out.println("========= Input Audio Buffer Completed ========="); + System.out.println(event); + } + + @Override + public void onConversationAudioCompleted( + WebsocketsChatClient client, ConversationAudioCompletedEvent event) { + try { + ExampleUtils.writePcmToWavFile(buffer.array(), "output.wav"); + System.out.println("========= Output Audio Completed ========="); + isDone = true; + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onClientException(WebsocketsChatClient client, Throwable e) { + e.printStackTrace(); + } + + @Override + public void onConversationAudioDelta( + WebsocketsChatClient client, ConversationAudioDeltaEvent event) { + byte[] audioData = event.getData().getAudio(); + buffer.put(audioData); + } + + @Override + public void onConversationChatRequiresAction( + WebsocketsChatClient client, ConversationChatRequiresActionEvent event) { + List toolOutputs = new ArrayList<>(); + for (ChatToolCall call : + event.getData().getRequiredAction().getSubmitToolOutputs().getToolCalls()) { + toolOutputs.add( + ToolOutput.builder() + .toolCallID(call.getID()) + // 模拟端插件返回 + .output(Utils.toJson(new Weather("今天深圳的天气是 10 到 20 摄氏度"))) + .build()); + } + ConversationChatSubmitToolOutputsEvent.Data data = + ConversationChatSubmitToolOutputsEvent.Data.builder() + .chatID(event.getData().getID()) + .toolOutputs(toolOutputs) + .build(); + client.conversationChatSubmitToolOutputs(data); + System.out.println("========= Conversation Chat Submit Tool Outputs ========="); + } + } + + // For non-streaming chat API, it is necessary to create a chat first and then poll the chat + // results. + public static void main(String[] args) throws Exception { + // Get an access_token through personal access token or oauth. + String token = System.getenv("COZE_API_TOKEN"); + String botID = System.getenv("PUBLISHED_BOT_ID"); + String voiceID = System.getenv("COZE_VOICE_ID"); + TokenAuth authCli = new TokenAuth(token); + + // Init the Coze client through the access_token. + CozeAPI coze = + new CozeAPI.Builder() + .baseURL(System.getenv("COZE_API_BASE")) + .auth(authCli) + .readTimeout(10000) + .build(); + + WebsocketsChatClient client = null; + try { + client = + coze.websockets() + .chat() + .create(new WebsocketsChatCreateReq(botID, new CallbackHandler())); + CreateSpeechResp speechResp = + coze.audio() + .speech() + .create( + CreateSpeechReq.builder() + .input("今天深圳的天气怎么样?") + .voiceID(voiceID) + .responseFormat(AudioFormat.WAV) + .sampleRate(24000) + .build()); + + try (InputStream inputStream = speechResp.getResponse().byteStream()) { + byte[] buffer = new byte[1024]; + int bytesRead; + + while ((bytesRead = inputStream.read(buffer)) != -1) { + client.inputAudioBufferAppend(Arrays.copyOf(buffer, bytesRead)); + // 模拟人说话的间隔 + TimeUnit.MILLISECONDS.sleep(100); + } + client.inputAudioBufferComplete(); + } catch (IOException e) { + e.printStackTrace(); + } + while (!isDone) { + TimeUnit.MILLISECONDS.sleep(100); + } + + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (client != null) { + client.close(); + } + coze.shutdownExecutor(); + } + } +} diff --git a/pom.xml b/pom.xml index ee428297..284c4a80 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 1.8 1.8 1.18.24 - 5.8.2 + 5.10.2 @@ -58,6 +58,10 @@ **/client/**/* + **/BaseCallbackHandler.java + **/WebsocketsChatCallbackHandler.java + **/WebsocketsAudioTranscriptionsCallbackHandler.java + **/WebsocketsAudioSpeechCallbackHandler.java