diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index 7b717a3f..cfbb107f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -5,6 +5,9 @@ import java.util.List; import java.util.Map; import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -35,6 +38,7 @@ import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity; +import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; @@ -50,6 +54,7 @@ public class AgentController { private final AgentTemplateService agentTemplateService; private final DeviceService deviceService; private final AgentChatHistoryService agentChatHistoryService; + private final AgentChatAudioService agentChatAudioService; @GetMapping("/list") @Operation(summary = "获取用户智能体列表") @@ -229,4 +234,18 @@ public class AgentController { List result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId); return new Result>().ok(result); } + + @GetMapping("/audio/{audioId}") + @Operation(summary = "下载音频") + @RequiresPermissions("sys:role:normal") + public ResponseEntity downloadAudio(@PathVariable("audioId") String audioId) { + byte[] audioData = agentChatAudioService.getAudio(audioId); + if (audioData == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"play.wav\"") + .body(audioData); + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java index fdd0e1f6..e284b69f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java @@ -19,4 +19,12 @@ public interface AgentChatAudioService extends IService { * @return 音频ID */ String saveAudio(byte[] audioData); + + /** + * 获取音频数据 + * + * @param audioId 音频ID + * @return 音频数据 + */ + byte[] getAudio(String audioId); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java index c222abe4..cf804678 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java @@ -25,4 +25,10 @@ public class AgentChatAudioServiceImpl extends ServiceImpl future = new CompletableFuture<>(); + WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); - client.doHandshake(new WebSocketTestHandler(future), null, URI.create(url)); + client.execute(new WebSocketTestHandler(future), headers, URI.create(url)); // 等待最多5秒获取连接结果 return future.get(5, TimeUnit.SECONDS); diff --git a/main/manager-web/src/assets/user-avatar1.png b/main/manager-web/src/assets/user-avatar1.png new file mode 100644 index 00000000..d835d2f7 Binary files /dev/null and b/main/manager-web/src/assets/user-avatar1.png differ diff --git a/main/manager-web/src/assets/user-avatar2.png b/main/manager-web/src/assets/user-avatar2.png new file mode 100644 index 00000000..71d5fd1b Binary files /dev/null and b/main/manager-web/src/assets/user-avatar2.png differ diff --git a/main/manager-web/src/assets/user-avatar3.png b/main/manager-web/src/assets/user-avatar3.png new file mode 100644 index 00000000..159e6886 Binary files /dev/null and b/main/manager-web/src/assets/user-avatar3.png differ diff --git a/main/manager-web/src/assets/user-avatar4.png b/main/manager-web/src/assets/user-avatar4.png new file mode 100644 index 00000000..0d078144 Binary files /dev/null and b/main/manager-web/src/assets/user-avatar4.png differ diff --git a/main/manager-web/src/assets/user-avatar.png b/main/manager-web/src/assets/user-avatar5.png similarity index 100% rename from main/manager-web/src/assets/user-avatar.png rename to main/manager-web/src/assets/user-avatar5.png diff --git a/main/manager-web/src/components/ChatHistoryDialog.vue b/main/manager-web/src/components/ChatHistoryDialog.vue index 646164b9..4c5421cd 100644 --- a/main/manager-web/src/components/ChatHistoryDialog.vue +++ b/main/manager-web/src/components/ChatHistoryDialog.vue @@ -5,7 +5,7 @@
- +
{{ formatTime(session.createdAt) }}
{{ session.chatCount > 99 ? '99' : session.chatCount }}
@@ -21,9 +21,13 @@ {{ message.content }}
- -
{{ message.content }}
+
+ {{ message.content }} + +
@@ -66,7 +70,9 @@ export default { loading: false, hasMore: true, scrollTimer: null, - isFirstLoad: true + isFirstLoad: true, + playingAudioId: null, + audioElement: null }; }, watch: { @@ -207,6 +213,54 @@ export default { const day = date.getDate().toString().padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } + }, + getAudioIconClass(message) { + if (this.playingAudioId === message.audioId) { + return 'el-icon-loading'; + } + return 'el-icon-video-play'; + }, + playAudio(message) { + if (this.playingAudioId === message.audioId) { + // 如果正在播放当前音频,则停止播放 + if (this.audioElement) { + this.audioElement.pause(); + this.audioElement = null; + } + this.playingAudioId = null; + return; + } + + // 停止当前正在播放的音频 + if (this.audioElement) { + this.audioElement.pause(); + this.audioElement = null; + } + + // 播放新音频 + this.playingAudioId = message.audioId; + this.audioElement = new Audio(Api.getServiceUrl() + `/agent/audio/${message.audioId}`); + + this.audioElement.onended = () => { + this.playingAudioId = null; + this.audioElement = null; + }; + + this.audioElement.play(); + }, + getUserAvatar(sessionId) { + // 从 sessionId 中提取所有数字 + const numbers = sessionId.match(/\d+/g); + if (!numbers) return require('@/assets/user-avatar1.png'); + + // 将所有数字相加 + const sum = numbers.reduce((acc, num) => acc + parseInt(num), 0); + + // 计算模5并加1,得到1-5之间的数字 + const avatarIndex = (sum % 5) + 1; + + // 返回对应的头像图片 + return require(`@/assets/user-avatar${avatarIndex}.png`); } } }; @@ -269,7 +323,7 @@ export default { .message-count { font-size: 14px; color: #fff; - background-color: #1890ff; + background-color: #b4b4b4; border-radius: 20px; float: left; width: 20px; @@ -302,11 +356,26 @@ export default { margin: 0 10px; text-align: left; line-height: 20px; + position: relative; + display: flex; + align-items: center; +} + +.audio-icon { + font-size: 20px; + cursor: pointer; + margin: 0 5px; + color: #1890ff; } .user-message .message-content { background-color: #1890ff; color: white; + flex-direction: row-reverse; +} + +.user-message .audio-icon { + color: white; } .loading, @@ -355,6 +424,13 @@ export default { transform: translate(-50%, -50%); height: 80vh; max-width: 85vw; + border-radius: 12px; + overflow: hidden; +} + +.chat-history-dialog .el-dialog__header { + background-color: #e6f7ff; + padding: 15px 20px; } .chat-history-dialog .el-dialog__body {