diff --git a/main/manager-api/src/main/java/xiaozhi/common/config/SwaggerConfig.java b/main/manager-api/src/main/java/xiaozhi/common/config/SwaggerConfig.java index cbdb5a19..3efe261c 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/config/SwaggerConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/common/config/SwaggerConfig.java @@ -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() diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java index 820f4089..134b66c5 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java @@ -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; // 文件大小超过限制 } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index 77907cc5..5e2435e9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -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<>(); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentCorrectWordMappingDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentCorrectWordMappingDao.java new file mode 100644 index 00000000..6e327dcc --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentCorrectWordMappingDao.java @@ -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 { + + int deleteByAgentId(@Param("agentId") String agentId); + + int deleteByFileId(@Param("fileId") String fileId); + + int batchInsertMapping(@Param("list") List mappings); + + List selectByAgentId(@Param("agentId") String agentId); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java index 13d95ddc..f2053ac5 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java @@ -88,6 +88,9 @@ public class AgentUpdateDTO implements Serializable { @Schema(description = "上下文源配置", nullable = true) private List contextProviders; + @Schema(description = "替换词文件ID列表", nullable = true) + private List correctWordFileIds; + @Data @Schema(description = "插件函数信息") public static class FunctionInfo implements Serializable { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentCorrectWordMappingEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentCorrectWordMappingEntity.java new file mode 100644 index 00000000..6b7d0c5a --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentCorrectWordMappingEntity.java @@ -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; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index ff8add76..56cd70a6 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -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 imp private final ModelProviderService modelProviderService; private final AgentContextProviderService agentContextProviderService; private final AgentTagService agentTagService; + private final CorrectWordFileService correctWordFileService; @Override public PageData adminAgentList(Map params) { @@ -104,6 +106,10 @@ public class AgentServiceImpl extends BaseServiceImpl imp agent.setContextProviders(contextProviderEntity.getContextProviders()); } + // 查询替换词文件ID列表 + List correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id); + agent.setCorrectWordFileIds(correctWordFileIds); + // 无需额外查询插件列表,已通过SQL查询出来 return agent; } @@ -427,6 +433,11 @@ public class AgentServiceImpl extends BaseServiceImpl 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); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java index 3da8b71c..658ed6f2 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java @@ -25,4 +25,7 @@ public class AgentInfoVO extends AgentEntity @Schema(description = "上下文源配置") private List contextProviders; + + @Schema(description = "替换词文件ID列表") + private List correctWordFileIds; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java index f21795a3..1bbbcaa4 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java @@ -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().ok(models); } + + @PostMapping("correct-words") + @Operation(summary = "获取智能体替换词") + public Result getCorrectWords(@Valid @RequestBody CorrectWordsDTO dto) { + ValidatorUtils.validateEntity(dto); + List list = configService.getCorrectWords(dto.getMacAddress()); + return new Result().ok(list); + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/dto/CorrectWordsDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/config/dto/CorrectWordsDTO.java new file mode 100644 index 00000000..0c689d5d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/dto/CorrectWordsDTO.java @@ -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; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/ConfigService.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/ConfigService.java index 93845e8e..e234e22d 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/ConfigService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/ConfigService.java @@ -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 getAgentModels(String macAddress, Map selectedModule); + + /** + * 获取智能体替换词 + * + * @param macAddress 设备MAC地址 + * @return 替换词列表,格式如 ["模板1|模板01", "模板2|模板02"] + */ + List getCorrectWords(String macAddress); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index 278d4299..8e35e0b7 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -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 getCorrectWords(String macAddress) { + DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); + if (device == null) { + return Collections.emptyList(); + } + List items = correctWordFileService.getAllItemsByAgentId(device.getAgentId()); + return items.stream() + .map(item -> item.getSourceWord() + "|" + item.getTargetWord()) + .collect(Collectors.toList()); + } + /** * 构建配置信息 * diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/controller/CorrectWordController.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/controller/CorrectWordController.java new file mode 100644 index 00000000..5569587f --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/controller/CorrectWordController.java @@ -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 createFile(@Valid @RequestBody CorrectWordFileCreateDTO dto) { + CorrectWordFileVO vo = correctWordFileService.createFile(dto); + return new Result().ok(vo); + } + + @PutMapping("/file/{fileId}") + @Operation(summary = "修改替换词文件") + @RequiresPermissions("sys:role:normal") + public Result 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> listFiles( + @Parameter(hidden = true) @RequestParam Map params) { + PageData page = correctWordFileService.listFiles(params); + return new Result>().ok(page); + } + + @GetMapping("/file/select") + @Operation(summary = "智能体获取当前用户替换词文件列表") + @RequiresPermissions("sys:role:normal") + public Result> listAllFiles() { + List list = correctWordFileService.listAllFiles(); + return new Result>().ok(list); + } + + @GetMapping("/file/download/{fileId}") + @Operation(summary = "下载替换词文件") + @RequiresPermissions("sys:role:normal") + public ResponseEntity 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 deleteFile(@PathVariable String fileId) { + correctWordFileService.deleteFile(fileId); + return new Result<>(); + } + + @PostMapping("/file/batch-delete") + @Operation(summary = "批量删除替换词文件") + @RequiresPermissions("sys:role:normal") + public Result batchDeleteFiles(@RequestBody List fileIds) { + if (fileIds == null || fileIds.isEmpty()) { + return new Result<>(); + } + correctWordFileService.batchDeleteFiles(fileIds); + return new Result<>(); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/dao/CorrectWordFileDao.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/dao/CorrectWordFileDao.java new file mode 100644 index 00000000..769c23c7 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/dao/CorrectWordFileDao.java @@ -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 { +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/dao/CorrectWordItemDao.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/dao/CorrectWordItemDao.java new file mode 100644 index 00000000..cd1696b5 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/dao/CorrectWordItemDao.java @@ -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 { + + int batchInsert(@Param("list") List items); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/dto/CorrectWordFileCreateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/dto/CorrectWordFileCreateDTO.java new file mode 100644 index 00000000..9941abda --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/dto/CorrectWordFileCreateDTO.java @@ -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 content; + + @Schema(description = "文件大小(字节),不能超过1MB") + private Long fileSize; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/entity/CorrectWordFileEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/entity/CorrectWordFileEntity.java new file mode 100644 index 00000000..1d32623e --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/entity/CorrectWordFileEntity.java @@ -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; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/entity/CorrectWordItemEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/entity/CorrectWordItemEntity.java new file mode 100644 index 00000000..42b9f188 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/entity/CorrectWordItemEntity.java @@ -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; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/service/CorrectWordFileService.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/service/CorrectWordFileService.java new file mode 100644 index 00000000..31e29c8e --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/service/CorrectWordFileService.java @@ -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 listFiles(Map params); + + /** + * 获取当前用户的替换词文件列表(不分页,用于下拉选择) + * + * @return 文件列表 + */ + List 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 getAllItemsByAgentId(String agentId); + + /** + * 获取智能体关联的替换词文件ID列表 + * + * @param agentId 智能体ID + * @return 文件ID列表 + */ + List getAgentCorrectWordFileIds(String agentId); + + /** + * 保存智能体关联的替换词文件(全量替换) + * + * @param agentId 智能体ID + * @param fileIds 文件ID列表 + */ + void saveAgentCorrectWords(String agentId, List fileIds); + + /** + * 批量删除替换词文件 + * + * @param fileIds 文件ID列表 + */ + void batchDeleteFiles(List fileIds); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/service/impl/CorrectWordFileServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/service/impl/CorrectWordFileServiceImpl.java new file mode 100644 index 00000000..b35a0e09 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/service/impl/CorrectWordFileServiceImpl.java @@ -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 + 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 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 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 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 deleteWrapper = new LambdaQueryWrapper<>(); + deleteWrapper.eq(CorrectWordItemEntity::getFileId, fileId); + correctWordItemDao.delete(deleteWrapper); + + // 解析新词条并批量保存 + List 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 listFiles(Map params) { + Long userId = SecurityUser.getUserId(); + IPage page = getPage(params, "created_at", false); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CorrectWordFileEntity::getCreator, userId) + .orderByDesc(CorrectWordFileEntity::getCreatedAt); + correctWordFileDao.selectPage(page, wrapper); + List voList = toVOList(page.getRecords()); + return new PageData<>(voList, page.getTotal()); + } + + @Override + public List listAllFiles() { + Long userId = SecurityUser.getUserId(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(CorrectWordFileEntity::getCreator, userId) + .orderByDesc(CorrectWordFileEntity::getCreatedAt); + List 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 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 getAllItemsByAgentId(String agentId) { + // 通过关联表获取文件ID列表 + List mappings = agentCorrectWordMappingDao.selectByAgentId(agentId); + if (mappings == null || mappings.isEmpty()) { + return new ArrayList<>(); + } + List fileIds = mappings.stream() + .map(AgentCorrectWordMappingEntity::getFileId) + .collect(Collectors.toList()); + + // 根据文件ID列表查询词条 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(CorrectWordItemEntity::getFileId, fileIds); + List entities = correctWordItemDao.selectList(wrapper); + return ConvertUtils.sourceToTarget(entities, CorrectWordSimpleVO.class); + } + + @Override + public List getAgentCorrectWordFileIds(String agentId) { + List 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 fileIds) { + // 先删除旧的关联记录 + agentCorrectWordMappingDao.deleteByAgentId(agentId); + + if (fileIds == null || fileIds.isEmpty()) { + return; + } + + // 批量插入新的关联记录 + Long userId = SecurityUser.getUserId(); + Date now = new Date(); + List 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 fileIds) { + if (fileIds == null || fileIds.isEmpty()) { + return; + } + for (String fileId : fileIds) { + if (fileId == null || fileId.trim().isEmpty()) { + continue; + } + deleteFile(fileId.trim()); + } + } + + /** + * 解析替换词内容,每条格式:原词|替换词 + */ + private List parseContent(List lines) { + List 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 toVOList(List entities) { + if (entities == null || entities.isEmpty()) { + return new ArrayList<>(); + } + return entities.stream().map(this::toVO).collect(Collectors.toList()); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/vo/CorrectWordFileVO.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/vo/CorrectWordFileVO.java new file mode 100644 index 00000000..a881b8d5 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/vo/CorrectWordFileVO.java @@ -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 content; + + @Schema(description = "创建时间") + private Date createdAt; + + @Schema(description = "更新时间") + private Date updatedAt; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/correctword/vo/CorrectWordSimpleVO.java b/main/manager-api/src/main/java/xiaozhi/modules/correctword/vo/CorrectWordSimpleVO.java new file mode 100644 index 00000000..289b8a4e --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/correctword/vo/CorrectWordSimpleVO.java @@ -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; +} diff --git a/main/manager-api/src/main/resources/db/changelog/202604211515.sql b/main/manager-api/src/main/resources/db/changelog/202604211515.sql new file mode 100644 index 00000000..39c4f966 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202604211515.sql @@ -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='智能体替换词文件关联'; diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index dff9e0ea..725831cb 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -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 diff --git a/main/manager-api/src/main/resources/i18n/messages.properties b/main/manager-api/src/main/resources/i18n/messages.properties index 685fdcde..860bd12a 100644 --- a/main/manager-api/src/main/resources/i18n/messages.properties +++ b/main/manager-api/src/main/resources/i18n/messages.properties @@ -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 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_de_DE.properties b/main/manager-api/src/main/resources/i18n/messages_de_DE.properties index 67dc3019..cf70b00c 100644 --- a/main/manager-api/src/main/resources/i18n/messages_de_DE.properties +++ b/main/manager-api/src/main/resources/i18n/messages_de_DE.properties @@ -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 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_en_US.properties b/main/manager-api/src/main/resources/i18n/messages_en_US.properties index bce68ba9..8354dfc8 100644 --- a/main/manager-api/src/main/resources/i18n/messages_en_US.properties +++ b/main/manager-api/src/main/resources/i18n/messages_en_US.properties @@ -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 diff --git a/main/manager-api/src/main/resources/i18n/messages_pt_BR.properties b/main/manager-api/src/main/resources/i18n/messages_pt_BR.properties index b679f253..39926674 100644 --- a/main/manager-api/src/main/resources/i18n/messages_pt_BR.properties +++ b/main/manager-api/src/main/resources/i18n/messages_pt_BR.properties @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties b/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties index c5973226..228b92b1 100644 --- a/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_vi_VN.properties @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties index 9350a892..860bd12a 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties @@ -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 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties index 4bd8fbe8..15b61351 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties @@ -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 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentCorrectWordMappingDao.xml b/main/manager-api/src/main/resources/mapper/agent/AgentCorrectWordMappingDao.xml new file mode 100644 index 00000000..5cc131af --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/agent/AgentCorrectWordMappingDao.xml @@ -0,0 +1,27 @@ + + + + + + DELETE FROM ai_agent_correct_word_mapping WHERE agent_id = #{agentId} + + + + DELETE FROM ai_agent_correct_word_mapping WHERE file_id = #{fileId} + + + + INSERT INTO ai_agent_correct_word_mapping (id, agent_id, file_id, creator, created_at, updater, updated_at) + VALUES + + (#{item.id}, #{item.agentId}, #{item.fileId}, #{item.creator}, #{item.createdAt}, #{item.updater}, #{item.updatedAt}) + + + + + + diff --git a/main/manager-api/src/main/resources/mapper/correctword/CorrectWordItemDao.xml b/main/manager-api/src/main/resources/mapper/correctword/CorrectWordItemDao.xml new file mode 100644 index 00000000..27ea5f40 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/correctword/CorrectWordItemDao.xml @@ -0,0 +1,13 @@ + + + + + + INSERT INTO ai_agent_correct_word_item (id, file_id, source_word, target_word) + VALUES + + (#{item.id}, #{item.fileId}, #{item.sourceWord}, #{item.targetWord}) + + + + \ No newline at end of file diff --git a/main/xiaozhi-server/config/config_loader.py b/main/xiaozhi-server/config/config_loader.py index e0220e33..9c093e58 100644 --- a/main/xiaozhi-server/config/config_loader.py +++ b/main/xiaozhi-server/config/config_loader.py @@ -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): diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py index e6f7c918..7028f975 100644 --- a/main/xiaozhi-server/config/manage_api_client.py +++ b/main/xiaozhi-server/config/manage_api_client.py @@ -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: diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 73e5cba5..4503b825 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -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( diff --git a/main/xiaozhi-server/core/providers/tts/alibl_stream.py b/main/xiaozhi-server/core/providers/tts/alibl_stream.py index f3032b5e..4ab30593 100644 --- a/main/xiaozhi-server/core/providers/tts/alibl_stream.py +++ b/main/xiaozhi-server/core/providers/tts/alibl_stream.py @@ -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": { diff --git a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py index f9581ee8..66a0df9c 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py @@ -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, diff --git a/main/xiaozhi-server/core/providers/tts/base.py b/main/xiaozhi-server/core/providers/tts/base.py index 4c3dd6be..c2333db3 100644 --- a/main/xiaozhi-server/core/providers/tts/base.py +++ b/main/xiaozhi-server/core/providers/tts/base.py @@ -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 diff --git a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py index 0d783754..a6ecc21c 100644 --- a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py +++ b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py @@ -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: # 发送文本 diff --git a/main/xiaozhi-server/core/providers/tts/index_stream.py b/main/xiaozhi-server/core/providers/tts/index_stream.py index 72d2d68d..7314ac9c 100644 --- a/main/xiaozhi-server/core/providers/tts/index_stream.py +++ b/main/xiaozhi-server/core/providers/tts/index_stream.py @@ -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} diff --git a/main/xiaozhi-server/core/providers/tts/linkerai.py b/main/xiaozhi-server/core/providers/tts/linkerai.py index 7de5f3cd..ecfb8b38 100644 --- a/main/xiaozhi-server/core/providers/tts/linkerai.py +++ b/main/xiaozhi-server/core/providers/tts/linkerai.py @@ -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, diff --git a/main/xiaozhi-server/core/providers/tts/minimax_httpstream.py b/main/xiaozhi-server/core/providers/tts/minimax_httpstream.py index b5b0cd37..f620adfb 100644 --- a/main/xiaozhi-server/core/providers/tts/minimax_httpstream.py +++ b/main/xiaozhi-server/core/providers/tts/minimax_httpstream.py @@ -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, diff --git a/main/xiaozhi-server/core/providers/tts/xunfei_stream.py b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py index 6cdb8e19..812c282b 100644 --- a/main/xiaozhi-server/core/providers/tts/xunfei_stream.py +++ b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py @@ -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)