Agent 业务逻辑完善
This commit is contained in:
16
pom.xml
16
pom.xml
@@ -292,6 +292,13 @@
|
|||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OkHttp 日志拦截器(新增,必须与核心包版本一致) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>logging-interceptor</artifactId>
|
||||||
|
<version>${okhttp3.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.sendgrid</groupId>
|
<groupId>com.sendgrid</groupId>
|
||||||
<artifactId>sendgrid-java</artifactId>
|
<artifactId>sendgrid-java</artifactId>
|
||||||
@@ -373,6 +380,15 @@
|
|||||||
<version>2.1.3</version>
|
<version>2.1.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.tyrus</groupId>
|
||||||
|
<artifactId>tyrus-client</artifactId>
|
||||||
|
<version>2.1.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,15 @@
|
|||||||
<artifactId>tyrus-client</artifactId>
|
<artifactId>tyrus-client</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>logging-interceptor</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.tyrus</groupId>
|
||||||
|
<artifactId>tyrus-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package com.vetti.socket;
|
package com.vetti.socket;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
|
||||||
import javax.websocket.*;
|
import javax.websocket.*;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
// 客户端端点类
|
// 客户端端点类
|
||||||
@ClientEndpoint
|
@ClientEndpoint
|
||||||
@@ -11,21 +16,51 @@ public class MyWebSocketClient {
|
|||||||
@OnOpen
|
@OnOpen
|
||||||
public void onOpen(Session session) {
|
public void onOpen(Session session) {
|
||||||
System.out.println("连接已建立,Session ID: " + session.getId());
|
System.out.println("连接已建立,Session ID: " + session.getId());
|
||||||
try {
|
|
||||||
// 发送消息到服务端
|
|
||||||
session.getBasicRemote().sendText("Hello, Server!");
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收到服务端消息时触发
|
// 收到服务端消息时触发
|
||||||
@OnMessage
|
@OnMessage
|
||||||
public void onMessage(String message, Session session) {
|
public void onMessage(String message, Session session) {
|
||||||
System.out.println("收到服务端消息: " + message);
|
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<String,String> 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<String,String> 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
|
@OnClose
|
||||||
public void onClose(Session session, CloseReason reason) {
|
public void onClose(Session session, CloseReason reason) {
|
||||||
@@ -41,16 +76,18 @@ public class MyWebSocketClient {
|
|||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// WebSocket服务端地址(示例)
|
// 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 {
|
try {
|
||||||
// 获取WebSocket容器
|
// 获取WebSocket容器
|
||||||
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
|
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
|
||||||
// 连接服务端(传入客户端端点实例和服务端URI)
|
// 连接服务端(传入客户端端点实例和服务端URI)
|
||||||
container.connectToServer(new MyWebSocketClient(), new URI(serverUri));
|
container.connectToServer(new MyWebSocketClient(), new URI(serverUri));
|
||||||
|
// // 1. 设置文本消息缓冲区:256KB(测试足够用)
|
||||||
// 阻塞主线程,避免程序退出(实际场景根据需求处理)
|
// container.setDefaultMaxTextMessageBufferSize(2560 * 1024);
|
||||||
Thread.sleep(60000);
|
// // 2. 设置二进制消息缓冲区(语音流用)
|
||||||
|
// container.setDefaultMaxBinaryMessageBufferSize(2560 * 1024);
|
||||||
|
// // 阻塞主线程,避免程序退出(实际场景根据需求处理)
|
||||||
|
Thread.sleep(6000000);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String, List<String>> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +1,81 @@
|
|||||||
package com.vetti.socket.agents;
|
package com.vetti.socket.agents;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
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
|
@Slf4j
|
||||||
public class ElevenLabsAgentEndpoint extends Endpoint {
|
public class ElevenLabsAgentEndpoint extends Endpoint {
|
||||||
|
|
||||||
private Session agentSession;
|
private Session session;
|
||||||
private String traceId;
|
|
||||||
private WebSocketSession frontendSession;
|
|
||||||
|
|
||||||
private final AtomicInteger audioCount = new AtomicInteger();
|
private final Consumer<Object> onMessage;
|
||||||
|
|
||||||
|
public ElevenLabsAgentEndpoint(Consumer<Object> onMessage) {
|
||||||
|
this.onMessage = onMessage;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOpen(Session session, EndpointConfig config) {
|
public void onOpen(Session session, EndpointConfig config) {
|
||||||
|
log.info("自动链接上了Agent");
|
||||||
log.info("客户端链接啦:{}", traceId);
|
this.session = session;
|
||||||
|
log.info("可以开始发送消息了");
|
||||||
this.agentSession = session;
|
session.addMessageHandler(String.class, onMessage::accept);
|
||||||
this.traceId = (String) config.getUserProperties().get("traceId");
|
session.addMessageHandler(ByteBuffer.class, onMessage::accept);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendText(String text) {
|
public void sendText(String text) {
|
||||||
if (agentSession != null && agentSession.isOpen()) {
|
log.info("Agent-开始发送文本消息: {}", text);
|
||||||
agentSession.getAsyncRemote().sendText(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 {
|
try {
|
||||||
if (agentSession != null) {
|
log.info("Agent-开发发送提交拉");
|
||||||
agentSession.close();
|
Map<String,String> map = new HashMap<>();
|
||||||
|
map.put("type","input_audio_buffer.commit");
|
||||||
|
session.getAsyncRemote().sendText(JSONUtil.toJsonStr(map));
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String, List<String>> 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<String,String> 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<String,String> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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("*");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<String, List<String>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.vetti.web.service;
|
||||||
|
|
||||||
|
public interface ElevenLabsConvAiTokenClientService {
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -960,19 +960,18 @@ public class HotakeAiCommonToolsServiceImpl extends BaseServiceImpl implements I
|
|||||||
|
|
||||||
String prompt = AiCommonPromptConstants.initializationAiInterviewQuestionsPrompt();
|
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" +
|
"\n" +
|
||||||
"**Job Information**:\n" +
|
"**Job Information**:\n" +
|
||||||
"职位名称:【】\n" +
|
"Job Title:【"+rolesInfo.getRoleName()+"】\n" +
|
||||||
"技术要求:【】\n" +
|
"Technical Requirements:【"+rolesInfo.getRequiredSkillsJson()+"-"+rolesInfo.getNiceToHaveSkillsJson()+"】\n" +
|
||||||
"经验要求:【】\n" +
|
"Experience Requirements:【"+rolesInfo.getJobExperience()+"】\n" +
|
||||||
"面试时长:【】\n" +
|
"Interview Duration:【】\n" +
|
||||||
"公司文化:【】\n" +
|
"Company Culture:【】\n" +
|
||||||
"特殊要求:【】\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<Map<String, String>> listOne = new LinkedList();
|
List<Map<String, String>> listOne = new LinkedList();
|
||||||
Map<String, String> mapEntityOne = new HashMap<>();
|
Map<String, String> mapEntityOne = new HashMap<>();
|
||||||
mapEntityOne.put("role", "system");
|
mapEntityOne.put("role", "system");
|
||||||
@@ -985,8 +984,8 @@ public class HotakeAiCommonToolsServiceImpl extends BaseServiceImpl implements I
|
|||||||
String promptJsonOne = JSONUtil.toJsonStr(listOne);
|
String promptJsonOne = JSONUtil.toJsonStr(listOne);
|
||||||
String resultStrOne = chatGPTClient.handleAiChat(promptJsonOne,"RLINKAL");
|
String resultStrOne = chatGPTClient.handleAiChat(promptJsonOne,"RLINKAL");
|
||||||
String resultJsonOne = resultStrOne.replaceAll("```json","").replaceAll("```","");
|
String resultJsonOne = resultStrOne.replaceAll("```json","").replaceAll("```","");
|
||||||
log.info("招聘信息补全:{}",resultJsonOne);
|
log.info("AI面试问题生成结果:{}",resultJsonOne);
|
||||||
|
|
||||||
return "";
|
return resultJsonOne;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user