业务逻辑修改以及完善
This commit is contained in:
@@ -1,173 +1,80 @@
|
||||
package com.vetti.socket.agents;
|
||||
|
||||
import okhttp3.*;
|
||||
import okio.ByteString;
|
||||
import org.springframework.web.socket.BinaryMessage;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import javax.websocket.ClientEndpointConfig;
|
||||
import javax.websocket.ContainerProvider;
|
||||
import javax.websocket.WebSocketContainer;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class ElevenLabsAgentClient extends TextWebSocketHandler{
|
||||
@Slf4j
|
||||
public class ElevenLabsAgentClient {
|
||||
|
||||
// 存储Vue会话与ElevenLabs WebSocket的映射(多客户端隔离)
|
||||
private static final Map<WebSocketSession, WebSocket> SESSION_MAP = new ConcurrentHashMap<>();
|
||||
// ElevenLabs配置
|
||||
private static final String ELEVEN_LABS_API_KEY = "你的ElevenLabs API Key";
|
||||
private static final String AGENT_ID = "你的ElevenLabs Agent ID";
|
||||
private static final String ELEVEN_LABS_WSS_URL = "wss://api.elevenlabs.io/v1/agents/" + AGENT_ID + "/stream";
|
||||
// OkHttp客户端
|
||||
private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder()
|
||||
.readTimeout(0, TimeUnit.MILLISECONDS) // WebSocket长连接取消读超时
|
||||
.build();
|
||||
private static final String AGENT_WS_URL =
|
||||
"wss://api.elevenlabs.io/v1/agents/%s/stream";
|
||||
|
||||
// ==================== 1. 处理Vue前端连接 ====================
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession vueSession) throws Exception {
|
||||
super.afterConnectionEstablished(vueSession);
|
||||
System.out.println("Vue前端连接成功:" + vueSession.getId());
|
||||
|
||||
// 建立与ElevenLabs Agents的WSS连接
|
||||
buildElevenLabsWssConnection(vueSession);
|
||||
|
||||
private final ElevenLabsAgentEndpoint endpoint;
|
||||
|
||||
public ElevenLabsAgentClient(String traceId, WebSocketSession frontendSession) {
|
||||
this.endpoint = new ElevenLabsAgentEndpoint();
|
||||
connect(traceId, frontendSession);
|
||||
}
|
||||
|
||||
// ==================== 2. 处理Vue前端发送的文本消息(如语音指令、配置信息) ====================
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession vueSession, TextMessage message) throws Exception {
|
||||
super.handleTextMessage(vueSession, message);
|
||||
System.out.println("接收Vue文本消息:" + message.getPayload());
|
||||
// 获取对应ElevenLabs WebSocket连接,转发消息
|
||||
WebSocket elevenLabsWs = SESSION_MAP.get(vueSession);
|
||||
if (elevenLabsWs != null && elevenLabsWs.queueSize() == 0) {
|
||||
elevenLabsWs.send(message.getPayload());
|
||||
System.out.println("转发文本消息到ElevenLabs成功");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 3. 处理Vue前端发送的二进制消息(核心:语音流数据) ====================
|
||||
@Override
|
||||
protected void handleBinaryMessage(WebSocketSession vueSession, BinaryMessage message) {
|
||||
super.handleBinaryMessage(vueSession, message);
|
||||
byte[] voiceData = message.getPayload().array();
|
||||
System.out.println("接收Vue语音流数据,字节长度:" + voiceData.length);
|
||||
|
||||
// 获取对应ElevenLabs WebSocket连接,转发语音流(二进制)
|
||||
WebSocket elevenLabsWs = SESSION_MAP.get(vueSession);
|
||||
if (elevenLabsWs != null && elevenLabsWs.queueSize() == 0) {
|
||||
elevenLabsWs.send(ByteString.of(voiceData));
|
||||
System.out.println("转发语音流到ElevenLabs成功");
|
||||
}
|
||||
|
||||
// 释放二进制消息资源
|
||||
// message.isLast();
|
||||
public void sendAudio(java.nio.ByteBuffer buffer) {
|
||||
endpoint.sendAudio(buffer);
|
||||
}
|
||||
|
||||
// ==================== 4. 处理Vue前端连接关闭 ====================
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession vueSession, org.springframework.web.socket.CloseStatus status) throws Exception {
|
||||
super.afterConnectionClosed(vueSession, status);
|
||||
System.out.println("Vue前端连接关闭:" + vueSession.getId() + ",原因:" + status.getReason());
|
||||
|
||||
// 关闭对应的ElevenLabs WSS连接
|
||||
WebSocket elevenLabsWs = SESSION_MAP.remove(vueSession);
|
||||
if (elevenLabsWs != null) {
|
||||
elevenLabsWs.close(1000, "Vue客户端断开连接");
|
||||
System.out.println("关闭ElevenLabs WSS连接成功");
|
||||
}
|
||||
public void sendText(String text) {
|
||||
endpoint.sendText(text);
|
||||
}
|
||||
|
||||
// ==================== 5. 建立与ElevenLabs Agents的WSS连接 ====================
|
||||
private void buildElevenLabsWssConnection(WebSocketSession vueSession) {
|
||||
// 1. 构建ElevenLabs WSS请求(携带认证头)
|
||||
Request request = new Request.Builder()
|
||||
.url(ELEVEN_LABS_WSS_URL)
|
||||
.header("xi-api-key", ELEVEN_LABS_API_KEY) // 必选认证头
|
||||
.build();
|
||||
|
||||
// 2. 构建ElevenLabs WSS监听器(处理响应并回流到Vue)
|
||||
WebSocketListener elevenLabsListener = new WebSocketListener() {
|
||||
// ElevenLabs WSS连接建立
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
super.onOpen(webSocket, response);
|
||||
System.out.println("与ElevenLabs Agents WSS连接成功");
|
||||
// 存储Vue会话与ElevenLabs WS的映射
|
||||
SESSION_MAP.put(vueSession, webSocket);
|
||||
}
|
||||
|
||||
// 接收ElevenLabs文本消息(如状态、错误提示),回流到Vue
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
super.onMessage(webSocket, text);
|
||||
System.out.println("接收ElevenLabs文本消息:" + text);
|
||||
try {
|
||||
// 回流到Vue前端
|
||||
if (vueSession.isOpen()) {
|
||||
vueSession.sendMessage(new TextMessage(text));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("回流文本消息到Vue失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 接收ElevenLabs二进制消息(核心:音频流响应),回流到Vue
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
||||
super.onMessage(webSocket, bytes);
|
||||
byte[] audioData = bytes.toByteArray();
|
||||
System.out.println("接收ElevenLabs音频流数据,字节长度:" + audioData.length);
|
||||
try {
|
||||
// 回流二进制音频流到Vue前端
|
||||
if (vueSession.isOpen()) {
|
||||
vueSession.sendMessage(new BinaryMessage(audioData));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("回流音频流到Vue失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ElevenLabs WSS连接关闭
|
||||
@Override
|
||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||
super.onClosed(webSocket, code, reason);
|
||||
System.out.println("ElevenLabs WSS连接关闭:" + reason + ",状态码:" + code);
|
||||
// 移除映射,关闭Vue连接
|
||||
SESSION_MAP.remove(vueSession);
|
||||
try {
|
||||
if (vueSession.isOpen()) {
|
||||
vueSession.close(org.springframework.web.socket.CloseStatus.NORMAL);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// ElevenLabs WSS连接异常
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||
super.onFailure(webSocket, t, response);
|
||||
System.err.println("ElevenLabs WSS连接异常:" + t.getMessage());
|
||||
// 移除映射,关闭Vue连接
|
||||
SESSION_MAP.remove(vueSession);
|
||||
try {
|
||||
if (vueSession.isOpen()) {
|
||||
vueSession.close(org.springframework.web.socket.CloseStatus.SERVER_ERROR);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 建立ElevenLabs WSS连接
|
||||
OK_HTTP_CLIENT.newWebSocket(request, elevenLabsListener);
|
||||
public void close() {
|
||||
endpoint.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.vetti.socket.agents;
|
||||
|
||||
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 java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Slf4j
|
||||
public class ElevenLabsAgentEndpoint extends Endpoint {
|
||||
|
||||
private Session agentSession;
|
||||
private String traceId;
|
||||
private WebSocketSession frontendSession;
|
||||
|
||||
private final AtomicInteger audioCount = new AtomicInteger();
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendText(String text) {
|
||||
if (agentSession != null && agentSession.isOpen()) {
|
||||
agentSession.getAsyncRemote().sendText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
if (agentSession != null) {
|
||||
agentSession.close();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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,16 @@
|
||||
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("*");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vetti.web.controller.ai;
|
||||
import com.vetti.common.core.controller.BaseController;
|
||||
import com.vetti.common.core.domain.R;
|
||||
import com.vetti.hotake.domain.HotakeInitialScreeningQuestionsInfo;
|
||||
import com.vetti.hotake.domain.HotakeRolesInfo;
|
||||
import com.vetti.hotake.domain.dto.*;
|
||||
import com.vetti.hotake.domain.vo.*;
|
||||
import com.vetti.hotake.service.IHotakeAiCommonToolsService;
|
||||
@@ -160,4 +161,24 @@ public class HotakeAiCommonToolsController extends BaseController {
|
||||
return R.ok(hotakeAiCommonToolsService.getRoleLinkAnalysis(roleLinkAnalysisVo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 招聘信息分析补全
|
||||
*/
|
||||
@ApiOperation("招聘信息分析补全(按照步骤生成)")
|
||||
@PostMapping(value = "/roleInfoAnalysisSetUp")
|
||||
public R<HotakeRolesInfoDto> handleRoleInfoAnalysisSetUp(@RequestBody HotakeRolesInfoDto rolesInfoDto)
|
||||
{
|
||||
return R.ok(hotakeAiCommonToolsService.getRoleInfoAnalysisSetUp(rolesInfoDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* AI面试问题生成
|
||||
*/
|
||||
@ApiOperation("AI面试问题生成")
|
||||
@PostMapping(value = "/aiInterviewQuestions")
|
||||
public R<?> handleAiInterviewQuestions(@RequestBody HotakeRolesInfo rolesInfo)
|
||||
{
|
||||
return R.ok(hotakeAiCommonToolsService.getAiInterviewQuestions(rolesInfo));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -155,6 +155,8 @@ elevenLabs:
|
||||
apiKey: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
|
||||
# apiKey: sk_88f5a560e1bbde0e5b8b6b6eb1812163a98bfb98554acbec
|
||||
modelId: eleven_turbo_v2_5
|
||||
agent-id: 9c5cb2f7ba9efb61d0f0eee01427b6e00c6abe92d4754cfb794884ac4d73c79d
|
||||
api-key: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
|
||||
|
||||
# 语音转文本
|
||||
whisper:
|
||||
|
||||
Reference in New Issue
Block a user