Merge pull request #2311 from xinnan-tech/voice-clone

update:智控台音色克隆
This commit is contained in:
欣南科技
2025-10-07 23:57:56 +08:00
committed by GitHub
42 changed files with 4140 additions and 531 deletions
@@ -257,7 +257,7 @@ public interface Constant {
/**
* 版本号
*/
public static final String VERSION = "0.8.3";
public static final String VERSION = "0.8.4";
/**
* 无效固件URL
@@ -175,4 +175,17 @@ public interface ErrorCode {
int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效
int CHAT_ROLE_USER = 10138; // 用户角色
int CHAT_ROLE_AGENT = 10139; // 智能体角色
// 声音克隆相关错误码
int VOICE_CLONE_AUDIO_EMPTY = 10140; // 音频文件不能为空
int VOICE_CLONE_NOT_AUDIO_FILE = 10141; // 只支持音频文件
int VOICE_CLONE_AUDIO_TOO_LARGE = 10142; // 音频文件大小不能超过10MB
int VOICE_CLONE_UPLOAD_FAILED = 10143; // 上传失败
int VOICE_CLONE_RECORD_NOT_EXIST = 10144; // 声音克隆记录不存在
int VOICE_RESOURCE_INFO_EMPTY = 10145; // 音色资源信息不能为空
int VOICE_RESOURCE_PLATFORM_NAME_EMPTY = 10146; // 平台名称不能为空
int VOICE_RESOURCE_ID_EMPTY = 10147; // 音色ID不能为空
int VOICE_RESOURCE_ACCOUNT_EMPTY = 10148; // 归属账号不能为空
int VOICE_RESOURCE_DELETE_ID_EMPTY = 10149; // 删除的音色资源ID不能为空
int VOICE_RESOURCE_NO_PERMISSION = 10150; // 您没有权限操作该记录
}
@@ -146,4 +146,10 @@ public class RedisKeys {
return "agent:chat:history:" + uuid;
}
/**
* 获取音色克隆音频ID的缓存key
*/
public static String getVoiceCloneAudioIdKey(String uuid) {
return "voiceClone:audio:id:" + uuid;
}
}
@@ -12,8 +12,6 @@ import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -49,7 +47,6 @@ import xiaozhi.modules.security.user.SecurityUser;
@RestController
@RequestMapping("/agent/chat-history")
public class AgentChatHistoryController {
private static final Logger logger = LoggerFactory.getLogger(AgentChatHistoryController.class);
private final AgentChatHistoryBizService agentChatHistoryBizService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentService agentService;
@@ -1,6 +1,7 @@
package xiaozhi.modules.model.dao;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -15,4 +16,9 @@ public interface ModelConfigDao extends BaseDao<ModelConfigEntity> {
* get model_code list
*/
List<String> getModelCodeList(@Param("modelType") String modelType, @Param("modelName") String modelName);
/**
* 获取符合条件的TTS平台列表(id和modelName)
*/
List<Map<String, Object>> getTtsPlatformList();
}
@@ -1,6 +1,7 @@
package xiaozhi.modules.model.service;
import java.util.List;
import java.util.Map;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
@@ -53,9 +54,16 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
/**
* 设置默认模型
*
*
* @param modelType 模型类型
* @param isDefault 是否默认(1:是,0:否)
*/
void setDefaultModel(String modelType, int isDefault);
/**
* 获取符合条件的TTS平台列表
*
* @return TTS平台列表(id和modelName)
*/
List<Map<String, Object>> getTtsPlatformList();
}
@@ -516,4 +516,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
}
}
}
/**
* 获取符合条件的TTS平台列表
*/
@Override
public List<Map<String, Object>> getTtsPlatformList() {
return modelConfigDao.getTtsPlatformList();
}
}
@@ -91,6 +91,7 @@ public class ShiroConfig {
filterMap.put("/agent/chat-history/download/**", "anon");
filterMap.put("/agent/saveMemory/**", "server");
filterMap.put("/agent/play/**", "anon");
filterMap.put("/voiceClone/play/**", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
@@ -0,0 +1,190 @@
package xiaozhi.modules.voiceclone.controller;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
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.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
@Tag(name = "音色资源管理", description = "音色资源开通相关接口")
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/voiceClone")
public class VoiceCloneController {
private final VoiceCloneService voiceCloneService;
private final RedisUtils redisUtils;
@GetMapping
@Operation(summary = "分页查询音色资源")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
})
@RequiresPermissions("sys:role:normal")
public Result<PageData<VoiceCloneResponseDTO>> page(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
ValidatorUtils.validateEntity(params);
UserDetail user = SecurityUser.getUser();
params.put("userId", user.getId().toString());
PageData<VoiceCloneResponseDTO> page = voiceCloneService.pageWithNames(params);
return new Result<PageData<VoiceCloneResponseDTO>>().ok(page);
}
@PostMapping("/upload")
@Operation(summary = "上传音频进行声音克隆")
@Parameters({
@Parameter(name = "id", description = "声音克隆记录ID", required = true),
@Parameter(name = "voiceFile", description = "音频文件", required = true)
})
@RequiresPermissions("sys:role:normal")
public Result<String> uploadVoice(
@RequestParam("id") String id,
@RequestParam("voiceFile") MultipartFile voiceFile) {
try {
// 验证文件
if (voiceFile == null || voiceFile.isEmpty()) {
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_EMPTY);
}
// 验证文件类型
String contentType = voiceFile.getContentType();
if (contentType == null || !contentType.startsWith("audio/")) {
return new Result<String>().error(ErrorCode.VOICE_CLONE_NOT_AUDIO_FILE);
}
// 验证文件大小 (最大10MB)
if (voiceFile.getSize() > 10 * 1024 * 1024) {
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE);
}
// 检查权限
checkPermission(id);
// 调用服务层处理
voiceCloneService.uploadVoice(id, voiceFile);
return new Result<String>();
} catch (Exception e) {
return new Result<String>().error(ErrorCode.VOICE_CLONE_UPLOAD_FAILED, e.getMessage());
}
}
@PostMapping("/updateName")
@Operation(summary = "更新声音克隆名称")
@RequiresPermissions("sys:role:normal")
public Result<String> updateName(@RequestBody Map<String, String> params) {
try {
String id = params.get("id");
String name = params.get("name");
if (id == null || id.isEmpty()) {
return new Result<String>().error(ErrorCode.IDENTIFIER_NOT_NULL, "唯一标识不能为空");
}
if (name == null) {
return new Result<String>().error(ErrorCode.NOT_NULL, "名称不能为空");
}
// 检查权限
checkPermission(id);
voiceCloneService.updateName(id, name);
return new Result<String>();
} catch (Exception e) {
return new Result<String>().error(ErrorCode.UPDATE_DATA_FAILED, "更新失败: " + e.getMessage());
}
}
@PostMapping("/audio/{id}")
@Operation(summary = "获取音频下载ID")
@RequiresPermissions("sys:role:normal")
public Result<String> getAudioId(@PathVariable("id") String id) {
// 检查权限
checkPermission(id);
byte[] audioData = voiceCloneService.getVoiceData(id);
if (audioData == null) {
return new Result<String>().error(ErrorCode.RESOURCE_NOT_FOUND, "音频不存在");
}
String uuid = UUID.randomUUID().toString();
redisUtils.set(RedisKeys.getVoiceCloneAudioIdKey(uuid), id);
return new Result<String>().ok(uuid);
}
@GetMapping("/play/{uuid}")
@Operation(summary = "播放音频")
public void playVoice(@PathVariable("uuid") String uuid, HttpServletResponse response) {
try {
String id = (String) redisUtils.get(RedisKeys.getVoiceCloneAudioIdKey(uuid));
redisUtils.delete(RedisKeys.getVoiceCloneAudioIdKey(uuid));
if (StringUtils.isBlank(id)) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 获取音频数据
byte[] voiceData = voiceCloneService.getVoiceData(id);
if (voiceData == null || voiceData.length == 0) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
response.setContentType("audio/wav");
response.setContentLength(voiceData.length);
response.setHeader("Content-Disposition", "inline; filename=voice.wav");
// 写入音频数据
response.getOutputStream().write(voiceData);
response.getOutputStream().flush();
} catch (Exception e) {
log.error("播放音频失败", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
@PostMapping("/cloneAudio")
@Operation(summary = "复刻音频")
@RequiresPermissions("sys:role:normal")
public Result<String> cloneAudio(@RequestBody Map<String, String> params) {
String cloneId = params.get("cloneId");
checkPermission(cloneId);
return new Result<String>();
}
private void checkPermission(String id) {
VoiceCloneEntity voiceClone = voiceCloneService.selectById(id);
if (voiceClone == null) {
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
}
if (!voiceClone.getUserId().equals(SecurityUser.getUser().getId())) {
throw new RenException(ErrorCode.VOICE_RESOURCE_NO_PERMISSION);
}
}
}
@@ -0,0 +1,114 @@
package xiaozhi.modules.voiceclone.controller;
import java.util.List;
import java.util.Map;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
@Tag(name = "音色资源管理", description = "音色资源开通相关接口")
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/voiceResource")
public class VoiceResourceController {
private final VoiceCloneService voiceCloneService;
private final ModelConfigService modelConfigService;
@GetMapping
@Operation(summary = "分页查询音色资源")
@Parameters({
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
})
@RequiresPermissions("sys:role:superAdmin")
public Result<PageData<VoiceCloneResponseDTO>> page(
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
ValidatorUtils.validateEntity(params);
PageData<VoiceCloneResponseDTO> page = voiceCloneService.pageWithNames(params);
return new Result<PageData<VoiceCloneResponseDTO>>().ok(page);
}
@GetMapping("{id}")
@Operation(summary = "获取音色资源详情")
@RequiresPermissions("sys:role:superAdmin")
public Result<VoiceCloneResponseDTO> get(@PathVariable("id") String id) {
VoiceCloneResponseDTO data = voiceCloneService.getByIdWithNames(id);
return new Result<VoiceCloneResponseDTO>().ok(data);
}
@PostMapping
@Operation(summary = "新增音色资源")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> save(@RequestBody VoiceCloneDTO dto) {
if (dto == null) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_INFO_EMPTY);
}
if (dto.getModelId() == null || dto.getModelId().isEmpty()) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_PLATFORM_NAME_EMPTY);
}
if (dto.getVoiceIds() == null || dto.getVoiceIds().isEmpty()) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_ID_EMPTY);
}
if (dto.getUserId() == null) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_ACCOUNT_EMPTY);
}
try {
voiceCloneService.save(dto);
return new Result<Void>();
} catch (RuntimeException e) {
return new Result<Void>().error(ErrorCode.ADD_DATA_FAILED, e.getMessage());
}
}
@DeleteMapping("/{id}")
@Operation(summary = "删除音色资源")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> delete(@PathVariable("id") String[] ids) {
if (ids == null || ids.length == 0) {
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_DELETE_ID_EMPTY);
}
voiceCloneService.delete(ids);
return new Result<Void>();
}
@GetMapping("/user/{userId}")
@Operation(summary = "根据用户ID获取音色资源列表")
@RequiresPermissions("sys:role:normal")
public Result<List<VoiceCloneResponseDTO>> getByUserId(@PathVariable("userId") Long userId) {
List<VoiceCloneResponseDTO> list = voiceCloneService.getByUserIdWithNames(userId);
return new Result<List<VoiceCloneResponseDTO>>().ok(list);
}
@GetMapping("/ttsPlatforms")
@Operation(summary = "获取TTS平台列表")
@RequiresPermissions("sys:role:superAdmin")
public Result<List<Map<String, Object>>> getTtsPlatformList() {
List<Map<String, Object>> list = modelConfigService.getTtsPlatformList();
return new Result<List<Map<String, Object>>>().ok(list);
}
}
@@ -0,0 +1,15 @@
package xiaozhi.modules.voiceclone.dao;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
/**
* 声音克隆
*/
@Mapper
public interface VoiceCloneDao extends BaseMapper<VoiceCloneEntity> {
}
@@ -0,0 +1,20 @@
package xiaozhi.modules.voiceclone.dto;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "声音克隆DTO")
public class VoiceCloneDTO {
@Schema(description = "模型ID")
private String modelId;
@Schema(description = "音色ID列表")
private List<String> voiceIds;
@Schema(description = "用户ID")
private Long userId;
}
@@ -0,0 +1,48 @@
package xiaozhi.modules.voiceclone.dto;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 声音克隆响应DTO
* 用于向前端展示声音克隆信息,包含模型名称和用户名称
*/
@Data
@Schema(description = "声音克隆响应DTO")
public class VoiceCloneResponseDTO {
@Schema(description = "唯一标识")
private String id;
@Schema(description = "声音名称")
private String name;
@Schema(description = "模型id")
private String modelId;
@Schema(description = "模型名称")
private String modelName;
@Schema(description = "声音id")
private String voiceId;
@Schema(description = "用户ID(关联用户表)")
private Long userId;
@Schema(description = "用户名称")
private String userName;
@Schema(description = "训练状态:0待训练 1训练中 2训练成功 3训练失败")
private Integer trainStatus;
@Schema(description = "训练错误原因")
private String trainError;
@Schema(description = "创建时间")
private Date createDate;
@Schema(description = "是否有音频数据")
private Boolean hasVoice;
}
@@ -0,0 +1,53 @@
package xiaozhi.modules.voiceclone.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ai_voice_clone")
@Schema(description = "声音克隆")
public class VoiceCloneEntity {
@TableId(type = IdType.ASSIGN_UUID)
@Schema(description = "唯一标识")
private String id;
@Schema(description = "声音名称")
private String name;
@Schema(description = "模型id")
private String modelId;
@Schema(description = "声音id")
private String voiceId;
@Schema(description = "用户 ID(关联用户表)")
private Long userId;
@Schema(description = "声音")
private byte[] voice;
@Schema(description = "训练状态:0待训练 1训练中 2训练成功 3训练失败")
private Integer trainStatus;
@Schema(description = "训练错误原因")
private String trainError;
@Schema(description = "创建者")
@TableField(fill = FieldFill.INSERT)
private Long creator;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date createDate;
}
@@ -0,0 +1,68 @@
package xiaozhi.modules.voiceclone.service;
import java.util.List;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
/**
* 声音克隆管理
*/
public interface VoiceCloneService extends BaseService<VoiceCloneEntity> {
/**
* 分页查询
*/
PageData<VoiceCloneEntity> page(Map<String, Object> params);
/**
* 保存声音克隆
*/
void save(VoiceCloneDTO dto);
/**
* 批量删除
*/
void delete(String[] ids);
/**
* 根据用户ID查询声音克隆列表
*/
List<VoiceCloneEntity> getByUserId(Long userId);
/**
* 分页查询带模型名称和用户名称的声音克隆列表
*/
PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params);
/**
* 根据ID查询带模型名称和用户名称的声音克隆信息
*/
VoiceCloneResponseDTO getByIdWithNames(String id);
/**
* 根据用户ID查询带模型名称的声音克隆列表
*/
List<VoiceCloneResponseDTO> getByUserIdWithNames(Long userId);
/**
* 上传音频文件
*/
void uploadVoice(String id, MultipartFile voiceFile) throws Exception;
/**
* 更新声音克隆名称
*/
void updateName(String id, String name);
/**
* 获取音频数据
*/
byte[] getVoiceData(String id);
}
@@ -0,0 +1,210 @@
package xiaozhi.modules.voiceclone.service.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.RequiredArgsConstructor;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.sys.service.SysUserService;
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
@Service
@RequiredArgsConstructor
public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceCloneEntity>
implements VoiceCloneService {
private final ModelConfigService modelConfigService;
private final SysUserService sysUserService;
@Override
public PageData<VoiceCloneEntity> page(Map<String, Object> params) {
IPage<VoiceCloneEntity> page = baseDao.selectPage(
getPage(params, "create_date", true),
getWrapper(params));
return new PageData<>(page.getRecords(), page.getTotal());
}
private QueryWrapper<VoiceCloneEntity> getWrapper(Map<String, Object> params) {
String name = (String) params.get("name");
String userId = (String) params.get("userId");
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
wrapper.eq(StringUtils.isNotBlank(userId), "user_id", userId);
if (StringUtils.isNotBlank(name)) {
wrapper.and(w -> w.like("name", name)
.or().eq("voice_id", name));
}
return wrapper;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void save(VoiceCloneDTO dto) {
// 遍历选择的音色ID,为每个音色ID创建一条记录
int index = 0;
String namePrefix = DateUtils.format(new java.util.Date(), "MMddHHmm");
for (String voiceId : dto.getVoiceIds()) {
index++;
VoiceCloneEntity entity = new VoiceCloneEntity();
entity.setModelId(dto.getModelId());
entity.setVoiceId(voiceId);
entity.setName(namePrefix + "_" + index);
entity.setUserId(dto.getUserId());
entity.setTrainStatus(0); // 默认训练中
baseDao.insert(entity);
}
}
@Override
public void delete(String[] ids) {
baseDao.deleteBatchIds(Arrays.asList(ids));
}
@Override
public List<VoiceCloneEntity> getByUserId(Long userId) {
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
wrapper.orderByDesc("created_at");
return baseDao.selectList(wrapper);
}
@Override
public PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params) {
// 先查询分页数据
IPage<VoiceCloneEntity> page = baseDao.selectPage(
getPage(params, "create_date", true),
getWrapper(params));
// 将实体列表转换为DTO列表
List<VoiceCloneResponseDTO> dtoList = convertToResponseDTOList(page.getRecords());
return new PageData<>(dtoList, page.getTotal());
}
@Override
public VoiceCloneResponseDTO getByIdWithNames(String id) {
VoiceCloneEntity entity = baseDao.selectById(id);
if (entity == null) {
return null;
}
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
// 设置模型名称
if (StringUtils.isNotBlank(entity.getModelId())) {
dto.setModelName(modelConfigService.getModelNameById(entity.getModelId()));
}
// 设置用户名称
if (entity.getUserId() != null) {
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
}
return dto;
}
@Override
public List<VoiceCloneResponseDTO> getByUserIdWithNames(Long userId) {
List<VoiceCloneEntity> entityList = getByUserId(userId);
return convertToResponseDTOList(entityList);
}
/**
* 将VoiceCloneEntity列表转换为VoiceCloneResponseDTO列表
*/
private List<VoiceCloneResponseDTO> convertToResponseDTOList(List<VoiceCloneEntity> entityList) {
if (entityList == null || entityList.isEmpty()) {
return new ArrayList<>();
}
List<VoiceCloneResponseDTO> dtoList = new ArrayList<>(entityList.size());
// 转换每个实体为DTO
for (VoiceCloneEntity entity : entityList) {
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
// 设置模型名称
if (StringUtils.isNotBlank(entity.getModelId())) {
dto.setModelName(modelConfigService.getModelNameById(entity.getModelId()));
}
// 设置用户名称
if (entity.getUserId() != null) {
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
}
// 设置是否有音频数据
dto.setHasVoice(entity.getVoice() != null);
dtoList.add(dto);
}
return dtoList;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void uploadVoice(String id, MultipartFile voiceFile) throws Exception {
// 查询声音克隆记录
VoiceCloneEntity entity = baseDao.selectById(id);
if (entity == null) {
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
}
// 读取音频文件并转为字节数组
byte[] voiceData = voiceFile.getBytes();
// 更新voice字段
entity.setVoice(voiceData);
// 更新训练状态为待训练
entity.setTrainStatus(0);
// 保存到数据库
baseDao.updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateName(String id, String name) {
// 查询声音克隆记录
VoiceCloneEntity entity = baseDao.selectById(id);
if (entity == null) {
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
}
// 更新名称
entity.setName(name);
baseDao.updateById(entity);
}
@Override
public byte[] getVoiceData(String id) {
VoiceCloneEntity entity = baseDao.selectById(id);
if (entity == null) {
return null;
}
return entity.getVoice();
}
}
@@ -0,0 +1,15 @@
-- 声音克隆表
DROP TABLE IF EXISTS `ai_voice_clone`;
CREATE TABLE `ai_voice_clone` (
`id` VARCHAR(32) NOT NULL COMMENT '唯一标识',
`name` VARCHAR(64) COMMENT '声音名称',
`model_id` VARCHAR(32) COMMENT '模型id',
`voice_id` VARCHAR(32) COMMENT '声音id',
`user_id` BIGINT COMMENT '用户 ID(关联用户表)',
`voice` LONGBLOB COMMENT '声音',
`train_status` TINYINT(1) DEFAULT 0 COMMENT '训练状态:0待训练 1训练中 2训练成功 3训练失败',
`train_error` VARCHAR(255) COMMENT '训练错误原因',
`creator` BIGINT COMMENT '创建者 ID',
`create_date` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声音克隆表';
@@ -388,3 +388,10 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509220958.sql
- changeSet:
id: 202510071521
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202510071521.sql
@@ -1,146 +1,157 @@
#简体中文
500=服务器内部异常
401=未授权
403=拒绝访问,没有权限
#\u7B80\u4F53\u4E2D\u6587
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=\u672A\u6388\u6743
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}不能为空
10002=数据库中已存在该记录
10003=获取参数失败
10004=账号或密码错误
10005=账号已被停用
10006=唯一标识不能为空
10007=验证码不正确
10008=先删除子菜单或按钮
10009=原密码不正确
10010=账号或密码不正确,您还有可以尝试{0}次
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=上级部门选择错误
10012=上级菜单不能为自身
10013=数据权限接口,只能是Map类型参数
10014=请先删除下级部门
10015=请先删除部门下的用户
10016=部署失败,没有流程
10017=模型图不正确,请检查
10018=导出失败,模型ID为{0}
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=请上传文件
10020=token不能为空
10021=token失效,请重新登录
10022=账号已被锁定
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上传文件失败{0}
10025=发送短信失败{0}
10026=邮件模板不存在
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服务异常
10028=定时任务失败
10029=不能包含非法字符
10030=密码长度不足{0}位
10031=密码必须同时包含数字、大小写字母和特殊字符组成
10032=删除本数据异常
10033=设备验证码错误
10027=Redis\u670D\u52A1\u5F02\u5E38
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5199\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=参数值不能为空
10035=参数类型不能为空
10036=不支持的参数类型
10037=参数值必须是有效的数字
10038=参数值必须是true或false
10039=参数值必须是有效的JSON数组格式
10040=参数值必须是有效的JSON格式
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=设备未找到
10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0}
10043=删除数据失败
10044=用户未登录
10045=WebSocket连接失败或连接超时
10046=保存声纹错误,请联系管理员
10047=每日发送次数已达上限
10048=旧密码输入错误
10049=设置的LLM不是openai和ollama
10050=token生成失败
10051=资源不存在
10052=默认智能体未找到
10053=智能体未找到
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
10055=短信发送失败
10056=短信连接建立失败
10057=智能体的声纹创建失败
10058=智能体的对应声纹更新失败
10059=智能体的对应声纹删除失败
10060=发送太频繁,请{0}秒后再试
10061=激活码不能为空
10062=激活码错误
10063=设备已激活
10064=该模型为默认模型,请先设置其他模型为默认模型
10065=新增数据失败
10066=修改数据失败
10067=图形验证码错误
10068=没有开启手机注册,没法使用短信验证码功能
10069=用户名不是手机号码,请重新输入
10070=此手机号码已经注册过
10071=输入的手机号码未注册
10072=当前不允许普通用户注册
10073=没有开启手机注册,没法使用找回密码功能
10074=输入的手机号码格式不正确
10075=输入的手机验证码错误
10076=字典类型不存在
10077=字典类型编码重复
10078=读取资源失败
10079=LLM大模型和Intent意图识别,选择参数不匹配
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
10081=删除声纹出现错误
10082=此次修改不允许,此声音已经注册为声纹了({0})
10083=修改声纹错误,请联系管理员
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
10085=音频数据不属于这个智能体
10086=音频数据是空的请检查上传数据
10087=声纹保存失败,请求不成功
10088=声纹保存失败,请求处理失败
10089=声纹注销失败,请求不成功
10090=声纹注销失败,请求处理失败
10091=供应器不存在
10092=设置的LLM不存在
10093=该模型配置已被智能体{0}引用,无法删除
10094=该LLM模型已被意图识别配置引用,无法删除
10095=无效服务端操作
10096=未配置服务端WebSocket地址
10097=目标WebSocket地址不存在
10098=WebSocket地址列表不能为空
10099=WebSocket地址不能使用localhost127.0.0.1
10100=WebSocket地址格式不正确
10101=WebSocket连接测试失败
10102=OTA地址不能为空
10103=OTA地址不能使用localhost127.0.0.1
10104=OTA地址必须以http或https开头
10105=OTA地址必须以/ota/结尾
10106=OTA接口访问失败
10107=OTA接口返回内容格式不正确
10108=OTA接口验证失败
10109=MCP地址不能为空
10110=MCP地址不能使用localhost127.0.0.1
10111=不是正确的MCP地址
10112=MCP接口访问失败
10113=MCP接口返回内容格式不正确
10114=MCP接口验证失败
10115=声纹接口地址不能为空
10116=声纹接口地址不能使用localhost127.0.0.1
10117=不是正确的声纹接口地址
10118=声纹接口地址必须以http或https开头
10119=声纹接口访问失败
10120=声纹接口返回内容格式不正确
10121=声纹接口验证失败
10122=mqtt密钥不能为空
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
10125=您的mqtt密钥包含弱密码
10128=字典标签重复
10129=modelTypeprovideCode不能为空
10132=没有权限查看该智能体的聊天记录
10133=会话ID不能为空
10134=智能体ID不能为空
10135=聊天记录下载失败
10136=下载链接已过期或无效
10137=下载链接无效
10138=用户
10139=智体
10043=\u5220\u9664\u6570\u636E\u5931\u8D25
10044=\u7528\u6237\u672A\u767B\u5F55
10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token\u751F\u6210\u5931\u8D25
10051=\u8D44\u6E90\u4E0D\u5B58\u5728
10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
10054=\u58F0\u7EB9\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u53C2\u6570\u914D\u7F6E\u4E2D\u914D\u7F6E\u58F0\u7EB9\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
10080=\u6B64\u58F0\u97F3\u58F0\u7EB9\u5BF9\u5E94\u7684\u4EBA\uFF08{0}\uFF09\u5DF2\u7ECF\u6CE8\u518C\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u58F0\u97F3\u6CE8\u518C
10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10084=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5B58\u5728\u9519\u8BEF\uFF0C\u8BF7\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u4FEE\u6539\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
10123=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0C\u5B57\u7B26\u6570\u9700\u8981\u81F3\u5C118\u4E2A\u5B57\u7B26\u4E14\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
10129=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
10137=\u4E0B\u8F7D\u94FE\u63A5\u65E0\u6548
10138=\u7528\u6237
10139=\u667A\u4F53
10140=\u97F3\u9891\u6587\u4EF6\u4E0D\u80FD\u4E3A\u7A7A
10141=\u53EA\u652F\u6301\u97F3\u9891\u6587\u4EF6
10142=\u97F3\u9891\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u8FC710MB
10143=\u4E0A\u4F20\u5931\u8D25
10144=\u58F0\u97F3\u514B\u9686\u8BB0\u5F55\u4E0D\u5B58\u5728
10145=\u97F3\u8272\u8D44\u6E90\u4FE1\u606F\u4E0D\u80FD\u4E3A\u7A7A
10146=\u5E73\u53F0\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
10147=\u97F3\u8272ID\u4E0D\u80FD\u4E3A\u7A7A
10148=\u5F52\u5C5E\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
10149=\u5220\u9664\u7684\u97F3\u8272\u8D44\u6E90ID\u4E0D\u80FD\u4E3A\u7A7A
10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55
@@ -145,4 +145,15 @@
10136=Download link expired or invalid
10137=Download link invalid
10138=User
10139=Agent
10139=Agent
10140=Audio file cannot be empty
10141=Only audio files are supported
10142=Audio file size cannot exceed 10MB
10143=Upload failed
10144=Voice clone record does not exist
10145=Voice resource information cannot be empty
10146=TTS platform name cannot be empty
10147=Voice ID cannot be empty
10148=Owner account cannot be empty
10149=Voice resource ID to delete cannot be empty
10150=You do not have permission to operate this record
@@ -1,148 +1,159 @@
#简体中文
500=服务器内部异常
401=未授权
403=拒绝访问,没有权限
#\u7B80\u4F53\u4E2D\u6587
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
401=\u672A\u6388\u6743
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
10001={0}不能为空
10002=数据库中已存在该记录
10003=获取参数失败
10004=账号或密码错误
10005=账号已被停用
10006=唯一标识不能为空
10007=验证码不正确
10008=先删除子菜单或按钮
10009=原密码不正确
10010=账号或密码不正确,您还有可以尝试{0}次
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
10011=上级部门选择错误
10012=上级菜单不能为自身
10013=数据权限接口,只能是Map类型参数
10014=请先删除下级部门
10015=请先删除部门下的用户
10016=部署失败,没有流程
10017=模型图不正确,请检查
10018=导出失败,模型ID为{0}
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
10019=请上传文件
10020=token不能为空
10021=token失效,请重新登录
10022=账号已被锁定
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
10020=token\u4E0D\u80FD\u4E3A\u7A7A
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上传文件失败{0}
10025=发送短信失败{0}
10026=邮件模板不存在
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服务异常
10028=定时任务失败
10029=不能包含非法字符
10030=密码长度不足{0}位
10031=密码必须同时包含数字、大小写字母和特殊字符组成
10032=删除本数据异常
10033=设备验证码错误
10027=Redis\u670D\u52A1\u5F02\u5E38
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5199\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
10034=参数值不能为空
10035=参数类型不能为空
10036=不支持的参数类型
10037=参数值必须是有效的数字
10038=参数值必须是true或false
10039=参数值必须是有效的JSON数组格式
10040=参数值必须是有效的JSON格式
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=设备未找到
10041=\u8BBE\u5907\u672A\u627E\u5230
10042={0}
10043=删除数据失败
10044=用户未登录
10045=WebSocket连接失败或连接超时
10046=保存声纹错误,请联系管理员
10047=每日发送次数已达上限
10048=旧密码输入错误
10049=设置的LLM不是openai和ollama
10050=token生成失败
10051=资源不存在
10052=默认智能体未找到
10053=智能体未找到
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
10055=短信发送失败
10056=短信连接建立失败
10057=智能体的声纹创建失败
10058=智能体的对应声纹更新失败
10059=智能体的对应声纹删除失败
10060=发送太频繁,请{0}秒后再试
10061=激活码不能为空
10062=激活码错误
10063=设备已激活
10064=该模型为默认模型,请先设置其他模型为默认模型
10065=新增数据失败
10066=修改数据失败
10067=图形验证码错误
10068=没有开启手机注册,没法使用短信验证码功能
10069=用户名不是手机号码,请重新输入
10070=此手机号码已经注册过
10071=输入的手机号码未注册
10072=当前不允许普通用户注册
10073=没有开启手机注册,没法使用找回密码功能
10074=输入的手机号码格式不正确
10075=输入的手机验证码错误
10076=字典类型不存在
10077=字典类型编码重复
10078=读取资源失败
10079=LLM大模型和Intent意图识别,选择参数不匹配
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
10081=删除声纹出现错误
10082=此次修改不允许,此声音已经注册为声纹了({0})
10083=修改声纹错误,请联系管理员
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
10085=音频数据不属于这个智能体
10086=音频数据是空的请检查上传数据
10087=声纹保存失败,请求不成功
10088=声纹保存失败,请求处理失败
10089=声纹注销失败,请求不成功
10090=声纹注销失败,请求处理失败
10091=供应器不存在
10092=设置的LLM不存在
10093=该模型配置已被智能体{0}引用,无法删除
10094=该LLM模型已被意图识别配置引用,无法删除
10095=无效服务端操作
10096=未配置服务端WebSocket地址
10097=目标WebSocket地址不存在
10098=WebSocket地址列表不能为空
10099=WebSocket地址不能使用localhost127.0.0.1
10100=WebSocket地址格式不正确
10101=WebSocket连接测试失败
10102=OTA地址不能为空
10103=OTA地址不能使用localhost127.0.0.1
10104=OTA地址必须以http或https开头
10105=OTA地址必须以/ota/结尾
10106=OTA接口访问失败
10107=OTA接口返回内容格式不正确
10108=OTA接口验证失败
10109=MCP地址不能为空
10110=MCP地址不能使用localhost127.0.0.1
10111=不是正确的MCP地址
10112=MCP接口访问失败
10113=MCP接口返回内容格式不正确
10114=MCP接口验证失败
10115=声纹接口地址不能为空
10116=声纹接口地址不能使用localhost127.0.0.1
10117=不是正确的声纹接口地址
10118=声纹接口地址必须以http或https开头
10119=声纹接口访问失败
10120=声纹接口返回内容格式不正确
10121=声纹接口验证失败
10122=mqtt密钥不能为空
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
10125=您的mqtt密钥包含弱密码
10128=字典标签重复
10129=SM2密钥未配置
10130=SM2解密失败
10131=modelTypeprovideCode不能为空
10132=没有权限查看该智能体的聊天记录
10133=会话ID不能为空
10134=智能体ID不能为空
10135=聊天记录下载失败
10136=下载链接已过期或无效
10137=下载链接无效
10138=用户
10139=智体
10043=\u5220\u9664\u6570\u636E\u5931\u8D25
10044=\u7528\u6237\u672A\u767B\u5F55
10045=WebSocket\u8FDE\u63A5\u5931\u8D25\u6216\u8FDE\u63A5\u8D85\u65F6
10046=\u4FDD\u5B58\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10047=\u6BCF\u65E5\u53D1\u9001\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650
10048=\u65E7\u5BC6\u7801\u8F93\u5165\u9519\u8BEF
10049=\u8BBE\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token\u751F\u6210\u5931\u8D25
10051=\u8D44\u6E90\u4E0D\u5B58\u5728
10052=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
10053=\u667A\u80FD\u4F53\u672A\u627E\u5230
10054=\u58F0\u7EB9\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8BF7\u5148\u5728\u53C2\u6570\u914D\u7F6E\u4E2D\u914D\u7F6E\u58F0\u7EB9\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
10056=\u77ED\u4FE1\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25
10057=\u667A\u80FD\u4F53\u7684\u58F0\u7EB9\u521B\u5EFA\u5931\u8D25
10058=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u66F4\u65B0\u5931\u8D25
10059=\u667A\u80FD\u4F53\u7684\u5BF9\u5E94\u58F0\u7EB9\u5220\u9664\u5931\u8D25
10060=\u53D1\u9001\u592A\u9891\u7E41\uFF0C\u8BF7{0}\u79D2\u540E\u518D\u8BD5
10061=\u6FC0\u6D3B\u7801\u4E0D\u80FD\u4E3A\u7A7A
10062=\u6FC0\u6D3B\u7801\u9519\u8BEF
10063=\u8BBE\u5907\u5DF2\u6FC0\u6D3B
10064=\u8BE5\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B\uFF0C\u8BF7\u5148\u8BBE\u7F6E\u5176\u4ED6\u6A21\u578B\u4E3A\u9ED8\u8BA4\u6A21\u578B
10065=\u65B0\u589E\u6570\u636E\u5931\u8D25
10066=\u4FEE\u6539\u6570\u636E\u5931\u8D25
10067=\u56FE\u5F62\u9A8C\u8BC1\u7801\u9519\u8BEF
10068=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A8C\u8BC1\u7801\u529F\u80FD
10069=\u7528\u6237\u540D\u4E0D\u662F\u624B\u673A\u53F7\u7801\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165
10070=\u6B64\u624B\u673A\u53F7\u7801\u5DF2\u7ECF\u6CE8\u518C\u8FC7
10071=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u672A\u6CE8\u518C
10072=\u5F53\u524D\u4E0D\u5141\u8BB8\u666E\u901A\u7528\u6237\u6CE8\u518C
10073=\u6CA1\u6709\u5F00\u542F\u624B\u673A\u6CE8\u518C\uFF0C\u6CA1\u6CD5\u4F7F\u7528\u627E\u56DE\u5BC6\u7801\u529F\u80FD
10074=\u8F93\u5165\u7684\u624B\u673A\u53F7\u7801\u683C\u5F0F\u4E0D\u6B63\u786E
10075=\u8F93\u5165\u7684\u624B\u673A\u9A8C\u8BC1\u7801\u9519\u8BEF
10076=\u5B57\u5178\u7C7B\u578B\u4E0D\u5B58\u5728
10077=\u5B57\u5178\u7C7B\u578B\u7F16\u7801\u91CD\u590D
10078=\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25
10079=LLM\u5927\u6A21\u578B\u548CIntent\u610F\u56FE\u8BC6\u522B\uFF0C\u9009\u62E9\u53C2\u6570\u4E0D\u5339\u914D
10080=\u6B64\u58F0\u97F3\u58F0\u7EB9\u5BF9\u5E94\u7684\u4EBA\uFF08{0}\uFF09\u5DF2\u7ECF\u6CE8\u518C\uFF0C\u8BF7\u9009\u62E9\u5176\u4ED6\u58F0\u97F3\u6CE8\u518C
10081=\u5220\u9664\u58F0\u7EB9\u51FA\u73B0\u9519\u8BEF
10082=\u6B64\u6B21\u4FEE\u6539\u4E0D\u5141\u8BB8\uFF0C\u6B64\u58F0\u97F3\u5DF2\u7ECF\u6CE8\u518C\u4E3A\u58F0\u7EB9\u4E86\uFF08{0}\uFF09
10083=\u4FEE\u6539\u58F0\u7EB9\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
10084=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5B58\u5728\u9519\u8BEF\uFF0C\u8BF7\u8FDB\u5165\u53C2\u6570\u7BA1\u7406\u4FEE\u6539\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10085=\u97F3\u9891\u6570\u636E\u4E0D\u5C5E\u4E8E\u8FD9\u4E2A\u667A\u80FD\u4F53
10086=\u97F3\u9891\u6570\u636E\u662F\u7A7A\u7684\u8BF7\u68C0\u67E5\u4E0A\u4F20\u6570\u636E
10087=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10088=\u58F0\u7EB9\u4FDD\u5B58\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10089=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u4E0D\u6210\u529F
10090=\u58F0\u7EB9\u6CE8\u9500\u5931\u8D25\uFF0C\u8BF7\u6C42\u5904\u7406\u5931\u8D25
10091=\u4F9B\u5E94\u5668\u4E0D\u5B58\u5728
10092=\u8BBE\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=\u8BE5\u6A21\u578B\u914D\u7F6E\u5DF2\u88AB\u667A\u80FD\u4F53{0}\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10094=\u8BE5LLM\u6A21\u578B\u5DF2\u88AB\u610F\u56FE\u8BC6\u522B\u914D\u7F6E\u5F15\u7528\uFF0C\u65E0\u6CD5\u5220\u9664
10095=\u65E0\u6548\u670D\u52A1\u7AEF\u64CD\u4F5C
10096=\u672A\u914D\u7F6E\u670D\u52A1\u7AEFWebSocket\u5730\u5740
10097=\u76EE\u6807WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u4E3A\u7A7A
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u786E
10101=WebSocket\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
10102=OTA\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10105=OTA\u5730\u5740\u5FC5\u987B\u4EE5/ota/\u7ED3\u5C3E
10106=OTA\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10108=OTA\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10109=MCP\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=\u4E0D\u662F\u6B63\u786E\u7684MCP\u5730\u5740
10112=MCP\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10114=MCP\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10115=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
10116=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=\u4E0D\u662F\u6B63\u786E\u7684\u58F0\u7EB9\u63A5\u53E3\u5730\u5740
10118=\u58F0\u7EB9\u63A5\u53E3\u5730\u5740\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
10119=\u58F0\u7EB9\u63A5\u53E3\u8BBF\u95EE\u5931\u8D25
10120=\u58F0\u7EB9\u63A5\u53E3\u8FD4\u56DE\u5185\u5BB9\u683C\u5F0F\u4E0D\u6B63\u786E
10121=\u58F0\u7EB9\u63A5\u53E3\u9A8C\u8BC1\u5931\u8D25
10122=mqtt\u5BC6\u94A5\u4E0D\u80FD\u4E3A\u7A7A
10123=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0C\u5B57\u7B26\u6570\u9700\u8981\u81F3\u5C118\u4E2A\u5B57\u7B26\u4E14\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD
10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801
10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D
10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E
10130=SM2\u89E3\u5BC6\u5931\u8D25
10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A
10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55
10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A
10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A
10135=\u804A\u5929\u8BB0\u5F55\u4E0B\u8F7D\u5931\u8D25
10136=\u4E0B\u8F7D\u94FE\u63A5\u5DF2\u8FC7\u671F\u6216\u65E0\u6548
10137=\u4E0B\u8F7D\u94FE\u63A5\u65E0\u6548
10138=\u7528\u6237
10139=\u667A\u4F53
10140=\u97F3\u9891\u6587\u4EF6\u4E0D\u80FD\u4E3A\u7A7A
10141=\u53EA\u652F\u6301\u97F3\u9891\u6587\u4EF6
10142=\u97F3\u9891\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u8FC710MB
10143=\u4E0A\u4F20\u5931\u8D25
10144=\u58F0\u97F3\u514B\u9686\u8BB0\u5F55\u4E0D\u5B58\u5728
10145=\u97F3\u8272\u8D44\u6E90\u4FE1\u606F\u4E0D\u80FD\u4E3A\u7A7A
10146=\u5E73\u53F0\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
10147=\u97F3\u8272ID\u4E0D\u80FD\u4E3A\u7A7A
10148=\u5F52\u5C5E\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
10149=\u5220\u9664\u7684\u97F3\u8272\u8D44\u6E90ID\u4E0D\u80FD\u4E3A\u7A7A
10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55
@@ -1,148 +1,159 @@
#繁體中文
500=服務器內部異常
401=未授權
403=拒絕訪問,沒有权限
#\u7E41\u9AD4\u4E2D\u6587
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
401=\u672A\u6388\u6B0A
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6743\u9650
10001={0}不能為空
10002=數據庫中已存在該記錄
10003=獲取參數失敗
10004=賬號或密碼錯誤
10005=賬號已被停用
10006=唯一標識不能為空
10007=驗證碼不正確
10008=先刪除子菜單或按鈕
10009=原密碼不正確
10010=帳號或密碼不正確,您還有可以嘗試{0}次
10001={0}\u4E0D\u80FD\u70BA\u7A7A
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
10004=\u8CEC\u865F\u6216\u5BC6\u78BC\u932F\u8AA4
10005=\u8CEC\u865F\u5DF2\u88AB\u505C\u7528
10006=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
10010=\u5E33\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
10011=上級部門選擇錯誤
10012=上級菜單不能為自身
10013=數據權限接口,只能是Map類型參數
10014=請先刪除下級部門
10015=請先刪除部門下的用戶
10016=部署失敗,沒有流程
10017=模型圖不正確,請檢查
10018=導出失敗,模型ID為{0}
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
10019=請上傳文件
10020=token不能為空
10021=token失效,請重新登錄
10022=賬號已被鎖定
10023=請上傳zip、bar、bpmn、bpmn20.xml格式文件
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
10020=token\u4E0D\u80FD\u70BA\u7A7A
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
10024=上傳文件失敗{0}
10025=發送短信失敗{0}
10026=郵件模板不存在
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
10027=Redis服務異常
10028=定時任務失敗
10029=不能包含非法字符
10030=密碼長度不足{0}位
10031=密碼必須同時包含數字、大小寫字母和特殊字符組成
10032=刪除本數據異常
10033=設備驗證碼錯誤
10027=Redis\u670D\u52D9\u7570\u5E38
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5BEB\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
10032=\u522A\u9664\u672C\u6578\u64DA\u7570\u5E38
10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
10034=參數值不能為空
10035=參數類型不能為空
10036=不支持的參數類型
10037=參數值必須是有效的數字
10038=參數值必須是true或false
10039=參數值必須是有效的JSON數組格式
10040=參數值必須是有效的JSON格式
10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
10036=\u4E0D\u652F\u6301\u7684\u53C3\u6578\u985E\u578B
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
10041=設備未找到
10041=\u8A2D\u5099\u672A\u627E\u5230
10042={0}
10043=刪除數據失敗
10044=用戶未登錄
10045=WebSocket連接失敗或連接超時
10046=保存聲紋錯誤,請聯繫管理員
10047=每日發送次數已達上限
10048=舊密碼輸入錯誤
10049=設置的LLM不是openai和ollama
10050=token生成失敗
10051=資源不存在
10052=默認智能體未找到
10053=智能體未找到
10054=聲紋接口未配置,請先在參數配置中配置聲紋接口地址(server.voice_print)
10055=短信發送失敗
10056=短信連接建立失敗
10057=智能體的聲紋創建失敗
10058=智能體的對應聲紋更新失敗
10059=智能體的對應聲紋刪除失敗
10060=發送太頻繁,請{0}秒後再試
10061=激活碼不能為空
10062=激活碼錯誤
10063=設備已激活
10064=該模型為預設模型,請先設置其他模型為預設模型
10065=新增數據失敗
10066=更新數據失敗
10067=圖形驗證碼錯誤
10068=未開啟手機註冊,無法使用短信驗證碼功能
10069=用戶名不是手機號碼,請重新輸入
10070=該手機號碼已註冊
10071=輸入的手機號碼未註冊
10072=目前不允許普通用戶註冊
10073=未開啟手機註冊,無法使用密碼找回功能
10074=輸入的手機號碼格式不正確
10075=輸入的短信驗證碼不正確
10076=字典類型不存在
10077=字典類型編碼重複
10078=讀取資源失敗
10079=LLM模型與意圖識別、參數選擇不匹配
10080=此聲紋屬於已註冊的{0},請選擇其他聲音
10081=刪除聲紋時發生錯誤
10082=不允許修改,此聲音已註冊為聲紋({0})
10083=修改聲紋錯誤,請聯繫管理員
10084=聲紋接口地址錯誤,請前往參數管理修改聲紋接口地址
10085=音頻數據不屬於此智能體
10086=音頻數據為空,請檢查上傳數據
10087=聲紋註冊失敗,請求未成功
10088=聲紋註冊失敗,請求處理失敗
10089=聲紋註銷失敗,請求未成功
10090=聲紋註銷失敗,請求處理失敗
10091=模型提供商不存在
10092=配置的LLM不存在
10093=此模型配置被智能體{0}引用,無法刪除
10094=此LLM模型被意圖識別配置引用,無法刪除
10095=無效的服務器操作
10096=未配置服務器WebSocket地址
10097=目標WebSocket地址不存在
10098=WebSocket地址列表不能為空
10099=WebSocket地址不能使用localhost127.0.0.1
10100=WebSocket地址格式不正確
10101=WebSocket連接測試失敗
10102=OTA地址不能為空
10103=OTA地址不能使用localhost127.0.0.1
10104=OTA地址必須以http或https開頭
10105=OTA地址必須以/ota/結尾
10106=OTA接口訪問失敗
10107=OTA接口返回內容格式不正確
10108=OTA接口驗證失敗
10109=MCP地址不能為空
10110=MCP地址不能使用localhost127.0.0.1
10111=不是有效的MCP地址
10112=MCP接口訪問失敗
10113=MCP接口返回內容格式不正確
10114=MCP接口驗證失敗
10115=聲紋接口地址不能為空
10116=聲紋接口地址不能使用localhost127.0.0.1
10117=不是有效的聲紋接口地址
10118=聲紋接口地址必須以http或https開頭
10119=聲紋接口訪問失敗
10120=聲紋接口返回內容格式不正確
10121=聲紋接口驗證失敗
10122=MQTT密鑰不能為空
10123=您的MQTT密鑰不安全,需至少8個字符且必須同時包含大小寫字母
10124=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母
10125=您的MQTT密鑰包含弱密碼
10128=字典標籤重複
10129=SM2密鑰未配置
10130=SM2解密失敗
10131=modelTypeprovideCode不能為空
10132=沒有權限查看該智能體的聊天記錄
10133=會話ID不能為空
10134=智能體ID不能為空
10135=聊天記錄下載失敗
10136=下載鏈接已過期或無效
10137=下載鏈接無效
10138=用戶
10139=智體
10043=\u522A\u9664\u6578\u64DA\u5931\u6557
10044=\u7528\u6236\u672A\u767B\u9304
10045=WebSocket\u9023\u63A5\u5931\u6557\u6216\u9023\u63A5\u8D85\u6642
10046=\u4FDD\u5B58\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
10047=\u6BCF\u65E5\u767C\u9001\u6B21\u6578\u5DF2\u9054\u4E0A\u9650
10048=\u820A\u5BC6\u78BC\u8F38\u5165\u932F\u8AA4
10049=\u8A2D\u7F6E\u7684LLM\u4E0D\u662Fopenai\u548Collama
10050=token\u751F\u6210\u5931\u6557
10051=\u8CC7\u6E90\u4E0D\u5B58\u5728
10052=\u9ED8\u8A8D\u667A\u80FD\u9AD4\u672A\u627E\u5230
10053=\u667A\u80FD\u9AD4\u672A\u627E\u5230
10054=\u8072\u7D0B\u63A5\u53E3\u672A\u914D\u7F6E\uFF0C\u8ACB\u5148\u5728\u53C3\u6578\u914D\u7F6E\u4E2D\u914D\u7F6E\u8072\u7D0B\u63A5\u53E3\u5730\u5740(server.voice_print)
10055=\u77ED\u4FE1\u767C\u9001\u5931\u6557
10056=\u77ED\u4FE1\u9023\u63A5\u5EFA\u7ACB\u5931\u6557
10057=\u667A\u80FD\u9AD4\u7684\u8072\u7D0B\u5275\u5EFA\u5931\u6557
10058=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u66F4\u65B0\u5931\u6557
10059=\u667A\u80FD\u9AD4\u7684\u5C0D\u61C9\u8072\u7D0B\u522A\u9664\u5931\u6557
10060=\u767C\u9001\u592A\u983B\u7E41\uFF0C\u8ACB{0}\u79D2\u5F8C\u518D\u8A66
10061=\u6FC0\u6D3B\u78BC\u4E0D\u80FD\u70BA\u7A7A
10062=\u6FC0\u6D3B\u78BC\u932F\u8AA4
10063=\u8A2D\u5099\u5DF2\u6FC0\u6D3B
10064=\u8A72\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B\uFF0C\u8ACB\u5148\u8A2D\u7F6E\u5176\u4ED6\u6A21\u578B\u70BA\u9810\u8A2D\u6A21\u578B
10065=\u65B0\u589E\u6578\u64DA\u5931\u6557
10066=\u66F4\u65B0\u6578\u64DA\u5931\u6557
10067=\u5716\u5F62\u9A57\u8B49\u78BC\u932F\u8AA4
10068=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u77ED\u4FE1\u9A57\u8B49\u78BC\u529F\u80FD
10069=\u7528\u6236\u540D\u4E0D\u662F\u624B\u6A5F\u865F\u78BC\uFF0C\u8ACB\u91CD\u65B0\u8F38\u5165
10070=\u8A72\u624B\u6A5F\u865F\u78BC\u5DF2\u8A3B\u518A
10071=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u672A\u8A3B\u518A
10072=\u76EE\u524D\u4E0D\u5141\u8A31\u666E\u901A\u7528\u6236\u8A3B\u518A
10073=\u672A\u958B\u555F\u624B\u6A5F\u8A3B\u518A\uFF0C\u7121\u6CD5\u4F7F\u7528\u5BC6\u78BC\u627E\u56DE\u529F\u80FD
10074=\u8F38\u5165\u7684\u624B\u6A5F\u865F\u78BC\u683C\u5F0F\u4E0D\u6B63\u78BA
10075=\u8F38\u5165\u7684\u77ED\u4FE1\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
10076=\u5B57\u5178\u985E\u578B\u4E0D\u5B58\u5728
10077=\u5B57\u5178\u985E\u578B\u7DE8\u78BC\u91CD\u8907
10078=\u8B80\u53D6\u8CC7\u6E90\u5931\u6557
10079=LLM\u6A21\u578B\u8207\u610F\u5716\u8B58\u5225\u3001\u53C3\u6578\u9078\u64C7\u4E0D\u5339\u914D
10080=\u6B64\u8072\u7D0B\u5C6C\u65BC\u5DF2\u8A3B\u518A\u7684{0}\uFF0C\u8ACB\u9078\u64C7\u5176\u4ED6\u8072\u97F3
10081=\u522A\u9664\u8072\u7D0B\u6642\u767C\u751F\u932F\u8AA4
10082=\u4E0D\u5141\u8A31\u4FEE\u6539\uFF0C\u6B64\u8072\u97F3\u5DF2\u8A3B\u518A\u70BA\u8072\u7D0B({0})
10083=\u4FEE\u6539\u8072\u7D0B\u932F\u8AA4\uFF0C\u8ACB\u806F\u7E6B\u7BA1\u7406\u54E1
10084=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u932F\u8AA4\uFF0C\u8ACB\u524D\u5F80\u53C3\u6578\u7BA1\u7406\u4FEE\u6539\u8072\u7D0B\u63A5\u53E3\u5730\u5740
10085=\u97F3\u983B\u6578\u64DA\u4E0D\u5C6C\u65BC\u6B64\u667A\u80FD\u9AD4
10086=\u97F3\u983B\u6578\u64DA\u70BA\u7A7A\uFF0C\u8ACB\u6AA2\u67E5\u4E0A\u50B3\u6578\u64DA
10087=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
10088=\u8072\u7D0B\u8A3B\u518A\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
10089=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u672A\u6210\u529F
10090=\u8072\u7D0B\u8A3B\u92B7\u5931\u6557\uFF0C\u8ACB\u6C42\u8655\u7406\u5931\u6557
10091=\u6A21\u578B\u63D0\u4F9B\u5546\u4E0D\u5B58\u5728
10092=\u914D\u7F6E\u7684LLM\u4E0D\u5B58\u5728
10093=\u6B64\u6A21\u578B\u914D\u7F6E\u88AB\u667A\u80FD\u9AD4{0}\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
10094=\u6B64LLM\u6A21\u578B\u88AB\u610F\u5716\u8B58\u5225\u914D\u7F6E\u5F15\u7528\uFF0C\u7121\u6CD5\u522A\u9664
10095=\u7121\u6548\u7684\u670D\u52D9\u5668\u64CD\u4F5C
10096=\u672A\u914D\u7F6E\u670D\u52D9\u5668WebSocket\u5730\u5740
10097=\u76EE\u6A19WebSocket\u5730\u5740\u4E0D\u5B58\u5728
10098=WebSocket\u5730\u5740\u5217\u8868\u4E0D\u80FD\u70BA\u7A7A
10099=WebSocket\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10100=WebSocket\u5730\u5740\u683C\u5F0F\u4E0D\u6B63\u78BA
10101=WebSocket\u9023\u63A5\u6E2C\u8A66\u5931\u6557
10102=OTA\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10103=OTA\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10104=OTA\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
10105=OTA\u5730\u5740\u5FC5\u9808\u4EE5/ota/\u7D50\u5C3E
10106=OTA\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10107=OTA\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10108=OTA\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10109=MCP\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10110=MCP\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10111=\u4E0D\u662F\u6709\u6548\u7684MCP\u5730\u5740
10112=MCP\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10113=MCP\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10114=MCP\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10115=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u70BA\u7A7A
10116=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u4E0D\u80FD\u4F7F\u7528localhost\u6216127.0.0.1
10117=\u4E0D\u662F\u6709\u6548\u7684\u8072\u7D0B\u63A5\u53E3\u5730\u5740
10118=\u8072\u7D0B\u63A5\u53E3\u5730\u5740\u5FC5\u9808\u4EE5http\u6216https\u958B\u982D
10119=\u8072\u7D0B\u63A5\u53E3\u8A2A\u554F\u5931\u6557
10120=\u8072\u7D0B\u63A5\u53E3\u8FD4\u56DE\u5167\u5BB9\u683C\u5F0F\u4E0D\u6B63\u78BA
10121=\u8072\u7D0B\u63A5\u53E3\u9A57\u8B49\u5931\u6557
10122=MQTT\u5BC6\u9470\u4E0D\u80FD\u70BA\u7A7A
10123=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0C\u9700\u81F3\u5C118\u500B\u5B57\u7B26\u4E14\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
10124=\u60A8\u7684MQTT\u5BC6\u9470\u4E0D\u5B89\u5168\uFF0CMQTT\u5BC6\u9470\u5FC5\u9808\u540C\u6642\u5305\u542B\u5927\u5C0F\u5BEB\u5B57\u6BCD
10125=\u60A8\u7684MQTT\u5BC6\u9470\u5305\u542B\u5F31\u5BC6\u78BC
10128=\u5B57\u5178\u6A19\u7C64\u91CD\u8907
10129=SM2\u5BC6\u9470\u672A\u914D\u7F6E
10130=SM2\u89E3\u5BC6\u5931\u6557
10131=modelType\u548CprovideCode\u4E0D\u80FD\u70BA\u7A7A
10132=\u6C92\u6709\u6B0A\u9650\u67E5\u770B\u8A72\u667A\u80FD\u9AD4\u7684\u804A\u5929\u8A18\u9304
10133=\u6703\u8A71ID\u4E0D\u80FD\u70BA\u7A7A
10134=\u667A\u80FD\u9AD4ID\u4E0D\u80FD\u70BA\u7A7A
10135=\u804A\u5929\u8A18\u9304\u4E0B\u8F09\u5931\u6557
10136=\u4E0B\u8F09\u93C8\u63A5\u5DF2\u904E\u671F\u6216\u7121\u6548
10137=\u4E0B\u8F09\u93C8\u63A5\u7121\u6548
10138=\u7528\u6236
10139=\u667A\u9AD4
10140=\u97F3\u983B\u6587\u4EF6\u4E0D\u80FD\u70BA\u7A7A
10141=\u53EA\u652F\u6301\u97F3\u983B\u6587\u4EF6
10142=\u97F3\u983B\u6587\u4EF6\u5927\u5C0F\u4E0D\u80FD\u8D85\u904E10MB
10143=\u4E0A\u50B3\u5931\u6557
10144=\u8072\u97F3\u514B\u9686\u8A18\u9304\u4E0D\u5B58\u5728
10145=\u97F3\u8272\u8CC7\u6E90\u4FE1\u606F\u4E0D\u80FD\u70BA\u7A7A
10146=\u5E73\u53F0\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
10147=\u97F3\u8272ID\u4E0D\u80FD\u70BA\u7A7A
10148=\u6B78\u5C6C\u5E33\u865F\u4E0D\u80FD\u70BA\u7A7A
10149=\u522A\u9664\u7684\u97F3\u8272\u8CC7\u6E90ID\u4E0D\u80FD\u70BA\u7A7A
10150=\u60A8\u6C92\u6709\u6B0A\u9650\u64CD\u4F5C\u8A72\u8A18\u9304
@@ -23,4 +23,11 @@
select model_name from ai_model_config where model_type = #{modelType}
<if test="modelName != null">and model_name = #{modelName}</if>
</select>
<!-- 获取符合条件的TTS平台列表 -->
<select id="getTtsPlatformList" resultType="map">
select id, model_name as modelName from ai_model_config
where model_type = 'TTS'
and JSON_EXTRACT(config_json, '$.type') = 'huoshan_double_stream'
</select>
</mapper>
+1 -1
View File
@@ -240,7 +240,7 @@ export default {
'settings.restartLater': 'Later',
// About us
'settings.aboutApp': 'About XiaoZhi Console',
'settings.aboutContent': 'XiaoZhi Console\n\nA cross-platform mobile management app built with Vue.js 3 + uni-app, providing device management, agent configuration and other functions for xiaozhi ESP32 smart hardware.\n\n© 2025 xiaozhi-esp32-server 0.8.3',
'settings.aboutContent': 'XiaoZhi Console\n\nA cross-platform mobile management app built with Vue.js 3 + uni-app, providing device management, agent configuration and other functions for xiaozhi ESP32 smart hardware.\n\n© 2025 xiaozhi-esp32-server {version}',
'settings.restartSuccess': 'Saved, you can manually restart the app later',
'settings.serverUrlSavedAndCacheCleared': 'Server URL saved and cache cleared',
'settings.resetToDefaultAndCacheCleared': 'Reset to default and cache cleared',
+1 -1
View File
@@ -240,7 +240,7 @@ export default {
'settings.restartLater': '稍後',
// 關於我們
'settings.aboutApp': '關於小智智控台',
'settings.aboutContent': '小智智控台\n\n基於 Vue.js 3 + uni-app 構建的跨平台移動端管理應用,為小智ESP32智能硬體提供設備管理、智能體配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.8.3',
'settings.aboutContent': '小智智控台\n\n基於 Vue.js 3 + uni-app 構建的跨平台移動端管理應用,為小智ESP32智能硬體提供設備管理、智能體配置等功能。\n\n© 2025 xiaozhi-esp32-server {version}',
'settings.restartSuccess': '已保存,可稍後手動重啟應用',
'settings.serverUrlSavedAndCacheCleared': '服務端地址已保存,緩存已清除',
'settings.resetToDefaultAndCacheCleared': '已恢復默認設置,緩存已清除',
@@ -235,7 +235,7 @@ function showAbout() {
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
content: t('settings.aboutContent', {
appName: import.meta.env.VITE_APP_TITLE,
version: '0.8.3'
version: '0.8.4'
}),
showCancel: false,
confirmText: t('common.confirm'),
+7 -1
View File
@@ -7,6 +7,10 @@ import model from './module/model.js'
import ota from './module/ota.js'
import timbre from "./module/timbre.js"
import user from './module/user.js'
import voiceClone from './module/voiceClone.js'
import voiceResource from './module/voiceResource.js'
/**
* 接口地址
@@ -33,5 +37,7 @@ export default {
model,
timbre,
ota,
dict
dict,
voiceResource,
voiceClone
}
@@ -0,0 +1,98 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 分页查询音色资源
getVoiceCloneList(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone`)
.method('GET')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取音色列表失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceCloneList(params, callback);
});
}).send();
},
// 上传音频文件
uploadVoice(formData, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone/upload`)
.method('POST')
.data(formData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('上传音频失败:', err);
RequestService.reAjaxFun(() => {
this.uploadVoice(formData, callback);
});
}).send();
},
// 更新音色名称
updateName(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone/updateName`)
.method('POST')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('更新名称失败:', err);
RequestService.reAjaxFun(() => {
this.updateName(params, callback);
});
}).send();
},
// 获取音频下载ID
getAudioId(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone/audio/${id}`)
.method('POST')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取音频ID失败:', err);
RequestService.reAjaxFun(() => {
this.getAudioId(id, callback);
});
}).send();
},
// 获取音频播放URL
getPlayVoiceUrl(uuid) {
return `${getServiceUrl()}/voiceClone/play/${uuid}`;
},
// 复刻音频
cloneAudio(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceClone/cloneAudio`)
.method('POST')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('上传失败:', err);
RequestService.reAjaxFun(() => {
this.cloneAudio(params, callback);
});
}).send();
}
}
@@ -0,0 +1,103 @@
import { getServiceUrl } from '../api';
import RequestService from '../httpRequest';
export default {
// 分页查询音色资源
getVoiceResourceList(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource`)
.method('GET')
.data(params)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取音色资源列表失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceResourceList(params, callback);
});
}).send();
},
// 获取单个音色资源信息
getVoiceResourceInfo(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource/${id}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取音色资源信息失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceResourceInfo(id, callback);
});
}).send();
},
// 保存音色资源
saveVoiceResource(entity, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource`)
.method('POST')
.data(entity)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('保存音色资源失败:', err);
RequestService.reAjaxFun(() => {
this.saveVoiceResource(entity, callback);
});
}).send();
},
// 删除音色资源
deleteVoiceResource(ids, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource/${ids}`)
.method('DELETE')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('删除音色资源失败:', err);
RequestService.reAjaxFun(() => {
this.deleteVoiceResource(ids, callback);
});
}).send();
},
// 根据用户ID获取音色资源列表
getVoiceResourceByUserId(userId, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource/user/${userId}`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取用户音色资源列表失败:', err);
RequestService.reAjaxFun(() => {
this.getVoiceResourceByUserId(userId, callback);
});
}).send();
},
// 获取TTS平台列表
getTtsPlatformList(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/voiceResource/ttsPlatforms`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.networkFail((err) => {
console.error('获取TTS平台列表失败:', err);
RequestService.reAjaxFun(() => {
this.getTtsPlatformList(callback);
});
}).send();
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,11 +1,13 @@
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose" @open="handleOpen">
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
@open="handleOpen">
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
</el-form-item>
<el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type">
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;" filterable :disabled="isTypeDisabled">
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;"
filterable :disabled="isTypeDisabled">
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select>
</el-form-item>
@@ -14,7 +16,7 @@
</el-form-item>
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
:before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"
:before-upload="beforeUpload" :accept="'.bin,.apk,.wav'" :limit="1" :multiple="false" :auto-upload="true"
:on-remove="handleRemove">
<el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button>
<div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div>
@@ -26,7 +28,8 @@
</div>
</el-form-item>
<el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
<el-input type="textarea" v-model="form.remark" :placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
<el-input type="textarea" v-model="form.remark"
:placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@@ -128,13 +131,13 @@ export default {
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
if (!isValidType) {
this.$message.error(this.$t('firmwareDialog.invalidFileType'))
return false
}
if (!isValidSize) {
this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
return false
}
this.$message.error(this.$t('firmwareDialog.invalidFileType'))
return false
}
if (!isValidSize) {
this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
return false
}
return true
},
handleUpload(options) {
+138 -86
View File
@@ -16,6 +16,34 @@
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
<span class="nav-text">{{ $t('header.smartManagement') }}</span>
</div>
<!-- 普通用户显示音色克隆 -->
<div v-if="!isSuperAdmin" class="equipment-management"
:class="{ 'active-tab': $route.path === '/voice-clone-management' }" @click="goVoiceCloneManagement">
<img loading="lazy" alt="" src="@/assets/header/voice.png"
:style="{ filter: $route.path === '/voice-clone-management' ? 'brightness(0) invert(1)' : 'None' }" />
<span class="nav-text">{{ $t('header.voiceCloneManagement') }}</span>
</div>
<!-- 超级管理员显示音色克隆下拉菜单 -->
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
:class="{ 'active-tab': $route.path === '/voice-clone-management' || $route.path === '/voice-resource-management' }"
@visible-change="handleVoiceCloneDropdownVisibleChange">
<span class="el-dropdown-link">
<img loading="lazy" alt="" src="@/assets/header/voice.png"
:style="{ filter: $route.path === '/voice-clone-management' || $route.path === '/voice-resource-management' ? 'brightness(0) invert(1)' : 'None' }" />
<span class="nav-text">{{ $t('header.voiceCloneManagement') }}</span>
<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">
{{ $t('header.voiceCloneManagement') }}
</el-dropdown-item>
<el-dropdown-item @click.native="goVoiceResourceManagement">
{{ $t('header.voiceResourceManagement') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
@click="goModelConfig">
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
@@ -28,22 +56,19 @@
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
<span class="nav-text">{{ $t('header.userManagement') }}</span>
</div>
<div v-if="isSuperAdmin" class="equipment-management"
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
<span class="nav-text">{{ $t('header.otaManagement') }}</span>
</div>
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' || $route.path === '/agent-template-management' }"
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' || $route.path === '/agent-template-management' || $route.path === '/ota-management' }"
@visible-change="handleParamDropdownVisibleChange">
<span class="el-dropdown-link">
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' || $route.path === '/agent-template-management' ? 'brightness(0) invert(1)' : 'None' }" />
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' || $route.path === '/agent-template-management' || $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
<span class="nav-text">{{ $t('header.paramDictionary') }}</span>
<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="goOtaManagement">
{{ $t('header.otaManagement') }}
</el-dropdown-item>
<el-dropdown-item @click.native="goParamManagement">
{{ $t('header.paramManagement') }}
</el-dropdown-item>
@@ -91,37 +116,18 @@
</div>
</div>
<!-- 语言切换下拉菜单 -->
<el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange">
<span class="el-dropdown-link">
<span class="current-language-text">{{ currentLanguageText }}</span>
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changeLanguage('zh_CN')">
{{ $t('language.zhCN') }}
</el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('zh_TW')">
{{ $t('language.zhTW') }}
</el-dropdown-item>
<el-dropdown-item @click.native="changeLanguage('en')">
{{ $t('language.en') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
<span class="el-dropdown-link">
{{ userInfo.username || '加载中...' }}
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword')
}}</el-dropdown-item>
<el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" @click="handleAvatarClick" />
<span class="el-dropdown-link" @click="handleAvatarClick">
{{ userInfo.username || '加载中...' }}
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-cascader :options="userMenuOptions" trigger="click" :props="cascaderProps"
style="width: 0px;overflow: hidden;" :show-all-levels="false" @change="handleCascaderChange"
ref="userCascader">
<template slot-scope="{ data }">
<span>{{ data.label }}</span>
</template>
</el-cascader>
</div>
</div>
@@ -150,15 +156,21 @@ export default {
mobile: ''
},
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
userDropdownVisible: false,
paramDropdownVisible: false,
languageDropdownVisible: false,
voiceCloneDropdownVisible: false,
isSmallScreen: false,
// 搜索历史相关
searchHistory: [],
showHistory: false,
SEARCH_HISTORY_KEY: 'xiaozhi_search_history',
MAX_HISTORY_COUNT: 3
MAX_HISTORY_COUNT: 3,
// Cascader 配置
cascaderProps: {
expandTrigger: 'click',
value: 'value',
label: 'label',
children: 'children'
}
}
},
computed: {
@@ -183,6 +195,37 @@ export default {
default:
return this.$t('language.zhCN');
}
},
// 用户菜单选项
userMenuOptions() {
return [
{
label: this.currentLanguageText,
value: 'language',
children: [
{
label: this.$t('language.zhCN'),
value: 'zh_CN'
},
{
label: this.$t('language.zhTW'),
value: 'zh_TW'
},
{
label: this.$t('language.en'),
value: 'en'
}
]
},
{
label: this.$t('header.changePassword'),
value: 'changePassword'
},
{
label: this.$t('header.logout'),
value: 'logout'
}
];
}
},
mounted() {
@@ -207,6 +250,9 @@ export default {
goModelConfig() {
this.$router.push('/model-config')
},
goVoiceCloneManagement() {
this.$router.push('/voice-clone-management')
},
goParamManagement() {
this.$router.push('/params-management')
},
@@ -222,6 +268,11 @@ export default {
goServerSideManagement() {
this.$router.push('/server-side-management')
},
// 跳转到音色资源管理
goVoiceResourceManagement() {
this.$router.push('/voice-resource-management')
},
// 添加默认角色模板管理导航方法
goAgentTemplateManagement() {
this.$router.push('/agent-template-management')
@@ -364,27 +415,55 @@ export default {
});
}
},
handleUserDropdownVisibleChange(visible) {
this.userDropdownVisible = visible;
},
// 监听第二个下拉菜单的可见状态变化
// 监听参数字典下拉菜单的可见状态变化
handleParamDropdownVisibleChange(visible) {
this.paramDropdownVisible = visible;
},
// 监听语言下拉菜单的可见状态变化
handleLanguageDropdownVisibleChange(visible) {
this.languageDropdownVisible = visible;
// 监听音色克隆下拉菜单的可见状态变化
handleVoiceCloneDropdownVisibleChange(visible) {
this.voiceCloneDropdownVisible = visible;
},
// 处理 Cascader 选择变化
handleCascaderChange(value) {
if (!value || value.length === 0) {
return;
}
const action = value[value.length - 1];
// 处理语言切换
if (value.length === 2 && value[0] === 'language') {
this.changeLanguage(action);
return;
}
// 处理其他操作
switch (action) {
case 'changePassword':
this.showChangePasswordDialog();
break;
case 'logout':
this.handleLogout();
break;
}
},
// 切换语言
changeLanguage(lang) {
changeLanguage(lang);
this.languageDropdownVisible = false;
this.$message.success({
message: this.$t('message.success'),
showClose: true
});
},
// 点击头像触发cascader下拉菜单
handleAvatarClick() {
if (this.$refs.userCascader) {
this.$refs.userCascader.toggleDropDownVisible();
}
},
// 使用 mapActions 引入 Vuex 的 logout action
...mapActions(['logout'])
}
@@ -430,7 +509,7 @@ export default {
align-items: center;
gap: 25px;
position: absolute;
left: 44%;
left: 50%;
transform: translateX(-50%);
}
@@ -542,6 +621,13 @@ export default {
visibility: hidden;
}
.more-dropdown .el-dropdown-link {
display: flex;
align-items: center;
gap: 7px;
}
.search-history-item:hover .clear-item-icon {
visibility: visible;
}
@@ -579,41 +665,7 @@ export default {
width: 21px;
height: 21px;
flex-shrink: 0;
}
.user-dropdown {
flex-shrink: 0;
}
.language-dropdown {
flex-shrink: 0;
margin-right: 5px;
}
.current-language-text {
margin-left: 4px;
margin-right: 4px;
font-size: 12px;
color: #3d4566;
}
.more-dropdown {
padding-right: 20px;
}
.more-dropdown .el-dropdown-link {
display: flex;
align-items: center;
gap: 7px;
}
.rotate-down {
transform: rotate(180deg);
transition: transform 0.3s ease;
}
.el-icon-arrow-down {
transition: transform 0.3s ease;
cursor: pointer;
}
/* 导航文本样式 - 支持中英文换行 */
@@ -0,0 +1,694 @@
<template>
<el-dialog :title="$t('voiceClone.dialogTitle')" :visible.sync="visible" width="900px" top="10vh"
:before-close="handleClose" class="voice-clone-dialog">
<div class="dialog-content">
<!-- 步骤指示器 -->
<div class="steps-header">
<div class="step-item" :class="{ 'active': currentStep === 1, 'completed': currentStep > 1 }">
<div class="step-number">
<i class="el-icon-check" v-if="currentStep > 1"></i>
<span v-else>1</span>
</div>
<div class="step-label">{{ $t('voiceClone.stepUpload') }}</div>
<div class="step-line"></div>
</div>
<div class="step-item" :class="{ 'active': currentStep === 2, 'completed': currentStep > 2 }">
<div class="step-number">
<i class="el-icon-check" v-if="currentStep > 2"></i>
<span v-else>2</span>
</div>
<div class="step-label">{{ $t('voiceClone.stepEdit') }}</div>
</div>
</div>
<!-- 步骤1: 音频上传 -->
<div v-if="currentStep === 1" class="step-content">
<div class="upload-area">
<el-upload class="audio-uploader" drag :action="uploadAction" :auto-upload="false"
:on-change="handleFileChange" :show-file-list="false" accept="audio/*">
<i class="el-icon-upload"></i>
<div class="el-upload__text">{{ $t('voiceClone.dragOrClick') }}</div>
<div class="el-upload__tip">{{ $t('voiceClone.uploadTip') }}</div>
</el-upload>
</div>
</div>
<!-- 步骤2: 音频编辑 -->
<div v-if="currentStep === 2" class="step-content">
<div class="audio-edit-area">
<div class="edit-tips">
<p>{{ $t('voiceClone.editTip1') }}</p>
<p>{{ $t('voiceClone.editTip2') }}</p>
</div>
<!-- 波形显示区域 -->
<div class="waveform-container">
<canvas ref="waveformCanvas" class="waveform-canvas" @mousedown="handleWaveformMouseDown"
@mousemove="handleWaveformMouseMove" @mouseup="handleWaveformMouseUp"></canvas>
<div class="selection-overlay" v-if="isSelecting || selectionStart !== null"
:style="selectionStyle"></div>
<div class="duration-display">
{{ $t('voiceClone.selectedDuration', { duration: selectedDuration.toFixed(1) }) }}
</div>
</div>
<!-- 音频控制按钮 -->
<div class="audio-controls">
<el-button size="small" :icon="isPlaying ? 'el-icon-video-pause' : 'el-icon-video-play'"
@click="togglePlay" type="primary">
{{ isPlaying ? $t('voiceClone.pause') : $t('voiceClone.play') }}
</el-button>
<el-button size="small" icon="el-icon-scissors" @click="handleTrim"
:disabled="selectionStart === null">
{{ $t('voiceClone.trim') }}
</el-button>
<el-button size="small" icon="el-icon-refresh-left" @click="handleReset">
{{ $t('voiceClone.reset') }}
</el-button>
</div>
<!-- 音频元素 -->
<audio ref="audioPlayer" @timeupdate="handleTimeUpdate" @ended="handleAudioEnded"
style="display: none;"></audio>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{ currentStep === 1 ? $t('voiceClone.cancel') : $t('voiceClone.prevStep')
}}</el-button>
<el-button type="primary" @click="handleNext" :loading="uploading">
{{ currentStep === 1 ? $t('voiceClone.nextStep') : $t('voiceClone.upload') }}
</el-button>
</span>
</el-dialog>
</template>
<script>
import Api from "@/apis/api";
export default {
name: 'VoiceCloneDialog',
props: {
visible: {
type: Boolean,
default: false
},
voiceCloneData: {
type: Object,
default: () => ({})
}
},
data() {
return {
currentStep: 1,
uploadAction: '',
audioFile: null,
originalAudioFile: null,
audioBuffer: null,
originalAudioBuffer: null,
isPlaying: false,
uploading: false,
// 波形相关
waveformData: [],
// 选择相关
isSelecting: false,
selectionStart: null,
selectionEnd: null,
mouseStartX: 0,
// 音频上下文
audioContext: null,
audioSource: null,
};
},
computed: {
selectedDuration() {
if (this.selectionStart !== null && this.selectionEnd !== null && this.audioBuffer) {
const duration = this.audioBuffer.duration;
const start = Math.min(this.selectionStart, this.selectionEnd) * duration;
const end = Math.max(this.selectionStart, this.selectionEnd) * duration;
return end - start;
}
return this.audioBuffer ? this.audioBuffer.duration : 0;
},
selectionStyle() {
if (this.selectionStart === null) return {};
const canvas = this.$refs.waveformCanvas;
if (!canvas) return {};
const start = Math.min(this.selectionStart, this.selectionEnd || this.selectionStart);
const end = Math.max(this.selectionStart, this.selectionEnd || this.selectionStart);
return {
left: `${start * 100}%`,
width: `${(end - start) * 100}%`
};
}
},
methods: {
handleClose() {
if (this.currentStep === 2) {
this.currentStep = 1;
} else {
this.resetDialog();
this.$emit('update:visible', false);
}
},
resetDialog() {
this.currentStep = 1;
this.audioFile = null;
this.originalAudioFile = null;
this.audioBuffer = null;
this.originalAudioBuffer = null;
this.isPlaying = false;
this.uploading = false;
this.waveformData = [];
this.selectionStart = null;
this.selectionEnd = null;
if (this.audioSource) {
this.audioSource.stop();
this.audioSource = null;
}
},
async handleFileChange(file) {
if (!file || !file.raw) return;
this.audioFile = file.raw;
this.originalAudioFile = file.raw;
// 先进入第二步,确保DOM已渲染
this.currentStep = 2;
// 等待DOM更新后再加载音频
await this.$nextTick();
await this.loadAudio(file.raw);
},
async loadAudio(file) {
try {
const arrayBuffer = await file.arrayBuffer();
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer.slice(0));
this.originalAudioBuffer = await this.audioContext.decodeAudioData(await file.arrayBuffer());
// 设置音频播放器
if (this.$refs.audioPlayer) {
const audioUrl = URL.createObjectURL(file);
this.$refs.audioPlayer.src = audioUrl;
// 加载音频元数据
this.$refs.audioPlayer.load();
}
// 生成波形数据
await this.generateWaveform();
} catch (error) {
console.error('加载音频失败:', error);
this.$message.error(this.$t('voiceClone.loadAudioFailed'));
}
},
async generateWaveform() {
if (!this.audioBuffer) return;
await this.$nextTick();
const canvas = this.$refs.waveformCanvas;
if (!canvas) {
console.error('Canvas元素不存在');
return;
}
// 设置canvas大小
const containerWidth = canvas.parentElement.offsetWidth;
const containerHeight = canvas.parentElement.offsetHeight;
canvas.width = containerWidth || 800;
canvas.height = containerHeight || 200;
const canvasWidth = canvas.width;
const channelData = this.audioBuffer.getChannelData(0);
const step = Math.floor(channelData.length / canvasWidth);
const waveformData = [];
for (let i = 0; i < canvasWidth; i++) {
let sum = 0;
for (let j = 0; j < step; j++) {
sum += Math.abs(channelData[i * step + j] || 0);
}
waveformData.push(sum / step);
}
this.waveformData = waveformData;
this.drawWaveform();
},
drawWaveform() {
const canvas = this.$refs.waveformCanvas;
if (!canvas) {
console.error('绘制波形时Canvas不存在');
return;
}
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 绘制背景
ctx.fillStyle = '#e0f2ff';
ctx.fillRect(0, 0, width, height);
if (this.waveformData.length === 0) {
console.error('波形数据为空');
return;
}
// 找到最大值用于归一化
const maxValue = Math.max(...this.waveformData);
// 绘制波形
ctx.fillStyle = '#4ade80';
ctx.strokeStyle = '#4ade80';
ctx.lineWidth = 1;
const barWidth = width / this.waveformData.length;
this.waveformData.forEach((value, index) => {
// 归一化并放大,使用80%的高度
const normalizedValue = maxValue > 0 ? value / maxValue : 0;
const barHeight = Math.max(1, normalizedValue * height * 0.8);
const x = index * barWidth;
const y = (height - barHeight) / 2;
ctx.fillRect(x, y, Math.max(1, barWidth - 1), barHeight);
});
},
handleWaveformMouseDown(e) {
const canvas = this.$refs.waveformCanvas;
const rect = canvas.getBoundingClientRect();
this.mouseStartX = e.clientX - rect.left;
this.selectionStart = this.mouseStartX / rect.width;
this.selectionEnd = this.selectionStart;
this.isSelecting = true;
},
handleWaveformMouseMove(e) {
if (!this.isSelecting) return;
const canvas = this.$refs.waveformCanvas;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
this.selectionEnd = Math.max(0, Math.min(1, x / rect.width));
},
handleWaveformMouseUp() {
this.isSelecting = false;
},
async handleTrim() {
if (this.selectionStart === null || this.selectionEnd === null || !this.audioBuffer) return;
const start = Math.min(this.selectionStart, this.selectionEnd);
const end = Math.max(this.selectionStart, this.selectionEnd);
// 创建新的音频buffer
const duration = this.audioBuffer.duration;
const startTime = start * duration;
const endTime = end * duration;
const startOffset = Math.floor(startTime * this.audioBuffer.sampleRate);
const endOffset = Math.floor(endTime * this.audioBuffer.sampleRate);
const newLength = endOffset - startOffset;
const newBuffer = this.audioContext.createBuffer(
this.audioBuffer.numberOfChannels,
newLength,
this.audioBuffer.sampleRate
);
for (let channel = 0; channel < this.audioBuffer.numberOfChannels; channel++) {
const oldData = this.audioBuffer.getChannelData(channel);
const newData = newBuffer.getChannelData(channel);
for (let i = 0; i < newLength; i++) {
newData[i] = oldData[startOffset + i];
}
}
this.audioBuffer = newBuffer;
// 更新音频文件
await this.bufferToFile(newBuffer);
// 重置选择
this.selectionStart = null;
this.selectionEnd = null;
// 重新生成波形
this.generateWaveform();
this.$message.success(this.$t('voiceClone.trimSuccess'));
},
async handleReset() {
if (!this.originalAudioFile) return;
this.audioFile = this.originalAudioFile;
await this.loadAudio(this.originalAudioFile);
this.selectionStart = null;
this.selectionEnd = null;
this.$message.success(this.$t('voiceClone.resetSuccess'));
},
togglePlay() {
const audio = this.$refs.audioPlayer;
if (this.isPlaying) {
audio.pause();
this.isPlaying = false;
} else {
audio.play();
this.isPlaying = true;
}
},
handleTimeUpdate() {
// 可以在这里更新播放进度
},
handleAudioEnded() {
this.isPlaying = false;
},
async bufferToFile(buffer) {
// 将AudioBuffer转换为WAV文件
const wav = this.audioBufferToWav(buffer);
const blob = new Blob([wav], { type: 'audio/wav' });
this.audioFile = new File([blob], 'audio.wav', { type: 'audio/wav' });
// 更新播放器
await this.$nextTick();
if (this.$refs.audioPlayer) {
const audioUrl = URL.createObjectURL(blob);
this.$refs.audioPlayer.src = audioUrl;
}
},
audioBufferToWav(buffer) {
const length = buffer.length * buffer.numberOfChannels * 2 + 44;
const arrayBuffer = new ArrayBuffer(length);
const view = new DataView(arrayBuffer);
const channels = [];
let offset = 0;
let pos = 0;
// 写入WAV文件头
const setUint16 = (data) => {
view.setUint16(pos, data, true);
pos += 2;
};
const setUint32 = (data) => {
view.setUint32(pos, data, true);
pos += 4;
};
// "RIFF" chunk descriptor
setUint32(0x46464952); // "RIFF"
setUint32(length - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
// "fmt " sub-chunk
setUint32(0x20746d66); // "fmt "
setUint32(16); // SubChunk1Size = 16
setUint16(1); // AudioFormat = 1 (PCM)
setUint16(buffer.numberOfChannels);
setUint32(buffer.sampleRate);
setUint32(buffer.sampleRate * 2 * buffer.numberOfChannels); // byte rate
setUint16(buffer.numberOfChannels * 2); // block align
setUint16(16); // bits per sample
// "data" sub-chunk
setUint32(0x61746164); // "data"
setUint32(length - pos - 4); // SubChunk2Size
// 写入音频数据
for (let i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
while (pos < length) {
for (let i = 0; i < buffer.numberOfChannels; i++) {
let sample = Math.max(-1, Math.min(1, channels[i][offset]));
sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
view.setInt16(pos, sample, true);
pos += 2;
}
offset++;
}
return arrayBuffer;
},
async handleNext() {
if (this.currentStep === 1) {
// 验证是否已选择文件
if (!this.audioFile) {
this.$message.warning(this.$t('voiceClone.pleaseSelectAudio'));
return;
}
this.currentStep = 2;
} else {
// 上传音频
await this.uploadAudio();
}
},
async uploadAudio() {
if (!this.audioFile) {
this.$message.warning(this.$t('voiceClone.pleaseSelectAudio'));
return;
}
// 验证音频时长(8-60秒)
if (this.audioBuffer) {
const duration = this.audioBuffer.duration;
if (duration < 8 || duration > 60) {
this.$message.warning(this.$t('voiceClone.durationError'));
return;
}
}
this.uploading = true;
try {
const formData = new FormData();
formData.append('voiceFile', this.audioFile);
formData.append('id', this.voiceCloneData.id);
await Api.voiceClone.uploadVoice(formData, (res) => {
this.uploading = false;
res = res.data;
if (res.code === 0) {
this.$message.success(this.$t('voiceClone.uploadSuccess'));
this.resetDialog();
this.$emit('update:visible', false);
this.$emit('success');
} else {
this.$message.error(res.msg || this.$t('voiceClone.uploadFailed'));
}
});
} catch (error) {
this.uploading = false;
console.error('上传音频失败:', error);
this.$message.error(this.$t('voiceClone.uploadFailed'));
}
}
},
mounted() {
// 设置canvas大小
this.$nextTick(() => {
const canvas = this.$refs.waveformCanvas;
if (canvas) {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
});
},
beforeDestroy() {
if (this.audioContext) {
this.audioContext.close();
}
}
};
</script>
<style lang="scss" scoped>
.voice-clone-dialog {
::v-deep .el-dialog__body {
padding: 20px 30px;
}
}
.steps-header {
display: flex;
justify-content: center;
align-items: center;
padding: 0 50px;
}
.step-item {
display: flex;
align-items: center;
position: relative;
&.active {
.step-number {
background: #6b8cff;
color: white;
}
.step-label {
color: #333;
font-weight: 600;
}
}
&.completed {
.step-number {
background: #67c23a;
color: white;
}
.step-line {
background: #67c23a;
}
}
}
.step-number {
width: 36px;
height: 36px;
border-radius: 50%;
background: #e4e7ed;
color: #909399;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
z-index: 1;
}
.step-label {
margin-left: 12px;
color: #909399;
font-size: 14px;
}
.step-line {
width: 200px;
height: 2px;
background: #e4e7ed;
margin-left: 12px;
}
.step-content {
padding: 20px 0;
}
.upload-area {
padding: 40px 0;
}
.audio-uploader {
::v-deep .el-upload {
width: 100%;
}
::v-deep .el-upload-dragger {
width: 100%;
height: 280px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.el-icon-upload {
font-size: 67px;
color: #c0c4cc;
margin: 0 0 16px;
line-height: 50px;
}
}
.el-upload__text {
color: #606266;
font-size: 14px;
margin-bottom: 8px;
}
.el-upload__tip {
font-size: 12px;
color: #909399;
margin-top: 7px;
}
}
.audio-edit-area {
padding: 0 20px;
}
.edit-tips {
margin-bottom: 20px;
padding: 12px;
background: #f0f9ff;
border-radius: 4px;
border-left: 3px solid #1890ff;
p {
margin: 4px 0;
font-size: 13px;
color: #606266;
&:first-child {
font-weight: 500;
}
}
}
.waveform-container {
position: relative;
width: 100%;
height: 200px;
margin-bottom: 20px;
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
cursor: crosshair;
}
.waveform-canvas {
width: 100%;
height: 100%;
}
.selection-overlay {
position: absolute;
top: 0;
bottom: 0;
background: rgba(107, 140, 255, 0.3);
border: 1px solid #6b8cff;
pointer-events: none;
}
.duration-display {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.audio-controls {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 20px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
@@ -0,0 +1,154 @@
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
@open="handleOpen" width="500px">
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item :label="$t('voiceClone.platformName')" prop="modelId">
<el-select v-model="form.modelId" :placeholder="$t('voiceClone.platformNamePlaceholder')" filterable
style="width: 100%" @change="handlePlatformChange">
<el-option v-for="model in platformList" :key="model.id" :label="model.modelName" :value="model.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('voiceClone.voiceId')" prop="voiceIds">
<el-select v-model="form.voiceIds" :placeholder="$t('voiceClone.voiceIdPlaceholder')" filterable multiple
allow-create default-first-option style="width: 100%">
<el-option v-for="voiceId in voiceIdList" :key="voiceId" :label="voiceId" :value="voiceId">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('voiceClone.userId')" prop="userId">
<el-select v-model="form.userId" :placeholder="$t('voiceClone.userIdPlaceholder')" filterable remote
:remote-method="remoteSearchUser" :loading="userLoading" style="width: 100%">
<el-option v-for="user in userList" :key="user.userid" :label="user.mobile" :value="user.userid">
</el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel">{{ $t('voiceClone.operationCancelled') }}</el-button>
<el-button type="primary" @click="handleSubmit">{{ $t('voiceClone.addNew') }}</el-button>
</div>
</el-dialog>
</template>
<script>
import Api from '@/apis/api';
export default {
name: 'VoiceCloneDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
form: {
type: Object,
default: () => ({})
}
},
data() {
return {
dialogVisible: this.visible,
platformList: [],
voiceIdList: [],
userList: [],
userLoading: false,
rules: {
modelId: [
{ required: true, message: this.$t('voiceClone.platformNameRequired'), trigger: 'change' }
],
voiceIds: [
{ required: true, message: this.$t('voiceClone.voiceIdRequired'), trigger: 'change' }
],
userId: [
{ required: true, message: this.$t('voiceClone.userIdRequired'), trigger: 'change' }
]
}
}
},
watch: {
visible(val) {
this.dialogVisible = val;
},
dialogVisible(val) {
this.$emit('update:visible', val);
}
},
methods: {
handleClose() {
this.dialogVisible = false;
this.$emit('cancel');
},
handleCancel() {
this.$refs.form.clearValidate();
this.$emit('cancel');
},
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
this.$emit('submit', this.form);
}
});
},
handleOpen() {
// 对话框打开时加载平台列表
this.fetchPlatformList();
// 重置音色ID列表
this.voiceIdList = [];
},
handlePlatformChange(modelId) {
// 清空音色ID选择
this.form.voiceIds = [];
},
// 获取TTS平台列表
fetchPlatformList() {
Api.voiceResource.getTtsPlatformList((res) => {
if (res.data.code === 0) {
this.platformList = res.data.data;
}
});
},
// 远程搜索用户
remoteSearchUser(query) {
if (query !== '') {
this.userLoading = true;
const params = {
page: 1,
limit: 20,
mobile: query
};
Api.admin.getUserList(params, (res) => {
this.userLoading = false;
if (res.data.code === 0) {
this.userList = res.data.data.list;
}
});
} else {
this.userList = [];
}
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-dialog {
border-radius: 20px;
}
</style>
+84 -2
View File
@@ -9,8 +9,10 @@ export default {
// HeaderBar组件文本
'header.smartManagement': 'Agents',
'header.modelConfig': 'Models',
'header.voiceCloneManagement': 'Voice Clone',
'header.voiceResourceManagement': 'Voice Resource',
'header.userManagement': 'Users',
'header.otaManagement': 'OTA',
'header.otaManagement': 'OTA Management',
'header.paramDictionary': 'More',
'header.paramManagement': 'Params Management',
'header.dictManagement': 'Dict Management',
@@ -1063,5 +1065,85 @@ export default {
'sm2.invalidPublicKey': 'Invalid public key format',
'sm2.encryptionError': 'Error occurred during encryption',
'sm2.publicKeyRetry': 'Retrying to get public key...',
'sm2.publicKeyRetryFailed': 'Public key retrieval retry failed'
'sm2.publicKeyRetryFailed': 'Public key retrieval retry failed',
// 音色资源管理
'voiceClone.title': 'Voice Clone',
'voiceResource.title': 'Voice Resource',
'voiceClone.platformName': 'Platform Name',
'voiceClone.voiceId': 'Voice ID',
'voiceClone.userId': 'Account Owner',
'voiceClone.name': 'Voice Name',
'voiceClone.clone': 'Clone Voice',
'voiceClone.action': 'Action',
'voiceClone.modelId': 'Model ID',
'voiceClone.trainStatus': 'Training Status',
'voiceClone.trainError': 'Training Error',
'voiceClone.createdAt': 'Creation Time',
'voiceClone.search': 'Search',
'voiceClone.searchPlaceholder': 'Please enter voice name or voice ID',
'voiceClone.addNew': 'Add',
'voiceClone.delete': 'Delete',
'voiceClone.selectAll': 'Select All',
'voiceClone.deselectAll': 'Deselect All',
'voiceClone.addVoiceClone': 'Add Voice Resource',
'voiceClone.confirmDelete': 'Are you sure you want to delete the selected {count} voice resources?',
'voiceClone.deleteSuccess': 'Successfully deleted {count} voice resources',
'voiceClone.deleteFailed': 'Deletion failed',
'voiceClone.addSuccess': 'Add successful',
'voiceClone.addFailed': 'Add failed',
'voiceClone.updateSuccess': 'Update successful',
'voiceClone.updateFailed': 'Update failed',
'voiceClone.selectFirst': 'Please select voice resources to delete first',
'voiceClone.operationCancelled': 'Cancel',
'voiceClone.operationClosed': 'Popup closed',
'voiceClone.cloneSuccess': 'Clone successful',
'voiceClone.cloneFailed': 'Clone failed',
'voiceClone.confirmClone': 'Are you sure you want to clone this voice?',
'voiceClone.onlySuccessCanClone': 'Only successfully trained voices can be cloned',
'common.insufficient': 'Insufficient',
'voiceClone.platformNameRequired': 'Please select platform name',
'voiceClone.voiceIdRequired': 'Please select voice ID',
'voiceClone.userIdRequired': 'Please select account owner',
'voiceClone.platformNamePlaceholder': 'Please select platform name',
'voiceClone.voiceIdPlaceholder': 'Please enter voice ID and press Enter',
'voiceClone.userIdPlaceholder': 'Please enter keyword to select account owner',
'voiceClone.waitingUpload': 'Waiting for upload',
'voiceClone.waitingTraining': 'Waiting for clone',
'voiceClone.training': 'Training',
'voiceClone.trainSuccess': 'Training successful',
'voiceClone.trainFailed': 'Training failed',
'voiceClone.itemsPerPage': '{items} items per page',
'voiceClone.firstPage': 'First Page',
'voiceClone.prevPage': 'Previous Page',
'voiceClone.nextPage': 'Next Page',
'voiceClone.totalRecords': '{total} records in total',
'voiceClone.noVoiceCloneAssigned': 'Your account has no voice resources assigned',
'voiceClone.contactAdmin': 'Please contact administrator for voice resource assignment',
'voiceClone.dialogTitle': 'Voice Clone',
'voiceClone.stepUpload': 'Prepare Audio',
'voiceClone.stepEdit': 'Audio Edit',
'voiceClone.dragOrClick': 'Drag audio file here, or click to upload',
'voiceClone.uploadTip': 'Support all mainstream audio formats, selected duration must be between 8-60 seconds',
'voiceClone.editTip1': 'Please confirm if the uploaded audio is satisfactory',
'voiceClone.editTip2': 'You can listen and trim the audio, if not satisfied you can go back to re-record or upload',
'voiceClone.selectedDuration': 'Selected valid segment: {duration} seconds',
'voiceClone.trim': 'Trim selected segment',
'voiceClone.reset': 'Reset',
'voiceClone.play': 'Play',
'voiceClone.pause': 'Pause',
'voiceClone.cancel': 'Cancel',
'voiceClone.nextStep': 'Next',
'voiceClone.prevStep': 'Previous',
'voiceClone.upload': 'Upload Audio',
'voiceClone.pleaseSelectAudio': 'Please select an audio file first',
'voiceClone.durationError': 'Audio duration must be between 8-60 seconds',
'voiceClone.loadAudioFailed': 'Failed to load audio',
'voiceClone.trimSuccess': 'Trim successful',
'voiceClone.resetSuccess': 'Reset successful',
'voiceClone.uploadSuccess': 'Upload successful',
'voiceClone.uploadFailed': 'Upload failed',
'voiceClone.updateNameSuccess': 'Name updated successfully',
'voiceClone.updateNameFailed': 'Failed to update name',
'voiceClone.playFailed': 'Play failed',
}
+83 -1
View File
@@ -8,6 +8,8 @@ export default {
// HeaderBar组件文本
'header.smartManagement': '智能体管理',
'header.voiceCloneManagement': '音色克隆',
'header.voiceResourceManagement': '音色资源',
'header.modelConfig': '模型配置',
'header.userManagement': '用户管理',
'header.otaManagement': 'OTA管理',
@@ -1064,5 +1066,85 @@ export default {
'sm2.invalidPublicKey': '无效的公钥格式',
'sm2.encryptionError': '加密过程中发生错误',
'sm2.publicKeyRetry': '正在重试获取公钥...',
'sm2.publicKeyRetryFailed': '公钥获取重试失败'
'sm2.publicKeyRetryFailed': '公钥获取重试失败',
// 音色资源管理
'voiceClone.title': '音色克隆',
'voiceResource.title': '音色资源',
'voiceClone.platformName': '平台名称',
'voiceClone.voiceId': '声音ID',
'voiceClone.userId': '归属账号',
'voiceClone.name': '声音名称',
'voiceClone.clone': '立即复刻',
'voiceClone.modelId': '模型ID',
'voiceClone.trainStatus': '训练状态',
'voiceClone.trainError': '训练错误',
'voiceClone.createdAt': '创建时间',
'voiceClone.search': '搜索',
'voiceClone.searchPlaceholder': '请输入声音名称或音色ID',
'voiceClone.addNew': '新增',
'voiceClone.delete': '删除',
'voiceClone.selectAll': '全选',
'voiceClone.deselectAll': '取消全选',
'voiceClone.addVoiceClone': '新增音色资源',
'voiceClone.confirmDelete': '确定要删除选中的 {count} 条音色资源吗?',
'voiceClone.deleteSuccess': '成功删除 {count} 条音色资源',
'voiceClone.deleteFailed': '删除失败',
'voiceClone.addSuccess': '添加成功',
'voiceClone.addFailed': '添加失败',
'voiceClone.updateSuccess': '更新成功',
'voiceClone.updateFailed': '更新失败',
'voiceClone.selectFirst': '请先选择要删除的音色资源',
'voiceClone.operationCancelled': '取消',
'voiceClone.operationClosed': '已关闭弹窗',
'voiceClone.action': '操作',
'voiceClone.cloneSuccess': '复刻成功',
'voiceClone.cloneFailed': '复刻失败',
'voiceClone.confirmClone': '确定要复刻此音色吗?',
'voiceClone.onlySuccessCanClone': '只有训练成功的音色才能复刻',
'common.insufficient': '不足',
'voiceClone.platformNameRequired': '请选择平台名称',
'voiceClone.voiceIdRequired': '请选择音色ID',
'voiceClone.userIdRequired': '请选择归属账号',
'voiceClone.platformNamePlaceholder': '请选择平台名称',
'voiceClone.voiceIdPlaceholder': '请输入音色ID并按回车',
'voiceClone.userIdPlaceholder': '请输入关键词选择归属账号',
'voiceClone.waitingUpload': '待上传',
'voiceClone.waitingTraining': '待复刻',
'voiceClone.training': '训练中',
'voiceClone.trainSuccess': '训练成功',
'voiceClone.trainFailed': '训练失败',
'voiceClone.itemsPerPage': '每页 {items} 条',
'voiceClone.firstPage': '首页',
'voiceClone.prevPage': '上一页',
'voiceClone.nextPage': '下一页',
'voiceClone.totalRecords': '共 {total} 条',
'voiceClone.noVoiceCloneAssigned': '您的账号暂无音色资源',
'voiceClone.contactAdmin': '请联系管理员分配音色资源',
'voiceClone.dialogTitle': '声音复刻',
'voiceClone.stepUpload': '准备音频',
'voiceClone.stepEdit': '音频编辑',
'voiceClone.dragOrClick': '将音频文件拖到此处,或点击上传',
'voiceClone.uploadTip': '支持所有主流音频格式,选区时长需要在8-60秒之间',
'voiceClone.editTip1': '请确认上传音频是否满意',
'voiceClone.editTip2': '您可以试听并裁剪音频,如果不满意可以返回重新录制或上传',
'voiceClone.selectedDuration': '已选择有效片段:{duration}秒',
'voiceClone.trim': '对选择区域进行裁剪',
'voiceClone.reset': '重置',
'voiceClone.play': '播放',
'voiceClone.pause': '暂停',
'voiceClone.cancel': '取消',
'voiceClone.nextStep': '下一步',
'voiceClone.prevStep': '上一步',
'voiceClone.upload': '上传音频',
'voiceClone.pleaseSelectAudio': '请先选择音频文件',
'voiceClone.durationError': '音频时长必须在8-60秒之间',
'voiceClone.loadAudioFailed': '加载音频失败',
'voiceClone.trimSuccess': '裁剪成功',
'voiceClone.resetSuccess': '重置成功',
'voiceClone.uploadSuccess': '上传成功',
'voiceClone.uploadFailed': '上传失败',
'voiceClone.updateNameSuccess': '名称更新成功',
'voiceClone.updateNameFailed': '名称更新失败',
'voiceClone.playFailed': '播放失败',
}
+83 -1
View File
@@ -10,6 +10,8 @@ export default {
'header.smartManagement': '智能體管理',
'header.modelConfig': '模型配置',
'header.userManagement': '用戶管理',
'header.voiceCloneManagement': '音色克隆',
'header.voiceResourceManagement': '音色資源',
'header.otaManagement': 'OTA管理',
'header.paramDictionary': '參數字典',
'header.paramManagement': '參數管理',
@@ -1065,5 +1067,85 @@ export default {
'sm2.invalidPublicKey': '無效的公鑰格式',
'sm2.encryptionError': '加密過程中發生錯誤',
'sm2.publicKeyRetry': '正在重試獲取公鑰...',
'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗'
'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗',
// 音色資源管理
'voiceClone.title': '音色克隆',
'voiceResource.title': '音色資源',
'voiceClone.platformName': '平台名稱',
'voiceClone.voiceId': '聲音ID',
'voiceClone.userId': '歸屬帳號',
'voiceClone.name': '聲音名稱',
'voiceClone.clone': '立即複刻',
'voiceClone.action': '操作',
'voiceClone.modelId': '模型ID',
'voiceClone.trainStatus': '訓練狀態',
'voiceClone.trainError': '訓練錯誤',
'voiceClone.createdAt': '建立時間',
'voiceClone.search': '搜尋',
'voiceClone.searchPlaceholder': '請輸入聲音名稱或音色ID',
'voiceClone.addNew': '新增',
'voiceClone.delete': '刪除',
'voiceClone.selectAll': '全選',
'voiceClone.deselectAll': '取消全選',
'voiceClone.addVoiceClone': '新增音色資源',
'voiceClone.confirmDelete': '確定要刪除選中的 {count} 條音色資源嗎?',
'voiceClone.deleteSuccess': '成功刪除 {count} 條音色資源',
'voiceClone.deleteFailed': '刪除失敗',
'voiceClone.addSuccess': '新增成功',
'voiceClone.addFailed': '新增失敗',
'voiceClone.updateSuccess': '更新成功',
'voiceClone.updateFailed': '更新失敗',
'voiceClone.selectFirst': '請先選擇要刪除的音色資源',
'voiceClone.operationCancelled': '取消',
'voiceClone.operationClosed': '已關閉彈窗',
'voiceClone.cloneSuccess': '複刻成功',
'voiceClone.cloneFailed': '複刻失敗',
'voiceClone.confirmClone': '確定要複刻此音色嗎?',
'voiceClone.onlySuccessCanClone': '只有訓練成功的音色才能複刻',
'common.insufficient': '不足',
'voiceClone.platformNameRequired': '請選擇平台名稱',
'voiceClone.voiceIdRequired': '請選擇音色ID',
'voiceClone.userIdRequired': '請選擇歸屬帳號',
'voiceClone.platformNamePlaceholder': '請選擇平台名稱',
'voiceClone.voiceIdPlaceholder': '請輸入音色ID並按回车',
'voiceClone.userIdPlaceholder': '請輸入关键词選擇歸屬帳號',
'voiceClone.waitingUpload': '待上傳',
'voiceClone.waitingTraining': '待複刻',
'voiceClone.training': '訓練中',
'voiceClone.trainSuccess': '訓練成功',
'voiceClone.trainFailed': '訓練失敗',
'voiceClone.itemsPerPage': '每頁 {items} 條',
'voiceClone.firstPage': '首頁',
'voiceClone.prevPage': '上一頁',
'voiceClone.nextPage': '下一頁',
'voiceClone.totalRecords': '共 {total} 條',
'voiceClone.noVoiceCloneAssigned': '您的帳號暂无音色資源',
'voiceClone.contactAdmin': '請聯繫管理員分配音色資源',
'voiceClone.dialogTitle': '聲音複刻',
'voiceClone.stepUpload': '準備音頻',
'voiceClone.stepEdit': '音頻編輯',
'voiceClone.dragOrClick': '將音頻文件拖到此處,或點擊上傳',
'voiceClone.uploadTip': '支持所有主流音頻格式,選區時長需要在8-60秒之間',
'voiceClone.editTip1': '請確認上傳音頻是否滿意',
'voiceClone.editTip2': '您可以試聽並裁剪音頻,如果不滿意可以返回重新錄製或上傳',
'voiceClone.selectedDuration': '已選擇有效片段:{duration}秒',
'voiceClone.trim': '對選擇區域進行裁剪',
'voiceClone.reset': '重置',
'voiceClone.play': '播放',
'voiceClone.pause': '暫停',
'voiceClone.cancel': '取消',
'voiceClone.nextStep': '下一步',
'voiceClone.prevStep': '上一步',
'voiceClone.upload': '上傳音頻',
'voiceClone.pleaseSelectAudio': '請先選擇音頻文件',
'voiceClone.durationError': '音頻時長必須在8-60秒之間',
'voiceClone.loadAudioFailed': '加載音頻失敗',
'voiceClone.trimSuccess': '裁剪成功',
'voiceClone.resetSuccess': '重置成功',
'voiceClone.uploadSuccess': '上傳成功',
'voiceClone.uploadFailed': '上傳失敗',
'voiceClone.updateNameSuccess': '名稱更新成功',
'voiceClone.updateNameFailed': '名稱更新失敗',
'voiceClone.playFailed': '播放失敗',
}
+23 -1
View File
@@ -18,7 +18,7 @@ const routes = [
return import('../views/roleConfig.vue')
}
},
{
{
path: '/voice-print',
name: 'VoicePrint',
component: function () {
@@ -110,6 +110,28 @@ const routes = [
title: 'OTA管理'
}
},
{
path: '/voice-resource-management',
name: 'VoiceResourceManagement',
component: function () {
return import('../views/VoiceResourceManagement.vue')
},
meta: {
requiresAuth: true,
title: '音色资源开通'
}
},
{
path: '/voice-clone-management',
name: 'VoiceCloneManagement',
component: function () {
return import('../views/VoiceCloneManagement.vue')
},
meta: {
requiresAuth: true,
title: '音色克隆管理'
}
},
{
path: '/dict-management',
name: 'DictManagement',
@@ -0,0 +1,697 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('voiceClone.title') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('voiceClone.searchPlaceholder')" v-model="searchName" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('voiceClone.search') }}</el-button>
</div>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<!-- 显示表格或空状态 -->
<el-card class="params-card" shadow="never" v-if="total > 0">
<el-table ref="paramsTable" :data="voiceCloneList" class="transparent-table" v-loading="loading"
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)">
<el-table-column :label="$t('voiceClone.voiceId')" prop="voiceId"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.name')" align="center">
<template #default="{ row }">
<el-input v-show="row.isEdit" v-model="row.name" size="mini" maxlength="64"
show-word-limit @blur="onNameBlur(row)" @keyup.enter.native="onNameEnter(row)"
ref="nameInput" />
<span v-show="!row.isEdit" class="name-view">
<i class="el-icon-edit" @click="handleEditName(row)"
style="cursor: pointer;"></i>
<span @click="handleEditName(row)">
{{ row.name || '-' }}
</span>
</span>
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.trainStatus')" prop="trainStatus" align="center">
<template slot-scope="scope">
{{ getTrainStatusText(scope.row) }}
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.action')" align="center" width="180">
<template slot-scope="scope">
<el-button v-if="scope.row.hasVoice" size="mini" type="text"
@click="handlePlay(scope.row)">
{{ $t('voiceClone.play') }}
</el-button>
<el-button size="mini" type="text" @click="handleUpload(scope.row)">
{{ $t('voiceClone.upload') }}
</el-button>
<el-button v-if="scope.row.hasVoice" size="mini" type="text"
@click="handleClone(scope.row)">
{{ $t('voiceClone.clone') }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
</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="$t('voiceClone.itemsPerPage', { items: item })" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('voiceClone.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('voiceClone.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('voiceClone.nextPage') }}
</button>
<span class="total-text">{{ $t('voiceClone.totalRecords', { total }) }}</span>
</div>
</div>
</el-card>
<!-- 空状态提示 -->
<div v-else-if="!loading" class="empty-state-wrapper">
<div class="empty-state">
<div class="empty-icon">
<i class="el-icon-microphone" style="font-size: 48px;"></i>
</div>
<div class="empty-text">
{{ $t('voiceClone.noVoiceCloneAssigned') }}
</div>
<div class="empty-desc">
{{ $t('voiceClone.contactAdmin') }}
</div>
</div>
</div>
</div>
</div>
</div>
<el-footer>
<version-footer />
</el-footer>
<!-- 复刻弹框 -->
<VoiceCloneDialog :visible.sync="cloneDialogVisible" :voiceCloneData="currentVoiceClone"
@success="handleCloneSuccess" />
</div>
</template>
<script>
import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import VoiceCloneDialog from "@/components/VoiceCloneDialog.vue";
import { formatDate } from "@/utils/format";
export default {
components: { HeaderBar, VersionFooter, VoiceCloneDialog },
data() {
return {
searchName: "",
loading: false,
voiceCloneList: [],
currentPage: 1,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
total: 0,
dialogVisible: false,
cloneDialogVisible: false,
currentVoiceClone: {},
isAllSelected: false,
voiceCloneForm: {
modelId: "",
voiceIds: [],
userId: null
}
};
},
created() {
this.fetchVoiceCloneList();
},
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;
},
},
methods: {
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
this.fetchVoiceCloneList();
},
fetchVoiceCloneList() {
this.loading = true;
const params = {
pageNum: this.currentPage,
pageSize: this.pageSize,
name: this.searchName || "",
orderField: "create_date",
order: "desc"
};
Api.voiceClone.getVoiceCloneList(params, (res) => {
this.loading = false;
res = res.data
if (res.code === 0) {
this.voiceCloneList = res.data.list;
this.total = res.data.total || 0;
} else {
this.voiceCloneList = [];
this.total = 0;
this.$message.error({
message: res?.data?.msg || this.$t('voiceClone.deleteFailed'),
showClose: true
});
}
});
},
handleSearch() {
this.currentPage = 1;
this.fetchVoiceCloneList();
},
goFirst() {
this.currentPage = 1;
this.fetchVoiceCloneList();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchVoiceCloneList();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.fetchVoiceCloneList();
}
},
goToPage(page) {
this.currentPage = page;
this.fetchVoiceCloneList();
},
formatDate,
getTrainStatusText(row) {
if (!row.hasVoice) {
return this.$t('voiceClone.waitingUpload');
}
switch (row.trainStatus) {
case 0:
return this.$t('voiceClone.waitingTraining');
case 1:
return this.$t('voiceClone.training');
case 2:
return this.$t('voiceClone.trainSuccess');
case 3:
return this.$t('voiceClone.trainFailed');
default:
return '';
}
},
// 处理复刻操作
handleClone(row) {
const params = {
cloneId: row.id
};
Api.voiceClone.cloneAudio(params, (res) => {
res = res.data;
if (res.code === 0) {
this.$message.success(this.$t('message.success'));
} else {
this.$message.error(res.msg || this.$t('message.error'));
}
});
},
// 复刻成功后的回调
handleCloneSuccess() {
this.fetchVoiceCloneList();
},
// 进入编辑模式
handleEditName(row) {
this.$set(row, 'isEdit', true);
this.$nextTick(() => {
// 聚焦到输入框
const input = this.$refs.nameInput;
if (input) {
// nameInput 可能是一个数组
if (Array.isArray(input)) {
const idx = this.voiceCloneList.indexOf(row);
if (input[idx]) {
input[idx].focus();
}
} else {
input.focus();
}
}
});
},
// 提交名称修改
submitName(row) {
// 防止重复提交
if (row._submitting) {
return;
}
row._submitting = true;
const params = {
id: row.id,
name: row.name
};
Api.voiceClone.updateName(params, (res) => {
res = res.data;
if (res.code === 0) {
this.$message.success(this.$t('voiceClone.updateNameSuccess') || '名称更新成功');
} else {
this.$message.error(res.msg || this.$t('voiceClone.updateNameFailed') || '名称更新失败');
// 失败时恢复原值
this.fetchVoiceCloneList();
}
row._submitting = false;
});
},
// 名称输入框:失焦时提交
onNameBlur(row) {
row.isEdit = false;
setTimeout(() => {
this.submitName(row);
}, 100); // 延迟 100ms,避开 enter+blur 同时触发的窗口
},
// 名称输入框:按回车时提交
onNameEnter(row) {
row.isEdit = false;
this.submitName(row);
},
// 播放音频
handlePlay(row) {
// 先获取音频下载ID
Api.voiceClone.getAudioId(row.id, (res) => {
res = res.data;
if (res.code === 0) {
const uuid = res.data;
// 使用获取到的uuid播放音频
const audioUrl = Api.voiceClone.getPlayVoiceUrl(uuid);
const audio = new Audio(audioUrl);
audio.play().catch(err => {
console.error('播放失败:', err);
this.$message.error(this.$t('voiceClone.playFailed') || '播放失败');
});
} else {
this.$message.error(res.msg || this.$t('voiceClone.audioNotExist') || '音频不存在');
}
});
},
// 上传音频
handleUpload(row) {
this.currentVoiceClone = row;
this.cloneDialogVisible = true;
}
},
};
</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 {
margin: 5px 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
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: 16px 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;
padding-bottom: 10px;
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
}
.custom-pagination {
display: flex;
align-items: center;
gap: 5px;
.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;
}
}
}
.empty-state-wrapper {
margin-top: 20vh;
}
.total-text {
margin-left: 10px;
color: #606266;
font-size: 14px;
}
.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(.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;
font-weight: 600;
height: 40px;
padding: 8px 0;
font-size: 14px;
border-bottom: 1px solid #e4e7ed;
}
.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);
padding: 8px 0;
height: 40px;
color: #606266;
font-size: 14px;
}
}
.el-table__row:hover>td {
background-color: #f5f7fa !important;
}
&::before {
display: none;
}
}
:deep(.el-table .el-button--text) {
color: #7079aa !important;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5 !important;
}
.name-view {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
i {
color: #909399;
font-size: 14px;
&:hover {
color: #5a64b5;
}
}
span {
&:hover {
color: #5a64b5;
}
}
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !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;
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
.el-table {
--table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
@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;
}
}
}
}
</style>
@@ -0,0 +1,704 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('voiceResource.title') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('voiceClone.searchPlaceholder')" v-model="searchName" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('voiceClone.search') }}</el-button>
</div>
</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="voiceCloneList" class="transparent-table" v-loading="loading"
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
:header-cell-class-name="headerCellClassName">
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.voiceId')" prop="voiceId"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.name')" prop="name"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.userId')" prop="userName"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.platformName')" prop="modelName"
align="center"></el-table-column>
<el-table-column :label="$t('voiceClone.trainStatus')" prop="trainStatus" align="center">
<template slot-scope="scope">
{{ getTrainStatusText(scope.row) }}
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.createdAt')" prop="createdAt" align="center">
<template slot-scope="scope">
{{ formatDate(scope.row.createDate) }}
</template>
</el-table-column>
<el-table-column :label="$t('voiceClone.action')" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="deleteVoiceClone(scope.row)">{{
$t('voiceClone.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">
{{ isAllSelected ? $t('voiceClone.deselectAll') : $t('voiceClone.selectAll') }}
</el-button>
<el-button size="mini" type="success" @click="showAddDialog"
style="background: #5bc98c;border: None;">{{
$t('voiceClone.addNew') }}</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete"
@click="deleteSelectedVoiceClones">{{
$t('voiceClone.delete') }}</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="$t('voiceClone.itemsPerPage', { items: item })" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('voiceClone.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('voiceClone.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('voiceClone.nextPage') }}
</button>
<span class="total-text">{{ $t('voiceClone.totalRecords', { total }) }}</span>
</div>
</div>
</el-card>
</div>
</div>
</div>
<!-- 新增音色资源对话框 -->
<voice-clone-dialog :title="$t('voiceClone.addVoiceClone')" :visible.sync="dialogVisible" :form="voiceCloneForm"
@submit="handleSubmit" @cancel="dialogVisible = false" />
<el-footer>
<version-footer />
</el-footer>
</div>
</template>
<script>
import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import VoiceCloneDialog from "@/components/VoiceResourceDialog.vue";
import { formatDate } from "@/utils/format";
export default {
components: { HeaderBar, VoiceCloneDialog, VersionFooter },
data() {
return {
searchName: "",
loading: false,
voiceCloneList: [],
currentPage: 1,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
total: 0,
dialogVisible: false,
isAllSelected: false,
voiceCloneForm: {
modelId: "",
voiceIds: [],
userId: null
}
};
},
created() {
this.fetchVoiceCloneList();
},
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;
},
},
methods: {
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
this.fetchVoiceCloneList();
},
fetchVoiceCloneList() {
this.loading = true;
const params = {
pageNum: this.currentPage,
pageSize: this.pageSize,
name: this.searchName || "",
orderField: "create_date",
order: "desc"
};
Api.voiceResource.getVoiceResourceList(params, (res) => {
this.loading = false;
res = res.data
if (res.code === 0) {
this.voiceCloneList = res.data.list.map(item => ({
...item,
selected: false
}));
this.total = res.data.total || 0;
} else {
this.voiceCloneList = [];
this.total = 0;
this.$message.error({
message: res?.data?.msg || this.$t('voiceClone.deleteFailed'),
showClose: true
});
}
});
},
handleSearch() {
this.currentPage = 1;
this.fetchVoiceCloneList();
},
handleSelectAll() {
this.isAllSelected = !this.isAllSelected;
this.voiceCloneList.forEach(row => {
row.selected = this.isAllSelected;
});
},
showAddDialog() {
this.voiceCloneForm = {
modelId: "",
voiceIds: [],
userId: null
};
this.$nextTick(() => {
if (this.$refs.voiceCloneForm) {
this.$refs.voiceCloneForm.clearValidate();
}
});
this.dialogVisible = true;
},
handleSubmit(formData) {
Api.voiceResource.saveVoiceResource(formData, (res) => {
res = res.data;
if (res.code === 0) {
this.$message.success({
message: this.$t('voiceClone.addSuccess'),
showClose: true
});
this.dialogVisible = false;
this.fetchVoiceCloneList();
} else {
this.$message.error({
message: res.msg || this.$t('voiceClone.addFailed'),
showClose: true
});
}
});
},
deleteSelectedVoiceClones() {
const selectedRows = this.voiceCloneList.filter(row => row.selected);
if (selectedRows.length === 0) {
this.$message.warning({
message: this.$t('voiceClone.selectFirst'),
showClose: true
});
return;
}
this.deleteVoiceClone(selectedRows);
},
deleteVoiceClone(row) {
const items = Array.isArray(row) ? row : [row];
if (Array.isArray(row) && row.length === 0) {
this.$message.warning({
message: this.$t('voiceClone.selectFirst'),
showClose: true
});
return;
}
const itemCount = items.length;
this.$confirm(this.$t('voiceClone.confirmDelete', { count: itemCount }), 'Warning', {
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
distinguishCancelAndClose: true
}).then(() => {
const ids = items.map(item => item.id);
if (ids.some(id => !id)) {
this.$message.error({
message: this.$t('voiceClone.deleteFailed'),
showClose: true
});
return;
}
Api.voiceResource.deleteVoiceResource(ids, (res) => {
res = res.data;
if (res.code === 0) {
this.$message.success({
message: this.$t('voiceClone.deleteSuccess', { count: itemCount }),
showClose: true
});
this.fetchVoiceCloneList();
} else {
this.$message.error({
message: res.msg || this.$t('voiceClone.deleteFailed'),
showClose: true
});
}
});
}).catch(action => {
if (action === 'cancel') {
this.$message({
type: 'info',
message: this.$t('voiceClone.operationCancelled'),
duration: 1000
});
} else {
this.$message({
type: 'info',
message: this.$t('voiceClone.operationClosed'),
duration: 1000
});
}
});
},
headerCellClassName({ columnIndex }) {
if (columnIndex === 0) {
return "custom-selection-header";
}
return "";
},
goFirst() {
this.currentPage = 1;
this.fetchVoiceCloneList();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchVoiceCloneList();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.fetchVoiceCloneList();
}
},
goToPage(page) {
this.currentPage = page;
this.fetchVoiceCloneList();
},
formatDate,
getTrainStatusText(row) {
if (!row.hasVoice) {
return this.$t('voiceClone.waitingUpload');
}
switch (row.trainStatus) {
case 0:
return this.$t('voiceClone.waitingTraining');
case 1:
return this.$t('voiceClone.training');
case 2:
return this.$t('voiceClone.trainSuccess');
case 3:
return this.$t('voiceClone.trainFailed');
default:
return '';
}
}
},
};
</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 {
margin: 5px 22px;
border-radius: 15px;
min-height: calc(100vh - 24vh);
height: auto;
max-height: 80vh;
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: 16px 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;
padding-bottom: 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: 5px;
.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 {
margin-left: 10px;
color: #606266;
font-size: 14px;
}
.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(.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;
font-weight: 600;
height: 40px;
padding: 8px 0;
font-size: 14px;
border-bottom: 1px solid #e4e7ed;
}
.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);
padding: 8px 0;
height: 40px;
color: #606266;
font-size: 14px;
}
}
.el-table__row:hover>td {
background-color: #f5f7fa !important;
}
&::before {
display: none;
}
}
:deep(.el-table .el-button--text) {
color: #7079aa !important;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5 !important;
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !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;
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
.el-table {
--table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
@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;
}
}
}
}
</style>
+1 -1
View File
@@ -5,7 +5,7 @@ from config.config_loader import load_config
from config.settings import check_config_file
from datetime import datetime
SERVER_VERSION = "0.8.3"
SERVER_VERSION = "0.8.4"
_logger_initialized = False