diff --git a/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketConfig.java b/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketConfig.java new file mode 100644 index 0000000..e2280d2 --- /dev/null +++ b/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketConfig.java @@ -0,0 +1,16 @@ +package com.vetti.socket; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration +public class ChatWebSocketConfig { + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} + diff --git a/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketHandler.java b/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketHandler.java new file mode 100644 index 0000000..648aec6 --- /dev/null +++ b/vetti-admin/src/main/java/com/vetti/socket/ChatWebSocketHandler.java @@ -0,0 +1,239 @@ +package com.vetti.socket; + +import com.vetti.common.ai.elevenLabs.ElevenLabsClient; +import com.vetti.common.ai.gpt.ChatGPTClient; +import com.vetti.common.ai.whisper.WhisperClient; +import com.vetti.common.config.RuoYiConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.websocket.*; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * 语音面试 web处理器 + * + */ +@Slf4j +@ServerEndpoint("/voice-websocket/{clientId}") +@Component +public class ChatWebSocketHandler { + + // 音频参数配置 - 根据实际发送的音频流参数调整 + private static final float SAMPLE_RATE = 16000.0f; // 采样率 + private static final int SAMPLE_SIZE_IN_BITS = 16; // 采样位数 + private static final int CHANNELS = 1; // 声道数 (1=单声道, 2=立体声) + private static final boolean SIGNED = true; // 是否有符号 + private static final boolean BIG_ENDIAN = false; // 字节序 + + // 语音文件保存目录 + private static final String VOICE_STORAGE_DIR = "/voice_files/"; + + // 语音结果文件保存目录 + private static final String VOICE_STORAGE_RESULT_DIR = "/voice_result_files/"; + + @Autowired + private ElevenLabsClient elevenLabsClient; + + @Autowired + private ChatGPTClient chatGPTClient; + + @Autowired + private WhisperClient whisperClient; + + + public ChatWebSocketHandler() { + // 初始化存储目录 + File dir = new File(RuoYiConfig.getProfile()+VOICE_STORAGE_DIR); + if (!dir.exists()) { + dir.mkdirs(); + } + + File resultDir = new File(RuoYiConfig.getProfile()+VOICE_STORAGE_RESULT_DIR); + if (!resultDir.exists()) { + resultDir.mkdirs(); + } + } + + // 连接建立时调用 + @OnOpen + public void onOpen(Session session, @PathParam("clientId") String clientId) { + log.info("WebSocket 链接已建立:{}",clientId); + //创建会话 + + } + + // 接收文本消息 + @OnMessage + public void onTextMessage(Session session, String message) { + System.out.println("接收到文本消息: " + message); + // 可以在这里处理文本流数据 + try { + // 发送响应 + session.getBasicRemote().sendText("已收到文本: " + message); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // 接收二进制消息(流数据) + @OnMessage + public void onBinaryMessage(Session session, @PathParam("clientId") String clientId, ByteBuffer byteBuffer) { + try{ + log.info("客户端ID为:{}",clientId); + // 处理二进制流数据 + byte[] data = new byte[byteBuffer.remaining()]; + // 生成唯一文件名 + String fileName = clientId + "_" + System.currentTimeMillis() + ".mp3"; + String pathUrl = RuoYiConfig.getProfile()+VOICE_STORAGE_DIR + fileName; + log.info("文件路径为:{}",pathUrl); + saveAsWebM(data,pathUrl); +// //拿到文件进行文字转换 +// String resultText = whisperClient.handleVoiceToText(pathUrl); +// //把提问的文字发送给CPT +// String resultMsg = chatGPTClient.handleAiChat(resultText); +// //把结果文字转成语音文件 +// //生成文件 +// // 生成唯一文件名 +// String resultFileName = clientId + "_" + System.currentTimeMillis() + ".webm"; +// String resultPathUrl = RuoYiConfig.getProfile()+VOICE_STORAGE_DIR + resultFileName; +// elevenLabsClient.handleTextToVoice(resultMsg,resultPathUrl); + //把语音文件转换成流,发送给前端 + System.out.println("接收到二进制数据,长度: " + data.length + " bytes"); + try { + //文件转换成文件流 +// ByteBuffer outByteBuffer = convertFileToByteBuffer(resultPathUrl); +// session.getBasicRemote().sendBinary(outByteBuffer); + // 发送响应确认 + session.getBasicRemote().sendText("已收到二进制数据,长度: " + data.length); + } catch (IOException e) { + e.printStackTrace(); + } + }catch (Exception e){ + e.printStackTrace(); + } + } + + // 连接关闭时调用 + @OnClose + public void onClose(Session session, CloseReason reason) { + System.out.println("WebSocket连接已关闭: " + session.getId() + ", 原因: " + reason.getReasonPhrase()); + } + + // 发生错误时调用 + @OnError + public void onError(Session session, Throwable throwable) { + System.err.println("WebSocket错误发生: " + throwable.getMessage()); + throwable.printStackTrace(); + } + + /** + * 将字节数组保存为WebM文件 + * @param byteData 包含WebM数据的字节数组 + * @param filePath 目标文件路径 + * @return 操作是否成功 + */ + private boolean saveAsWebM(byte[] byteData, String filePath) { + // 检查输入参数 + if (byteData == null || byteData.length == 0) { + System.err.println("字节数组为空,无法生成WebM文件"); + return false; + } + + if (filePath == null || filePath.trim().isEmpty()) { + System.err.println("文件路径不能为空"); + return false; + } + + // 确保文件以.webm结尾 + if (!filePath.toLowerCase().endsWith(".webm")) { + filePath += ".webm"; + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(filePath); + fos.write(byteData); + fos.flush(); + System.out.println("WebM文件已成功生成: " + filePath); + return true; + } catch (IOException e) { + System.err.println("写入文件时发生错误: " + e.getMessage()); + e.printStackTrace(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return false; + } + + /** + * 将接收的音频数据保存为WAV文件 + */ + private void saveAudioToFile(byte[] audioData,String pathUrl) { + if (audioData == null || audioData.length == 0) { + System.out.println("没有接收到音频数据,不保存文件"); + return; + } + try { + // 合并所有音频数据 + // 创建音频格式 + AudioFormat format = new AudioFormat( + SAMPLE_RATE, + SAMPLE_SIZE_IN_BITS, + CHANNELS, + SIGNED, + BIG_ENDIAN + ); + // 创建音频输入流 + ByteArrayInputStream bais = new ByteArrayInputStream(audioData); + AudioInputStream ais = new AudioInputStream(bais, format, audioData.length / format.getFrameSize()); + // 保存为WAV文件 + File outputFile = new File(pathUrl); + AudioSystem.write(ais, AudioFileFormat.Type.WAVE, outputFile); + System.out.println("音频文件保存成功: " + outputFile.getAbsolutePath()); + } catch (IOException e) { + System.err.println("保存音频文件失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * File 转换成 ByteBuffer + * @param fileUrl 文件路径 + * @return + */ + private ByteBuffer convertFileToByteBuffer(String fileUrl){ + File file = new File(fileUrl); + // 使用RandomAccessFile获取FileChannel + try (RandomAccessFile raf = new RandomAccessFile(file, "r"); + FileChannel channel = raf.getChannel()) { + // 分配与文件大小相同的ByteBuffer + ByteBuffer buffer = ByteBuffer.allocate((int) channel.size()); + // 从通道读取数据到缓冲区 + channel.read(buffer); + // 切换为读模式 + buffer.flip(); + return buffer; + }catch (Exception e){ + e.printStackTrace(); + } + return null; + } + +} + diff --git a/vetti-admin/src/main/java/com/vetti/socket/FileReceiverConfig.java b/vetti-admin/src/main/java/com/vetti/socket/FileReceiverConfig.java deleted file mode 100644 index a56220c..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/FileReceiverConfig.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.vetti.socket; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import org.springframework.web.socket.server.HandshakeInterceptor; - -import javax.servlet.http.HttpServletRequest; -import java.util.Map; - -@Configuration -@EnableWebSocket -public class FileReceiverConfig implements WebSocketConfigurer { - - @Bean - public FileReceiverWebSocketHandler fileReceiverWebSocketHandler() { - return new FileReceiverWebSocketHandler(); - } - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(fileReceiverWebSocketHandler(), "/voice-websocket111111") - .addInterceptors(handshakeInterceptor()) - .setAllowedOrigins("*"); // 生产环境需替换为具体域名 - } - - @Bean - public HandshakeInterceptor handshakeInterceptor() { - return new HandshakeInterceptor() { - @Override - public boolean beforeHandshake(org.springframework.http.server.ServerHttpRequest request, - org.springframework.http.server.ServerHttpResponse response, - org.springframework.web.socket.WebSocketHandler wsHandler, - Map attributes) throws Exception { - if (request instanceof org.springframework.http.server.ServletServerHttpRequest) { - HttpServletRequest servletRequest = - ((org.springframework.http.server.ServletServerHttpRequest) request).getServletRequest(); - String clientId = servletRequest.getParameter("clientId"); - if (clientId != null && !clientId.isEmpty()) { - attributes.put("clientId", clientId); - return true; - } - } - return false; - } - - @Override - public void afterHandshake(org.springframework.http.server.ServerHttpRequest request, - org.springframework.http.server.ServerHttpResponse response, - org.springframework.web.socket.WebSocketHandler wsHandler, - Exception exception) { - // 握手后处理 - } - }; - } -} diff --git a/vetti-admin/src/main/java/com/vetti/socket/FileReceiverWebSocketHandler.java b/vetti-admin/src/main/java/com/vetti/socket/FileReceiverWebSocketHandler.java deleted file mode 100644 index 65d307b..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/FileReceiverWebSocketHandler.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.vetti.socket; - -import com.vetti.socket.vo.FileMetadata; -import com.vetti.socket.vo.FileTransferState; -import org.springframework.web.socket.*; -import org.springframework.web.socket.handler.TextWebSocketHandler; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.*; -import java.nio.ByteBuffer; -import java.nio.file.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -public class FileReceiverWebSocketHandler extends TextWebSocketHandler { - // 存储客户端的文件传输状态:clientId -> FileTransferState - private final Map transferStates = new ConcurrentHashMap<>(); - private final ObjectMapper objectMapper = new ObjectMapper(); - private static final String STORAGE_DIR = "received_files/"; - - // 初始化存储目录 - static { - try { - Files.createDirectories(Paths.get(STORAGE_DIR)); - } catch (IOException e) { - throw new RuntimeException("无法创建文件存储目录", e); - } - } - - @Override - public void afterConnectionEstablished(WebSocketSession session) throws Exception { - String clientId = (String) session.getAttributes().get("clientId"); - transferStates.put(clientId, new FileTransferState()); - System.out.println("客户端连接: " + clientId); - } - - // 处理文本消息(文件元数据) - @Override - protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { - String clientId = (String) session.getAttributes().get("clientId"); - FileMetadata metadata = objectMapper.readValue(message.getPayload(), FileMetadata.class); - - // 初始化文件传输状态 - FileTransferState state = transferStates.get(clientId); - state.setFileName(metadata.getFileName()); - state.setTotalSize(metadata.getTotalSize()); - state.setTotalParts(metadata.getTotalParts()); - state.setOutputStream(new FileOutputStream(STORAGE_DIR + metadata.getFileName())); - - System.out.println("开始接收文件: " + metadata.getFileName() + " (" + metadata.getTotalParts() + "个分片)"); - - // 确认已收到元数据 - session.sendMessage(new TextMessage("{\"type\":\"metadata_ack\"}")); - } - - // 处理二进制消息(文件分片) - @Override - protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message){ - try{ - System.out.println("开始-接收文件分片数据流"); - String clientId = (String) session.getAttributes().get("clientId"); - FileTransferState state = transferStates.get(clientId); - - if (state == null || state.getOutputStream() == null) { - session.sendMessage(new TextMessage("{\"type\":\"error\", \"message\":\"未收到文件元数据\"}")); - return; - } - System.out.println("进行中-接收文件分片数据流"); - // 解析分片数据 - ByteBuffer payload = message.getPayload(); - // int partNumber = payload.getInt(); // 前4字节是分片编号 - byte[] data = new byte[payload.remaining()]; - payload.get(data); - - // 写入文件 - state.getOutputStream().write(data); - state.incrementReceivedParts(); - - // 发送进度更新(每5个分片或最后一个分片) - if (state.getReceivedParts() % 5 == 0 || state.getReceivedParts() == state.getTotalParts()) { - // 检查是否接收完成 - if (state.getReceivedParts() == state.getTotalParts()) { - System.out.println("生成完整的文件-接收文件分片数据流"); - completeFileTransfer(session, state, clientId); - //进行文件数据转换 - - //获取最终的文件结果 - - //把文件转成对应的文件流,返回给前端 -// session.sendMessage(new BinaryMessage()); - } - } - }catch (Exception e){ - e.printStackTrace(); - } - } - - // 完成文件传输 - private void completeFileTransfer(WebSocketSession session, FileTransferState state, String clientId) throws IOException { - // 关闭文件输出流 - state.getOutputStream().close(); - - // 验证文件大小 - File file = new File(STORAGE_DIR + state.getFileName()); - boolean fileValid = file.length() == state.getTotalSize(); - - // 发送完成消息 - String result = fileValid ? - "{\"type\":\"complete\", \"message\":\"文件接收完成\", \"filePath\":\"" + file.getAbsolutePath() + "\"}" : - "{\"type\":\"error\", \"message\":\"文件损坏,大小不匹配\"}"; - session.sendMessage(new TextMessage(result)); - - System.out.println("文件接收" + (fileValid ? "完成" : "失败") + ": " + state.getFileName()); - - // 清理状态 - transferStates.remove(clientId); - } - - @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { - String clientId = (String) session.getAttributes().get("clientId"); - FileTransferState state = transferStates.remove(clientId); - - // 关闭可能存在的文件流 - if (state != null && state.getOutputStream() != null) { - try { - state.getOutputStream().close(); - // 删除未完成的文件 - Files.deleteIfExists(Paths.get(STORAGE_DIR + state.getFileName())); - } catch (IOException e) { - e.printStackTrace(); - } - } - System.out.println("客户端断开连接: " + clientId); - } - - @Override - public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { - System.err.println("传输错误: " + exception.getMessage()); - session.close(CloseStatus.SERVER_ERROR); - } -} diff --git a/vetti-admin/src/main/java/com/vetti/socket/LargeMessageWebSocketConfigurator.java b/vetti-admin/src/main/java/com/vetti/socket/LargeMessageWebSocketConfigurator.java new file mode 100644 index 0000000..5de8a28 --- /dev/null +++ b/vetti-admin/src/main/java/com/vetti/socket/LargeMessageWebSocketConfigurator.java @@ -0,0 +1,19 @@ +package com.vetti.socket; + +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; + +import javax.websocket.server.ServerEndpointConfig; + +@Component +public class LargeMessageWebSocketConfigurator extends ServerEndpointConfig.Configurator { + + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + container.setMaxTextMessageBufferSize(1048576); // 文本消息缓冲区 + container.setMaxBinaryMessageBufferSize(10485760); // 二进制消息缓冲区 + return container; + } +} diff --git a/vetti-admin/src/main/java/com/vetti/socket/VoiceHandshakeInterceptor.java b/vetti-admin/src/main/java/com/vetti/socket/VoiceHandshakeInterceptor.java deleted file mode 100644 index d2ba28e..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/VoiceHandshakeInterceptor.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vetti.socket; - -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.server.HandshakeInterceptor; - -import java.util.Map; - -@Component -public class VoiceHandshakeInterceptor implements HandshakeInterceptor { - - @Override - public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, - WebSocketHandler wsHandler, Map attributes) throws Exception { - // 从请求参数中获取客户端ID - if (request instanceof ServletServerHttpRequest) { - ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; - String clientId = servletRequest.getServletRequest().getParameter("clientId"); - if (clientId != null && !clientId.isEmpty()) { - attributes.put("clientId", clientId); - System.out.println("客户端连接: " + clientId); - } - } - return true; - } - - @Override - public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, - WebSocketHandler wsHandler, Exception exception) { - // 握手后操作,可留空 - } -} - diff --git a/vetti-admin/src/main/java/com/vetti/socket/VoiceWebSocketHandler.java b/vetti-admin/src/main/java/com/vetti/socket/VoiceWebSocketHandler.java deleted file mode 100644 index 58a982c..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/VoiceWebSocketHandler.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.vetti.socket; - -import cn.hutool.json.JSONUtil; -import com.vetti.socket.vo.FileTransferState; -import com.vetti.socket.vo.VoicePartMessage; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.BinaryMessage; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.handler.TextWebSocketHandler; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.*; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; -import java.util.Base64; - -@Slf4j -@Component -public class VoiceWebSocketHandler extends TextWebSocketHandler { - - // 存储每个客户端的语音分片,key: clientId, value: 分片映射 - private final Map> clientVoiceParts = new ConcurrentHashMap<>(); - // 存储每个客户端的总分片数,key: clientId - private final Map clientTotalParts = new ConcurrentHashMap<>(); - // 用于并发控制的锁 - private final Map clientLocks = new ConcurrentHashMap<>(); - // JSON序列化工具 - private final ObjectMapper objectMapper = new ObjectMapper(); - // 语音文件保存目录 - private static final String VOICE_STORAGE_DIR = "voice_files/"; - - public VoiceWebSocketHandler() { - // 初始化存储目录 - File dir = new File(VOICE_STORAGE_DIR); - if (!dir.exists()) { - dir.mkdirs(); - } - } - - @Override - public void afterConnectionEstablished(WebSocketSession session) throws Exception { - String clientId = getClientId(session); - if (clientId != null) { - // 初始化客户端数据结构 - clientVoiceParts.put(clientId, new TreeMap<>()); // TreeMap保证分片有序 - clientLocks.putIfAbsent(clientId, new ReentrantLock()); - System.out.println("客户端连接建立: " + clientId); - } - } - - @Override - protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { - log.info("开始进入文本传输里面了"); - String clientId = getClientId(session); - if (clientId == null) { - System.err.println("无法获取客户端ID"); - return; - } - - try { - // 解析前端发送的JSON消息 - VoicePartMessage voiceMessage = objectMapper.readValue(message.getPayload(), VoicePartMessage.class); - - // 处理语音分片 - if ("voice_part".equals(voiceMessage.getType())) { - processVoicePart(clientId, voiceMessage, session); - } - } catch (Exception e) { - System.err.println("处理消息出错: " + e.getMessage()); - e.printStackTrace(); - } - } - - // 处理二进制消息(文件分片) - @Override - protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message){ - log.info("开始进入文件流传输里面了"); - log.info("获取的数据为:{}", JSONUtil.toJsonStr(message)); - } - - @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { - String clientId = getClientId(session); - if (clientId != null) { - // 清理客户端资源 - clientVoiceParts.remove(clientId); - clientTotalParts.remove(clientId); - clientLocks.remove(clientId); - System.out.println("客户端连接关闭: " + clientId); - } - } - - /** - * 处理语音分片 - */ - private void processVoicePart(String clientId, VoicePartMessage message, WebSocketSession session) throws Exception { - ReentrantLock lock = clientLocks.get(clientId); - lock.lock(); // 加锁确保线程安全 - try { - // 保存总分片数 - clientTotalParts.put(clientId, message.getTotalParts()); - - // 解码Base64数据并存储分片 - byte[] voiceData = Base64.getDecoder().decode(message.getData()); - clientVoiceParts.get(clientId).put(message.getPartNumber(), voiceData); - - System.out.printf("接收客户端 %s 的分片 %d/%d%n", - clientId, message.getPartNumber() + 1, message.getTotalParts()); - - // 检查是否所有分片都已接收 - checkAndMergeParts(clientId, session); - } finally { - lock.unlock(); // 释放锁 - } - } - - /** - * 检查是否所有分片都已接收,如果是则合并 - */ - private void checkAndMergeParts(String clientId, WebSocketSession session) throws Exception { - Map parts = clientVoiceParts.get(clientId); - Integer totalParts = clientTotalParts.get(clientId); - - if (parts == null || totalParts == null) { - return; - } - - // 所有分片都已接收 - if (parts.size() == totalParts) { - System.out.println("所有分片接收完成,开始合并: " + clientId); - - // 生成唯一文件名 - String fileName = clientId + "_" + System.currentTimeMillis() + ".wav"; - Path outputPath = Paths.get(VOICE_STORAGE_DIR + fileName); - - // 合并分片 - try (FileOutputStream fos = new FileOutputStream(outputPath.toFile())) { - for (byte[] part : parts.values()) { - fos.write(part); - } - } - - System.out.println("语音文件合并完成,保存路径: " + outputPath); - - // 向客户端发送处理完成消息 - Map response = new HashMap<>(); - response.put("type", "complete"); - response.put("message", "语音接收完成"); - response.put("fileName", fileName); - session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response))); - - // 清理已合并的分片数据 - clientVoiceParts.get(clientId).clear(); - } - } - - /** - * 从会话中获取客户端ID - */ - private String getClientId(WebSocketSession session) { - return (String) session.getAttributes().get("clientId"); - } -} diff --git a/vetti-admin/src/main/java/com/vetti/socket/WebSocketConfig.java b/vetti-admin/src/main/java/com/vetti/socket/WebSocketConfig.java deleted file mode 100644 index 0b52f1c..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/WebSocketConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.vetti.socket; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; - - -@Configuration -@EnableWebSocket -public class WebSocketConfig implements WebSocketConfigurer { - - private final VoiceWebSocketHandler voiceWebSocketHandler; - - private final VoiceHandshakeInterceptor voiceHandshakeInterceptor; - - // 构造函数注入 - public WebSocketConfig(VoiceWebSocketHandler voiceWebSocketHandler, - VoiceHandshakeInterceptor interceptor) { - this.voiceWebSocketHandler = voiceWebSocketHandler; - this.voiceHandshakeInterceptor = interceptor; - } - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - // 注册WebSocket处理器,设置路径和允许跨域 - registry.addHandler(voiceWebSocketHandler, "/voice-websocket") - .addInterceptors(voiceHandshakeInterceptor) - .setAllowedOrigins("*"); // 生产环境应指定具体域名而非* - } -} diff --git a/vetti-admin/src/main/java/com/vetti/socket/vo/FileMetadata.java b/vetti-admin/src/main/java/com/vetti/socket/vo/FileMetadata.java deleted file mode 100644 index 4b50e69..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/vo/FileMetadata.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.vetti.socket.vo; - -/** - * 文件元数据类 - */ -public class FileMetadata { - - private String fileName; - private long totalSize; - private int totalParts; - - // getter和setter方法 - public String getFileName() { return fileName; } - public void setFileName(String fileName) { this.fileName = fileName; } - public long getTotalSize() { return totalSize; } - public void setTotalSize(long totalSize) { this.totalSize = totalSize; } - public int getTotalParts() { return totalParts; } - public void setTotalParts(int totalParts) { this.totalParts = totalParts; } - -} diff --git a/vetti-admin/src/main/java/com/vetti/socket/vo/FileTransferState.java b/vetti-admin/src/main/java/com/vetti/socket/vo/FileTransferState.java deleted file mode 100644 index 8555af4..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/vo/FileTransferState.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.vetti.socket.vo; - -import java.io.FileOutputStream; - -/** - * 文件传输状态类 - */ -public class FileTransferState { - - private String fileName; - private long totalSize; - private int totalParts; - private int receivedParts = 0; - private FileOutputStream outputStream; - - // getter和setter方法 - public String getFileName() { return fileName; } - public void setFileName(String fileName) { this.fileName = fileName; } - public long getTotalSize() { return totalSize; } - public void setTotalSize(long totalSize) { this.totalSize = totalSize; } - public int getTotalParts() { return totalParts; } - public void setTotalParts(int totalParts) { this.totalParts = totalParts; } - public int getReceivedParts() { return receivedParts; } - public void incrementReceivedParts() { this.receivedParts++; } - public FileOutputStream getOutputStream() { return outputStream; } - public void setOutputStream(FileOutputStream outputStream) { this.outputStream = outputStream; } -} diff --git a/vetti-admin/src/main/java/com/vetti/socket/vo/VoicePartMessage.java b/vetti-admin/src/main/java/com/vetti/socket/vo/VoicePartMessage.java deleted file mode 100644 index 47da4c1..0000000 --- a/vetti-admin/src/main/java/com/vetti/socket/vo/VoicePartMessage.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.vetti.socket.vo; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * 语音分片消息实体类,对应前端发送的JSON结构 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class VoicePartMessage { - private String type; // 消息类型,如"voice_part" - private int partNumber; // 分片编号,从0开始 - private int totalParts; // 总分片数 - private String data; // Base64编码的分片数据 - - // getter和setter - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public int getPartNumber() { - return partNumber; - } - - public void setPartNumber(int partNumber) { - this.partNumber = partNumber; - } - - public int getTotalParts() { - return totalParts; - } - - public void setTotalParts(int totalParts) { - this.totalParts = totalParts; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } -} diff --git a/vetti-admin/src/main/java/com/vetti/web/controller/ai/AiCommonController.java b/vetti-admin/src/main/java/com/vetti/web/controller/ai/AiCommonController.java index 53a1069..baaff39 100644 --- a/vetti-admin/src/main/java/com/vetti/web/controller/ai/AiCommonController.java +++ b/vetti-admin/src/main/java/com/vetti/web/controller/ai/AiCommonController.java @@ -25,7 +25,6 @@ public class AiCommonController extends BaseController @Autowired private ElevenLabsClient elevenLabsClient; - @Autowired private ChatGPTClient chatGPTClient; diff --git a/vetti-admin/src/main/resources/application.yml b/vetti-admin/src/main/resources/application.yml index 7f2213d..92c58d2 100644 --- a/vetti-admin/src/main/resources/application.yml +++ b/vetti-admin/src/main/resources/application.yml @@ -75,6 +75,6 @@ xss: # 过滤开关 enabled: true # 排除链接(多个用逗号分隔) - excludes: /system/notice + excludes: /system/notice,/voice-websocket/* # 匹配链接 - urlPatterns: /system/*,/monitor/*,/tool/* + urlPatterns: /system/*,/monitor/*,/tool/*,/voice-websocket/* diff --git a/vetti-admin/target/classes/application.yml b/vetti-admin/target/classes/application.yml index 7f2213d..92c58d2 100644 --- a/vetti-admin/target/classes/application.yml +++ b/vetti-admin/target/classes/application.yml @@ -75,6 +75,6 @@ xss: # 过滤开关 enabled: true # 排除链接(多个用逗号分隔) - excludes: /system/notice + excludes: /system/notice,/voice-websocket/* # 匹配链接 - urlPatterns: /system/*,/monitor/*,/tool/* + urlPatterns: /system/*,/monitor/*,/tool/*,/voice-websocket/* diff --git a/vetti-framework/src/main/java/com/vetti/framework/config/SecurityConfig.java b/vetti-framework/src/main/java/com/vetti/framework/config/SecurityConfig.java index a2d100b..eaa7950 100644 --- a/vetti-framework/src/main/java/com/vetti/framework/config/SecurityConfig.java +++ b/vetti-framework/src/main/java/com/vetti/framework/config/SecurityConfig.java @@ -111,7 +111,7 @@ public class SecurityConfig .authorizeHttpRequests((requests) -> { permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - requests.antMatchers("/login", "/register", "/captchaImage","/aiCommon/**","/voice-websocket").permitAll() + requests.antMatchers("/login", "/register", "/captchaImage","/aiCommon/**","/voice-websocket/**").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()