mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23:23:55 +08:00
Merge branch 'main' into py_test_tts
# Conflicts: # main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
This commit is contained in:
@@ -116,6 +116,11 @@ public interface Constant {
|
||||
*/
|
||||
String SERVER_MCP_ENDPOINT = "server.mcp_endpoint";
|
||||
|
||||
/**
|
||||
* mcp接入点路径
|
||||
*/
|
||||
String SERVER_VOICE_PRINT = "server.voice_print";
|
||||
|
||||
/**
|
||||
* 无记忆
|
||||
*/
|
||||
@@ -232,7 +237,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.6.3";
|
||||
public static final String VERSION = "0.7.1";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package xiaozhi.modules.agent.Enums;
|
||||
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录类型
|
||||
*/
|
||||
@Getter
|
||||
public enum AgentChatHistoryType {
|
||||
|
||||
USER((byte) 1),
|
||||
AGENT((byte) 2);
|
||||
|
||||
private final byte value;
|
||||
|
||||
AgentChatHistoryType(byte i) {
|
||||
this.value = i;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
@@ -181,6 +182,33 @@ public class AgentController {
|
||||
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||
}
|
||||
@GetMapping("/{id}/chat-history/user")
|
||||
@Operation(summary = "获取智能体聊天记录(用户)")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentChatHistoryUserVO>> getRecentlyFiftyByAgentId(
|
||||
@PathVariable("id") String id) {
|
||||
// 获取当前用户
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
// 检查权限
|
||||
if (!agentService.checkAgentPermission(id, user.getId())) {
|
||||
return new Result<List<AgentChatHistoryUserVO>>().error("没有权限查看该智能体的聊天记录");
|
||||
}
|
||||
|
||||
// 查询聊天记录
|
||||
List<AgentChatHistoryUserVO> data = agentChatHistoryService.getRecentlyFiftyByAgentId(id);
|
||||
return new Result<List<AgentChatHistoryUserVO>>().ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/chat-history/audio")
|
||||
@Operation(summary = "获取音频内容")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> getContentByAudioId(
|
||||
@PathVariable("id") String id) {
|
||||
// 查询聊天记录
|
||||
String data = agentChatHistoryService.getContentByAudioId(id);
|
||||
return new Result<String>().ok(data);
|
||||
}
|
||||
|
||||
@PostMapping("/audio/{audioId}")
|
||||
@Operation(summary = "获取音频下载ID")
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package xiaozhi.modules.agent.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentVoicePrintUpdateDTO;
|
||||
import xiaozhi.modules.agent.service.AgentVoicePrintService;
|
||||
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Tag(name = "智能体声纹管理")
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/agent/voice-print")
|
||||
public class AgentVoicePrintController {
|
||||
private final AgentVoicePrintService agentVoicePrintService;
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建智能体的声纹")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> save(@RequestBody @Valid AgentVoicePrintSaveDTO dto) {
|
||||
boolean b = agentVoicePrintService.insert(dto);
|
||||
if (b) {
|
||||
return new Result<>();
|
||||
}
|
||||
return new Result<Void>().error("智能体的声纹创建失败");
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "更新智能体的对应声纹")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> update(@RequestBody @Valid AgentVoicePrintUpdateDTO dto) {
|
||||
Long userId = SecurityUser.getUserId();
|
||||
boolean b = agentVoicePrintService.update(userId, dto);
|
||||
if (b) {
|
||||
return new Result<>();
|
||||
}
|
||||
return new Result<Void>().error("智能体的对应声纹更新失败");
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除智能体对应声纹")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
Long userId = SecurityUser.getUserId();
|
||||
// 先删除关联的设备
|
||||
boolean delete = agentVoicePrintService.delete(userId, id);
|
||||
if (delete) {
|
||||
return new Result<>();
|
||||
}
|
||||
return new Result<Void>().error("智能体的对应声纹删除失败");
|
||||
}
|
||||
|
||||
@GetMapping("/list/{id}")
|
||||
@Operation(summary = "获取用户指定智能体声纹列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentVoicePrintVO>> list(@PathVariable String id) {
|
||||
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
|
||||
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
|
||||
throw new RenException("声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)");
|
||||
}
|
||||
Long userId = SecurityUser.getUserId();
|
||||
List<AgentVoicePrintVO> list = agentVoicePrintService.list(userId, id);
|
||||
return new Result<List<AgentVoicePrintVO>>().ok(list);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
||||
|
||||
/**
|
||||
* {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface AgentVoicePrintDao extends BaseMapper<AgentVoicePrintEntity> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 保存智能体声纹的dto
|
||||
*
|
||||
* @author zjy
|
||||
*/
|
||||
@Data
|
||||
public class AgentVoicePrintSaveDTO {
|
||||
/**
|
||||
* 关联的智能体id
|
||||
*/
|
||||
private String agentId;
|
||||
/**
|
||||
* 音频文件id
|
||||
*/
|
||||
private String audioId;
|
||||
/**
|
||||
* 声纹来源的人姓名
|
||||
*/
|
||||
private String sourceName;
|
||||
/**
|
||||
* 描述声纹来源的人
|
||||
*/
|
||||
private String introduce;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 修改智能体声纹的dto
|
||||
*
|
||||
* @author zjy
|
||||
*/
|
||||
@Data
|
||||
public class AgentVoicePrintUpdateDTO {
|
||||
/**
|
||||
* 智能体声纹id
|
||||
*/
|
||||
private String id;
|
||||
/**
|
||||
* 音频文件id
|
||||
*/
|
||||
private String audioId;
|
||||
/**
|
||||
* 声纹来源的人姓名
|
||||
*/
|
||||
private String sourceName;
|
||||
/**
|
||||
* 描述声纹来源的人
|
||||
*/
|
||||
private String introduce;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 声纹识别接口返回的对象
|
||||
*/
|
||||
@Data
|
||||
public class IdentifyVoicePrintResponse {
|
||||
/**
|
||||
* 最匹配的声纹id
|
||||
*/
|
||||
@JsonProperty("speaker_id")
|
||||
private String speakerId;
|
||||
/**
|
||||
* 声纹的分数
|
||||
*/
|
||||
private Double score;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体声纹表
|
||||
*
|
||||
* @author zjy
|
||||
*/
|
||||
@TableName(value = "ai_agent_voice_print")
|
||||
@Data
|
||||
public class AgentVoicePrintEntity {
|
||||
/**
|
||||
* 主键id
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
/**
|
||||
* 关联的智能体id
|
||||
*/
|
||||
private String agentId;
|
||||
/**
|
||||
* 关联的音频id
|
||||
*/
|
||||
private String audioId;
|
||||
/**
|
||||
* 声纹来源的人姓名
|
||||
*/
|
||||
private String sourceName;
|
||||
/**
|
||||
* 描述声纹来源的人
|
||||
*/
|
||||
private String introduce;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long creator;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createDate;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updater;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateDate;
|
||||
}
|
||||
+27
@@ -9,6 +9,7 @@ import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录表处理service
|
||||
@@ -44,4 +45,30 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
|
||||
* @param deleteText 是否删除文本
|
||||
*/
|
||||
void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText);
|
||||
|
||||
/**
|
||||
* 根据智能体ID获取最近50条用户的聊天记录数据(带音频数据)
|
||||
*
|
||||
* @param agentId 智能体id
|
||||
* @return 聊天记录列表(只有用户)
|
||||
*/
|
||||
List<AgentChatHistoryUserVO> getRecentlyFiftyByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据音频数据ID获取聊天内容
|
||||
*
|
||||
* @param audioId 音频id
|
||||
* @return 聊天内容
|
||||
*/
|
||||
String getContentByAudioId(String audioId);
|
||||
|
||||
|
||||
/**
|
||||
* 查询此音频id是否属于此智能体
|
||||
*
|
||||
* @param audioId 音频id
|
||||
* @param agentId 音频id
|
||||
* @return T:属于 F:不属于
|
||||
*/
|
||||
boolean isAudioOwnedByAgent(String audioId,String agentId);
|
||||
}
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentVoicePrintUpdateDTO;
|
||||
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
|
||||
|
||||
/**
|
||||
* 智能体声纹处理service
|
||||
*
|
||||
* @author zjy
|
||||
*/
|
||||
public interface AgentVoicePrintService {
|
||||
/**
|
||||
* 添加智能体新的声纹
|
||||
*
|
||||
* @param dto 保存智能体声纹的数据
|
||||
* @return T:成功 F:失败
|
||||
*/
|
||||
boolean insert(AgentVoicePrintSaveDTO dto);
|
||||
|
||||
/**
|
||||
* 删除智能体的指的声纹
|
||||
*
|
||||
* @param userId 当前登录的用户id
|
||||
* @param voicePrintId 声纹id
|
||||
* @return 是否成功 T:成功 F:失败
|
||||
*/
|
||||
boolean delete(Long userId, String voicePrintId);
|
||||
|
||||
/**
|
||||
* 获取指定智能体的所有声纹数据
|
||||
*
|
||||
* @param userId 当前登录的用户id
|
||||
* @param agentId 智能体id
|
||||
* @return 声纹数据集合
|
||||
*/
|
||||
List<AgentVoicePrintVO> list(Long userId, String agentId);
|
||||
|
||||
/**
|
||||
* 更新智能体的指的声纹数据
|
||||
*
|
||||
* @param userId 当前登录的用户id
|
||||
* @param dto 修改的声纹的数据
|
||||
* @return 是否成功 T:成功 F:失败
|
||||
*/
|
||||
boolean update(Long userId, AgentVoicePrintUpdateDTO dto);
|
||||
|
||||
}
|
||||
+74
@@ -8,6 +8,7 @@ import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -16,11 +17,14 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.Enums.AgentChatHistoryType;
|
||||
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录表处理service {@link AgentChatHistoryService} impl
|
||||
@@ -90,4 +94,74 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentChatHistoryUserVO> getRecentlyFiftyByAgentId(String agentId) {
|
||||
// 构建查询条件(不添加按照创建时间排序,数据本来就是主键越大创建时间越大
|
||||
// 不添加这样可以减少排序全部数据在分页的全盘扫描消耗)
|
||||
LambdaQueryWrapper<AgentChatHistoryEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.select(AgentChatHistoryEntity::getContent, AgentChatHistoryEntity::getAudioId)
|
||||
.eq(AgentChatHistoryEntity::getAgentId, agentId)
|
||||
.eq(AgentChatHistoryEntity::getChatType, AgentChatHistoryType.USER.getValue())
|
||||
.isNotNull(AgentChatHistoryEntity::getAudioId);
|
||||
|
||||
// 构建分页查询,查询前50页数据
|
||||
Page<AgentChatHistoryEntity> pageParam = new Page<>(0, 50);
|
||||
IPage<AgentChatHistoryEntity> result = this.baseMapper.selectPage(pageParam, wrapper);
|
||||
return result.getRecords().stream().map(item -> {
|
||||
AgentChatHistoryUserVO vo = ConvertUtils.sourceToTarget(item, AgentChatHistoryUserVO.class);
|
||||
// 处理 content 字段,确保只返回聊天内容
|
||||
if (vo != null && vo.getContent() != null) {
|
||||
vo.setContent(extractContentFromString(vo.getContent()));
|
||||
}
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 content 字段中提取聊天内容
|
||||
* 如果 content 是 JSON 格式(如 {"speaker": "未知说话人", "content": "现在几点了。"}),则提取 content
|
||||
* 字段
|
||||
* 如果 content 是普通字符串,则直接返回
|
||||
*
|
||||
* @param content 原始内容
|
||||
* @return 提取的聊天内容
|
||||
*/
|
||||
private String extractContentFromString(String content) {
|
||||
if (content == null || content.trim().isEmpty()) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// 尝试解析为 JSON
|
||||
try {
|
||||
Map<String, Object> jsonMap = JsonUtils.parseObject(content, Map.class);
|
||||
if (jsonMap != null && jsonMap.containsKey("content")) {
|
||||
Object contentObj = jsonMap.get("content");
|
||||
return contentObj != null ? contentObj.toString() : content;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果不是有效的 JSON,直接返回原内容
|
||||
}
|
||||
|
||||
// 如果不是 JSON 格式或没有 content 字段,直接返回原内容
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentByAudioId(String audioId) {
|
||||
AgentChatHistoryEntity agentChatHistoryEntity = baseMapper
|
||||
.selectOne(new LambdaQueryWrapper<AgentChatHistoryEntity>()
|
||||
.select(AgentChatHistoryEntity::getContent)
|
||||
.eq(AgentChatHistoryEntity::getAudioId, audioId));
|
||||
return agentChatHistoryEntity == null ? null : agentChatHistoryEntity.getContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAudioOwnedByAgent(String audioId, String agentId) {
|
||||
// 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体
|
||||
Long row = baseMapper.selectCount(new LambdaQueryWrapper<AgentChatHistoryEntity>()
|
||||
.eq(AgentChatHistoryEntity::getAudioId, audioId)
|
||||
.eq(AgentChatHistoryEntity::getAgentId, agentId));
|
||||
return row == 1;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -178,7 +178,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取智能体 MCP 工具列表失败,智能体ID: {}", id, e);
|
||||
log.error("获取智能体 MCP 工具列表失败,智能体ID: {},错误原因:{}", id, e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
||||
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentVoicePrintUpdateDTO;
|
||||
import xiaozhi.modules.agent.dto.IdentifyVoicePrintResponse;
|
||||
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentVoicePrintService;
|
||||
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* @author zjy
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao, AgentVoicePrintEntity>
|
||||
implements AgentVoicePrintService {
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final RestTemplate restTemplate;
|
||||
private final SysParamsService sysParamsService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
// Springboot提供的编程事务类
|
||||
private final TransactionTemplate transactionTemplate;
|
||||
// 识别度
|
||||
private final Double RECOGNITION = 0.5;
|
||||
private final Executor taskExecutor;
|
||||
|
||||
public AgentVoicePrintServiceImpl(AgentChatAudioService agentChatAudioService, RestTemplate restTemplate,
|
||||
SysParamsService sysParamsService, AgentChatHistoryService agentChatHistoryService,
|
||||
TransactionTemplate transactionTemplate, @Qualifier("taskExecutor") Executor taskExecutor) {
|
||||
this.agentChatAudioService = agentChatAudioService;
|
||||
this.restTemplate = restTemplate;
|
||||
this.sysParamsService = sysParamsService;
|
||||
this.agentChatHistoryService = agentChatHistoryService;
|
||||
this.transactionTemplate = transactionTemplate;
|
||||
this.taskExecutor = taskExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean insert(AgentVoicePrintSaveDTO dto) {
|
||||
// 获取音频数据
|
||||
ByteArrayResource resource = getVoicePrintAudioWAV(dto.getAgentId(), dto.getAudioId());
|
||||
// 识别一下此声音是否注册过
|
||||
IdentifyVoicePrintResponse response = identifyVoicePrint(dto.getAgentId(), resource);
|
||||
if (response != null && response.getScore() > RECOGNITION) {
|
||||
// 根据识别出的声纹ID查询对应的用户信息
|
||||
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
|
||||
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
|
||||
throw new RenException("此声音声纹对应的人(" + existingUserName + ")已经注册,请选择其他声音注册");
|
||||
}
|
||||
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
|
||||
// 开启事务
|
||||
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||
try {
|
||||
// 保存声纹信息
|
||||
int row = baseMapper.insert(entity);
|
||||
// 插入一条数据,影响的数据不等于1说明出现了,保存问题回滚
|
||||
if (row != 1) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
return false;
|
||||
}
|
||||
// 发送注册声纹请求
|
||||
registerVoicePrint(entity.getId(), resource);
|
||||
return true;
|
||||
} catch (RenException e) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
log.error("保存声纹错误原因:{}", e.getMessage());
|
||||
throw new RenException("保存声纹错误,请联系管理员");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(Long userId, String voicePrintId) {
|
||||
// 开启事务
|
||||
boolean b = Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||
try {
|
||||
// 删除声纹,按照指定当前登录用户和智能体
|
||||
int row = baseMapper.delete(new LambdaQueryWrapper<AgentVoicePrintEntity>()
|
||||
.eq(AgentVoicePrintEntity::getId, voicePrintId)
|
||||
.eq(AgentVoicePrintEntity::getCreator, userId));
|
||||
if (row != 1) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
log.error("删除声纹存在错误原因:{}", e.getMessage());
|
||||
throw new RenException("删除声纹出现了错误");
|
||||
}
|
||||
}));
|
||||
// 数据库声纹数据删除成功才继续执行删除声纹服务的数据
|
||||
if(b){
|
||||
taskExecutor.execute(()-> {
|
||||
try {
|
||||
cancelVoicePrint(voicePrintId);
|
||||
}catch (RuntimeException e) {
|
||||
log.error("删除声纹存在运行时错误原因:{},id:{}", e.getMessage(),voicePrintId);
|
||||
}
|
||||
});
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentVoicePrintVO> list(Long userId, String agentId) {
|
||||
// 按照指定当前登录用户和智能体查找数据
|
||||
List<AgentVoicePrintEntity> list = baseMapper.selectList(new LambdaQueryWrapper<AgentVoicePrintEntity>()
|
||||
.eq(AgentVoicePrintEntity::getAgentId, agentId)
|
||||
.eq(AgentVoicePrintEntity::getCreator, userId));
|
||||
return list.stream().map(entity -> {
|
||||
// 遍历转换成AgentVoicePrintVO类型
|
||||
return ConvertUtils.sourceToTarget(entity, AgentVoicePrintVO.class);
|
||||
}).toList();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(Long userId, AgentVoicePrintUpdateDTO dto) {
|
||||
AgentVoicePrintEntity agentVoicePrintEntity = baseMapper
|
||||
.selectOne(new LambdaQueryWrapper<AgentVoicePrintEntity>()
|
||||
.eq(AgentVoicePrintEntity::getId, dto.getId())
|
||||
.eq(AgentVoicePrintEntity::getCreator, userId));
|
||||
if (agentVoicePrintEntity == null) {
|
||||
return false;
|
||||
}
|
||||
// 获取音频Id
|
||||
String audioId = dto.getAudioId();
|
||||
// 获取智能体id
|
||||
String agentId = agentVoicePrintEntity.getAgentId();
|
||||
ByteArrayResource resource;
|
||||
// audioId不等于空,且audioId和之前的保存的音频id不一样,则需要重新获取音频数据生成声纹
|
||||
if (!StringUtils.isEmpty(audioId) && !audioId.equals(agentVoicePrintEntity.getAudioId())) {
|
||||
resource = getVoicePrintAudioWAV(agentId, audioId);
|
||||
|
||||
// 识别一下此声音是否注册过
|
||||
IdentifyVoicePrintResponse response = identifyVoicePrint(agentId, resource);
|
||||
// 返回分数高于RECOGNITION说明这个声纹已经有了
|
||||
if (response != null && response.getScore() > RECOGNITION) {
|
||||
// 判断返回的id如果不是要修改的声纹id,说明这个声纹id,现在要注册的声音已经存在且不是原来的声纹,不允许修改
|
||||
if (!response.getSpeakerId().equals(dto.getId())) {
|
||||
// 根据识别出的声纹ID查询对应的用户信息
|
||||
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
|
||||
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
|
||||
throw new RenException("此次修改不允许,此声音已经注册为声纹了(" + existingUserName + ")");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resource = null;
|
||||
}
|
||||
// 开启事务
|
||||
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||
try {
|
||||
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
|
||||
int row = baseMapper.updateById(entity);
|
||||
if (row != 1) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
return false;
|
||||
}
|
||||
if (resource != null) {
|
||||
String id = entity.getId();
|
||||
// 先注销之前这个声纹id上的声纹向量
|
||||
cancelVoicePrint(id);
|
||||
// 发送注册声纹请求
|
||||
registerVoicePrint(id, resource);
|
||||
}
|
||||
return true;
|
||||
} catch (RenException e) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
log.error("修改声纹错误原因:{}", e.getMessage());
|
||||
throw new RenException("修改声纹错误,请联系管理员");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取生纹接口URI对象
|
||||
*
|
||||
* @return URI对象
|
||||
*/
|
||||
private URI getVoicePrintURI() {
|
||||
// 获取声纹接口地址
|
||||
String voicePrint = sysParamsService.getValue(Constant.SERVER_VOICE_PRINT, true);
|
||||
try {
|
||||
return new URI(voicePrint);
|
||||
} catch (URISyntaxException e) {
|
||||
log.error("路径格式不正确路径:{},\n错误信息:{}", voicePrint, e.getMessage());
|
||||
throw new RuntimeException("声纹接口的地址存在错误,请进入参数管理修改声纹接口地址");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取声纹地址基础路径
|
||||
*
|
||||
* @param uri 声纹地址uri
|
||||
* @return 基础路径
|
||||
*/
|
||||
private String getBaseUrl(URI uri) {
|
||||
String protocol = uri.getScheme();
|
||||
String host = uri.getHost();
|
||||
int port = uri.getPort();
|
||||
if (port == -1) {
|
||||
return "%s://%s".formatted(protocol, host);
|
||||
} else {
|
||||
return "%s://%s:%s".formatted(protocol, host, port);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证Authorization
|
||||
*
|
||||
* @param uri 声纹地址uri
|
||||
* @return Authorization值
|
||||
*/
|
||||
private String getAuthorization(URI uri) {
|
||||
// 获取参数
|
||||
String query = uri.getQuery();
|
||||
// 获取aes加密密钥
|
||||
String str = "key=";
|
||||
return "Bearer " + query.substring(query.indexOf(str) + str.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取声纹音频资源数据
|
||||
*
|
||||
* @param audioId 音频Id
|
||||
* @return 声纹音频资源数据
|
||||
*/
|
||||
private ByteArrayResource getVoicePrintAudioWAV(String agentId, String audioId) {
|
||||
// 判断这个音频是否属于当前智能体
|
||||
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
|
||||
if (!b) {
|
||||
throw new RenException("音频数据不属于这个智能体");
|
||||
}
|
||||
// 获取到音频数据
|
||||
byte[] audio = agentChatAudioService.getAudio(audioId);
|
||||
// 如果音频数据为空的直接报错不进行下去
|
||||
if (audio == null || audio.length == 0) {
|
||||
throw new RenException("音频数据是空的请检查上传数据");
|
||||
}
|
||||
// 将字节数组包装为资源,返回
|
||||
return new ByteArrayResource(audio) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "VoicePrint.WAV"; // 设置文件名
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注册声纹http请求
|
||||
*
|
||||
* @param id 声纹id
|
||||
* @param resource 声纹音频资源
|
||||
*/
|
||||
private void registerVoicePrint(String id, ByteArrayResource resource) {
|
||||
// 处理声纹接口地址,获取前缀
|
||||
URI uri = getVoicePrintURI();
|
||||
String baseUrl = getBaseUrl(uri);
|
||||
String requestUrl = baseUrl + "/voiceprint/register";
|
||||
// 创建请求体
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("speaker_id", id);
|
||||
body.add("file", resource);
|
||||
|
||||
// 创建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", getAuthorization(uri));
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
// 创建请求体
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||||
// 发送 POST 请求
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(requestUrl, requestEntity, String.class);
|
||||
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("声纹注册失败,请求路径:{}", requestUrl);
|
||||
throw new RenException("声纹保存失败,请求不成功");
|
||||
}
|
||||
// 检查响应内容
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody == null || !responseBody.contains("true")) {
|
||||
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
|
||||
throw new RenException("声纹保存失败,请求处理失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送注销声纹的请求
|
||||
*
|
||||
* @param voicePrintId 声纹id
|
||||
*/
|
||||
private void cancelVoicePrint(String voicePrintId) {
|
||||
URI uri = getVoicePrintURI();
|
||||
String baseUrl = getBaseUrl(uri);
|
||||
String requestUrl = baseUrl + "/voiceprint/" + voicePrintId;
|
||||
// 创建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", getAuthorization(uri));
|
||||
// 创建请求体
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
// 发送 POST 请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(requestUrl, HttpMethod.DELETE, requestEntity,
|
||||
String.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("声纹注销失败,请求路径:{}", requestUrl);
|
||||
throw new RenException("声纹注销失败,请求不成功");
|
||||
}
|
||||
// 检查响应内容
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody == null || !responseBody.contains("true")) {
|
||||
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
|
||||
throw new RenException("声纹注销失败,请求处理失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送识别声纹http请求
|
||||
*
|
||||
* @param agentId 智能体id
|
||||
* @param resource 声纹音频资源
|
||||
* @return 返回识别数据
|
||||
*/
|
||||
private IdentifyVoicePrintResponse identifyVoicePrint(String agentId, ByteArrayResource resource) {
|
||||
|
||||
// 获取该智能体所有注册的声纹
|
||||
List<AgentVoicePrintEntity> agentVoicePrintList = baseMapper
|
||||
.selectList(new LambdaQueryWrapper<AgentVoicePrintEntity>()
|
||||
.select(AgentVoicePrintEntity::getId)
|
||||
.eq(AgentVoicePrintEntity::getAgentId, agentId));
|
||||
|
||||
// 声纹数量为0,说明还没注册过声纹不需要发生识别请求
|
||||
if (agentVoicePrintList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// 处理声纹接口地址,获取前缀
|
||||
URI uri = getVoicePrintURI();
|
||||
String baseUrl = getBaseUrl(uri);
|
||||
String requestUrl = baseUrl + "/voiceprint/identify";
|
||||
// 创建请求体
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
|
||||
// 创建speaker_id参数
|
||||
String speakerIds = agentVoicePrintList.stream()
|
||||
.map(AgentVoicePrintEntity::getId)
|
||||
.collect(Collectors.joining(","));
|
||||
body.add("speaker_ids", speakerIds);
|
||||
body.add("file", resource);
|
||||
|
||||
// 创建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", getAuthorization(uri));
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
// 创建请求体
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||||
// 发送 POST 请求
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(requestUrl, requestEntity, String.class);
|
||||
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("声纹识别请求失败,请求路径:{}", requestUrl);
|
||||
throw new RenException("声纹识别失败,请求不成功");
|
||||
}
|
||||
// 检查响应内容
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody != null) {
|
||||
return JsonUtils.parseObject(responseBody, IdentifyVoicePrintResponse.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package xiaozhi.modules.agent.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体用户个人聊天数据的VO
|
||||
*/
|
||||
@Data
|
||||
public class AgentChatHistoryUserVO {
|
||||
@Schema(description = "聊天内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "音频ID")
|
||||
private String audioId;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package xiaozhi.modules.agent.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 展示智能体声纹列表VO
|
||||
*/
|
||||
@Data
|
||||
public class AgentVoicePrintVO {
|
||||
|
||||
/**
|
||||
* 主键id
|
||||
*/
|
||||
private String id;
|
||||
/**
|
||||
* 音频文件id
|
||||
*/
|
||||
private String audioId;
|
||||
/**
|
||||
* 声纹来源的人姓名
|
||||
*/
|
||||
private String sourceName;
|
||||
/**
|
||||
* 描述声纹来源的人
|
||||
*/
|
||||
private String introduce;
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createDate;
|
||||
}
|
||||
+65
@@ -9,20 +9,26 @@ import java.util.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
@@ -45,6 +51,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private final TimbreService timbreService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
||||
private final AgentVoicePrintDao agentVoicePrintDao;
|
||||
|
||||
@Override
|
||||
public Object getConfig(Boolean isCache) {
|
||||
@@ -162,6 +169,8 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
|
||||
result.put("mcp_endpoint", mcpEndpoint);
|
||||
}
|
||||
// 获取声纹信息
|
||||
buildVoiceprintConfig(agent.getId(), result);
|
||||
|
||||
// 构建模块配置
|
||||
buildModuleConfig(
|
||||
@@ -255,6 +264,62 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建声纹配置信息
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param result 结果Map
|
||||
*/
|
||||
private void buildVoiceprintConfig(String agentId, Map<String, Object> result) {
|
||||
try {
|
||||
// 获取声纹接口地址
|
||||
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
|
||||
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取智能体关联的声纹信息(不需要用户权限验证)
|
||||
List<AgentVoicePrintVO> voiceprints = getVoiceprintsByAgentId(agentId);
|
||||
if (voiceprints == null || voiceprints.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建speakers列表
|
||||
List<String> speakers = new ArrayList<>();
|
||||
for (AgentVoicePrintVO voiceprint : voiceprints) {
|
||||
String speakerStr = String.format("%s,%s,%s",
|
||||
voiceprint.getId(),
|
||||
voiceprint.getSourceName(),
|
||||
voiceprint.getIntroduce() != null ? voiceprint.getIntroduce() : "");
|
||||
speakers.add(speakerStr);
|
||||
}
|
||||
|
||||
// 构建声纹配置
|
||||
Map<String, Object> voiceprintConfig = new HashMap<>();
|
||||
voiceprintConfig.put("url", voiceprintUrl);
|
||||
voiceprintConfig.put("speakers", speakers);
|
||||
|
||||
result.put("voiceprint", voiceprintConfig);
|
||||
} catch (Exception e) {
|
||||
// 声纹配置获取失败时不影响其他功能
|
||||
System.err.println("获取声纹配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体关联的声纹信息
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 声纹信息列表
|
||||
*/
|
||||
private List<AgentVoicePrintVO> getVoiceprintsByAgentId(String agentId) {
|
||||
LambdaQueryWrapper<AgentVoicePrintEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(AgentVoicePrintEntity::getAgentId, agentId);
|
||||
queryWrapper.orderByAsc(AgentVoicePrintEntity::getCreateDate);
|
||||
List<AgentVoicePrintEntity> entities = agentVoicePrintDao.selectList(queryWrapper);
|
||||
return ConvertUtils.sourceToTarget(entities, AgentVoicePrintVO.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建模块配置
|
||||
*
|
||||
|
||||
+37
@@ -107,6 +107,9 @@ public class SysParamsController {
|
||||
// 验证MCP地址
|
||||
validateMcpUrl(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
//
|
||||
validateVoicePrint(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
sysParamsService.update(dto);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
@@ -212,6 +215,7 @@ public class SysParamsController {
|
||||
if (!url.toLowerCase().contains("key")) {
|
||||
throw new RenException("不是正确的MCP地址");
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送GET请求
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
@@ -227,4 +231,37 @@ public class SysParamsController {
|
||||
throw new RenException("MCP接口验证失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
// 验证声纹接口地址是否正常
|
||||
private void validateVoicePrint(String paramCode, String url) {
|
||||
if (!paramCode.equals(Constant.SERVER_VOICE_PRINT)) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException("声纹接口地址不能为空");
|
||||
}
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("声纹接口地址不能使用localhost或127.0.0.1");
|
||||
}
|
||||
if (!url.toLowerCase().contains("key")) {
|
||||
throw new RenException("不是正确的声纹接口地址");
|
||||
}
|
||||
// 验证URL格式
|
||||
if (!url.toLowerCase().startsWith("http")) {
|
||||
throw new RenException("声纹接口地址必须以http或https开头");
|
||||
}
|
||||
try {
|
||||
// 发送GET请求
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RenException("声纹接口访问失败,状态码:" + response.getStatusCode());
|
||||
}
|
||||
// 检查响应内容
|
||||
String body = response.getBody();
|
||||
if (body == null || !body.contains("healthy")) {
|
||||
throw new RenException("声纹接口返回内容格式不正确,可能不是一个真实的MCP接口");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RenException("声纹接口验证失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 添加声纹接口地址参数配置
|
||||
delete from `sys_params` where id = 114;
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
|
||||
VALUES (114, 'server.voice_print', 'null', 'string', 1, '声纹接口地址');
|
||||
@@ -0,0 +1,12 @@
|
||||
DROP TABLE IF EXISTS ai_agent_voice_print;
|
||||
create table ai_agent_voice_print (
|
||||
id varchar(32) NOT NULL COMMENT '声纹ID',
|
||||
agent_id varchar(32) NOT NULL COMMENT '关联的智能体ID',
|
||||
source_name varchar(50) NOT NULL COMMENT '声纹来源的人的姓名',
|
||||
introduce varchar(200) COMMENT '描述声纹来源的这个人',
|
||||
create_date DATETIME COMMENT '创建时间',
|
||||
creator bigint COMMENT '创建者',
|
||||
update_date DATETIME COMMENT '修改时间',
|
||||
updater bigint COMMENT '修改者',
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体声纹表'
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 智能体声纹添加新字段
|
||||
ALTER TABLE ai_agent_voice_print
|
||||
ADD COLUMN audio_id VARCHAR(32) NOT NULL COMMENT '音频ID';
|
||||
@@ -261,3 +261,24 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202507071530.sql
|
||||
- changeSet:
|
||||
id: 202507031602
|
||||
author: zjy
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202507031602.sql
|
||||
- changeSet:
|
||||
id: 202507041018
|
||||
author: zjy
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202507041018.sql
|
||||
- changeSet:
|
||||
id: 202507081646
|
||||
author: zjy
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202507081646.sql
|
||||
|
||||
Reference in New Issue
Block a user