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