Compare commits

...
Author SHA1 Message Date
dependabot[bot]andGitHub 451ae02167 build(deps): bump aiohttp from 3.13.2 to 3.13.5 in /main/xiaozhi-server
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.13.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 11:43:18 +00:00
wengzhandGitHub 1b5c00231a Merge pull request #3165 from xinnan-tech/fix-chat-total
fix: 修复聊天记录左侧会话数量统计
2026-05-09 15:56:05 +08:00
zhuoqinglian 95acba1126 fix: 修复聊天记录左侧会话数量统计 2026-05-09 15:54:53 +08:00
wengzhandGitHub 1e6f0fd1be Merge pull request #3162 from xinnan-tech/perf-tool-call-optimization
fix:修复音乐播放工具TTS上报记录问题
2026-05-08 16:40:37 +08:00
DaGou12138 eb571fae57 fix:修复音乐播放工具TTS上报记录问题 2026-05-08 16:38:23 +08:00
wengzhandGitHub 2946be7284 Merge pull request #3161 from xinnan-tech/perf-tool-call-optimization
fix:优化天气工具调用提示词
2026-05-08 14:23:11 +08:00
DaGou12138 425cb1d6bd fix:优化天气工具调用提示词,本地上下文存在当前所在地的天气信息无需再度查询工具 2026-05-08 14:20:52 +08:00
wengzhandGitHub cf9b0ec712 Merge pull request #3160 from xinnan-tech/fix-chat-title
fix: 修复聊天记录标题过长展示异常的问题
2026-05-08 11:08:23 +08:00
zhuoqinglian 1b053dc9c8 fix: 修复聊天记录标题过长展示异常的问题 2026-05-08 11:05:20 +08:00
wengzhandGitHub d7ad656bd0 Merge pull request #3159 from xinnan-tech/perf-tool-call-optimization
fix:优化对话过程中JSON闭合符号清理的问题
2026-05-08 10:48:58 +08:00
DaGou12138 7dae67a57b fix:优化对话过程中JSON闭合符号清理的问题 2026-05-08 10:46:37 +08:00
wengzhandGitHub 7b0f59a747 Merge pull request #3158 from xinnan-tech/chore/cleanup
chore: 暂时移除近期不可用或不稳定的TTS供应器及相关配置
2026-05-08 10:38:53 +08:00
DaGou12138 2eaa5bbb11 fix:优化对话过程中异常符号记录的问题 2026-05-08 10:29:50 +08:00
FAN-yeB c693ff1564 update:linkerai 近期多次测试,linkerai接口无法提供稳定服务,先暂时下架这个供应器 2026-05-08 10:18:26 +08:00
DaGou12138andGitHub e9bf006d9d Merge pull request #3154 from haixinke/Bug-get-user-prifile-null
fix:当打开用户画像开关时,修复获取用户画像为空的问题
2026-05-07 16:01:57 +08:00
wengzhandGitHub bef96f36e2 Merge pull request #3157 from xinnan-tech/fix-streaming-tts-original-text
Fix streaming tts original text
2026-05-07 15:36:56 +08:00
wengzhandGitHub bebb7eac69 Merge pull request #3156 from xinnan-tech/perf-tool-call-optimization
perf:优化新闻工具新闻源配置获取的逻辑
2026-05-07 11:43:17 +08:00
DaGou12138 a787130b74 perf:
1、优化新闻工具新闻源获取的逻辑
2、调整新闻工具描述内容
2026-05-07 11:41:17 +08:00
FAN-yeB 8b7f16b9c2 update:TTS_GizwitsTTS 近期机智云语音合成不成功,先暂时下架这个供应器 2026-05-07 11:27:32 +08:00
FAN-yeB 9ed66ec013 update:TTS_ACGNTTS 海豚配音截止5月7日未找到token入口,先暂时下架这个供应器 2026-05-07 10:54:03 +08:00
FAN-yeB 05c22cdf2a update:更新 .gitignore忽略.claude目录 2026-05-07 10:23:02 +08:00
FAN-yeB 4bc0b67cd7 update:去除无用的.claude目录 2026-05-07 10:20:48 +08:00
Sakura-RanChen 0a6f811448 fix: 修复流式情况下替换词失败 2026-05-07 10:20:10 +08:00
wengzhandGitHub 5aeb55fffd Merge pull request #3155 from xinnan-tech/perf-tool-call-optimization
perf:优化工具调用问题、调整后端智能体搜索条件逻辑
2026-05-06 17:01:15 +08:00
DaGou12138 c33ee9442e perf:
1、优化小智后端智能体搜索条件逻辑
2、优化工具调用准确率问题,修改工具提示词,去除非必要的few-shot逻辑
3、优化工具描述内容
2026-05-06 16:53:06 +08:00
minwang 4670ad2925 delete log 2026-05-01 16:03:02 +08:00
minwangandClaude Sonnet 4.6 b542388053 🐛 修复PowerMem用户画像查询返回空的问题
修复 get_user_profile() 方法在缓存未命中时主动从 SDK 获取用户画
像,支持 profile_content 和 topics 两种格式,解决新对话中用户画像无法显示的问题

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 14:57:36 +08:00
rainv123 a71ce53a68 fix:修复使用替换词时,火山引擎双向流式tts的文本显示/上报问题 2026-04-30 17:33:40 +08:00
rainv123 dd88107131 Revert "fix: 修复流式TTS替换词显示/上报问题"
This reverts commit 8b847a3826.
2026-04-30 15:57:03 +08:00
rainv123 8b847a3826 fix: 修复流式TTS替换词显示/上报问题 2026-04-30 14:32:36 +08:00
wengzhandGitHub 036b89f24e Merge pull request #3151 from xinnan-tech/py-refactor-asr-tts
调整配置
2026-04-30 11:42:36 +08:00
Sakura-RanChen 354280d430 调整配置 2026-04-30 11:41:16 +08:00
hrzandGitHub e2d16ec8d4 Merge pull request #3150 from Mirrorium227/main
Closes #Issue3149 修复文档序号
2026-04-30 11:27:15 +08:00
Radar SongandGitHub 6a0192de27 Closes #Issue3149 修复文档序号 2026-04-29 20:18:17 +08:00
hrzandGitHub ac6eeb5c1f Merge pull request #3145 from haixinke/fix-bug_prompt_cache_key_wrong
Fix:【设备快速提示词缓存key错误】
2026-04-29 14:32:42 +08:00
minwang 23e213f6ea 修复设备快速提示词缓存key错误 2026-04-28 15:52:23 +08:00
wengzhandGitHub 382c948482 Merge pull request #3144 from xinnan-tech/fix-memory-config
fix:修复记忆模型配置保存字段未删除问题
2026-04-28 15:25:49 +08:00
DaGou12138 a67d938523 fix:修复记忆模型配置保存字段未删除问题 2026-04-28 15:23:57 +08:00
wengzhandGitHub 26aca54f4b Merge pull request #3141 from xinnan-tech/add-correct-word
fix修复 asyncio.gather 吞掉业务异常的问题
2026-04-27 17:31:47 +08:00
rainv123 4717b8beac fix修复 asyncio.gather 吞掉业务异常的问题 2026-04-27 17:29:41 +08:00
wengzhandGitHub cd580b6f81 Merge pull request #3139 from xinnan-tech/py-fix-mem0ai
fix: 修复Mem0AI记忆模式工具调用时保存记忆400错误
2026-04-27 16:57:26 +08:00
3030332422 70740af7a6 fix: 修复Mem0AI记忆模式工具调用时保存记忆400错误 2026-04-27 16:25:05 +08:00
wengzhandGitHub 7b30c25923 Merge pull request #3132 from xinnan-tech/replacement-word
feat: 新增替换词管理功能
2026-04-27 16:23:54 +08:00
wengzhandGitHub 7db13b8be2 Merge pull request #3130 from xinnan-tech/add-correct-word
add:替换词功能
2026-04-27 16:23:33 +08:00
wengzhandGitHub a0df41ac24 Merge pull request #3136 from xinnan-tech/performance_tester
update:解决base类修改后测速工具兼容性问题
2026-04-27 15:08:26 +08:00
FAN-yeB a432c295d3 update:解决base类修改后测速工具兼容性问题 2026-04-27 15:06:40 +08:00
wengzhandGitHub ab8a35e233 Merge pull request #3135 from xinnan-tech/perf-tool-call-optimization
Perf:优化工具调用准确性、禁用标题总结与记忆模型思考模式、拆分动态上下文
2026-04-27 14:54:16 +08:00
DaGou12138 160b67197f Merge branch 'refs/heads/main' into perf-tool-call-optimization 2026-04-27 14:23:28 +08:00
DaGou12138 151a0c1b99 perf:
1、优化工具调用相关提示词,拆分静态system-prompt和动态上下文。
2、新增few-shot示例,使用"调用→结果→回复"流程闭环,显著提高多次会话后工具调用准确性。
3、去除对话标题总结和记忆的模型思考模式。
2026-04-27 11:08:25 +08:00
zhuoqinglian 52af722463 update: 更新替换词数量限制 2026-04-24 14:54:37 +08:00
rainv123 606a42cfb2 update:替换词功能 2026-04-24 10:06:26 +08:00
zhuoqinglian f85e863144 feat: 新增替换词管理功能 2026-04-23 17:50:57 +08:00
DaGou12138 8c0899415a Merge branch 'refs/heads/main' into perf-tool-call-optimization 2026-04-21 15:02:31 +08:00
DaGou12138 3736a5845a perf:
1、优化提示词context动态变化导致的缓存失效问题,拆分静态system-prompt和动态上下文
2026-04-21 15:01:57 +08:00
DaGou12138 90888de0ba perf:
1、新增工具调用Action枚举RECORD,通过完整的三段式 assistant(tool_calls) → tool → assistant(response)让模型准确学习到工具调用的正确行为,优化play_music的工具调用枚举NONE变更为RECORD,优化解决音乐播放工具调用缺陷问题。
2、处理悬空 tool_calls问题。
3、去除工具提示词强化注入逻辑。
2026-04-21 11:36:06 +08:00
84 changed files with 3735 additions and 840 deletions
+1
View File
@@ -149,6 +149,7 @@ tmp
.history
.DS_Store
main/xiaozhi-server/data
main/xiaozhi-server/.claude
main/manager-web/node_modules
.config.yaml
+1 -1
View File
@@ -418,7 +418,7 @@ manager-api:
secret: 12345678-xxxx-xxxx-xxxx-123456789000
```
## 5.运行项目
## 9.运行项目
```
# 确保在xiaozhi-server目录下执行
@@ -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;
}
@@ -140,40 +146,32 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
// 如果有搜索关键词,根据搜索类型添加相应的查询条件
if (StringUtils.isNotBlank(keyword)) {
if ("mac".equals(searchType)) {
// 按MAC地址搜索:先搜索设备,再获取对应的智能体
queryWrapper.and(w -> {
// 按名称搜索
w.like("agent_name", keyword);
// 按MAC地址搜索:先查设备,再获取对应的智能体ID
List<DeviceEntity> devices = Optional
.ofNullable(deviceService.searchDevicesByMacAddress(keyword, userId)).orElseGet(ArrayList::new);
// 获取设备对应的智能体ID列表
.ofNullable(deviceService.searchDevicesByMacAddress(keyword, userId))
.orElseGet(ArrayList::new);
List<String> agentIds = devices.stream()
.map(DeviceEntity::getAgentId)
.distinct()
.collect(Collectors.toList());
if (ToolUtil.isNotEmpty(agentIds)) {
queryWrapper.in("id", agentIds);
} else {
return new ArrayList<>();
w.or().in("id", agentIds);
}
} else {
// 按名称搜索(默认):同时搜索智能体名称和标签名
// 按标签名搜索
List<String> tagAgentIds = agentTagService.getAgentIdsByTagName(keyword);
if (ToolUtil.isNotEmpty(tagAgentIds)) {
queryWrapper.and(wrapper -> wrapper
.like("agent_name", keyword)
.or()
.in("id", tagAgentIds));
} else {
queryWrapper.like("agent_name", keyword);
w.or().in("id", tagAgentIds);
}
}
});
}
// 执行查询
List<AgentEntity> agentEntities = baseDao.selectList(queryWrapper);
// 转换为DTO并设置所有必要字段
return agentEntities.stream().map(this::buildAgentDTO).collect(Collectors.toList());
}
@@ -427,6 +425,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;
}
@@ -1,6 +1,7 @@
package xiaozhi.modules.llm.service.impl;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -30,11 +31,34 @@ import xiaozhi.modules.model.service.ModelConfigService;
@Service
public class OpenAIStyleLLMServiceImpl implements LLMService {
// 需要禁用思考模式的平台域名及其对应参数
private static final Map<String, Map<String, Object>> THINKING_DISABLED_DOMAINS = new LinkedHashMap<>();
static {
THINKING_DISABLED_DOMAINS.put("aliyuncs.com", Map.of("enable_thinking", false));
Map<String, Object> thinkingDisabled = Map.of("thinking", Map.of("type", "disabled"));
THINKING_DISABLED_DOMAINS.put("bigmodel.cn", thinkingDisabled);
THINKING_DISABLED_DOMAINS.put("moonshot.cn", thinkingDisabled);
THINKING_DISABLED_DOMAINS.put("volces.com", thinkingDisabled);
}
@Autowired
private ModelConfigService modelConfigService;
private final RestTemplate restTemplate = new RestTemplate();
/**
* 根据域名自动禁用思考模式
*/
private void applyThinkingDisabled(String baseUrl, Map<String, Object> requestBody) {
for (Map.Entry<String, Map<String, Object>> entry : THINKING_DISABLED_DOMAINS.entrySet()) {
if (baseUrl.contains(entry.getKey())) {
requestBody.putAll(entry.getValue());
log.info("为域名 {} 禁用思考模式,参数: {}", baseUrl, entry.getValue());
break;
}
}
}
private static final String DEFAULT_SUMMARY_PROMPT = "你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:\n1、总结用户的重要信息,以便在未来的对话中提供更个性化的服务\n2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字,否则不要遗忘、不要压缩用户的历史记忆\n3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中\n4、聊天内容中的今天的日期时间、今天的天气情况与用户事件无关的数据,这些信息如果当成记忆存储会影响后续对话,这些信息不需要加入到总结中\n5、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中\n6、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的\n7、只需要返回总结摘要,严格控制在1800字内\n8、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容\n9、如果提供了历史记忆,请将新对话内容与历史记忆进行智能合并,保留有价值的历史信息,同时添加新的重要信息\n\n历史记忆:\n{history_memory}\n\n新对话内容:\n{conversation}";
private static final String DEFAULT_TITLE_PROMPT = "请根据以下对话内容,生成一个简洁的会话标题(约15字以内),只返回标题,不要包含任何解释或标点符号:\n{conversation}";
@@ -102,6 +126,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
requestBody.put("temperature", temperature != null ? temperature : 0.7);
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
// 禁用思考模式
applyThinkingDisabled(baseUrl, requestBody);
// 发送HTTP请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
@@ -156,10 +183,8 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
// 从智控台获取LLM模型配置
ModelConfigEntity llmConfig;
if (modelId != null && !modelId.trim().isEmpty()) {
// 通过具体模型ID获取配置
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
} else {
// 保持向后兼容,使用默认配置
llmConfig = getDefaultLLMConfig();
}
@@ -197,6 +222,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
requestBody.put("temperature", 0.2);
requestBody.put("max_tokens", 2000);
// 禁用思考模式
applyThinkingDisabled(baseUrl, requestBody);
// 发送HTTP请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
@@ -350,6 +378,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
requestBody.put("temperature", 0.3);
requestBody.put("max_tokens", 50);
// 禁用思考模式
applyThinkingDisabled(baseUrl, requestBody);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + apiKey);
@@ -371,6 +371,13 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
}
}
// 删除在新JSON中不存在的非敏感字段
for (String oldKey : originalJson.keySet().toArray(new String[0])) {
if (!modelConfigBodyDTO.getConfigJson().containsKey(oldKey) && !SensitiveDataUtils.isSensitiveField(oldKey)) {
updatedJson.remove(oldKey);
}
}
modelConfigEntity.setConfigJson(updatedJson);
}
@@ -403,6 +410,13 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
}
}
}
// 删除在新JSON中不存在的非敏感子字段
for (String oldChildKey : originalChild.keySet().toArray(new String[0])) {
if (!updated.containsKey(oldChildKey) && !SensitiveDataUtils.isSensitiveField(oldChildKey)) {
originalChild.remove(oldChildKey);
}
}
}
/**
@@ -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='智能体替换词文件关联';
@@ -0,0 +1,57 @@
-- 修改模型名称
UPDATE `ai_model_config` SET `model_name` = '豆包语音识别2.0(流式)' WHERE `id` = 'ASR_DoubaoStreamASRV2';
UPDATE `ai_model_config` SET `model_name` = '豆包语音合成2.0(流式)' WHERE `id` = 'TTS_HSDSTTS_V2';
-- 调整模型音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_HSDSTTS_V2';
-- 通用场景 and 视频配音
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0001', 'TTS_HSDSTTS_V2', '湾湾小何', 'zh_female_xiaohe_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_xiaohe_uranus_bigtts.mp3', NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0002', 'TTS_HSDSTTS_V2', 'Vivi', 'zh_female_vv_uranus_bigtts', '普通话、日语、印尼语、墨西哥西班牙语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_vv_uranus_bigtts.wav', NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0003', 'TTS_HSDSTTS_V2', '可爱女生', 'saturn_zh_female_keainvsheng_tob', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/saturn_zh_female_keainvsheng_tob.wav', NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0004', 'TTS_HSDSTTS_V2', '调皮公主', 'saturn_zh_female_tiaopigongzhu_tob', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/saturn_zh_female_tiaopigongzhu_tob.wav', NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0005', 'TTS_HSDSTTS_V2', '阳光青年', 'zh_male_yangguangqingnian_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_yangguangqingnian_uranus_bigtts.mp3', NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0006', 'TTS_HSDSTTS_V2', '爽朗少年', 'saturn_zh_male_shuanglangshaonian_tob', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/saturn_zh_male_shuanglangshaonian_tob.wav', NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0007', 'TTS_HSDSTTS_V2', '爽朗少年', 'saturn_zh_male_tiancaitongzhuo_tob', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/saturn_zh_male_tiancaitongzhuo_tob.wav', NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0008', 'TTS_HSDSTTS_V2', '大壹', 'zh_male_dayi_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_dayi_saturn_bigtts.wav', NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0009', 'TTS_HSDSTTS_V2', '黑猫侦探社咪仔', 'zh_female_mizai_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_mizai_saturn_bigtts.wav', NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0010', 'TTS_HSDSTTS_V2', '鸡汤女', 'zh_female_jitangnv_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_jitangnv_saturn_bigtts.wav', NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0011', 'TTS_HSDSTTS_V2', '魅力女友', 'zh_female_meilinvyou_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_meilinvyou_saturn_bigtts.wav', NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0012', 'TTS_HSDSTTS_V2', '流畅女声', 'zh_female_liuchangnv_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B5%81%E7%95%85%E5%A5%B3%E5%A3%B0.wav', NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0013', 'TTS_HSDSTTS_V2', '儒雅逸辰', 'zh_male_ruyayichen_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_ruyayichen_saturn_bigtts.wav', NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0014', 'TTS_HSDSTTS_V2', '云舟', 'zh_male_m191_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_m191_uranus_bigtts.mp3', NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0015', 'TTS_HSDSTTS_V2', '小天', 'zh_male_taocheng_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_taocheng_uranus_bigtts.mp3', NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0016', 'TTS_HSDSTTS_V2', '刘飞', 'zh_male_liufei_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_liufei_uranus_bigtts.mp3', NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0017', 'TTS_HSDSTTS_V2', '魅力苏菲', 'zh_female_sophie_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_sophie_uranus_bigtts.mp3', NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0018', 'TTS_HSDSTTS_V2', '清新女声', 'zh_female_qingxinnvsheng_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_qingxinnvsheng_uranus_bigtts.mp3', NULL, NULL, NULL, 18, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0019', 'TTS_HSDSTTS_V2', '知性灿灿', 'zh_female_cancan_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_cancan_uranus_bigtts.mp3', NULL, NULL, NULL, 19, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0020', 'TTS_HSDSTTS_V2', '撒娇学妹', 'zh_female_sajiaoxuemei_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_sajiaoxuemei_uranus_bigtts.mp3', NULL, NULL, NULL, 20, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0021', 'TTS_HSDSTTS_V2', '甜美小源', 'zh_female_tianmeixiaoyuan_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_tianmeixiaoyuan_uranus_bigtts.mp3', NULL, NULL, NULL, 21, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0022', 'TTS_HSDSTTS_V2', '甜美桃子', 'zh_female_tianmeitaozi_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_tianmeitaozi_uranus_bigtts.mp3', NULL, NULL, NULL, 22, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0023', 'TTS_HSDSTTS_V2', '邻家女孩', 'zh_female_linjianvhai_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_linjianvhai_uranus_bigtts.mp3', NULL, NULL, NULL, 23, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0024', 'TTS_HSDSTTS_V2', '爽快思思', 'zh_female_shuangkuaisisi_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_shuangkuaisisi_uranus_bigtts.mp3', NULL, NULL, NULL, 24, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0025', 'TTS_HSDSTTS_V2', '少年梓辛/Brayan', 'zh_male_shaonianzixin_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_shaonianzixin_uranus_bigtts.mp3', NULL, NULL, NULL, 25, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0026', 'TTS_HSDSTTS_V2', '温暖阿虎/Alvin', 'zh_male_wennuanahu_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_wennuanahu_uranus_bigtts.mp3', NULL, NULL, NULL, 26, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0027', 'TTS_HSDSTTS_V2', '奶气萌娃', 'zh_male_naiqimengwa_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_naiqimengwa_uranus_bigtts.mp3', NULL, NULL, NULL, 27, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0028', 'TTS_HSDSTTS_V2', '婆婆', 'zh_female_popo_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_popo_uranus_bigtts.mp3', NULL, NULL, NULL, 28, NULL, NULL, NULL, NULL);
-- 客服场景
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0029', 'TTS_HSDSTTS_V2', '轻盈朵朵', 'saturn_zh_female_qingyingduoduo_cs_tob', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/ICL_zh_female_qingyingduoduo_cs_tob.mp3', NULL, NULL, NULL, 29, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0030', 'TTS_HSDSTTS_V2', '温婉珊珊', 'saturn_zh_female_wenwanshanshan_cs_tob', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/ICL_zh_female_wenwanshanshan_cs_tob.mp3', NULL, NULL, NULL, 30, NULL, NULL, NULL, NULL);
-- 多语种
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0031', 'TTS_HSDSTTS_V2', 'Tim', 'en_male_tim_uranus_bigtts', '英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/en_male_tim_uranus_bigtts.mp3', NULL, NULL, NULL, 31, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0032', 'TTS_HSDSTTS_V2', 'Dacey', 'en_female_dacey_uranus_bigtts', '英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/en_female_dacey_uranus_bigtts.mp3', NULL, NULL, NULL, 32, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0033', 'TTS_HSDSTTS_V2', 'Stokie', 'en_female_stokie_uranus_bigtts', '英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/en_female_stokie_uranus_bigtts.mp3', NULL, NULL, NULL, 33, NULL, NULL, NULL, NULL);
-- 有声阅读
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0034', 'TTS_HSDSTTS_V2', '古风少御', 'zh_female_gufengshaoyu_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_gufengshaoyu_uranus_bigtts.mp3', NULL, NULL, NULL, 34, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0035', 'TTS_HSDSTTS_V2', '霸气青叔', 'zh_male_baqiqingshu_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_baqiqingshu_uranus_bigtts.mp3', NULL, NULL, NULL, 35, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0036', 'TTS_HSDSTTS_V2', '悬疑解说', 'zh_male_xuanyijieshuo_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_xuanyijieshuo_uranus_bigtts.mp3', NULL, NULL, NULL, 36, NULL, NULL, NULL, NULL);
-- 角色扮演
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0037', 'TTS_HSDSTTS_V2', '佩奇猪', 'zh_female_peiqi_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_female_peiqi_uranus_bigtts.mp3', NULL, NULL, NULL, 37, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0038', 'TTS_HSDSTTS_V2', '猴哥', 'zh_male_sunwukong_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_sunwukong_uranus_bigtts.mp3', NULL, NULL, NULL, 38, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0039', 'TTS_HSDSTTS_V2', '猪八戒', 'zh_male_zhubajie_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_zhubajie_uranus_bigtts.mp3', NULL, NULL, NULL, 39, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_HSDSTTS_V2_0040', 'TTS_HSDSTTS_V2', '唐僧', 'zh_male_tangseng_uranus_bigtts', '普通话、英语', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/zh_male_tangseng_uranus_bigtts.mp3', NULL, NULL, NULL, 40, NULL, NULL, NULL, NULL);
@@ -0,0 +1,5 @@
-- 删除provider_code为ttson的供应器
DELETE FROM `ai_model_provider` WHERE `provider_code` = 'ttson';
-- 删除model_code为ACGNTTS的配置
DELETE FROM `ai_model_config` WHERE `model_code` = 'ACGNTTS';
@@ -0,0 +1,6 @@
-- 删除model_code为GizwitsTTS的配置
DELETE FROM `ai_model_config` WHERE `model_code` = 'GizwitsTTS';
-- 删除关联的TTS音色记录
DELETE FROM `ai_tts_voice` WHERE `tts_model_id` = 'TTS_GizwitsTTS';
DELETE FROM `ai_tts_voice` WHERE `tts_model_id` = 'TTS_ACGNTTS';
@@ -0,0 +1,8 @@
-- 删除provider_code为linkerai的供应商配置
DELETE FROM `ai_model_provider` WHERE `provider_code` = 'linkerai';
-- 删除model_code为LinkeraiTTS的模型配置
DELETE FROM `ai_model_config` WHERE `model_code` = 'LinkeraiTTS';
-- 删除LinkeraiTTS关联的TTS音色记录
DELETE FROM `ai_tts_voice` WHERE `tts_model_id` = 'TTS_LinkeraiTTS';
@@ -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
@@ -627,3 +634,31 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202604211700.sql
- changeSet:
id: 202604300930
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202604300930.sql
- changeSet:
id: 202605071037
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202605071037.sql
- changeSet:
id: 202605071120
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202605071120.sql
- changeSet:
id: 202605081008
author: fyb
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202605081008.sql
@@ -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>
+3 -1
View File
@@ -10,6 +10,7 @@ import user from './module/user.js'
import voiceClone from './module/voiceClone.js'
import voiceResource from './module/voiceResource.js'
import knowledgeBase from './module/knowledgeBase.js'
import correctWord from './module/correctWord.js'
@@ -41,5 +42,6 @@ export default {
dict,
voiceResource,
voiceClone,
knowledgeBase
knowledgeBase,
correctWord
}
@@ -0,0 +1,129 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 获取替换词文件列表
getFileList(params, callback) {
const queryParams = new URLSearchParams({
page: params.page,
pageSize: params.pageSize
}).toString();
RequestService.sendRequest()
.url(`${getServiceUrl()}/correct-word/file/list?${queryParams}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.networkFail((err) => {
console.error('获取替换词文件列表失败:', err)
RequestService.reAjaxFun(() => {
this.getFileList(params, callback)
})
}).send()
},
// 获取所有替换词文件(不分页)
selectAll(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/correct-word/file/select`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.networkFail((err) => {
console.error('获取所有替换词文件失败:', err)
RequestService.reAjaxFun(() => {
this.selectAll(callback)
})
}).send()
},
// 下载替换词文件
downloadFile(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/correct-word/file/download/${id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
RequestService.clearRequestTime()
callback(err)
}).send()
},
// 新增替换词文件
addFile(data, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/correct-word/file`)
.method('POST')
.data(data)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
RequestService.clearRequestTime()
callback(err)
}).send()
},
// 更新替换词文件
updateFile(data, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/correct-word/file/${data.id}`)
.method('PUT')
.data({
fileName: data.fileName,
content: data.content
})
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
RequestService.clearRequestTime()
callback(err)
}).send()
},
// 删除替换词文件
deleteFile(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/correct-word/file/${id}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.networkFail((err) => {
console.error('删除替换词文件失败:', err)
RequestService.reAjaxFun(() => {
this.deleteFile(id, callback)
})
}).send()
},
// 批量删除替换词文件
batchDeleteFile(ids, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/correct-word/file/batch-delete`)
.method('POST')
.data(ids)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.networkFail((err) => {
console.error('批量删除替换词文件失败:', err)
RequestService.reAjaxFun(() => {
this.batchDeleteFile(ids, callback)
})
}).send()
}
}
@@ -267,6 +267,13 @@ export default {
if (this.messages.length > 0 && this.messages[0].macAddress) {
this.currentMacAddress = this.messages[0].macAddress;
}
// 更新会话列表中的聊天记录数量
this.sessions = this.sessions.map(item => {
if (item.sessionId === session.sessionId) {
item.chatCount = this.messages.length;
}
return item;
})
}
});
},
@@ -426,7 +433,7 @@ export default {
}
.session-info {
flex: 1;
width: calc(100% - 50px);
}
.session-time {
+37 -58
View File
@@ -2,7 +2,7 @@
<el-header class="header">
<div class="header-container">
<!-- 左侧元素 -->
<div class="header-left" @click="goHome">
<div class="header-left" @click="handleRouter('home')">
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img" />
<img loading="lazy" alt="" :src="xiaozhiAiIcon" class="brand-img" />
</div>
@@ -14,7 +14,7 @@
$route.path === '/home' ||
$route.path === '/role-config' ||
$route.path === '/device-management',
}" @click="goHome">
}" @click="handleRouter('home')">
<img loading="lazy" alt="" src="@/assets/header/robot.png" :style="{
filter:
$route.path === '/home' ||
@@ -27,7 +27,7 @@
</div>
<!-- 普通用户显示音色克隆 -->
<div v-if="!userInfo.superAdmin && featureStatus.voiceClone" class="equipment-management"
:class="{ 'active-tab': $route.path === '/voice-clone-management' }" @click="goVoiceCloneManagement">
:class="{ 'active-tab': $route.path === '/voice-clone-management' }" @click="handleRouter('voiceCloneManagement')">
<img loading="lazy" alt="" src="@/assets/header/voice.png" :style="{
filter:
$route.path === '/voice-clone-management'
@@ -55,17 +55,17 @@
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': voiceCloneDropdownVisible }"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="goVoiceCloneManagement">
<el-dropdown-item @click.native="handleRouter('voiceCloneManagement')">
{{ $t("header.voiceCloneManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goVoiceResourceManagement">
<el-dropdown-item @click.native="handleRouter('voiceResourceManagement')">
{{ $t("header.voiceResourceManagement") }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div v-if="userInfo.superAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
@click="goModelConfig">
@click="handleRouter('modelConfig')">
<img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{
filter:
$route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None',
@@ -74,7 +74,7 @@
</div>
<div v-if="featureStatus.knowledgeBase" class="equipment-management"
:class="{ 'active-tab': $route.path === '/knowledge-base-management' || $route.path === '/knowledge-file-upload' }"
@click="goKnowledgeBaseManagement">
@click="handleRouter('knowledgeBaseManagement')">
<img loading="lazy" alt="" src="@/assets/header/knowledge_base.png" :style="{
filter:
$route.path === '/knowledge-base-management' || $route.path === '/knowledge-file-upload' ? 'brightness(0) invert(1)' : 'None',
@@ -110,28 +110,31 @@
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="goParamManagement">
<el-dropdown-item @click.native="handleRouter('paramManagement')">
{{ $t("header.paramManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goUserManagement">
<el-dropdown-item @click.native="handleRouter('userManagement')">
{{ $t("header.userManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goOtaManagement">
<el-dropdown-item @click.native="handleRouter('otaManagement')">
{{ $t("header.otaManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goDictManagement">
<el-dropdown-item @click.native="handleRouter('dictManagement')">
{{ $t("header.dictManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goProviderManagement">
<el-dropdown-item @click.native="handleRouter('providerManagement')">
{{ $t("header.providerManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goAgentTemplateManagement">
<el-dropdown-item @click.native="handleRouter('agentTemplate')">
{{ $t("header.agentTemplate") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goServerSideManagement">
<el-dropdown-item @click.native="handleRouter('replacementWordManagement')">
{{ $t("header.replacementWordManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="handleRouter('serverSideManagement')">
{{ $t("header.serverSideManagement") }}
</el-dropdown-item>
<el-dropdown-item @click.native="goFeatureManagement">
<el-dropdown-item @click.native="handleRouter('featureManagement')">
{{ $t("header.featureManagement") }}
</el-dropdown-item>
</el-dropdown-menu>
@@ -220,6 +223,23 @@ export default {
label: "label",
children: "children",
},
// 跳转页面配置
routerPaths: {
home: "/home",
modelConfig: "/model-config",
knowledgeBaseManagement: "/knowledge-base-management",
voiceCloneManagement: "/voice-clone-management",
voiceResourceManagement: "/voice-resource-management",
paramManagement: "/params-management",
userManagement: "/user-management",
otaManagement: "/ota-management",
dictManagement: "/dict-management",
providerManagement: "/provider-management",
agentTemplate: "/agent-template-management",
replacementWordManagement: "/replacement-word-management",
serverSideManagement: "/server-side-management",
featureManagement: "/feature-management",
}
};
},
computed: {
@@ -331,49 +351,8 @@ export default {
window.removeEventListener("resize", this.checkScreenSize);
},
methods: {
goHome() {
// 跳转到首页
this.$router.push("/home");
},
goUserManagement() {
this.$router.push("/user-management");
},
goModelConfig() {
this.$router.push("/model-config");
},
goKnowledgeBaseManagement() {
this.$router.push("/knowledge-base-management");
},
goVoiceCloneManagement() {
this.$router.push("/voice-clone-management");
},
goParamManagement() {
this.$router.push("/params-management");
},
goOtaManagement() {
this.$router.push("/ota-management");
},
goDictManagement() {
this.$router.push("/dict-management");
},
goProviderManagement() {
this.$router.push("/provider-management");
},
goServerSideManagement() {
this.$router.push("/server-side-management");
},
// 跳转到音色资源管理
goVoiceResourceManagement() {
this.$router.push("/voice-resource-management");
},
// 添加默认角色模板管理导航方法
goAgentTemplateManagement() {
this.$router.push("/agent-template-management");
},
// 跳转到功能管理
goFeatureManagement() {
this.$router.push("/feature-management");
handleRouter(type) {
this.$router.push(this.routerPaths[type]);
},
// 加载功能状态
async loadFeatureStatus() {
@@ -0,0 +1,536 @@
<template>
<el-dialog :title="title" :visible.sync="visible" width="700px" class="param-dialog-wrapper" :append-to-body="true"
:close-on-click-modal="false" :key="dialogKey" custom-class="custom-param-dialog" :show-close="false">
<div class="dialog-container">
<div class="dialog-header">
<h2 class="dialog-title">{{ title }}</h2>
<button class="custom-close-btn" @click="cancel">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
</div>
<el-form :model="localForm" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
<el-form-item :label="$t('replacementDialog.fileName')" prop="fileName" class="form-item">
<el-input v-model="localForm.fileName" :placeholder="$t('replacementDialog.fileNamePlaceholder')"
class="custom-input" @input="clearFieldError('fileName')"></el-input>
</el-form-item>
<el-form-item :label="$t('replacementDialog.content')" prop="content" class="form-item content-item">
<div class="content-wrapper">
<el-input
type="textarea"
v-model="localForm.content"
:placeholder="$t('replacementDialog.contentPlaceholder')"
:rows="8"
class="custom-textarea"
@input="clearFieldError('content')"
></el-input>
<p class="format-tip">{{ $t('replacementDialog.formatTip') }}</p>
<div class="upload-section">
<el-upload
class="upload-btn"
action=""
:auto-upload="false"
:show-file-list="false"
accept=".txt"
:on-change="handleFileChange"
>
<el-button size="small" type="primary" class="upload-file-btn">
<div class="upload-file-content">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
<path d="M7 1V13M1 7H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<div>
<p>{{ $t('replacementDialog.clickUploadTip') }}</p>
<p>{{ $t('replacementDialog.uploadCoverTip') }}</p>
</div>
</div>
</el-button>
</el-upload>
<span class="word-count" :class="{ 'over-limit': isOverLimit }">
{{ wordCountText }}{{ $t('replacementDialog.wordCountUnit') }}
</span>
</div>
</div>
</el-form-item>
</el-form>
<div class="dialog-footer">
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
{{ $t('replacementDialog.save') }}
</el-button>
<el-button @click="cancel" class="cancel-btn">
{{ $t('replacementDialog.cancel') }}
</el-button>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '新增替换词'
},
visible: {
type: Boolean,
default: false
},
form: {
type: Object,
default: () => ({
id: undefined,
fileName: '',
content: ''
})
}
},
data() {
const MAX_WORD_COUNT = 4000;
const validateContent = (rule, value, callback) => {
if (!value || !value.trim()) {
callback(new Error(this.$t('replacementDialog.requiredContent')));
return;
}
const lines = this.getValidLines(value);
const lineCount = lines.length;
if (lineCount > MAX_WORD_COUNT) {
callback(new Error(this.$t('replacementDialog.maxWordCountExceeded', { max: MAX_WORD_COUNT })));
return;
}
for (let i = 0; i < lines.length; i++) {
const pipeCount = (lines[i].match(/\|/g) || []).length;
if (pipeCount !== 1) {
callback(new Error(this.$t('replacementDialog.invalidPipeCount', { line: i + 1 })));
return;
}
const parts = lines[i].split('|');
if (!parts[0] || !parts[0].trim()) {
callback(new Error(this.$t('replacementDialog.emptyOriginal', { line: i + 1 })));
return;
}
if (!parts[1] || !parts[1].trim()) {
callback(new Error(this.$t('replacementDialog.emptyReplacement', { line: i + 1 })));
return;
}
const specialCharRegex = /[!@#$%^&*()_+=\[\]{};':"\\<>?\/`~]/;
if (specialCharRegex.test(parts[0])) {
callback(new Error(this.$t('replacementDialog.invalidOriginalChar', { line: i + 1 })));
return;
}
if (specialCharRegex.test(parts[1])) {
callback(new Error(this.$t('replacementDialog.invalidReplacementChar', { line: i + 1 })));
return;
}
}
callback();
};
return {
dialogKey: Date.now(),
saving: false,
localForm: {
id: undefined,
fileName: '',
content: ''
},
maxWordCount: MAX_WORD_COUNT,
rules: {
fileName: [
{ required: true, message: this.$t('replacementDialog.requiredFileName'), trigger: "blur" }
],
content: [
{ required: true, validator: validateContent, trigger: "blur" }
]
}
};
},
computed: {
wordCount() {
if (!this.localForm.content) return 0;
const contentStr = Array.isArray(this.localForm.content)
? this.localForm.content.join('\n')
: this.localForm.content;
if (!contentStr.trim()) {
return 0;
}
const lines = this.getValidLines();
return lines.filter(line => line.includes('|')).length;
},
isOverLimit() {
return this.wordCount > this.maxWordCount;
},
wordCountText() {
return `${this.wordCount} / ${this.maxWordCount}`;
}
},
methods: {
getValidLines() {
if (!this.localForm.content) return [];
const contentStr = Array.isArray(this.localForm.content)
? this.localForm.content.join('\n')
: this.localForm.content;
return contentStr.split(/\r?\n/).filter(line => line.trim());
},
clearFieldError(field) {
if (this.$refs.form) {
this.$refs.form.clearValidate(field);
}
},
handleFileChange(file) {
if (!file) return;
const rawFile = file.raw;
if (!rawFile) return;
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
this.localForm.content = content;
const lines = this.getValidLines(content);
if (lines.length > this.maxWordCount) {
this.$message.warning(
this.$t('replacementDialog.maxWordCountExceeded', { max: this.maxWordCount })
);
}
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate('content');
}
});
};
reader.onerror = () => {
this.$message.error(this.$t('replacementDialog.readFileError'));
};
reader.readAsText(rawFile);
},
submit() {
this.$refs.form.validate((valid) => {
if (valid) {
if (this.isOverLimit) {
this.$message.error(
this.$t('replacementDialog.maxWordCountExceeded', { max: this.maxWordCount })
);
return;
}
const submitData = {
id: this.localForm.id,
fileName: this.localForm.fileName,
content: this.getValidLines()
};
this.saving = true;
this.$emit('submit', submitData);
}
});
},
cancel() {
this.saving = false;
this.dialogKey = Date.now();
this.$emit('cancel');
},
resetSaving() {
this.saving = false;
}
},
watch: {
visible(newVal) {
if (newVal) {
this.localForm.id = this.form.id;
this.localForm.fileName = this.form.fileName || '';
const contentData = this.form.content;
this.localForm.content = Array.isArray(contentData) ? contentData.join('\n') : (contentData || '');
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
});
} else {
this.saving = false;
}
}
}
};
</script>
<style>
.custom-param-dialog {
border-radius: 16px !important;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
border: none !important;
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0 !important;
border-radius: 16px;
}
}
</style>
<style scoped lang="scss">
.param-dialog-wrapper {
.dialog-container {
padding: 24px 32px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.dialog-header {
position: relative;
margin-bottom: 24px;
text-align: center;
}
.dialog-title {
font-size: 20px;
color: #1e293b;
margin: 0;
padding: 0;
font-weight: 600;
letter-spacing: 0.5px;
}
.custom-close-btn {
position: absolute;
top: -8px;
right: -8px;
width: 32px;
height: 32px;
border-radius: 50%;
border: none;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
outline: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
color: #ffffff;
background: #ef4444;
transform: rotate(90deg);
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.2);
}
svg {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.param-form {
.form-item {
margin-bottom: 20px;
:deep(.el-form-item__label) {
color: #475569;
font-weight: 500;
padding-right: 12px;
text-align: right;
font-size: 14px;
letter-spacing: 0.2px;
}
}
.content-item {
:deep(.el-form-item__content) {
line-height: 1;
}
}
.custom-input {
:deep(.el-input__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
height: 42px;
padding: 0 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.content-wrapper {
width: 100%;
}
.custom-textarea {
:deep(.el-textarea__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
padding: 12px 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
line-height: 1.8;
resize: none;
white-space: pre;
overflow-x: auto;
word-wrap: normal;
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.format-tip {
font-size: 12px;
color: #5778ff;
}
.upload-section {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12px;
}
.upload-btn {
display: inline-block;
}
.upload-file-btn {
background: #3b82f6;
border-color: #3b82f6;
border-radius: 6px;
font-size: 13px;
padding: 8px 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
.upload-file-content {
display: flex;
flex-direction: row;
align-items: center;
> div {
text-align: left;
> p {
margin: 4px;
}
}
}
&:hover {
background: #2563eb;
border-color: #2563eb;
transform: translateY(-1px);
}
}
.word-count {
font-size: 13px;
color: #64748b;
font-weight: 500;
transition: color 0.3s ease;
&.over-limit {
color: #ef4444;
font-weight: 600;
}
}
}
.dialog-footer {
display: flex;
justify-content: center;
padding: 16px 0 0;
margin-top: 16px;
.save-btn {
width: 120px;
height: 42px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #3b82f6;
color: white;
border: none;
letter-spacing: 0.5px;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
&:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 3px rgba(59, 130, 246, 0.2);
}
}
.cancel-btn {
width: 120px;
height: 42px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #ffffff;
color: #64748b;
border: 1px solid #e2e8f0;
margin-left: 16px;
letter-spacing: 0.5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:hover {
background: #f8fafc;
color: #475569;
border-color: #cbd5e1;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
}
}
</style>
@@ -57,9 +57,32 @@
class="tts-slider"
/>
<span class="slider-hint">{{ $t('roleConfig.pitchHint') }}</span>
</div>
</div>
</el-form-item>
</el-form>
<!-- 关联替换词 -->
<div>
<h4 class="replacement-label">
{{ $t('roleConfig.replacementWordLabel') }}
<el-tooltip popper-class="tts-tooltip" :content="$t('roleConfig.replacementWordTip')" effect="light" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</h4>
<el-select
v-model="replacementWordIds"
multiple
:placeholder="$t('replacementDialog.selectPlaceholder')"
class="replacement-word-select"
>
<el-option
v-for="item in replacementWordList"
:key="item.id"
:label="item.fileName"
:value="item.id"
/>
</el-select>
</div>
</div>
<div class="drawer-footer">
@@ -70,6 +93,8 @@
</template>
<script>
import correctWord from '@/apis/module/correctWord';
export default {
name: 'TtsAdvancedSettings',
props: {
@@ -84,15 +109,21 @@ export default {
speed: 0,
pitch: 0
})
}
},
checkedReplacementWordIds: {
type: Array,
default: () => []
},
},
data() {
return {
localSettings: {
volume: 0,
speed: 0,
pitch: 0
}
pitch: 0,
},
replacementWordIds: [],
replacementWordList: []
};
},
computed: {
@@ -110,6 +141,8 @@ export default {
if (newVal) {
// 当抽屉打开时,复制当前设置到本地
this.localSettings = { ...this.settings };
this.replacementWordIds = [...this.checkedReplacementWordIds];
this.fetchReplacementWordList();
}
}
},
@@ -123,11 +156,22 @@ export default {
},
handleSave() {
// 保存设置并关闭
this.$emit('save', { ...this.localSettings });
this.$emit('save', { ...this.localSettings, replacementWordIds: this.replacementWordIds });
this.handleClose();
},
formatTooltip(val) {
return `${val}%`;
},
fetchReplacementWordList() {
correctWord.selectAll(({ data }) => {
if (data.code === 0) {
this.replacementWordList = data.data;
}
});
},
getTagName(id) {
const item = this.replacementWordList.find(item => item.id === id);
return item ? item.fileName : '';
}
}
};
@@ -224,6 +268,30 @@ export default {
::v-deep .el-form-item {
margin-bottom: 24px;
}
.replacement-label i {
margin-left: 4px;
color: #909399;
cursor: pointer;
font-size: 14px;
}
.replacement-label i:hover {
color: #409eff;
}
.replacement-word-select ::v-deep .el-tag {
background: #e6ebff;
color: #5778ff;
border-radius: 8px;
font-size: 12px;
font-weight: normal;
border: none;
}
.replacement-word-select ::v-deep .el-tag__close {
color: #5778ff;
}
.replacement-word-select ::v-deep .el-icon-close {
background: #fff;
}
</style>
<style>
@@ -237,4 +305,45 @@ export default {
flex-direction: column;
padding: 0;
}
.replacement-word-select {
width: 100%;
margin-bottom: 12px;
}
.replacement-label {
text-align: left;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.replacement-tag {
background-color: #409eff;
border-color: #409eff;
color: #fff;
}
.tts-tooltip {
max-height: 60vh !important;
max-width: 400px !important;
overflow-y: auto !important;
scrollbar-width: thin;
word-break: break-word;
}
.tts-tooltip .popper__arrow {
display: none !important;
}
.tts-tooltip[x-placement^="top"] .popper__arrow {
border-top-color: transparent !important;
}
.tts-tooltip[x-placement^="bottom"] .popper__arrow {
border-bottom-color: transparent !important;
}
</style>
+53 -1
View File
@@ -22,6 +22,7 @@ export default {
'header.clearHistory': 'Verlauf löschen',
'header.providerManagement': 'Anbieterverwaltung',
'header.serverSideManagement': 'Serververwaltung',
'header.replacementWordManagement': 'Ersetzungswortverwaltung',
'header.featureManagement': 'Systemfunktionsverwaltung',
'header.changePassword': 'Passwort ändern',
'header.logout': 'Abmelden',
@@ -349,6 +350,54 @@ export default {
'paramDialog.arrayType': 'Array',
'paramDialog.jsonType': 'JSON-Objekt',
// Replacement word management page related
'replacementWordManagement.pageTitle': 'Ersetzungswortverwaltung',
'replacementWordManagement.fileName': 'Dateiname',
'replacementWordManagement.replacementWordCount': 'Anzahl der Ersetzungswörter',
'replacementWordManagement.replacementWordContent': 'Ersetzungswortinhalt',
'replacementWordManagement.operation': 'Operation',
'replacementWordManagement.createTime': 'Erstellungszeit',
'replacementWordManagement.updateTime': 'Aktualisierungszeit',
'replacementWordManagement.addFile': 'Ersetzungsdatei hinzufügen',
'replacementWordManagement.edit': 'Bearbeiten',
'replacementWordManagement.download': 'Herunterladen',
'replacementWordManagement.delete': 'Löschen',
// Replacement word dialog related
'replacementDialog.fileName': 'Dateiname',
'replacementDialog.fileNamePlaceholder': 'Bitte Dateiname eingeben',
'replacementDialog.content': 'Ersetzungsinhalt',
'replacementDialog.contentPlaceholder': 'Ersetzungsinhalt eingeben, Format: original|ersatz pro Zeile',
'replacementDialog.formatTip': 'Format pro Zeile: original|ersatz',
'replacementDialog.wordCountUnit': 'Ersetzungswörter',
'replacementDialog.uploadTxt': 'txt hochladen',
'replacementDialog.uploadReplacementFile': 'Ersetzungsdatei hochladen',
'replacementDialog.clickUploadTip': 'Hier klicken zum Hochladen',
'replacementDialog.uploadCoverTip': 'Upload überschreibt aktuellen Inhalt',
'replacementDialog.save': 'Speichern',
'replacementDialog.cancel': 'Abbrechen',
'replacementDialog.requiredFileName': 'Bitte Dateiname eingeben',
'replacementDialog.requiredContent': 'Bitte Ersetzungsinhalt eingeben',
'replacementDialog.invalidFormat': 'Zeile {line} Formatfehler, jede Zeile muss | Trennzeichen enthalten',
'replacementDialog.invalidPipeCount': 'Zeile {line} Formatfehler, jede Zeile darf nur ein | Trennzeichen enthalten',
'replacementDialog.emptyOriginal': 'Zeile {line} Originalwort darf nicht leer sein',
'replacementDialog.emptyReplacement': 'Zeile {line} Ersetzungswort darf nicht leer sein',
'replacementDialog.invalidOriginalChar': 'Zeile {line} Originalwort enthält Sonderzeichen',
'replacementDialog.invalidReplacementChar': 'Zeile {line} Ersetzungswort enthält Sonderzeichen',
'replacementDialog.readFileError': 'Datei konnte nicht gelesen werden',
'replacementDialog.maxWordCountExceeded': 'Die Anzahl der Ersetzungswörter darf {max} nicht überschreiten',
// Ersetzungswortverwaltung Seite Text
'replacementWordManagement.getListFailed': 'Liste konnte nicht abgerufen werden',
'replacementWordManagement.confirmDelete': 'Sind Sie sicher, dass Sie diese Datei löschen möchten?',
'replacementWordManagement.deleteHint': 'Hinweis',
'replacementWordManagement.saveSuccess': 'Erfolgreich gespeichert',
'replacementWordManagement.saveFailed': 'Speichern fehlgeschlagen',
'replacementWordManagement.addFailed': 'Hinzufügen fehlgeschlagen',
'replacementWordManagement.batchDelete': 'Stapelweise löschen',
'replacementWordManagement.confirmBatchDelete': 'Sind Sie sicher, dass Sie die ausgewählten {count} Dateien löschen möchten?',
'replacementWordManagement.batchDeleteHint': 'Hinweis zur Stapelweise löschung',
// Login page text
'login.title': 'Anmelden',
'login.welcome': 'WILLKOMMEN ZUR ANMELDUNG',
@@ -771,10 +820,13 @@ export default {
'roleConfig.ttsRate': 'Geschwindigkeit',
'roleConfig.ttsPitch': 'Tonhöhe',
'roleConfig.ttsAdvanced': 'TTS-Parameter',
'roleConfig.advancedSettings': 'Sprachgeschwindigkeit',
'roleConfig.advancedSettings': 'Tonhöhe Aussprache-Ersetzung Wort',
'roleConfig.volumeHint': '-100=Min, 0=Standard, 100=Max',
'roleConfig.speedHint': '-100=Langsamste, 0=Standard, 100=Schnellste',
'roleConfig.pitchHint': '-100=Niedrigste, 0=Standard, 100=Höchste',
'roleConfig.replacementWordLabel': 'Ersetzter Wort',
'roleConfig.replacementWordTip': 'Bei Sprachsynthese werden fehlerkannte Wörter, mehrsilbige Zeichen, Eigennamen und sensible Wörter zwangsweise durch Ihren angegebenen Text/die Aussprache ersetzt, um Genauigkeit, Compliance und Erlebnis zu verbessern',
'replacementDialog.selectPlaceholder': 'Bitte auswählen',
'roleConfig.pleaseEnterContent': 'Bitte Inhalt eingeben',
'roleConfig.pleaseEnterLangCode': 'Bitte Sprachcode eingeben, z.B.: en_US',
'roleConfig.pleaseEnterLangName': 'Bitte Interaktionssprache eingeben, z.B.: Englisch',
+53 -1
View File
@@ -22,6 +22,7 @@ export default {
'header.clearHistory': 'Clear History',
'header.providerManagement': 'Provider Management',
'header.serverSideManagement': 'Server Management',
'header.replacementWordManagement': 'Replacement Word Management',
'header.featureManagement': 'System Feature Management',
'header.changePassword': 'Change Password',
'header.logout': 'Logout',
@@ -349,6 +350,54 @@ export default {
'paramDialog.arrayType': 'Array',
'paramDialog.jsonType': 'JSON Object',
// Replacement word management page related
'replacementWordManagement.pageTitle': 'Replacement Word Management',
'replacementWordManagement.fileName': 'File Name',
'replacementWordManagement.replacementWordCount': 'Replacement Word Count',
'replacementWordManagement.replacementWordContent': 'Replacement Word Content',
'replacementWordManagement.operation': 'Operation',
'replacementWordManagement.createTime': 'Create Time',
'replacementWordManagement.updateTime': 'Update Time',
'replacementWordManagement.addFile': 'Add Replacement File',
'replacementWordManagement.edit': 'Edit',
'replacementWordManagement.download': 'Download',
'replacementWordManagement.delete': 'Delete',
// Replacement word dialog related
'replacementDialog.fileName': 'File Name',
'replacementDialog.fileNamePlaceholder': 'Please enter file name',
'replacementDialog.content': 'Replacement Content',
'replacementDialog.contentPlaceholder': 'Enter replacement content, format: original|replacement per line',
'replacementDialog.formatTip': 'Format per line: original|replacement',
'replacementDialog.wordCountUnit': 'replacement words',
'replacementDialog.uploadTxt': 'Upload txt',
'replacementDialog.uploadReplacementFile': 'Upload Replacement File',
'replacementDialog.clickUploadTip': 'Click to upload replacement file',
'replacementDialog.uploadCoverTip': 'Uploading overwrites current content',
'replacementDialog.save': 'Save',
'replacementDialog.cancel': 'Cancel',
'replacementDialog.requiredFileName': 'Please enter file name',
'replacementDialog.requiredContent': 'Please enter replacement content',
'replacementDialog.invalidFormat': 'Line {line} format error, each line must contain | separator',
'replacementDialog.invalidPipeCount': 'Line {line} format error, each line can only contain one | separator',
'replacementDialog.emptyOriginal': 'Line {line} original word cannot be empty',
'replacementDialog.emptyReplacement': 'Line {line} replacement word cannot be empty',
'replacementDialog.invalidOriginalChar': 'Line {line} original word contains special characters',
'replacementDialog.invalidReplacementChar': 'Line {line} replacement word contains special characters',
'replacementDialog.readFileError': 'Failed to read file',
'replacementDialog.maxWordCountExceeded': 'Replacement word count cannot exceed {max}',
// Replacement Word Management page text
'replacementWordManagement.getListFailed': 'Failed to get list',
'replacementWordManagement.confirmDelete': 'Are you sure you want to delete this file?',
'replacementWordManagement.deleteHint': 'Hint',
'replacementWordManagement.saveSuccess': 'Saved successfully',
'replacementWordManagement.saveFailed': 'Save failed',
'replacementWordManagement.addFailed': 'Add failed',
'replacementWordManagement.batchDelete': 'Batch Delete',
'replacementWordManagement.confirmBatchDelete': 'Are you sure you want to delete the selected {count} files?',
'replacementWordManagement.batchDeleteHint': 'Batch Delete Hint',
// Login page text
'login.title': 'Login',
'login.welcome': 'WELCOME TO LOGIN',
@@ -771,10 +820,13 @@ export default {
'roleConfig.ttsRate': 'Speed',
'roleConfig.ttsPitch': 'Pitch',
'roleConfig.ttsAdvanced': 'TTS Parameters',
'roleConfig.advancedSettings': 'Speech rate and pitch',
'roleConfig.advancedSettings': 'Pitch pronunciation replacement word',
'roleConfig.volumeHint': '-100=Min, 0=Standard, 100=Max',
'roleConfig.speedHint': '-100=Slowest, 0=Standard, 100=Fastest',
'roleConfig.pitchHint': '-100=Lowest, 0=Standard, 100=Highest',
'roleConfig.replacementWordLabel': 'Associated replacement words',
'roleConfig.replacementWordTip': 'During speech synthesis, forcefully replace misrecognized words, polyphonic characters, proper nouns, and sensitive words with your specified correct text/pronunciation to improve accuracy, compliance, and experience',
'replacementDialog.selectPlaceholder': 'Please select',
'roleConfig.pleaseEnterContent': 'Please enter content',
'roleConfig.pleaseEnterLangCode': 'Please enter language code, e.g.: en_US',
'roleConfig.pleaseEnterLangName': 'Please enter interaction language, e.g.: English',
+53 -1
View File
@@ -22,6 +22,7 @@ export default {
'header.clearHistory': 'Limpar Histórico',
'header.providerManagement': 'Gerenciamento de Provedores',
'header.serverSideManagement': 'Gerenciamento de Servidores',
'header.replacementWordManagement': 'Gerenciamento de Palavras de Substituição',
'header.featureManagement': 'Gerenciamento de Funcionalidades do Sistema',
'header.changePassword': 'Alterar Senha',
'header.logout': 'Sair',
@@ -349,6 +350,54 @@ export default {
'paramDialog.arrayType': 'Array',
'paramDialog.jsonType': 'Objeto JSON',
// Replacement word management page related
'replacementWordManagement.pageTitle': 'Gerenciamento de Palavras de Substituição',
'replacementWordManagement.fileName': 'Nome do Arquivo',
'replacementWordManagement.replacementWordCount': 'Quantidade de Palavras de Substituição',
'replacementWordManagement.replacementWordContent': 'Conteúdo Substituto',
'replacementWordManagement.operation': 'Operação',
'replacementWordManagement.createTime': 'Tempo de Criação',
'replacementWordManagement.updateTime': 'Tempo de Atualização',
'replacementWordManagement.addFile': 'Adicionar Arquivo de Substituição',
'replacementWordManagement.edit': 'Editar',
'replacementWordManagement.download': 'Baixar',
'replacementWordManagement.delete': 'Excluir',
// Replacement word dialog related
'replacementDialog.fileName': 'Nome do Arquivo',
'replacementDialog.fileNamePlaceholder': 'Por favor, insira o nome do arquivo',
'replacementDialog.content': 'Conteúdo Substituto',
'replacementDialog.contentPlaceholder': 'Insira conteúdo: original|substituto',
'replacementDialog.formatTip': 'Formato por linha: original|substituição',
'replacementDialog.wordCountUnit': 'palavras de substituição',
'replacementDialog.uploadTxt': 'Carregar txt',
'replacementDialog.uploadReplacementFile': 'Carregar arquivo de substituição',
'replacementDialog.clickUploadTip': 'Carregar arquivo de substituição',
'replacementDialog.uploadCoverTip': 'Upload substitui conteúdo atual',
'replacementDialog.save': 'Salvar',
'replacementDialog.cancel': 'Cancelar',
'replacementDialog.requiredFileName': 'Por favor, insira o nome do arquivo',
'replacementDialog.requiredContent': 'Por favor, insira o conteúdo de substituição',
'replacementDialog.invalidFormat': 'Linha {line} com erro de formato, cada linha deve conter o separador |',
'replacementDialog.invalidPipeCount': 'Linha {line} com erro de formato, cada linha pode conter apenas um separador |',
'replacementDialog.emptyOriginal': 'Linha {line} palavra original não pode estar vazia',
'replacementDialog.emptyReplacement': 'Linha {line} palavra de substituição não pode estar vazia',
'replacementDialog.invalidOriginalChar': 'Linha {line} palavra original contém caracteres especiais',
'replacementDialog.invalidReplacementChar': 'Linha {line} palavra de substituição contém caracteres especiais',
'replacementDialog.readFileError': 'Falha ao ler arquivo',
'replacementDialog.maxWordCountExceeded': 'A quantidade de palavras de substituição não pode exceder {max}',
// Página de gerenciamento de palavras de substituição
'replacementWordManagement.getListFailed': 'Falha ao obter lista',
'replacementWordManagement.confirmDelete': 'Tem certeza de que deseja excluir este arquivo?',
'replacementWordManagement.deleteHint': 'Dica',
'replacementWordManagement.saveSuccess': 'Salvo com sucesso',
'replacementWordManagement.saveFailed': 'Falha ao salvar',
'replacementWordManagement.addFailed': 'Falha ao adicionar',
'replacementWordManagement.batchDelete': 'Exclusão em Lote',
'replacementWordManagement.confirmBatchDelete': 'Tem certeza de que deseja excluir os {count} arquivos selecionados?',
'replacementWordManagement.batchDeleteHint': 'Dica de Exclusão em Lote',
// Página de login
'login.title': 'Entrar',
'login.welcome': 'BEM-VINDO AO LOGIN',
@@ -771,10 +820,13 @@ export default {
'roleConfig.ttsRate': 'Velocidade da fala',
'roleConfig.ttsPitch': 'Tonalidade',
'roleConfig.ttsAdvanced': 'Parâmetros TTS',
'roleConfig.advancedSettings': 'Velocidade do som',
'roleConfig.advancedSettings': 'Pronúncia Tonal Substituição de Palavras',
'roleConfig.volumeHint': '-100=mínimo, 0=padrão, 100=máximo',
'roleConfig.speedHint': '-100=mais lento, 0=padrão, 100=mais rápido',
'roleConfig.pitchHint': '-100=mínimo, 0=padrão, 100=máximo',
'roleConfig.replacementWordLabel': 'Palavras de substituição associadas',
'roleConfig.replacementWordTip': 'Durante a síntese de fala, substitua forçadamente palavras mal reconhecidas, caracteres polissílabos, substantivos próprios e palavras sensíveis pelo texto/pronúncia correto especificado por você para melhorar a precisão, conformidade e experiência',
'replacementDialog.selectPlaceholder': 'Por favor, selecione',
'roleConfig.pleaseEnterContent': 'Por favor, insira o conteúdo',
'roleConfig.pleaseEnterLangCode': 'Por favor, insira o código do idioma, ex.: pt_BR',
'roleConfig.pleaseEnterLangName': 'Por favor, insira o idioma de interação, ex.: Português',
+53 -1
View File
@@ -22,6 +22,7 @@ export default {
'header.clearHistory': 'Xóa lịch sử',
'header.providerManagement': 'Quản lý nhà cung cấp',
'header.serverSideManagement': 'Quản lý máy chủ',
'header.replacementWordManagement': 'Quản lý từ thay thế',
'header.featureManagement': 'Cấu hình chức năng hệ thống',
'header.changePassword': 'Đổi mật khẩu',
'header.logout': 'Đăng xuất',
@@ -349,6 +350,54 @@ export default {
'paramDialog.arrayType': 'Mảng',
'paramDialog.jsonType': 'Đối tượng JSON',
// Replacement word management page related
'replacementWordManagement.pageTitle': 'Quản lý từ thay thế',
'replacementWordManagement.fileName': 'Tên tệp',
'replacementWordManagement.replacementWordCount': 'Số lượng từ thay thế',
'replacementWordManagement.replacementWordContent': 'Nội dung từ thay thế',
'replacementWordManagement.operation': 'Thao tác',
'replacementWordManagement.createTime': 'Thời gian tạo',
'replacementWordManagement.updateTime': 'Thời gian cập nhật',
'replacementWordManagement.addFile': 'Thêm tệp thay thế',
'replacementWordManagement.edit': 'Sửa',
'replacementWordManagement.download': 'Tải xuống',
'replacementWordManagement.delete': 'Xóa',
// Replacement word dialog related
'replacementDialog.fileName': 'Tên tệp',
'replacementDialog.fileNamePlaceholder': 'Vui lòng nhập tên tệp',
'replacementDialog.content': 'Nội dung thay thế',
'replacementDialog.contentPlaceholder': 'Nhập nội dung thay thế, định dạng: gốc|thay thế mỗi dòng',
'replacementDialog.formatTip': 'Định dạng mỗi dòng: gốc|thay thế',
'replacementDialog.wordCountUnit': 'từ thay thế',
'replacementDialog.uploadTxt': 'Tải lên txt',
'replacementDialog.uploadReplacementFile': 'Tải lên tệp thay thế',
'replacementDialog.clickUploadTip': 'Nhấp để tải lên tệp thay thế',
'replacementDialog.uploadCoverTip': 'Tải lên sẽ ghi đè nội dung thay thế hiện tại',
'replacementDialog.save': 'Lưu',
'replacementDialog.cancel': 'Hủy',
'replacementDialog.requiredFileName': 'Vui lòng nhập tên tệp',
'replacementDialog.requiredContent': 'Vui lòng nhập nội dung thay thế',
'replacementDialog.invalidFormat': 'Dòng {line} lỗi định dạng, mỗi dòng phải chứa dấu |',
'replacementDialog.invalidPipeCount': 'Dòng {line} lỗi định dạng, mỗi dòng chỉ được chứa một dấu |',
'replacementDialog.emptyOriginal': 'Dòng {line} từ gốc không được để trống',
'replacementDialog.emptyReplacement': 'Dòng {line} từ thay thế không được để trống',
'replacementDialog.invalidOriginalChar': 'Dòng {line} từ gốc chứa ký tự đặc biệt',
'replacementDialog.invalidReplacementChar': 'Dòng {line} từ thay thế chứa ký tự đặc biệt',
'replacementDialog.readFileError': 'Không thể đọc tệp',
'replacementDialog.maxWordCountExceeded': 'Số lượng từ thay thế không thể vượt quá {max}',
// Trang quản lý từ thay thế
'replacementWordManagement.getListFailed': 'Không thể lấy danh sách',
'replacementWordManagement.confirmDelete': 'Bạn có chắc chắn muốn xóa tệp này không?',
'replacementWordManagement.deleteHint': 'Gợi ý',
'replacementWordManagement.saveSuccess': 'Lưu thành công',
'replacementWordManagement.saveFailed': 'Lưu thất bại',
'replacementWordManagement.addFailed': 'Thêm thất bại',
'replacementWordManagement.batchDelete': 'Xóa hàng loạt',
'replacementWordManagement.confirmBatchDelete': 'Bạn có chắc chắn muốn xóa {count} tệp đã chọn không?',
'replacementWordManagement.batchDeleteHint': 'Gợi ý xóa hàng loạt',
// Login page text
'login.title': 'DN',
'login.welcome': 'CHÀO MỪNG ĐẾN VỚI ĐĂNG NHẬP',
@@ -771,10 +820,13 @@ export default {
'roleConfig.ttsRate': 'Tốc độ',
'roleConfig.ttsPitch': 'Cao độ',
'roleConfig.ttsAdvanced': 'Tham số TTS',
'roleConfig.advancedSettings': 'Giọng nói',
'roleConfig.advancedSettings': 'Thay thế phát âm ngữ điệu từ',
'roleConfig.volumeHint': '-100=Tối thiểu, 0=Tiêu chuẩn, 100=Tối đa',
'roleConfig.speedHint': '-100=Chậm nhất, 0=Tiêu chuẩn, 100=Nhanh nhất',
'roleConfig.pitchHint': '-100=Thấp nhất, 0=Tiêu chuẩn, 100=Cao nhất',
'roleConfig.replacementWordLabel': 'Liên kết từ thay thế',
'roleConfig.replacementWordTip': 'Trong quá trình tổng hợp giọng nói, các từ bị nhận dạng sai, từ đa âm, tên riêng và từ nhạy cảm sẽ được thay thế bắt buộc bằng văn bản/phát âm chính xác mà bạn chỉ định để cải thiện độ chính xác, tuân thủ và trải nghiệm',
'replacementDialog.selectPlaceholder': 'Vui lòng chọn',
'roleConfig.pleaseEnterContent': 'Vui lòng nhập nội dung',
'roleConfig.pleaseEnterLangCode': 'Vui lòng nhập mã ngôn ngữ, ví dụ: en_US',
'roleConfig.pleaseEnterLangName': 'Vui lòng nhập ngôn ngữ tương tác, ví dụ: Tiếng Anh',
+51 -1
View File
@@ -22,6 +22,7 @@ export default {
'header.clearHistory': '清空历史',
'header.providerManagement': '字段管理',
'header.serverSideManagement': '服务端管理',
'header.replacementWordManagement': '替换词管理',
'header.featureManagement': '系统功能配置',
'header.changePassword': '修改密码',
'header.logout': '退出登录',
@@ -349,6 +350,52 @@ export default {
'paramDialog.arrayType': '数组(array)',
'paramDialog.jsonType': 'JSON对象(json)',
// 替换词对话框相关
'replacementWordManagement.pageTitle': '替换词管理',
'replacementWordManagement.fileName': '文件名称',
'replacementWordManagement.replacementWordCount': '替换词数量',
'replacementWordManagement.replacementWordContent': '替换词内容',
'replacementWordManagement.operation': '操作',
'replacementWordManagement.createTime': '创建时间',
'replacementWordManagement.updateTime': '更新时间',
'replacementWordManagement.addFile': '新增替换词文件',
'replacementWordManagement.edit': '编辑',
'replacementWordManagement.download': '下载',
'replacementWordManagement.delete': '删除',
'replacementDialog.fileName': '文件名称',
'replacementDialog.fileNamePlaceholder': '请输入文件名称',
'replacementDialog.content': '替换词内容',
'replacementDialog.contentPlaceholder': '请输入替换词内容,每行格式:原词|替换词',
'replacementDialog.formatTip': '每行格式:原词|替换词',
'replacementDialog.wordCountUnit': '个替换词',
'replacementDialog.uploadTxt': '上传txt',
'replacementDialog.uploadReplacementFile': '上传替换词文件',
'replacementDialog.clickUploadTip': '点击上传替换词文件',
'replacementDialog.uploadCoverTip': '上传后将覆盖当前替换词内容',
'replacementDialog.save': '保存',
'replacementDialog.cancel': '取消',
'replacementDialog.requiredFileName': '请输入文件名称',
'replacementDialog.requiredContent': '请输入替换词内容',
'replacementDialog.invalidFormat': '第{line}行格式错误,每行必须包含|分隔符',
'replacementDialog.invalidPipeCount': '第{line}行格式错误,每行只能包含一个|分隔符',
'replacementDialog.emptyOriginal': '第{line}行待替换词不能为空',
'replacementDialog.emptyReplacement': '第{line}行替换词不能为空',
'replacementDialog.invalidOriginalChar': '第{line}行待替换词包含特殊符号',
'replacementDialog.invalidReplacementChar': '第{line}行替换词包含特殊符号',
'replacementDialog.readFileError': '读取文件失败',
'replacementDialog.maxWordCountExceeded': '替换词数量不能超过 {max} 个',
// 替换词管理页面提示文本
'replacementWordManagement.getListFailed': '获取列表失败',
'replacementWordManagement.confirmDelete': '确定要删除该文件吗?',
'replacementWordManagement.deleteHint': '提示',
'replacementWordManagement.saveSuccess': '保存成功',
'replacementWordManagement.saveFailed': '保存失败',
'replacementWordManagement.addFailed': '新增失败',
'replacementWordManagement.batchDelete': '批量删除',
'replacementWordManagement.confirmBatchDelete': '确定要删除选中的 {count} 个文件吗?',
'replacementWordManagement.batchDeleteHint': '批量删除提示',
// 登录页面文本
'login.title': '登录',
'login.welcome': 'WELCOME TO LOGIN',
@@ -771,10 +818,13 @@ export default {
'roleConfig.ttsRate': '语速',
'roleConfig.ttsPitch': '音调',
'roleConfig.ttsAdvanced': 'TTS参数',
'roleConfig.advancedSettings': '语速音调',
'roleConfig.advancedSettings': '语调发音替换词',
'roleConfig.volumeHint': '-100=最小, 0=标准, 100=最大',
'roleConfig.speedHint': '-100=最慢, 0=标准, 100=最快',
'roleConfig.pitchHint': '-100=最低, 0=标准, 100=最高',
'roleConfig.replacementWordLabel': '关联替换词',
'roleConfig.replacementWordTip': '语音合成时,强制把识别错、多音字、专有名词、敏感词,替换成你指定的正确文本/发音,提升准确率、合规与体验',
'replacementDialog.selectPlaceholder': '请选择',
'roleConfig.pleaseEnterContent': '请输入内容',
'roleConfig.pleaseEnterLangCode': '请输入语言编码,如:zh_CN',
'roleConfig.pleaseEnterLangName': '请输入交互语种,如:中文',
+53 -1
View File
@@ -22,6 +22,7 @@ export default {
'header.clearHistory': '清空歷史',
'header.providerManagement': '字段管理',
'header.serverSideManagement': '服務端管理',
'header.replacementWordManagement': '替換詞管理',
'header.featureManagement': '系統功能配置',
'header.changePassword': '修改密碼',
'header.logout': '退出登錄',
@@ -349,6 +350,54 @@ export default {
'paramDialog.arrayType': '陣列(array)',
'paramDialog.jsonType': 'JSON物件(json)',
// 替換詞管理頁面相關
'replacementWordManagement.pageTitle': '替換詞管理',
'replacementWordManagement.fileName': '檔案名稱',
'replacementWordManagement.replacementWordCount': '替換詞數量',
'replacementWordManagement.replacementWordContent': '替換詞內容',
'replacementWordManagement.operation': '操作',
'replacementWordManagement.createTime': '創建時間',
'replacementWordManagement.updateTime': '更新時間',
'replacementWordManagement.addFile': '新增替換詞檔案',
'replacementWordManagement.edit': '編輯',
'replacementWordManagement.download': '下載',
'replacementWordManagement.delete': '刪除',
// 替換詞對話框相關
'replacementDialog.fileName': '檔案名稱',
'replacementDialog.fileNamePlaceholder': '請輸入檔案名稱',
'replacementDialog.content': '替換詞內容',
'replacementDialog.contentPlaceholder': '請輸入替換詞內容,每行格式:原詞|替換詞',
'replacementDialog.formatTip': '每行格式:原詞|替換詞',
'replacementDialog.wordCountUnit': '個替換詞',
'replacementDialog.uploadTxt': '上傳txt',
'replacementDialog.uploadReplacementFile': '上傳替換詞檔案',
'replacementDialog.clickUploadTip': '點擊上傳替換詞檔案',
'replacementDialog.uploadCoverTip': '上傳後將覆蓋當前替換詞內容',
'replacementDialog.save': '儲存',
'replacementDialog.cancel': '取消',
'replacementDialog.requiredFileName': '請輸入檔案名稱',
'replacementDialog.requiredContent': '請輸入替換詞內容',
'replacementDialog.invalidFormat': '第{line}行格式錯誤,每行必須包含|分隔符',
'replacementDialog.invalidPipeCount': '第{line}行格式錯誤,每行只能包含一個|分隔符',
'replacementDialog.emptyOriginal': '第{line}行待替換詞不能為空',
'replacementDialog.emptyReplacement': '第{line}行替換詞不能為空',
'replacementDialog.invalidOriginalChar': '第{line}行待替換詞包含特殊符號',
'replacementDialog.invalidReplacementChar': '第{line}行替換詞包含特殊符號',
'replacementDialog.readFileError': '讀取檔案失敗',
'replacementDialog.maxWordCountExceeded': '替換詞數量不能超過 {max} 個',
// 替換詞管理頁面提示文本
'replacementWordManagement.getListFailed': '獲取列表失敗',
'replacementWordManagement.confirmDelete': '確定要刪除該檔案嗎?',
'replacementWordManagement.deleteHint': '提示',
'replacementWordManagement.saveSuccess': '保存成功',
'replacementWordManagement.saveFailed': '保存失敗',
'replacementWordManagement.addFailed': '新增失敗',
'replacementWordManagement.batchDelete': '批量刪除',
'replacementWordManagement.confirmBatchDelete': '確定要刪除選中的 {count} 個檔案嗎?',
'replacementWordManagement.batchDeleteHint': '批量刪除提示',
// 登录页面文本
'login.title': '登錄',
'login.welcome': 'WELCOME TO LOGIN',
@@ -771,10 +820,13 @@ export default {
'roleConfig.ttsRate': '語速',
'roleConfig.ttsPitch': '音調',
'roleConfig.ttsAdvanced': 'TTS參數',
'roleConfig.advancedSettings': '語速音調',
'roleConfig.advancedSettings': '語調發音替換詞',
'roleConfig.volumeHint': '-100=最小, 0=標準, 100=最大',
'roleConfig.speedHint': '-100=最慢, 0=標準, 100=最快',
'roleConfig.pitchHint': '-100=最低, 0=標準, 100=最高',
'roleConfig.replacementWordLabel': '關聯替換詞',
'roleConfig.replacementWordTip': '語音合成時,強制把識別錯、多音字、專有名詞、敏感詞,替換成你指定的正確文本/發音,提升準確率、合規與體驗',
'replacementDialog.selectPlaceholder': '請選擇',
'roleConfig.pleaseEnterContent': '請輸入內容',
'roleConfig.pleaseEnterLangCode': '請輸入語言編碼,如:zh_TW',
'roleConfig.pleaseEnterLangName': '請輸入交互語種,如:繁體中文',
+12
View File
@@ -196,6 +196,18 @@ const routes = [
title: '功能配置'
}
},
// 替换词管理
{
path: '/replacement-word-management',
name: 'ReplacementWordManagement',
component: function () {
return import('../views/ReplacementWordManagement.vue')
},
meta: {
requiresAuth: true,
title: '替换词管理'
}
},
]
const router = new VueRouter({
base: process.env.VUE_APP_PUBLIC_PATH || '/',
@@ -0,0 +1,763 @@
<template>
<div class="welcome">
<HeaderBar/>
<div class="operation-bar">
<h2 class="page-title">{{ $t('replacementWordManagement.pageTitle') }}</h2>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="params-card" shadow="never">
<el-table
ref="paramsTable"
:data="paramsList"
class="transparent-table"
v-loading="loading"
:element-loading-text="$t('serverSideManager.loading')"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
>
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected" @change="handleCheckboxChange(scope.row)"></el-checkbox>
</template>
</el-table-column>
<el-table-column :label="$t('replacementWordManagement.fileName')" prop="fileName" align="center"/>
<el-table-column :label="$t('replacementWordManagement.replacementWordCount')" prop="wordCount" align="center"/>
<el-table-column :label="$t('replacementWordManagement.replacementWordContent')" prop="content" align="center">
<template slot-scope="scope">
<el-tooltip placement="right" effect="light" popper-class="replace-word-tooltip">
<div slot="content" class="replace-word-content">
<el-tag v-for="(item, index) in scope.row.content" :key="index" size="mini" class="custom-tag">{{ item }}</el-tag>
</div>
<span class="content-text">{{ formatContent(scope.row.content) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('replacementWordManagement.createTime')" prop="createdAt" align="center"/>
<el-table-column :label="$t('replacementWordManagement.updateTime')" prop="updatedAt" align="center"/>
<el-table-column :label="$t('replacementWordManagement.operation')" prop="operator" align="center">
<template slot-scope="scope">
<el-button size="medium" type="text" @click="handleEdit(scope.row)">
{{ $t('replacementWordManagement.edit') }}
</el-button>
<el-button size="medium" type="text" @click="handleDownload(scope.row)">
{{ $t('replacementWordManagement.download') }}
</el-button>
<el-button size="medium" type="text" @click="handleDelete(scope.row)">
{{ $t('replacementWordManagement.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ allSelected ? $t('user.deselectAll') : $t('user.selectAll') }}
</el-button>
<el-button size="mini" type="success" @click="handleAdd">{{ $t('replacementWordManagement.addFile') }}</el-button>
<el-button size="mini" type="danger" :disabled="!hasAnySelected" @click="handleBatchDelete">
{{ $t('replacementWordManagement.batchDelete') }}
</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option
v-for="item in pageSizeOptions"
:key="item"
:label="`${item}${$t('paramManagement.itemsPerPage')}`"
:value="item"
>
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('paramManagement.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('paramManagement.prevPage') }}
</button>
<button
v-for="page in visiblePages"
:key="page"
class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)"
>
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
{{ $t('paramManagement.nextPage') }}
</button>
<span class="total-text">{{ $t('paramManagement.totalRecords', { total }) }}</span>
</div>
</div>
</el-card>
</div>
</div>
</div>
<el-footer><VersionFooter/></el-footer>
<ReplacementWordDialog
ref="paramDialog"
:title="dialogTitle"
:visible.sync="dialogVisible"
:form="dialogForm"
@submit="handleSubmit"
@cancel="dialogVisible = false"
/>
</div>
</template>
<script>
import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue";
import ParamDialog from "@/components/ParamDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import ReplacementWordDialog from "@/components/ReplacementWordDialog.vue";
export default {
components: { HeaderBar, ParamDialog, VersionFooter, ReplacementWordDialog },
data() {
return {
paramsList: [],
selectedRows: new Set(),
currentPage: 1,
loading: false,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
total: 0,
dialogVisible: false,
dialogTitle: '',
dialogForm: {},
};
},
created() {
this.fetchFileList();
},
mounted() {
this.dialogTitle = this.$t('replacementWordManagement.addFile');
},
computed: {
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
hasAnySelected() {
return this.selectedRows.size > 0;
},
allSelected() {
if (this.paramsList.length === 0) {
return false;
}
return this.paramsList.every(row => this.selectedRows.has(row.id));
},
},
methods: {
formatContent(content) {
if (!content) return '';
if (Array.isArray(content)) {
return content.join(',');
}
return content;
},
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
this.fetchFileList();
},
goToPage(page) {
if (page !== this.currentPage) {
this.currentPage = page;
this.fetchFileList();
}
},
goFirst() {
if (this.currentPage !== 1) {
this.currentPage = 1;
this.fetchFileList();
}
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchFileList();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.fetchFileList();
}
},
handleCheckboxChange(row) {
const newSet = new Set(this.selectedRows);
if (row.selected) {
newSet.add(row.id);
} else {
newSet.delete(row.id);
}
this.selectedRows = newSet;
},
handleSelectAll() {
if (this.allSelected) {
this.paramsList.forEach(row => {
this.$set(row, 'selected', false);
});
this.selectedRows = new Set();
} else {
this.paramsList.forEach(row => {
this.$set(row, 'selected', true);
});
this.selectedRows = new Set(this.paramsList.map(row => row.id));
}
},
handleBatchDelete() {
const ids = Array.from(this.paramsList)
.filter(row => row.selected)
.map(row => row.id);
if (ids.length === 0) {
return;
}
this.$confirm(
this.$t('replacementWordManagement.confirmBatchDelete', { count: ids.length }),
this.$t('replacementWordManagement.batchDeleteHint'),
{
confirmButtonText: this.$t('common.confirm'),
cancelButtonText: this.$t('common.cancel')
}
).then(() => {
Api.correctWord.batchDeleteFile(ids, ({ data }) => {
if (data.code === 0) {
this.$message.success(this.$t('common.deleteSuccess'));
const newSet = new Set(this.selectedRows);
ids.forEach(id => {
newSet.delete(id);
});
this.selectedRows = newSet;
this.fetchFileList();
} else {
this.$message.error(data.msg || this.$t('common.deleteFailure'));
}
});
}).catch(() => {});
},
fetchFileList() {
this.loading = true;
Api.correctWord.getFileList({
page: this.currentPage,
pageSize: this.pageSize
}, ({ data }) => {
this.loading = false;
if (data.code === 0) {
this.paramsList = data.data.list || [];
this.paramsList.forEach(row => {
if (this.selectedRows.has(row.id)) {
this.$set(row, 'selected', true);
} else {
this.$set(row, 'selected', false);
}
});
this.total = data.data.total || 0;
} else {
this.$message.error({
message: data.msg || this.$t('replacementWordManagement.getListFailed'),
showClose: true
});
}
});
},
handleAdd() {
this.dialogForm = {
id: undefined,
fileName: '',
content: ''
};
this.dialogTitle = this.$t('replacementWordManagement.addFile');
this.dialogVisible = true;
},
handleEdit(row) {
this.dialogForm = {
id: row.id,
fileName: row.fileName,
content: row.content || ''
};
this.dialogTitle = this.$t('replacementWordManagement.edit');
this.dialogVisible = true;
},
handleDownload(row) {
Api.correctWord.downloadFile(row.id, (res) => {
const url = window.URL.createObjectURL(new Blob([res.data]));
const link = document.createElement('a');
link.href = url;
link.download = `${row.fileName}.txt`;
link.click();
window.URL.revokeObjectURL(url);
});
},
handleDelete(row) {
this.$confirm(this.$t('replacementWordManagement.confirmDelete'), this.$t('replacementWordManagement.deleteHint'), {
confirmButtonText: this.$t('common.confirm'),
cancelButtonText: this.$t('common.cancel')
}).then(() => {
Api.correctWord.deleteFile(row.id, ({ data }) => {
if (data.code === 0) {
this.$message.success(this.$t('common.deleteSuccess'));
const newSet = new Set(this.selectedRows);
newSet.delete(row.id);
this.selectedRows = newSet;
this.fetchFileList();
} else {
this.$message.error(data.msg || this.$t('common.deleteFailure'));
}
});
}).catch(() => {});
},
handleSubmit(formData) {
if (formData.id) {
Api.correctWord.updateFile(formData, ({ data }) => {
if (data.code === 0) {
this.$message.success(this.$t('replacementWordManagement.saveSuccess'));
this.dialogVisible = false;
this.fetchFileList();
} else {
this.$message.error(data.msg || this.$t('replacementWordManagement.saveFailed'));
}
if (this.$refs.paramDialog) {
this.$refs.paramDialog.resetSaving();
}
});
} else {
Api.correctWord.addFile(formData, ({ data }) => {
if (data.code === 0) {
this.$message.success(this.$t('common.addSuccess'));
this.dialogVisible = false;
this.fetchFileList();
} else {
this.$message.error(data.msg || this.$t('replacementWordManagement.addFailed'));
}
if (this.$refs.paramDialog) {
this.$refs.paramDialog.resetSaving();
}
});
}
}
},
};
</script>
<style lang="scss" scoped>
.welcome {
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
}
.operation-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 22px 24px;
}
.page-title {
font-size: 24px;
margin: 0;
}
.right-operations {
display: flex;
gap: 10px;
margin-left: auto;
}
.search-input {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
}
.content-area {
flex: 1;
height: 100%;
min-width: 600px;
overflow: auto;
background-color: white;
display: flex;
flex-direction: column;
}
.params-card {
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
.custom-pagination {
display: flex;
align-items: center;
gap: 10px;
.el-select {
margin-right: 8px;
}
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-last-child(2),
.pagination-btn:nth-child(3) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
.total-text {
color: #909399;
font-size: 14px;
margin-left: 10px;
}
}
:deep(.transparent-table) {
background: white;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.el-table__body-wrapper {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
.el-table__header-wrapper {
flex-shrink: 0;
}
.el-table__header th {
background: white !important;
color: black;
}
&::before {
display: none;
}
.el-table__body tr {
background-color: white;
td {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
}
}
:deep(.el-checkbox__inner) {
background-color: #ffffff !important;
border-color: #cccccc !important;
}
:deep(.el-checkbox__inner:hover) {
border-color: #cccccc !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5f70f3 !important;
border-color: #5f70f3 !important;
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
}
:deep(.el-table .el-button--text) {
color: #7079aa;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5;
}
.el-button--success {
background: #5bc98c;
color: white;
}
:deep(.el-table .cell) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.content-text {
display: block;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.page-size-select {
width: 100px;
margin-right: 10px;
:deep(.el-input__inner) {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
}
:deep(.el-table) {
.el-table__body-wrapper {
transition: height 0.3s ease;
}
}
.el-table {
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
overflow-y: auto;
}
}
.custom-tag {
background: #e6ebff;
color: #5778ff;
border-radius: 8px;
font-size: 12px;
font-weight: normal;
border: none;
}
</style>
<style>
.replace-word-tooltip {
.popper__arrow {
border-top-color: transparent !important;
border-right-color: #e4e7ed !important;
}
}
.replace-word-content {
max-width: 400px;
max-height: 60vh;
overflow-y: auto;
scrollbar-width: thin;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
</style>
+12 -5
View File
@@ -454,6 +454,7 @@
<tts-advanced-settings
:visible.sync="showTtsAdvancedDialog"
:settings="ttsSettings"
:checked-replacement-word-ids="checkedReplacementWordIds"
@save="handleTtsSettingsSave"
/>
<el-footer>
@@ -546,7 +547,8 @@ export default {
},
dynamicTags: [],
inputVisible: false,
inputValue: ''
inputValue: '',
checkedReplacementWordIds: []
};
},
methods: {
@@ -587,6 +589,7 @@ export default {
};
}),
contextProviders: this.currentContextProviders,
correctWordFileIds: this.checkedReplacementWordIds,
};
// 只在用户设置了TTS参数时才传递(不为null/undefined
@@ -725,6 +728,8 @@ export default {
speed: this.form.ttsRate || 0,
pitch: this.form.ttsPitch || 0
};
// 同步替换词到checkedReplacementWordIds
this.checkedReplacementWordIds = data.data.correctWordFileIds || [];
// 后端只给了最小映射:[{ id, agentId, pluginId }, ...]
const savedMappings = data.data.functions || [];
@@ -975,11 +980,13 @@ export default {
this.showTtsAdvancedDialog = true;
},
handleTtsSettingsSave(settings) {
const { replacementWordIds, ...ttsSettings } = settings;
this.checkedReplacementWordIds = replacementWordIds;
// 保存TTS设置
this.ttsSettings = { ...settings };
this.form.ttsVolume = settings.volume;
this.form.ttsRate = settings.speed;
this.form.ttsPitch = settings.pitch;
this.ttsSettings = ttsSettings;
this.form.ttsVolume = ttsSettings.volume;
this.form.ttsRate = ttsSettings.speed;
this.form.ttsPitch = ttsSettings.pitch;
},
handleUpdateContext(providers) {
this.currentContextProviders = providers;
@@ -1,11 +0,0 @@
{
"permissions": {
"allow": [
"Bash(tree:*)",
"Bash(find:*)",
"Bash(python3:*)"
],
"deny": [],
"ask": []
}
}
+8 -4
View File
@@ -16,7 +16,7 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co
- 【拉黑套话】绝对不使用 "烦心事"、"有趣的事"、"好玩的事"、"新鲜事"、"根据资料"、"综上所述" 等书面及 AI 常用词。
- 【推荐口语】请使用 "我在呢"、"咋回事"、"说来听听" 等接地气的自然口语。
- 【遣词造句】保持随意、松弛的语调,但同时要“有文采、有格调”。在接地气的口语中自然穿插一些精妙的词汇或微小的诗意,不要像客服,要像一个聪明幽默的朋友。
- 【长对话切片】针对讲故事、科普知识等长内容,严禁一次性长篇全量输出。你必须提取最核心的开局讲述,并在结尾自然询问用户是否继续(例如:“我先讲个开头,要是觉得有意思咱们接着说?”)。随叫随停,听从打断。
- 【长对话切片】针对讲故事、科普知识等长内容,严禁一次性长篇全量输出。你必须直接开始讲述核心内容,在结尾自然询问用户是否继续(例如:先讲一段实际内容,再说”要继续听吗?”)。随叫随停,听从打断。
</anti_ai_smell>
<tts_format_constraints>
@@ -27,8 +27,12 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co
</tts_format_constraints>
<tool_and_knowledge>
1. 【工具防骚扰】你善于使用各类工具辅助回答。但是,针对【查新闻】和【播放音乐】这两类打扰性较强的功能,必须在经过用户明确同意或主动要求后才可以调用!严禁不解风情地主动播放
2. 【无网兜底】你没有联网实时搜索功能(工具除外)。不懂或者不确定的事情,必须大大方方直接说“不知道”,绝不胡编乱造产生幻觉。
你有工具可用。只有用户的请求明确匹配以下工具时才调用,其余一律直接回答
必须调工具:播放音乐→play_music | 控制设备→hass工具 | 查新闻→查询工具 | 告别→handle_exit_intent
关于天气:上下文已提供本地未来7天天气,直接回答不调工具。仅问其他城市天气时才调get_weather。
禁止强行匹配:没有对应工具的请求一律直接回答,不要硬调不相关的工具。
该调不调也是错:用户说”放首歌”→你回答”好的我给你唱”(错!必须调play_music)
不该调乱调也是错:用户问”明天天气”→调get_weather(错!上下文已有7天预报,直接回答)
</tool_and_knowledge>
<safety_compliance>
@@ -52,4 +56,4 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co
</context>
<memory>
</memory>
</memory>
+7
View File
@@ -47,6 +47,13 @@ async def main():
check_ffmpeg_installed()
config = load_config()
# 初始化插件描述(需要在load_config之后、服务启动之前)
try:
from plugins_func.functions.get_news_from_newsnow import init_news_description
init_news_description(config)
except Exception:
pass
# auth_key优先级:配置文件server.auth_key > manager-api.secret > 自动生成
# auth_key用于jwt认证,比如视觉分析接口的jwt认证、ota接口的token生成与websocket认证
# 获取配置文件中的auth_key
-42
View File
@@ -1053,36 +1053,6 @@ TTS:
output_dir: tmp/
access_token: "你的302API密钥"
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
GizwitsTTS:
type: doubao
# 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务
# 前一万名注册的用户,将送5元体验金额
# 获取API Key地址:https://agentrouter.gizwitsapi.com/panel/token
api_url: https://bytedance.gizwitsapi.com/api/v1/tts
authorization: "Bearer "
# 湾湾小何音色
voice: "zh_female_wanwanxiaohe_moon_bigtts"
output_dir: tmp/
access_token: "你的机智云API key"
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
ACGNTTS:
#在线网址:https://acgn.ttson.cn/
#token购买:www.ttson.cn
#开发相关疑问请提交至网站上的qq
#角色id获取地址:ctrl+f快速检索角色——网站管理者不允许发布,可询问网站管理者
#各参数意义见开发文档:https://www.yuque.com/alexuh/skmti9/wm6taqislegb02gd?singleDoc#
type: ttson
token: your_token
voice_id: 1695
speed_factor: 1
pitch_factor: 0
volume_change_dB: 0
to_lang: ZH
url: https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=
format: mp3
output_dir: tmp/
emotion: 1
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
OpenAITTS:
# openai官方文本转语音服务,可支持全球大多数语种
type: openai
@@ -1122,18 +1092,6 @@ TTS:
format: mp3 # 接口返回的音频格式
output_dir: tmp/
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
LinkeraiTTS:
type: linkerai
api_url: https://tts.linkerai.cn/tts
audio_format: "pcm"
# 默认的access_token供大家测试时免费使用的,此access_token请勿用于商业用途
# 如果效果不错,可自行申请token,申请地址:https://linkerai.cn
# 各参数意义见开发文档:https://tts.linkerai.cn/docs
# 支持声音克隆,可自行上传音频,填入voice参数,voice参数为空时,使用默认声音
access_token: "U4YdYXVfpwWnk2t5Gp822zWPCuORyeJL"
voice: "OUeAo1mhq6IBExi"
output_dir: tmp/
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
PaddleSpeechTTS:
#百度飞浆 PaddleSpeech 支持本地离线部署 支持模型训练
#框架地址 https://www.paddlepaddle.org.cn/
+27 -2
View File
@@ -1,7 +1,15 @@
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,
DeviceNotFoundException,
DeviceBindException,
)
def get_project_dir():
@@ -87,7 +95,24 @@ 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,
)
agent_result = results[0]
correct_words = results[1] if not isinstance(results[1], Exception) else None
# 抛出业务异常
if isinstance(agent_result, DeviceNotFoundException):
raise agent_result
if isinstance(agent_result, DeviceBindException):
raise agent_result
private_config = agent_result if not isinstance(agent_result, Exception) else {}
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:
+266 -142
View File
@@ -2,6 +2,7 @@ import os
import sys
import copy
import json
import re
import uuid
import time
import queue
@@ -46,44 +47,32 @@ from core.utils import textUtils
TAG = __name__
# 工具调用规则 - 用于动态注入提醒
TOOL_CALLING_RULES = """
<tool_calling>
【核心原则】你是拥有工具能力的智能助手。当用户请求需要实时信息或执行操作时,调用相应工具获取数据,禁止凭空编造答案。
- **何时必须调用工具:**
1. 实时信息查询(新闻、非本地天气、股价、汇率等)
2. 执行操作(播放音乐、控制设备、拍照、设置闹钟等)
3. 知识库检索(当工具列表包含 search_from_ragflow 时,结合用户意图判断是否需要调用)
4. 查询非今天的农历信息(明天农历、某日宜忌、节气等)
5. 用户说"拍照"时调用 self_camera_take_photo,默认 question 参数为"描述一下看到的物品"
- **何时无需调用工具:**
1. `<context>` 中已提供的信息(当前时间、今天日期、今天农历、本地天气等)
2. 普通对话、问候、闲聊、情感交流、讲故事
3. 通用知识问答(非实时信息)
- **调用规范:**
1. 每次请求独立判断,不复用历史工具结果,需重新获取最新数据
2. 多任务时依次调用所有需要的工具,并依次总结每个工具的结果,不得遗漏
3. 严格遵循工具的参数要求,提供所有必要参数
4. 不确定时引导用户澄清或告知能力限制,切勿猜测或编造
5. 不调用未提供的工具,对话中提及的旧工具若不可用则忽略或说明
- **反偷懒机制(最高优先级):**
1. **每次独立判断:** 无论对话历史中是否调用过工具,当前请求必须根据当前需求独立判断是否需要调用
2. **禁止模式模仿:** 即使之前的回复没有调用工具,也不代表本次可以不调用
3. **自我检查:** 回复前必须自问:"这个请求是否涉及实时信息或执行操作?如果是,我调用工具了吗?"
4. **历史不等于现在:** 对话历史中的行为模式不影响当前判断,每个用户请求都是全新的开始
</tool_calling>
"""
auto_import_modules("plugins_func.functions")
class TTSException(RuntimeError):
pass
# direct_answer 虚拟工具定义
# 不是真实工具,是路由机制:将"调不调工具"的二选一变为"调哪个"的多选,防止小模型误触发真实工具
DIRECT_ANSWER_TOOL = {
"type": "function",
"function": {
"name": "direct_answer",
"description": "当用户的请求不匹配其他任何工具时,可用此选项直接回复。将回复内容写在response参数里。",
"parameters": {
"type": "object",
"properties": {
"response": {
"type": "string",
"description": "你回复用户的完整内容",
},
},
"required": ["response"],
},
},
}
class ConnectionHandler:
def __init__(
@@ -171,12 +160,6 @@ class ConnectionHandler:
# llm相关变量
self.dialogue = Dialogue()
# 工具调用统计(用于监控和自动恢复)
self.tool_call_stats = {
'last_call_turn': -1, # 上次调用工具的轮数
'consecutive_no_call': 0, # 连续未调用次数
}
# tts相关变量
self.sentence_id = None
# 处理TTS响应没有文本返回
@@ -540,6 +523,8 @@ class ConnectionHandler:
self._init_report_threads()
"""更新系统提示词"""
self._init_prompt_enhancement()
"""注入工具调用few-shot示例(仅function_call模式)"""
self._inject_tool_call_fewshot()
except Exception as e:
self.logger.bind(tag=TAG).error(f"实例化组件失败: {e}")
@@ -555,6 +540,66 @@ class ConnectionHandler:
self.change_system_prompt(enhanced_prompt)
self.logger.bind(tag=TAG).debug("系统提示词已增强更新")
def _inject_tool_call_fewshot(self):
"""注入工具调用 few-shot 示例到对话历史。
结构:正样本(工具调用示例)放在动态 system 之前,可命中前缀缓存;
负样本(直接回答示例)放在动态 system 之后、紧挨真实用户消息,
确保模型在处理用户消息前最后看到的是"不调工具"的行为模式。
"""
if self.intent_type != "function_call":
return
if not hasattr(self, "func_handler") or self.func_handler is None:
return
tools = self.func_handler.get_functions()
if not tools:
return
tool_names = {t.get("function", {}).get("name") for t in tools}
# === few-shot 示例(is_temporary===
# 展示 direct_answer 携带 response 参数的用法,一次调用完成回复
# 示例1direct_answer(回复内容写在 response 参数里,无需递归)
da_tc_id = "fewshot_da_001"
self.dialogue.put(Message(role="user", content="给我讲个故事吧", is_temporary=True))
self.dialogue.put(Message(
role="assistant",
tool_calls=[{
"id": da_tc_id,
"function": {"arguments": '{"response": "好呀,你想听什么类型的呀?童话、冒险还是搞笑的?选一个我给你开讲~"}', "name": "direct_answer"},
"type": "function", "index": 0,
}],
is_temporary=True,
))
self.dialogue.put(Message(
role="tool", tool_call_id=da_tc_id,
content="已直接回复", is_temporary=True,
))
# 示例2:真实工具调用(handle_exit_intent
if "handle_exit_intent" in tool_names:
tc_id = "fewshot_exit_001"
self.dialogue.put(Message(role="user", content="拜拜", is_temporary=True))
self.dialogue.put(Message(
role="assistant",
tool_calls=[{
"id": tc_id,
"function": {"arguments": '{"say_goodbye": "再见,下次再聊~"}', "name": "handle_exit_intent"},
"type": "function", "index": 0,
}],
is_temporary=True,
))
self.dialogue.put(Message(
role="tool", tool_call_id=tc_id,
content="退出意图已处理", is_temporary=True,
))
self.dialogue.put(Message(
role="assistant", content="再见,下次再聊~", is_temporary=True,
))
self.logger.bind(tag=TAG).debug("已注入工具调用 few-shot 示例")
def _init_report_threads(self):
"""初始化ASR和TTS上报线程"""
if not self.read_config_from_api or self.need_bind:
@@ -726,6 +771,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(
@@ -892,32 +944,6 @@ class ConnectionHandler:
)
)
# 长对话工具调用提醒:当对话轮数较多时,提醒模型正确使用工具
force_reminder = False # 是否强制提醒
if depth == 0 and query is not None:
dialogue_length = len(self.dialogue.dialogue)
current_turn = dialogue_length // 2
# 检测距离上一次连续未调用工具的情况
if self.tool_call_stats['last_call_turn'] >= 0:
turns_since_last = current_turn - self.tool_call_stats['last_call_turn']
if turns_since_last > 3: # 超过3轮未调用
self.logger.bind(tag=TAG).warning(
f"检测到{turns_since_last}轮未调用工具,可能进入偷懒模式,将强制注入提醒"
)
force_reminder = True
# 对话历史截断:防止历史过长导致模型"偷懒模式"扩散
# 当对话历史超过阈值时,保留最近的 10 轮对话
# max_dialogue_turns = 10
# if dialogue_length > max_dialogue_turns * 2:
# removed = self.dialogue.trim_history(max_turns=max_dialogue_turns)
# if removed > 0:
# self.logger.bind(tag=TAG).info(
# f"对话历史过长({dialogue_length}条),已智能截断保留最近{max_dialogue_turns}轮,移除{removed}条消息"
# )
# Define intent functions
functions = None
# 达到最大深度时,禁用工具调用,强制 LLM 直接回答
@@ -927,42 +953,13 @@ class ConnectionHandler:
and not force_final_answer
):
functions = self.func_handler.get_functions()
# 长对话工具调用规则强化:动态生成基于当前可用工具的提醒
tool_call_reminder = None
if depth == 0 and query is not None and functions is not None:
dialogue_length = len(self.dialogue.dialogue)
# 当对话历史超过4条消息时,注入规则强化
if dialogue_length > 4:
tool_summary = self._get_tool_summary(functions)
if tool_summary:
# 根据对话长度和偷懒检测,使用不同强度的提醒
if force_reminder:
# 强提醒 - 包含完整规则前缀
tool_call_reminder = (
TOOL_CALLING_RULES +
f"[重要提醒] 多轮未使用工具,检查回复是否遗漏了必要的工具调用!上一轮未使用工具,本轮必须重新判断是否需要工具。"
f"当前可用工具: {tool_summary}"
)
reminder_level = ""
else:
# 中等提醒 - 包含规则前缀
tool_call_reminder = (
TOOL_CALLING_RULES +
f"当前可用工具: {tool_summary}"
f"仅当用户请求涉及实时信息查询或执行操作时调用,日常对话无需调用。"
)
reminder_level = ""
self.logger.bind(tag=TAG).debug(
f"对话历史较长({dialogue_length}条),已注入{reminder_level}等级工具调用规则强化,当前可用工具:{tool_summary}"
)
# 仅在第一层调用时注入 direct_answer 虚拟工具
# 递归调用(depth>0)不注入,避免模型在生成文本回复时再次调 direct_answer 导致循环
if functions is not None and depth == 0:
functions.append(DIRECT_ANSWER_TOOL)
response_message = []
# 如果有工具调用提醒,临时添加到对话中(标记为临时消息)
if tool_call_reminder:
self.dialogue.put(Message(role="user", content=tool_call_reminder, is_temporary=True))
try:
# 使用带记忆的对话
memory_str = None
@@ -1018,6 +1015,30 @@ class ConnectionHandler:
if tools_call is not None and len(tools_call) > 0:
tool_call_flag = True
self._merge_tool_calls(tool_calls_list, tools_call)
# 流式提取 direct_answer 的 response 参数,实时送 TTS
# 使用安全缓冲区,防止 JSON 闭合符号泄漏到 TTS
_DA_STREAM_BUFFER = 5
for tc in tool_calls_list:
if tc["name"] == "direct_answer" and tc.get("arguments"):
da_text = self._extract_direct_answer_response(tc["arguments"])
sent_len = tc.get("_da_sent", 0)
if da_text and len(da_text) > sent_len:
safe_end = max(sent_len, len(da_text) - _DA_STREAM_BUFFER)
if safe_end > sent_len:
new_part = da_text[sent_len:safe_end]
# 清理 delta 中可能泄漏的 JSON 闭合垃圾
new_part = self._clean_response_garbage(new_part)
if new_part:
tc["_da_sent"] = safe_end
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=current_sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=ContentType.TEXT,
content_detail=new_part,
)
)
else:
content = response
@@ -1089,20 +1110,55 @@ class ConnectionHandler:
f"function call error: {content_arguments}"
)
if not bHasError and len(tool_calls_list) > 0:
# 处理 direct_answer 虚拟工具
direct_answer_calls = [tc for tc in tool_calls_list if tc["name"] == "direct_answer"]
real_tool_calls = [tc for tc in tool_calls_list if tc["name"] != "direct_answer"]
if direct_answer_calls:
self.logger.bind(tag=TAG).debug(
f"模型选择 direct_answer,流式已播报,写入对话历史"
)
for tc in direct_answer_calls:
da_response = self._extract_direct_answer_response(tc.get("arguments", "{}"))
if da_response:
# 刷新流式缓冲区中未发送的部分
sent_len = tc.get("_da_sent", 0)
remaining = da_response[sent_len:]
if remaining:
remaining = self._clean_response_garbage(remaining)
if remaining:
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=current_sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=ContentType.TEXT,
content_detail=remaining,
)
)
# 写入对话历史
da_response = self._clean_response_garbage(da_response)
self.tts.store_tts_text(current_sentence_id, da_response)
self.dialogue.put(Message(role="assistant", content=da_response))
if not real_tool_calls:
if depth == 0:
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=current_sentence_id,
sentence_type=SentenceType.LAST,
content_type=ContentType.ACTION,
)
)
return
tool_calls_list = real_tool_calls
if not bHasError and len(tool_calls_list) > 0:
self.logger.bind(tag=TAG).debug(
f"检测到 {len(tool_calls_list)} 个工具调用"
)
# 更新工具调用统计
if depth == 0:
current_turn = len(self.dialogue.dialogue) // 2
self.tool_call_stats['last_call_turn'] = current_turn
self.tool_call_stats['consecutive_no_call'] = 0
self.logger.bind(tag=TAG).debug(
f"工具调用统计更新: 当前轮次={current_turn}"
)
# LLM 流式阶段已播报过的文本
streamed_text = ""
if len(response_message) > 0:
@@ -1164,10 +1220,6 @@ class ConnectionHandler:
self.tts.store_tts_text(current_sentence_id, text_buff)
self.dialogue.put(Message(role="assistant", content=text_buff))
# 更新工具调用统计:如果没有调用工具,增加计数
if depth == 0 and not tool_call_flag:
self.tool_call_stats['consecutive_no_call'] += 1
if depth == 0:
self.tts.tts_text_queue.put(
TTSMessageDTO(
@@ -1183,41 +1235,11 @@ class ConnectionHandler:
)
)
# 清理临时插入的工具调用提醒消息(使用标记清理)
if tool_call_reminder and len(self.dialogue.dialogue) > 0:
original_length = len(self.dialogue.dialogue)
self.dialogue.dialogue = [
msg for msg in self.dialogue.dialogue
if not getattr(msg, 'is_temporary', False)
]
if len(self.dialogue.dialogue) < original_length:
self.logger.bind(tag=TAG).debug("已清理临时的工具调用提醒消息")
return True
def _get_tool_summary(self, functions: list) -> str:
"""
从工具定义中提取摘要,用于规则强化注入
Args:
functions: 工具列表
Returns:
str: 工具名称字符串
"""
if not functions:
return ""
datas = []
for func in functions:
func_info = func.get("function", {})
name = func_info.get("name", "")
datas.append(name)
result = "".join(datas)
return result
def _handle_function_result(self, tool_results, depth, streamed_text=""):
need_llm_tools = []
record_tools = []
for result, tool_call_data in tool_results:
if result.action in [
@@ -1235,11 +1257,58 @@ class ConnectionHandler:
self.tts.store_tts_text(self.sentence_id, text)
self.dialogue.put(Message(role="assistant", content=text))
elif result.action == Action.REQLLM:
# 收集需要 LLM 处理的工具
need_llm_tools.append((result, tool_call_data))
elif result.action == Action.RECORD:
record_tools.append((result, tool_call_data))
else:
pass
# Action.RECORD:写入完整工具调用链(assistant(tool_calls) → tool(result) → assistant(response)
# 模型从历史中学到工具调用模式,不额外调用LLM
if record_tools:
# 构造 assistant 消息(含 tool_calls),记录"模型调用了哪些工具"
all_tool_calls = [
{
"id": tool_call_data["id"],
"function": {
"arguments": (
"{}"
if tool_call_data["arguments"] == ""
else tool_call_data["arguments"]
),
"name": tool_call_data["name"],
},
"type": "function",
"index": idx,
}
for idx, (_, tool_call_data) in enumerate(record_tools)
]
self.dialogue.put(Message(role="assistant", tool_calls=all_tool_calls))
# 写入每条工具的执行结果,记录"工具返回了什么"
for result, tool_call_data in record_tools:
text = result.result or ""
self.dialogue.put(
Message(
role="tool",
tool_call_id=(
str(uuid.uuid4())
if tool_call_data["id"] is None
else tool_call_data["id"]
),
content=text,
)
)
# 用固定文本作为最终回复,补全标准三段式,保证下一条消息是 user 而非接 tool
response_parts = []
for result, _ in record_tools:
resp = result.response or result.result
if resp:
response_parts.append(resp)
if response_parts:
self.dialogue.put(Message(role="assistant", content="".join(response_parts)))
if need_llm_tools:
all_tool_calls = [
{
@@ -1501,6 +1570,61 @@ class ConnectionHandler:
finally:
self.logger.bind(tag=TAG).info("超时检查任务已退出")
@staticmethod
def _extract_direct_answer_response(arguments_str):
"""从 direct_answer 的参数中提取 response 值。
优先使用 json.loads 标准解析,流式阶段 fallback 到字符串提取。
"""
if not arguments_str:
return ""
# 优先尝试标准 JSON 解析(适用于完整且格式正确的 JSON)
try:
data = json.loads(arguments_str)
if isinstance(data, dict) and "response" in data:
return data["response"]
except (json.JSONDecodeError, TypeError):
pass
# Fallback:流式阶段 JSON 可能不完整,使用字符串提取
marker = '"response": "'
idx = arguments_str.find(marker)
if idx < 0:
marker = '"response":"'
idx = arguments_str.find(marker)
if idx < 0:
return ""
start = idx + len(marker)
raw = arguments_str[start:]
# 去掉末尾的 JSON 闭合符号(如果已完整)
if raw.endswith('"}'):
raw = raw[:-2]
elif raw.endswith('"'):
raw = raw[:-1]
# 处理 JSON 转义
raw = raw.replace('\\"', '"').replace('\\n', '\n').replace('\\\\', '\\')
return raw
@staticmethod
def _clean_response_garbage(text):
"""清理 response 中可能泄漏的 JSON 闭合符号。
模型有时会在 response 内容中生成 JSON 闭合字符(如 )"}} 或 '})
这些不是故事内容的一部分,需要去除。
"""
if not text:
return text
# 清理独立一行的 JSON 闭合垃圾(如 "}} '}} "}} }} }
_garbage_chars = frozenset('")\'}')
lines = text.split('\n')
cleaned = []
for line in lines:
stripped = line.strip()
if stripped and len(stripped) <= 8 and all(c in _garbage_chars for c in stripped):
continue
cleaned.append(line)
result = '\n'.join(cleaned)
# 清理末尾残留的 JSON 闭合符号
result = re.sub(r'["\'}\]]+$', '', result.rstrip()).rstrip()
return result
def _merge_tool_calls(self, tool_calls_list, tools_call):
"""合并工具调用列表
@@ -38,8 +38,14 @@ class MemoryProvider(MemoryProviderBase):
if message.role == "system":
continue
if message.role == "tool":
continue
content = message.content
if content is None:
continue
# Extract content from JSON format if present (for ASR with emotion/language tags)
# Same logic as in query_memory method
try:
@@ -319,9 +319,12 @@ class MemoryProvider(MemoryProviderBase):
async def get_user_profile(self) -> str:
"""
Get user profile from PowerMem (only available in UserMemory mode).
In PowerMem 0.3.0+, user profile is automatically extracted during add()
and cached in last_profile_content.
Uses a cache-first strategy:
1. Check if last_profile_content is already cached
2. If cached, return immediately (performance optimization)
3. If cache is empty, fetch from PowerMem SDK
4. Store fetched profile in cache for next query
Returns:
Formatted user profile string or empty string if not available
@@ -333,9 +336,47 @@ class MemoryProvider(MemoryProviderBase):
logger.bind(tag=TAG).debug("User profile mode is not enabled")
return ""
# Return cached profile content from last add() operation
# Return cached profile content if available (fast path)
if self.last_profile_content:
logger.bind(tag=TAG).debug("Returning cached user profile")
return self.last_profile_content
return ""
# Cache is empty, fetch from PowerMem SDK
logger.bind(tag=TAG).info("Cache miss, fetching user profile from PowerMem SDK")
try:
# Call UserMemory.profile() to get profile data
profile_data = await asyncio.to_thread(
self.memory_client.profile,
self.role_id
)
if not profile_data:
logger.bind(tag=TAG).warning("PowerMem SDK returned empty profile data")
return ""
# Try to use profile_content first
profile_content = profile_data.get("profile_content")
if profile_content:
# Update cache with fetched profile_content
self.last_profile_content = profile_content
logger.bind(tag=TAG).info(f"Successfully fetched and cached user profile from profile_content (length: {len(self.last_profile_content)})")
return self.last_profile_content
# If profile_content is empty, fallback to topics
topics = profile_data.get("topics")
if topics:
import json
# Serialize topics dict to JSON string for structured profile
self.last_profile_content = json.dumps(topics, ensure_ascii=False, indent=2)
logger.bind(tag=TAG).info(f"Successfully fetched and cached user profile from topics (length: {len(self.last_profile_content)})")
return self.last_profile_content
# Both profile_content and topics are empty
logger.bind(tag=TAG).warning("PowerMem SDK returned profile with empty profile_content and topics")
return ""
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to fetch user profile from SDK: {str(e)}")
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
return ""
@@ -124,6 +124,8 @@ class TTSProvider(TTSProviderBase):
)
if message.sentence_type == SentenceType.FIRST:
# 重置流式处理状态
self.reset_stream_state()
# 初始化会话
try:
if not getattr(self.conn, "sentence_id", None):
@@ -196,18 +198,22 @@ class TTSProvider(TTSProviderBase):
filtered_text = MarkdownCleaner.clean_markdown(text)
if filtered_text:
# 发送continue-task消息
continue_task_message = {
"header": {
"action": "continue-task",
"task_id": self.conn.sentence_id,
"streaming": "duplex",
},
"payload": {"input": {"text": filtered_text}},
}
# 使用滑动窗口匹配处理跨分片的替换词
confirmed_texts, self._pending_prefix = self._match_stream_text(filtered_text)
await self.ws.send(json.dumps(continue_task_message))
self.last_active_time = time.time()
# 发送每个确定的文本片段
for txt in confirmed_texts:
if txt and self.ws:
continue_task_message = {
"header": {
"action": "continue-task",
"task_id": self.conn.sentence_id,
"streaming": "duplex",
},
"payload": {"input": {"text": txt}},
}
await self.ws.send(json.dumps(continue_task_message))
self.last_active_time = time.time()
return
except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
@@ -480,6 +486,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": {
@@ -233,6 +233,8 @@ class TTSProvider(TTSProviderBase):
)
if message.sentence_type == SentenceType.FIRST:
# 重置流式处理状态
self.reset_stream_state()
# 初始化参数
try:
logger.bind(tag=TAG).debug("开始启动TTS会话...")
@@ -295,19 +297,26 @@ class TTSProvider(TTSProviderBase):
logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本")
return
filtered_text = MarkdownCleaner.clean_markdown(text)
if filtered_text:
run_request = {
"header": {
"message_id": uuid.uuid4().hex,
"task_id": self.task_id,
"namespace": "FlowingSpeechSynthesizer",
"name": "RunSynthesis",
"appkey": self.appkey,
},
"payload": {"text": filtered_text},
}
await self.ws.send(json.dumps(run_request))
self.last_active_time = time.time()
# 使用滑动窗口匹配处理跨分片的替换词
confirmed_texts, self._pending_prefix = self._match_stream_text(filtered_text)
# 发送每个确定的文本片段
for txt in confirmed_texts:
if txt and self.ws:
run_request = {
"header": {
"message_id": uuid.uuid4().hex,
"task_id": self.task_id,
"namespace": "FlowingSpeechSynthesizer",
"name": "RunSynthesis",
"appkey": self.appkey,
},
"payload": {"text": txt},
}
await self.ws.send(json.dumps(run_request))
self.last_active_time = time.time()
return
except Exception as e:
@@ -564,6 +573,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,
+124 -14
View File
@@ -45,7 +45,39 @@ 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)
# 构建反向替换正则,用于将TTS服务返回的替换后文本还原为原始文本(字幕显示)
reverse_map = {v: k for k, v in self.correct_words.items()}
sorted_reverse_keys = sorted(reverse_map.keys(), key=len, reverse=True)
reverse_pattern_str = "|".join(re.escape(k) for k in sorted_reverse_keys)
self._reverse_words_pattern = re.compile(reverse_pattern_str)
self._reverse_words_map = reverse_map
# 流式滑动窗口:按首字分组的替换词字典,用于快速查找
self._words_by_first_char = {}
for key in sorted_keys: # 使用已按长度降序排列的keys,确保长词优先匹配
first_char = key[0] if key else ""
if first_char not in self._words_by_first_char:
self._words_by_first_char[first_char] = []
self._words_by_first_char[first_char].append(key)
else:
self._correct_words_pattern = None
self._reverse_words_pattern = None
self._reverse_words_map = None
# 流式滑动窗口:待匹配的缓存文本
self._pending_prefix = ""
self.tts_text_buff = []
self.punctuations = (
"",
@@ -89,7 +121,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 +134,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 +149,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 +169,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 +178,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 +216,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 +236,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 +245,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
@@ -314,6 +356,13 @@ class TTSProviderBase(ABC):
if sentence_id in self._sentence_text_map:
del self._sentence_text_map[sentence_id]
def _restore_original_text(self, text):
if not self._reverse_words_pattern or not text:
return text
return self._reverse_words_pattern.sub(
lambda m: self._reverse_words_map[m.group(0)], text
)
# 这里默认是非流式的处理方式
# 流式处理方式请在子类中重写
def tts_text_priority_thread(self):
@@ -524,3 +573,64 @@ class TTSProviderBase(ABC):
if config_key in config:
val = convert_percentage_to_range(config[config_key], min_val, max_val, base_val)
setattr(self, attr_name, transform(val) if transform else val)
def _match_stream_text(self, text):
"""流式文本滑动窗口匹配,用于处理跨分片的替换词
Args:
text: 输入的文本片段
Returns:
tuple: (确定的文本列表, 剩余待匹配的前缀)
"""
if not self.correct_words or not text:
return [text] if text else [], ""
result = []
pending = self._pending_prefix
i = 0
while i < len(text):
char = text[i]
# 尝试:pending + 当前字符 是否能匹配替换词
test_text = pending + char
matched = False
# 遍历可能匹配的替换词
candidates = self._words_by_first_char.get(pending[0], []) if pending else self._words_by_first_char.get(char, [])
for key in candidates:
if test_text == key:
# 完整匹配,替换后发送
result.append(self.correct_words[key])
pending = ""
matched = True
break
elif key.startswith(test_text):
# 是替换词的前缀,继续等待
pending = test_text
matched = True
break
if matched:
i += 1
continue
# 没有匹配到更长的词,pending 的内容确定可以发送
if pending:
result.append(pending)
pending = ""
# 检查当前字符是否是某个替换词的开头
if char in self._words_by_first_char:
pending = char
else:
result.append(char)
i += 1
return result, pending
def reset_stream_state(self):
"""重置流式处理状态,用于会话开始时清理残留状态"""
self._pending_prefix = ""
@@ -300,6 +300,8 @@ class TTSProvider(TTSProviderBase):
)
if message.sentence_type == SentenceType.FIRST:
# 重置流式处理状态
self.reset_stream_state()
# 初始化参数
try:
if not getattr(self.conn, "sentence_id", None):
@@ -372,8 +374,14 @@ class TTSProvider(TTSProviderBase):
filtered_text = MarkdownCleaner.clean_markdown(text)
if filtered_text:
# 发送文本
await self.send_text(self.voice, filtered_text, self.conn.sentence_id)
# 使用滑动窗口匹配处理跨分片的替换词
confirmed_texts, self._pending_prefix = self._match_stream_text(filtered_text)
# 发送每个确定的文本片段
for txt in confirmed_texts:
if txt and self.ws:
await self.send_text(self.voice, txt, self.conn.sentence_id)
return
except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
@@ -512,9 +520,9 @@ class TTSProvider(TTSProviderBase):
json_data = json.loads(res.payload.decode("utf-8"))
self.tts_text = json_data.get("text", "")
logger.bind(tag=TAG).debug(f"句子语音生成开始: {self.tts_text}")
self.tts_audio_queue.put(
(SentenceType.FIRST, [], self.tts_text)
)
# 将TTS服务返回的替换后文本还原为原始文本,用于字幕显示
display_text = self._restore_original_text(self.tts_text)
self.tts_audio_queue.put((SentenceType.FIRST, [], display_text))
elif (
res.optional.event == EVENT_TTSResponse
and res.header.message_type == AUDIO_ONLY_RESPONSE
@@ -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}
@@ -1,262 +0,0 @@
import os
import time
import queue
import aiohttp
import asyncio
import requests
import traceback
from config.logger import setup_logging
from core.utils.tts import MarkdownCleaner
from core.providers.tts.base import TTSProviderBase
from core.utils import opus_encoder_utils, textUtils
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
TAG = __name__
logger = setup_logging()
class TTSProvider(TTSProviderBase):
def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file)
self.interface_type = InterfaceType.SINGLE_STREAM
self.access_token = config.get("access_token")
self.voice = config.get("voice")
self.api_url = config.get("api_url")
self.audio_format = "pcm"
self.before_stop_play_files = []
# PCM缓冲区
self.pcm_buffer = bytearray()
def tts_text_priority_thread(self):
"""流式文本处理线程"""
while not self.conn.stop_event.is_set():
try:
message = self.tts_text_queue.get(timeout=1)
if message.sentence_type == SentenceType.FIRST:
# 初始化参数
self.tts_stop_request = False
self.processed_chars = 0
self.tts_text_buff = []
self.before_stop_play_files.clear()
elif ContentType.TEXT == message.content_type:
self.tts_text_buff.append(message.content_detail)
segment_text = self._get_segment_text()
if segment_text:
self.to_tts_single_stream(segment_text)
elif ContentType.FILE == message.content_type:
logger.bind(tag=TAG).info(
f"添加音频文件到待播放列表: {message.content_file}"
)
if message.content_file and os.path.exists(message.content_file):
# 先处理文件音频数据
self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail))
if message.sentence_type == SentenceType.LAST:
# 处理剩余的文本
self._process_remaining_text_stream(True)
except queue.Empty:
continue
except Exception as e:
logger.bind(tag=TAG).error(
f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}"
)
def _process_remaining_text_stream(self, is_last=False):
"""处理剩余的文本并生成语音
Returns:
bool: 是否成功处理了文本
"""
full_text = "".join(self.tts_text_buff)
remaining_text = full_text[self.processed_chars :]
if remaining_text:
segment_text = textUtils.get_string_no_punctuation_or_emoji(remaining_text)
if segment_text:
self.to_tts_single_stream(segment_text, is_last)
self.processed_chars += len(full_text)
else:
self._process_before_stop_play_files()
else:
self._process_before_stop_play_files()
def to_tts_single_stream(self, text, is_last=False):
try:
max_repeat_time = 5
text = MarkdownCleaner.clean_markdown(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}"
)
max_repeat_time -= 1
if max_repeat_time > 0:
logger.bind(tag=TAG).info(
f"语音生成成功: {text},重试{5 - max_repeat_time}"
)
else:
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
)
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
finally:
return None
async def text_to_speak(self, text, is_last):
"""流式处理TTS音频,每句只推送一次音频列表"""
await self._tts_request(text, is_last)
async def close(self):
"""资源清理"""
await super().close()
if hasattr(self, "opus_encoder"):
self.opus_encoder.close()
async def _tts_request(self, text: str, is_last: bool) -> None:
params = {
"tts_text": text,
"spk_id": self.voice,
"frame_durition": 60,
"stream": "true",
"target_sr": self.conn.sample_rate,
"audio_format": "pcm",
"instruct_text": "请生成一段自然流畅的语音",
}
headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json",
}
# 一帧 PCM 所需字节数:60 ms × sample_rate × 1 ch × 2 B
frame_bytes = int(
self.opus_encoder.sample_rate
* self.opus_encoder.channels # 1
* self.opus_encoder.frame_size_ms
/ 1000
* 2
) # 16-bit = 2 bytes
try:
async with aiohttp.ClientSession() as session:
async with session.get(
self.api_url, params=params, headers=headers, timeout=10
) as resp:
if resp.status != 200:
logger.bind(tag=TAG).error(
f"TTS请求失败: {resp.status}, {await resp.text()}"
)
self.tts_audio_queue.put((SentenceType.LAST, [], None))
return
self.pcm_buffer.clear()
self.tts_audio_queue.put((SentenceType.FIRST, [], text))
# 兼容 iter_chunked / iter_chunks / iter_any
async for chunk in resp.content.iter_any():
data = chunk[0] if isinstance(chunk, (list, tuple)) else chunk
if not data:
continue
# 拼到 buffer
self.pcm_buffer.extend(data)
# 够一帧就编码
while len(self.pcm_buffer) >= frame_bytes:
frame = bytes(self.pcm_buffer[:frame_bytes])
del self.pcm_buffer[:frame_bytes]
self.opus_encoder.encode_pcm_to_opus_stream(
frame,
end_of_stream=False,
callback=self.handle_opus
)
# flush 剩余不足一帧的数据
if self.pcm_buffer:
self.opus_encoder.encode_pcm_to_opus_stream(
bytes(self.pcm_buffer),
end_of_stream=True,
callback=self.handle_opus
)
self.pcm_buffer.clear()
# 如果是最后一段,输出音频获取完毕
if is_last:
self._process_before_stop_play_files()
except Exception as e:
logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
self.tts_audio_queue.put((SentenceType.LAST, [], None))
def to_tts(self, text: str) -> list:
"""非流式TTS处理,用于测试及保存音频文件的场景
Args:
text: 要转换的文本
Returns:
list: 返回opus编码后的音频数据列表
"""
start_time = time.time()
text = MarkdownCleaner.clean_markdown(text)
params = {
"tts_text": text,
"spk_id": self.voice,
"frame_duration": 60,
"stream": False,
"target_sr": self.conn.sample_rate,
"audio_format": self.audio_format,
"instruct_text": "请生成一段自然流畅的语音",
}
headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json",
}
try:
with requests.get(
self.api_url, params=params, headers=headers, timeout=5
) as response:
if response.status_code != 200:
logger.bind(tag=TAG).error(
f"TTS请求失败: {response.status_code}, {response.text}"
)
return []
logger.info(f"TTS请求成功: {text}, 耗时: {time.time() - start_time}")
# 使用opus编码器处理PCM数据
opus_datas = []
pcm_data = response.content
# 计算每帧的字节数
frame_bytes = int(
self.opus_encoder.sample_rate
* self.opus_encoder.channels
* self.opus_encoder.frame_size_ms
/ 1000
* 2
)
# 分帧处理PCM数据
for i in range(0, len(pcm_data), frame_bytes):
frame = pcm_data[i : i + frame_bytes]
if len(frame) < frame_bytes:
# 最后一帧可能不足,用0填充
frame = frame + b"\x00" * (frame_bytes - len(frame))
self.opus_encoder.encode_pcm_to_opus_stream(
frame,
end_of_stream=(i + frame_bytes >= len(pcm_data)),
callback=lambda opus: opus_datas.append(opus)
)
return opus_datas
except Exception as e:
logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
return []
@@ -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,
@@ -1,96 +0,0 @@
import os
import uuid
import json
import requests
import shutil
from datetime import datetime
from core.providers.tts.base import TTSProviderBase
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class TTSProvider(TTSProviderBase):
TTS_PARAM_CONFIG = [
("ttsVolume", "volume_change_dB", -10, 10, 0, int),
("ttsRate", "speed_factor", 0.5, 2, 0, lambda v: round(float(v), 1)),
("ttsPitch", "pitch_factor", -8, 8, 0, lambda v: round(float(v), 1)),
]
def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file)
self.url = config.get(
"url",
"https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=",
)
if config.get("private_voice"):
self.voice = int(config.get("private_voice"))
else:
self.voice = int(config.get("voice_id", 1695))
self.token = config.get("token")
self.to_lang = config.get("to_lang")
self.volume_change_dB = int(config.get("volume_change_dB", 0))
self.speed_factor = int(config.get("speed_factor", 1))
self.stream = str(config.get("stream", False)).lower() in ("true", "1", "yes")
self.output_file = config.get("output_dir")
self.pitch_factor = int(config.get("pitch_factor", 0))
self.audio_file_type = config.get("format", "mp3")
self.emotion = int(config.get("emotion", 1))
self.header = {"Content-Type": "application/json"}
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
def generate_filename(self, extension=".mp3"):
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file):
url = f"{self.url}{self.token}"
result = "firefly"
payload = json.dumps(
{
"to_lang": self.to_lang,
"text": text,
"emotion": self.emotion,
"format": self.audio_file_type,
"volume_change_dB": self.volume_change_dB,
"voice_id": self.voice,
"pitch_factor": self.pitch_factor,
"speed_factor": self.speed_factor,
"token": self.token,
}
)
resp = requests.request("POST", url, data=payload)
if resp.status_code != 200:
logger.bind(tag=TAG).error(f"TTSON 请求失败: {resp.text}")
raise Exception(f"{__name__}: TTS请求失败")
resp_json = resp.json()
try:
result = (
resp_json["url"]
+ ":"
+ str(resp_json["port"])
+ "/flashsummary/retrieveFileData?stream=True&token="
+ self.token
+ "&voice_audio_path="
+ resp_json["voice_path"]
)
audio_content = requests.get(result)
if output_file:
with open(output_file, "wb") as f:
f.write(audio_content.content)
else:
return audio_content.content
voice_path = resp_json.get("voice_path")
des_path = output_file
shutil.move(voice_path, des_path)
except Exception as e:
print("error:", e)
raise Exception(f"{__name__}: TTS请求失败")
@@ -168,6 +168,8 @@ class TTSProvider(TTSProviderBase):
)
if message.sentence_type == SentenceType.FIRST:
# 重置流式处理状态
self.reset_stream_state()
# 重置序列号
self.text_seq = 0
# 增加序列号
@@ -245,10 +247,17 @@ class TTSProvider(TTSProviderBase):
return
filtered_text = MarkdownCleaner.clean_markdown(text)
if filtered_text:
# 发送文本合成请求
run_request = self._build_base_request(status=1,text=filtered_text)
await self.ws.send(json.dumps(run_request))
# 使用滑动窗口匹配处理跨分片的替换词
confirmed_texts, self._pending_prefix = self._match_stream_text(filtered_text)
# 发送每个确定的文本片段
for txt in confirmed_texts:
if txt and self.ws:
# 发送文本合成请求
run_request = self._build_base_request(status=1, text=txt)
await self.ws.send(json.dumps(run_request))
return
except Exception as e:
@@ -441,6 +450,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)
+67 -76
View File
@@ -61,67 +61,35 @@ class Dialogue:
else:
self.put(Message(role="system", content=new_content))
def trim_history(self, max_turns: int = 10) -> int:
def _ensure_tool_calls_complete(self, messages: List[Message]) -> List[Message]:
"""
智能截断对话历史保留工具调用的完整性
Args:
max_turns: 保留的最大对话轮数每轮 = user + assistant/tool 相关消息
Returns:
int: 被移除的消息数量
确保所有 tool_calls 都有对应的 tool 响应
修复被打断导致的悬空 tool_calls防止大模型 API 400 错误
"""
if len(self.dialogue) <= max_turns * 2 + 1: # +1 是系统消息
return 0
pending_tool_calls = set()
result = []
# 分离系统消息和对话消息
system_messages = [msg for msg in self.dialogue if msg.role == "system"]
conversation_messages = [msg for msg in self.dialogue if msg.role != "system"]
for msg in messages:
result.append(msg)
if len(conversation_messages) <= max_turns * 2:
return 0
if msg.role == "assistant" and msg.tool_calls:
for tc in msg.tool_calls:
tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", None)
if tc_id:
pending_tool_calls.add(tc_id)
# 智能截断:保留完整的工具调用链路
keep_messages = []
i = len(conversation_messages) - 1
turn_count = 0
elif msg.role == "tool" and msg.tool_call_id:
pending_tool_calls.discard(msg.tool_call_id)
while i >= 0 and turn_count < max_turns:
msg = conversation_messages[i]
for missing_id in pending_tool_calls:
dummy_tool_msg = Message(
role="tool",
content='{"status": "interrupted", "message": "动作已取消/被打断"}',
tool_call_id=missing_id
)
result.append(dummy_tool_msg)
# 从后向前收集消息
if msg.role == "user":
# 遇到 user 消息,说明一轮对话开始
keep_messages.insert(0, msg)
turn_count += 1
i -= 1
elif msg.role == "assistant":
# 收集 assistant 消息
keep_messages.insert(0, msg)
# 如果这个 assistant 有 tool_calls,需要收集对应的 tool 响应
if msg.tool_calls is not None:
i -= 1
# 继续向后收集所有相关的 tool 消息
while i >= 0 and conversation_messages[i].role == "tool":
keep_messages.insert(0, conversation_messages[i])
i -= 1
else:
i -= 1
elif msg.role == "tool":
# tool 消息应该已经被上面的逻辑收集了
# 如果单独遇到,也要保留(防止边界情况)
keep_messages.insert(0, msg)
i -= 1
else:
i -= 1
removed_count = len(conversation_messages) - len(keep_messages)
# 重建对话列表
self.dialogue = system_messages + keep_messages
return removed_count
return result
def get_llm_dialogue_with_memory(
self, memory_str: str = None, voiceprint_config: dict = None
@@ -135,48 +103,71 @@ class Dialogue:
)
if system_message:
# 基础系统提示
enhanced_system_prompt = system_message.content
# 以 <context> 为分界点,拆分静态 system prompt 和动态上下文
# 静态部分(规则、身份等)保持不变,可命中前缀缓存
# 动态部分(时间、天气、记忆等)作为第二条 system 消息,保持 system 权威性
full_prompt = system_message.content
context_match = re.search(r"<context>", full_prompt)
if context_match:
static_part = full_prompt[:context_match.start()]
dynamic_part = full_prompt[context_match.start():]
else:
static_part = full_prompt
dynamic_part = ""
# 第一段:静态 system prompt(前缀缓存可命中)
dialogue.append({"role": "system", "content": static_part})
# 第二段:few-shot 示例(会话内不变,也是缓存前缀的一部分)
non_system_messages = [m for m in self.dialogue if m.role != "system"]
fewshot_messages = [m for m in non_system_messages if m.is_temporary]
complete_fewshot = self._ensure_tool_calls_complete(fewshot_messages)
for m in complete_fewshot:
self.getMessages(m, dialogue)
# 第三段:动态上下文 system prompt(时间、记忆、说话人等)
# 保持 system 角色以确保模型权威性,不降级为 user
if system_message and dynamic_part:
# 替换时间占位符
enhanced_system_prompt = enhanced_system_prompt.replace(
dynamic_part = dynamic_part.replace(
"{{current_time}}", datetime.now().strftime("%H:%M")
)
# 添加说话人个性化描述
# 填充记忆
if memory_str is not None:
dynamic_part = re.sub(
r"<memory>.*?</memory>",
f"<memory>\n{memory_str}\n</memory>",
dynamic_part,
flags=re.DOTALL,
)
# 追加说话人信息
try:
speakers = voiceprint_config.get("speakers", [])
if speakers:
enhanced_system_prompt += "\n\n<speakers_info>"
dynamic_part += "\n<speakers_info>"
for speaker_str in speakers:
try:
parts = speaker_str.split(",", 2)
if len(parts) >= 2:
name = parts[1].strip()
# 如果描述为空,则为""
description = (
parts[2].strip() if len(parts) >= 3 else ""
)
enhanced_system_prompt += f"\n- {name}{description}"
dynamic_part += f"\n- {name}{description}"
except:
pass
enhanced_system_prompt += "\n\n</speakers_info>"
dynamic_part += "\n</speakers_info>"
except:
# 配置读取失败时忽略错误,不影响其他功能
pass
# 使用正则表达式匹配 <memory> 标签,不管中间有什么内容
if memory_str is not None:
enhanced_system_prompt = re.sub(
r"<memory>.*?</memory>",
f"<memory>\n{memory_str}\n</memory>",
enhanced_system_prompt,
flags=re.DOTALL,
)
dialogue.append({"role": "system", "content": enhanced_system_prompt})
dialogue.append({"role": "system", "content": dynamic_part})
# 添加用户和助手的对话
for m in self.dialogue:
if m.role != "system": # 跳过原始的系统消息
self.getMessages(m, dialogue)
# 第四段:实际对话历史(不含 few-shot)
actual_messages = [m for m in non_system_messages if not m.is_temporary]
complete_actual = self._ensure_tool_calls_complete(actual_messages)
for m in complete_actual:
self.getMessages(m, dialogue)
return dialogue
@@ -119,7 +119,7 @@ class PromptManager:
# 使用传入的提示词并缓存(如果有设备ID)
if device_id:
device_cache_key = f"device_prompt:{device_id}"
self.cache_manager.set(self.CacheType.CONFIG, device_cache_key, user_prompt)
self.cache_manager.set(self.CacheType.DEVICE_PROMPT, device_cache_key, user_prompt)
self.logger.bind(tag=TAG).debug(f"设备 {device_id} 的提示词已缓存")
self.logger.bind(tag=TAG).info(f"使用快速提示词: {user_prompt[:50]}...")
@@ -68,7 +68,7 @@ class ASRPerformanceTester:
"""测试单个音频文件的性能"""
try:
start_time = time.time()
text, _ = await stt.speech_to_text([audio_data], "1", stt.audio_format)
text, _ = await stt.speech_to_text_wrapper([audio_data], "1", stt.audio_format)
if text is None:
return None
@@ -2,6 +2,7 @@ import asyncio
import logging
import os
import time
import threading
from typing import Dict
import yaml
from tabulate import tabulate
@@ -44,6 +45,21 @@ class TTSPerformanceTester:
module_type = config.get("type", tts_name)
tts = create_tts_instance(module_type, config, delete_audio_file=True)
# 设置 mock conn 对象,避免 TTS 实现访问 self.conn.sample_rate 时为 None
class MockConn:
sample_rate = 16000
audio_format = "pcm"
stop_event = threading.Event() # 需要是真正的 Event 对象
client_abort = False
headers = {}
tts.conn = MockConn()
# 设置 mock opus_encoder,避免某些 TTS 访问 self.opus_encoder 时为 None
class MockOpusEncoder:
pass
if not hasattr(tts, 'opus_encoder') or tts.opus_encoder is None:
tts.opus_encoder = MockOpusEncoder()
print(f"测试 TTS: {tts_name}")
# 连接测试
@@ -18,10 +18,9 @@ GET_NEWS_FROM_CHINANEWS_FUNCTION_DESC = {
"function": {
"name": "get_news_from_chinanews",
"description": (
"获取最新新闻,随机选择一条新闻进行播报"
"当用户要求查看或收听新闻时调用(如'来条新闻''今天有什么新闻'"
"用户可以指定新闻类型,如社会新闻、科技新闻、国际新闻等。"
"如果没有指定,默认播报社会新闻。"
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
),
"parameters": {
"type": "object",
@@ -51,33 +51,36 @@ CHANNEL_MAP = {
"虫部落": "chongbuluo-latest",
}
# 默认新闻来源字典,当配置中没有指定时使用
DEFAULT_NEWS_SOURCES = "澎湃新闻;百度热搜;财联社"
def _get_newsnow_config(conn):
"""从连接配置中获取newsnow插件配置,优先用conn.common_config,兜底用conn.config"""
# 优先从公共配置获取(保留本地config.yaml的配置)
common_plugins = getattr(conn, "common_config", {}).get("plugins", {})
common_newsnow = common_plugins.get("get_news_from_newsnow", {})
common_sources = common_newsnow.get("news_sources", "")
if isinstance(common_sources, str) and common_sources.strip():
return common_sources
# 兜底从连接配置获取
plugins = conn.config.get("plugins", {})
newsnow = plugins.get("get_news_from_newsnow", {})
sources = newsnow.get("news_sources", "")
if isinstance(sources, str) and sources.strip():
return sources
return ""
def get_news_sources_from_config(conn):
"""从配置中获取新闻源字符串"""
try:
# 尝试从插件配置中获取新闻源
if (
conn.config.get("plugins")
and conn.config["plugins"].get("get_news_from_newsnow")
and conn.config["plugins"]["get_news_from_newsnow"].get("news_sources")
):
# 获取配置的新闻源字符串
news_sources_config = conn.config["plugins"]["get_news_from_newsnow"][
"news_sources"
]
if isinstance(news_sources_config, str) and news_sources_config.strip():
logger.bind(tag=TAG).debug(f"使用配置的新闻源: {news_sources_config}")
return news_sources_config
else:
logger.bind(tag=TAG).warning("新闻源配置为空或格式错误,使用默认配置")
else:
logger.bind(tag=TAG).debug("未找到新闻源配置,使用默认配置")
result = _get_newsnow_config(conn)
if result:
logger.bind(tag=TAG).debug(f"使用配置的新闻源: {result}")
return result
logger.bind(tag=TAG).debug("未找到新闻源配置,使用默认配置")
return DEFAULT_NEWS_SOURCES
except Exception as e:
@@ -85,20 +88,32 @@ def get_news_sources_from_config(conn):
return DEFAULT_NEWS_SOURCES
# 从CHANNEL_MAP获取所有可用的新闻源名称
available_sources = list(CHANNEL_MAP.keys())
example_sources_str = "".join(available_sources)
# 从默认配置获取可用的新闻源名称(运行时由get_news_sources_from_config动态获取)
example_sources_str = DEFAULT_NEWS_SOURCES.replace(";","")
def init_news_description(config: dict):
"""项目启动时调用一次,根据配置更新工具描述中的新闻源示例"""
from types import SimpleNamespace
from plugins_func.register import all_function_registry
# 复用get_news_sources_from_config,用SimpleNamespace模拟conn
conn_wrapper = SimpleNamespace(config=config)
news_sources = get_news_sources_from_config(conn_wrapper)
sources_str = news_sources.replace(";","")
func_item = all_function_registry.get("get_news_from_newsnow")
if func_item:
func_item.description["function"]["parameters"]["properties"]["source"][
"description"
] = f"新闻源的标准中文名称,例如{sources_str}等。可选参数,如果不提供则使用默认新闻源"
logger.bind(tag=TAG).info(f"新闻工具描述已初始化,可用新闻源: {sources_str}")
GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC = {
"type": "function",
"function": {
"name": "get_news_from_newsnow",
"description": (
"获取最新新闻,随机选择一条新闻进行播报。"
f"用户可以选择不同的新闻源,标准的名称是:{example_sources_str}"
"例如用户要求百度新闻,其实就是百度热搜。如果没有指定,默认从澎湃新闻获取。"
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
),
"description": "当用户要求查看或收听新闻时调用(如'来条新闻''今天有什么新闻')。",
"parameters": {
"type": "object",
"properties": {
@@ -233,7 +248,7 @@ def get_news_from_newsnow(
# f"新闻来源: {source_name}\n"
f"详细内容: {detail_content}\n\n"
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
f"不要提及这是总结,就像是在讲述一个完整的新闻)"
)
return ActionResponse(Action.REQLLM, detail_report, None)
@@ -18,7 +18,7 @@ GET_WEATHER_FUNCTION_DESC = {
"description": (
"获取某个地点的天气,用户应提供一个位置,比如用户说杭州天气,参数为:杭州。"
"如果用户说的是省份,默认用省会城市。如果用户说的不是省份或城市而是一个地名,默认用该地所在省份的省会城市。"
"如果用户没有指明地点,说“天气怎么样”,”今天天气如何“,location参数为空"
"重要:本地未来7天天气已在上下文中提供,用户未指明其他城市时绝对不要调用此工具。"
),
"parameters": {
"type": "object",
@@ -22,7 +22,7 @@ play_music_function_desc = {
"type": "function",
"function": {
"name": "play_music",
"description": "唱歌、听歌、播放音乐的方法",
"description": "当用户要求播放音乐、歌曲时调用",
"parameters": {
"type": "object",
"properties": {
@@ -67,7 +67,7 @@ def play_music(conn: "ConnectionHandler", song_name: str):
task.add_done_callback(handle_done)
return ActionResponse(
action=Action.NONE, result="指令已接收", response="正在为您播放音乐"
action=Action.RECORD, result="指令已接收", response="正在为您播放音乐"
)
except Exception as e:
conn.logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
@@ -217,7 +217,8 @@ async def play_local_music(conn: "ConnectionHandler", specific_file=None):
conn.logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
return
text = _get_random_play_prompt(selected_music)
conn.dialogue.put(Message(role="assistant", content=text))
conn.tts.store_tts_text(conn.sentence_id, text)
# conn.dialogue.put(Message(role="assistant", content=text))
if conn.intent_type == "intent_llm":
conn.tts.tts_text_queue.put(
@@ -28,6 +28,7 @@ class Action(Enum):
NONE = (1, "啥也不干")
RESPONSE = (2, "直接回复")
REQLLM = (3, "调用函数后再请求llm生成回复")
RECORD = (4, "记录工具调用到对话历史,不调用LLM")
def __init__(self, code, message):
self.code = code
+1 -1
View File
@@ -15,7 +15,7 @@ openai==2.8.1
google-generativeai==0.8.5
edge_tts==7.2.6
httpx==0.28.1
aiohttp==3.13.2
aiohttp==3.13.5
aiohttp_cors==0.8.1
ormsgpack==1.12.0
ruamel.yaml==0.18.16