From 92b6657159b8f8e6b8c7e386cd607186182c95ae Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 26 Feb 2025 17:05:22 +0800 Subject: [PATCH 01/50] feat: add audio transcription interface Change-Id: I12ae9f032638b32264d5dbf4ac13578922fffb48 --- .../openapi/api/AudioTranscriptionAPI.java | 19 +++++++ .../CreateTranscriptionsReq.java | 42 +++++++++++++++ .../CreateTranscriptionsResp.java | 17 ++++++ .../coze/openapi/service/service/CozeAPI.java | 3 +- .../service/service/audio/AudioService.java | 13 ++++- .../service/audio/TranscriptionService.java | 53 +++++++++++++++++++ .../CreateTranscriptionExample.java | 25 +++++++++ 7 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/com/coze/openapi/api/AudioTranscriptionAPI.java create mode 100644 api/src/main/java/com/coze/openapi/client/audio/transcriptions/CreateTranscriptionsReq.java create mode 100644 api/src/main/java/com/coze/openapi/client/audio/transcriptions/CreateTranscriptionsResp.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/audio/TranscriptionService.java create mode 100644 example/src/main/java/example/audio/transcriptions/CreateTranscriptionExample.java 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/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/service/service/CozeAPI.java b/api/src/main/java/com/coze/openapi/service/service/CozeAPI.java index 2b49aee3..f68050b7 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 @@ -192,7 +192,8 @@ 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)); return new CozeAPI( this.baseURL, 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..54bad1f5 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/audio/TranscriptionService.java @@ -0,0 +1,53 @@ +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 roomApi; + + public TranscriptionService(AudioTranscriptionAPI roomApi) { + this.roomApi = roomApi; + } + + 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) { + requestFile = RequestBody.create((File) fileSource, MediaType.parse("multipart/form-data")); + } else { + requestFile = RequestBody.create((byte[]) fileSource, MediaType.parse("multipart/form-data")); + } + + MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, requestFile); + + BaseResponse resp = Utils.execute(roomApi.create(body, req)); + CreateTranscriptionsResp data = resp.getData(); + data.setLogID(resp.getLogID()); + return data; + } +} 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); + } +} From 1311529a716b55a93b17fb75d33179f68310314b Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Fri, 28 Feb 2025 18:04:02 +0800 Subject: [PATCH 02/50] save chat ws Change-Id: Ic2427cb8f1ff9d20ad2f31c7e50f0aad1e812e5e --- .../client/audio/speech/CreateSpeechReq.java | 3 + .../connversations/message/model/Message.java | 8 + .../message/model/MessageContentType.java | 2 + .../client/exception/CozeApiException.java | 20 +- .../client/websocket/common/BaseEvent.java | 20 ++ .../client/websocket/common/Detail.java | 8 + .../client/websocket/event/EventType.java | 86 ++++++++ .../event/downstream/ChatCreatedEvent.java | 26 +++ .../event/downstream/ChatUpdatedEvent.java | 27 +++ .../ConversationAudioCompletedEvent.java | 26 +++ .../ConversationAudioDeltaEvent.java | 26 +++ ...ersationAudioTranscriptCompletedEvent.java | 34 +++ ...onversationAudioTranscriptUpdateEvent.java | 34 +++ .../ConversationChatCanceledEvent.java | 21 ++ .../ConversationChatCompletedEvent.java | 26 +++ .../ConversationChatCreatedEvent.java | 26 +++ .../ConversationChatFailedEvent.java | 26 +++ .../ConversationChatInProgressEvent.java | 26 +++ .../ConversationChatRequiresActionEvent.java | 26 +++ .../downstream/ConversationClearedEvent.java | 21 ++ .../ConversationMessageCompletedEvent.java | 26 +++ .../ConversationMessageDeltaEvent.java | 26 +++ .../event/downstream/ErrorEvent.java | 26 +++ .../InputAudioBufferClearedEvent.java | 21 ++ .../InputAudioBufferCompletedEvent.java | 21 ++ .../InputTextBufferCompletedEvent.java | 22 ++ .../downstream/SpeechAudioCompletedEvent.java | 24 +++ .../downstream/SpeechAudioUpdateEvent.java | 41 ++++ .../event/downstream/SpeechCreatedEvent.java | 24 +++ .../event/downstream/SpeechUpdatedEvent.java | 30 +++ .../websocket/event/model/ChatConfig.java | 32 +++ .../event/model/ChatUpdateEventData.java | 24 +++ .../websocket/event/model/InputAudio.java | 27 +++ .../websocket/event/model/OpusConfig.java | 21 ++ .../websocket/event/model/OutputAudio.java | 27 +++ .../websocket/event/model/PCMConfig.java | 15 ++ .../model/SpeechEventUpdateEventData.java | 14 ++ .../event/upstream/ChatUpdateEvent.java | 30 +++ .../upstream/ConversationChatCancelEvent.java | 21 ++ ...onversationChatSubmitToolOutputsEvent.java | 42 ++++ .../upstream/ConversationClearEvent.java | 24 +++ .../ConversationMessageCreateEvent.java | 30 +++ .../upstream/InputAudioBufferAppendEvent.java | 36 ++++ .../upstream/InputAudioBufferClearEvent.java | 21 ++ .../InputAudioBufferCompleteEvent.java | 24 +++ .../upstream/InputTextBufferAppendEvent.java | 40 ++++ .../InputTextBufferCompleteEvent.java | 24 +++ .../event/upstream/SpeechUpdateEvent.java | 30 +++ .../openapi/service/auth/OAuthClient.java | 2 +- .../coze/openapi/service/service/CozeAPI.java | 19 +- .../service/common/AbstractEventCallback.java | 2 +- .../service/websocket/WebsocketClient.java | 18 ++ .../WebsocketAudioSpeechCallbackHandler.java | 27 +++ .../speech/WebsocketAudioSpeechClient.java | 100 +++++++++ .../chat/WebsocketChatCallbackHandler.java | 90 ++++++++ .../websocket/chat/WebsocketChatClient.java | 198 ++++++++++++++++++ .../chat/WebsocketChatClientBuilder.java | 14 ++ .../service/websocket/chat/WsChatReq.java | 16 ++ .../websocket/common/BaseCallbackHandler.java | 24 +++ .../common/BaseWebSocketListener.java | 51 +++++ .../com/coze/openapi/service/utils/Utils.java | 27 ++- .../chat/WebsocketChatClientTest.java | 93 ++++++++ .../coze/openapi/service/utils/UtilsTest.java | 2 +- .../main/java/example/utils/ExampleUtils.java | 50 +++++ .../example/websocket/chat/ChatExample.java | 179 ++++++++++++++++ 65 files changed, 2125 insertions(+), 22 deletions(-) create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/common/BaseEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/common/Detail.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/EventType.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatCreatedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatUpdatedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioCompletedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioDeltaEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioTranscriptCompletedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationAudioTranscriptUpdateEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCanceledEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCompletedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatCreatedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatFailedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatInProgressEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationChatRequiresActionEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationClearedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationMessageCompletedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ConversationMessageDeltaEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ErrorEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferClearedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferCompletedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputTextBufferCompletedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioCompletedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioUpdateEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechCreatedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechUpdatedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatConfig.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatUpdateEventData.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/InputAudio.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/OpusConfig.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/OutputAudio.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/PCMConfig.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechEventUpdateEventData.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ChatUpdateEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationChatCancelEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationChatSubmitToolOutputsEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationClearEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/ConversationMessageCreateEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferAppendEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferClearEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferCompleteEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputTextBufferAppendEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputTextBufferCompleteEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/SpeechUpdateEvent.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/chat/WsChatReq.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseCallbackHandler.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java create mode 100644 api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java create mode 100644 example/src/main/java/example/utils/ExampleUtils.java create mode 100644 example/src/main/java/example/websocket/chat/ChatExample.java 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/connversations/message/model/Message.java b/api/src/main/java/com/coze/openapi/client/connversations/message/model/Message.java index 7193ce7c..aca7f4a4 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,13 @@ public class Message { @JsonProperty("reasoning_content") private String reasoningContent; + public byte[] getAudio() { + if (this.contentType == MessageContentType.AUDIO) { + 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/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..c25bf8ff --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/common/Detail.java @@ -0,0 +1,8 @@ +package com.coze.openapi.client.websocket.common; + +import com.fasterxml.jackson.annotation.JsonProperty; + +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..87c6bb5b --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/EventType.java @@ -0,0 +1,86 @@ +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 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 +} 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..c0ec783c --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatCreatedEvent.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.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 +@Builder +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..4e38678f --- /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; + +@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..6324efe1 --- /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; + +@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/InputTextBufferCompletedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputTextBufferCompletedEvent.java new file mode 100644 index 00000000..059500c5 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputTextBufferCompletedEvent.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 +@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; + + +} \ No newline at end of file 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..66323fef --- /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; +} \ No newline at end of file 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..d968a276 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/SpeechAudioUpdateEvent.java @@ -0,0 +1,41 @@ +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; + +import java.util.Base64; + +@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; + + @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); + } +} \ No newline at end of file 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..07d4b8cc --- /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; +} \ No newline at end of file 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..3475bad7 --- /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.SpeechEventUpdateEventData; +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 SpeechEventUpdateEventData data; +} \ No newline at end of file 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..c5a3e396 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatUpdateEventData.java @@ -0,0 +1,24 @@ +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; +} 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/OpusConfig.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/OpusConfig.java new file mode 100644 index 00000000..39136c8a --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/OpusConfig.java @@ -0,0 +1,21 @@ +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; +} 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/SpeechEventUpdateEventData.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechEventUpdateEventData.java new file mode 100644 index 00000000..2820fc5f --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechEventUpdateEventData.java @@ -0,0 +1,14 @@ +package com.coze.openapi.client.websocket.event.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +public class SpeechEventUpdateEventData { + @JsonProperty("output_audio") + private OutputAudio outputAudio; +} 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..3cc4b299 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferAppendEvent.java @@ -0,0 +1,36 @@ +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.service.utils.Utils; +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..38ef20fe --- /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.SpeechEventUpdateEventData; +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 SpeechEventUpdateEventData data; +} \ No newline at end of file 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..1c7b549f 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 @@ -40,7 +40,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; 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 f68050b7..edd310fc 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.WebsocketClient; 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 WebsocketClient websocket; private CozeAPI( String baseURL, @@ -57,7 +60,8 @@ private CozeAPI( WorkflowService workflowAPI, ChatService chatAPI, AudioService audioAPI, - TemplateService templateAPI) { + TemplateService templateAPI, + WebsocketClient 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 WebsocketClient websocket() { + 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)); @@ -195,6 +204,9 @@ public CozeAPI build() { retrofit.create(AudioSpeechAPI.class), retrofit.create(AudioTranscriptionAPI.class)); TemplateService templateAPI = new TemplateService(retrofit.create(TemplateAPI.class)); + WebsocketClient websocket = + new WebsocketClient( + this.client, Strings.replace(this.baseURL, "https://api", "wss://ws")); return new CozeAPI( this.baseURL, executorService, @@ -207,7 +219,8 @@ public CozeAPI build() { workflowAPI, chatAPI, audioAPI, - templateAPI); + templateAPI, + websocket); } // 确保加上了 Auth 拦截器 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/websocket/WebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java new file mode 100644 index 00000000..f7a72c3c --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java @@ -0,0 +1,18 @@ +package com.coze.openapi.service.service.websocket; + +import com.coze.openapi.service.service.websocket.chat.WebsocketChatClientBuilder; + +import okhttp3.OkHttpClient; + +public class WebsocketClient { + + private final WebsocketChatClientBuilder chat; + + public WebsocketClient(OkHttpClient client, String baseURL) { + this.chat = new WebsocketChatClientBuilder(baseURL, client); + } + + public WebsocketChatClientBuilder chat() { + return chat; + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java new file mode 100644 index 00000000..0839a95d --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java @@ -0,0 +1,27 @@ +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 WebsocketAudioSpeechCallbackHandler extends BaseCallbackHandler { + public WebsocketAudioSpeechCallbackHandler() {} + + // 语音创建成功事件 (speech.created) + public void onSpeechCreated(WebsocketAudioSpeechClient client, SpeechCreatedEvent event) {} + + // 语音配置更新事件 (speech.update) + public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEvent event) {} + + // 语音数据更新事件 (speech.audio.update) + public void onSpeechAudioUpdate(WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) {} + + // 语音数据完成事件 (speech.audio.completed) + public void onSpeechAudioCompleted(WebsocketAudioSpeechClient client, SpeechAudioCompletedEvent event) {} + + // 文本缓冲区完成事件 (input_text_buffer.completed) + public void onInputTextBufferCompleted(WebsocketAudioSpeechClient client, InputTextBufferCompletedEvent event) {} +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java new file mode 100644 index 00000000..7adf076e --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java @@ -0,0 +1,100 @@ +package com.coze.openapi.service.service.websocket.audio.speech; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.downstream.*; +import com.coze.openapi.client.websocket.event.model.SpeechEventUpdateEventData; +import com.coze.openapi.client.websocket.event.upstream.*; +import com.coze.openapi.service.service.websocket.common.BaseWebSocketListener; +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 class WebsocketAudioSpeechClient { + private final ObjectMapper objectMapper = Utils.getMapper(); + private final WebSocket ws; + private final WebsocketAudioSpeechCallbackHandler handler; + private static final String uri = "/v1/audio/speech"; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + public WebsocketAudioSpeechClient(OkHttpClient client, String wsHost, WebsocketAudioSpeechCallbackHandler handler) { + String url = String.format("%s%s?voice_id=%s", wsHost, uri, "req.getVoiceID()"); + Request request = new Request.Builder().url(url).build(); + this.handler = handler; + this.ws = client.newWebSocket(request, new BaseWebSocketListener<>(this::handleEvent, this.handler, this)); + } + + private void sendEvent(BaseEvent event) { + this.ws.send(Utils.toJson(event)); + } + + // 发送语音配置更新事件 + public void speechUpdate(SpeechEventUpdateEventData 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()); + } + + void handleEvent(WebSocket ws, String text) { + try { + JsonNode jsonNode = objectMapper.readTree(text); + String eventType = jsonNode.get("event_type").asText(); + + switch (eventType) { + case EventType.SPEECH_CREATED: + SpeechCreatedEvent speechCreatedEvent = + objectMapper.treeToValue(jsonNode, SpeechCreatedEvent.class); + handler.onSpeechCreated(WebsocketAudioSpeechClient.this, speechCreatedEvent); + break; + case EventType.SPEECH_UPDATED: + SpeechUpdatedEvent speechUpdatedEvent = + objectMapper.treeToValue(jsonNode, SpeechUpdatedEvent.class); + handler.onSpeechUpdated(WebsocketAudioSpeechClient.this, speechUpdatedEvent); + break; + case EventType.SPEECH_AUDIO_UPDATE: + SpeechAudioUpdateEvent audioUpdateEvent = + objectMapper.treeToValue(jsonNode, SpeechAudioUpdateEvent.class); + handler.onSpeechAudioUpdate(WebsocketAudioSpeechClient.this, audioUpdateEvent); + break; + case EventType.SPEECH_AUDIO_COMPLETED: + SpeechAudioCompletedEvent audioCompletedEvent = + objectMapper.treeToValue(jsonNode, SpeechAudioCompletedEvent.class); + handler.onSpeechAudioCompleted(WebsocketAudioSpeechClient.this, audioCompletedEvent); + break; + case EventType.INPUT_TEXT_BUFFER_COMPLETED: + InputTextBufferCompletedEvent bufferCompletedEvent = + objectMapper.treeToValue(jsonNode, InputTextBufferCompletedEvent.class); + handler.onInputTextBufferCompleted(WebsocketAudioSpeechClient.this, bufferCompletedEvent); + break; + case EventType.ERROR: + ErrorEvent errorEvent = objectMapper.treeToValue(jsonNode, ErrorEvent.class); + handler.onError(WebsocketAudioSpeechClient.this, errorEvent); + break; + default: + System.out.println("未知事件类型: " + eventType); + } + } catch (Exception e) { + handler.onClientException(WebsocketAudioSpeechClient.this, new RuntimeException(e)); + } + } + + public void close() { + this.ws.close(1000, null); + executorService.shutdown(); + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java new file mode 100644 index 00000000..dcdfdf4b --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java @@ -0,0 +1,90 @@ +package com.coze.openapi.service.service.websocket.chat; + +import com.coze.openapi.client.websocket.event.downstream.ChatCreatedEvent; +import com.coze.openapi.client.websocket.event.downstream.ChatUpdatedEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationAudioCompletedEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationAudioDeltaEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationAudioTranscriptCompletedEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationAudioTranscriptUpdateEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationChatCanceledEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationChatCompletedEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationChatCreatedEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationChatFailedEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationChatInProgressEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationChatRequiresActionEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationClearedEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationMessageCompletedEvent; +import com.coze.openapi.client.websocket.event.downstream.ConversationMessageDeltaEvent; +import com.coze.openapi.client.websocket.event.downstream.InputAudioBufferClearedEvent; +import com.coze.openapi.client.websocket.event.downstream.InputAudioBufferCompletedEvent; +import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; + +public abstract class WebsocketChatCallbackHandler extends BaseCallbackHandler { + public WebsocketChatCallbackHandler() {} + + // 对话连接成功事件 (chat.created) + public void onChatCreated(WebsocketChatClient client, ChatCreatedEvent event) {} + + // 对话配置成功事件 (chat.updated) + public void onChatUpdated(WebsocketChatClient client, ChatUpdatedEvent event) {} + + // 语音回复完成事件 (conversation.audio.completed) + public void onConversationAudioCompleted( + WebsocketChatClient client, ConversationAudioCompletedEvent event) {} + + // 增量语音事件 (conversation.audio.delta) + public void onConversationAudioDelta( + WebsocketChatClient client, ConversationAudioDeltaEvent event) {} + + // 语音转录完成事件 (conversation.audio_transcript.completed) + public void onConversationAudioTranscriptCompleted( + WebsocketChatClient client, ConversationAudioTranscriptCompletedEvent event) {} + + // 语音转录更新事件 (conversation.audio_transcript.update) + public void onConversationAudioTranscriptUpdate( + WebsocketChatClient client, ConversationAudioTranscriptUpdateEvent event) {} + + // 对话取消事件 (conversation.chat.canceled) + public void onConversationChatCanceled( + WebsocketChatClient client, ConversationChatCanceledEvent event) {} + + // 对话完成事件 (conversation.chat.completed) + public void onConversationChatCompleted( + WebsocketChatClient client, ConversationChatCompletedEvent event) {} + + // 对话创建事件 (conversation.chat.created) + public void onConversationChatCreated( + WebsocketChatClient client, ConversationChatCreatedEvent event) {} + + // 对话失败事件 (conversation.chat.failed) + public void onConversationChatFailed( + WebsocketChatClient client, ConversationChatFailedEvent event) {} + + // 对话正在处理事件 (conversation.chat.in_progress) + public void onConversationChatInProgress( + WebsocketChatClient client, ConversationChatInProgressEvent event) {} + + // 端插件事件 (conversation.chat.requires_action) + public void onConversationChatRequiresAction( + WebsocketChatClient client, ConversationChatRequiresActionEvent event) {} + + // 对话清除事件 (conversation.cleared) + public void onConversationCleared(WebsocketChatClient client, ConversationClearedEvent event) {} + + // 消息完成事件 (conversation.message.completed) + public void onConversationMessageCompleted( + WebsocketChatClient client, ConversationMessageCompletedEvent event) {} + + // 增量消息事件 (conversation.message.delta) + public void onConversationMessageDelta( + WebsocketChatClient client, ConversationMessageDeltaEvent event) {} + + // 语音清除成功事件 (input_audio_buffer.cleared) + public void onInputAudioBufferCleared( + WebsocketChatClient client, InputAudioBufferClearedEvent event) {} + + // 语音提交成功事件 (input_audio_buffer.completed) + public void onInputAudioBufferCompleted( + WebsocketChatClient client, InputAudioBufferCompletedEvent event) {} + +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java new file mode 100644 index 00000000..b515c9fd --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java @@ -0,0 +1,198 @@ +package com.coze.openapi.service.service.websocket.chat; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +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.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.BaseWebSocketListener; +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 class WebsocketChatClient { + + private final ObjectMapper objectMapper = Utils.getMapper(); + + private final WebSocket ws; + + private final WebsocketChatCallbackHandler handler; + + private static final String uri = "/v1/chat"; + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + public WebsocketChatClient(OkHttpClient client, String wsHost, WsChatReq req) { + String url = String.format("%s%s?bot_id=%s", wsHost, uri, req.getBotID()); + Request request = new Request.Builder().url(url).build(); + this.handler = req.getCallbackHandler(); + this.ws = client.newWebSocket(request, new BaseWebSocketListener<>(this::handleEvent, this.handler, this)); + } + + private void sendEvent(BaseEvent event) { + this.ws.send(Utils.toJson(event)); + } + + private 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(InputAudioBufferAppendEvent.Data data) { + this.sendEvent(InputAudioBufferAppendEvent.builder().data(data).build()); + } + + public void inputAudioBufferAppend(String data) { + this.sendEvent( + InputAudioBufferAppendEvent.builder() + .data(new InputAudioBufferAppendEvent.Data(data)) + .build()); + } + + public void inputAudioBufferClear() { + this.sendEvent(new InputAudioBufferClearEvent()); + } + + public void inputAudioBufferComplete() { + this.sendEvent(new InputAudioBufferCompleteEvent()); + } + + void handleEvent(WebSocket ws, String text) { + try { + // 解析 JSON + JsonNode jsonNode = objectMapper.readTree(text); + String eventType = jsonNode.get("event_type").asText(); + + switch (eventType) { + case EventType.CHAT_CREATED: + ChatCreatedEvent chatCreatedEvent = + objectMapper.treeToValue(jsonNode, ChatCreatedEvent.class); + handler.onChatCreated(WebsocketChatClient.this, chatCreatedEvent); + break; + case EventType.CHAT_UPDATED: + ChatUpdatedEvent chatUpdatedEvent = + objectMapper.treeToValue(jsonNode, ChatUpdatedEvent.class); + handler.onChatUpdated(WebsocketChatClient.this, chatUpdatedEvent); + break; + case EventType.CONVERSATION_AUDIO_COMPLETED: + ConversationAudioCompletedEvent audioCompletedEvent = + objectMapper.treeToValue(jsonNode, ConversationAudioCompletedEvent.class); + handler.onConversationAudioCompleted(WebsocketChatClient.this, audioCompletedEvent); + break; + case EventType.CONVERSATION_AUDIO_DELTA: + ConversationAudioDeltaEvent audioDeltaEvent = + objectMapper.treeToValue(jsonNode, ConversationAudioDeltaEvent.class); + handler.onConversationAudioDelta(WebsocketChatClient.this, audioDeltaEvent); + break; + case EventType.CONVERSATION_AUDIO_TRANSCRIPT_COMPLETED: + ConversationAudioTranscriptCompletedEvent transcriptCompletedEvent = + objectMapper.treeToValue(jsonNode, ConversationAudioTranscriptCompletedEvent.class); + handler.onConversationAudioTranscriptCompleted( + WebsocketChatClient.this, transcriptCompletedEvent); + break; + case EventType.CONVERSATION_AUDIO_TRANSCRIPT_UPDATE: + ConversationAudioTranscriptUpdateEvent transcriptUpdateEvent = + objectMapper.treeToValue(jsonNode, ConversationAudioTranscriptUpdateEvent.class); + handler.onConversationAudioTranscriptUpdate( + WebsocketChatClient.this, transcriptUpdateEvent); + break; + case EventType.CONVERSATION_CHAT_CANCELED: + ConversationChatCanceledEvent chatCanceledEvent = + objectMapper.treeToValue(jsonNode, ConversationChatCanceledEvent.class); + handler.onConversationChatCanceled(WebsocketChatClient.this, chatCanceledEvent); + break; + case EventType.CONVERSATION_CHAT_COMPLETED: + ConversationChatCompletedEvent chatCompletedEvent = + objectMapper.treeToValue(jsonNode, ConversationChatCompletedEvent.class); + handler.onConversationChatCompleted(WebsocketChatClient.this, chatCompletedEvent); + break; + case EventType.CONVERSATION_CHAT_CREATED: + ConversationChatCreatedEvent conversationChatCreatedEvent = + objectMapper.treeToValue(jsonNode, ConversationChatCreatedEvent.class); + handler.onConversationChatCreated(WebsocketChatClient.this, conversationChatCreatedEvent); + break; + case EventType.CONVERSATION_CHAT_FAILED: + ConversationChatFailedEvent chatFailedEvent = + objectMapper.treeToValue(jsonNode, ConversationChatFailedEvent.class); + handler.onConversationChatFailed(WebsocketChatClient.this, chatFailedEvent); + break; + case EventType.CONVERSATION_CHAT_IN_PROGRESS: + ConversationChatInProgressEvent chatInProgressEvent = + objectMapper.treeToValue(jsonNode, ConversationChatInProgressEvent.class); + handler.onConversationChatInProgress(WebsocketChatClient.this, chatInProgressEvent); + break; + case EventType.CONVERSATION_CHAT_REQUIRES_ACTION: + ConversationChatRequiresActionEvent chatRequiresActionEvent = + objectMapper.treeToValue(jsonNode, ConversationChatRequiresActionEvent.class); + handler.onConversationChatRequiresAction( + WebsocketChatClient.this, chatRequiresActionEvent); + break; + case EventType.CONVERSATION_CLEARED: + ConversationClearedEvent clearedEvent = + objectMapper.treeToValue(jsonNode, ConversationClearedEvent.class); + handler.onConversationCleared(WebsocketChatClient.this, clearedEvent); + break; + case EventType.CONVERSATION_MESSAGE_COMPLETED: + ConversationMessageCompletedEvent messageCompletedEvent = + objectMapper.treeToValue(jsonNode, ConversationMessageCompletedEvent.class); + handler.onConversationMessageCompleted(WebsocketChatClient.this, messageCompletedEvent); + break; + case EventType.CONVERSATION_MESSAGE_DELTA: + ConversationMessageDeltaEvent messageDeltaEvent = + objectMapper.treeToValue(jsonNode, ConversationMessageDeltaEvent.class); + handler.onConversationMessageDelta(WebsocketChatClient.this, messageDeltaEvent); + break; + case EventType.INPUT_AUDIO_BUFFER_CLEARED: + InputAudioBufferClearedEvent bufferClearedEvent = + objectMapper.treeToValue(jsonNode, InputAudioBufferClearedEvent.class); + handler.onInputAudioBufferCleared(WebsocketChatClient.this, bufferClearedEvent); + break; + case EventType.INPUT_AUDIO_BUFFER_COMPLETED: + InputAudioBufferCompletedEvent bufferCompletedEvent = + objectMapper.treeToValue(jsonNode, InputAudioBufferCompletedEvent.class); + handler.onInputAudioBufferCompleted(WebsocketChatClient.this, bufferCompletedEvent); + break; + case EventType.ERROR: + ErrorEvent errorEvent = objectMapper.treeToValue(jsonNode, ErrorEvent.class); + handler.onError(WebsocketChatClient.this, errorEvent); + break; + default: + // todo 用 log + System.out.println("未知事件类型: " + eventType); + } + } catch (Exception e) { + handler.onClientException(WebsocketChatClient.this, new RuntimeException(e)); + } + } + + public void close() { + this.ws.close(1000, null); + executorService.shutdown(); + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java new file mode 100644 index 00000000..8d4dfc70 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java @@ -0,0 +1,14 @@ +package com.coze.openapi.service.service.websocket.chat; + +import lombok.AllArgsConstructor; +import okhttp3.OkHttpClient; + +@AllArgsConstructor +public class WebsocketChatClientBuilder { + private final String baseUrl; + private final OkHttpClient httpClient; + + public WebsocketChatClient create(WsChatReq req) { + return new WebsocketChatClient(httpClient, baseUrl, req); + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WsChatReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WsChatReq.java new file mode 100644 index 00000000..178b10e8 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WsChatReq.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 WsChatReq extends BaseReq { + @NonNull private String botID; + @NonNull private WebsocketChatCallbackHandler 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..3314aba8 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseCallbackHandler.java @@ -0,0 +1,24 @@ +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/BaseWebSocketListener.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java new file mode 100644 index 00000000..056cb0d6 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java @@ -0,0 +1,51 @@ +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 BaseWebSocketListener extends WebSocketListener { + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final BiConsumer handleEvent; + private final BaseCallbackHandler callbackHandler; + private final T client; + + public BaseWebSocketListener(BiConsumer handleEvent, BaseCallbackHandler handler, T client) { + this.handleEvent = handleEvent; + this.callbackHandler = handler; + this.client = client; + } + + @Override + public void onOpen(WebSocket webSocket, okhttp3.Response response) { + } + + @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); + } +} 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/service/websocket/chat/WebsocketChatClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java new file mode 100644 index 00000000..2e74fb48 --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java @@ -0,0 +1,93 @@ +package com.coze.openapi.service.service.websocket.chat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.coze.openapi.client.websocket.event.downstream.ConversationAudioDeltaEvent; +import com.coze.openapi.service.utils.Utils; + +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +class WebsocketChatClientTest { + + @Mock private OkHttpClient mockOkHttpClient; + + @Mock private WebSocket mockWebSocket; + + @Mock private WebsocketChatCallbackHandler mockCallbackHandler; + + private WebsocketChatClient client; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); + + WsChatReq req = new WsChatReq("test-bot-id", mockCallbackHandler); + client = new WebsocketChatClient(mockOkHttpClient, "ws://test.com", req); + } + + @Test + void testHandleAudioDeltaEvent() { + // 准备模拟的音频数据 + String audioData = "base64EncodedAudioData"; + ConversationAudioDeltaEvent event = + ConversationAudioDeltaEvent.builder() + // .data(Message.builder().audio(audioData).build()) + .build(); + + // 模拟 WebSocket 接收到消息 + String eventJson = Utils.toJson(event); + client.handleEvent(mockWebSocket, eventJson); + + // 验证回调处理器被正确调用 + verify(mockCallbackHandler) + .onConversationAudioDelta(eq(client), any(ConversationAudioDeltaEvent.class)); + } + + @Test + void testSendEvent() { + // 捕获发送到 WebSocket 的数据 + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + + // 调用方法发送音频数据 + client.inputAudioBufferAppend("test-audio-data"); + + // 验证 WebSocket.send() 被调用,并捕获发送的消息 + verify(mockWebSocket).send(messageCaptor.capture()); + + // 验证发送的消息格式正确 + String sentMessage = messageCaptor.getValue(); + // assertThat(sentMessage).contains(EventType.INPUT_AUDIO_BUFFER_APPEND); + } + + @Test + void testClose() { + client.close(); + + // 验证 WebSocket 被正确关闭 + verify(mockWebSocket).close(eq(1000), isNull()); + } + + @Test + void testHandleUnknownEvent() { + String unknownEventJson = + "{\"event_type\":\"conversation.audio.delta\",\"id\":\"cd471e11-9a77-4f89-942d-da4650563fe1\",\"data\":{\"id\":\"7476315154641125428\",\"conversation_id\":\"7476315134009507875\",\"bot_id\":\"7433626593467514921\",\"role\":\"assistant\",\"type\":\"answer\",\"content\":\"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FQANABIAFAAPABQADgASABgAFAAWABcAFQAaABsAGwAdACQAIgAiACYAJAAnACUAKwAkACUAJQArAC0AMQAxACsALQAvADYAMAAtADMAMQAxADgAOgA1ADoANgA3ADQAPgBBAD0APwA9AEAAOgA9AD4AQgBDAEUASQBMAE4ATwBIAEoASgBLAE8ATgBLAFAATwBOAEsASQBBAEAARABDAEIARABAAEYATgBQAE4ATQBRAE0ASABMAEoATQBLAEoAUQBKAEcASgBMAEcARQA/AEcARAA/AD4APgA7AEMAQQA0AD4AOgA/AEIARwBDAEAARgBGAEsATQBGAEcASABEAEcARABCAEIAPwA9AEAAQABCADsANwA4ADgAPAA9AD0APAA/ADwAPgBBADgAMQA6ADoAOQA3ADUANQAxAC0AKgAqAC0AKQAnACUAIgAjACkAJQAlACIAIAAlACEAHwAfACQAIgAfACAAHQAaABYAFgAVABIADwAPAA4ACgADAAgADAAPABAAEwASABAAEgAQABUAEgARABAADwAPAAgADAAHAAcABAAIAAYABgAAAAAAAAAAAP///f8AAP//AwADAAEA+//8//n/9P/6//b/+f/4//X/9v/x//P/+P/1//f/+v/6//z//P/3//3/+//6//3/+//2//b/+f/7//n/+v/7//b/9v/0//X/9P/0//X/8//0//f/+f/5//X/9P/0//b/+P/0//T/9v/3//X/9P/v/+v/6//w/+3/7f/v/+j/8P/t/+z/7f/u/+7/6f/k/+//7P/r/+j/6v/w/+L/4P/j/9z/5P/j/9n/1v/S/9H/zv/P/8z/yP/K/8f/wv/M/8v/yf/L/8//0P/L/8n/yv/N/9P/0P/P/8v/zP/V/8z/xP/L/8z/x//K/8n/yv/J/8j/y//J/8z/y//N/8z/x//M/87/y//O/8//0v/T/9r/1v/N/9b/1f/Q/9H/0v/R/8//zf/R/83/yv/M/8//0f/N/9P/0f/P/9H/1f/V/9H/2P/U/9f/1v/b/9z/2P/V/9b/1f/S/9b/0f/O/87/0P/R/9f/2P/f/+H/3P/g/+H/2//Z/93/4//h/9z/3P/d/+b/5f/i/93/3v/a/93/4f/d/9//3//d/9//4//e/+D/5f/m/+b/5f/q/+z/5v/k/+n/6f/l/+H/5P/n/+v/6P/m/+r/5//u//D/6P/y//D/7//s//P/9f/z//T/9f/z/+z/7//x//n/+P/w//n/9//4//z/AAD7//n////+//r/9f8AAAAA+//7/wEAAAD/////AAAGAAIABwADAAUABQAGAAUABgAHAAoADwAPABEAEQAPAA8AEAAUABIADQAOABcAGQAXABcAFAAYABkAGAAZABcAHAAcACAAIAAgACIAIgAiACEAIAAmAB8AHgAlACUAKwAlACYAJQAnACkAKQArACcAJQAoADMALAAuACwAKQArACsAKQA0ADEAKwArACEALAAtAC0AKAAqACYAKwAvAC0AKwAvACgAKgArACYAKwAqAC4ALQAuADIAKgAtACoAKQAqADAAMgAvACwALgAnAC0ALQAtACsAMQAoACoA\",\"content_type\":\"audio\",\"chat_id\":\"7476308073020276770\",\"section_id\":\"7476315134009507875\"},\"detail\":{\"logid\":\"02174071526740100000000000000000000ffff0a807486d6926f\"}}\n"; + client.handleEvent(mockWebSocket, unknownEventJson); + + // 验证未知事件类型被正确处理 + verifyNoInteractions(mockCallbackHandler); + } + + @Test + void testHandleInvalidJson() { + String invalidJson = "invalid json"; + } +} 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..eed63fd9 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 @@ -25,7 +25,7 @@ public class UtilsTest { @BeforeEach public void setUp() { - objectMapper = Utils.defaultObjectMapper(); + objectMapper = Utils.getMapper(); } public static class TestResponse extends BaseResp {} 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/chat/ChatExample.java b/example/src/main/java/example/websocket/chat/ChatExample.java new file mode 100644 index 00000000..cd9184fa --- /dev/null +++ b/example/src/main/java/example/websocket/chat/ChatExample.java @@ -0,0 +1,179 @@ +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.Base64; +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.WebsocketChatCallbackHandler; +import com.coze.openapi.service.service.websocket.chat.WebsocketChatClient; +import com.coze.openapi.service.service.websocket.chat.WsChatReq; +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 describes how to use the chat interface to initiate conversations, +poll the status of the conversation, and obtain the messages after the conversation is completed. +* */ +public class ChatExample { + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + private static class Weather { + @JsonProperty("weather") + private String weather; + } + + private static class CallbackHandler extends WebsocketChatCallbackHandler { + private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 + + public CallbackHandler() { + super(); + } + + @Override + public void onChatCreated(WebsocketChatClient client, ChatCreatedEvent event) { + System.out.println(event); + // client.sendEvent(new BaseEvent()); + } + + @Override + public void onConversationMessageDelta( + WebsocketChatClient client, ConversationMessageDeltaEvent event) { + System.out.printf("Revieve: %s\n", event.getData().getContent()); + } + + @Override + public void onError(WebsocketChatClient client, ErrorEvent event) { + System.out.println(event); + } + + @Override + public void onInputAudioBufferCompleted( + WebsocketChatClient client, InputAudioBufferCompletedEvent event) { + System.out.println("========= Input Audio Buffer Completed ========="); + System.out.println(event); + } + + @Override + public void onConversationAudioCompleted( + WebsocketChatClient client, ConversationAudioCompletedEvent event) { + try { + ExampleUtils.writePcmToWavFile(buffer.array(), "output.wav"); + System.out.println("========= Output Audio Completed ========="); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onClientException(WebsocketChatClient client, Throwable e) { + e.printStackTrace(); + } + + @Override + public void onConversationAudioDelta( + WebsocketChatClient client, ConversationAudioDeltaEvent event) { + byte[] audioData = event.getData().getAudio(); + buffer.put(audioData); + } + + @Override + public void onConversationChatRequiresAction( + WebsocketChatClient 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(); + + WebsocketChatClient client = null; + try { + client = coze.websocket().chat().create(new WsChatReq(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) { + // 将读取到的字节转换为 base64 编码 + String base64Data = Base64.getEncoder().encodeToString(Arrays.copyOf(buffer, bytesRead)); + client.inputAudioBufferAppend(base64Data); + } + client.inputAudioBufferComplete(); + } catch (IOException e) { + e.printStackTrace(); + } + + TimeUnit.SECONDS.sleep(100); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (client != null) { + client.close(); + } + coze.shutdownExecutor(); + } + } +} From c4dfaf79af66afdf68fa28fac84728c298b14e26 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 15:17:21 +0800 Subject: [PATCH 03/50] save ws transcription Change-Id: Iacf33262e6d5fb0e0537a834b3726686be854e08 --- .gitignore | 1 + .../client/websocket/event/EventType.java | 2 + .../InputTextBufferCompletedEvent.java | 10 +- .../downstream/SpeechAudioCompletedEvent.java | 8 +- .../downstream/SpeechAudioUpdateEvent.java | 48 ++--- .../event/downstream/SpeechCreatedEvent.java | 8 +- .../event/downstream/SpeechUpdatedEvent.java | 14 +- .../TranscriptionsCreatedEvent.java | 21 +++ .../TranscriptionsMessageCompletedEvent.java | 22 +++ .../TranscriptionsMessageUpdateEvent.java | 34 ++++ .../TranscriptionsUpdatedEvent.java | 26 +++ ...ntData.java => SpeechUpdateEventData.java} | 7 +- .../model/TranscriptionsUpdateEventData.java | 15 ++ .../upstream/InputAudioBufferAppendEvent.java | 1 - .../event/upstream/SpeechUpdateEvent.java | 14 +- .../upstream/TranscriptionsUpdateEvent.java | 26 +++ .../service/websocket/WebsocketClient.java | 8 + .../websocket/audio/WebsocketAudioClient.java | 26 +++ .../speech/WebsocketAudioSpeechBuilder.java | 14 ++ .../WebsocketAudioSpeechCallbackHandler.java | 28 +-- .../speech/WebsocketAudioSpeechClient.java | 147 ++++++++-------- .../speech/WebsocketAudioSpeechCreateReq.java | 15 ++ .../WebsocketAudioTranscriptionsBuilder.java | 14 ++ ...ketAudioTranscriptionsCallbackHandler.java | 38 ++++ .../WebsocketAudioTranscriptionsClient.java | 103 +++++++++++ ...WebsocketAudioTranscriptionsCreateReq.java | 15 ++ .../chat/WebsocketChatCallbackHandler.java | 4 +- .../websocket/chat/WebsocketChatClient.java | 67 ++++--- .../chat/WebsocketChatClientBuilder.java | 2 +- ...atReq.java => WebsocketChatCreateReq.java} | 2 +- .../websocket/common/BaseCallbackHandler.java | 25 ++- .../common/BaseWebSocketListener.java | 15 +- .../websocket/common/BaseWebsocketClient.java | 58 +++++++ .../chat/WebsocketChatClientTest.java | 2 +- .../speech/WebsocketAudioSpeechExample.java | 133 ++++++++++++++ .../WebsocketTranscriptionsExample.java | 164 ++++++++++++++++++ .../example/websocket/chat/ChatExample.java | 5 +- 37 files changed, 938 insertions(+), 204 deletions(-) create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsCreatedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsMessageCompletedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsMessageUpdateEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/TranscriptionsUpdatedEvent.java rename api/src/main/java/com/coze/openapi/client/websocket/event/model/{SpeechEventUpdateEventData.java => SpeechUpdateEventData.java} (64%) create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/TranscriptionsUpdateEventData.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/upstream/TranscriptionsUpdateEvent.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketAudioClient.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechBuilder.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCreateReq.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsBuilder.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCallbackHandler.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCreateReq.java rename api/src/main/java/com/coze/openapi/service/service/websocket/chat/{WsChatReq.java => WebsocketChatCreateReq.java} (86%) create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java create mode 100644 example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java create mode 100644 example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java 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/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 index 87c6bb5b..943173f0 100644 --- 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 @@ -41,6 +41,8 @@ public class EventType { // 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 = 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 index 059500c5..7c5a939a 100644 --- 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 @@ -14,9 +14,7 @@ @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; - - -} \ No newline at end of file + @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 index 66323fef..40e14b43 100644 --- 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 @@ -18,7 +18,7 @@ @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; -} \ No newline at end of file + @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 index d968a276..968a897a 100644 --- 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 @@ -1,5 +1,7 @@ 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; @@ -7,8 +9,6 @@ import lombok.*; import lombok.experimental.SuperBuilder; -import java.util.Base64; - @Data @SuperBuilder @NoArgsConstructor @@ -17,25 +17,25 @@ @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; - - @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); - } -} \ No newline at end of file + @JsonProperty("event_type") + @Builder.Default + private final String eventType = EventType.SPEECH_AUDIO_UPDATE; + + @JsonProperty("data") + private Data 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 index 07d4b8cc..0e8d151e 100644 --- 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 @@ -18,7 +18,7 @@ @ToString(callSuper = true) // event_type: speech.created public class SpeechCreatedEvent extends BaseEvent { - @JsonProperty("event_type") - @Builder.Default - private final String eventType = EventType.SPEECH_CREATED; -} \ No newline at end of file + @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 index 3475bad7..16b64b52 100644 --- 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 @@ -2,7 +2,7 @@ import com.coze.openapi.client.websocket.common.BaseEvent; import com.coze.openapi.client.websocket.event.EventType; -import com.coze.openapi.client.websocket.event.model.SpeechEventUpdateEventData; +import com.coze.openapi.client.websocket.event.model.SpeechUpdateEventData; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -21,10 +21,10 @@ @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("event_type") + @Builder.Default + private final String eventType = EventType.SPEECH_UPDATED; - @JsonProperty("data") - private SpeechEventUpdateEventData data; -} \ No newline at end of file + @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/SpeechEventUpdateEventData.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechUpdateEventData.java similarity index 64% rename from api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechEventUpdateEventData.java rename to api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechUpdateEventData.java index 2820fc5f..cf64e621 100644 --- a/api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechEventUpdateEventData.java +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/SpeechUpdateEventData.java @@ -1,6 +1,7 @@ package com.coze.openapi.client.websocket.event.model; import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.*; @Data @@ -8,7 +9,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode -public class SpeechEventUpdateEventData { - @JsonProperty("output_audio") - private OutputAudio outputAudio; +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/upstream/InputAudioBufferAppendEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/upstream/InputAudioBufferAppendEvent.java index 3cc4b299..d2208217 100644 --- 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 @@ -2,7 +2,6 @@ import com.coze.openapi.client.websocket.common.BaseEvent; import com.coze.openapi.client.websocket.event.EventType; -import com.coze.openapi.service.utils.Utils; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; 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 index 38ef20fe..ba715011 100644 --- 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 @@ -2,7 +2,7 @@ import com.coze.openapi.client.websocket.common.BaseEvent; import com.coze.openapi.client.websocket.event.EventType; -import com.coze.openapi.client.websocket.event.model.SpeechEventUpdateEventData; +import com.coze.openapi.client.websocket.event.model.SpeechUpdateEventData; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -21,10 +21,10 @@ @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("event_type") + @Builder.Default + private final String eventType = EventType.SPEECH_UPDATE; - @JsonProperty("data") - private SpeechEventUpdateEventData data; -} \ No newline at end of file + @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/service/service/websocket/WebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java index f7a72c3c..5d0f6d6f 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java @@ -1,5 +1,6 @@ package com.coze.openapi.service.service.websocket; +import com.coze.openapi.service.service.websocket.audio.WebsocketAudioClient; import com.coze.openapi.service.service.websocket.chat.WebsocketChatClientBuilder; import okhttp3.OkHttpClient; @@ -8,11 +9,18 @@ public class WebsocketClient { private final WebsocketChatClientBuilder chat; + private final WebsocketAudioClient audio; + public WebsocketClient(OkHttpClient client, String baseURL) { this.chat = new WebsocketChatClientBuilder(baseURL, client); + this.audio = new WebsocketAudioClient(baseURL, client); } public WebsocketChatClientBuilder chat() { return chat; } + + public WebsocketAudioClient audio() { + return audio; + } } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketAudioClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketAudioClient.java new file mode 100644 index 00000000..95313841 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketAudioClient.java @@ -0,0 +1,26 @@ +package com.coze.openapi.service.service.websocket.audio; + +import com.coze.openapi.service.service.websocket.audio.speech.WebsocketAudioSpeechBuilder; +import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketAudioTranscriptionsBuilder; + +import okhttp3.OkHttpClient; + +public class WebsocketAudioClient { + + private final WebsocketAudioSpeechBuilder websocketAudioSpeechBuilder; + private final WebsocketAudioTranscriptionsBuilder websocketAudioTranscriptionsBuilder; + + public WebsocketAudioClient(String baseUrl, OkHttpClient httpClient) { + this.websocketAudioSpeechBuilder = new WebsocketAudioSpeechBuilder(baseUrl, httpClient); + this.websocketAudioTranscriptionsBuilder = + new WebsocketAudioTranscriptionsBuilder(baseUrl, httpClient); + } + + public WebsocketAudioSpeechBuilder speech() { + return websocketAudioSpeechBuilder; + } + + public WebsocketAudioTranscriptionsBuilder transcriptions() { + return websocketAudioTranscriptionsBuilder; + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechBuilder.java new file mode 100644 index 00000000..ed8bcb87 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechBuilder.java @@ -0,0 +1,14 @@ +package com.coze.openapi.service.service.websocket.audio.speech; + +import lombok.AllArgsConstructor; +import okhttp3.OkHttpClient; + +@AllArgsConstructor +public class WebsocketAudioSpeechBuilder { + private final String baseUrl; + private final OkHttpClient httpClient; + + public WebsocketAudioSpeechClient create(WebsocketAudioSpeechCreateReq req) { + return new WebsocketAudioSpeechClient(httpClient, baseUrl, req); + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java index 0839a95d..b6601f84 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java @@ -7,21 +7,25 @@ import com.coze.openapi.client.websocket.event.downstream.SpeechUpdatedEvent; import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; -public abstract class WebsocketAudioSpeechCallbackHandler extends BaseCallbackHandler { - public WebsocketAudioSpeechCallbackHandler() {} +public abstract class WebsocketAudioSpeechCallbackHandler + extends BaseCallbackHandler { + public WebsocketAudioSpeechCallbackHandler() {} - // 语音创建成功事件 (speech.created) - public void onSpeechCreated(WebsocketAudioSpeechClient client, SpeechCreatedEvent event) {} + // 语音创建成功事件 (speech.created) + public void onSpeechCreated(WebsocketAudioSpeechClient client, SpeechCreatedEvent event) {} - // 语音配置更新事件 (speech.update) - public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEvent event) {} + // 语音配置更新事件 (speech.update) + public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEvent event) {} - // 语音数据更新事件 (speech.audio.update) - public void onSpeechAudioUpdate(WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) {} + // 语音数据更新事件 (speech.audio.update) + public void onSpeechAudioUpdate( + WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) {} - // 语音数据完成事件 (speech.audio.completed) - public void onSpeechAudioCompleted(WebsocketAudioSpeechClient client, SpeechAudioCompletedEvent event) {} + // 语音数据完成事件 (speech.audio.completed) + public void onSpeechAudioCompleted( + WebsocketAudioSpeechClient client, SpeechAudioCompletedEvent event) {} - // 文本缓冲区完成事件 (input_text_buffer.completed) - public void onInputTextBufferCompleted(WebsocketAudioSpeechClient client, InputTextBufferCompletedEvent event) {} + // 文本缓冲区完成事件 (input_text_buffer.completed) + public void onInputTextBufferCompleted( + WebsocketAudioSpeechClient client, InputTextBufferCompletedEvent event) {} } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java index 7adf076e..39c45a8a 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java @@ -3,98 +3,95 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import com.coze.openapi.client.websocket.common.BaseEvent; import com.coze.openapi.client.websocket.event.EventType; import com.coze.openapi.client.websocket.event.downstream.*; -import com.coze.openapi.client.websocket.event.model.SpeechEventUpdateEventData; +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.BaseWebSocketListener; +import com.coze.openapi.service.service.websocket.common.BaseWebsocketClient; 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 class WebsocketAudioSpeechClient { - private final ObjectMapper objectMapper = Utils.getMapper(); - private final WebSocket ws; - private final WebsocketAudioSpeechCallbackHandler handler; - private static final String uri = "/v1/audio/speech"; - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); +public class WebsocketAudioSpeechClient extends BaseWebsocketClient { + private final ObjectMapper objectMapper = Utils.getMapper(); + private final WebsocketAudioSpeechCallbackHandler handler; + private static final String uri = "/v1/audio/speech"; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - public WebsocketAudioSpeechClient(OkHttpClient client, String wsHost, WebsocketAudioSpeechCallbackHandler handler) { - String url = String.format("%s%s?voice_id=%s", wsHost, uri, "req.getVoiceID()"); - Request request = new Request.Builder().url(url).build(); - this.handler = handler; - this.ws = client.newWebSocket(request, new BaseWebSocketListener<>(this::handleEvent, this.handler, this)); - } + protected WebsocketAudioSpeechClient( + OkHttpClient client, String wsHost, WebsocketAudioSpeechCreateReq req) { + super(client, buildUrl(wsHost, uri), req.getCallbackHandler()); + this.handler = req.getCallbackHandler(); + } - private void sendEvent(BaseEvent event) { - this.ws.send(Utils.toJson(event)); - } + protected static String buildUrl(String wsHost, String uri) { + return String.format("%s%s", wsHost, uri); + } - // 发送语音配置更新事件 - public void speechUpdate(SpeechEventUpdateEventData data) { - this.sendEvent(SpeechUpdateEvent.builder().data(data).build()); - } + // 发送语音配置更新事件 + public void speechUpdate(SpeechUpdateEventData data) { + this.sendEvent(SpeechUpdateEvent.builder().data(data).build()); + } - // 发送文本缓冲区追加事件 - public void inputTextBufferAppend(String data) { - this.sendEvent(InputTextBufferAppendEvent.of(data)); - } + // 发送文本缓冲区追加事件 + public void inputTextBufferAppend(String data) { + this.sendEvent(InputTextBufferAppendEvent.of(data)); + } - // 发送文本缓冲区完成事件 - public void inputTextBufferComplete() { - this.sendEvent(new InputTextBufferCompleteEvent()); - } + // 发送文本缓冲区完成事件 + public void inputTextBufferComplete() { + this.sendEvent(new InputTextBufferCompleteEvent()); + } - void handleEvent(WebSocket ws, String text) { - try { - JsonNode jsonNode = objectMapper.readTree(text); - String eventType = jsonNode.get("event_type").asText(); + @Override + protected void handleEvent(WebSocket ws, String text) { + try { + JsonNode jsonNode = objectMapper.readTree(text); + String eventType = jsonNode.get("event_type").asText(); - switch (eventType) { - case EventType.SPEECH_CREATED: - SpeechCreatedEvent speechCreatedEvent = - objectMapper.treeToValue(jsonNode, SpeechCreatedEvent.class); - handler.onSpeechCreated(WebsocketAudioSpeechClient.this, speechCreatedEvent); - break; - case EventType.SPEECH_UPDATED: - SpeechUpdatedEvent speechUpdatedEvent = - objectMapper.treeToValue(jsonNode, SpeechUpdatedEvent.class); - handler.onSpeechUpdated(WebsocketAudioSpeechClient.this, speechUpdatedEvent); - break; - case EventType.SPEECH_AUDIO_UPDATE: - SpeechAudioUpdateEvent audioUpdateEvent = - objectMapper.treeToValue(jsonNode, SpeechAudioUpdateEvent.class); - handler.onSpeechAudioUpdate(WebsocketAudioSpeechClient.this, audioUpdateEvent); - break; - case EventType.SPEECH_AUDIO_COMPLETED: - SpeechAudioCompletedEvent audioCompletedEvent = - objectMapper.treeToValue(jsonNode, SpeechAudioCompletedEvent.class); - handler.onSpeechAudioCompleted(WebsocketAudioSpeechClient.this, audioCompletedEvent); - break; - case EventType.INPUT_TEXT_BUFFER_COMPLETED: - InputTextBufferCompletedEvent bufferCompletedEvent = - objectMapper.treeToValue(jsonNode, InputTextBufferCompletedEvent.class); - handler.onInputTextBufferCompleted(WebsocketAudioSpeechClient.this, bufferCompletedEvent); - break; - case EventType.ERROR: - ErrorEvent errorEvent = objectMapper.treeToValue(jsonNode, ErrorEvent.class); - handler.onError(WebsocketAudioSpeechClient.this, errorEvent); - break; - default: - System.out.println("未知事件类型: " + eventType); - } - } catch (Exception e) { - handler.onClientException(WebsocketAudioSpeechClient.this, new RuntimeException(e)); - } + 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: + System.out.println("未知事件类型: " + eventType); + } + } catch (Exception e) { + handler.onClientException(this, new RuntimeException(e)); } + } - public void close() { - this.ws.close(1000, null); - executorService.shutdown(); - } + public void close() { + this.close(); + executorService.shutdown(); + } } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCreateReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCreateReq.java new file mode 100644 index 00000000..731d6fd0 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCreateReq.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 WebsocketAudioSpeechCreateReq extends BaseReq { + @NonNull private WebsocketAudioSpeechCallbackHandler callbackHandler; +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsBuilder.java new file mode 100644 index 00000000..b1965bd3 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsBuilder.java @@ -0,0 +1,14 @@ +package com.coze.openapi.service.service.websocket.audio.transcriptions; + +import lombok.AllArgsConstructor; +import okhttp3.OkHttpClient; + +@AllArgsConstructor +public class WebsocketAudioTranscriptionsBuilder { + private final String baseUrl; + private final OkHttpClient httpClient; + + public WebsocketAudioTranscriptionsClient create(WebsocketAudioTranscriptionsCreateReq req) { + return new WebsocketAudioTranscriptionsClient(httpClient, baseUrl, req); + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCallbackHandler.java new file mode 100644 index 00000000..8bb46988 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCallbackHandler.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 WebsocketAudioTranscriptionsCallbackHandler + extends BaseCallbackHandler { + public WebsocketAudioTranscriptionsCallbackHandler() {} + + // 转录创建事件 (transcriptions.created) + public void onTranscriptionsCreated( + WebsocketAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) {} + + // 转录配置更新事件 (transcriptions.updated) + public void onTranscriptionsUpdated( + WebsocketAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) {} + + // 转录消息更新事件 (transcriptions.message.update) + public void onTranscriptionsMessageUpdate( + WebsocketAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) {} + + // 转录消息完成事件 (transcriptions.message.completed) + public void onTranscriptionsMessageCompleted( + WebsocketAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) {} + + // 语音缓冲区清除事件 (input_audio_buffer.cleared) + public void onInputAudioBufferCleared( + WebsocketAudioTranscriptionsClient client, InputAudioBufferClearedEvent event) {} + + // 语音缓冲区完成事件 (input_audio_buffer.completed) + public void onInputAudioBufferCompleted( + WebsocketAudioTranscriptionsClient client, InputAudioBufferCompletedEvent event) {} +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java new file mode 100644 index 00000000..937ef8a4 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java @@ -0,0 +1,103 @@ +package com.coze.openapi.service.service.websocket.audio.transcriptions; + +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.BaseWebsocketClient; +import com.fasterxml.jackson.databind.JsonNode; + +import okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketAudioTranscriptionsClient extends BaseWebsocketClient { + private final WebsocketAudioTranscriptionsCallbackHandler handler; + private static final String uri = "/v1/audio/transcriptions"; + + protected WebsocketAudioTranscriptionsClient( + OkHttpClient client, String wsHost, WebsocketAudioTranscriptionsCreateReq req) { + super(client, buildUrl(wsHost, uri), req.getCallbackHandler()); + this.handler = req.getCallbackHandler(); + } + + protected static String buildUrl(String wsHost, String uri) { + 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.sendEvent(InputAudioBufferAppendEvent.of(data)); + } + + public void inputAudioBufferAppend(InputAudioBufferAppendEvent.Data data) { + this.sendEvent(InputAudioBufferAppendEvent.builder().data(data).build()); + } + + // 发送语音缓冲区清除事件 + public void inputAudioBufferClear() { + this.sendEvent(new InputAudioBufferClearEvent()); + } + + // 发送语音缓冲区完成事件 + public void inputAudioBufferComplete() { + this.sendEvent(new InputAudioBufferCompleteEvent()); + } + + @Override + protected void handleEvent(WebSocket ws, String text) { + try { + JsonNode jsonNode = objectMapper.readTree(text); + String eventType = jsonNode.get("event_type").asText(); + + 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: + System.out.println("未知事件类型: " + eventType); + } + } catch (Exception e) { + handler.onClientException(this, new RuntimeException(e)); + } + } +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCreateReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCreateReq.java new file mode 100644 index 00000000..900d96c8 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCreateReq.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 WebsocketAudioTranscriptionsCreateReq extends BaseReq { + @NonNull private WebsocketAudioTranscriptionsCallbackHandler callbackHandler; +} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java index dcdfdf4b..2f18cc12 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java @@ -19,7 +19,8 @@ import com.coze.openapi.client.websocket.event.downstream.InputAudioBufferCompletedEvent; import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; -public abstract class WebsocketChatCallbackHandler extends BaseCallbackHandler { +public abstract class WebsocketChatCallbackHandler + extends BaseCallbackHandler { public WebsocketChatCallbackHandler() {} // 对话连接成功事件 (chat.created) @@ -86,5 +87,4 @@ public void onInputAudioBufferCleared( // 语音提交成功事件 (input_audio_buffer.completed) public void onInputAudioBufferCompleted( WebsocketChatClient client, InputAudioBufferCompletedEvent event) {} - } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java index b515c9fd..42146d79 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java @@ -4,7 +4,6 @@ import java.util.concurrent.Executors; 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.coze.openapi.client.websocket.event.downstream.*; import com.coze.openapi.client.websocket.event.model.ChatUpdateEventData; @@ -12,36 +11,34 @@ 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.BaseWebSocketListener; +import com.coze.openapi.service.service.websocket.common.BaseWebsocketClient; 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 class WebsocketChatClient { +public class WebsocketChatClient extends BaseWebsocketClient { private final ObjectMapper objectMapper = Utils.getMapper(); - private final WebSocket ws; - private final WebsocketChatCallbackHandler handler; private static final String uri = "/v1/chat"; + private final WebsocketChatCreateReq req; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - public WebsocketChatClient(OkHttpClient client, String wsHost, WsChatReq req) { - String url = String.format("%s%s?bot_id=%s", wsHost, uri, req.getBotID()); - Request request = new Request.Builder().url(url).build(); + protected WebsocketChatClient(OkHttpClient client, String wsHost, WebsocketChatCreateReq req) { + super(client, buildUrl(wsHost, uri, req), req.getCallbackHandler()); this.handler = req.getCallbackHandler(); - this.ws = client.newWebSocket(request, new BaseWebSocketListener<>(this::handleEvent, this.handler, this)); + this.req = req; } - private void sendEvent(BaseEvent event) { - this.ws.send(Utils.toJson(event)); + protected static String buildUrl(String wsHost, String uri, WebsocketChatCreateReq req) { + return String.format("%s%s?bot_id=%s", wsHost, uri, req.getBotID()); } private void chatUpdate(ChatUpdateEventData data) { @@ -69,10 +66,7 @@ public void inputAudioBufferAppend(InputAudioBufferAppendEvent.Data data) { } public void inputAudioBufferAppend(String data) { - this.sendEvent( - InputAudioBufferAppendEvent.builder() - .data(new InputAudioBufferAppendEvent.Data(data)) - .build()); + this.sendEvent(InputAudioBufferAppendEvent.of(data)); } public void inputAudioBufferClear() { @@ -83,7 +77,8 @@ public void inputAudioBufferComplete() { this.sendEvent(new InputAudioBufferCompleteEvent()); } - void handleEvent(WebSocket ws, String text) { + @Override + protected void handleEvent(WebSocket ws, String text) { try { // 解析 JSON JsonNode jsonNode = objectMapper.readTree(text); @@ -93,101 +88,101 @@ void handleEvent(WebSocket ws, String text) { case EventType.CHAT_CREATED: ChatCreatedEvent chatCreatedEvent = objectMapper.treeToValue(jsonNode, ChatCreatedEvent.class); - handler.onChatCreated(WebsocketChatClient.this, chatCreatedEvent); + handler.onChatCreated(this, chatCreatedEvent); break; case EventType.CHAT_UPDATED: ChatUpdatedEvent chatUpdatedEvent = objectMapper.treeToValue(jsonNode, ChatUpdatedEvent.class); - handler.onChatUpdated(WebsocketChatClient.this, chatUpdatedEvent); + handler.onChatUpdated(this, chatUpdatedEvent); break; case EventType.CONVERSATION_AUDIO_COMPLETED: ConversationAudioCompletedEvent audioCompletedEvent = objectMapper.treeToValue(jsonNode, ConversationAudioCompletedEvent.class); - handler.onConversationAudioCompleted(WebsocketChatClient.this, audioCompletedEvent); + handler.onConversationAudioCompleted(this, audioCompletedEvent); break; case EventType.CONVERSATION_AUDIO_DELTA: ConversationAudioDeltaEvent audioDeltaEvent = objectMapper.treeToValue(jsonNode, ConversationAudioDeltaEvent.class); - handler.onConversationAudioDelta(WebsocketChatClient.this, audioDeltaEvent); + handler.onConversationAudioDelta(this, audioDeltaEvent); break; case EventType.CONVERSATION_AUDIO_TRANSCRIPT_COMPLETED: ConversationAudioTranscriptCompletedEvent transcriptCompletedEvent = objectMapper.treeToValue(jsonNode, ConversationAudioTranscriptCompletedEvent.class); handler.onConversationAudioTranscriptCompleted( - WebsocketChatClient.this, transcriptCompletedEvent); + this, transcriptCompletedEvent); break; case EventType.CONVERSATION_AUDIO_TRANSCRIPT_UPDATE: ConversationAudioTranscriptUpdateEvent transcriptUpdateEvent = objectMapper.treeToValue(jsonNode, ConversationAudioTranscriptUpdateEvent.class); handler.onConversationAudioTranscriptUpdate( - WebsocketChatClient.this, transcriptUpdateEvent); + this, transcriptUpdateEvent); break; case EventType.CONVERSATION_CHAT_CANCELED: ConversationChatCanceledEvent chatCanceledEvent = objectMapper.treeToValue(jsonNode, ConversationChatCanceledEvent.class); - handler.onConversationChatCanceled(WebsocketChatClient.this, chatCanceledEvent); + handler.onConversationChatCanceled(this, chatCanceledEvent); break; case EventType.CONVERSATION_CHAT_COMPLETED: ConversationChatCompletedEvent chatCompletedEvent = objectMapper.treeToValue(jsonNode, ConversationChatCompletedEvent.class); - handler.onConversationChatCompleted(WebsocketChatClient.this, chatCompletedEvent); + handler.onConversationChatCompleted(this, chatCompletedEvent); break; case EventType.CONVERSATION_CHAT_CREATED: ConversationChatCreatedEvent conversationChatCreatedEvent = objectMapper.treeToValue(jsonNode, ConversationChatCreatedEvent.class); - handler.onConversationChatCreated(WebsocketChatClient.this, conversationChatCreatedEvent); + handler.onConversationChatCreated(this, conversationChatCreatedEvent); break; case EventType.CONVERSATION_CHAT_FAILED: ConversationChatFailedEvent chatFailedEvent = objectMapper.treeToValue(jsonNode, ConversationChatFailedEvent.class); - handler.onConversationChatFailed(WebsocketChatClient.this, chatFailedEvent); + handler.onConversationChatFailed(this, chatFailedEvent); break; case EventType.CONVERSATION_CHAT_IN_PROGRESS: ConversationChatInProgressEvent chatInProgressEvent = objectMapper.treeToValue(jsonNode, ConversationChatInProgressEvent.class); - handler.onConversationChatInProgress(WebsocketChatClient.this, chatInProgressEvent); + handler.onConversationChatInProgress(this, chatInProgressEvent); break; case EventType.CONVERSATION_CHAT_REQUIRES_ACTION: ConversationChatRequiresActionEvent chatRequiresActionEvent = objectMapper.treeToValue(jsonNode, ConversationChatRequiresActionEvent.class); handler.onConversationChatRequiresAction( - WebsocketChatClient.this, chatRequiresActionEvent); + this, chatRequiresActionEvent); break; case EventType.CONVERSATION_CLEARED: ConversationClearedEvent clearedEvent = objectMapper.treeToValue(jsonNode, ConversationClearedEvent.class); - handler.onConversationCleared(WebsocketChatClient.this, clearedEvent); + handler.onConversationCleared(this, clearedEvent); break; case EventType.CONVERSATION_MESSAGE_COMPLETED: ConversationMessageCompletedEvent messageCompletedEvent = objectMapper.treeToValue(jsonNode, ConversationMessageCompletedEvent.class); - handler.onConversationMessageCompleted(WebsocketChatClient.this, messageCompletedEvent); + handler.onConversationMessageCompleted(this, messageCompletedEvent); break; case EventType.CONVERSATION_MESSAGE_DELTA: ConversationMessageDeltaEvent messageDeltaEvent = objectMapper.treeToValue(jsonNode, ConversationMessageDeltaEvent.class); - handler.onConversationMessageDelta(WebsocketChatClient.this, messageDeltaEvent); + handler.onConversationMessageDelta(this, messageDeltaEvent); break; case EventType.INPUT_AUDIO_BUFFER_CLEARED: InputAudioBufferClearedEvent bufferClearedEvent = objectMapper.treeToValue(jsonNode, InputAudioBufferClearedEvent.class); - handler.onInputAudioBufferCleared(WebsocketChatClient.this, bufferClearedEvent); + handler.onInputAudioBufferCleared(this, bufferClearedEvent); break; case EventType.INPUT_AUDIO_BUFFER_COMPLETED: InputAudioBufferCompletedEvent bufferCompletedEvent = objectMapper.treeToValue(jsonNode, InputAudioBufferCompletedEvent.class); - handler.onInputAudioBufferCompleted(WebsocketChatClient.this, bufferCompletedEvent); + handler.onInputAudioBufferCompleted(this, bufferCompletedEvent); break; case EventType.ERROR: ErrorEvent errorEvent = objectMapper.treeToValue(jsonNode, ErrorEvent.class); - handler.onError(WebsocketChatClient.this, errorEvent); + handler.onError(this, errorEvent); break; default: // todo 用 log System.out.println("未知事件类型: " + eventType); } } catch (Exception e) { - handler.onClientException(WebsocketChatClient.this, new RuntimeException(e)); + handler.onClientException(this, new RuntimeException(e)); } } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java index 8d4dfc70..1f1101a7 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java @@ -8,7 +8,7 @@ public class WebsocketChatClientBuilder { private final String baseUrl; private final OkHttpClient httpClient; - public WebsocketChatClient create(WsChatReq req) { + public WebsocketChatClient create(WebsocketChatCreateReq req) { return new WebsocketChatClient(httpClient, baseUrl, req); } } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WsChatReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCreateReq.java similarity index 86% rename from api/src/main/java/com/coze/openapi/service/service/websocket/chat/WsChatReq.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCreateReq.java index 178b10e8..099b5d1a 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WsChatReq.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCreateReq.java @@ -10,7 +10,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public class WsChatReq extends BaseReq { +public class WebsocketChatCreateReq extends BaseReq { @NonNull private String botID; @NonNull private WebsocketChatCallbackHandler 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 index 3314aba8..35713276 100644 --- 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 @@ -2,23 +2,22 @@ import com.coze.openapi.client.websocket.event.downstream.ErrorEvent; -public abstract class BaseCallbackHandler{ +public abstract class BaseCallbackHandler { - public BaseCallbackHandler() {} + public BaseCallbackHandler() {} - // websocket closing - public void onClosing(T client, int code, String reason) {} + // websocket closing + public void onClosing(T client, int code, String reason) {} - // websocket closed - public void onClosed(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) {} + // 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) {} + // 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/BaseWebSocketListener.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java index 056cb0d6..e7dbec85 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java @@ -8,13 +8,16 @@ import okhttp3.WebSocketListener; import okio.ByteString; -public class BaseWebSocketListener extends WebSocketListener { +public class BaseWebSocketListener extends WebSocketListener { private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private final BiConsumer handleEvent; - private final BaseCallbackHandler callbackHandler; - private final T client; + private final BaseCallbackHandler callbackHandler; + private final BaseWebsocketClient client; - public BaseWebSocketListener(BiConsumer handleEvent, BaseCallbackHandler handler, T client) { + public BaseWebSocketListener( + BiConsumer handleEvent, + BaseCallbackHandler handler, + BaseWebsocketClient client) { this.handleEvent = handleEvent; this.callbackHandler = handler; this.client = client; @@ -22,6 +25,7 @@ public BaseWebSocketListener(BiConsumer handleEvent, BaseCall @Override public void onOpen(WebSocket webSocket, okhttp3.Response response) { + System.out.println("onOpen"); } @Override @@ -41,7 +45,8 @@ public void onClosing(WebSocket webSocket, int code, String 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)); + this.callbackHandler.onFailure( + this.client, new RuntimeException("websocket connection error: " + t.getMessage(), t)); } @Override diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java new file mode 100644 index 00000000..de2bdf6f --- /dev/null +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java @@ -0,0 +1,58 @@ +package com.coze.openapi.service.service.websocket.common; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import com.coze.openapi.client.websocket.common.BaseEvent; +import com.coze.openapi.service.utils.Utils; +import com.fasterxml.jackson.databind.ObjectMapper; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.WebSocket; + +public abstract class BaseWebsocketClient { + protected final ObjectMapper objectMapper = Utils.getMapper(); + protected final WebSocket ws; + protected final ExecutorService executorService; + protected static final int CLOSE_TIMEOUT_SECONDS = 10; + + protected BaseWebsocketClient(OkHttpClient client, String url, BaseCallbackHandler handler) { + Request request = new Request.Builder().url(url).build(); + this.executorService = Executors.newSingleThreadExecutor(); + this.ws = + client.newWebSocket(request, new BaseWebSocketListener(this::handleEvent, handler, this)); + } + + protected void sendEvent(BaseEvent event) { + this.ws.send(Utils.toJson(event)); + } + + protected abstract void handleEvent(WebSocket ws, String text); + + 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(); + } + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java index 2e74fb48..596a9aa5 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java @@ -30,7 +30,7 @@ void setUp() { MockitoAnnotations.openMocks(this); when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); - WsChatReq req = new WsChatReq("test-bot-id", mockCallbackHandler); + WebsocketChatCreateReq req = new WebsocketChatCreateReq("test-bot-id", mockCallbackHandler); client = new WebsocketChatClient(mockOkHttpClient, "ws://test.com", req); } 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..4ab71ff5 --- /dev/null +++ b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java @@ -0,0 +1,133 @@ +package example.websocket.audio.speech; + +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.WebsocketAudioSpeechCallbackHandler; +import com.coze.openapi.service.service.websocket.audio.speech.WebsocketAudioSpeechClient; +import com.coze.openapi.service.service.websocket.audio.speech.WebsocketAudioSpeechCreateReq; +import com.fasterxml.jackson.annotation.JsonProperty; +import example.utils.ExampleUtils; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + + +/* +This example describes how to use the chat interface to initiate conversations, +poll the status of the conversation, and obtain the messages after the conversation is completed. +* */ +public class WebsocketAudioSpeechExample { + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + private static class Weather { + @JsonProperty("weather") + private String weather; + } + + private static class CallbackHandler extends WebsocketAudioSpeechCallbackHandler { + private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 + + public CallbackHandler() { + super(); + } + + // 语音创建成功事件 (speech.created) + @Override + public void onSpeechCreated(WebsocketAudioSpeechClient client, SpeechCreatedEvent event) { + System.out.println("==== Speech Created ===="); + System.out.println(event); + } + + // 语音配置更新事件 (speech.update) + @Override + public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEvent event) { + System.out.println("==== Speech Updated ===="); + System.out.println(event); + } + + // 语音数据更新事件 (speech.audio.update) + @Override + public void onSpeechAudioUpdate( + WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) { + buffer.put(event.getDelta()); + } + + // 语音数据完成事件 (speech.audio.completed) + @Override + public void onSpeechAudioCompleted( + WebsocketAudioSpeechClient client, SpeechAudioCompletedEvent event) { + try { + ExampleUtils.writePcmToWavFile(buffer.array(), "output_speech.wav"); + System.out.println("========= On Speech Audio Completed ========="); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // 文本缓冲区完成事件 (input_text_buffer.completed) + @Override + public void onInputTextBufferCompleted( + WebsocketAudioSpeechClient client, InputTextBufferCompletedEvent event) { + System.out.println("==== Input Text Buffer Completed ===="); + System.out.println(event); + } + + @Override + public void onError(WebsocketAudioSpeechClient 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(); + + WebsocketAudioSpeechClient client = null; + try { + client = + coze.websocket().audio().speech().create(new WebsocketAudioSpeechCreateReq(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(); + TimeUnit.SECONDS.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..59770f88 --- /dev/null +++ b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java @@ -0,0 +1,164 @@ +package example.websocket.audio.transcriptions; + +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.WebsocketAudioTranscriptionsCallbackHandler; +import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketAudioTranscriptionsClient; +import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketAudioTranscriptionsCreateReq; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Base64; +import java.util.concurrent.TimeUnit; + +/* +This example describes how to use the chat interface to initiate conversations, +poll the status of the conversation, and obtain the messages after the conversation is completed. +* */ +public class WebsocketTranscriptionsExample { + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + private static class Weather { + @JsonProperty("weather") + private String weather; + } + + private static class CallbackHandler extends WebsocketAudioTranscriptionsCallbackHandler { + private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 + + public CallbackHandler() { + super(); + } + + + @Override + public void onError(WebsocketAudioTranscriptionsClient client, ErrorEvent event) { + System.out.println(event); + } + + + @Override + public void onClientException(WebsocketAudioTranscriptionsClient client, Throwable e) { + e.printStackTrace(); + } + + // 转录配置更新事件 (transcriptions.updated) + @Override + public void onTranscriptionsUpdated( + WebsocketAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) { + System.out.println("=== Transcriptions Updated ==="); + System.out.println(event); + } + + // 转录创建事件 (transcriptions.created) + @Override + public void onTranscriptionsCreated( + WebsocketAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) { + System.out.println("=== Transcriptions Created ==="); + System.out.println(event); + } + + // 转录消息更新事件 (transcriptions.message.update) + @Override + public void onTranscriptionsMessageUpdate( + WebsocketAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) { + System.out.println(event.getData().getContent()); + } + + // 转录消息完成事件 (transcriptions.message.completed) + @Override + public void onTranscriptionsMessageCompleted( + WebsocketAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) { + System.out.println("=== Transcriptions Message Completed ==="); + System.out.println(event); + } + + + // 语音缓冲区完成事件 (input_audio_buffer.completed) + @Override + public void onInputAudioBufferCompleted( + WebsocketAudioTranscriptionsClient 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(); + + WebsocketAudioTranscriptionsClient client = null; + try { + client = + coze.websocket().audio().transcriptions().create(new WebsocketAudioTranscriptionsCreateReq(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) { + // 将读取到的字节转换为 base64 编码 + String base64Data = Base64.getEncoder().encodeToString(Arrays.copyOf(buffer, bytesRead)); + client.inputAudioBufferAppend(base64Data); + } + client.inputAudioBufferComplete(); + } catch (IOException e) { + e.printStackTrace(); + } + + TimeUnit.SECONDS.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 index cd9184fa..8861551e 100644 --- a/example/src/main/java/example/websocket/chat/ChatExample.java +++ b/example/src/main/java/example/websocket/chat/ChatExample.java @@ -20,7 +20,7 @@ import com.coze.openapi.service.service.CozeAPI; import com.coze.openapi.service.service.websocket.chat.WebsocketChatCallbackHandler; import com.coze.openapi.service.service.websocket.chat.WebsocketChatClient; -import com.coze.openapi.service.service.websocket.chat.WsChatReq; +import com.coze.openapi.service.service.websocket.chat.WebsocketChatCreateReq; import com.coze.openapi.service.utils.Utils; import com.fasterxml.jackson.annotation.JsonProperty; @@ -140,7 +140,8 @@ public static void main(String[] args) throws Exception { WebsocketChatClient client = null; try { - client = coze.websocket().chat().create(new WsChatReq(botID, new CallbackHandler())); + client = + coze.websocket().chat().create(new WebsocketChatCreateReq(botID, new CallbackHandler())); CreateSpeechResp speechResp = coze.audio() .speech() From bd58521b52f6a0b452f2e51af2a5145fba83f9d0 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 15:18:03 +0800 Subject: [PATCH 04/50] format code Change-Id: I09ffad819a8963805cdd44c806dd8619ee323c2f --- .../WebsocketAudioTranscriptionsClient.java | 12 +- .../websocket/chat/WebsocketChatClient.java | 9 +- .../speech/WebsocketAudioSpeechExample.java | 194 +++++++------- .../WebsocketTranscriptionsExample.java | 253 +++++++++--------- 4 files changed, 230 insertions(+), 238 deletions(-) diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java index 937ef8a4..cd5910f6 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java @@ -68,26 +68,22 @@ protected void handleEvent(WebSocket ws, String text) { case EventType.TRANSCRIPTIONS_MESSAGE_UPDATE: TranscriptionsMessageUpdateEvent messageUpdateEvent = objectMapper.treeToValue(jsonNode, TranscriptionsMessageUpdateEvent.class); - handler.onTranscriptionsMessageUpdate( - this, messageUpdateEvent); + handler.onTranscriptionsMessageUpdate(this, messageUpdateEvent); break; case EventType.TRANSCRIPTIONS_MESSAGE_COMPLETED: TranscriptionsMessageCompletedEvent messageCompletedEvent = objectMapper.treeToValue(jsonNode, TranscriptionsMessageCompletedEvent.class); - handler.onTranscriptionsMessageCompleted( - this, messageCompletedEvent); + handler.onTranscriptionsMessageCompleted(this, messageCompletedEvent); break; case EventType.INPUT_AUDIO_BUFFER_CLEARED: InputAudioBufferClearedEvent bufferClearedEvent = objectMapper.treeToValue(jsonNode, InputAudioBufferClearedEvent.class); - handler.onInputAudioBufferCleared( - this, bufferClearedEvent); + handler.onInputAudioBufferCleared(this, bufferClearedEvent); break; case EventType.INPUT_AUDIO_BUFFER_COMPLETED: InputAudioBufferCompletedEvent bufferCompletedEvent = objectMapper.treeToValue(jsonNode, InputAudioBufferCompletedEvent.class); - handler.onInputAudioBufferCompleted( - this, bufferCompletedEvent); + handler.onInputAudioBufferCompleted(this, bufferCompletedEvent); break; case EventType.ERROR: ErrorEvent errorEvent = objectMapper.treeToValue(jsonNode, ErrorEvent.class); diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java index 42146d79..512e91fe 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java @@ -108,14 +108,12 @@ protected void handleEvent(WebSocket ws, String text) { case EventType.CONVERSATION_AUDIO_TRANSCRIPT_COMPLETED: ConversationAudioTranscriptCompletedEvent transcriptCompletedEvent = objectMapper.treeToValue(jsonNode, ConversationAudioTranscriptCompletedEvent.class); - handler.onConversationAudioTranscriptCompleted( - this, transcriptCompletedEvent); + handler.onConversationAudioTranscriptCompleted(this, transcriptCompletedEvent); break; case EventType.CONVERSATION_AUDIO_TRANSCRIPT_UPDATE: ConversationAudioTranscriptUpdateEvent transcriptUpdateEvent = objectMapper.treeToValue(jsonNode, ConversationAudioTranscriptUpdateEvent.class); - handler.onConversationAudioTranscriptUpdate( - this, transcriptUpdateEvent); + handler.onConversationAudioTranscriptUpdate(this, transcriptUpdateEvent); break; case EventType.CONVERSATION_CHAT_CANCELED: ConversationChatCanceledEvent chatCanceledEvent = @@ -145,8 +143,7 @@ protected void handleEvent(WebSocket ws, String text) { case EventType.CONVERSATION_CHAT_REQUIRES_ACTION: ConversationChatRequiresActionEvent chatRequiresActionEvent = objectMapper.treeToValue(jsonNode, ConversationChatRequiresActionEvent.class); - handler.onConversationChatRequiresAction( - this, chatRequiresActionEvent); + handler.onConversationChatRequiresAction(this, chatRequiresActionEvent); break; case EventType.CONVERSATION_CLEARED: ConversationClearedEvent clearedEvent = diff --git a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java index 4ab71ff5..8d325c29 100644 --- a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java +++ b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java @@ -1,5 +1,9 @@ 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; @@ -10,124 +14,122 @@ import com.coze.openapi.service.service.websocket.audio.speech.WebsocketAudioSpeechClient; import com.coze.openapi.service.service.websocket.audio.speech.WebsocketAudioSpeechCreateReq; import com.fasterxml.jackson.annotation.JsonProperty; + import example.utils.ExampleUtils; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.TimeUnit; - - /* This example describes how to use the chat interface to initiate conversations, poll the status of the conversation, and obtain the messages after the conversation is completed. * */ public class WebsocketAudioSpeechExample { - @Data - @Builder - @AllArgsConstructor - @NoArgsConstructor - private static class Weather { - @JsonProperty("weather") - private String weather; - } + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + private static class Weather { + @JsonProperty("weather") + private String weather; + } - private static class CallbackHandler extends WebsocketAudioSpeechCallbackHandler { - private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 + private static class CallbackHandler extends WebsocketAudioSpeechCallbackHandler { + private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 - public CallbackHandler() { - super(); - } + public CallbackHandler() { + super(); + } - // 语音创建成功事件 (speech.created) - @Override - public void onSpeechCreated(WebsocketAudioSpeechClient client, SpeechCreatedEvent event) { - System.out.println("==== Speech Created ===="); - System.out.println(event); - } + // 语音创建成功事件 (speech.created) + @Override + public void onSpeechCreated(WebsocketAudioSpeechClient client, SpeechCreatedEvent event) { + System.out.println("==== Speech Created ===="); + System.out.println(event); + } - // 语音配置更新事件 (speech.update) - @Override - public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEvent event) { - System.out.println("==== Speech Updated ===="); - System.out.println(event); - } + // 语音配置更新事件 (speech.update) + @Override + public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEvent event) { + System.out.println("==== Speech Updated ===="); + System.out.println(event); + } - // 语音数据更新事件 (speech.audio.update) - @Override - public void onSpeechAudioUpdate( - WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) { - buffer.put(event.getDelta()); - } + // 语音数据更新事件 (speech.audio.update) + @Override + public void onSpeechAudioUpdate( + WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) { + buffer.put(event.getDelta()); + } - // 语音数据完成事件 (speech.audio.completed) - @Override - public void onSpeechAudioCompleted( - WebsocketAudioSpeechClient client, SpeechAudioCompletedEvent event) { - try { - ExampleUtils.writePcmToWavFile(buffer.array(), "output_speech.wav"); - System.out.println("========= On Speech Audio Completed ========="); - } catch (IOException e) { - e.printStackTrace(); - } - } + // 语音数据完成事件 (speech.audio.completed) + @Override + public void onSpeechAudioCompleted( + WebsocketAudioSpeechClient client, SpeechAudioCompletedEvent event) { + try { + ExampleUtils.writePcmToWavFile(buffer.array(), "output_speech.wav"); + System.out.println("========= On Speech Audio Completed ========="); + } catch (IOException e) { + e.printStackTrace(); + } + } - // 文本缓冲区完成事件 (input_text_buffer.completed) - @Override - public void onInputTextBufferCompleted( - WebsocketAudioSpeechClient client, InputTextBufferCompletedEvent event) { - System.out.println("==== Input Text Buffer Completed ===="); - System.out.println(event); - } + // 文本缓冲区完成事件 (input_text_buffer.completed) + @Override + public void onInputTextBufferCompleted( + WebsocketAudioSpeechClient client, InputTextBufferCompletedEvent event) { + System.out.println("==== Input Text Buffer Completed ===="); + System.out.println(event); + } - @Override - public void onError(WebsocketAudioSpeechClient client, ErrorEvent event) { - System.out.println(event); - } + @Override + public void onError(WebsocketAudioSpeechClient 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); + // 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(); + // Init the Coze client through the access_token. + CozeAPI coze = + new CozeAPI.Builder() + .baseURL(System.getenv("COZE_API_BASE")) + .auth(authCli) + .readTimeout(10000) + .build(); - WebsocketAudioSpeechClient client = null; - try { - client = - coze.websocket().audio().speech().create(new WebsocketAudioSpeechCreateReq(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(); - TimeUnit.SECONDS.sleep(100); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (client != null) { - client.close(); - } - coze.shutdownExecutor(); - } + WebsocketAudioSpeechClient client = null; + try { + client = + coze.websocket() + .audio() + .speech() + .create(new WebsocketAudioSpeechCreateReq(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(); + TimeUnit.SECONDS.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 index 59770f88..787103a4 100644 --- a/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java +++ b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java @@ -1,5 +1,12 @@ package example.websocket.audio.transcriptions; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Base64; +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; @@ -12,153 +19,143 @@ import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketAudioTranscriptionsClient; import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketAudioTranscriptionsCreateReq; import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Base64; -import java.util.concurrent.TimeUnit; - /* This example describes how to use the chat interface to initiate conversations, poll the status of the conversation, and obtain the messages after the conversation is completed. * */ public class WebsocketTranscriptionsExample { - @Data - @Builder - @AllArgsConstructor - @NoArgsConstructor - private static class Weather { - @JsonProperty("weather") - private String weather; + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + private static class Weather { + @JsonProperty("weather") + private String weather; + } + + private static class CallbackHandler extends WebsocketAudioTranscriptionsCallbackHandler { + private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 + + public CallbackHandler() { + super(); } - private static class CallbackHandler extends WebsocketAudioTranscriptionsCallbackHandler { - private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 - - public CallbackHandler() { - super(); - } - - - @Override - public void onError(WebsocketAudioTranscriptionsClient client, ErrorEvent event) { - System.out.println(event); - } - - - @Override - public void onClientException(WebsocketAudioTranscriptionsClient client, Throwable e) { - e.printStackTrace(); - } - - // 转录配置更新事件 (transcriptions.updated) - @Override - public void onTranscriptionsUpdated( - WebsocketAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) { - System.out.println("=== Transcriptions Updated ==="); - System.out.println(event); - } + @Override + public void onError(WebsocketAudioTranscriptionsClient client, ErrorEvent event) { + System.out.println(event); + } - // 转录创建事件 (transcriptions.created) - @Override - public void onTranscriptionsCreated( - WebsocketAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) { - System.out.println("=== Transcriptions Created ==="); - System.out.println(event); - } + @Override + public void onClientException(WebsocketAudioTranscriptionsClient client, Throwable e) { + e.printStackTrace(); + } - // 转录消息更新事件 (transcriptions.message.update) - @Override - public void onTranscriptionsMessageUpdate( - WebsocketAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) { - System.out.println(event.getData().getContent()); - } + // 转录配置更新事件 (transcriptions.updated) + @Override + public void onTranscriptionsUpdated( + WebsocketAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) { + System.out.println("=== Transcriptions Updated ==="); + System.out.println(event); + } - // 转录消息完成事件 (transcriptions.message.completed) - @Override - public void onTranscriptionsMessageCompleted( - WebsocketAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) { - System.out.println("=== Transcriptions Message Completed ==="); - System.out.println(event); - } + // 转录创建事件 (transcriptions.created) + @Override + public void onTranscriptionsCreated( + WebsocketAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) { + System.out.println("=== Transcriptions Created ==="); + System.out.println(event); + } + // 转录消息更新事件 (transcriptions.message.update) + @Override + public void onTranscriptionsMessageUpdate( + WebsocketAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) { + System.out.println(event.getData().getContent()); + } - // 语音缓冲区完成事件 (input_audio_buffer.completed) - @Override - public void onInputAudioBufferCompleted( - WebsocketAudioTranscriptionsClient client, InputAudioBufferCompletedEvent event) { - System.out.println("=== Input Audio Buffer Completed ==="); - System.out.println(event); - } + // 转录消息完成事件 (transcriptions.message.completed) + @Override + public void onTranscriptionsMessageCompleted( + WebsocketAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) { + System.out.println("=== Transcriptions Message 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(); - - WebsocketAudioTranscriptionsClient client = null; - try { - client = - coze.websocket().audio().transcriptions().create(new WebsocketAudioTranscriptionsCreateReq(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) { - // 将读取到的字节转换为 base64 编码 - String base64Data = Base64.getEncoder().encodeToString(Arrays.copyOf(buffer, bytesRead)); - client.inputAudioBufferAppend(base64Data); - } - client.inputAudioBufferComplete(); - } catch (IOException e) { - e.printStackTrace(); - } - - TimeUnit.SECONDS.sleep(100); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (client != null) { - client.close(); - } - coze.shutdownExecutor(); + // 语音缓冲区完成事件 (input_audio_buffer.completed) + @Override + public void onInputAudioBufferCompleted( + WebsocketAudioTranscriptionsClient 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(); + + WebsocketAudioTranscriptionsClient client = null; + try { + client = + coze.websocket() + .audio() + .transcriptions() + .create(new WebsocketAudioTranscriptionsCreateReq(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) { + // 将读取到的字节转换为 base64 编码 + String base64Data = Base64.getEncoder().encodeToString(Arrays.copyOf(buffer, bytesRead)); + client.inputAudioBufferAppend(base64Data); } + client.inputAudioBufferComplete(); + } catch (IOException e) { + e.printStackTrace(); + } + + TimeUnit.SECONDS.sleep(100); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (client != null) { + client.close(); + } + coze.shutdownExecutor(); } + } } From 8ae410b353b22a896bdbb303bd7226f61bc8290b Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 17:12:15 +0800 Subject: [PATCH 05/50] add ut Change-Id: I47d0e6183927f3fca503d21b9482daa2dfa87091 --- .../openapi/client/chat/model/ChatUsage.java | 6 + .../client/websocket/common/Detail.java | 8 + .../event/downstream/ChatCreatedEvent.java | 1 - ...ersationAudioTranscriptCompletedEvent.java | 2 +- ...onversationAudioTranscriptUpdateEvent.java | 2 +- .../downstream/SpeechAudioUpdateEvent.java | 3 +- .../websocket/event/model/LimitConfig.java | 16 + .../websocket/event/model/OpusConfig.java | 3 + .../chat/WebsocketChatCallbackHandler.java | 72 +- .../WebsocketAudioSpeechClientTest.java | 223 ++++++ ...ebsocketAudioTranscriptionsClientTest.java | 248 ++++++ .../chat/WebsocketChatClientTest.java | 716 +++++++++++++++++- 12 files changed, 1221 insertions(+), 79 deletions(-) create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/LimitConfig.java create mode 100644 api/src/test/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClientTest.java create mode 100644 api/src/test/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClientTest.java 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/websocket/common/Detail.java b/api/src/main/java/com/coze/openapi/client/websocket/common/Detail.java index c25bf8ff..79100cda 100644 --- 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 @@ -1,7 +1,15 @@ 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/downstream/ChatCreatedEvent.java b/api/src/main/java/com/coze/openapi/client/websocket/event/downstream/ChatCreatedEvent.java index c0ec783c..1de0291e 100644 --- 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 @@ -18,7 +18,6 @@ @ToString(callSuper = true) // 对话创建事件 // event_type: chat.created -@Builder public class ChatCreatedEvent extends BaseEvent { @JsonProperty("event_type") @Builder.Default 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 index 4e38678f..2ab896ad 100644 --- 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 @@ -7,7 +7,7 @@ import lombok.*; import lombok.experimental.SuperBuilder; -@Data +@lombok.Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor 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 index 6324efe1..20ea5b81 100644 --- 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 @@ -7,7 +7,7 @@ import lombok.*; import lombok.experimental.SuperBuilder; -@Data +@lombok.Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor 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 index 968a897a..56ccfd81 100644 --- 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 @@ -9,7 +9,7 @@ import lombok.*; import lombok.experimental.SuperBuilder; -@Data +@lombok.Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor @@ -24,6 +24,7 @@ public class SpeechAudioUpdateEvent extends BaseEvent { @JsonProperty("data") private Data data; + @lombok.Data @NoArgsConstructor @AllArgsConstructor public static class Data { 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..fd2299c1 --- /dev/null +++ b/api/src/main/java/com/coze/openapi/client/websocket/event/model/LimitConfig.java @@ -0,0 +1,16 @@ +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 index 39136c8a..c61806ee 100644 --- 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 @@ -18,4 +18,7 @@ public class OpusConfig { @JsonProperty("frame_size_ms") private Double frameSizeMs; + + @JsonProperty("limit_config") + private LimitConfig limitConfig; } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java index 2f18cc12..55d65b70 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java @@ -29,62 +29,64 @@ public void onChatCreated(WebsocketChatClient client, ChatCreatedEvent event) {} // 对话配置成功事件 (chat.updated) public void onChatUpdated(WebsocketChatClient client, ChatUpdatedEvent event) {} - // 语音回复完成事件 (conversation.audio.completed) - public void onConversationAudioCompleted( - WebsocketChatClient client, ConversationAudioCompletedEvent event) {} + // 对话创建事件 (conversation.chat.created) + public void onConversationChatCreated( + WebsocketChatClient client, ConversationChatCreatedEvent event) {} + + // 对话正在处理事件 (conversation.chat.in_progress) + public void onConversationChatInProgress( + WebsocketChatClient client, ConversationChatInProgressEvent event) {} + + // 增量消息事件 (conversation.message.delta) + public void onConversationMessageDelta( + WebsocketChatClient client, ConversationMessageDeltaEvent event) {} // 增量语音事件 (conversation.audio.delta) public void onConversationAudioDelta( WebsocketChatClient client, ConversationAudioDeltaEvent event) {} - // 语音转录完成事件 (conversation.audio_transcript.completed) - public void onConversationAudioTranscriptCompleted( - WebsocketChatClient client, ConversationAudioTranscriptCompletedEvent event) {} - - // 语音转录更新事件 (conversation.audio_transcript.update) - public void onConversationAudioTranscriptUpdate( - WebsocketChatClient client, ConversationAudioTranscriptUpdateEvent event) {} + // 消息完成事件 (conversation.message.completed) + public void onConversationMessageCompleted( + WebsocketChatClient client, ConversationMessageCompletedEvent event) {} - // 对话取消事件 (conversation.chat.canceled) - public void onConversationChatCanceled( - WebsocketChatClient client, ConversationChatCanceledEvent event) {} + // 语音回复完成事件 (conversation.audio.completed) + public void onConversationAudioCompleted( + WebsocketChatClient client, ConversationAudioCompletedEvent event) {} // 对话完成事件 (conversation.chat.completed) public void onConversationChatCompleted( WebsocketChatClient client, ConversationChatCompletedEvent event) {} - // 对话创建事件 (conversation.chat.created) - public void onConversationChatCreated( - WebsocketChatClient client, ConversationChatCreatedEvent event) {} - // 对话失败事件 (conversation.chat.failed) public void onConversationChatFailed( WebsocketChatClient client, ConversationChatFailedEvent event) {} - // 对话正在处理事件 (conversation.chat.in_progress) - public void onConversationChatInProgress( - WebsocketChatClient client, ConversationChatInProgressEvent event) {} + // 语音提交成功事件 (input_audio_buffer.completed) + public void onInputAudioBufferCompleted( + WebsocketChatClient client, InputAudioBufferCompletedEvent event) {} - // 端插件事件 (conversation.chat.requires_action) - public void onConversationChatRequiresAction( - WebsocketChatClient client, ConversationChatRequiresActionEvent event) {} + + // 语音清除成功事件 (input_audio_buffer.cleared) + public void onInputAudioBufferCleared( + WebsocketChatClient client, InputAudioBufferClearedEvent event) {} // 对话清除事件 (conversation.cleared) public void onConversationCleared(WebsocketChatClient client, ConversationClearedEvent event) {} - // 消息完成事件 (conversation.message.completed) - public void onConversationMessageCompleted( - WebsocketChatClient client, ConversationMessageCompletedEvent event) {} + // 对话取消事件 (conversation.chat.canceled) + public void onConversationChatCanceled( + WebsocketChatClient client, ConversationChatCanceledEvent event) {} - // 增量消息事件 (conversation.message.delta) - public void onConversationMessageDelta( - WebsocketChatClient client, ConversationMessageDeltaEvent event) {} + // 语音转录更新事件 (conversation.audio_transcript.update) + public void onConversationAudioTranscriptUpdate( + WebsocketChatClient client, ConversationAudioTranscriptUpdateEvent event) {} - // 语音清除成功事件 (input_audio_buffer.cleared) - public void onInputAudioBufferCleared( - WebsocketChatClient client, InputAudioBufferClearedEvent event) {} + // 语音转录完成事件 (conversation.audio_transcript.completed) + public void onConversationAudioTranscriptCompleted( + WebsocketChatClient client, ConversationAudioTranscriptCompletedEvent event) {} + + // 端插件事件 (conversation.chat.requires_action) + public void onConversationChatRequiresAction( + WebsocketChatClient client, ConversationChatRequiresActionEvent event) {} - // 语音提交成功事件 (input_audio_buffer.completed) - public void onInputAudioBufferCompleted( - WebsocketChatClient client, InputAudioBufferCompletedEvent event) {} } 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..ad9040f5 --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClientTest.java @@ -0,0 +1,223 @@ +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 okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketAudioSpeechClientTest { + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private WebsocketAudioSpeechCallbackHandler 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 WebsocketAudioSpeechClient client; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); + + WebsocketAudioSpeechCreateReq req = WebsocketAudioSpeechCreateReq.builder() + .callbackHandler(mockCallbackHandler) + .build(); + client = new WebsocketAudioSpeechClient(mockOkHttpClient, "ws://test.com", req); + } + + @Test + public void testHandleSpeechCreatedEvent() { + String json = "{\n" + + " \"id\": \"7446668538246561xxxx\",\n" + + " \"event_type\": \"speech.created\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("20241210152726467C48D89D6DB2F3***", 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\": \"20241210152726467C48D89D6DB2F3***\" }\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("20241210152726467C48D89D6DB2F3***", 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\": \"20241210152726467C48D89D6DB2F3***\" }\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("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleSpeechAudioCompletedEvent() { + String json = "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"speech.audio.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleInputTextBufferCompletedEvent() { + String json = "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"input_text_buffer.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("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)); + } +} \ No newline at end of file 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..012df9a6 --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClientTest.java @@ -0,0 +1,248 @@ +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.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 okhttp3.OkHttpClient; +import okhttp3.WebSocket; + +public class WebsocketAudioTranscriptionsClientTest { + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private WebsocketAudioTranscriptionsCallbackHandler 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 WebsocketAudioTranscriptionsClient client; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); + + WebsocketAudioTranscriptionsCreateReq req = WebsocketAudioTranscriptionsCreateReq.builder() + .callbackHandler(mockCallbackHandler) + .build(); + client = new WebsocketAudioTranscriptionsClient(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)); + } +} \ No newline at end of file diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java index 596a9aa5..0bc5ecc0 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java @@ -1,93 +1,729 @@ 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.websocket.event.downstream.ConversationAudioDeltaEvent; -import com.coze.openapi.service.utils.Utils; +import com.coze.openapi.client.chat.model.ChatToolCall; +import com.coze.openapi.client.websocket.event.EventType; +import com.coze.openapi.client.websocket.event.downstream.*; import okhttp3.OkHttpClient; import okhttp3.WebSocket; -class WebsocketChatClientTest { - +public class WebsocketChatClientTest { @Mock private OkHttpClient mockOkHttpClient; - @Mock private WebSocket mockWebSocket; - @Mock private WebsocketChatCallbackHandler 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 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 WebsocketChatClient client; @BeforeEach - void setUp() { + public void setup() { MockitoAnnotations.openMocks(this); when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); - WebsocketChatCreateReq req = new WebsocketChatCreateReq("test-bot-id", mockCallbackHandler); + WebsocketChatCreateReq req = + WebsocketChatCreateReq.builder() + .botID("test-bot-id") + .callbackHandler(mockCallbackHandler) + .build(); client = new WebsocketChatClient(mockOkHttpClient, "ws://test.com", req); } @Test - void testHandleAudioDeltaEvent() { - // 准备模拟的音频数据 - String audioData = "base64EncodedAudioData"; - ConversationAudioDeltaEvent event = - ConversationAudioDeltaEvent.builder() - // .data(Message.builder().audio(audioData).build()) - .build(); + 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)); + } - // 模拟 WebSocket 接收到消息 - String eventJson = Utils.toJson(event); - client.handleEvent(mockWebSocket, eventJson); + @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) - .onConversationAudioDelta(eq(client), any(ConversationAudioDeltaEvent.class)); + .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 - void testSendEvent() { - // 捕获发送到 WebSocket 的数据 - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + 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.inputAudioBufferAppend("test-audio-data"); + client.handleEvent(mockWebSocket, json); - // 验证 WebSocket.send() 被调用,并捕获发送的消息 - verify(mockWebSocket).send(messageCaptor.capture()); + 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"; - // 验证发送的消息格式正确 - String sentMessage = messageCaptor.getValue(); - // assertThat(sentMessage).contains(EventType.INPUT_AUDIO_BUFFER_APPEND); + 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 - void testClose() { - client.close(); + 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); - // 验证 WebSocket 被正确关闭 - verify(mockWebSocket).close(eq(1000), isNull()); + 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 - void testHandleUnknownEvent() { - String unknownEventJson = - "{\"event_type\":\"conversation.audio.delta\",\"id\":\"cd471e11-9a77-4f89-942d-da4650563fe1\",\"data\":{\"id\":\"7476315154641125428\",\"conversation_id\":\"7476315134009507875\",\"bot_id\":\"7433626593467514921\",\"role\":\"assistant\",\"type\":\"answer\",\"content\":\"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FQANABIAFAAPABQADgASABgAFAAWABcAFQAaABsAGwAdACQAIgAiACYAJAAnACUAKwAkACUAJQArAC0AMQAxACsALQAvADYAMAAtADMAMQAxADgAOgA1ADoANgA3ADQAPgBBAD0APwA9AEAAOgA9AD4AQgBDAEUASQBMAE4ATwBIAEoASgBLAE8ATgBLAFAATwBOAEsASQBBAEAARABDAEIARABAAEYATgBQAE4ATQBRAE0ASABMAEoATQBLAEoAUQBKAEcASgBMAEcARQA/AEcARAA/AD4APgA7AEMAQQA0AD4AOgA/AEIARwBDAEAARgBGAEsATQBGAEcASABEAEcARABCAEIAPwA9AEAAQABCADsANwA4ADgAPAA9AD0APAA/ADwAPgBBADgAMQA6ADoAOQA3ADUANQAxAC0AKgAqAC0AKQAnACUAIgAjACkAJQAlACIAIAAlACEAHwAfACQAIgAfACAAHQAaABYAFgAVABIADwAPAA4ACgADAAgADAAPABAAEwASABAAEgAQABUAEgARABAADwAPAAgADAAHAAcABAAIAAYABgAAAAAAAAAAAP///f8AAP//AwADAAEA+//8//n/9P/6//b/+f/4//X/9v/x//P/+P/1//f/+v/6//z//P/3//3/+//6//3/+//2//b/+f/7//n/+v/7//b/9v/0//X/9P/0//X/8//0//f/+f/5//X/9P/0//b/+P/0//T/9v/3//X/9P/v/+v/6//w/+3/7f/v/+j/8P/t/+z/7f/u/+7/6f/k/+//7P/r/+j/6v/w/+L/4P/j/9z/5P/j/9n/1v/S/9H/zv/P/8z/yP/K/8f/wv/M/8v/yf/L/8//0P/L/8n/yv/N/9P/0P/P/8v/zP/V/8z/xP/L/8z/x//K/8n/yv/J/8j/y//J/8z/y//N/8z/x//M/87/y//O/8//0v/T/9r/1v/N/9b/1f/Q/9H/0v/R/8//zf/R/83/yv/M/8//0f/N/9P/0f/P/9H/1f/V/9H/2P/U/9f/1v/b/9z/2P/V/9b/1f/S/9b/0f/O/87/0P/R/9f/2P/f/+H/3P/g/+H/2//Z/93/4//h/9z/3P/d/+b/5f/i/93/3v/a/93/4f/d/9//3//d/9//4//e/+D/5f/m/+b/5f/q/+z/5v/k/+n/6f/l/+H/5P/n/+v/6P/m/+r/5//u//D/6P/y//D/7//s//P/9f/z//T/9f/z/+z/7//x//n/+P/w//n/9//4//z/AAD7//n////+//r/9f8AAAAA+//7/wEAAAD/////AAAGAAIABwADAAUABQAGAAUABgAHAAoADwAPABEAEQAPAA8AEAAUABIADQAOABcAGQAXABcAFAAYABkAGAAZABcAHAAcACAAIAAgACIAIgAiACEAIAAmAB8AHgAlACUAKwAlACYAJQAnACkAKQArACcAJQAoADMALAAuACwAKQArACsAKQA0ADEAKwArACEALAAtAC0AKAAqACYAKwAvAC0AKwAvACgAKgArACYAKwAqAC4ALQAuADIAKgAtACoAKQAqADAAMgAvACwALgAnAC0ALQAtACsAMQAoACoA\",\"content_type\":\"audio\",\"chat_id\":\"7476308073020276770\",\"section_id\":\"7476315134009507875\"},\"detail\":{\"logid\":\"02174071526740100000000000000000000ffff0a807486d6926f\"}}\n"; - client.handleEvent(mockWebSocket, unknownEventJson); + public void testHandleUnknownEvent() { + // event_type: unknown + String json = "{\"event_type\":\"unknown\"}"; + + client.handleEvent(mockWebSocket, json); - // 验证未知事件类型被正确处理 verifyNoInteractions(mockCallbackHandler); } @Test - void testHandleInvalidJson() { + public void testHandleInvalidJson() { String invalidJson = "invalid json"; + + client.handleEvent(mockWebSocket, invalidJson); + + verify(mockCallbackHandler).onClientException(eq(client), any(RuntimeException.class)); } } From b562d4d699cda7fca6cf7d3ca034d3769de5f8c7 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 17:23:45 +0800 Subject: [PATCH 06/50] format code Change-Id: I0b7bd881bae19cab4fbae6f8906757fe06fe0999 --- .../client/websocket/common/Detail.java | 1 + .../websocket/event/model/LimitConfig.java | 10 +- .../chat/WebsocketChatCallbackHandler.java | 28 +- .../WebsocketAudioSpeechClientTest.java | 412 +++++++------- ...ebsocketAudioTranscriptionsClientTest.java | 476 ++++++++-------- .../chat/WebsocketChatClientTest.java | 522 +++++++++--------- 6 files changed, 754 insertions(+), 695 deletions(-) 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 index 79100cda..f43a146b 100644 --- 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 @@ -1,6 +1,7 @@ package com.coze.openapi.client.websocket.common; import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; 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 index fd2299c1..11eae02a 100644 --- 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 @@ -1,6 +1,7 @@ package com.coze.openapi.client.websocket.event.model; import com.fasterxml.jackson.annotation.JsonProperty; + import lombok.*; @Data @@ -9,8 +10,9 @@ @AllArgsConstructor @EqualsAndHashCode public class LimitConfig { - @JsonProperty("period") - private Integer period; - @JsonProperty("max_frame_num") - private Integer maxFrameNum; + @JsonProperty("period") + private Integer period; + + @JsonProperty("max_frame_num") + private Integer maxFrameNum; } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java index 55d65b70..d40bd161 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java @@ -29,25 +29,25 @@ public void onChatCreated(WebsocketChatClient client, ChatCreatedEvent event) {} // 对话配置成功事件 (chat.updated) public void onChatUpdated(WebsocketChatClient client, ChatUpdatedEvent event) {} - // 对话创建事件 (conversation.chat.created) - public void onConversationChatCreated( - WebsocketChatClient client, ConversationChatCreatedEvent event) {} + // 对话创建事件 (conversation.chat.created) + public void onConversationChatCreated( + WebsocketChatClient client, ConversationChatCreatedEvent event) {} - // 对话正在处理事件 (conversation.chat.in_progress) + // 对话正在处理事件 (conversation.chat.in_progress) public void onConversationChatInProgress( - WebsocketChatClient client, ConversationChatInProgressEvent event) {} + WebsocketChatClient client, ConversationChatInProgressEvent event) {} - // 增量消息事件 (conversation.message.delta) + // 增量消息事件 (conversation.message.delta) public void onConversationMessageDelta( - WebsocketChatClient client, ConversationMessageDeltaEvent event) {} + WebsocketChatClient client, ConversationMessageDeltaEvent event) {} // 增量语音事件 (conversation.audio.delta) public void onConversationAudioDelta( WebsocketChatClient client, ConversationAudioDeltaEvent event) {} - // 消息完成事件 (conversation.message.completed) + // 消息完成事件 (conversation.message.completed) public void onConversationMessageCompleted( - WebsocketChatClient client, ConversationMessageCompletedEvent event) {} + WebsocketChatClient client, ConversationMessageCompletedEvent event) {} // 语音回复完成事件 (conversation.audio.completed) public void onConversationAudioCompleted( @@ -61,10 +61,9 @@ public void onConversationChatCompleted( public void onConversationChatFailed( WebsocketChatClient client, ConversationChatFailedEvent event) {} - // 语音提交成功事件 (input_audio_buffer.completed) - public void onInputAudioBufferCompleted( - WebsocketChatClient client, InputAudioBufferCompletedEvent event) {} - + // 语音提交成功事件 (input_audio_buffer.completed) + public void onInputAudioBufferCompleted( + WebsocketChatClient client, InputAudioBufferCompletedEvent event) {} // 语音清除成功事件 (input_audio_buffer.cleared) public void onInputAudioBufferCleared( @@ -84,9 +83,8 @@ public void onConversationAudioTranscriptUpdate( // 语音转录完成事件 (conversation.audio_transcript.completed) public void onConversationAudioTranscriptCompleted( WebsocketChatClient client, ConversationAudioTranscriptCompletedEvent event) {} - + // 端插件事件 (conversation.chat.requires_action) public void onConversationChatRequiresAction( WebsocketChatClient client, ConversationChatRequiresActionEvent event) {} - } 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 index ad9040f5..b63071e7 100644 --- 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 @@ -19,205 +19,213 @@ import okhttp3.WebSocket; public class WebsocketAudioSpeechClientTest { - @Mock private OkHttpClient mockOkHttpClient; - @Mock private WebSocket mockWebSocket; - @Mock private WebsocketAudioSpeechCallbackHandler 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 WebsocketAudioSpeechClient client; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); - - WebsocketAudioSpeechCreateReq req = WebsocketAudioSpeechCreateReq.builder() - .callbackHandler(mockCallbackHandler) - .build(); - client = new WebsocketAudioSpeechClient(mockOkHttpClient, "ws://test.com", req); - } - - @Test - public void testHandleSpeechCreatedEvent() { - String json = "{\n" + - " \"id\": \"7446668538246561xxxx\",\n" + - " \"event_type\": \"speech.created\",\n" + - " \"detail\": {\n" + - " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("20241210152726467C48D89D6DB2F3***", 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\": \"20241210152726467C48D89D6DB2F3***\" }\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("20241210152726467C48D89D6DB2F3***", 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\": \"20241210152726467C48D89D6DB2F3***\" }\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("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); - } - - @Test - public void testHandleSpeechAudioCompletedEvent() { - String json = "{\n" + - " \"id\": \"event_id\",\n" + - " \"event_type\": \"speech.audio.completed\",\n" + - " \"detail\": {\n" + - " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); - } - - @Test - public void testHandleInputTextBufferCompletedEvent() { - String json = "{\n" + - " \"id\": \"event_id\",\n" + - " \"event_type\": \"input_text_buffer.completed\",\n" + - " \"detail\": {\n" + - " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("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)); - } -} \ No newline at end of file + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private WebsocketAudioSpeechCallbackHandler 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 WebsocketAudioSpeechClient client; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); + + WebsocketAudioSpeechCreateReq req = + WebsocketAudioSpeechCreateReq.builder().callbackHandler(mockCallbackHandler).build(); + client = new WebsocketAudioSpeechClient(mockOkHttpClient, "ws://test.com", req); + } + + @Test + public void testHandleSpeechCreatedEvent() { + String json = + "{\n" + + " \"id\": \"7446668538246561xxxx\",\n" + + " \"event_type\": \"speech.created\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("20241210152726467C48D89D6DB2F3***", 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\": \"20241210152726467C48D89D6DB2F3***\" }\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("20241210152726467C48D89D6DB2F3***", 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\": \"20241210152726467C48D89D6DB2F3***\" }\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("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleSpeechAudioCompletedEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"speech.audio.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + } + + @Test + public void testHandleInputTextBufferCompletedEvent() { + String json = + "{\n" + + " \"id\": \"event_id\",\n" + + " \"event_type\": \"input_text_buffer.completed\",\n" + + " \"detail\": {\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\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("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)); + } +} 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 index 012df9a6..2a5aa3e9 100644 --- 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 @@ -19,230 +19,256 @@ import okhttp3.WebSocket; public class WebsocketAudioTranscriptionsClientTest { - @Mock private OkHttpClient mockOkHttpClient; - @Mock private WebSocket mockWebSocket; - @Mock private WebsocketAudioTranscriptionsCallbackHandler 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 WebsocketAudioTranscriptionsClient client; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); - - WebsocketAudioTranscriptionsCreateReq req = WebsocketAudioTranscriptionsCreateReq.builder() + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private WebsocketAudioTranscriptionsCallbackHandler 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 WebsocketAudioTranscriptionsClient client; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); + + WebsocketAudioTranscriptionsCreateReq req = + WebsocketAudioTranscriptionsCreateReq.builder() .callbackHandler(mockCallbackHandler) .build(); - client = new WebsocketAudioTranscriptionsClient(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)); - } -} \ No newline at end of file + client = new WebsocketAudioTranscriptionsClient(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)); + } +} diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java index 0bc5ecc0..1e86e8c7 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java @@ -32,11 +32,21 @@ public class WebsocketChatClientTest { @Captor private ArgumentCaptor chatUpdatedEventCaptor; @Captor private ArgumentCaptor audioCompletedEventCaptor; @Captor private ArgumentCaptor audioDeltaEventCaptor; - @Captor private ArgumentCaptor audioTranscriptUpdateEventCaptor; + + @Captor + private ArgumentCaptor audioTranscriptUpdateEventCaptor; + @Captor private ArgumentCaptor inputAudioBufferClearedEventCaptor; - @Captor private ArgumentCaptor inputAudioBufferCompletedEventCaptor; + + @Captor + private ArgumentCaptor inputAudioBufferCompletedEventCaptor; + @Captor private ArgumentCaptor errorEventCaptor; - @Captor private ArgumentCaptor audioTranscriptCompletedEventCaptor; + + @Captor + private ArgumentCaptor + audioTranscriptCompletedEventCaptor; + @Captor private ArgumentCaptor chatCompletedEventCaptor; @Captor private ArgumentCaptor conversationChatCreatedEventCaptor; @Captor private ArgumentCaptor chatFailedEventCaptor; @@ -131,7 +141,7 @@ public void testHandleChatUpdatedEvent() { 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()); @@ -143,17 +153,18 @@ public void testHandleChatUpdatedEvent() { 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( + 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()); } @@ -161,33 +172,34 @@ public void testHandleChatUpdatedEvent() { @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"; + 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()); - + 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()); @@ -196,14 +208,14 @@ public void testHandleConversationAudioCompletedEvent() { 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()); } @@ -212,33 +224,33 @@ public void testHandleConversationAudioCompletedEvent() { 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"; + "{\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()); - + 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()); @@ -248,7 +260,7 @@ public void testHandleConversationAudioDeltaEvent() { assertEquals("123", event.getData().getChatId()); assertEquals("123", event.getData().getConversationId()); assertEquals("222", event.getData().getBotId()); - + // 验证 detail assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); } @@ -256,29 +268,32 @@ public void testHandleConversationAudioDeltaEvent() { @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"; + 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(); + 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()); } @@ -287,29 +302,30 @@ public void testHandleConversationAudioTranscriptCompletedEvent() { 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"; + "{\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()); - + 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()); } @@ -328,39 +344,40 @@ public void testHandleConversationChatCanceledEvent() { @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"; + 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()); - + 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()); @@ -369,12 +386,12 @@ public void testHandleConversationChatCompletedEvent() { 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()); } @@ -382,34 +399,35 @@ public void testHandleConversationChatCompletedEvent() { @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"; + 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()); - + 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()); @@ -419,7 +437,7 @@ public void testHandleConversationChatCreatedEvent() { assertNull(event.getData().getLastError()); assertEquals("created", event.getData().getStatus().getValue()); assertNull(event.getData().getUsage()); - + // 验证 detail assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); } @@ -427,42 +445,43 @@ public void testHandleConversationChatCreatedEvent() { @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" + - "}"; + 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()); - + 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()); @@ -472,12 +491,12 @@ public void testHandleConversationChatFailedEvent() { 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()); } @@ -485,34 +504,35 @@ public void testHandleConversationChatFailedEvent() { @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"; + 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()); - + 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()); @@ -522,7 +542,7 @@ public void testHandleConversationChatInProgressEvent() { assertNull(event.getData().getLastError()); assertEquals("in_progress", event.getData().getStatus().getValue()); assertNull(event.getData().getUsage()); - + // 验证 detail assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); } @@ -582,33 +602,34 @@ public void testHandleConversationClearedEvent() { @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"; + 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()); - + 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()); @@ -617,14 +638,14 @@ public void testHandleConversationMessageCompletedEvent() { 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()); } @@ -632,23 +653,24 @@ public void testHandleConversationMessageCompletedEvent() { @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"; + 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()); - + 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()); } @@ -656,23 +678,24 @@ public void testHandleInputAudioBufferClearedEvent() { @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"; + 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()); - + 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()); } @@ -680,30 +703,31 @@ public void testHandleInputAudioBufferCompletedEvent() { @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"; + 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()); } From 99910cf047a0b970b6b50316e22826d7081d9034 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 17:41:24 +0800 Subject: [PATCH 07/50] format code Change-Id: I8edd9ead4048ffc9537da965305356cdd197b47f --- api/pom.xml | 2 +- .../openapi/client/bots/CreateBotReq.java | 7 +++--- .../openapi/client/bots/UpdateBotReq.java | 9 ++------ .../client/dataset/ProcessDatasetReq.java | 10 +++------ .../dataset/document/CreateDocumentReq.java | 7 +++--- .../dataset/document/ListDocumentReq.java | 5 ++--- .../service/auth/DeviceOAuthClient.java | 3 +-- .../openapi/service/auth/OAuthClient.java | 22 ++++++++----------- .../openapi/service/auth/PKCEOAuthClient.java | 22 +++++++------------ .../openapi/service/auth/WebOAuthClient.java | 7 ++---- .../coze/openapi/service/service/CozeAPI.java | 2 +- .../service/audio/TranscriptionService.java | 6 +++-- .../service/service/audio/VoiceService.java | 21 +++++++++--------- .../service/service/bots/BotService.java | 15 +++++-------- .../conversation/ConversationService.java | 4 +--- .../service/conversation/MessageService.java | 8 ++----- .../service/dataset/DocumentService.java | 4 +--- .../service/service/file/FileService.java | 6 +++-- 18 files changed, 63 insertions(+), 97 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index cb908ef2..074591ef 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -68,7 +68,7 @@ 2.14.2 0.11.5 5.10.2 - 4.8.1 + 3.14.9 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/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/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/OAuthClient.java b/api/src/main/java/com/coze/openapi/service/auth/OAuthClient.java index 1c7b549f..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; @@ -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/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 edd310fc..b265204b 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 @@ -242,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/TranscriptionService.java b/api/src/main/java/com/coze/openapi/service/service/audio/TranscriptionService.java index 54bad1f5..8b5d778b 100644 --- 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 @@ -38,9 +38,11 @@ public CreateTranscriptionsResp create(CreateTranscriptionsReq req) { private CreateTranscriptionsResp 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/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/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); From 75c382a84cb7ea6022ba77ce8bcc25f566581f3c Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 17:49:36 +0800 Subject: [PATCH 08/50] fix ut Change-Id: I2a722f8f59595f52b5481369b3cd7738009ae7ba --- .../service/auth/DeviceOAuthClientTest.java | 18 ++++++++++-------- .../service/APIResponseCallAdapterTest.java | 2 +- .../service/audio/SpeechServiceTest.java | 2 +- .../workflow/WorkFlowRunServiceTest.java | 2 +- .../coze/openapi/service/utils/UtilsTest.java | 4 +++- 5 files changed, 16 insertions(+), 12 deletions(-) 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/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/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 eed63fd9..acebdb6c 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; @@ -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); // 执行测试,应该抛出异常 From f35b3c2c6008269b7d5719b33dddb80b88d34d8a Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 19:33:30 +0800 Subject: [PATCH 09/50] fix ci Change-Id: If3e68477b3a5d030f0fa356ed7048c3138336db9 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86ca143c..db07a541 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: - name: Set up JDK ${{ matrix.java-version }} uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: ${{ matrix.java-version }} cache: 'maven' - name: Code style check From b57805e4307a08edb4f0bc34fe061163752311a0 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 19:39:02 +0800 Subject: [PATCH 10/50] fix ci Change-Id: I0a50066141fcc8b7f3bc44d815b7761e56ba39c9 --- api/pom.xml | 7 ++++--- .../java/com/coze/openapi/service/utils/UtilsTest.java | 5 +++++ pom.xml | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index 074591ef..159d030a 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -280,7 +280,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.2.5 **/*Test.java @@ -288,9 +288,10 @@ false 1 true - @{argLine} -Djava.security.egd=file:/dev/./urandom + @{argLine} - jdk + jdk-8 + file:/dev/./urandom 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 acebdb6c..f5aaf151 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 @@ -133,6 +133,11 @@ public void testJsonConversion() { @Test public void testGenRandomSign() { + // 添加调试信息 + System.out.println("OS Name: " + System.getProperty("os.name")); + System.out.println("OS Version: " + System.getProperty("os.version")); + System.out.println("OS Arch: " + System.getProperty("os.arch")); + // 测试生成16位随机字符串 String sign = Utils.genRandomSign(16); assertNotNull(sign); diff --git a/pom.xml b/pom.xml index ee428297..6464dce4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 1.8 1.8 1.18.24 - 5.8.2 + 5.10.2 From 7315347cc6eb9160c70c5cd4de0ac0a715f4c3ea Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 19:41:38 +0800 Subject: [PATCH 11/50] fix ci Change-Id: I2c29b92667a8000a0197bcc16a77045f0154d32b --- .../test/java/com/coze/openapi/service/utils/UtilsTest.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 f5aaf151..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 @@ -133,11 +133,7 @@ public void testJsonConversion() { @Test public void testGenRandomSign() { - // 添加调试信息 - System.out.println("OS Name: " + System.getProperty("os.name")); - System.out.println("OS Version: " + System.getProperty("os.version")); - System.out.println("OS Arch: " + System.getProperty("os.arch")); - + // 测试生成16位随机字符串 String sign = Utils.genRandomSign(16); assertNotNull(sign); From 65e3689c1469ca6e56870a6219187fb7f1107aec Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 19:45:30 +0800 Subject: [PATCH 12/50] fix ci Change-Id: I91567f5444973dde68e6cc8d3a25ed84894ab656 --- .github/workflows/ci.yml | 3 +++ api/pom.xml | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db07a541..15dbdf91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,9 @@ jobs: run: mvn spotless:check - name: Build and Test with Coverage run: | + if [ "${{ matrix.java-version }}" = "8" ]; then + export MAVEN_OPTS="-Djava.security.manager=allow -Djava.security.egd=file:/dev/urandom -Djava.security.properties=${JAVA_HOME}/lib/security/java.security" + fi mvn -pl api clean test-compile mvn -pl api test jacoco:report - name: Debug Test Results (Unix) diff --git a/api/pom.xml b/api/pom.xml index 159d030a..b2c7d43c 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -288,10 +288,9 @@ false 1 true - @{argLine} + @{argLine} -Djava.security.manager=allow -Djava.security.egd=file:/dev/urandom -Djava.security.properties=${project.basedir}/src/test/resources/java.security jdk-8 - file:/dev/./urandom From 4e85b6f758ed4369e0f0551758c3990b068975bd Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 19:47:47 +0800 Subject: [PATCH 13/50] fix ci Change-Id: Icf3b9be95b603276e10901af80f4cd271b22aded --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15dbdf91..db07a541 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,6 @@ jobs: run: mvn spotless:check - name: Build and Test with Coverage run: | - if [ "${{ matrix.java-version }}" = "8" ]; then - export MAVEN_OPTS="-Djava.security.manager=allow -Djava.security.egd=file:/dev/urandom -Djava.security.properties=${JAVA_HOME}/lib/security/java.security" - fi mvn -pl api clean test-compile mvn -pl api test jacoco:report - name: Debug Test Results (Unix) From c5ed443336ef1020d92da13bf8a20fd3557c4028 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:00:02 +0800 Subject: [PATCH 14/50] fix ci Change-Id: I9fba369125eae78b1a2d4c68a5229e4129fd88cf --- api/pom.xml | 6 +++++- .../client/connversations/message/model/Message.java | 3 +++ .../service/service/audio/TranscriptionService.java | 8 ++++---- .../audio/speech/WebsocketAudioSpeechClient.java | 5 ----- .../service/websocket/common/BaseWebSocketListener.java | 6 ++++++ .../service/websocket/common/BaseWebsocketClient.java | 6 +++++- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index b2c7d43c..7aa44ac9 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -288,10 +288,14 @@ false 1 true - @{argLine} -Djava.security.manager=allow -Djava.security.egd=file:/dev/urandom -Djava.security.properties=${project.basedir}/src/test/resources/java.security + @{argLine} jdk-8 + file:/dev/urandom + + -Djava.security.manager=allow + 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 aca7f4a4..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 @@ -78,6 +78,9 @@ public class Message { 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]; 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 index 8b5d778b..f003d2b8 100644 --- 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 @@ -14,10 +14,10 @@ import okhttp3.RequestBody; public class TranscriptionService { - private final AudioTranscriptionAPI roomApi; + private final AudioTranscriptionAPI transcriptionAPI; - public TranscriptionService(AudioTranscriptionAPI roomApi) { - this.roomApi = roomApi; + public TranscriptionService(AudioTranscriptionAPI transcriptionAPI) { + this.transcriptionAPI = transcriptionAPI; } public CreateTranscriptionsResp create(CreateTranscriptionsReq req) { @@ -47,7 +47,7 @@ private CreateTranscriptionsResp uploadFile(Object fileSource, String filename, MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, requestFile); - BaseResponse resp = Utils.execute(roomApi.create(body, req)); + 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/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java index 39c45a8a..f5fb9cf5 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java @@ -89,9 +89,4 @@ protected void handleEvent(WebSocket ws, String text) { handler.onClientException(this, new RuntimeException(e)); } } - - public void close() { - this.close(); - executorService.shutdown(); - } } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java index e7dbec85..d2e11d0a 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java @@ -53,4 +53,10 @@ public void onFailure(WebSocket webSocket, Throwable t, okhttp3.Response respons 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/service/websocket/common/BaseWebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java index de2bdf6f..2581fd68 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java @@ -17,12 +17,14 @@ public abstract class BaseWebsocketClient { protected final WebSocket ws; protected final ExecutorService executorService; protected static final int CLOSE_TIMEOUT_SECONDS = 10; + protected final BaseWebSocketListener listener; protected BaseWebsocketClient(OkHttpClient client, String url, BaseCallbackHandler handler) { Request request = new Request.Builder().url(url).build(); this.executorService = Executors.newSingleThreadExecutor(); + this.listener = new BaseWebSocketListener(this::handleEvent, handler, this); this.ws = - client.newWebSocket(request, new BaseWebSocketListener(this::handleEvent, handler, this)); + client.newWebSocket(request, this.listener); } protected void sendEvent(BaseEvent event) { @@ -53,6 +55,8 @@ public void close() { Thread.currentThread().interrupt(); // 强制关闭 executorService.shutdownNow(); + }finally { + this.listener.shutdown(); } } } From d8835b20168445e35e5422e0788931d28ada6805 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:01:02 +0800 Subject: [PATCH 15/50] fix ci Change-Id: I9f727a09a857e896f703b39acfcbc08468dbac8e --- .../service/websocket/common/BaseWebSocketListener.java | 8 ++++---- .../service/websocket/common/BaseWebsocketClient.java | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java index d2e11d0a..b3c96aed 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java @@ -55,8 +55,8 @@ public void onClosed(WebSocket webSocket, int code, String reason) { } public void shutdown() { - if (!executorService.isShutdown()) { - executorService.shutdown(); - } - } + if (!executorService.isShutdown()) { + executorService.shutdown(); + } + } } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java index 2581fd68..9f49f706 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java @@ -23,8 +23,7 @@ protected BaseWebsocketClient(OkHttpClient client, String url, BaseCallbackHandl Request request = new Request.Builder().url(url).build(); this.executorService = Executors.newSingleThreadExecutor(); this.listener = new BaseWebSocketListener(this::handleEvent, handler, this); - this.ws = - client.newWebSocket(request, this.listener); + this.ws = client.newWebSocket(request, this.listener); } protected void sendEvent(BaseEvent event) { @@ -55,7 +54,7 @@ public void close() { Thread.currentThread().interrupt(); // 强制关闭 executorService.shutdownNow(); - }finally { + } finally { this.listener.shutdown(); } } From 7f515244711b1bb3167d9bd431e2060af902bb26 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:03:04 +0800 Subject: [PATCH 16/50] fix ci Change-Id: Ifbfa4ea0f72eb0fc1e2a90fc7182230584c6bc93 --- api/pom.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index 7aa44ac9..2d5b16d9 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -293,9 +293,6 @@ jdk-8 file:/dev/urandom - - -Djava.security.manager=allow - From 43be7ee090fd951c3fe58b4670ea04f92d910822 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:06:56 +0800 Subject: [PATCH 17/50] fix ci Change-Id: Id29221110273970f4539c205a63f664b5a45363e --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db07a541..75a5ff2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,8 @@ jobs: - name: Code style check run: mvn spotless:check - name: Build and Test with Coverage + env: + MAVEN_OPTS: ${{ matrix.java-version == '8' && '-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider -Djava.security.egd=file:/dev/urandom' || '' }} run: | mvn -pl api clean test-compile mvn -pl api test jacoco:report From 1fdab4d5ff4818bea5af82bff50361e7ea127ea1 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:09:20 +0800 Subject: [PATCH 18/50] fix ci Change-Id: Ib4308d93d8c8f29f5d9ab8fb1434d98e9e3bce2f --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75a5ff2e..671ebec8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: run: mvn spotless:check - name: Build and Test with Coverage env: - MAVEN_OPTS: ${{ matrix.java-version == '8' && '-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider -Djava.security.egd=file:/dev/urandom' || '' }} + MAVEN_OPTS: ${{ matrix.java-version == '8' && '-Dos.name=Linux' || '' }} run: | mvn -pl api clean test-compile mvn -pl api test jacoco:report From be44ab1fd609ab7f565556f615f1acd3778e0c11 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:12:08 +0800 Subject: [PATCH 19/50] fix ci Change-Id: Ia276624409a649d29b19fa54d39392f1c12804b7 --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 671ebec8..fd9d3c38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,11 @@ jobs: cache: 'maven' - name: Code style check run: mvn spotless:check + - name: Setup Java 8 environment + if: matrix.java-version == '8' + run: | + sudo apt-get update + sudo apt-get install -y libnss3-dev - name: Build and Test with Coverage env: MAVEN_OPTS: ${{ matrix.java-version == '8' && '-Dos.name=Linux' || '' }} From 4fcadd303c412ea68e45bc444ac5f2b5c55b231c Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:13:58 +0800 Subject: [PATCH 20/50] fix ci Change-Id: I1a4d0628cf8fbb947bd5f324cf1f3d346e96d45f --- .github/workflows/ci.yml | 4 +++- api/pom.xml | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd9d3c38..6bba2224 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,9 @@ jobs: if: matrix.java-version == '8' run: | sudo apt-get update - sudo apt-get install -y libnss3-dev + sudo apt-get install -y libnss3-dev libsunec + sudo mkdir -p /usr/java/packages/lib/amd64 + sudo ln -s /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so /usr/java/packages/lib/amd64/ - name: Build and Test with Coverage env: MAVEN_OPTS: ${{ matrix.java-version == '8' && '-Dos.name=Linux' || '' }} diff --git a/api/pom.xml b/api/pom.xml index 2d5b16d9..d146f985 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -292,6 +292,10 @@ jdk-8 file:/dev/urandom + ${java.home} + ${project.build.directory}/tmp + amd64 + Linux From 7ee1a3ed9a498e0883860646e2c365e62aee198b Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:16:37 +0800 Subject: [PATCH 21/50] fix ci Change-Id: Iadf7f78881352c7053c8c371993b4730a36d28a9 --- api/pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index d146f985..2d5b16d9 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -292,10 +292,6 @@ jdk-8 file:/dev/urandom - ${java.home} - ${project.build.directory}/tmp - amd64 - Linux From b1294a558877b8218024a4f8b64e833ce5193e31 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:18:04 +0800 Subject: [PATCH 22/50] fix ci Change-Id: Id107efc31f4da187a7515aa45e71b6702e990766 --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bba2224..ae02f81c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,9 +36,12 @@ jobs: if: matrix.java-version == '8' run: | sudo apt-get update - sudo apt-get install -y libnss3-dev libsunec + sudo apt-get install -y libnss3-dev + # 创建目录并链接 Java 8 自带的 libsunec.so sudo mkdir -p /usr/java/packages/lib/amd64 - sudo ln -s /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so /usr/java/packages/lib/amd64/ + sudo ln -sf $(readlink -f /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so) /usr/java/packages/lib/amd64/ + # 确保文件存在 + ls -l /usr/java/packages/lib/amd64/libsunec.so - name: Build and Test with Coverage env: MAVEN_OPTS: ${{ matrix.java-version == '8' && '-Dos.name=Linux' || '' }} From 9b400d7c891e447118912403715d615b70aca426 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:22:52 +0800 Subject: [PATCH 23/50] fix ci Change-Id: Ib5cfa3263c124fb0c7ca0a326fbfb60399513ae7 --- .github/workflows/ci.yml | 10 ---------- api/pom.xml | 2 ++ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae02f81c..671ebec8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,16 +32,6 @@ jobs: cache: 'maven' - name: Code style check run: mvn spotless:check - - name: Setup Java 8 environment - if: matrix.java-version == '8' - run: | - sudo apt-get update - sudo apt-get install -y libnss3-dev - # 创建目录并链接 Java 8 自带的 libsunec.so - sudo mkdir -p /usr/java/packages/lib/amd64 - sudo ln -sf $(readlink -f /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so) /usr/java/packages/lib/amd64/ - # 确保文件存在 - ls -l /usr/java/packages/lib/amd64/libsunec.so - name: Build and Test with Coverage env: MAVEN_OPTS: ${{ matrix.java-version == '8' && '-Dos.name=Linux' || '' }} diff --git a/api/pom.xml b/api/pom.xml index 2d5b16d9..a2ed51de 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -292,6 +292,8 @@ jdk-8 file:/dev/urandom + NONE + NONE From 51f1f1ee403c66f0352454ace04ee864f4119c08 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:26:59 +0800 Subject: [PATCH 24/50] fix ci Change-Id: If912f0f959946fc6b4b8cc8bdeef50a3f22d63a6 --- .github/workflows/ci.yml | 7 +++++++ api/pom.xml | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 671ebec8..ad112796 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,13 @@ jobs: distribution: 'temurin' java-version: ${{ matrix.java-version }} cache: 'maven' + - name: Setup Java 8 environment + if: matrix.java-version == '8' + run: | + sudo apt-get update + sudo apt-get install -y libnss3-dev + sudo mkdir -p /usr/java/packages/lib/amd64 + sudo ln -sf $(readlink -f /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so) /usr/java/packages/lib/amd64/ - name: Code style check run: mvn spotless:check - name: Build and Test with Coverage diff --git a/api/pom.xml b/api/pom.xml index a2ed51de..e5bd0a83 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -292,8 +292,8 @@ jdk-8 file:/dev/urandom - NONE - NONE + ${java.home}/lib/security/cacerts + changeit From 680a6eb0365f0528093e29096dc619abeec62189 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:30:22 +0800 Subject: [PATCH 25/50] fix ci Change-Id: Ib529aa9b182293c3183ad706a72bebd7e05c323c --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad112796..121b9265 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,12 @@ jobs: run: mvn spotless:check - name: Build and Test with Coverage env: - MAVEN_OPTS: ${{ matrix.java-version == '8' && '-Dos.name=Linux' || '' }} + MAVEN_OPTS: >- + ${{ matrix.java-version == '8' && ' + -Djava.nio.fs.spi.DefaultFileSystemProvider=sun.nio.fs.LinuxFileSystemProvider + -Dos.name=Linux + -Dos.arch=amd64 + ' || '' }} run: | mvn -pl api clean test-compile mvn -pl api test jacoco:report From 159abbef0f6ec88d26a41abbf3b4b338dcca43a0 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:39:43 +0800 Subject: [PATCH 26/50] fix ci Change-Id: If7d66fa059e00e089bf6fdc125b4eb080ad7844e --- .github/workflows/ci.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 121b9265..bddc4952 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,10 +33,27 @@ jobs: - name: Setup Java 8 environment if: matrix.java-version == '8' run: | + # 1. 安装基础依赖 sudo apt-get update sudo apt-get install -y libnss3-dev + + # 2. 确保所有可能的库路径都存在 sudo mkdir -p /usr/java/packages/lib/amd64 - sudo ln -sf $(readlink -f /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so) /usr/java/packages/lib/amd64/ + sudo mkdir -p /usr/lib/jni + sudo mkdir -p /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server + + # 3. 复制而不是链接 native 库 + sudo cp /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so /usr/lib/ + sudo cp /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so /usr/java/packages/lib/amd64/ + sudo cp /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so /usr/lib/jni/ + + # 4. 设置正确的权限 + sudo chmod 755 /usr/lib/libsunec.so + sudo chmod 755 /usr/java/packages/lib/amd64/libsunec.so + sudo chmod 755 /usr/lib/jni/libsunec.so + + # 5. 更新库缓存 + sudo ldconfig - name: Code style check run: mvn spotless:check - name: Build and Test with Coverage From 1d592b80ecb92fea7a8c3a07b836e301e80c2065 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:41:34 +0800 Subject: [PATCH 27/50] fix ci Change-Id: I96b7c3df7a266dd96cf0b719c417c88e3ac36056 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bddc4952..c6f1e19f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - name: Setup Java 8 environment if: matrix.java-version == '8' run: | - # 1. 安装基础依赖 + # 1. 安装基础依赖 sudo apt-get update sudo apt-get install -y libnss3-dev From 931d8132c67936e6328728cc4d920c13c712c550 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:46:05 +0800 Subject: [PATCH 28/50] fix ci Change-Id: I4a5d5953920e5e9a3ac2f771e8488e50c94b3e18 --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6f1e19f..f1f50290 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,23 @@ jobs: # 5. 更新库缓存 sudo ldconfig + + # 6. 验证设置 + echo "=== Verifying library setup ===" + echo "Checking all libsunec.so locations:" + find / -name libsunec.so 2>/dev/null + + echo "=== Checking library dependencies ===" + for lib in /usr/lib/libsunec.so /usr/java/packages/lib/amd64/libsunec.so /usr/lib/jni/libsunec.so; do + echo "Checking $lib:" + ldd $lib || echo "Failed to check dependencies for $lib" + done + + echo "=== Library permissions ===" + ls -l /usr/lib/libsunec.so + ls -l /usr/java/packages/lib/amd64/libsunec.so + ls -l /usr/lib/jni/libsunec.so + - name: Code style check run: mvn spotless:check - name: Build and Test with Coverage From f9ce9d40439193e002d8c83399b2acbcaac00f37 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:50:55 +0800 Subject: [PATCH 29/50] fix ci Change-Id: Id20323dde4ca47920aca56cdb6b3c00c4d820869 --- .github/workflows/ci.yml | 54 +++++++--------------------------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1f50290..de995e6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,11 +17,16 @@ jobs: fail-fast: false matrix: java-version: [ "8", "11", "17" ] - os: [ "ubuntu-latest" ] - os-label: [ "Ubuntu" ] + os: [ "windows-latest", "macos-latest" ] + os-label: [ "Windows", "macOS" ] include: - - { java-version: "11", os: "windows-latest", os-label: "Windows" } - - { java-version: "11", os: "macos-latest", os-label: "macOS" } + - { java-version: "11", os: "ubuntu-latest", os-label: "Ubuntu" } + - { java-version: "17", os: "ubuntu-latest", os-label: "Ubuntu" } + exclude: + - os: "windows-latest" + os-label: "macOS" + - os: "macos-latest" + os-label: "Windows" steps: - uses: actions/checkout@v4 - name: Set up JDK ${{ matrix.java-version }} @@ -30,47 +35,6 @@ jobs: distribution: 'temurin' java-version: ${{ matrix.java-version }} cache: 'maven' - - name: Setup Java 8 environment - if: matrix.java-version == '8' - run: | - # 1. 安装基础依赖 - sudo apt-get update - sudo apt-get install -y libnss3-dev - - # 2. 确保所有可能的库路径都存在 - sudo mkdir -p /usr/java/packages/lib/amd64 - sudo mkdir -p /usr/lib/jni - sudo mkdir -p /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64/server - - # 3. 复制而不是链接 native 库 - sudo cp /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so /usr/lib/ - sudo cp /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so /usr/java/packages/lib/amd64/ - sudo cp /usr/lib/jvm/temurin-8-jdk-amd64/jre/lib/amd64/libsunec.so /usr/lib/jni/ - - # 4. 设置正确的权限 - sudo chmod 755 /usr/lib/libsunec.so - sudo chmod 755 /usr/java/packages/lib/amd64/libsunec.so - sudo chmod 755 /usr/lib/jni/libsunec.so - - # 5. 更新库缓存 - sudo ldconfig - - # 6. 验证设置 - echo "=== Verifying library setup ===" - echo "Checking all libsunec.so locations:" - find / -name libsunec.so 2>/dev/null - - echo "=== Checking library dependencies ===" - for lib in /usr/lib/libsunec.so /usr/java/packages/lib/amd64/libsunec.so /usr/lib/jni/libsunec.so; do - echo "Checking $lib:" - ldd $lib || echo "Failed to check dependencies for $lib" - done - - echo "=== Library permissions ===" - ls -l /usr/lib/libsunec.so - ls -l /usr/java/packages/lib/amd64/libsunec.so - ls -l /usr/lib/jni/libsunec.so - - name: Code style check run: mvn spotless:check - name: Build and Test with Coverage From 143248fb689bedd2220360dbe9752b75149afa25 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:54:53 +0800 Subject: [PATCH 30/50] fix ci Change-Id: Ica60161b875b65feac8a01a0fb2181d2b6044219 --- .github/workflows/ci.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de995e6d..ae9611aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,23 +16,31 @@ jobs: strategy: fail-fast: false matrix: - java-version: [ "8", "11", "17" ] - os: [ "windows-latest", "macos-latest" ] - os-label: [ "Windows", "macOS" ] + java-version: [ "11", "17" ] + os: [ "ubuntu-latest", "windows-latest", "macos-latest" ] + os-label: [ "Ubuntu", "Windows", "macOS" ] include: - - { java-version: "11", os: "ubuntu-latest", os-label: "Ubuntu" } - - { java-version: "17", os: "ubuntu-latest", os-label: "Ubuntu" } + - { java-version: "8", os: "windows-latest", os-label: "Windows", distribution: "temurin" } + - { 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: 'temurin' + distribution: ${{ matrix.java-version == '8' && matrix.os == 'macos-latest' && 'zulu' || 'temurin' }} java-version: ${{ matrix.java-version }} cache: 'maven' - name: Code style check From dc60f14bdb20c2abd94c810b29c0c9a806047854 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 20:57:52 +0800 Subject: [PATCH 31/50] fix ci Change-Id: Id7e62e721bce7b29c29f8d1d5561c75ccd293818 --- .github/workflows/ci.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae9611aa..0cae4f93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,16 +43,22 @@ jobs: 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 - env: - MAVEN_OPTS: >- - ${{ matrix.java-version == '8' && ' - -Djava.nio.fs.spi.DefaultFileSystemProvider=sun.nio.fs.LinuxFileSystemProvider - -Dos.name=Linux - -Dos.arch=amd64 - ' || '' }} run: | mvn -pl api clean test-compile mvn -pl api test jacoco:report From ef75f9c3e2063b92e72c301eca8d94074bc60562 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 3 Mar 2025 21:01:10 +0800 Subject: [PATCH 32/50] fix ci Change-Id: I8c09c098b8feb4387f945acdb13a582dc933facf --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cae4f93..39648bbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,6 @@ jobs: os: [ "ubuntu-latest", "windows-latest", "macos-latest" ] os-label: [ "Ubuntu", "Windows", "macOS" ] include: - - { java-version: "8", os: "windows-latest", os-label: "Windows", distribution: "temurin" } - { java-version: "8", os: "macos-latest", os-label: "macOS", distribution: "zulu" } exclude: - os: "windows-latest" From c69b6a40fbb26b97da8650014c41a23eec2c8740 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Tue, 4 Mar 2025 20:35:35 +0800 Subject: [PATCH 33/50] fix ci Change-Id: Id8952000182a9c09296cf357bb10f5a7439d474a --- README.md | 122 ++++++++++++++ api/pom.xml | 2 +- .../speech/WebsocketAudioSpeechClient.java | 10 +- .../WebsocketAudioTranscriptionsClient.java | 6 +- .../websocket/chat/WebsocketChatClient.java | 22 +-- .../websocket/common/BaseWebsocketClient.java | 30 +++- .../service/utils/UserAgentInterceptor.java | 2 +- .../audio/TranscriptionServiceTest.java | 129 +++++++++++++++ .../WebsocketAudioSpeechClientTest.java | 37 +++++ ...ebsocketAudioTranscriptionsClientTest.java | 56 +++++++ .../chat/WebsocketChatClientTest.java | 121 ++++++++++++++ .../common/BaseWebSocketListenerTest.java | 155 ++++++++++++++++++ .../common/BaseWebsocketClientTest.java | 150 +++++++++++++++++ example/pom.xml | 2 +- pom.xml | 4 + 15 files changed, 815 insertions(+), 33 deletions(-) create mode 100644 api/src/test/java/com/coze/openapi/service/service/audio/TranscriptionServiceTest.java create mode 100644 api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListenerTest.java create mode 100644 api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java diff --git a/README.md b/README.md index cf225ca0..002cfeb4 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 base64AudioData = "..."; // Base64 encoded audio data +client.inputAudioBufferAppend(base64AudioData); +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 base64AudioData = "..."; // Base64 encoded audio data +client.inputAudioBufferAppend(base64AudioData); +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 e5bd0a83..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 diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java index f5fb9cf5..cbaf3e89 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java @@ -1,8 +1,5 @@ package com.coze.openapi.service.service.websocket.audio.speech; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - 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; @@ -19,15 +16,14 @@ public class WebsocketAudioSpeechClient extends BaseWebsocketClient { private final ObjectMapper objectMapper = Utils.getMapper(); private final WebsocketAudioSpeechCallbackHandler handler; private static final String uri = "/v1/audio/speech"; - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); protected WebsocketAudioSpeechClient( OkHttpClient client, String wsHost, WebsocketAudioSpeechCreateReq req) { - super(client, buildUrl(wsHost, uri), req.getCallbackHandler()); + super(client, buildUrl(wsHost), req.getCallbackHandler(), req); this.handler = req.getCallbackHandler(); } - protected static String buildUrl(String wsHost, String uri) { + protected static String buildUrl(String wsHost) { return String.format("%s%s", wsHost, uri); } @@ -83,7 +79,7 @@ protected void handleEvent(WebSocket ws, String text) { handler.onError(this, errorEvent); break; default: - System.out.println("未知事件类型: " + eventType); + 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/WebsocketAudioTranscriptionsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java index cd5910f6..740714e7 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java @@ -16,11 +16,11 @@ public class WebsocketAudioTranscriptionsClient extends BaseWebsocketClient { protected WebsocketAudioTranscriptionsClient( OkHttpClient client, String wsHost, WebsocketAudioTranscriptionsCreateReq req) { - super(client, buildUrl(wsHost, uri), req.getCallbackHandler()); + super(client, buildUrl(wsHost), req.getCallbackHandler(), req); this.handler = req.getCallbackHandler(); } - protected static String buildUrl(String wsHost, String uri) { + protected static String buildUrl(String wsHost) { return String.format("%s%s", wsHost, uri); } @@ -90,7 +90,7 @@ protected void handleEvent(WebSocket ws, String text) { handler.onError(this, errorEvent); break; default: - System.out.println("未知事件类型: " + eventType); + 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/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java index 512e91fe..08696c7f 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java @@ -1,8 +1,5 @@ package com.coze.openapi.service.service.websocket.chat; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - 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.*; @@ -27,21 +24,16 @@ public class WebsocketChatClient extends BaseWebsocketClient { private static final String uri = "/v1/chat"; - private final WebsocketChatCreateReq req; - - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); - protected WebsocketChatClient(OkHttpClient client, String wsHost, WebsocketChatCreateReq req) { - super(client, buildUrl(wsHost, uri, req), req.getCallbackHandler()); + super(client, buildUrl(wsHost, req), req.getCallbackHandler(), req); this.handler = req.getCallbackHandler(); - this.req = req; } - protected static String buildUrl(String wsHost, String uri, WebsocketChatCreateReq req) { + protected static String buildUrl(String wsHost, WebsocketChatCreateReq req) { return String.format("%s%s?bot_id=%s", wsHost, uri, req.getBotID()); } - private void chatUpdate(ChatUpdateEventData data) { + public void chatUpdate(ChatUpdateEventData data) { this.sendEvent(ChatUpdateEvent.builder().data(data).build()); } @@ -175,16 +167,10 @@ protected void handleEvent(WebSocket ws, String text) { handler.onError(this, errorEvent); break; default: - // todo 用 log - System.out.println("未知事件类型: " + eventType); + logger.error("unknown event type: {}, event string: {}", eventType, text); } } catch (Exception e) { handler.onClientException(this, new RuntimeException(e)); } } - - public void close() { - this.ws.close(1000, null); - executorService.shutdown(); - } } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java index 9f49f706..380cef54 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java @@ -4,7 +4,11 @@ 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.ObjectMapper; @@ -14,16 +18,38 @@ public abstract class BaseWebsocketClient { 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 BaseWebSocketListener listener; - protected BaseWebsocketClient(OkHttpClient client, String url, BaseCallbackHandler handler) { + protected BaseWebsocketClient( + OkHttpClient client, String url, BaseCallbackHandler handler, BaseReq req) { Request request = new Request.Builder().url(url).build(); this.executorService = Executors.newSingleThreadExecutor(); this.listener = new BaseWebSocketListener(this::handleEvent, handler, this); - this.ws = client.newWebSocket(request, this.listener); + 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) { 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/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 index b63071e7..38a24d9e 100644 --- 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 @@ -14,6 +14,9 @@ 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; @@ -228,4 +231,38 @@ public void testHandleInvalidJson() { 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 index 2a5aa3e9..26103f6e 100644 --- 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 @@ -2,6 +2,7 @@ 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.*; @@ -14,6 +15,9 @@ 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 com.coze.openapi.client.websocket.event.upstream.InputAudioBufferAppendEvent; import okhttp3.OkHttpClient; import okhttp3.WebSocket; @@ -271,4 +275,56 @@ public void testHandleInvalidJson() { 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() { + InputAudioBufferAppendEvent.Data data = + new InputAudioBufferAppendEvent.Data("base64EncodedAudioData"); + + 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/WebsocketChatClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java index 1e86e8c7..27a55f7d 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java @@ -15,8 +15,12 @@ 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 com.coze.openapi.client.websocket.event.upstream.InputAudioBufferAppendEvent; import okhttp3.OkHttpClient; import okhttp3.WebSocket; @@ -32,6 +36,7 @@ public class WebsocketChatClientTest { @Captor private ArgumentCaptor chatUpdatedEventCaptor; @Captor private ArgumentCaptor audioCompletedEventCaptor; @Captor private ArgumentCaptor audioDeltaEventCaptor; + @Captor private ArgumentCaptor conversationMessageDeltaEventCaptor; @Captor private ArgumentCaptor audioTranscriptUpdateEventCaptor; @@ -66,6 +71,7 @@ public void setup() { .botID("test-bot-id") .callbackHandler(mockCallbackHandler) .build(); + client = new WebsocketChatClient(mockOkHttpClient, "ws://test.com", req); } @@ -742,6 +748,50 @@ public void testHandleUnknownEvent() { 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"; @@ -750,4 +800,75 @@ public void testHandleInvalidJson() { 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(new InputAudioBufferAppendEvent.Data("hello")); + + verify(mockWebSocket).send(anyString()); + } + + @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/BaseWebSocketListenerTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListenerTest.java new file mode 100644 index 00000000..c86216f5 --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListenerTest.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 BaseWebSocketListenerTest { + + @Mock private WebSocket mockWebSocket; + private Response mockResponse; + @Mock private BaseCallbackHandler mockCallbackHandler; + @Mock private BaseWebsocketClient mockClient; + @Mock private BiConsumer mockHandleEvent; + + private BaseWebSocketListener 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 BaseWebSocketListener(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/websocket/common/BaseWebsocketClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java new file mode 100644 index 00000000..d9bb8223 --- /dev/null +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java @@ -0,0 +1,150 @@ +package com.coze.openapi.service.service.websocket.common; + +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.websocket.common.BaseEvent; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.WebSocket; + +class BaseWebsocketClientTest { + + @Mock private OkHttpClient mockOkHttpClient; + @Mock private WebSocket mockWebSocket; + @Mock private BaseCallbackHandler mockCallbackHandler; + @Mock private BaseWebSocketListener mockListener; + + private TestBaseWebsocketClient client; + + // 创建一个测试用的具体实现类 + private static class TestBaseWebsocketClient extends BaseWebsocketClient { + public TestBaseWebsocketClient(OkHttpClient client, String url, BaseCallbackHandler handler) { + super(client, url, handler, null); + } + + @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(BaseWebSocketListener.class))) + .thenReturn(mockWebSocket); + + client = new TestBaseWebsocketClient(mockOkHttpClient, "ws://test.com", mockCallbackHandler); + } + + @Test + void testInitialization() { + verify(mockOkHttpClient).newWebSocket(any(Request.class), any(BaseWebSocketListener.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 = + BaseWebsocketClient.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/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/pom.xml b/pom.xml index 6464dce4..95f0b2f8 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,10 @@ **/client/**/* + **/BaseCallbackHandler.java + **/WebsocketChatCallbackHandler.java + **/WebsocketAudioTranscriptionsCallbackHandler.java + **/WebsocketAudioSpeechCallbackHandler.java From c28e3656d85b11abd6676e3f6d91717fbeccd6d5 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 10:46:42 +0800 Subject: [PATCH 34/50] fix ci Change-Id: I22cde596577d5a542713b721e8fd93e398903bca --- .../WebsocketAudioSpeechClientTest.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 index 38a24d9e..ecde71b9 100644 --- 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 @@ -52,7 +52,7 @@ public void testHandleSpeechCreatedEvent() { + " \"id\": \"7446668538246561xxxx\",\n" + " \"event_type\": \"speech.created\",\n" + " \"detail\": {\n" - + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\"\n" + " }\n" + "}\n"; @@ -65,7 +65,7 @@ public void testHandleSpeechCreatedEvent() { assertEquals("7446668538246561xxxx", event.getId()); // 验证 detail - assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); } @Test @@ -85,7 +85,7 @@ public void testHandleSpeechUpdatedEvent() { + " }\n" + " },\n" + " \"detail\": {\n" - + " \"logid\": \"20241210152726467C48D89D6DB2F3***\" }\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\" }\n" + "}\n"; client.handleEvent(mockWebSocket, json); @@ -103,7 +103,7 @@ public void testHandleSpeechUpdatedEvent() { assertEquals("音色id", event.getData().getOutputAudio().getVoiceId()); // 验证 detail - assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); } @Test @@ -116,7 +116,7 @@ public void testHandleSpeechAudioUpdateEvent() { + " \"delta\": \"base64EncodedAudioDelta\"\n" + " },\n" + " \"detail\": {\n" - + " \"logid\": \"20241210152726467C48D89D6DB2F3***\" }\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\" }\n" + "}\n"; client.handleEvent(mockWebSocket, json); @@ -132,7 +132,7 @@ public void testHandleSpeechAudioUpdateEvent() { assertEquals("base64EncodedAudioDelta", event.getData().getDelta()); // 验证 detail - assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); } @Test @@ -142,7 +142,7 @@ public void testHandleSpeechAudioCompletedEvent() { + " \"id\": \"event_id\",\n" + " \"event_type\": \"speech.audio.completed\",\n" + " \"detail\": {\n" - + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\"\n" + " }\n" + "}\n"; @@ -156,7 +156,7 @@ public void testHandleSpeechAudioCompletedEvent() { assertEquals("event_id", event.getId()); // 验证 detail - assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); } @Test @@ -166,7 +166,7 @@ public void testHandleInputTextBufferCompletedEvent() { + " \"id\": \"event_id\",\n" + " \"event_type\": \"input_text_buffer.completed\",\n" + " \"detail\": {\n" - + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\"\n" + " }\n" + "}\n"; @@ -180,7 +180,7 @@ public void testHandleInputTextBufferCompletedEvent() { assertEquals("event_id", event.getId()); // 验证 detail - assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); } @Test @@ -194,7 +194,7 @@ public void testHandleErrorEvent() { + " \"msg\": \"error message\"\n" + " },\n" + " \"detail\": {\n" - + " \"logid\": \"20241210152726467C48D89D6DB2F3***\"\n" + + " \"logid\": \"20241210152726467C48D89D6DB2F***\"\n" + " }\n" + "}\n"; @@ -211,7 +211,7 @@ public void testHandleErrorEvent() { assertEquals("error message", event.getData().getMsg()); // 验证 detail - assertEquals("20241210152726467C48D89D6DB2F3***", event.getDetail().getLogID()); + assertEquals("20241210152726467C48D89D6DB2F***", event.getDetail().getLogID()); } @Test From 2cc61530696d81ece8f8bada7cd99ccac6700557 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 10:49:46 +0800 Subject: [PATCH 35/50] fix cr Change-Id: I6ef5e32548c01bf65603e49489f1b02377d8fd13 --- .../service/websocket/chat/WebsocketChatClient.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java index 08696c7f..db46dd0e 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java @@ -74,7 +74,13 @@ protected void handleEvent(WebSocket ws, String text) { try { // 解析 JSON JsonNode jsonNode = objectMapper.readTree(text); - String eventType = jsonNode.get("event_type").asText(); + JsonNode eventTypeNode = jsonNode.get("event_type"); + if (eventTypeNode == null) { + logger.error("Missing event_type field in event: {}", text); + handler.onClientException(this, new RuntimeException("Missing event_type field in event")); + return; + } + String eventType = eventTypeNode.asText(); switch (eventType) { case EventType.CHAT_CREATED: From 8f120723ec364bd194af94d21da3119d7831bd52 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 10:53:50 +0800 Subject: [PATCH 36/50] fix code format Change-Id: I390163dd72e1517f9a60ff8762f5c89bf25c3f1e --- .../service/websocket/chat/WebsocketChatClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java index db46dd0e..bd712fef 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java @@ -76,10 +76,10 @@ protected void handleEvent(WebSocket ws, String text) { JsonNode jsonNode = objectMapper.readTree(text); JsonNode eventTypeNode = jsonNode.get("event_type"); if (eventTypeNode == null) { - logger.error("Missing event_type field in event: {}", text); - handler.onClientException(this, new RuntimeException("Missing event_type field in event")); - return; - } + logger.error("Missing event_type field in event: {}", text); + handler.onClientException(this, new RuntimeException("Missing event_type field in event")); + return; + } String eventType = eventTypeNode.asText(); switch (eventType) { From f3f5e9fa5f9cd440dfa2bd62fcfdcab6e3145add Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 11:25:59 +0800 Subject: [PATCH 37/50] fix: cr Change-Id: I5f681076dda513eee81b517f73aa4578fe0c1e98 --- .../audio/speech/WebsocketAudioSpeechClient.java | 8 +++++++- .../WebsocketAudioTranscriptionsClient.java | 9 +++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java index cbaf3e89..400ee804 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java @@ -46,7 +46,13 @@ public void inputTextBufferComplete() { protected void handleEvent(WebSocket ws, String text) { try { JsonNode jsonNode = objectMapper.readTree(text); - String eventType = jsonNode.get("event_type").asText(); + JsonNode eventTypeNode = jsonNode.get("event_type"); + if (eventTypeNode == null) { + logger.error("Missing event_type field in event: {}", text); + handler.onClientException(this, new RuntimeException("Missing event_type field in event")); + return; + } + String eventType = eventTypeNode.asText(); switch (eventType) { case EventType.SPEECH_CREATED: diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java index 740714e7..5f0b6ddf 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java @@ -52,8 +52,13 @@ public void inputAudioBufferComplete() { protected void handleEvent(WebSocket ws, String text) { try { JsonNode jsonNode = objectMapper.readTree(text); - String eventType = jsonNode.get("event_type").asText(); - + JsonNode eventTypeNode = jsonNode.get("event_type"); + if (eventTypeNode == null) { + logger.error("Missing event_type field in event: {}", text); + handler.onClientException(this, new RuntimeException("Missing event_type field in event")); + return; + } + String eventType = eventTypeNode.asText(); switch (eventType) { case EventType.TRANSCRIPTIONS_CREATED: TranscriptionsCreatedEvent createdEvent = From 7b745dd227e2e5a7b7ce1ef65dca2fab90da0e85 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 11:43:20 +0800 Subject: [PATCH 38/50] fix: code format Change-Id: I14af44d6d0467b53aa342d2813163d5aa6c0405f --- .../audio/speech/WebsocketAudioSpeechClient.java | 14 +++++++------- .../WebsocketAudioTranscriptionsClient.java | 14 +++++++------- .../websocket/chat/WebsocketChatClient.java | 15 +++++++-------- .../websocket/common/BaseWebsocketClient.java | 14 ++++++++++++++ .../websocket/common/BaseWebsocketClientTest.java | 8 ++++++++ 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java index 400ee804..dfa31df0 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java @@ -4,6 +4,7 @@ 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.BaseWebsocketClient; import com.coze.openapi.service.utils.Utils; import com.fasterxml.jackson.databind.JsonNode; @@ -42,17 +43,16 @@ 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); - JsonNode eventTypeNode = jsonNode.get("event_type"); - if (eventTypeNode == null) { - logger.error("Missing event_type field in event: {}", text); - handler.onClientException(this, new RuntimeException("Missing event_type field in event")); - return; - } - String eventType = eventTypeNode.asText(); + String eventType = parseEventType(jsonNode, text); switch (eventType) { case EventType.SPEECH_CREATED: diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java index 5f0b6ddf..a25b65f1 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java @@ -4,6 +4,7 @@ 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.BaseWebsocketClient; import com.fasterxml.jackson.databind.JsonNode; @@ -48,17 +49,16 @@ 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); - JsonNode eventTypeNode = jsonNode.get("event_type"); - if (eventTypeNode == null) { - logger.error("Missing event_type field in event: {}", text); - handler.onClientException(this, new RuntimeException("Missing event_type field in event")); - return; - } - String eventType = eventTypeNode.asText(); + String eventType = parseEventType(jsonNode, text); switch (eventType) { case EventType.TRANSCRIPTIONS_CREATED: TranscriptionsCreatedEvent createdEvent = diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java index bd712fef..2652e9ce 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java @@ -8,6 +8,7 @@ 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.BaseWebsocketClient; import com.coze.openapi.service.utils.Utils; import com.fasterxml.jackson.databind.JsonNode; @@ -69,19 +70,17 @@ 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); - JsonNode eventTypeNode = jsonNode.get("event_type"); - if (eventTypeNode == null) { - logger.error("Missing event_type field in event: {}", text); - handler.onClientException(this, new RuntimeException("Missing event_type field in event")); - return; - } - String eventType = eventTypeNode.asText(); - + String eventType = parseEventType(jsonNode, text); switch (eventType) { case EventType.CHAT_CREATED: ChatCreatedEvent chatCreatedEvent = diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java index 380cef54..fe8b06bb 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java @@ -10,6 +10,7 @@ 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; @@ -58,6 +59,19 @@ protected void sendEvent(BaseEvent 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 连接 diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java index d9bb8223..9f4f1549 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java @@ -27,8 +27,16 @@ class BaseWebsocketClientTest { // 创建一个测试用的具体实现类 private static class TestBaseWebsocketClient extends BaseWebsocketClient { + BaseCallbackHandler handler; + public TestBaseWebsocketClient(OkHttpClient client, String url, BaseCallbackHandler handler) { super(client, url, handler, null); + this.handler = handler; + } + + @Override + protected BaseCallbackHandler getCallbackHandler() { + return handler; } @Override From 92f47cbab520adc19599a57c496e1825f1253c72 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 14:44:45 +0800 Subject: [PATCH 39/50] fix cr Change-Id: I2aea2ceb0247ed4798469dd0798431e77d091675 --- .../websocket/audio/speech/WebsocketAudioSpeechClient.java | 6 ++++-- .../transcriptions/WebsocketAudioTranscriptionsClient.java | 4 ++++ .../service/service/websocket/chat/WebsocketChatClient.java | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java index dfa31df0..3039d3fc 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java @@ -14,7 +14,6 @@ import okhttp3.WebSocket; public class WebsocketAudioSpeechClient extends BaseWebsocketClient { - private final ObjectMapper objectMapper = Utils.getMapper(); private final WebsocketAudioSpeechCallbackHandler handler; private static final String uri = "/v1/audio/speech"; @@ -53,7 +52,10 @@ 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 = diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java index a25b65f1..8c09e4d9 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java @@ -59,6 +59,10 @@ 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 = diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java index 2652e9ce..e2bd81cd 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java @@ -81,6 +81,10 @@ protected void handleEvent(WebSocket ws, String text) { // 解析 JSON JsonNode jsonNode = objectMapper.readTree(text); String eventType = parseEventType(jsonNode, text); + if (eventType == null) { + // 这里在上面已经抛出异常了,直接 return 即可 + return; + } switch (eventType) { case EventType.CHAT_CREATED: ChatCreatedEvent chatCreatedEvent = From 34793a8744ce4d9b3dd6dc94fd5ee5ca5b31df9f Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 14:50:59 +0800 Subject: [PATCH 40/50] fix code style Change-Id: I315a3b35a1730f8efe28924db584e8f68762ec82 --- .../websocket/audio/speech/WebsocketAudioSpeechClient.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java index 3039d3fc..8c35d124 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java @@ -6,9 +6,7 @@ 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.BaseWebsocketClient; -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; From a1f71cd9e0b4a9518ad51d92bb25abae06a2fb28 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 16:38:57 +0800 Subject: [PATCH 41/50] fix ci Change-Id: I35b0c2de8aeefdfcc1e9885757aafa4e08b1c2c0 --- .../common/BaseWebsocketClientTest.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java index 9f4f1549..50f85cd7 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java @@ -1,5 +1,6 @@ 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.*; @@ -10,6 +11,7 @@ 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; @@ -29,8 +31,9 @@ class BaseWebsocketClientTest { private static class TestBaseWebsocketClient extends BaseWebsocketClient { BaseCallbackHandler handler; - public TestBaseWebsocketClient(OkHttpClient client, String url, BaseCallbackHandler handler) { - super(client, url, handler, null); + public TestBaseWebsocketClient( + OkHttpClient client, String url, BaseCallbackHandler handler, BaseReq req) { + super(client, url, handler, req); this.handler = handler; } @@ -56,7 +59,20 @@ void setUp() { when(mockOkHttpClient.newWebSocket(any(Request.class), any(BaseWebSocketListener.class))) .thenReturn(mockWebSocket); - client = new TestBaseWebsocketClient(mockOkHttpClient, "ws://test.com", mockCallbackHandler); + 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 From 74bf190b4f8cb72e707c1b0cd72b3d1b6946be66 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 19:19:17 +0800 Subject: [PATCH 42/50] jwt refresh timing Change-Id: I0d3c4d6c138639b705a50319aa253b324a379ff2 --- .../com/coze/openapi/service/auth/Auth.java | 19 +------------------ .../coze/openapi/service/auth/JWTOAuth.java | 8 +++++++- .../coze/openapi/service/auth/TokenAuth.java | 1 - .../java/example/auth/JWTOAuthExample.java | 12 +++++++++++- 4 files changed, 19 insertions(+), 21 deletions(-) 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..d6cfd764 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,7 +1,5 @@ package com.coze.openapi.service.auth; -import com.coze.openapi.client.auth.OAuthToken; - public abstract class Auth { protected String accessToken; @@ -9,11 +7,6 @@ public abstract class Auth { protected long expiresIn; protected OAuthClient client; - protected boolean needRefresh() { - // accessToken 为空代表第一次请求,需要刷新token - return accessToken == null || System.currentTimeMillis() / 1000 > expiresIn; - } - /** * 获取token类型 * @@ -28,15 +21,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/JWTOAuth.java b/api/src/main/java/com/coze/openapi/service/auth/JWTOAuth.java index acaeaa20..64af22e8 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 @@ -16,6 +16,8 @@ public class JWTOAuth extends Auth { private Integer ttl; private String sessionName; private Scope scope; + private long refreshAt; + @Builder.Default private long refreshBefore = 30; @NonNull private JWTOAuthClient jwtClient; public JWTOAuth(JWTOAuthClient client) { @@ -24,7 +26,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 +37,7 @@ 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 - this.refreshBefore; return this.accessToken; } } 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/example/src/main/java/example/auth/JWTOAuthExample.java b/example/src/main/java/example/auth/JWTOAuthExample.java index 8af0c006..5cdc601e 100644 --- a/example/src/main/java/example/auth/JWTOAuthExample.java +++ b/example/src/main/java/example/auth/JWTOAuthExample.java @@ -78,7 +78,17 @@ 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) + // the sdk will automatically refresh the token before it expires, + // the default is 30s, you can also customize the refresh time + .refreshBefore(30) + .build()) + .baseURL(cozeAPIBase) + .build(); // you can also specify the scope and session for it } } From 5e609499eba70762cc9ab566955eb8681179b9f2 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 5 Mar 2025 19:53:42 +0800 Subject: [PATCH 43/50] jwt refresh timing Change-Id: I04e30f6f92f17df59a9d8ef880d4129b5fb1ddea --- .../java/com/coze/openapi/service/auth/Auth.java | 1 + .../com/coze/openapi/service/auth/JWTOAuth.java | 15 ++++++++++++--- .../main/java/example/auth/JWTOAuthExample.java | 8 +------- 3 files changed, 14 insertions(+), 10 deletions(-) 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 d6cfd764..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 @@ -4,6 +4,7 @@ public abstract class Auth { protected String accessToken; protected String refreshToken; + protected long refreshAt; protected long expiresIn; protected OAuthClient client; 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 64af22e8..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 @@ -16,8 +16,6 @@ public class JWTOAuth extends Auth { private Integer ttl; private String sessionName; private Scope scope; - private long refreshAt; - @Builder.Default private long refreshBefore = 30; @NonNull private JWTOAuthClient jwtClient; public JWTOAuth(JWTOAuthClient client) { @@ -37,7 +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 - this.refreshBefore; + 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/example/src/main/java/example/auth/JWTOAuthExample.java b/example/src/main/java/example/auth/JWTOAuthExample.java index 5cdc601e..ac33b3b4 100644 --- a/example/src/main/java/example/auth/JWTOAuthExample.java +++ b/example/src/main/java/example/auth/JWTOAuthExample.java @@ -80,13 +80,7 @@ public static void main(String[] args) { * */ CozeAPI coze = new CozeAPI.Builder() - .auth( - JWTOAuth.builder() - .jwtClient(oauth) - // the sdk will automatically refresh the token before it expires, - // the default is 30s, you can also customize the refresh time - .refreshBefore(30) - .build()) + .auth(JWTOAuth.builder().jwtClient(oauth).build()) .baseURL(cozeAPIBase) .build(); // you can also specify the scope and session for it From bac158f34334d91c12a00228204e246ae209a118 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 10 Mar 2025 14:40:12 +0800 Subject: [PATCH 44/50] fix cr Change-Id: I8786c5f54f4792411fb5394af99fb306c53758bc --- README.md | 8 ++-- .../coze/openapi/service/service/CozeAPI.java | 12 ++--- .../service/websocket/WebsocketClient.java | 26 ----------- .../service/websocket/WebsocketsClient.java | 26 +++++++++++ .../websocket/audio/WebsocketAudioClient.java | 26 ----------- .../audio/WebsocketsAudioClient.java | 26 +++++++++++ ...java => WebsocketsAudioSpeechBuilder.java} | 6 +-- ...WebsocketsAudioSpeechCallbackHandler.java} | 16 +++---- ....java => WebsocketsAudioSpeechClient.java} | 10 ++--- ...va => WebsocketsAudioSpeechCreateReq.java} | 4 +- ...WebsocketsAudioTranscriptionsBuilder.java} | 6 +-- ...tsAudioTranscriptionsCallbackHandler.java} | 18 ++++---- ... WebsocketsAudioTranscriptionsClient.java} | 20 +++++---- ...bsocketsAudioTranscriptionsCreateReq.java} | 4 +- ...ava => WebsocketsChatCallbackHandler.java} | 40 ++++++++--------- ...tClient.java => WebsocketsChatClient.java} | 23 ++++++---- ....java => WebsocketsChatClientBuilder.java} | 6 +-- ...eReq.java => WebsocketsChatCreateReq.java} | 4 +- .../websocket/common/BaseCallbackHandler.java | 2 +- ...tClient.java => BaseWebsocketsClient.java} | 8 ++-- ...tener.java => BaseWebsocketsListener.java} | 8 ++-- .../WebsocketAudioSpeechClientTest.java | 10 ++--- ...ebsocketAudioTranscriptionsClientTest.java | 16 +++---- ...est.java => WebsocketsChatClientTest.java} | 15 +++---- ...est.java => BaseWebsocketsClientTest.java} | 12 ++--- ...t.java => BaseWebsocketsListenerTest.java} | 8 ++-- .../java/example/auth/JWTOAuthExample.java | 2 +- .../speech/WebsocketAudioSpeechExample.java | 26 +++++------ .../WebsocketTranscriptionsExample.java | 33 +++++++------- .../example/websocket/chat/ChatExample.java | 44 +++++++++++-------- 30 files changed, 236 insertions(+), 229 deletions(-) delete mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketsClient.java delete mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketAudioClient.java create mode 100644 api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketsAudioClient.java rename api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/{WebsocketAudioSpeechBuilder.java => WebsocketsAudioSpeechBuilder.java} (53%) rename api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/{WebsocketAudioSpeechCallbackHandler.java => WebsocketsAudioSpeechCallbackHandler.java} (61%) rename api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/{WebsocketAudioSpeechClient.java => WebsocketsAudioSpeechClient.java} (92%) rename api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/{WebsocketAudioSpeechCreateReq.java => WebsocketsAudioSpeechCreateReq.java} (66%) rename api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/{WebsocketAudioTranscriptionsBuilder.java => WebsocketsAudioTranscriptionsBuilder.java} (51%) rename api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/{WebsocketAudioTranscriptionsCallbackHandler.java => WebsocketsAudioTranscriptionsCallbackHandler.java} (63%) rename api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/{WebsocketAudioTranscriptionsClient.java => WebsocketsAudioTranscriptionsClient.java} (86%) rename api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/{WebsocketAudioTranscriptionsCreateReq.java => WebsocketsAudioTranscriptionsCreateReq.java} (64%) rename api/src/main/java/com/coze/openapi/service/service/websocket/chat/{WebsocketChatCallbackHandler.java => WebsocketsChatCallbackHandler.java} (70%) rename api/src/main/java/com/coze/openapi/service/service/websocket/chat/{WebsocketChatClient.java => WebsocketsChatClient.java} (92%) rename api/src/main/java/com/coze/openapi/service/service/websocket/chat/{WebsocketChatClientBuilder.java => WebsocketsChatClientBuilder.java} (56%) rename api/src/main/java/com/coze/openapi/service/service/websocket/chat/{WebsocketChatCreateReq.java => WebsocketsChatCreateReq.java} (70%) rename api/src/main/java/com/coze/openapi/service/service/websocket/common/{BaseWebsocketClient.java => BaseWebsocketsClient.java} (94%) rename api/src/main/java/com/coze/openapi/service/service/websocket/common/{BaseWebSocketListener.java => BaseWebsocketsListener.java} (90%) rename api/src/test/java/com/coze/openapi/service/service/websocket/chat/{WebsocketChatClientTest.java => WebsocketsChatClientTest.java} (98%) rename api/src/test/java/com/coze/openapi/service/service/websocket/common/{BaseWebsocketClientTest.java => BaseWebsocketsClientTest.java} (95%) rename api/src/test/java/com/coze/openapi/service/service/websocket/common/{BaseWebSocketListenerTest.java => BaseWebsocketsListenerTest.java} (94%) diff --git a/README.md b/README.md index 002cfeb4..84aa58f6 100644 --- a/README.md +++ b/README.md @@ -1004,8 +1004,8 @@ WebsocketChatClient client = coze.websocket() .create(new WebsocketChatCreateReq(botID, new CallbackHandler())); // Send audio data -String base64AudioData = "..."; // Base64 encoded audio data -client.inputAudioBufferAppend(base64AudioData); +String audioData = "..."; // Base64 encoded audio data +client.inputAudioBufferAppend(audioData); client.inputAudioBufferComplete(); // Handle responses in callback @@ -1078,8 +1078,8 @@ InputAudio inputAudio = InputAudio.builder() client.transcriptionsUpdate(new TranscriptionsUpdateEventData(inputAudio)); // Send audio for transcription -String base64AudioData = "..."; // Base64 encoded audio data -client.inputAudioBufferAppend(base64AudioData); +String audioData = "..."; // Base64 encoded audio data +client.inputAudioBufferAppend(audioData); client.inputAudioBufferComplete(); // Handle transcription results in callback 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 b265204b..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,7 +18,7 @@ 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.WebsocketClient; +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; @@ -46,7 +46,7 @@ public class CozeAPI { private final ChatService chatAPI; private final AudioService audioAPI; private final TemplateService templateAPI; - private final WebsocketClient websocket; + private final WebsocketsClient websocket; private CozeAPI( String baseURL, @@ -61,7 +61,7 @@ private CozeAPI( ChatService chatAPI, AudioService audioAPI, TemplateService templateAPI, - WebsocketClient websocket) { + WebsocketsClient websocket) { this.baseURL = baseURL; this.executorService = executorService; this.auth = auth; @@ -113,7 +113,7 @@ public TemplateService templates() { return this.templateAPI; } - public WebsocketClient websocket() { + public WebsocketsClient websockets() { return this.websocket; } @@ -204,8 +204,8 @@ public CozeAPI build() { retrofit.create(AudioSpeechAPI.class), retrofit.create(AudioTranscriptionAPI.class)); TemplateService templateAPI = new TemplateService(retrofit.create(TemplateAPI.class)); - WebsocketClient websocket = - new WebsocketClient( + WebsocketsClient websocket = + new WebsocketsClient( this.client, Strings.replace(this.baseURL, "https://api", "wss://ws")); return new CozeAPI( this.baseURL, diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java deleted file mode 100644 index 5d0f6d6f..00000000 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/WebsocketClient.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.coze.openapi.service.service.websocket; - -import com.coze.openapi.service.service.websocket.audio.WebsocketAudioClient; -import com.coze.openapi.service.service.websocket.chat.WebsocketChatClientBuilder; - -import okhttp3.OkHttpClient; - -public class WebsocketClient { - - private final WebsocketChatClientBuilder chat; - - private final WebsocketAudioClient audio; - - public WebsocketClient(OkHttpClient client, String baseURL) { - this.chat = new WebsocketChatClientBuilder(baseURL, client); - this.audio = new WebsocketAudioClient(baseURL, client); - } - - public WebsocketChatClientBuilder chat() { - return chat; - } - - public WebsocketAudioClient audio() { - return audio; - } -} 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/WebsocketAudioClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketAudioClient.java deleted file mode 100644 index 95313841..00000000 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/WebsocketAudioClient.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.coze.openapi.service.service.websocket.audio; - -import com.coze.openapi.service.service.websocket.audio.speech.WebsocketAudioSpeechBuilder; -import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketAudioTranscriptionsBuilder; - -import okhttp3.OkHttpClient; - -public class WebsocketAudioClient { - - private final WebsocketAudioSpeechBuilder websocketAudioSpeechBuilder; - private final WebsocketAudioTranscriptionsBuilder websocketAudioTranscriptionsBuilder; - - public WebsocketAudioClient(String baseUrl, OkHttpClient httpClient) { - this.websocketAudioSpeechBuilder = new WebsocketAudioSpeechBuilder(baseUrl, httpClient); - this.websocketAudioTranscriptionsBuilder = - new WebsocketAudioTranscriptionsBuilder(baseUrl, httpClient); - } - - public WebsocketAudioSpeechBuilder speech() { - return websocketAudioSpeechBuilder; - } - - public WebsocketAudioTranscriptionsBuilder transcriptions() { - return websocketAudioTranscriptionsBuilder; - } -} 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/WebsocketAudioSpeechBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechBuilder.java similarity index 53% rename from api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechBuilder.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechBuilder.java index ed8bcb87..bc0e192e 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechBuilder.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechBuilder.java @@ -4,11 +4,11 @@ import okhttp3.OkHttpClient; @AllArgsConstructor -public class WebsocketAudioSpeechBuilder { +public class WebsocketsAudioSpeechBuilder { private final String baseUrl; private final OkHttpClient httpClient; - public WebsocketAudioSpeechClient create(WebsocketAudioSpeechCreateReq req) { - return new WebsocketAudioSpeechClient(httpClient, baseUrl, req); + 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/WebsocketAudioSpeechCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCallbackHandler.java similarity index 61% rename from api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCallbackHandler.java index b6601f84..9bb0eb84 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCallbackHandler.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCallbackHandler.java @@ -7,25 +7,25 @@ import com.coze.openapi.client.websocket.event.downstream.SpeechUpdatedEvent; import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; -public abstract class WebsocketAudioSpeechCallbackHandler - extends BaseCallbackHandler { - public WebsocketAudioSpeechCallbackHandler() {} +public abstract class WebsocketsAudioSpeechCallbackHandler + extends BaseCallbackHandler { + public WebsocketsAudioSpeechCallbackHandler() {} // 语音创建成功事件 (speech.created) - public void onSpeechCreated(WebsocketAudioSpeechClient client, SpeechCreatedEvent event) {} + public void onSpeechCreated(WebsocketsAudioSpeechClient client, SpeechCreatedEvent event) {} // 语音配置更新事件 (speech.update) - public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEvent event) {} + public void onSpeechUpdated(WebsocketsAudioSpeechClient client, SpeechUpdatedEvent event) {} // 语音数据更新事件 (speech.audio.update) public void onSpeechAudioUpdate( - WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) {} + WebsocketsAudioSpeechClient client, SpeechAudioUpdateEvent event) {} // 语音数据完成事件 (speech.audio.completed) public void onSpeechAudioCompleted( - WebsocketAudioSpeechClient client, SpeechAudioCompletedEvent event) {} + WebsocketsAudioSpeechClient client, SpeechAudioCompletedEvent event) {} // 文本缓冲区完成事件 (input_text_buffer.completed) public void onInputTextBufferCompleted( - WebsocketAudioSpeechClient client, InputTextBufferCompletedEvent event) {} + WebsocketsAudioSpeechClient client, InputTextBufferCompletedEvent event) {} } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechClient.java similarity index 92% rename from api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechClient.java index 8c35d124..d72d3fbd 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechClient.java @@ -5,18 +5,18 @@ 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.BaseWebsocketClient; +import com.coze.openapi.service.service.websocket.common.BaseWebsocketsClient; import com.fasterxml.jackson.databind.JsonNode; import okhttp3.OkHttpClient; import okhttp3.WebSocket; -public class WebsocketAudioSpeechClient extends BaseWebsocketClient { - private final WebsocketAudioSpeechCallbackHandler handler; +public class WebsocketsAudioSpeechClient extends BaseWebsocketsClient { + private final WebsocketsAudioSpeechCallbackHandler handler; private static final String uri = "/v1/audio/speech"; - protected WebsocketAudioSpeechClient( - OkHttpClient client, String wsHost, WebsocketAudioSpeechCreateReq req) { + protected WebsocketsAudioSpeechClient( + OkHttpClient client, String wsHost, WebsocketsAudioSpeechCreateReq req) { super(client, buildUrl(wsHost), req.getCallbackHandler(), req); this.handler = req.getCallbackHandler(); } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCreateReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCreateReq.java similarity index 66% rename from api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCreateReq.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCreateReq.java index 731d6fd0..e1440b21 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketAudioSpeechCreateReq.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/speech/WebsocketsAudioSpeechCreateReq.java @@ -10,6 +10,6 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public class WebsocketAudioSpeechCreateReq extends BaseReq { - @NonNull private WebsocketAudioSpeechCallbackHandler callbackHandler; +public class WebsocketsAudioSpeechCreateReq extends BaseReq { + @NonNull private WebsocketsAudioSpeechCallbackHandler callbackHandler; } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsBuilder.java similarity index 51% rename from api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsBuilder.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsBuilder.java index b1965bd3..f4be265f 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsBuilder.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsBuilder.java @@ -4,11 +4,11 @@ import okhttp3.OkHttpClient; @AllArgsConstructor -public class WebsocketAudioTranscriptionsBuilder { +public class WebsocketsAudioTranscriptionsBuilder { private final String baseUrl; private final OkHttpClient httpClient; - public WebsocketAudioTranscriptionsClient create(WebsocketAudioTranscriptionsCreateReq req) { - return new WebsocketAudioTranscriptionsClient(httpClient, baseUrl, req); + 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/WebsocketAudioTranscriptionsCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCallbackHandler.java similarity index 63% rename from api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCallbackHandler.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCallbackHandler.java index 8bb46988..8024cd52 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCallbackHandler.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCallbackHandler.java @@ -8,31 +8,31 @@ import com.coze.openapi.client.websocket.event.downstream.TranscriptionsUpdatedEvent; import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; -public abstract class WebsocketAudioTranscriptionsCallbackHandler - extends BaseCallbackHandler { - public WebsocketAudioTranscriptionsCallbackHandler() {} +public abstract class WebsocketsAudioTranscriptionsCallbackHandler + extends BaseCallbackHandler { + public WebsocketsAudioTranscriptionsCallbackHandler() {} // 转录创建事件 (transcriptions.created) public void onTranscriptionsCreated( - WebsocketAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) {} + WebsocketsAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) {} // 转录配置更新事件 (transcriptions.updated) public void onTranscriptionsUpdated( - WebsocketAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) {} + WebsocketsAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) {} // 转录消息更新事件 (transcriptions.message.update) public void onTranscriptionsMessageUpdate( - WebsocketAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) {} + WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) {} // 转录消息完成事件 (transcriptions.message.completed) public void onTranscriptionsMessageCompleted( - WebsocketAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) {} + WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) {} // 语音缓冲区清除事件 (input_audio_buffer.cleared) public void onInputAudioBufferCleared( - WebsocketAudioTranscriptionsClient client, InputAudioBufferClearedEvent event) {} + WebsocketsAudioTranscriptionsClient client, InputAudioBufferClearedEvent event) {} // 语音缓冲区完成事件 (input_audio_buffer.completed) public void onInputAudioBufferCompleted( - WebsocketAudioTranscriptionsClient client, InputAudioBufferCompletedEvent event) {} + WebsocketsAudioTranscriptionsClient client, InputAudioBufferCompletedEvent event) {} } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsClient.java similarity index 86% rename from api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsClient.java index 8c09e4d9..eed3f6c2 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsClient.java @@ -1,22 +1,25 @@ 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.BaseWebsocketClient; +import com.coze.openapi.service.service.websocket.common.BaseWebsocketsClient; import com.fasterxml.jackson.databind.JsonNode; import okhttp3.OkHttpClient; import okhttp3.WebSocket; -public class WebsocketAudioTranscriptionsClient extends BaseWebsocketClient { - private final WebsocketAudioTranscriptionsCallbackHandler handler; +public class WebsocketsAudioTranscriptionsClient extends BaseWebsocketsClient { + private final WebsocketsAudioTranscriptionsCallbackHandler handler; private static final String uri = "/v1/audio/transcriptions"; - protected WebsocketAudioTranscriptionsClient( - OkHttpClient client, String wsHost, WebsocketAudioTranscriptionsCreateReq req) { + protected WebsocketsAudioTranscriptionsClient( + OkHttpClient client, String wsHost, WebsocketsAudioTranscriptionsCreateReq req) { super(client, buildUrl(wsHost), req.getCallbackHandler(), req); this.handler = req.getCallbackHandler(); } @@ -32,11 +35,12 @@ public void transcriptionsUpdate(TranscriptionsUpdateEventData data) { // 发送语音缓冲区追加事件 public void inputAudioBufferAppend(String data) { - this.sendEvent(InputAudioBufferAppendEvent.of(data)); + this.inputAudioBufferAppend(data.getBytes(StandardCharsets.UTF_8)); } - public void inputAudioBufferAppend(InputAudioBufferAppendEvent.Data data) { - this.sendEvent(InputAudioBufferAppendEvent.builder().data(data).build()); + public void inputAudioBufferAppend(byte[] data) { + String base64Data = Base64.getEncoder().encodeToString(data); + this.sendEvent(InputAudioBufferAppendEvent.of(base64Data)); } // 发送语音缓冲区清除事件 diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCreateReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCreateReq.java similarity index 64% rename from api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCreateReq.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCreateReq.java index 900d96c8..00f64f86 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketAudioTranscriptionsCreateReq.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/audio/transcriptions/WebsocketsAudioTranscriptionsCreateReq.java @@ -10,6 +10,6 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public class WebsocketAudioTranscriptionsCreateReq extends BaseReq { - @NonNull private WebsocketAudioTranscriptionsCallbackHandler callbackHandler; +public class WebsocketsAudioTranscriptionsCreateReq extends BaseReq { + @NonNull private WebsocketsAudioTranscriptionsCallbackHandler callbackHandler; } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCallbackHandler.java similarity index 70% rename from api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCallbackHandler.java index d40bd161..9e159264 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCallbackHandler.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCallbackHandler.java @@ -19,72 +19,72 @@ import com.coze.openapi.client.websocket.event.downstream.InputAudioBufferCompletedEvent; import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; -public abstract class WebsocketChatCallbackHandler - extends BaseCallbackHandler { - public WebsocketChatCallbackHandler() {} +public abstract class WebsocketsChatCallbackHandler + extends BaseCallbackHandler { + public WebsocketsChatCallbackHandler() {} // 对话连接成功事件 (chat.created) - public void onChatCreated(WebsocketChatClient client, ChatCreatedEvent event) {} + public void onChatCreated(WebsocketsChatClient client, ChatCreatedEvent event) {} // 对话配置成功事件 (chat.updated) - public void onChatUpdated(WebsocketChatClient client, ChatUpdatedEvent event) {} + public void onChatUpdated(WebsocketsChatClient client, ChatUpdatedEvent event) {} // 对话创建事件 (conversation.chat.created) public void onConversationChatCreated( - WebsocketChatClient client, ConversationChatCreatedEvent event) {} + WebsocketsChatClient client, ConversationChatCreatedEvent event) {} // 对话正在处理事件 (conversation.chat.in_progress) public void onConversationChatInProgress( - WebsocketChatClient client, ConversationChatInProgressEvent event) {} + WebsocketsChatClient client, ConversationChatInProgressEvent event) {} // 增量消息事件 (conversation.message.delta) public void onConversationMessageDelta( - WebsocketChatClient client, ConversationMessageDeltaEvent event) {} + WebsocketsChatClient client, ConversationMessageDeltaEvent event) {} // 增量语音事件 (conversation.audio.delta) public void onConversationAudioDelta( - WebsocketChatClient client, ConversationAudioDeltaEvent event) {} + WebsocketsChatClient client, ConversationAudioDeltaEvent event) {} // 消息完成事件 (conversation.message.completed) public void onConversationMessageCompleted( - WebsocketChatClient client, ConversationMessageCompletedEvent event) {} + WebsocketsChatClient client, ConversationMessageCompletedEvent event) {} // 语音回复完成事件 (conversation.audio.completed) public void onConversationAudioCompleted( - WebsocketChatClient client, ConversationAudioCompletedEvent event) {} + WebsocketsChatClient client, ConversationAudioCompletedEvent event) {} // 对话完成事件 (conversation.chat.completed) public void onConversationChatCompleted( - WebsocketChatClient client, ConversationChatCompletedEvent event) {} + WebsocketsChatClient client, ConversationChatCompletedEvent event) {} // 对话失败事件 (conversation.chat.failed) public void onConversationChatFailed( - WebsocketChatClient client, ConversationChatFailedEvent event) {} + WebsocketsChatClient client, ConversationChatFailedEvent event) {} // 语音提交成功事件 (input_audio_buffer.completed) public void onInputAudioBufferCompleted( - WebsocketChatClient client, InputAudioBufferCompletedEvent event) {} + WebsocketsChatClient client, InputAudioBufferCompletedEvent event) {} // 语音清除成功事件 (input_audio_buffer.cleared) public void onInputAudioBufferCleared( - WebsocketChatClient client, InputAudioBufferClearedEvent event) {} + WebsocketsChatClient client, InputAudioBufferClearedEvent event) {} // 对话清除事件 (conversation.cleared) - public void onConversationCleared(WebsocketChatClient client, ConversationClearedEvent event) {} + public void onConversationCleared(WebsocketsChatClient client, ConversationClearedEvent event) {} // 对话取消事件 (conversation.chat.canceled) public void onConversationChatCanceled( - WebsocketChatClient client, ConversationChatCanceledEvent event) {} + WebsocketsChatClient client, ConversationChatCanceledEvent event) {} // 语音转录更新事件 (conversation.audio_transcript.update) public void onConversationAudioTranscriptUpdate( - WebsocketChatClient client, ConversationAudioTranscriptUpdateEvent event) {} + WebsocketsChatClient client, ConversationAudioTranscriptUpdateEvent event) {} // 语音转录完成事件 (conversation.audio_transcript.completed) public void onConversationAudioTranscriptCompleted( - WebsocketChatClient client, ConversationAudioTranscriptCompletedEvent event) {} + WebsocketsChatClient client, ConversationAudioTranscriptCompletedEvent event) {} // 端插件事件 (conversation.chat.requires_action) public void onConversationChatRequiresAction( - WebsocketChatClient client, ConversationChatRequiresActionEvent event) {} + WebsocketsChatClient client, ConversationChatRequiresActionEvent event) {} } diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClient.java similarity index 92% rename from api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClient.java index e2bd81cd..008aae4a 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClient.java @@ -1,5 +1,8 @@ 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.*; @@ -9,7 +12,7 @@ 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.BaseWebsocketClient; +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; @@ -17,20 +20,20 @@ import okhttp3.OkHttpClient; import okhttp3.WebSocket; -public class WebsocketChatClient extends BaseWebsocketClient { +public class WebsocketsChatClient extends BaseWebsocketsClient { private final ObjectMapper objectMapper = Utils.getMapper(); - private final WebsocketChatCallbackHandler handler; + private final WebsocketsChatCallbackHandler handler; private static final String uri = "/v1/chat"; - protected WebsocketChatClient(OkHttpClient client, String wsHost, WebsocketChatCreateReq req) { + 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, WebsocketChatCreateReq req) { + protected static String buildUrl(String wsHost, WebsocketsChatCreateReq req) { return String.format("%s%s?bot_id=%s", wsHost, uri, req.getBotID()); } @@ -54,12 +57,14 @@ public void conversationMessageCreate(Message data) { this.sendEvent(ConversationMessageCreateEvent.builder().data(data).build()); } - public void inputAudioBufferAppend(InputAudioBufferAppendEvent.Data data) { - this.sendEvent(InputAudioBufferAppendEvent.builder().data(data).build()); + public void inputAudioBufferAppend(String data) { + this.sendEvent( + InputAudioBufferAppendEvent.of( + Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8)))); } - public void inputAudioBufferAppend(String data) { - this.sendEvent(InputAudioBufferAppendEvent.of(data)); + public void inputAudioBufferAppend(byte[] data) { + this.sendEvent(InputAudioBufferAppendEvent.of(Base64.getEncoder().encodeToString(data))); } public void inputAudioBufferClear() { diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientBuilder.java similarity index 56% rename from api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientBuilder.java index 1f1101a7..3b057b4b 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientBuilder.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientBuilder.java @@ -4,11 +4,11 @@ import okhttp3.OkHttpClient; @AllArgsConstructor -public class WebsocketChatClientBuilder { +public class WebsocketsChatClientBuilder { private final String baseUrl; private final OkHttpClient httpClient; - public WebsocketChatClient create(WebsocketChatCreateReq req) { - return new WebsocketChatClient(httpClient, baseUrl, req); + 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/WebsocketChatCreateReq.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCreateReq.java similarity index 70% rename from api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCreateReq.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCreateReq.java index 099b5d1a..2ea52083 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatCreateReq.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCreateReq.java @@ -10,7 +10,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public class WebsocketChatCreateReq extends BaseReq { +public class WebsocketsChatCreateReq extends BaseReq { @NonNull private String botID; - @NonNull private WebsocketChatCallbackHandler callbackHandler; + @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 index 35713276..2cf69f2c 100644 --- 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 @@ -2,7 +2,7 @@ import com.coze.openapi.client.websocket.event.downstream.ErrorEvent; -public abstract class BaseCallbackHandler { +public abstract class BaseCallbackHandler { public BaseCallbackHandler() {} diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClient.java similarity index 94% rename from api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClient.java index fe8b06bb..5b7fbc69 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClient.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClient.java @@ -17,19 +17,19 @@ import okhttp3.Request; import okhttp3.WebSocket; -public abstract class BaseWebsocketClient { +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 BaseWebSocketListener listener; + protected final BaseWebsocketsListener listener; - protected BaseWebsocketClient( + 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 BaseWebSocketListener(this::handleEvent, handler, this); + this.listener = new BaseWebsocketsListener(this::handleEvent, handler, this); OkHttpClient.Builder builder = client.newBuilder(); boolean hasSetTimeout = false; if (req != null) { diff --git a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListener.java similarity index 90% rename from api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java rename to api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListener.java index b3c96aed..4b60aa7c 100644 --- a/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListener.java +++ b/api/src/main/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListener.java @@ -8,16 +8,16 @@ import okhttp3.WebSocketListener; import okio.ByteString; -public class BaseWebSocketListener extends WebSocketListener { +public class BaseWebsocketsListener extends WebSocketListener { private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private final BiConsumer handleEvent; private final BaseCallbackHandler callbackHandler; - private final BaseWebsocketClient client; + private final BaseWebsocketsClient client; - public BaseWebSocketListener( + public BaseWebsocketsListener( BiConsumer handleEvent, BaseCallbackHandler handler, - BaseWebsocketClient client) { + BaseWebsocketsClient client) { this.handleEvent = handleEvent; this.callbackHandler = handler; this.client = client; 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 index ecde71b9..d30ccddb 100644 --- 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 @@ -24,7 +24,7 @@ public class WebsocketAudioSpeechClientTest { @Mock private OkHttpClient mockOkHttpClient; @Mock private WebSocket mockWebSocket; - @Mock private WebsocketAudioSpeechCallbackHandler mockCallbackHandler; + @Mock private WebsocketsAudioSpeechCallbackHandler mockCallbackHandler; @Captor private ArgumentCaptor speechCreatedEventCaptor; @Captor private ArgumentCaptor speechUpdatedEventCaptor; @@ -33,16 +33,16 @@ public class WebsocketAudioSpeechClientTest { @Captor private ArgumentCaptor inputTextBufferCompletedEventCaptor; @Captor private ArgumentCaptor errorEventCaptor; - private WebsocketAudioSpeechClient client; + private WebsocketsAudioSpeechClient client; @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); - WebsocketAudioSpeechCreateReq req = - WebsocketAudioSpeechCreateReq.builder().callbackHandler(mockCallbackHandler).build(); - client = new WebsocketAudioSpeechClient(mockOkHttpClient, "ws://test.com", req); + WebsocketsAudioSpeechCreateReq req = + WebsocketsAudioSpeechCreateReq.builder().callbackHandler(mockCallbackHandler).build(); + client = new WebsocketsAudioSpeechClient(mockOkHttpClient, "ws://test.com", req); } @Test 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 index 26103f6e..b9b8b6df 100644 --- 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 @@ -17,7 +17,6 @@ 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.client.websocket.event.upstream.InputAudioBufferAppendEvent; import okhttp3.OkHttpClient; import okhttp3.WebSocket; @@ -25,7 +24,7 @@ public class WebsocketAudioTranscriptionsClientTest { @Mock private OkHttpClient mockOkHttpClient; @Mock private WebSocket mockWebSocket; - @Mock private WebsocketAudioTranscriptionsCallbackHandler mockCallbackHandler; + @Mock private WebsocketsAudioTranscriptionsCallbackHandler mockCallbackHandler; @Captor private ArgumentCaptor transcriptionsCreatedEventCaptor; @Captor private ArgumentCaptor transcriptionsUpdatedEventCaptor; @@ -44,18 +43,18 @@ public class WebsocketAudioTranscriptionsClientTest { @Captor private ArgumentCaptor errorEventCaptor; - private WebsocketAudioTranscriptionsClient client; + private WebsocketsAudioTranscriptionsClient client; @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); - WebsocketAudioTranscriptionsCreateReq req = - WebsocketAudioTranscriptionsCreateReq.builder() + WebsocketsAudioTranscriptionsCreateReq req = + WebsocketsAudioTranscriptionsCreateReq.builder() .callbackHandler(mockCallbackHandler) .build(); - client = new WebsocketAudioTranscriptionsClient(mockOkHttpClient, "ws://test.com", req); + client = new WebsocketsAudioTranscriptionsClient(mockOkHttpClient, "ws://test.com", req); } @Test @@ -306,10 +305,7 @@ void testInputAudioBufferAppendWithString() { @Test void testInputAudioBufferAppendWithData() { - InputAudioBufferAppendEvent.Data data = - new InputAudioBufferAppendEvent.Data("base64EncodedAudioData"); - - client.inputAudioBufferAppend(data); + client.inputAudioBufferAppend("data"); verify(mockWebSocket).send(anyString()); } diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientTest.java similarity index 98% rename from api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java rename to api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientTest.java index 27a55f7d..60ef5a2e 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketChatClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatClientTest.java @@ -20,15 +20,14 @@ 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 com.coze.openapi.client.websocket.event.upstream.InputAudioBufferAppendEvent; import okhttp3.OkHttpClient; import okhttp3.WebSocket; -public class WebsocketChatClientTest { +public class WebsocketsChatClientTest { @Mock private OkHttpClient mockOkHttpClient; @Mock private WebSocket mockWebSocket; - @Mock private WebsocketChatCallbackHandler mockCallbackHandler; + @Mock private WebsocketsChatCallbackHandler mockCallbackHandler; @Captor private ArgumentCaptor chatCreatedEventCaptor; @Captor private ArgumentCaptor messageDeltaEventCaptor; @@ -59,20 +58,20 @@ public class WebsocketChatClientTest { @Captor private ArgumentCaptor messageCompletedEventCaptor; // ... 其他事件的 Captor - private WebsocketChatClient client; + private WebsocketsChatClient client; @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); when(mockOkHttpClient.newWebSocket(any(), any())).thenReturn(mockWebSocket); - WebsocketChatCreateReq req = - WebsocketChatCreateReq.builder() + WebsocketsChatCreateReq req = + WebsocketsChatCreateReq.builder() .botID("test-bot-id") .callbackHandler(mockCallbackHandler) .build(); - client = new WebsocketChatClient(mockOkHttpClient, "ws://test.com", req); + client = new WebsocketsChatClient(mockOkHttpClient, "ws://test.com", req); } @Test @@ -844,7 +843,7 @@ void testConversationMessageCreate() { @Test void testInputAudioBufferAppendWithData() { - client.inputAudioBufferAppend(new InputAudioBufferAppendEvent.Data("hello")); + client.inputAudioBufferAppend("hello"); verify(mockWebSocket).send(anyString()); } diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClientTest.java similarity index 95% rename from api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java rename to api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClientTest.java index 50f85cd7..ba634c83 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketClientTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsClientTest.java @@ -18,17 +18,17 @@ import okhttp3.Request; import okhttp3.WebSocket; -class BaseWebsocketClientTest { +class BaseWebsocketsClientTest { @Mock private OkHttpClient mockOkHttpClient; @Mock private WebSocket mockWebSocket; @Mock private BaseCallbackHandler mockCallbackHandler; - @Mock private BaseWebSocketListener mockListener; + @Mock private BaseWebsocketsListener mockListener; private TestBaseWebsocketClient client; // 创建一个测试用的具体实现类 - private static class TestBaseWebsocketClient extends BaseWebsocketClient { + private static class TestBaseWebsocketClient extends BaseWebsocketsClient { BaseCallbackHandler handler; public TestBaseWebsocketClient( @@ -56,7 +56,7 @@ public void testSendEvent(BaseEvent event) { @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - when(mockOkHttpClient.newWebSocket(any(Request.class), any(BaseWebSocketListener.class))) + when(mockOkHttpClient.newWebSocket(any(Request.class), any(BaseWebsocketsListener.class))) .thenReturn(mockWebSocket); client = @@ -77,7 +77,7 @@ void testConstructor() { @Test void testInitialization() { - verify(mockOkHttpClient).newWebSocket(any(Request.class), any(BaseWebSocketListener.class)); + verify(mockOkHttpClient).newWebSocket(any(Request.class), any(BaseWebsocketsListener.class)); } @Test @@ -148,7 +148,7 @@ void testListenerShutdown() { // 使用反射设置模拟的 listener try { java.lang.reflect.Field listenerField = - BaseWebsocketClient.class.getDeclaredField("listener"); + BaseWebsocketsClient.class.getDeclaredField("listener"); listenerField.setAccessible(true); listenerField.set(client, mockListener); } catch (Exception e) { diff --git a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListenerTest.java b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListenerTest.java similarity index 94% rename from api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListenerTest.java rename to api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListenerTest.java index c86216f5..ed9690d2 100644 --- a/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebSocketListenerTest.java +++ b/api/src/test/java/com/coze/openapi/service/service/websocket/common/BaseWebsocketsListenerTest.java @@ -19,15 +19,15 @@ import okhttp3.WebSocket; import okio.ByteString; -class BaseWebSocketListenerTest { +class BaseWebsocketsListenerTest { @Mock private WebSocket mockWebSocket; private Response mockResponse; @Mock private BaseCallbackHandler mockCallbackHandler; - @Mock private BaseWebsocketClient mockClient; + @Mock private BaseWebsocketsClient mockClient; @Mock private BiConsumer mockHandleEvent; - private BaseWebSocketListener listener; + private BaseWebsocketsListener listener; @BeforeEach void setUp() { @@ -42,7 +42,7 @@ void setUp() { .request(new Request.Builder().url("ws://test.com").build()) .build(); - listener = new BaseWebSocketListener(mockHandleEvent, mockCallbackHandler, mockClient); + listener = new BaseWebsocketsListener(mockHandleEvent, mockCallbackHandler, mockClient); } @Test diff --git a/example/src/main/java/example/auth/JWTOAuthExample.java b/example/src/main/java/example/auth/JWTOAuthExample.java index ac33b3b4..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 = diff --git a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java index 8d325c29..4c9599f9 100644 --- a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java +++ b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java @@ -10,9 +10,9 @@ 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.WebsocketAudioSpeechCallbackHandler; -import com.coze.openapi.service.service.websocket.audio.speech.WebsocketAudioSpeechClient; -import com.coze.openapi.service.service.websocket.audio.speech.WebsocketAudioSpeechCreateReq; +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 com.fasterxml.jackson.annotation.JsonProperty; import example.utils.ExampleUtils; @@ -35,7 +35,7 @@ private static class Weather { private String weather; } - private static class CallbackHandler extends WebsocketAudioSpeechCallbackHandler { + private static class CallbackHandler extends WebsocketsAudioSpeechCallbackHandler { private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 public CallbackHandler() { @@ -44,14 +44,14 @@ public CallbackHandler() { // 语音创建成功事件 (speech.created) @Override - public void onSpeechCreated(WebsocketAudioSpeechClient client, SpeechCreatedEvent event) { + public void onSpeechCreated(WebsocketsAudioSpeechClient client, SpeechCreatedEvent event) { System.out.println("==== Speech Created ===="); System.out.println(event); } // 语音配置更新事件 (speech.update) @Override - public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEvent event) { + public void onSpeechUpdated(WebsocketsAudioSpeechClient client, SpeechUpdatedEvent event) { System.out.println("==== Speech Updated ===="); System.out.println(event); } @@ -59,14 +59,14 @@ public void onSpeechUpdated(WebsocketAudioSpeechClient client, SpeechUpdatedEven // 语音数据更新事件 (speech.audio.update) @Override public void onSpeechAudioUpdate( - WebsocketAudioSpeechClient client, SpeechAudioUpdateEvent event) { + WebsocketsAudioSpeechClient client, SpeechAudioUpdateEvent event) { buffer.put(event.getDelta()); } // 语音数据完成事件 (speech.audio.completed) @Override public void onSpeechAudioCompleted( - WebsocketAudioSpeechClient client, SpeechAudioCompletedEvent event) { + WebsocketsAudioSpeechClient client, SpeechAudioCompletedEvent event) { try { ExampleUtils.writePcmToWavFile(buffer.array(), "output_speech.wav"); System.out.println("========= On Speech Audio Completed ========="); @@ -78,13 +78,13 @@ public void onSpeechAudioCompleted( // 文本缓冲区完成事件 (input_text_buffer.completed) @Override public void onInputTextBufferCompleted( - WebsocketAudioSpeechClient client, InputTextBufferCompletedEvent event) { + WebsocketsAudioSpeechClient client, InputTextBufferCompletedEvent event) { System.out.println("==== Input Text Buffer Completed ===="); System.out.println(event); } @Override - public void onError(WebsocketAudioSpeechClient client, ErrorEvent event) { + public void onError(WebsocketsAudioSpeechClient client, ErrorEvent event) { System.out.println(event); } } @@ -105,13 +105,13 @@ public static void main(String[] args) throws Exception { .readTimeout(10000) .build(); - WebsocketAudioSpeechClient client = null; + WebsocketsAudioSpeechClient client = null; try { client = - coze.websocket() + coze.websockets() .audio() .speech() - .create(new WebsocketAudioSpeechCreateReq(new CallbackHandler())); + .create(new WebsocketsAudioSpeechCreateReq(new CallbackHandler())); OutputAudio outputAudio = OutputAudio.builder() .voiceId(voiceID) diff --git a/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java index 787103a4..591cd9df 100644 --- a/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java +++ b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java @@ -4,7 +4,6 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.Base64; import java.util.concurrent.TimeUnit; import com.coze.openapi.client.audio.common.AudioFormat; @@ -15,9 +14,9 @@ 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.WebsocketAudioTranscriptionsCallbackHandler; -import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketAudioTranscriptionsClient; -import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketAudioTranscriptionsCreateReq; +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; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -39,7 +38,7 @@ private static class Weather { private String weather; } - private static class CallbackHandler extends WebsocketAudioTranscriptionsCallbackHandler { + private static class CallbackHandler extends WebsocketsAudioTranscriptionsCallbackHandler { private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 public CallbackHandler() { @@ -47,19 +46,19 @@ public CallbackHandler() { } @Override - public void onError(WebsocketAudioTranscriptionsClient client, ErrorEvent event) { + public void onError(WebsocketsAudioTranscriptionsClient client, ErrorEvent event) { System.out.println(event); } @Override - public void onClientException(WebsocketAudioTranscriptionsClient client, Throwable e) { + public void onClientException(WebsocketsAudioTranscriptionsClient client, Throwable e) { e.printStackTrace(); } // 转录配置更新事件 (transcriptions.updated) @Override public void onTranscriptionsUpdated( - WebsocketAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) { + WebsocketsAudioTranscriptionsClient client, TranscriptionsUpdatedEvent event) { System.out.println("=== Transcriptions Updated ==="); System.out.println(event); } @@ -67,7 +66,7 @@ public void onTranscriptionsUpdated( // 转录创建事件 (transcriptions.created) @Override public void onTranscriptionsCreated( - WebsocketAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) { + WebsocketsAudioTranscriptionsClient client, TranscriptionsCreatedEvent event) { System.out.println("=== Transcriptions Created ==="); System.out.println(event); } @@ -75,14 +74,14 @@ public void onTranscriptionsCreated( // 转录消息更新事件 (transcriptions.message.update) @Override public void onTranscriptionsMessageUpdate( - WebsocketAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) { + WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageUpdateEvent event) { System.out.println(event.getData().getContent()); } // 转录消息完成事件 (transcriptions.message.completed) @Override public void onTranscriptionsMessageCompleted( - WebsocketAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) { + WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) { System.out.println("=== Transcriptions Message Completed ==="); System.out.println(event); } @@ -90,7 +89,7 @@ public void onTranscriptionsMessageCompleted( // 语音缓冲区完成事件 (input_audio_buffer.completed) @Override public void onInputAudioBufferCompleted( - WebsocketAudioTranscriptionsClient client, InputAudioBufferCompletedEvent event) { + WebsocketsAudioTranscriptionsClient client, InputAudioBufferCompletedEvent event) { System.out.println("=== Input Audio Buffer Completed ==="); System.out.println(event); } @@ -112,13 +111,13 @@ public static void main(String[] args) throws Exception { .readTimeout(10000) .build(); - WebsocketAudioTranscriptionsClient client = null; + WebsocketsAudioTranscriptionsClient client = null; try { client = - coze.websocket() + coze.websockets() .audio() .transcriptions() - .create(new WebsocketAudioTranscriptionsCreateReq(new CallbackHandler())); + .create(new WebsocketsAudioTranscriptionsCreateReq(new CallbackHandler())); CreateSpeechResp speechResp = coze.audio() .speech() @@ -139,9 +138,7 @@ public static void main(String[] args) throws Exception { int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { - // 将读取到的字节转换为 base64 编码 - String base64Data = Base64.getEncoder().encodeToString(Arrays.copyOf(buffer, bytesRead)); - client.inputAudioBufferAppend(base64Data); + client.inputAudioBufferAppend(Arrays.copyOf(buffer, bytesRead)); } client.inputAudioBufferComplete(); } catch (IOException e) { diff --git a/example/src/main/java/example/websocket/chat/ChatExample.java b/example/src/main/java/example/websocket/chat/ChatExample.java index 8861551e..cddcad3b 100644 --- a/example/src/main/java/example/websocket/chat/ChatExample.java +++ b/example/src/main/java/example/websocket/chat/ChatExample.java @@ -5,7 +5,6 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.List; import java.util.concurrent.TimeUnit; @@ -18,9 +17,9 @@ 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.WebsocketChatCallbackHandler; -import com.coze.openapi.service.service.websocket.chat.WebsocketChatClient; -import com.coze.openapi.service.service.websocket.chat.WebsocketChatCreateReq; +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; @@ -35,6 +34,9 @@ poll the status of the conversation, and obtain the messages after the conversation is completed. * */ public class ChatExample { + + private static boolean isDone; + @Data @Builder @AllArgsConstructor @@ -44,7 +46,7 @@ private static class Weather { private String weather; } - private static class CallbackHandler extends WebsocketChatCallbackHandler { + private static class CallbackHandler extends WebsocketsChatCallbackHandler { private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 public CallbackHandler() { @@ -52,55 +54,56 @@ public CallbackHandler() { } @Override - public void onChatCreated(WebsocketChatClient client, ChatCreatedEvent event) { + public void onChatCreated(WebsocketsChatClient client, ChatCreatedEvent event) { System.out.println(event); // client.sendEvent(new BaseEvent()); } @Override public void onConversationMessageDelta( - WebsocketChatClient client, ConversationMessageDeltaEvent event) { + WebsocketsChatClient client, ConversationMessageDeltaEvent event) { System.out.printf("Revieve: %s\n", event.getData().getContent()); } @Override - public void onError(WebsocketChatClient client, ErrorEvent event) { + public void onError(WebsocketsChatClient client, ErrorEvent event) { System.out.println(event); } @Override public void onInputAudioBufferCompleted( - WebsocketChatClient client, InputAudioBufferCompletedEvent event) { + WebsocketsChatClient client, InputAudioBufferCompletedEvent event) { System.out.println("========= Input Audio Buffer Completed ========="); System.out.println(event); } @Override public void onConversationAudioCompleted( - WebsocketChatClient client, ConversationAudioCompletedEvent event) { + 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(WebsocketChatClient client, Throwable e) { + public void onClientException(WebsocketsChatClient client, Throwable e) { e.printStackTrace(); } @Override public void onConversationAudioDelta( - WebsocketChatClient client, ConversationAudioDeltaEvent event) { + WebsocketsChatClient client, ConversationAudioDeltaEvent event) { byte[] audioData = event.getData().getAudio(); buffer.put(audioData); } @Override public void onConversationChatRequiresAction( - WebsocketChatClient client, ConversationChatRequiresActionEvent event) { + WebsocketsChatClient client, ConversationChatRequiresActionEvent event) { List toolOutputs = new ArrayList<>(); for (ChatToolCall call : event.getData().getRequiredAction().getSubmitToolOutputs().getToolCalls()) { @@ -138,10 +141,12 @@ public static void main(String[] args) throws Exception { .readTimeout(10000) .build(); - WebsocketChatClient client = null; + WebsocketsChatClient client = null; try { client = - coze.websocket().chat().create(new WebsocketChatCreateReq(botID, new CallbackHandler())); + coze.websockets() + .chat() + .create(new WebsocketsChatCreateReq(botID, new CallbackHandler())); CreateSpeechResp speechResp = coze.audio() .speech() @@ -158,16 +163,17 @@ public static void main(String[] args) throws Exception { int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { - // 将读取到的字节转换为 base64 编码 - String base64Data = Base64.getEncoder().encodeToString(Arrays.copyOf(buffer, bytesRead)); - client.inputAudioBufferAppend(base64Data); + client.inputAudioBufferAppend(Arrays.copyOf(buffer, bytesRead)); + TimeUnit.MILLISECONDS.sleep(100); } client.inputAudioBufferComplete(); } catch (IOException e) { e.printStackTrace(); } + while (!isDone) { + TimeUnit.MILLISECONDS.sleep(100); + } - TimeUnit.SECONDS.sleep(100); } catch (Exception e) { e.printStackTrace(); } finally { From e0dc61daad9eba83f2004e6cdae198f8dfe6b348 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 10 Mar 2025 14:45:05 +0800 Subject: [PATCH 45/50] fix cr Change-Id: Id90fa6c4e25b2919197316dc60b368947e593466 --- .../speech/WebsocketAudioSpeechExample.java | 20 +++++----------- .../WebsocketTranscriptionsExample.java | 24 +++++++------------ .../example/websocket/chat/ChatExample.java | 1 + 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java index 4c9599f9..e7e07755 100644 --- a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java +++ b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java @@ -13,27 +13,16 @@ 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 com.fasterxml.jackson.annotation.JsonProperty; import example.utils.ExampleUtils; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; /* This example describes how to use the chat interface to initiate conversations, poll the status of the conversation, and obtain the messages after the conversation is completed. * */ public class WebsocketAudioSpeechExample { - @Data - @Builder - @AllArgsConstructor - @NoArgsConstructor - private static class Weather { - @JsonProperty("weather") - private String weather; - } + + private static boolean isDone = false; private static class CallbackHandler extends WebsocketsAudioSpeechCallbackHandler { private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 @@ -70,6 +59,7 @@ public void onSpeechAudioCompleted( try { ExampleUtils.writePcmToWavFile(buffer.array(), "output_speech.wav"); System.out.println("========= On Speech Audio Completed ========="); + isDone = true; } catch (IOException e) { e.printStackTrace(); } @@ -122,7 +112,9 @@ public static void main(String[] args) throws Exception { client.speechUpdate(new SpeechUpdateEventData(outputAudio)); client.inputTextBufferAppend("hello world, nice to meet you!"); client.inputTextBufferComplete(); - TimeUnit.SECONDS.sleep(100); + while (!isDone) { + TimeUnit.MILLISECONDS.sleep(100); + } } catch (Exception e) { e.printStackTrace(); } finally { diff --git a/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java index 591cd9df..5a07a210 100644 --- a/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java +++ b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java @@ -17,26 +17,14 @@ 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; -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; /* This example describes how to use the chat interface to initiate conversations, poll the status of the conversation, and obtain the messages after the conversation is completed. * */ public class WebsocketTranscriptionsExample { - @Data - @Builder - @AllArgsConstructor - @NoArgsConstructor - private static class Weather { - @JsonProperty("weather") - private String weather; - } + + public static boolean isDone = false; private static class CallbackHandler extends WebsocketsAudioTranscriptionsCallbackHandler { private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 分配 10MB 缓冲区 @@ -84,6 +72,7 @@ public void onTranscriptionsMessageCompleted( WebsocketsAudioTranscriptionsClient client, TranscriptionsMessageCompletedEvent event) { System.out.println("=== Transcriptions Message Completed ==="); System.out.println(event); + isDone = true; } // 语音缓冲区完成事件 (input_audio_buffer.completed) @@ -139,13 +128,18 @@ public static void main(String[] args) throws Exception { while ((bytesRead = inputStream.read(buffer)) != -1) { client.inputAudioBufferAppend(Arrays.copyOf(buffer, bytesRead)); + // 模拟人说话的间隔 + TimeUnit.MILLISECONDS.sleep(100); } client.inputAudioBufferComplete(); } catch (IOException e) { e.printStackTrace(); } - TimeUnit.SECONDS.sleep(100); + while (!isDone) { + TimeUnit.MILLISECONDS.sleep(100); + } + } catch (Exception e) { e.printStackTrace(); } finally { diff --git a/example/src/main/java/example/websocket/chat/ChatExample.java b/example/src/main/java/example/websocket/chat/ChatExample.java index cddcad3b..76a3caf6 100644 --- a/example/src/main/java/example/websocket/chat/ChatExample.java +++ b/example/src/main/java/example/websocket/chat/ChatExample.java @@ -164,6 +164,7 @@ public static void main(String[] args) throws Exception { while ((bytesRead = inputStream.read(buffer)) != -1) { client.inputAudioBufferAppend(Arrays.copyOf(buffer, bytesRead)); + // 模拟人说话的间隔 TimeUnit.MILLISECONDS.sleep(100); } client.inputAudioBufferComplete(); From 5da61356a9f78d8efacb700d19c4ce47e5c48055 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 10 Mar 2025 14:55:22 +0800 Subject: [PATCH 46/50] fix cr Change-Id: Ibd0b2cd1136f5c9a0ca2eccc60f1e46c5ecbb656 --- .../audio/speech/WebsocketAudioSpeechExample.java | 9 ++++++--- .../transcriptions/WebsocketTranscriptionsExample.java | 6 +++--- .../main/java/example/websocket/chat/ChatExample.java | 10 +++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java index e7e07755..17e27b47 100644 --- a/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java +++ b/example/src/main/java/example/websocket/audio/speech/WebsocketAudioSpeechExample.java @@ -17,9 +17,12 @@ import example.utils.ExampleUtils; /* -This example describes how to use the chat interface to initiate conversations, -poll the status of the conversation, and obtain the messages after the conversation is completed. -* */ +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; diff --git a/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java index 5a07a210..afdfd51f 100644 --- a/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java +++ b/example/src/main/java/example/websocket/audio/transcriptions/WebsocketTranscriptionsExample.java @@ -19,9 +19,9 @@ import com.coze.openapi.service.service.websocket.audio.transcriptions.WebsocketsAudioTranscriptionsCreateReq; /* -This example describes how to use the chat interface to initiate conversations, -poll the status of the conversation, and obtain the messages after the conversation is completed. -* */ +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; diff --git a/example/src/main/java/example/websocket/chat/ChatExample.java b/example/src/main/java/example/websocket/chat/ChatExample.java index 76a3caf6..d3a3a581 100644 --- a/example/src/main/java/example/websocket/chat/ChatExample.java +++ b/example/src/main/java/example/websocket/chat/ChatExample.java @@ -30,9 +30,13 @@ import lombok.NoArgsConstructor; /* -This example describes how to use the chat interface to initiate conversations, -poll the status of the conversation, and obtain the messages after the conversation is completed. -* */ +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; From b0359ab7ba132e32e25e64ade2075b8a5a9e60ed Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 10 Mar 2025 16:20:12 +0800 Subject: [PATCH 47/50] fix cr Change-Id: I4583f512d16ff29a132f951c462ce98decbbe26c --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 95f0b2f8..284c4a80 100644 --- a/pom.xml +++ b/pom.xml @@ -59,9 +59,9 @@ **/client/**/* **/BaseCallbackHandler.java - **/WebsocketChatCallbackHandler.java - **/WebsocketAudioTranscriptionsCallbackHandler.java - **/WebsocketAudioSpeechCallbackHandler.java + **/WebsocketsChatCallbackHandler.java + **/WebsocketsAudioTranscriptionsCallbackHandler.java + **/WebsocketsAudioSpeechCallbackHandler.java From a2d2018257c960e591bcbd465196c7d926bfc3f7 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Mon, 10 Mar 2025 17:12:51 +0800 Subject: [PATCH 48/50] add ut Change-Id: I5f7ba5d2f848cb829df805d2384bcb8c80dadd12 --- .../openapi/service/auth/JWTOAuthTest.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 api/src/test/java/com/coze/openapi/service/auth/JWTOAuthTest.java 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()); + } +} From d2ae3ab9bd34a8257b9619e603a3bec0f2a38393 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Tue, 11 Mar 2025 10:40:17 +0800 Subject: [PATCH 49/50] fix error code Change-Id: I7f687143392203a3b524e24b2255b77e2bb1ecb6 --- .../openapi/client/workflows/run/model/WorkflowRunHistory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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") From 30a8a9c2762d5458569c11485f5661f969eb35b5 Mon Sep 17 00:00:00 2001 From: hanzeINGH Date: Wed, 12 Mar 2025 10:43:54 +0800 Subject: [PATCH 50/50] add: turn detection event Change-Id: If1fcbdb9bf7a275eaeb2bbdea5571ecaa2b02294 --- .../client/websocket/event/EventType.java | 4 +++ .../InputAudioBufferSpeechStartedEvent.java | 21 +++++++++++++++ .../InputAudioBufferSpeechStoppedEvent.java | 19 ++++++++++++++ .../event/model/ChatUpdateEventData.java | 3 +++ .../websocket/event/model/TurnDetection.java | 22 ++++++++++++++++ .../chat/WebsocketsChatCallbackHandler.java | 26 +++++++------------ .../websocket/chat/WebsocketsChatClient.java | 10 +++++++ .../chat/WebsocketsChatClientTest.java | 22 ++++++++++++++++ 8 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferSpeechStartedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/downstream/InputAudioBufferSpeechStoppedEvent.java create mode 100644 api/src/main/java/com/coze/openapi/client/websocket/event/model/TurnDetection.java 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 index 943173f0..b71f0318 100644 --- 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 @@ -85,4 +85,8 @@ public class EventType { // 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/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/model/ChatUpdateEventData.java b/api/src/main/java/com/coze/openapi/client/websocket/event/model/ChatUpdateEventData.java index c5a3e396..9fad4fd9 100644 --- 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 @@ -21,4 +21,7 @@ public class ChatUpdateEventData { @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/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/service/service/websocket/chat/WebsocketsChatCallbackHandler.java b/api/src/main/java/com/coze/openapi/service/service/websocket/chat/WebsocketsChatCallbackHandler.java index 9e159264..6f0273ed 100644 --- 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 @@ -1,22 +1,6 @@ package com.coze.openapi.service.service.websocket.chat; -import com.coze.openapi.client.websocket.event.downstream.ChatCreatedEvent; -import com.coze.openapi.client.websocket.event.downstream.ChatUpdatedEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationAudioCompletedEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationAudioDeltaEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationAudioTranscriptCompletedEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationAudioTranscriptUpdateEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationChatCanceledEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationChatCompletedEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationChatCreatedEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationChatFailedEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationChatInProgressEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationChatRequiresActionEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationClearedEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationMessageCompletedEvent; -import com.coze.openapi.client.websocket.event.downstream.ConversationMessageDeltaEvent; -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.*; import com.coze.openapi.service.service.websocket.common.BaseCallbackHandler; public abstract class WebsocketsChatCallbackHandler @@ -87,4 +71,12 @@ public void onConversationAudioTranscriptCompleted( // 端插件事件 (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 index 008aae4a..ed59b4dc 100644 --- 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 @@ -176,6 +176,16 @@ protected void handleEvent(WebSocket ws, String text) { 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); 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 index 60ef5a2e..efccb96c 100644 --- 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 @@ -848,6 +848,28 @@ void testInputAudioBufferAppendWithData() { 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";