From 914c69c2deddbc8a99df85dbb58dac2fbfd7470e Mon Sep 17 00:00:00 2001 From: wangxiangshun Date: Thu, 15 Jan 2026 19:58:15 +0800 Subject: [PATCH] =?UTF-8?q?Agent=20=E4=B8=9A=E5=8A=A1=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 16 ++ vetti-admin/pom.xml | 9 + .../com/vetti/socket/MyWebSocketClient.java | 59 +++++- .../socket/agents/ElevenLabsAgentClient.java | 80 ------- .../agents/ElevenLabsAgentEndpoint.java | 121 +++++------ .../vetti/socket/agents/ElevenLabsConfig.java | 17 -- .../agents/FrontendWebSocketHandler.java | 36 ---- .../socket/agents/VoiceBridgeEndpoint.java | 200 ++++++++++++++++++ .../vetti/socket/agents/WebSocketConfig.java | 16 -- .../com/vetti/socket/agents/ai/AgentTest.java | 25 +++ .../agents/ai/ElevenLabsAgentEndpoint.java | 89 ++++++++ .../ElevenLabsConvAiTokenClientService.java | 4 + ...levenLabsConvAiTokenClientServiceImpl.java | 133 ++++++++++++ .../impl/HotakeAiCommonToolsServiceImpl.java | 21 +- 14 files changed, 591 insertions(+), 235 deletions(-) delete mode 100644 vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsAgentClient.java delete mode 100644 vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsConfig.java delete mode 100644 vetti-admin/src/main/java/com/vetti/socket/agents/FrontendWebSocketHandler.java create mode 100644 vetti-admin/src/main/java/com/vetti/socket/agents/VoiceBridgeEndpoint.java delete mode 100644 vetti-admin/src/main/java/com/vetti/socket/agents/WebSocketConfig.java create mode 100644 vetti-admin/src/main/java/com/vetti/socket/agents/ai/AgentTest.java create mode 100644 vetti-admin/src/main/java/com/vetti/socket/agents/ai/ElevenLabsAgentEndpoint.java create mode 100644 vetti-admin/src/main/java/com/vetti/web/service/ElevenLabsConvAiTokenClientService.java create mode 100644 vetti-admin/src/main/java/com/vetti/web/service/impl/ElevenLabsConvAiTokenClientServiceImpl.java diff --git a/pom.xml b/pom.xml index 4356e07..adc718b 100644 --- a/pom.xml +++ b/pom.xml @@ -292,6 +292,13 @@ + + + com.squareup.okhttp3 + logging-interceptor + ${okhttp3.version} + + com.sendgrid sendgrid-java @@ -373,6 +380,15 @@ 2.1.3 + + org.glassfish.tyrus + tyrus-client + 2.1.3 + + + + + diff --git a/vetti-admin/pom.xml b/vetti-admin/pom.xml index e758316..a3b0afa 100644 --- a/vetti-admin/pom.xml +++ b/vetti-admin/pom.xml @@ -97,6 +97,15 @@ tyrus-client + + com.squareup.okhttp3 + logging-interceptor + + + + org.glassfish.tyrus + tyrus-client + diff --git a/vetti-admin/src/main/java/com/vetti/socket/MyWebSocketClient.java b/vetti-admin/src/main/java/com/vetti/socket/MyWebSocketClient.java index 330bc3d..f2b78de 100644 --- a/vetti-admin/src/main/java/com/vetti/socket/MyWebSocketClient.java +++ b/vetti-admin/src/main/java/com/vetti/socket/MyWebSocketClient.java @@ -1,7 +1,12 @@ package com.vetti.socket; +import cn.hutool.json.JSONUtil; + import javax.websocket.*; import java.net.URI; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; // 客户端端点类 @ClientEndpoint @@ -11,21 +16,51 @@ public class MyWebSocketClient { @OnOpen public void onOpen(Session session) { System.out.println("连接已建立,Session ID: " + session.getId()); - try { - // 发送消息到服务端 - session.getBasicRemote().sendText("Hello, Server!"); - } catch (Exception e) { - e.printStackTrace(); - } + } // 收到服务端消息时触发 @OnMessage public void onMessage(String message, Session session) { System.out.println("收到服务端消息: " + message); + Map map1 = JSONUtil.parseObj(message); + Map data = (Map) map1.get("text_response_part"); + if(map1 != null && "conversation_initiation_metadata".equals(map1.get("type").toString())){ + try { + // 发送消息到服务端 + Map map = new HashMap<>(); + map.put("type", "contextual_update"); + map.put("user_id", session.getId()); + map.put("text", "你好,只回复一句话"); + String s = JSONUtil.toJsonStr(map); + session.getBasicRemote().sendText(s); + + } catch (Exception e) { + e.printStackTrace(); + } + } + if(data != null && "stop".equals(data.get("type").toString())&& "agent_chat_response_part".equals(map1.get("type").toString())){ + try { + Thread.sleep(2000); + // 发送消息到服务端 + Map map = new HashMap<>(); + map.put("type", "contextual_update"); + map.put("user_id", session.getId()); + map.put("text", "你能回答我其他的嘛"); + String s = JSONUtil.toJsonStr(map); + session.getBasicRemote().sendText(s); + + } catch (Exception e) { + e.printStackTrace(); + } + } // 可根据消息内容做后续处理 } +// @OnMessage +// public void onBinary(ByteBuffer buffer) { +// System.out.println("收到服务端语音流啦: " + buffer); +// } // 连接关闭时触发 @OnClose public void onClose(Session session, CloseReason reason) { @@ -41,16 +76,18 @@ public class MyWebSocketClient { public static void main(String[] args) { // WebSocket服务端地址(示例) - String serverUri = "ws://vetti.hotake.cn/prod-api/voice-websocket-opus/104"; - + String serverUri = "ws://vetti.hotake.cn/prod-api/voice-websocket/elevenLabsAgent/104"; try { // 获取WebSocket容器 WebSocketContainer container = ContainerProvider.getWebSocketContainer(); // 连接服务端(传入客户端端点实例和服务端URI) container.connectToServer(new MyWebSocketClient(), new URI(serverUri)); - - // 阻塞主线程,避免程序退出(实际场景根据需求处理) - Thread.sleep(60000); +// // 1. 设置文本消息缓冲区:256KB(测试足够用) +// container.setDefaultMaxTextMessageBufferSize(2560 * 1024); +// // 2. 设置二进制消息缓冲区(语音流用) +// container.setDefaultMaxBinaryMessageBufferSize(2560 * 1024); +// // 阻塞主线程,避免程序退出(实际场景根据需求处理) + Thread.sleep(6000000); } catch (Exception e) { e.printStackTrace(); } diff --git a/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsAgentClient.java b/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsAgentClient.java deleted file mode 100644 index 1dbb277..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsAgentClient.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.vetti.socket.agents; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.socket.WebSocketSession; - -import javax.websocket.ClientEndpointConfig; -import javax.websocket.ContainerProvider; -import javax.websocket.WebSocketContainer; -import java.net.URI; -import java.util.List; -import java.util.Map; - -@Slf4j -public class ElevenLabsAgentClient { - - private static final String AGENT_WS_URL = - "wss://api.elevenlabs.io/v1/agents/%s/stream"; - - - - private final ElevenLabsAgentEndpoint endpoint; - - public ElevenLabsAgentClient(String traceId, WebSocketSession frontendSession) { - this.endpoint = new ElevenLabsAgentEndpoint(); - connect(traceId, frontendSession); - } - - private void connect(String traceId, WebSocketSession frontendSession) { - try { - log.info("[traceId={}] Connecting to ElevenLabs Agent...", traceId); - - WebSocketContainer container = - ContainerProvider.getWebSocketContainer(); - - ClientEndpointConfig config = - ClientEndpointConfig.Builder.create() - .configurator(new ClientEndpointConfig.Configurator() { - @Override - public void beforeRequest( - Map> headers - ) { - headers.put( - "xi-api-key", - List.of("sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116") - ); - } - }) - .build(); - - config.getUserProperties().put("traceId", traceId); - config.getUserProperties().put("frontendSession", frontendSession); - - container.connectToServer( - endpoint, - config, - URI.create( - String.format( - AGENT_WS_URL, - "9c5cb2f7ba9efb61d0f0eee01427b6e00c6abe92d4754cfb794884ac4d73c79d" - ) - ) - ); - - } catch (Exception e) { - throw new RuntimeException("Connect ElevenLabs failed", e); - } - } - - public void sendAudio(java.nio.ByteBuffer buffer) { - endpoint.sendAudio(buffer); - } - - public void sendText(String text) { - endpoint.sendText(text); - } - - public void close() { - endpoint.close(); - } -} diff --git a/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsAgentEndpoint.java b/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsAgentEndpoint.java index a55d6af..6c864ce 100644 --- a/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsAgentEndpoint.java +++ b/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsAgentEndpoint.java @@ -1,88 +1,81 @@ package com.vetti.socket.agents; +import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.socket.BinaryMessage; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; -import javax.websocket.*; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.Session; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +/** + * ElevenLabs Agent 客户端端点 - 处理逻辑 + */ @Slf4j public class ElevenLabsAgentEndpoint extends Endpoint { - private Session agentSession; - private String traceId; - private WebSocketSession frontendSession; + private Session session; - private final AtomicInteger audioCount = new AtomicInteger(); + private final Consumer onMessage; + + public ElevenLabsAgentEndpoint(Consumer onMessage) { + this.onMessage = onMessage; + } @Override public void onOpen(Session session, EndpointConfig config) { - - log.info("客户端链接啦:{}", traceId); - - this.agentSession = session; - this.traceId = (String) config.getUserProperties().get("traceId"); - this.frontendSession = - (WebSocketSession) config.getUserProperties().get("frontendSession"); - - log.info("[traceId={}] ElevenLabs Agent CONNECTED", traceId); - - session.addMessageHandler(String.class, this::onText); - session.addMessageHandler(ByteBuffer.class, this::onAudio); - } - - private void onText(String message) { - try { - log.info("[traceId={}] Agent → TEXT {}", traceId, message); - frontendSession.sendMessage(new TextMessage(message)); - } catch (IOException e) { - log.error("[traceId={}] Send text failed", traceId, e); - } - } - - private void onAudio(ByteBuffer buffer) { - try { - int count = audioCount.incrementAndGet(); - if (count == 1) { - log.info( - "[traceId={}] Agent → AUDIO FIRST packet size={} bytes", - traceId, - buffer.remaining() - ); - } - frontendSession.sendMessage(new BinaryMessage(buffer)); - } catch (IOException e) { - log.error("[traceId={}] Send audio failed", traceId, e); - } - } - - @Override - public void onClose(Session session, CloseReason closeReason) { - log.info("[traceId={}] ElevenLabs Agent CLOSED {}", traceId, closeReason); - } - - public void sendAudio(ByteBuffer buffer) { - if (agentSession != null && agentSession.isOpen()) { - agentSession.getAsyncRemote().sendBinary(buffer); - } + log.info("自动链接上了Agent"); + this.session = session; + log.info("可以开始发送消息了"); + session.addMessageHandler(String.class, onMessage::accept); + session.addMessageHandler(ByteBuffer.class, onMessage::accept); } public void sendText(String text) { - if (agentSession != null && agentSession.isOpen()) { - agentSession.getAsyncRemote().sendText(text); + log.info("Agent-开始发送文本消息: {}", text); + log.info("Agent-发送文本的Session为: {}", session); + if (session == null || !session.isOpen()) return; + log.info("Agent-开始发送文本发送: {}", text); + session.getAsyncRemote().sendText(text); + } + + public void sendBinaryText(String text) { + log.info("Agent-开始发送语音文本消息: {}", text); + log.info("Agent-发送语音文本的Session为: {}", session); + if (session == null || !session.isOpen()) return; + log.info("Agent-开始发送语音文本发送: {}", text); + session.getAsyncRemote().sendText(text); + } + + public void sendBinary(ByteBuffer buffer) { + if (session == null || !session.isOpen()) return; + + session.getAsyncRemote().sendBinary(buffer); + } + + public void close() throws IOException { + if (session != null && session.isOpen()) { + session.close(); } } - public void close() { + /** + * 针对语音流发送的时候,如果什么都接收不到的时候,就直接进行提交 + */ + public void commit() { + if (session == null || !session.isOpen()) return; try { - if (agentSession != null) { - agentSession.close(); - } - } catch (Exception ignored) {} + log.info("Agent-开发发送提交拉"); + Map map = new HashMap<>(); + map.put("type","input_audio_buffer.commit"); + session.getAsyncRemote().sendText(JSONUtil.toJsonStr(map)); + } catch (Exception e) { + e.printStackTrace(); + } } } diff --git a/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsConfig.java b/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsConfig.java deleted file mode 100644 index f0efe41..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/agents/ElevenLabsConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.vetti.socket.agents; - -import lombok.Data; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -@Data -public class ElevenLabsConfig { - - @Value("${elevenLabs.agent-id}") - private String agentId; - - @Value("${elevenLabs.api-key}") - private String apiKey; -} - diff --git a/vetti-admin/src/main/java/com/vetti/socket/agents/FrontendWebSocketHandler.java b/vetti-admin/src/main/java/com/vetti/socket/agents/FrontendWebSocketHandler.java deleted file mode 100644 index 6e1eba4..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/agents/FrontendWebSocketHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vetti.socket.agents; - - -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.socket.*; -import org.springframework.web.socket.handler.BinaryWebSocketHandler; - -@Slf4j -public class FrontendWebSocketHandler extends BinaryWebSocketHandler { - - private ElevenLabsAgentClient agentClient; - - @Override - public void afterConnectionEstablished(WebSocketSession session) { - String traceId = session.getId(); - log.info("[traceId={}] Vue WebSocket CONNECTED", traceId); - - agentClient = new ElevenLabsAgentClient(traceId, session); - } - - @Override - protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) { - agentClient.sendAudio(message.getPayload()); - } - - @Override - protected void handleTextMessage(WebSocketSession session, TextMessage message) { - agentClient.sendText(message.getPayload()); - } - - @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { - log.info("[traceId={}] Vue WebSocket CLOSED", session.getId()); - agentClient.close(); - } -} diff --git a/vetti-admin/src/main/java/com/vetti/socket/agents/VoiceBridgeEndpoint.java b/vetti-admin/src/main/java/com/vetti/socket/agents/VoiceBridgeEndpoint.java new file mode 100644 index 0000000..ab1e789 --- /dev/null +++ b/vetti-admin/src/main/java/com/vetti/socket/agents/VoiceBridgeEndpoint.java @@ -0,0 +1,200 @@ +package com.vetti.socket.agents; + +import javax.websocket.*; +import javax.websocket.server.ServerEndpoint; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONUtil; +import com.vetti.common.config.RuoYiConfig; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Base64; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + + +@Slf4j +@ServerEndpoint("/voice-websocket/elevenLabsAgent/{clientId}") +@Component +public class VoiceBridgeEndpoint { + + // 语音文件保存目录 + private static final String VOICE_STORAGE_DIR = "/voice_files/"; + + // 语音结果文件保存目录 + private static final String VOICE_STORAGE_RESULT_DIR = "/voice_result_files/"; + + // 系统语音目录 + private static final String VOICE_SYSTEM_DIR = "/system_files/"; + + + private Session frontendSession; + + private ElevenLabsAgentEndpoint agentClient; + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private ScheduledFuture pendingCommit; + + private final long timeoutMs = 600; + + private static final String AGENT_URL = + "wss://api.elevenlabs.io/v1/convai/conversation" + + "?agent_id=agent_9401kd09yfjnes2vddz1n29wev2t"; + + @OnOpen + public void onOpen(Session session) throws Exception { + log.info("已经有客户端链接啦:{}",session.getId()); + this.frontendSession = session; + ClientEndpointConfig clientConfig = + ClientEndpointConfig.Builder.create() + .configurator(new ClientEndpointConfig.Configurator() { + @Override + public void beforeRequest( + Map> headers) { + headers.put( + "Authorization", + List.of("Bearer "+"sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116" ) + ); + } + }) + .build(); + WebSocketContainer container = + ContainerProvider.getWebSocketContainer(); + agentClient = new ElevenLabsAgentEndpoint(msg -> { + if (msg instanceof String) { + frontendSession.getAsyncRemote() + .sendText((String) msg); + } else if (msg instanceof ByteBuffer) { + frontendSession.getAsyncRemote() + .sendBinary((ByteBuffer) msg); + } + }); + // ✅ 完全匹配的方法签名 + container.connectToServer( + agentClient, // Endpoint 子类 + clientConfig, + URI.create(AGENT_URL) + ); + log.info("我开始准备发送启动的提示语音啦"); + //链接成功啦 + //发送初始化面试官语音流 +// String openingPathUrl = RuoYiConfig.getProfile() + VOICE_SYSTEM_DIR + "opening.wav"; +// sendVoiceBuffer(openingPathUrl, session); + log.info("发送完毕啦"); + } + + /** 前端 → ElevenLabs(JSON 控制) */ + @OnMessage + public void onText(String message) { + log.info("我收到前端发送过来的文本啦:{}",message); + agentClient.sendText(message); + } + + /** 前端 → ElevenLabs(语音 PCM Binary) */ + @OnMessage + public void onBinary(ByteBuffer buffer) { + log.info("我收到前端发送过来的PCM语音流啦"); + //处理语音流,base64推送过去 + String bufferBase64 = convertByteBufferToBase64Pcm16k(buffer); + Map binaryMap = new HashMap<>(); + binaryMap.put("type","input_audio_buffer.append"); + binaryMap.put("audio",bufferBase64); + String jsonStr = JSONUtil.toJsonStr(binaryMap); + log.info("记录Agent对象是不是为空:{}",jsonStr); + agentClient.sendBinaryText(jsonStr); + + //发送结束的语音流-进行语音流提交 + // 重置 commit 定时器 + if (pendingCommit != null) { + pendingCommit.cancel(false); + } + + pendingCommit = scheduler.schedule(() -> { + log.info("No audio received, commit()"); + agentClient.commit(); + }, timeoutMs, TimeUnit.MILLISECONDS); + + } + + @OnClose + public void onClose() throws IOException { + agentClient.close(); + } + + @OnError + public void onError(Throwable t) { + t.printStackTrace(); + } + + /** + * 发送语音流给前端 + * + * @param pathUrl 语音文件地址 + * @param session 客户端会话 + */ + private void sendVoiceBuffer(String pathUrl, Session session) { + try { + //文件转换成文件流 + ByteBuffer outByteBuffer = convertFileToByteBuffer(pathUrl); + //发送文件流数据 + session.getAsyncRemote().sendBinary(outByteBuffer); + try { + Thread.sleep(200); + }catch (Exception e){} + //提示已经结束 + Map dataText = new HashMap<>(); + dataText.put("type","voiceEnd"); + dataText.put("content",""); + session.getAsyncRemote().sendText(JSONUtil.toJsonStr(dataText)); + // 发送响应确认 + log.info("已经成功发送了语音流给前端:{}", DateUtil.now()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * File 转换成 ByteBuffer + * + * @param fileUrl 文件路径 + * @return + */ + private ByteBuffer convertFileToByteBuffer(String fileUrl) { + File file = new File(fileUrl); + try { + return ByteBuffer.wrap(FileUtils.readFileToByteArray(file)); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 核心方法:ByteBuffer 转 Base64 编码的 16K PCM + * @param buffer 原始16K PCM音频的ByteBuffer + * @return Base64字符串(16K PCM格式) + */ + private String convertByteBufferToBase64Pcm16k(ByteBuffer buffer) { + // 1. 从ByteBuffer提取字节数组(关键:避免越界) + byte[] audioBytes = new byte[buffer.remaining()]; + buffer.get(audioBytes); // 读取数据到字节数组,buffer指针会移动 + buffer.rewind(); // 重置buffer指针(可选,便于后续复用) + + // 2. 编码为Base64字符串(Java 8+ 原生支持) + return Base64.getEncoder().encodeToString(audioBytes); + } + +} + diff --git a/vetti-admin/src/main/java/com/vetti/socket/agents/WebSocketConfig.java b/vetti-admin/src/main/java/com/vetti/socket/agents/WebSocketConfig.java deleted file mode 100644 index 3b4d59a..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/agents/WebSocketConfig.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.vetti.socket.agents; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.*; - -@Configuration -@EnableWebSocket -public class WebSocketConfig implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(new FrontendWebSocketHandler(), "/voice-websocket/elevenLabsAgent/{clientId}") - .setAllowedOrigins("*"); - } -} - diff --git a/vetti-admin/src/main/java/com/vetti/socket/agents/ai/AgentTest.java b/vetti-admin/src/main/java/com/vetti/socket/agents/ai/AgentTest.java new file mode 100644 index 0000000..a0a5869 --- /dev/null +++ b/vetti-admin/src/main/java/com/vetti/socket/agents/ai/AgentTest.java @@ -0,0 +1,25 @@ +package com.vetti.socket.agents.ai; + +public class AgentTest { + public static void main(String[] args) throws Exception { + String agentId = "agent_9401kd09yfjnes2vddz1n29wev2t"; + String apiKey = "sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116"; + + ElevenLabsAgentEndpoint client = new ElevenLabsAgentEndpoint(); + client.connect(agentId, apiKey); + + // 发送第一条消息 + client.sendTextMessage("你好,只回复一句话"); + + // 可以睡几秒,等待 Agent 回复 + Thread.sleep(3000); + + // 发送第二条消息 + client.sendTextMessage("再来一句自我介绍"); + + Thread.sleep(5000); + + client.close(); + } +} + diff --git a/vetti-admin/src/main/java/com/vetti/socket/agents/ai/ElevenLabsAgentEndpoint.java b/vetti-admin/src/main/java/com/vetti/socket/agents/ai/ElevenLabsAgentEndpoint.java new file mode 100644 index 0000000..ddbebdc --- /dev/null +++ b/vetti-admin/src/main/java/com/vetti/socket/agents/ai/ElevenLabsAgentEndpoint.java @@ -0,0 +1,89 @@ +package com.vetti.socket.agents.ai; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.websocket.*; +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ElevenLabsAgentEndpoint extends Endpoint { + + private Session session; + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public void onOpen(Session session, EndpointConfig config) { + this.session = session; + System.out.println("WebSocket opened"); + + session.addMessageHandler(String.class, this::onMessage); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + System.out.println("WebSocket closed: " + closeReason); + } + + @Override + public void onError(Session session, Throwable thr) { + thr.printStackTrace(); + } + + private void onMessage(String message) { + try { + JsonNode json = mapper.readTree(message); + System.out.println("Received: " + json.toPrettyString()); + + // 判断 turn 结束 + if ("agent_chat_response_part".equals(json.path("type").asText())) { + JsonNode part = json.path("text_response_part"); + if ("stop".equals(part.path("type").asText())) { + System.out.println("Turn ended"); + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void sendTextMessage(String text) { + if (session != null && session.isOpen()) { + String msgJson = String.format("{\"type\":\"input_text\",\"text\":\"%s\"}", text); + session.getAsyncRemote().sendText(msgJson); + } else { + throw new IllegalStateException("Session not open"); + } + } + + public void connect(String agentId, String apiKey) throws Exception { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + + // 配置 HTTP header + ClientEndpointConfig.Configurator configurator = new ClientEndpointConfig.Configurator() { + @Override + public void beforeRequest(Map> headers) { + headers.put("xi-api-key", Collections.singletonList(apiKey)); + } + }; + ClientEndpointConfig config = ClientEndpointConfig.Builder.create() + .configurator(configurator) + .build(); + + String url = "wss://api.elevenlabs.io/v1/convai/conversation?agent_id=" + agentId; + + // 注意,这里是关键:返回 Session + Session wsSession = container.connectToServer(this, config, URI.create(url)); + this.session = wsSession; + } + + public void close() throws Exception { + if (session != null) { + session.close(); + } + } +} + diff --git a/vetti-admin/src/main/java/com/vetti/web/service/ElevenLabsConvAiTokenClientService.java b/vetti-admin/src/main/java/com/vetti/web/service/ElevenLabsConvAiTokenClientService.java new file mode 100644 index 0000000..3f58edf --- /dev/null +++ b/vetti-admin/src/main/java/com/vetti/web/service/ElevenLabsConvAiTokenClientService.java @@ -0,0 +1,4 @@ +package com.vetti.web.service; + +public interface ElevenLabsConvAiTokenClientService { +} diff --git a/vetti-admin/src/main/java/com/vetti/web/service/impl/ElevenLabsConvAiTokenClientServiceImpl.java b/vetti-admin/src/main/java/com/vetti/web/service/impl/ElevenLabsConvAiTokenClientServiceImpl.java new file mode 100644 index 0000000..689c40b --- /dev/null +++ b/vetti-admin/src/main/java/com/vetti/web/service/impl/ElevenLabsConvAiTokenClientServiceImpl.java @@ -0,0 +1,133 @@ +package com.vetti.web.service.impl; + +import com.google.gson.Gson; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class ElevenLabsConvAiTokenClientServiceImpl { + // ========== 配置项(替换为你的实际值) ========== + private static final String XI_API_KEY = "sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116"; // 从ElevenLabs控制台获取 + private static final String AGENT_ID = "agent_9401kd09yfjnes2vddz1n29wev2t"; // 如:agent_9401kd09yfjnes2vddz1n29wev2t + private static final String TOKEN_API_URL = "https://api.elevenlabs.io/v1/convai/conversation/token"; + + // JSON媒体类型 + private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8"); + private final OkHttpClient client; + private final Gson gson; + + // 构造方法:初始化客户端 + public ElevenLabsConvAiTokenClientServiceImpl() { + // 初始化OkHttp客户端(设置超时) + this.client = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build(); + this.gson = new Gson(); + } + + // ===================== 核心:获取ConvAI Token ===================== + /** + * 获取ConvAI对话Token + * @param conversationId 会话ID(可选,不传则自动生成) + * @param userId 用户ID(可选,用于区分用户) + * @return Token响应对象 + * @throws IOException 网络/接口异常 + */ + public ConvAiTokenResponse getConvAiToken(String conversationId, String userId) throws IOException { + // 1. 构造请求体 + ConvAiTokenRequest requestBody = new ConvAiTokenRequest(); + requestBody.setAgent_id(AGENT_ID); + // 会话ID:不传则生成随机ID + requestBody.setConversation_id(conversationId == null ? UUID.randomUUID().toString() : conversationId); + requestBody.setUser_id(userId); // 用户ID(可选) + + // 2. 构建HTTP请求 + Request request = new Request.Builder() + .url(TOKEN_API_URL) + // 核心鉴权:添加XI-API-KEY头 + .addHeader("xi-api-key", XI_API_KEY) + .addHeader("Content-Type", "application/json") + // 发送JSON请求体 + .post(RequestBody.create(gson.toJson(requestBody), JSON_MEDIA_TYPE)) + .build(); + + // 3. 执行请求并解析响应 + try (Response response = client.newCall(request).execute()) { + // 检查响应状态 + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "无错误信息"; + throw new IOException("获取Token失败:HTTP码=" + response.code() + ",错误信息=" + errorBody); + } + + // 解析JSON响应为实体类 + String responseBody = response.body().string(); + return gson.fromJson(responseBody, ConvAiTokenResponse.class); + } + } + + // ===================== 数据模型(匹配接口格式) ===================== + /** + * Token请求体(对应接口入参) + */ + static class ConvAiTokenRequest { + private String agent_id; // Agent ID(必填) + private String conversation_id; // 会话ID(可选) + private String user_id; // 用户ID(可选) + + // Getter & Setter + public String getAgent_id() { return agent_id; } + public void setAgent_id(String agent_id) { this.agent_id = agent_id; } + public String getConversation_id() { return conversation_id; } + public void setConversation_id(String conversation_id) { this.conversation_id = conversation_id; } + public String getUser_id() { return user_id; } + public void setUser_id(String user_id) { this.user_id = user_id; } + } + + /** + * Token响应体(对应接口返回) + */ + static class ConvAiTokenResponse { + private String token; // 核心对话Token + private String conversation_id; // 会话ID + private long expires_at; // Token过期时间(时间戳,秒) + + // Getter & Setter + public String getToken() { return token; } + public void setToken(String token) { this.token = token; } + public String getConversation_id() { return conversation_id; } + public void setConversation_id(String conversation_id) { this.conversation_id = conversation_id; } + public long getExpires_at() { return expires_at; } + public void setExpires_at(long expires_at) { this.expires_at = expires_at; } + } + + // ===================== 测试主方法 ===================== +// public static void main(String[] args) { +// ElevenLabsConvAiTokenClientServiceImpl client = new ElevenLabsConvAiTokenClientServiceImpl(); +// try { +// // 获取Token(传用户ID,会话ID自动生成) +// ConvAiTokenResponse response = client.getConvAiToken(null, "test_user_001"); +// +// // 打印结果 +// System.out.println("✅ 获取Token成功!"); +// System.out.println("Token:" + response.getToken()); +// System.out.println("会话ID:" + response.getConversation_id()); +// System.out.println("过期时间(时间戳):" + response.getExpires_at()); +// +// // 后续使用:拼接WebSocket地址 +// String wsUrl = "wss://api.elevenlabs.io/v1/convai/ws?token=" + response.getToken(); +// System.out.println("WebSocket连接地址:" + wsUrl); +// +// } catch (IOException e) { +// System.err.println("❌ 获取Token失败:" + e.getMessage()); +// e.printStackTrace(); +// } +// } + +} diff --git a/vetti-hotakes/src/main/java/com/vetti/hotake/service/impl/HotakeAiCommonToolsServiceImpl.java b/vetti-hotakes/src/main/java/com/vetti/hotake/service/impl/HotakeAiCommonToolsServiceImpl.java index 3ca6200..f00ab2b 100644 --- a/vetti-hotakes/src/main/java/com/vetti/hotake/service/impl/HotakeAiCommonToolsServiceImpl.java +++ b/vetti-hotakes/src/main/java/com/vetti/hotake/service/impl/HotakeAiCommonToolsServiceImpl.java @@ -960,19 +960,18 @@ public class HotakeAiCommonToolsServiceImpl extends BaseServiceImpl implements I String prompt = AiCommonPromptConstants.initializationAiInterviewQuestionsPrompt(); - String resultJson = "Please generate AI interview questions based on the following job description:\n" + + String userPrompt_1 = "Please generate AI interview questions based on the following job description:\n" + "\n" + "**Job Information**:\n" + - "职位名称:【】\n" + - "技术要求:【】\n" + - "经验要求:【】\n" + - "面试时长:【】\n" + - "公司文化:【】\n" + - "特殊要求:【】\n" + + "Job Title:【"+rolesInfo.getRoleName()+"】\n" + + "Technical Requirements:【"+rolesInfo.getRequiredSkillsJson()+"-"+rolesInfo.getNiceToHaveSkillsJson()+"】\n" + + "Experience Requirements:【"+rolesInfo.getJobExperience()+"】\n" + + "Interview Duration:【】\n" + + "Company Culture:【】\n" + + "Special Requirements:【"+rolesInfo.getAboutRole()+"】\n" + "`;"; - log.info("招聘链接信息提取:{}",resultJson); + log.info("AI面试问题生成:{}",userPrompt_1); //处理岗位信息补充 - String userPrompt_1 = "Please generate complete API-formatted data based on the extracted job information below:\\n\\n" +resultJson; List> listOne = new LinkedList(); Map mapEntityOne = new HashMap<>(); mapEntityOne.put("role", "system"); @@ -985,8 +984,8 @@ public class HotakeAiCommonToolsServiceImpl extends BaseServiceImpl implements I String promptJsonOne = JSONUtil.toJsonStr(listOne); String resultStrOne = chatGPTClient.handleAiChat(promptJsonOne,"RLINKAL"); String resultJsonOne = resultStrOne.replaceAll("```json","").replaceAll("```",""); - log.info("招聘信息补全:{}",resultJsonOne); + log.info("AI面试问题生成结果:{}",resultJsonOne); - return ""; + return resultJsonOne; } }