Merge pull request #3130 from xinnan-tech/add-correct-word

add:替换词功能
This commit is contained in:
wengzh
2026-04-27 16:23:33 +08:00
committed by GitHub
44 changed files with 1030 additions and 37 deletions
@@ -95,6 +95,14 @@ public class SwaggerConfig {
.build();
}
@Bean
public GroupedOpenApi correctWordApi() {
return GroupedOpenApi.builder()
.group("correct-word")
.pathsToMatch("/correct-word/**")
.build();
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI().info(new Info()
@@ -256,4 +256,8 @@ public interface ErrorCode {
int MCP_ACCESS_POINT_ADDRESS_NO_PERMISSION = 10200; // 没有权限查看该智能体的MCP接入点地址
int MCP_ACCESS_POINT_ADDRESS_NOT_CONFIGURED = 10201; // 请联系管理员进入参数管理配置mcp接入点地址
int MCP_ACCESS_POINT_TOOLS_LIST_NO_PERMISSION = 10202; // 没有权限查看该智能体的MCP工具列表
// 替换词相关错误码
int CORRECT_WORD_FILE_NAME_EXISTS = 10203; // 文件名已存在
int FILE_SIZE_OVER_LIMIT = 10204; // 文件大小超过限制
}
@@ -52,6 +52,7 @@ import xiaozhi.modules.agent.service.AgentContextProviderService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.correctword.service.CorrectWordFileService;
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.entity.DeviceEntity;
@@ -73,6 +74,7 @@ public class AgentController {
private final AgentChatSummaryService agentChatSummaryService;
private final RedisUtils redisUtils;
private final AgentTagService agentTagService;
private final CorrectWordFileService correctWordFileService;
@GetMapping("/list")
@Operation(summary = "获取用户智能体列表")
@@ -177,6 +179,8 @@ public class AgentController {
agentPluginMappingService.deleteByAgentId(id);
// 删除关联的上下文源配置
agentContextProviderService.deleteByAgentId(id);
// 删除关联的替换词文件关联记录
correctWordFileService.deleteMappingsByAgentId(id);
// 再删除智能体
agentService.deleteById(id);
return new Result<>();
@@ -0,0 +1,21 @@
package xiaozhi.modules.agent.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentCorrectWordMappingEntity;
@Mapper
public interface AgentCorrectWordMappingDao extends BaseDao<AgentCorrectWordMappingEntity> {
int deleteByAgentId(@Param("agentId") String agentId);
int deleteByFileId(@Param("fileId") String fileId);
int batchInsertMapping(@Param("list") List<AgentCorrectWordMappingEntity> mappings);
List<AgentCorrectWordMappingEntity> selectByAgentId(@Param("agentId") String agentId);
}
@@ -88,6 +88,9 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "上下文源配置", nullable = true)
private List<ContextProviderDTO> contextProviders;
@Schema(description = "替换词文件ID列表", nullable = true)
private List<String> correctWordFileIds;
@Data
@Schema(description = "插件函数信息")
public static class FunctionInfo implements Serializable {
@@ -0,0 +1,38 @@
package xiaozhi.modules.agent.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@TableName("ai_agent_correct_word_mapping")
@Schema(description = "智能体替换词文件关联")
public class AgentCorrectWordMappingEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "主键")
private String id;
@Schema(description = "智能体ID")
private String agentId;
@Schema(description = "替换词文件ID")
private String fileId;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updatedAt;
}
@@ -48,6 +48,7 @@ import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTagService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.correctword.service.CorrectWordFileService;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.dto.ModelProviderDTO;
@@ -74,6 +75,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
private final ModelProviderService modelProviderService;
private final AgentContextProviderService agentContextProviderService;
private final AgentTagService agentTagService;
private final CorrectWordFileService correctWordFileService;
@Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
@@ -104,6 +106,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
agent.setContextProviders(contextProviderEntity.getContextProviders());
}
// 查询替换词文件ID列表
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
agent.setCorrectWordFileIds(correctWordFileIds);
// 无需额外查询插件列表,已通过SQL查询出来
return agent;
}
@@ -427,6 +433,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
agentContextProviderService.saveOrUpdateByAgentId(contextEntity);
}
// 更新替换词文件关联
if (dto.getCorrectWordFileIds() != null) {
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
}
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
if (!b) {
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
@@ -25,4 +25,7 @@ public class AgentInfoVO extends AgentEntity
@Schema(description = "上下文源配置")
private List<ContextProviderDTO> contextProviders;
@Schema(description = "替换词文件ID列表")
private List<String> correctWordFileIds;
}
@@ -1,5 +1,7 @@
package xiaozhi.modules.config.controller;
import java.util.List;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -12,6 +14,7 @@ import lombok.AllArgsConstructor;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.config.dto.AgentModelsDTO;
import xiaozhi.modules.config.dto.CorrectWordsDTO;
import xiaozhi.modules.config.service.ConfigService;
/**
@@ -41,4 +44,12 @@ public class ConfigController {
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
return new Result<Object>().ok(models);
}
@PostMapping("correct-words")
@Operation(summary = "获取智能体替换词")
public Result<Object> getCorrectWords(@Valid @RequestBody CorrectWordsDTO dto) {
ValidatorUtils.validateEntity(dto);
List<String> list = configService.getCorrectWords(dto.getMacAddress());
return new Result<Object>().ok(list);
}
}
@@ -0,0 +1,14 @@
package xiaozhi.modules.config.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "获取智能体替换词DTO")
public class CorrectWordsDTO {
@NotBlank(message = "设备MAC地址不能为空")
@Schema(description = "设备MAC地址")
private String macAddress;
}
@@ -1,11 +1,12 @@
package xiaozhi.modules.config.service;
import java.util.List;
import java.util.Map;
public interface ConfigService {
/**
* 获取服务器配置
*
*
* @param isCache 是否缓存
* @return 配置信息
*/
@@ -13,10 +14,18 @@ public interface ConfigService {
/**
* 获取智能体模型配置
*
*
* @param macAddress MAC地址
* @param selectedModule 客户端已实例化的模型
* @return 模型配置信息
*/
Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule);
/**
* 获取智能体替换词
*
* @param macAddress 设备MAC地址
* @return 替换词列表,格式如 ["模板1|模板01", "模板2|模板02"]
*/
List<String> getCorrectWords(String macAddress);
}
@@ -1,10 +1,12 @@
package xiaozhi.modules.config.service.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -30,7 +32,9 @@ 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.correctword.service.CorrectWordFileService;
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
import xiaozhi.modules.correctword.vo.CorrectWordSimpleVO;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
@@ -58,6 +62,7 @@ public class ConfigServiceImpl implements ConfigService {
private final AgentContextProviderService agentContextProviderService;
private final VoiceCloneService cloneVoiceService;
private final AgentVoicePrintDao agentVoicePrintDao;
private final CorrectWordFileService correctWordFileService;
@Override
public Object getConfig(Boolean isCache) {
@@ -242,6 +247,18 @@ public class ConfigServiceImpl implements ConfigService {
return result;
}
@Override
public List<String> getCorrectWords(String macAddress) {
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null) {
return Collections.emptyList();
}
List<CorrectWordSimpleVO> items = correctWordFileService.getAllItemsByAgentId(device.getAgentId());
return items.stream()
.map(item -> item.getSourceWord() + "|" + item.getTargetWord())
.collect(Collectors.toList());
}
/**
* 构建配置信息
*
@@ -0,0 +1,116 @@
package xiaozhi.modules.correctword.controller;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
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;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.correctword.dto.CorrectWordFileCreateDTO;
import xiaozhi.modules.correctword.service.CorrectWordFileService;
import xiaozhi.modules.correctword.vo.CorrectWordFileVO;
@RestController
@RequestMapping("/correct-word")
@Tag(name = "替换词管理")
@AllArgsConstructor
public class CorrectWordController {
private final CorrectWordFileService correctWordFileService;
@PostMapping("/file")
@Operation(summary = "创建替换词文件")
@RequiresPermissions("sys:role:normal")
public Result<CorrectWordFileVO> createFile(@Valid @RequestBody CorrectWordFileCreateDTO dto) {
CorrectWordFileVO vo = correctWordFileService.createFile(dto);
return new Result<CorrectWordFileVO>().ok(vo);
}
@PutMapping("/file/{fileId}")
@Operation(summary = "修改替换词文件")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateFile(@PathVariable String fileId, @Valid @RequestBody CorrectWordFileCreateDTO dto) {
correctWordFileService.updateFile(fileId, dto);
return new Result<>();
}
@GetMapping("/file/list")
@Operation(summary = "分页获取当前用户替换词文件列表")
@RequiresPermissions("sys:role:normal")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
})
public Result<PageData<CorrectWordFileVO>> listFiles(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
PageData<CorrectWordFileVO> page = correctWordFileService.listFiles(params);
return new Result<PageData<CorrectWordFileVO>>().ok(page);
}
@GetMapping("/file/select")
@Operation(summary = "智能体获取当前用户替换词文件列表")
@RequiresPermissions("sys:role:normal")
public Result<List<CorrectWordFileVO>> listAllFiles() {
List<CorrectWordFileVO> list = correctWordFileService.listAllFiles();
return new Result<List<CorrectWordFileVO>>().ok(list);
}
@GetMapping("/file/download/{fileId}")
@Operation(summary = "下载替换词文件")
@RequiresPermissions("sys:role:normal")
public ResponseEntity<byte[]> downloadFile(@PathVariable String fileId) {
CorrectWordFileVO vo = correctWordFileService.getFileContent(fileId);
if (vo == null || vo.getContent() == null || vo.getContent().isEmpty()) {
return ResponseEntity.notFound().build();
}
byte[] bytes = String.join("\n", vo.getContent()).getBytes(StandardCharsets.UTF_8);
String encodedFileName = URLEncoder.encode(vo.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
String asciiFileName = vo.getFileName().replaceAll("[^\\x00-\\x7F]", "_");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + asciiFileName + "\"; filename*=UTF-8''" + encodedFileName)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
}
@DeleteMapping("/file/{fileId}")
@Operation(summary = "删除替换词文件")
@RequiresPermissions("sys:role:normal")
public Result<Void> deleteFile(@PathVariable String fileId) {
correctWordFileService.deleteFile(fileId);
return new Result<>();
}
@PostMapping("/file/batch-delete")
@Operation(summary = "批量删除替换词文件")
@RequiresPermissions("sys:role:normal")
public Result<Void> batchDeleteFiles(@RequestBody List<String> fileIds) {
if (fileIds == null || fileIds.isEmpty()) {
return new Result<>();
}
correctWordFileService.batchDeleteFiles(fileIds);
return new Result<>();
}
}
@@ -0,0 +1,10 @@
package xiaozhi.modules.correctword.dao;
import org.apache.ibatis.annotations.Mapper;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.correctword.entity.CorrectWordFileEntity;
@Mapper
public interface CorrectWordFileDao extends BaseDao<CorrectWordFileEntity> {
}
@@ -0,0 +1,15 @@
package xiaozhi.modules.correctword.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.correctword.entity.CorrectWordItemEntity;
@Mapper
public interface CorrectWordItemDao extends BaseDao<CorrectWordItemEntity> {
int batchInsert(@Param("list") List<CorrectWordItemEntity> items);
}
@@ -0,0 +1,24 @@
package xiaozhi.modules.correctword.dto;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Data
@Schema(description = "创建替换词文件DTO")
public class CorrectWordFileCreateDTO {
@NotBlank(message = "文件名不能为空")
@Schema(description = "文件名")
private String fileName;
@NotEmpty(message = "替换词内容不能为空")
@Schema(description = "替换词内容,每条格式:原词|替换词")
private List<String> content;
@Schema(description = "文件大小(字节),不能超过1MB")
private Long fileSize;
}
@@ -0,0 +1,41 @@
package xiaozhi.modules.correctword.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@TableName("ai_agent_correct_word_file")
@Schema(description = "智能体替换词文件")
public class CorrectWordFileEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "替换词文件ID")
private String id;
@Schema(description = "原始文件名")
private String fileName;
@Schema(description = "替换词数量")
private Integer wordCount;
@Schema(description = "文件原始内容(用于下载)")
private String content;
@Schema(description = "创建者")
private Long creator;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "更新者")
private Long updater;
@Schema(description = "更新时间")
private Date updatedAt;
}
@@ -0,0 +1,27 @@
package xiaozhi.modules.correctword.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@TableName("ai_agent_correct_word_item")
@Schema(description = "替换词词条")
public class CorrectWordItemEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "词条ID")
private String id;
@Schema(description = "所属文件ID")
private String fileId;
@Schema(description = "原词")
private String sourceWord;
@Schema(description = "替换词")
private String targetWord;
}
@@ -0,0 +1,96 @@
package xiaozhi.modules.correctword.service;
import java.util.List;
import java.util.Map;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.correctword.dto.CorrectWordFileCreateDTO;
import xiaozhi.modules.correctword.vo.CorrectWordFileVO;
import xiaozhi.modules.correctword.vo.CorrectWordSimpleVO;
public interface CorrectWordFileService {
/**
* 创建替换词文件
*
* @param dto 创建参数
* @return 文件VO
*/
CorrectWordFileVO createFile(CorrectWordFileCreateDTO dto);
/**
* 修改替换词文件(全量替换词条)
*
* @param fileId 文件ID
* @param dto 修改参数
*/
void updateFile(String fileId, CorrectWordFileCreateDTO dto);
/**
* 获取当前用户的替换词文件列表
*
* @param params 分页参数
* @return 分页数据
*/
PageData<CorrectWordFileVO> listFiles(Map<String, Object> params);
/**
* 获取当前用户的替换词文件列表(不分页,用于下拉选择)
*
* @return 文件列表
*/
List<CorrectWordFileVO> listAllFiles();
/**
* 获取文件原始内容(用于下载)
*
* @param fileId 文件ID
* @return 文件实体
*/
CorrectWordFileVO getFileContent(String fileId);
/**
* 删除替换词文件及其所有词条和关联记录
*
* @param fileId 文件ID
*/
void deleteFile(String fileId);
/**
* 删除智能体关联的替换词文件关联记录(不删文件本身)
*
* @param agentId 智能体ID
*/
void deleteMappingsByAgentId(String agentId);
/**
* 获取智能体的所有替换词条(精简版,供设备端使用)
*
* @param agentId 智能体ID
* @return 替换词列表
*/
List<CorrectWordSimpleVO> getAllItemsByAgentId(String agentId);
/**
* 获取智能体关联的替换词文件ID列表
*
* @param agentId 智能体ID
* @return 文件ID列表
*/
List<String> getAgentCorrectWordFileIds(String agentId);
/**
* 保存智能体关联的替换词文件(全量替换)
*
* @param agentId 智能体ID
* @param fileIds 文件ID列表
*/
void saveAgentCorrectWords(String agentId, List<String> fileIds);
/**
* 批量删除替换词文件
*
* @param fileIds 文件ID列表
*/
void batchDeleteFiles(List<String> fileIds);
}
@@ -0,0 +1,296 @@
package xiaozhi.modules.correctword.service.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
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.metadata.IPage;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.agent.dao.AgentCorrectWordMappingDao;
import xiaozhi.modules.correctword.dao.CorrectWordFileDao;
import xiaozhi.modules.correctword.dao.CorrectWordItemDao;
import xiaozhi.modules.correctword.dto.CorrectWordFileCreateDTO;
import xiaozhi.modules.agent.entity.AgentCorrectWordMappingEntity;
import xiaozhi.modules.correctword.entity.CorrectWordFileEntity;
import xiaozhi.modules.correctword.entity.CorrectWordItemEntity;
import xiaozhi.modules.correctword.service.CorrectWordFileService;
import xiaozhi.modules.correctword.vo.CorrectWordFileVO;
import xiaozhi.modules.correctword.vo.CorrectWordSimpleVO;
import xiaozhi.modules.security.user.SecurityUser;
@Service
@AllArgsConstructor
public class CorrectWordFileServiceImpl extends BaseServiceImpl<CorrectWordFileDao, CorrectWordFileEntity>
implements CorrectWordFileService {
private final CorrectWordFileDao correctWordFileDao;
private final CorrectWordItemDao correctWordItemDao;
private final AgentCorrectWordMappingDao agentCorrectWordMappingDao;
@Override
@Transactional(rollbackFor = Exception.class)
public CorrectWordFileVO createFile(CorrectWordFileCreateDTO dto) {
// 校验文件大小不能超过1MB
if (dto.getFileSize() != null && dto.getFileSize() > 1024 * 1024) {
throw new RenException(ErrorCode.FILE_SIZE_OVER_LIMIT);
}
// 校验文件名是否重复
Long userId = SecurityUser.getUserId();
LambdaQueryWrapper<CorrectWordFileEntity> nameWrapper = new LambdaQueryWrapper<>();
nameWrapper.eq(CorrectWordFileEntity::getCreator, userId)
.eq(CorrectWordFileEntity::getFileName, dto.getFileName());
if (correctWordFileDao.selectCount(nameWrapper) > 0) {
throw new RenException(ErrorCode.CORRECT_WORD_FILE_NAME_EXISTS);
}
List<CorrectWordItemEntity> items = parseContent(dto.getContent());
// 保存文件记录
CorrectWordFileEntity fileEntity = new CorrectWordFileEntity();
fileEntity.setFileName(dto.getFileName());
fileEntity.setWordCount(items.size());
fileEntity.setContent(String.join("\n", dto.getContent()));
fileEntity.setCreator(SecurityUser.getUserId());
fileEntity.setCreatedAt(new Date());
correctWordFileDao.insert(fileEntity);
// 设置fileId并批量保存词条
String fileId = fileEntity.getId();
for (CorrectWordItemEntity item : items) {
item.setFileId(fileId);
}
if (!items.isEmpty()) {
correctWordItemDao.batchInsert(items);
}
return toVO(fileEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateFile(String fileId, CorrectWordFileCreateDTO dto) {
CorrectWordFileEntity fileEntity = correctWordFileDao.selectById(fileId);
if (fileEntity == null) {
return;
}
// 校验文件名是否重复(排除自身)
Long userId = SecurityUser.getUserId();
LambdaQueryWrapper<CorrectWordFileEntity> nameWrapper = new LambdaQueryWrapper<>();
nameWrapper.eq(CorrectWordFileEntity::getCreator, userId)
.eq(CorrectWordFileEntity::getFileName, dto.getFileName())
.ne(CorrectWordFileEntity::getId, fileId);
if (correctWordFileDao.selectCount(nameWrapper) > 0) {
throw new RenException("文件名已存在:" + dto.getFileName());
}
// 先删除旧词条
LambdaQueryWrapper<CorrectWordItemEntity> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(CorrectWordItemEntity::getFileId, fileId);
correctWordItemDao.delete(deleteWrapper);
// 解析新词条并批量保存
List<CorrectWordItemEntity> items = parseContent(dto.getContent());
if (!items.isEmpty()) {
for (CorrectWordItemEntity item : items) {
item.setFileId(fileId);
}
correctWordItemDao.batchInsert(items);
}
// 更新文件记录
fileEntity.setFileName(dto.getFileName());
fileEntity.setWordCount(items.size());
fileEntity.setContent(String.join("\n", dto.getContent()));
fileEntity.setUpdater(SecurityUser.getUserId());
fileEntity.setUpdatedAt(new Date());
correctWordFileDao.updateById(fileEntity);
}
@Override
public PageData<CorrectWordFileVO> listFiles(Map<String, Object> params) {
Long userId = SecurityUser.getUserId();
IPage<CorrectWordFileEntity> page = getPage(params, "created_at", false);
LambdaQueryWrapper<CorrectWordFileEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CorrectWordFileEntity::getCreator, userId)
.orderByDesc(CorrectWordFileEntity::getCreatedAt);
correctWordFileDao.selectPage(page, wrapper);
List<CorrectWordFileVO> voList = toVOList(page.getRecords());
return new PageData<>(voList, page.getTotal());
}
@Override
public List<CorrectWordFileVO> listAllFiles() {
Long userId = SecurityUser.getUserId();
LambdaQueryWrapper<CorrectWordFileEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CorrectWordFileEntity::getCreator, userId)
.orderByDesc(CorrectWordFileEntity::getCreatedAt);
List<CorrectWordFileEntity> entities = correctWordFileDao.selectList(wrapper);
return toVOList(entities);
}
@Override
public CorrectWordFileVO getFileContent(String fileId) {
CorrectWordFileEntity entity = correctWordFileDao.selectById(fileId);
return toVO(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteFile(String fileId) {
if (fileId == null || fileId.trim().isEmpty()) {
return;
}
// 先删除关联表记录
agentCorrectWordMappingDao.deleteByFileId(fileId);
// 删除词条
LambdaQueryWrapper<CorrectWordItemEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CorrectWordItemEntity::getFileId, fileId);
correctWordItemDao.delete(wrapper);
// 删除文件
correctWordFileDao.deleteById(fileId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteMappingsByAgentId(String agentId) {
agentCorrectWordMappingDao.deleteByAgentId(agentId);
}
@Override
public List<CorrectWordSimpleVO> getAllItemsByAgentId(String agentId) {
// 通过关联表获取文件ID列表
List<AgentCorrectWordMappingEntity> mappings = agentCorrectWordMappingDao.selectByAgentId(agentId);
if (mappings == null || mappings.isEmpty()) {
return new ArrayList<>();
}
List<String> fileIds = mappings.stream()
.map(AgentCorrectWordMappingEntity::getFileId)
.collect(Collectors.toList());
// 根据文件ID列表查询词条
LambdaQueryWrapper<CorrectWordItemEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CorrectWordItemEntity::getFileId, fileIds);
List<CorrectWordItemEntity> entities = correctWordItemDao.selectList(wrapper);
return ConvertUtils.sourceToTarget(entities, CorrectWordSimpleVO.class);
}
@Override
public List<String> getAgentCorrectWordFileIds(String agentId) {
List<AgentCorrectWordMappingEntity> mappings = agentCorrectWordMappingDao.selectByAgentId(agentId);
if (mappings == null || mappings.isEmpty()) {
return new ArrayList<>();
}
return mappings.stream()
.map(AgentCorrectWordMappingEntity::getFileId)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveAgentCorrectWords(String agentId, List<String> fileIds) {
// 先删除旧的关联记录
agentCorrectWordMappingDao.deleteByAgentId(agentId);
if (fileIds == null || fileIds.isEmpty()) {
return;
}
// 批量插入新的关联记录
Long userId = SecurityUser.getUserId();
Date now = new Date();
List<AgentCorrectWordMappingEntity> mappings = new ArrayList<>();
for (String fileId : fileIds) {
AgentCorrectWordMappingEntity mapping = new AgentCorrectWordMappingEntity();
mapping.setAgentId(agentId);
mapping.setFileId(fileId);
mapping.setCreator(userId);
mapping.setCreatedAt(now);
mapping.setUpdater(userId);
mapping.setUpdatedAt(now);
mappings.add(mapping);
}
agentCorrectWordMappingDao.batchInsertMapping(mappings);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchDeleteFiles(List<String> fileIds) {
if (fileIds == null || fileIds.isEmpty()) {
return;
}
for (String fileId : fileIds) {
if (fileId == null || fileId.trim().isEmpty()) {
continue;
}
deleteFile(fileId.trim());
}
}
/**
* 解析替换词内容,每条格式:原词|替换词
*/
private List<CorrectWordItemEntity> parseContent(List<String> lines) {
List<CorrectWordItemEntity> items = new ArrayList<>();
if (lines == null || lines.isEmpty()) {
return items;
}
for (String line : lines) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
int idx = line.indexOf('|');
if (idx <= 0 || idx >= line.length() - 1) {
continue;
}
String sourceWord = line.substring(0, idx).trim();
String targetWord = line.substring(idx + 1).trim();
if (sourceWord.isEmpty() || targetWord.isEmpty()) {
continue;
}
CorrectWordItemEntity item = new CorrectWordItemEntity();
item.setSourceWord(sourceWord);
item.setTargetWord(targetWord);
items.add(item);
}
return items;
}
private CorrectWordFileVO toVO(CorrectWordFileEntity entity) {
if (entity == null) {
return null;
}
CorrectWordFileVO vo = new CorrectWordFileVO();
vo.setId(entity.getId());
vo.setFileName(entity.getFileName());
vo.setWordCount(entity.getWordCount());
vo.setContent(entity.getContent() != null
? Arrays.asList(entity.getContent().split("\n"))
: new ArrayList<>());
vo.setCreatedAt(entity.getCreatedAt());
vo.setUpdatedAt(entity.getUpdatedAt());
return vo;
}
private List<CorrectWordFileVO> toVOList(List<CorrectWordFileEntity> entities) {
if (entities == null || entities.isEmpty()) {
return new ArrayList<>();
}
return entities.stream().map(this::toVO).collect(Collectors.toList());
}
}
@@ -0,0 +1,30 @@
package xiaozhi.modules.correctword.vo;
import java.util.Date;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "替换词文件列表VO")
public class CorrectWordFileVO {
@Schema(description = "替换词文件ID")
private String id;
@Schema(description = "原始文件名")
private String fileName;
@Schema(description = "替换词数量")
private Integer wordCount;
@Schema(description = "替换词内容,每行一条")
private List<String> content;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "更新时间")
private Date updatedAt;
}
@@ -0,0 +1,15 @@
package xiaozhi.modules.correctword.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "替换词精简VO(设备端使用)")
public class CorrectWordSimpleVO {
@Schema(description = "原词")
private String sourceWord;
@Schema(description = "替换词")
private String targetWord;
}
@@ -0,0 +1,38 @@
-- 智能体替换词文件表
CREATE TABLE IF NOT EXISTS `ai_agent_correct_word_file` (
`id` VARCHAR(32) NOT NULL,
`file_name` VARCHAR(256) NOT NULL COMMENT '原始文件名',
`word_count` INT NOT NULL DEFAULT 0 COMMENT '替换词数量',
`content` TEXT COMMENT '文件原始内容',
`creator` BIGINT DEFAULT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updater` BIGINT DEFAULT NULL,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_creator` (`creator`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='替换词文件';
-- 替换词词条表
CREATE TABLE IF NOT EXISTS `ai_agent_correct_word_item` (
`id` VARCHAR(32) NOT NULL,
`file_id` VARCHAR(32) NOT NULL COMMENT '所属文件ID',
`source_word` VARCHAR(128) NOT NULL COMMENT '原词',
`target_word` VARCHAR(128) NOT NULL COMMENT '替换词',
PRIMARY KEY (`id`),
INDEX `idx_file_id` (`file_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='替换词词条';
-- 智能体替换词文件关联表
CREATE TABLE IF NOT EXISTS `ai_agent_correct_word_mapping` (
`id` VARCHAR(32) NOT NULL,
`agent_id` VARCHAR(32) NOT NULL,
`file_id` VARCHAR(32) NOT NULL,
`creator` BIGINT DEFAULT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updater` BIGINT DEFAULT NULL,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE INDEX `uk_agent_file` (`agent_id`, `file_id`),
INDEX `idx_agent_id` (`agent_id`),
INDEX `idx_file_id` (`file_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体替换词文件关联';
@@ -620,6 +620,13 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202604201719.sql
- changeSet:
id: 202604211515
author: rainv123
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202604211515.sql
- changeSet:
id: 202604211700
author: RanChen
@@ -209,4 +209,5 @@
10200=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684MCP\u63A5\u5165\u70B9\u5730\u5740
10201=\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u914D\u7F6Emcp\u63A5\u5165\u70B9\u5730\u5740
10202=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684MCP\u5DE5\u5177\u5217\u8868
10203=\u6587\u4EF6\u540D\u79F0\u5DF2\u5B58\u5728
10204=\u6587\u4EF6\u5927\u5C0F\u8D85\u8FC71MB\u9650\u5236
@@ -209,4 +209,5 @@
10200=Keine Berechtigung, die MCP-Endpunktadresse dieses Agenten anzuzeigen
10201=Bitte kontaktieren Sie den Administrator, um die MCP-Endpunktadresse in der Parameterverwaltung zu konfigurieren
10202=Keine Berechtigung, die MCP-Tool-Liste dieses Agenten anzuzeigen
10203=Dateiname existiert bereits
10204=Dateigr\u00f6\u00dfe \u00fcberschreitet 1MB Limit
@@ -209,4 +209,6 @@
10200=No permission to view the MCP endpoint address of this agent
10201=Please contact the administrator to configure the MCP endpoint address in parameter management
10202=No permission to view the MCP tool list of this agent
10203=File name already exists
10204=File size exceeds 1MB limit
@@ -21,7 +21,7 @@
10015=Exclua primeiro os usu\u00E1rios do departamento
10016=Falha na implanta\u00E7\u00E3o, sem fluxo
10017=Diagrama incorreto, verifique
10018=Falha na exporta\u00E7\u00E3o, ID do modelo {0}
10018=Falha na exporta\u00E7\u00E3o, ID do modelo {0}
10019=Por favor, fa\u00E7a upload do arquivo
10020=Token n\u00E3o pode ser vazio
@@ -57,7 +57,7 @@
10046=Erro ao salvar impress\u00E3o vocal, entre em contato com o administrador
10047=Limite di\u00E1rio de envio atingido
10048=Erro na inser\u00E7\u00E3o da senha antiga
10049=LLM configurado n\u00E3o openai ou Collama
10049=LLM configurado n\u00E3o openai ou Collama
10050=Falha na gera\u00E7\u00E3o do token
10051=Recurso n\u00E3o existe
10052=Agente padr\u00E3o n\u00E3o encontrado
@@ -72,12 +72,12 @@
10061=C\u00F3digo de ativa\u00E7\u00E3o n\u00E3o pode ser vazio
10062=C\u00F3digo de ativa\u00E7\u00E3o incorreto
10063=Dispositivo j\u00E1 ativado
10064=Este modelo o modelo padr\u00E3o, configure primeiro outro modelo como padr\u00E3o
10064=Este modelo o modelo padr\u00E3o, configure primeiro outro modelo como padr\u00E3o
10065=Falha ao adicionar dados
10066=Falha ao modificar dados
10067=C\u00F3digo de verifica\u00E7\u00E3o gr\u00E1fico incorreto
10068=Registro por telefone n\u00E3o habilitado, fun\u00E7\u00E3o de c\u00F3digo de verifica\u00E7\u00E3o por SMS indispon\u00EDvel
10069=Nome de usu\u00E1rio n\u00E3o n\u00FAmero de telefone, por favor insira novamente
10069=Nome de usu\u00E1rio n\u00E3o n\u00FAmero de telefone, por favor insira novamente
10070=Este n\u00FAmero de telefone j\u00E1 est\u00E1 registrado
10071=N\u00FAmero de telefone inserido n\u00E3o est\u00E1 registrado
10072=Registro de usu\u00E1rio comum n\u00E3o permitido atualmente
@@ -90,7 +90,7 @@
10079=Par\u00E2metros de sele\u00E7\u00E3o incompat\u00EDveis entre LLM e reconhecimento de inten\u00E7\u00E3o Intent
10080=A pessoa ({0}) correspondente a esta impress\u00E3o vocal j\u00E1 est\u00E1 registrada, escolha outra voz para registrar
10081=Erro ao excluir impress\u00E3o vocal
10082=Esta modifica\u00E7\u00E3o n\u00E3o permitida, esta voz j\u00E1 est\u00E1 registrada como impress\u00E3o vocal ({0})
10082=Esta modifica\u00E7\u00E3o n\u00E3o permitida, esta voz j\u00E1 est\u00E1 registrada como impress\u00E3o vocal ({0})
10083=Erro ao modificar impress\u00E3o vocal, entre em contato com o administrador
10084=Erro no endere\u00E7o da interface de impress\u00E3o vocal, acesse o gerenciamento de par\u00E2metros para modificar o endere\u00E7o
10085=Dados de \u00E1udio n\u00E3o pertencem a este agente
@@ -205,7 +205,9 @@
10196=Nome da etiqueta j\u00E1 existe
10197=Nome da etiqueta n\u00E3o pode ser vazio
10198=Etiqueta n\u00E3o existe
10199=Esta opera\u00E7\u00E3o n\u00E3o suportada na an\u00E1lise de arquivos chineses
10199=Esta opera\u00E7\u00E3o n\u00E3o suportada na an\u00E1lise de arquivos chineses
10200=Sem permiss\u00e3o para visualizar o endere\u00e7o do endpoint MCP deste agente
10201=Por favor, contate o administrador para configurar o endere\u00e7o do endpoint MCP no gerenciamento de par\u00e2metros
10202=Sem permiss\u00e3o para visualizar a lista de ferramentas MCP deste agente
10202=Sem permiss\u00e3o para visualizar a lista de ferramentas MCP deste agente
10203=Nome do arquivo j\u00E1 existe
10204=O tamanho do arquivo excede o limite de 1MB
@@ -208,4 +208,6 @@
10199=T\u1EC7p \u0111ang \u0111\u01B0\u1EE3c ph\u00E2n t\u00EDch, thao t\u00E1c n\u00E0y kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3
10200=Kh\u00f4ng c\u00f3 quy\u1ec1n xem \u0111\u1ecba ch\u1ec9 \u0111i\u1ec3m cu\u1ed1i MCP c\u1ee7a \u0111\u1ea1i l\u00fd n\u00e0y
10201=Vui l\u00f2ng li\u00ean h\u1ec7 qu\u1ea3n tr\u1ecb vi\u00ean \u0111\u1ec3 c\u1ea5u h\u00ecnh \u0111\u1ecba ch\u1ec9 \u0111i\u1ec3m cu\u1ed1i MCP trong qu\u1ea3n l\u00fd tham s\u1ed1
10202=Kh\u00f4ng c\u00f3 quy\u1ec1n xem danh s\u00e1ch c\u00f4ng c\u1ee5 MCP c\u1ee7a \u0111\u1ea1i l\u00fd n\u00e0y
10202=Kh\u00f4ng c\u00f3 quy\u1ec1n xem danh s\u00e1ch c\u00f4ng c\u1ee5 MCP c\u1ee7a \u0111\u1ea1i l\u00fd n\u00e0y
10203=T\u00EAn t\u1EC7p \u0111\u00E3 t\u1ED3n t\u1EA1i
10204=K\u00EDch th\u01b0\u1edbc t\u1ec7p v\u01b0\u1ee3t qu\u00e1 1MB
@@ -209,3 +209,5 @@
10200=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684MCP\u63A5\u5165\u70B9\u5730\u5740
10201=\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u914D\u7F6Emcp\u63A5\u5165\u70B9\u5730\u5740
10202=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684MCP\u5DE5\u5177\u5217\u8868
10203=\u6587\u4EF6\u540D\u79F0\u5DF2\u5B58\u5728
10204=\u6587\u4EF6\u5927\u5C0F\u8D85\u8FC71MB\u9650\u5236
@@ -209,3 +209,5 @@
10200=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684MCP\u63A5\u5165\u9EDE\u5730\u5740
10201=\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1\u9032\u5165\u53C3\u6578\u7BA1\u7406\u914D\u7F6Emcp\u63A5\u5165\u9EDE\u5730\u5740
10202=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684MCP\u5DE5\u5177\u5217\u8868
10203=\u6A94\u6848\u540D\u7A31\u5DF2\u5B58\u5728
10204=\u6A94\u6848\u5927\u5C0F\u8D85\u904E1MB\u9650\u5236
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.agent.dao.AgentCorrectWordMappingDao">
<delete id="deleteByAgentId">
DELETE FROM ai_agent_correct_word_mapping WHERE agent_id = #{agentId}
</delete>
<delete id="deleteByFileId">
DELETE FROM ai_agent_correct_word_mapping WHERE file_id = #{fileId}
</delete>
<insert id="batchInsertMapping" parameterType="java.util.List">
INSERT INTO ai_agent_correct_word_mapping (id, agent_id, file_id, creator, created_at, updater, updated_at)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.id}, #{item.agentId}, #{item.fileId}, #{item.creator}, #{item.createdAt}, #{item.updater}, #{item.updatedAt})
</foreach>
</insert>
<select id="selectByAgentId" resultType="xiaozhi.modules.agent.entity.AgentCorrectWordMappingEntity">
SELECT id, agent_id, file_id, creator, created_at, updater, updated_at
FROM ai_agent_correct_word_mapping
WHERE agent_id = #{agentId}
</select>
</mapper>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.correctword.dao.CorrectWordItemDao">
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO ai_agent_correct_word_item (id, file_id, source_word, target_word)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.id}, #{item.fileId}, #{item.sourceWord}, #{item.targetWord})
</foreach>
</insert>
</mapper>
+12 -2
View File
@@ -1,7 +1,8 @@
import os
import asyncio
import yaml
from collections.abc import Mapping
from config.manage_api_client import init_service, get_server_config, get_agent_models
from config.manage_api_client import init_service, get_server_config, get_agent_models, get_correct_words
def get_project_dir():
@@ -87,7 +88,16 @@ async def get_config_from_api_async(config):
async def get_private_config_from_api(config, device_id, client_id):
"""从Java API获取私有配置"""
return await get_agent_models(device_id, client_id, config["selected_module"])
results = await asyncio.gather(
get_agent_models(device_id, client_id, config["selected_module"]),
get_correct_words(device_id),
return_exceptions=True
)
private_config = results[0] if not isinstance(results[0], Exception) else {}
correct_words = results[1] if not isinstance(results[1], Exception) else None
if correct_words:
private_config["correct_words"] = correct_words
return private_config
def ensure_directories(config):
@@ -183,6 +183,18 @@ async def get_agent_models(
)
async def get_correct_words(mac_address: str) -> Optional[Dict]:
"""获取智能体替换词"""
try:
return await ManageApiClient._instance._execute_async_request(
"POST", "/config/correct-words",
json={"macAddress": mac_address}
)
except Exception as e:
print(f"获取替换词失败: {e}")
return None
async def generate_and_save_chat_summary(session_id: str) -> Optional[Dict]:
"""生成并保存聊天记录总结"""
try:
+7
View File
@@ -766,6 +766,13 @@ class ConnectionHandler:
if private_config.get("context_providers", None) is not None:
self.config["context_providers"] = private_config["context_providers"]
# 注入替换词到 TTS 模块配置
if private_config.get("correct_words", None) is not None:
select_tts_module = self.config["selected_module"]["TTS"]
self.config["TTS"][select_tts_module]["correct_words"] = private_config[
"correct_words"
]
# 使用 run_in_executor 在线程池中执行 initialize_modules,避免阻塞主循环
try:
modules = await self.loop.run_in_executor(
@@ -194,6 +194,8 @@ class TTSProvider(TTSProviderBase):
# 过滤Markdown
filtered_text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
filtered_text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], filtered_text)
if filtered_text:
# 发送continue-task消息
@@ -480,6 +482,8 @@ class TTSProvider(TTSProviderBase):
# 发送文本
filtered_text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
filtered_text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], filtered_text)
# 发送continue-task消息
continue_task_message = {
"header": {
@@ -295,6 +295,8 @@ class TTSProvider(TTSProviderBase):
logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本")
return
filtered_text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
filtered_text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], filtered_text)
if filtered_text:
run_request = {
"header": {
@@ -564,6 +566,8 @@ class TTSProvider(TTSProviderBase):
# 发送文本合成请求
filtered_text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
filtered_text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], filtered_text)
run_request = {
"header": {
"message_id": uuid.uuid4().hex,
+39 -14
View File
@@ -45,6 +45,21 @@ class TTSProviderBase(ABC):
self.report_on_last = False
# sentence_id 到文本的映射,用于流式TTS获取正确的字幕文本
self._sentence_text_map = {}
# 加载替换词,用于一次性正则替换
raw_words = config.get("correct_words", [])
self.correct_words = {}
for item in raw_words:
parts = item.split("|", 1)
if len(parts) == 2:
self.correct_words[parts[0]] = parts[1]
# 构建正则表达式,使用最长匹配优先(排序后转义拼接)
if self.correct_words:
# 按key长度降序排列,长的先匹配,避免短词部分干扰
sorted_keys = sorted(self.correct_words.keys(), key=len, reverse=True)
pattern_str = '|'.join(re.escape(k) for k in sorted_keys)
self._correct_words_pattern = re.compile(pattern_str)
else:
self._correct_words_pattern = None
self.tts_text_buff = []
self.punctuations = (
@@ -89,7 +104,12 @@ class TTSProviderBase(ABC):
self.before_stop_play_files.append((file_audio, text))
def to_tts_stream(self, text, opus_handler: Callable[[bytes], None] = None) -> None:
# 保留原始文本用于显示/上报
original_text = text
text = MarkdownCleaner.clean_markdown(text)
# 使用正则一次性替换,避免重复遍历和部分匹配问题
if self._correct_words_pattern:
text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text)
max_repeat_time = 5
if self.delete_audio_file:
# 需要删除文件的直接转为音频数据
@@ -97,7 +117,8 @@ class TTSProviderBase(ABC):
try:
audio_bytes = asyncio.run(self.text_to_speak(text, None))
if audio_bytes:
self.tts_audio_queue.put((SentenceType.FIRST, None, text, getattr(self, 'current_sentence_id', None)))
# 使用原始文本用于显示/上报
self.tts_audio_queue.put((SentenceType.FIRST, None, original_text, getattr(self, 'current_sentence_id', None)))
audio_bytes_to_data_stream(
audio_bytes,
file_type=self.audio_file_type,
@@ -111,16 +132,16 @@ class TTSProviderBase(ABC):
max_repeat_time -= 1
except Exception as e:
logger.bind(tag=TAG).warning(
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}"
)
max_repeat_time -= 1
if max_repeat_time > 0:
logger.bind(tag=TAG).info(
f"语音生成成功: {text},重试{5 - max_repeat_time}"
f"语音生成成功: {original_text},重试{5 - max_repeat_time}"
)
else:
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
f"语音生成失败: {original_text},请检查网络或服务是否正常"
)
return None
else:
@@ -131,7 +152,7 @@ class TTSProviderBase(ABC):
asyncio.run(self.text_to_speak(text, tmp_file))
except Exception as e:
logger.bind(tag=TAG).warning(
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}"
)
# 未执行成功,删除文件
if os.path.exists(tmp_file):
@@ -140,20 +161,24 @@ class TTSProviderBase(ABC):
if max_repeat_time > 0:
logger.bind(tag=TAG).info(
f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}"
f"语音生成成功: {original_text}:{tmp_file},重试{5 - max_repeat_time}"
)
else:
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
f"语音生成失败: {original_text},请检查网络或服务是否正常"
)
self.tts_audio_queue.put((SentenceType.FIRST, None, text, getattr(self, 'current_sentence_id', None)))
self.tts_audio_queue.put((SentenceType.FIRST, None, original_text, getattr(self, 'current_sentence_id', None)))
self._process_audio_file_stream(tmp_file, callback=opus_handler)
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
return None
def to_tts(self, text):
# 保留原始文本用于日志/显示
original_text = text
text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text)
max_repeat_time = 5
if self.delete_audio_file:
# 需要删除文件的直接转为音频数据
@@ -174,16 +199,16 @@ class TTSProviderBase(ABC):
max_repeat_time -= 1
except Exception as e:
logger.bind(tag=TAG).warning(
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}"
)
max_repeat_time -= 1
if max_repeat_time > 0:
logger.bind(tag=TAG).info(
f"语音生成成功: {text},重试{5 - max_repeat_time}"
f"语音生成成功: {original_text},重试{5 - max_repeat_time}"
)
else:
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
f"语音生成失败: {original_text},请检查网络或服务是否正常"
)
return None
else:
@@ -194,7 +219,7 @@ class TTSProviderBase(ABC):
asyncio.run(self.text_to_speak(text, tmp_file))
except Exception as e:
logger.bind(tag=TAG).warning(
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}"
)
# 未执行成功,删除文件
if os.path.exists(tmp_file):
@@ -203,11 +228,11 @@ class TTSProviderBase(ABC):
if max_repeat_time > 0:
logger.bind(tag=TAG).info(
f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}"
f"语音生成成功: {original_text}:{tmp_file},重试{5 - max_repeat_time}"
)
else:
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
f"语音生成失败: {original_text},请检查网络或服务是否正常"
)
return tmp_file
@@ -370,6 +370,8 @@ class TTSProvider(TTSProviderBase):
# 过滤Markdown
filtered_text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
filtered_text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], filtered_text)
if filtered_text:
# 发送文本
@@ -92,22 +92,25 @@ class TTSProvider(TTSProviderBase):
def to_tts_single_stream(self, text, is_last=False):
try:
max_repeat_time = 5
original_text = text
text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text)
try:
asyncio.run(self.text_to_speak(text, is_last))
except Exception as e:
logger.bind(tag=TAG).warning(
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}"
)
max_repeat_time -= 1
if max_repeat_time > 0:
logger.bind(tag=TAG).info(
f"语音生成成功: {text},重试{5 - max_repeat_time}"
f"语音生成成功: {original_text},重试{5 - max_repeat_time}"
)
else:
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
f"语音生成失败: {original_text},请检查网络或服务是否正常"
)
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
@@ -203,6 +206,8 @@ class TTSProvider(TTSProviderBase):
"""
start_time = time.time()
text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text)
payload = {"text": text, "character": self.voice}
@@ -84,22 +84,25 @@ class TTSProvider(TTSProviderBase):
def to_tts_single_stream(self, text, is_last=False):
try:
max_repeat_time = 5
original_text = text
text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text)
try:
asyncio.run(self.text_to_speak(text, is_last))
except Exception as e:
logger.bind(tag=TAG).warning(
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}"
)
max_repeat_time -= 1
if max_repeat_time > 0:
logger.bind(tag=TAG).info(
f"语音生成成功: {text},重试{5 - max_repeat_time}"
f"语音生成成功: {original_text},重试{5 - max_repeat_time}"
)
else:
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
f"语音生成失败: {original_text},请检查网络或服务是否正常"
)
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
@@ -202,6 +205,8 @@ class TTSProvider(TTSProviderBase):
"""
start_time = time.time()
text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text)
params = {
"tts_text": text,
@@ -148,22 +148,25 @@ class TTSProvider(TTSProviderBase):
def to_tts_single_stream(self, text, is_last=False):
try:
max_repeat_time = 5
original_text = text
text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text)
try:
asyncio.run(self.text_to_speak(text, is_last))
except Exception as e:
logger.bind(tag=TAG).warning(
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}"
)
max_repeat_time -= 1
if max_repeat_time > 0:
logger.bind(tag=TAG).info(
f"语音生成成功: {text},重试{5 - max_repeat_time}"
f"语音生成成功: {original_text},重试{5 - max_repeat_time}"
)
else:
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
f"语音生成失败: {original_text},请检查网络或服务是否正常"
)
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
@@ -298,6 +301,8 @@ class TTSProvider(TTSProviderBase):
"""
start_time = time.time()
text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text)
payload = {
"model": self.model,
@@ -245,6 +245,8 @@ class TTSProvider(TTSProviderBase):
return
filtered_text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
filtered_text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], filtered_text)
if filtered_text:
# 发送文本合成请求
run_request = self._build_base_request(status=1,text=filtered_text)
@@ -441,6 +443,8 @@ class TTSProvider(TTSProviderBase):
try:
filtered_text = MarkdownCleaner.clean_markdown(text)
if self._correct_words_pattern:
filtered_text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], filtered_text)
text_request = self._build_base_request(status=2,text=filtered_text)