业务逻辑修改以及完善

This commit is contained in:
2026-01-11 23:31:20 +08:00
parent 6edf7a4958
commit 85294a917a
17 changed files with 1014 additions and 378 deletions

View File

@@ -91,6 +91,12 @@
<artifactId>concentus</artifactId>
</dependency>
<!-- WebSocket Client -->
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-client</artifactId>
</dependency>
</dependencies>

View File

@@ -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();
}
}

View File

@@ -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) {}
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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("*");
}
}

View File

@@ -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));
}
}

View File

@@ -155,6 +155,8 @@ elevenLabs:
apiKey: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
# apiKey: sk_88f5a560e1bbde0e5b8b6b6eb1812163a98bfb98554acbec
modelId: eleven_turbo_v2_5
agent-id: 9c5cb2f7ba9efb61d0f0eee01427b6e00c6abe92d4754cfb794884ac4d73c79d
api-key: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
# 语音转文本
whisper:

View File

@@ -155,6 +155,8 @@ elevenLabs:
apiKey: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
# apiKey: sk_88f5a560e1bbde0e5b8b6b6eb1812163a98bfb98554acbec
modelId: eleven_turbo_v2_5
agent-id: 9c5cb2f7ba9efb61d0f0eee01427b6e00c6abe92d4754cfb794884ac4d73c79d
api-key: sk_dfe2b45e19bf8ad93a71d3a0faa61619a91e817df549d116
# 语音转文本
whisper: