mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 01:53:53 +08:00
Merge pull request #1097 from xinnan-tech/chat-history-ui
Chat history UI
This commit is contained in:
@@ -5,6 +5,9 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
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.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
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.dto.AgentUpdateDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
@@ -50,6 +54,7 @@ public class AgentController {
|
|||||||
private final AgentTemplateService agentTemplateService;
|
private final AgentTemplateService agentTemplateService;
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
private final AgentChatHistoryService agentChatHistoryService;
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@Operation(summary = "获取用户智能体列表")
|
@Operation(summary = "获取用户智能体列表")
|
||||||
@@ -229,4 +234,18 @@ public class AgentController {
|
|||||||
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||||
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/audio/{audioId}")
|
||||||
|
@Operation(summary = "下载音频")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public ResponseEntity<byte[]> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+8
@@ -19,4 +19,12 @@ public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
|
|||||||
* @return 音频ID
|
* @return 音频ID
|
||||||
*/
|
*/
|
||||||
String saveAudio(byte[] audioData);
|
String saveAudio(byte[] audioData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音频数据
|
||||||
|
*
|
||||||
|
* @param audioId 音频ID
|
||||||
|
* @return 音频数据
|
||||||
|
*/
|
||||||
|
byte[] getAudio(String audioId);
|
||||||
}
|
}
|
||||||
+6
@@ -25,4 +25,10 @@ public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao,
|
|||||||
save(entity);
|
save(entity);
|
||||||
return entity.getId();
|
return entity.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getAudio(String audioId) {
|
||||||
|
AgentChatAudioEntity entity = getById(audioId);
|
||||||
|
return entity != null ? entity.getAudio() : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package xiaozhi.modules.security.config;
|
package xiaozhi.modules.security.config;
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
import org.apache.shiro.mgt.SecurityManager;
|
||||||
import org.apache.shiro.session.mgt.SessionManager;
|
import org.apache.shiro.session.mgt.SessionManager;
|
||||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||||
@@ -11,15 +14,13 @@ import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
|||||||
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
|
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import jakarta.servlet.Filter;
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||||
import xiaozhi.modules.security.secret.ServerSecretFilter;
|
import xiaozhi.modules.security.secret.ServerSecretFilter;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shiro的配置文件
|
* Shiro的配置文件
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.util.regex.Pattern;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||||
import org.springframework.web.socket.client.WebSocketClient;
|
import org.springframework.web.socket.client.WebSocketClient;
|
||||||
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||||
|
|
||||||
@@ -45,8 +46,9 @@ public class WebSocketValidator {
|
|||||||
try {
|
try {
|
||||||
WebSocketClient client = new StandardWebSocketClient();
|
WebSocketClient client = new StandardWebSocketClient();
|
||||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
CompletableFuture<Boolean> 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秒获取连接结果
|
// 等待最多5秒获取连接结果
|
||||||
return future.get(5, TimeUnit.SECONDS);
|
return future.get(5, TimeUnit.SECONDS);
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -5,7 +5,7 @@
|
|||||||
<div class="session-list" @scroll="handleScroll">
|
<div class="session-list" @scroll="handleScroll">
|
||||||
<div v-for="session in sessions" :key="session.sessionId" class="session-item"
|
<div v-for="session in sessions" :key="session.sessionId" class="session-item"
|
||||||
:class="{ active: currentSessionId === session.sessionId }" @click="selectSession(session)">
|
:class="{ active: currentSessionId === session.sessionId }" @click="selectSession(session)">
|
||||||
<img src="@/assets/xiaozhi-logo.png" class="avatar" />
|
<img :src="getUserAvatar(session.sessionId)" class="avatar" />
|
||||||
<div class="session-info">
|
<div class="session-info">
|
||||||
<div class="session-time">{{ formatTime(session.createdAt) }}</div>
|
<div class="session-time">{{ formatTime(session.createdAt) }}</div>
|
||||||
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
|
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
|
||||||
@@ -21,9 +21,13 @@
|
|||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="message-item" :class="{ 'user-message': message.chatType === 1 }">
|
<div v-else class="message-item" :class="{ 'user-message': message.chatType === 1 }">
|
||||||
<img :src="message.chatType === 1 ? require('@/assets/user-avatar.png') : require('@/assets/xiaozhi-logo.png')"
|
<img :src="message.chatType === 1 ? getUserAvatar(currentSessionId) : require('@/assets/xiaozhi-logo.png')"
|
||||||
class="avatar" />
|
class="avatar" />
|
||||||
<div class="message-content">{{ message.content }}</div>
|
<div class="message-content">
|
||||||
|
{{ message.content }}
|
||||||
|
<i v-if="message.audioId" :class="getAudioIconClass(message)"
|
||||||
|
@click="playAudio(message)" class="audio-icon"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +70,9 @@ export default {
|
|||||||
loading: false,
|
loading: false,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
scrollTimer: null,
|
scrollTimer: null,
|
||||||
isFirstLoad: true
|
isFirstLoad: true,
|
||||||
|
playingAudioId: null,
|
||||||
|
audioElement: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -207,6 +213,54 @@ export default {
|
|||||||
const day = date.getDate().toString().padStart(2, '0');
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
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 {
|
.message-count {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #1890ff;
|
background-color: #b4b4b4;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
float: left;
|
float: left;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -302,11 +356,26 @@ export default {
|
|||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
line-height: 20px;
|
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 {
|
.user-message .message-content {
|
||||||
background-color: #1890ff;
|
background-color: #1890ff;
|
||||||
color: white;
|
color: white;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .audio-icon {
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading,
|
.loading,
|
||||||
@@ -355,6 +424,13 @@ export default {
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
max-width: 85vw;
|
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 {
|
.chat-history-dialog .el-dialog__body {
|
||||||
|
|||||||
Reference in New Issue
Block a user