mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 09:03:54 +08:00
Merge branch 'main' into feature_manage-web&api_agent&device
This commit is contained in:
@@ -19,6 +19,7 @@ import xiaozhi.modules.security.service.ShiroService;
|
|||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +48,7 @@ public class Oauth2Realm extends AuthorizingRealm {
|
|||||||
UserDetail user = (UserDetail) principals.getPrimaryPrincipal();
|
UserDetail user = (UserDetail) principals.getPrimaryPrincipal();
|
||||||
|
|
||||||
//用户权限列表
|
//用户权限列表
|
||||||
Set<String> permsSet = shiroService.getUserPermissions(user);
|
Set<String> permsSet = new HashSet<>();
|
||||||
|
|
||||||
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||||
permsSet.add("sys:role:superAdmin");
|
permsSet.add("sys:role:superAdmin");
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
package xiaozhi.modules.security.service;
|
package xiaozhi.modules.security.service;
|
||||||
|
|
||||||
import xiaozhi.common.user.UserDetail;
|
|
||||||
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* shiro相关接口
|
* shiro相关接口
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
* Website: https://www.renren.io
|
* Website: https://www.renren.io
|
||||||
*/
|
*/
|
||||||
public interface ShiroService {
|
public interface ShiroService {
|
||||||
/**
|
|
||||||
* 获取用户权限列表
|
|
||||||
*/
|
|
||||||
Set<String> getUserPermissions(UserDetail user);
|
|
||||||
|
|
||||||
SysUserTokenEntity getByToken(String token);
|
SysUserTokenEntity getByToken(String token);
|
||||||
|
|
||||||
|
|||||||
-16
@@ -19,22 +19,6 @@ public class ShiroServiceImpl implements ShiroService {
|
|||||||
private final SysUserDao sysUserDao;
|
private final SysUserDao sysUserDao;
|
||||||
private final SysUserTokenDao sysUserTokenDao;
|
private final SysUserTokenDao sysUserTokenDao;
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<String> getUserPermissions(UserDetail user) {
|
|
||||||
//系统管理员,拥有最高权限
|
|
||||||
// TODO: 暂时写死,后续改成从数据库查询
|
|
||||||
List<String> permissionsList = new ArrayList<>();
|
|
||||||
//用户权限列表
|
|
||||||
Set<String> permsSet = new HashSet<>();
|
|
||||||
for (String permissions : permissionsList) {
|
|
||||||
if (StringUtils.isBlank(permissions)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
permsSet.addAll(Arrays.asList(permissions.trim().split(",")));
|
|
||||||
}
|
|
||||||
|
|
||||||
return permsSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysUserTokenEntity getByToken(String token) {
|
public SysUserTokenEntity getByToken(String token) {
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package xiaozhi.modules.timbre.controller;
|
||||||
|
|
||||||
|
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.AllArgsConstructor;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
||||||
|
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
||||||
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色控制层
|
||||||
|
*
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/ttsVoice")
|
||||||
|
@Tag(name = "音色管理")
|
||||||
|
public class TimbreController {
|
||||||
|
private final TimbreService timbreService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "分页查找")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = "ttsModelId", description = "对应 TTS 模型主键", required = true),
|
||||||
|
@Parameter(name = "name", description = "音色名称"),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||||
|
})
|
||||||
|
public Result<PageData<TimbreDetailsVO>> page(
|
||||||
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
TimbrePageDTO dto = new TimbrePageDTO();
|
||||||
|
dto.setTtsModelId((String) params.get("ttsModelId"));
|
||||||
|
dto.setName((String) params.get("name"));
|
||||||
|
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||||
|
dto.setPage((String) params.get(Constant.PAGE));
|
||||||
|
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
PageData<TimbreDetailsVO> page = timbreService.page(dto);
|
||||||
|
return new Result<PageData<TimbreDetailsVO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "音色保存")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody TimbreDataDTO dto) {
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
timbreService.save(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "音色修改")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> update(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody TimbreDataDTO dto) {
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
timbreService.update(id, dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "音色删除")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> delete(@PathVariable Long id) {
|
||||||
|
timbreService.delete(new Long[]{id});
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package xiaozhi.modules.timbre.dao;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色持久层定义
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TimbreDao extends BaseMapper<TimbreEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package xiaozhi.modules.timbre.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色表数据DTO
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "音色表信息")
|
||||||
|
public class TimbreDataDTO {
|
||||||
|
|
||||||
|
@Schema(description = "语言")
|
||||||
|
@NotBlank(message = "{timbre.languages.require}")
|
||||||
|
private String languages;
|
||||||
|
|
||||||
|
@Schema(description = "音色名称")
|
||||||
|
@NotBlank(message = "{timbre.name.require}")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
@NotBlank(message = "{timbre.remark.require}")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
@Min(value = 0, message = "{sort.number}")
|
||||||
|
private long sort;
|
||||||
|
|
||||||
|
@Schema(description = "对应 TTS 模型主键")
|
||||||
|
@NotBlank(message = "{timbre.ttsModelId.require}")
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色编码")
|
||||||
|
@NotBlank(message = "{timbre.ttsVoice.require}")
|
||||||
|
private String ttsVoice;
|
||||||
|
|
||||||
|
@Schema(description = "音频播放地址")
|
||||||
|
@NotBlank(message = "{timbre.voiceDemo.require}")
|
||||||
|
private String voiceDemo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package xiaozhi.modules.timbre.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色分页参数DTO
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "音色分页参数")
|
||||||
|
public class TimbrePageDTO {
|
||||||
|
|
||||||
|
@Schema(description = "对应 TTS 模型主键")
|
||||||
|
@NotBlank(message = "{timbre.ttsModelId.require}")
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "页数")
|
||||||
|
private String page;
|
||||||
|
|
||||||
|
@Schema(description = "显示列数")
|
||||||
|
private String limit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package xiaozhi.modules.timbre.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import xiaozhi.common.entity.BaseEntity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色表实体类
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("ai_tts_voice")
|
||||||
|
@Schema(description = "音色信息")
|
||||||
|
public class TimbreEntity extends BaseEntity {
|
||||||
|
|
||||||
|
@Schema(description = "语言")
|
||||||
|
private String languages;
|
||||||
|
|
||||||
|
@Schema(description = "音色名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private long sort;
|
||||||
|
|
||||||
|
@Schema(description = "对应 TTS 模型主键")
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色编码")
|
||||||
|
private String ttsVoice;
|
||||||
|
|
||||||
|
@Schema(description = "音频播放地址")
|
||||||
|
private String voiceDemo;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package xiaozhi.modules.timbre.service;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
||||||
|
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
||||||
|
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
||||||
|
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色的业务层的定义
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
public interface TimbreService extends BaseService<TimbreEntity> {
|
||||||
|
/**
|
||||||
|
* 分页获取音色指定tts的下的音色
|
||||||
|
* @param dto 分页查找参数
|
||||||
|
* @return 音色列表分页数据
|
||||||
|
*/
|
||||||
|
PageData<TimbreDetailsVO> page(TimbrePageDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音色指定id的详情信息
|
||||||
|
* @param timbreId 音色表id
|
||||||
|
* @return 音色信息
|
||||||
|
*/
|
||||||
|
TimbreDetailsVO get(Long timbreId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存音色信息
|
||||||
|
* @param dto 需要保存数据
|
||||||
|
*/
|
||||||
|
void save(TimbreDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存音色信息
|
||||||
|
* @param timbreId 需要修改的id
|
||||||
|
* @param dto 需要修改的数据
|
||||||
|
*/
|
||||||
|
void update(Long timbreId,TimbreDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除音色
|
||||||
|
* @param ids 需要被删除的音色id列表
|
||||||
|
*/
|
||||||
|
void delete(Long[] ids);
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package xiaozhi.modules.timbre.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.modules.timbre.dao.TimbreDao;
|
||||||
|
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
||||||
|
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
||||||
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
||||||
|
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色的业务层的实现
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<TimbreDetailsVO> page(TimbrePageDTO dto) {
|
||||||
|
Map<String, Object> params = new HashMap<String, Object>();
|
||||||
|
params.put(Constant.PAGE, dto.getPage());
|
||||||
|
params.put(Constant.LIMIT,dto.getLimit());
|
||||||
|
IPage<TimbreEntity> page = baseDao.selectPage(
|
||||||
|
getPage(params, "sort", true),
|
||||||
|
//定义查询条件
|
||||||
|
new QueryWrapper<TimbreEntity>()
|
||||||
|
//必须按照ttsID查找
|
||||||
|
.eq("tts_model_id",dto.getTtsModelId())
|
||||||
|
//如果有音色名字,按照音色名模糊查找
|
||||||
|
.like(StringUtils.isNotBlank(dto.getName()),"name",dto.getName())
|
||||||
|
);
|
||||||
|
|
||||||
|
return getPageData(page, TimbreDetailsVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TimbreDetailsVO get(Long timbreId) {
|
||||||
|
TimbreEntity entity = baseDao.selectById(timbreId);
|
||||||
|
return ConvertUtils.sourceToTarget(entity, TimbreDetailsVO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void save(TimbreDataDTO dto) {
|
||||||
|
isTtsModelId(dto.getTtsModelId());
|
||||||
|
TimbreEntity timbreEntity = ConvertUtils.sourceToTarget(dto, TimbreEntity.class);
|
||||||
|
baseDao.insert(timbreEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void update(Long timbreId, TimbreDataDTO dto) {
|
||||||
|
isTtsModelId(dto.getTtsModelId());
|
||||||
|
TimbreEntity timbreEntity = ConvertUtils.sourceToTarget(dto, TimbreEntity.class);
|
||||||
|
timbreEntity.setId(timbreId);
|
||||||
|
baseDao.updateById(timbreEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void delete(Long[] ids) {
|
||||||
|
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理是不是tts模型的id
|
||||||
|
*/
|
||||||
|
private void isTtsModelId(String ttsModelId){
|
||||||
|
//等模型配置那边写好调用方法判断
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package xiaozhi.modules.timbre.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色详情展示VO
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TimbreDetailsVO implements Serializable {
|
||||||
|
@Schema(description = "音色id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "语言")
|
||||||
|
private String languages;
|
||||||
|
|
||||||
|
@Schema(description = "音色名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private long sort;
|
||||||
|
|
||||||
|
@Schema(description = "对应 TTS 模型主键")
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色编码")
|
||||||
|
private String ttsVoice;
|
||||||
|
|
||||||
|
@Schema(description = "音频播放地址")
|
||||||
|
private String voiceDemo;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,3 +20,12 @@ sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
|
|
||||||
|
|
||||||
|
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,11 @@ sysuser.deptId.require=Departments cannot be empty
|
|||||||
sysuser.status.range=State ranges from 0 to 1
|
sysuser.status.range=State ranges from 0 to 1
|
||||||
sysuser.captcha.require=The captcha cannot be empty
|
sysuser.captcha.require=The captcha cannot be empty
|
||||||
sysuser.uuid.require=The unique identifier cannot be empty
|
sysuser.uuid.require=The unique identifier cannot be empty
|
||||||
|
|
||||||
|
timbre.languages.require=The language of the timbre cannot be empty
|
||||||
|
timbre.name.require=The name of the timbre cannot be empty
|
||||||
|
timbre.remark.require=The remark of the timbre cannot be empty
|
||||||
|
timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
|
||||||
|
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
|
||||||
|
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,10 @@ sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
|||||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
|
|
||||||
|
timbre.languages.require=\u97F3\u8272\u7684\u8A9E\u8A00\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u7A31\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
|
timbre.remark.require=\u97F3\u8272\u7684\u5099\u6CE8\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
|
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,10 @@ sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||||
|
|
||||||
|
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
<el-header>
|
<el-header>
|
||||||
<div
|
<div
|
||||||
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/>
|
<img alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/>
|
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main style="position: relative;">
|
<el-main style="position: relative;">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div
|
<div
|
||||||
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||||
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/>
|
<img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||||
<div class="login-text">登录</div>
|
<div class="login-text">登录</div>
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
WELCOME TO LOGIN
|
WELCOME TO LOGIN
|
||||||
@@ -20,16 +20,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="padding: 0 30px;">
|
<div style="padding: 0 30px;">
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img src="@/assets/login/username.png" alt="" class="input-icon"/>
|
<img alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||||
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
|
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
<img alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="captchaUrl"
|
<img v-if="captchaUrl"
|
||||||
@@ -105,33 +105,43 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 封装输入验证逻辑
|
||||||
|
validateInput(input, message) {
|
||||||
|
if (!input.trim()) {
|
||||||
|
showDanger(message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
async login() {
|
async login() {
|
||||||
if (!this.form.username.trim()) { // 替换isNull校验
|
// 验证用户名
|
||||||
showDanger('用户名不能为空')
|
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!this.form.password.trim()) { // 替换isNull校验
|
// 验证密码
|
||||||
showDanger('密码不能为空')
|
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!this.form.captcha.trim()) { // 替换isNull校验
|
// 验证验证码
|
||||||
showDanger('验证码不能为空')
|
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.form.captchaId = this.captchaUuid
|
this.form.captchaId = this.captchaUuid
|
||||||
Api.user.login(this.form, ({data}) => {
|
Api.user.login(this.form, ({data}) => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
showSuccess('登陆成功!')
|
showSuccess('登陆成功!')
|
||||||
|
// 将令牌存储到 Vuex 中
|
||||||
this.$store.commit('setToken', JSON.stringify(data.data))
|
this.$store.commit('setToken', JSON.stringify(data.data))
|
||||||
|
|
||||||
goToPage('/home')
|
goToPage('/home')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 重新获取验证码
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.fetchCaptcha()
|
this.fetchCaptcha();
|
||||||
}, 1000)
|
}, 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
goToRegister() {
|
goToRegister() {
|
||||||
@@ -140,6 +150,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import './auth.scss'; // 添加这行引用
|
@import './auth.scss'; // 添加这行引用
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
<!-- 保持相同的头部 -->
|
<!-- 保持相同的头部 -->
|
||||||
<el-header>
|
<el-header>
|
||||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/>
|
<img alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;"/>
|
||||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/>
|
<img alt="" src="@/assets/xiaozhi-ai.png" style="width: 70px;height: 13px;"/>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<!-- 修改标题部分 -->
|
<!-- 修改标题部分 -->
|
||||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||||
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/>
|
<img alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;"/>
|
||||||
<div class="login-text">注册</div>
|
<div class="login-text">注册</div>
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
WELCOME TO REGISTER
|
WELCOME TO REGISTER
|
||||||
@@ -23,33 +23,33 @@
|
|||||||
<div style="padding: 0 30px;">
|
<div style="padding: 0 30px;">
|
||||||
<!-- 用户名输入框 -->
|
<!-- 用户名输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img src="@/assets/login/username.png" alt="" class="input-icon"/>
|
<img alt="" class="input-icon" src="@/assets/login/username.png"/>
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 密码输入框 -->
|
<!-- 密码输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||||
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
|
<el-input v-model="form.password" placeholder="请输入密码" type="password"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增确认密码 -->
|
<!-- 新增确认密码 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
<img alt="" class="input-icon" src="@/assets/login/password.png"/>
|
||||||
<el-input v-model="form.confirmPassword" type="password" placeholder="请确认密码"/>
|
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 验证码部分保持相同 -->
|
<!-- 验证码部分保持相同 -->
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
<img alt="" class="input-icon" src="@/assets/login/shield.png"/>
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||||
</div>
|
</div>
|
||||||
<img v-if="captchaUrl"
|
<img v-if="captchaUrl"
|
||||||
:src="captchaUrl"
|
:src="captchaUrl"
|
||||||
alt="验证码"
|
alt="验证码"
|
||||||
style="width: 150px; height: 40px; cursor: pointer;"
|
style="width: 150px; height: 40px; cursor: pointer;"
|
||||||
@click="fetchCaptcha"
|
@click="fetchCaptcha"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,24 +119,33 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 封装输入验证逻辑
|
||||||
|
validateInput(input, message) {
|
||||||
|
if (!input.trim()) {
|
||||||
|
showDanger(message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
// 注册逻辑
|
// 注册逻辑
|
||||||
register() {
|
register() {
|
||||||
if (!this.form.username.trim()) {
|
// 验证用户名
|
||||||
showDanger('用户名不能为空')
|
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!this.form.password.trim()) {
|
// 验证密码
|
||||||
showDanger('密码不能为空')
|
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (this.form.password !== this.form.confirmPassword) {
|
if (this.form.password !== this.form.confirmPassword) {
|
||||||
showDanger('两次输入的密码不一致')
|
showDanger('两次输入的密码不一致')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.form.captcha.trim()) {
|
// 验证验证码
|
||||||
showDanger('验证码不能为空')
|
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Api.user.register(this.form, ({data}) => {
|
Api.user.register(this.form, ({data}) => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
@@ -159,6 +168,6 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
@import './auth.scss'; // 修改为导入新建的SCSS文件
|
@import './auth.scss'; // 修改为导入新建的SCSS文件
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ dotenv.config();
|
|||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
devServer: {
|
devServer: {
|
||||||
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
|
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
|
||||||
|
port: 8001, // 指定端口为 8001
|
||||||
proxy: {
|
proxy: {
|
||||||
'/xiaozhi-esp32-api': {
|
'/xiaozhi-esp32-api': {
|
||||||
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
|
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ selected_module:
|
|||||||
TTS: EdgeTTS
|
TTS: EdgeTTS
|
||||||
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
|
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
|
||||||
Memory: nomem
|
Memory: nomem
|
||||||
# 意图识别模块,默认不开启。开启后,可以播放音乐、控制音量、识别退出指令
|
# 意图识别模块,默认使用function_call。开启后,可以播放音乐、控制音量、识别退出指令
|
||||||
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
|
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
|
||||||
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
|
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
|
||||||
# 如果意图识别设置成 function_call,建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
||||||
Intent: nointent
|
Intent: function_call
|
||||||
|
|
||||||
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
|
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
|
||||||
Intent:
|
Intent:
|
||||||
@@ -94,10 +94,11 @@ Intent:
|
|||||||
type: nointent
|
type: nointent
|
||||||
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
|
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
|
||||||
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
|
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
|
||||||
# 下面是加载查天气、角色切换的插件示例
|
# 下面是加载查天气、角色切换、加载查新闻的插件示例
|
||||||
functions:
|
functions:
|
||||||
- change_role
|
- change_role
|
||||||
- get_weather
|
- get_weather
|
||||||
|
- get_news
|
||||||
|
|
||||||
# 插件的基础配置
|
# 插件的基础配置
|
||||||
plugins:
|
plugins:
|
||||||
@@ -106,6 +107,14 @@ plugins:
|
|||||||
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
||||||
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
||||||
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
||||||
|
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
||||||
|
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
||||||
|
get_news:
|
||||||
|
default_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
|
||||||
|
category_urls:
|
||||||
|
society: "https://www.chinanews.com.cn/rss/society.xml"
|
||||||
|
world: "https://www.chinanews.com.cn/rss/world.xml"
|
||||||
|
finance: "https://www.chinanews.com.cn/rss/finance.xml"
|
||||||
|
|
||||||
Memory:
|
Memory:
|
||||||
mem0ai:
|
mem0ai:
|
||||||
@@ -156,6 +165,18 @@ LLM:
|
|||||||
top_p: 1
|
top_p: 1
|
||||||
top_k: 50
|
top_k: 50
|
||||||
frequency_penalty: 0 # 频率惩罚
|
frequency_penalty: 0 # 频率惩罚
|
||||||
|
AliAppLLM:
|
||||||
|
# 定义LLM API类型
|
||||||
|
type: AliBL
|
||||||
|
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||||
|
app_id: 你的app_id
|
||||||
|
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
|
||||||
|
api_key: 你的api_key
|
||||||
|
# 是否不使用本地prompt:true|false (默不用请在百练应用中设置prompt)
|
||||||
|
is_no_prompt: true
|
||||||
|
# Ali_memory_id:false(不使用)|你的memory_id(请在百练应用中设置中获取)
|
||||||
|
# Tips!:Ali_memory未实现多用户存储记忆(记忆按id调用)
|
||||||
|
ali_memory_id: false
|
||||||
DoubaoLLM:
|
DoubaoLLM:
|
||||||
# 定义LLM API类型
|
# 定义LLM API类型
|
||||||
type: openai
|
type: openai
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from config.logger import setup_logging
|
||||||
|
from http import HTTPStatus
|
||||||
|
from dashscope import Application
|
||||||
|
from core.providers.llm.base import LLMProviderBase
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
class LLMProvider(LLMProviderBase):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.api_key = config["api_key"]
|
||||||
|
self.app_id = config["app_id"]
|
||||||
|
self.base_url = config.get("base_url")
|
||||||
|
self.is_No_prompt = config.get("is_no_prompt")
|
||||||
|
self.memory_id = config.get("ali_memory_id")
|
||||||
|
|
||||||
|
def response(self, session_id, dialogue):
|
||||||
|
try:
|
||||||
|
# 处理dialogue
|
||||||
|
if self.is_No_prompt:
|
||||||
|
dialogue.pop(0)
|
||||||
|
logger.bind(tag=TAG).debug(f"【阿里百练API服务】处理后的dialogue: {dialogue}")
|
||||||
|
|
||||||
|
# 构造调用参数
|
||||||
|
call_params = {
|
||||||
|
"api_key": self.api_key,
|
||||||
|
"app_id": self.app_id,
|
||||||
|
"session_id": session_id,
|
||||||
|
"messages": dialogue
|
||||||
|
}
|
||||||
|
if self.memory_id != False:
|
||||||
|
# 百练memory需要prompt参数
|
||||||
|
prompt = dialogue[-1].get("content")
|
||||||
|
call_params["memory_id"] = self.memory_id
|
||||||
|
call_params["prompt"] = prompt
|
||||||
|
logger.bind(tag=TAG).debug(f"【阿里百练API服务】处理后的prompt: {prompt}")
|
||||||
|
|
||||||
|
responses = Application.call(**call_params)
|
||||||
|
if responses.status_code != HTTPStatus.OK:
|
||||||
|
logger.bind(tag=TAG).error(
|
||||||
|
f"code={responses.status_code}, "
|
||||||
|
f"message={responses.message}, "
|
||||||
|
f"请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code"
|
||||||
|
)
|
||||||
|
yield "【阿里百练API服务响应异常】"
|
||||||
|
else:
|
||||||
|
logger.bind(tag=TAG).debug(f"【阿里百练API服务】构造参数: {call_params}")
|
||||||
|
yield responses.output.text
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"【阿里百练API服务】响应异常: {e}")
|
||||||
|
yield "【LLM服务响应异常】"
|
||||||
@@ -5,6 +5,7 @@ import numpy as np
|
|||||||
import opuslib_next
|
import opuslib_next
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from core.utils.tts import MarkdownCleaner
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -23,6 +24,7 @@ class TTSProviderBase(ABC):
|
|||||||
tmp_file = self.generate_filename()
|
tmp_file = self.generate_filename()
|
||||||
try:
|
try:
|
||||||
max_repeat_time = 5
|
max_repeat_time = 5
|
||||||
|
text = MarkdownCleaner.clean_markdown(text)
|
||||||
while not os.path.exists(tmp_file) and max_repeat_time > 0:
|
while not os.path.exists(tmp_file) and max_repeat_time > 0:
|
||||||
asyncio.run(self.text_to_speak(text, tmp_file))
|
asyncio.run(self.text_to_speak(text, tmp_file))
|
||||||
if not os.path.exists(tmp_file):
|
if not os.path.exists(tmp_file):
|
||||||
@@ -47,7 +49,8 @@ class TTSProviderBase(ABC):
|
|||||||
file_type = os.path.splitext(audio_file_path)[1]
|
file_type = os.path.splitext(audio_file_path)[1]
|
||||||
if file_type:
|
if file_type:
|
||||||
file_type = file_type.lstrip('.')
|
file_type = file_type.lstrip('.')
|
||||||
audio = AudioSegment.from_file(audio_file_path, format=file_type)
|
# 读取音频文件,-nostdin 参数:不要从标准输入读取数据,否则FFmpeg会阻塞
|
||||||
|
audio = AudioSegment.from_file(audio_file_path, format=file_type, parameters=["-nostdin"])
|
||||||
|
|
||||||
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
|
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
|
||||||
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
|
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import importlib
|
import importlib
|
||||||
@@ -14,4 +15,98 @@ def create_instance(class_name, *args, **kwargs):
|
|||||||
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
|
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
|
||||||
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
||||||
|
|
||||||
raise ValueError(f"不支持的TTS类型: {class_name},请检查该配置的type是否设置正确")
|
raise ValueError(f"不支持的TTS类型: {class_name},请检查该配置的type是否设置正确")
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownCleaner:
|
||||||
|
"""
|
||||||
|
封装 Markdown 清理逻辑:直接用 MarkdownCleaner.clean_markdown(text) 即可
|
||||||
|
"""
|
||||||
|
# 公式字符
|
||||||
|
NORMAL_FORMULA_CHARS = re.compile(r'[a-zA-Z\\^_{}\+\-\(\)\[\]=]')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _replace_inline_dollar(m: re.Match) -> str:
|
||||||
|
"""
|
||||||
|
只要捕获到完整的 "$...$":
|
||||||
|
- 如果内部有典型公式字符 => 去掉两侧 $
|
||||||
|
- 否则 (纯数字/货币等) => 保留 "$...$"
|
||||||
|
"""
|
||||||
|
content = m.group(1)
|
||||||
|
if MarkdownCleaner.NORMAL_FORMULA_CHARS.search(content):
|
||||||
|
return content
|
||||||
|
else:
|
||||||
|
return m.group(0)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _replace_table_block(match: re.Match) -> str:
|
||||||
|
"""
|
||||||
|
当匹配到一个整段表格块时,回调该函数。
|
||||||
|
"""
|
||||||
|
block_text = match.group('table_block')
|
||||||
|
lines = block_text.strip('\n').split('\n')
|
||||||
|
|
||||||
|
parsed_table = []
|
||||||
|
for line in lines:
|
||||||
|
line_stripped = line.strip()
|
||||||
|
if re.match(r'^\|\s*[-:]+\s*(\|\s*[-:]+\s*)+\|?$', line_stripped):
|
||||||
|
continue
|
||||||
|
columns = [col.strip() for col in line_stripped.split('|') if col.strip() != '']
|
||||||
|
if columns:
|
||||||
|
parsed_table.append(columns)
|
||||||
|
|
||||||
|
if not parsed_table:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
headers = parsed_table[0]
|
||||||
|
data_rows = parsed_table[1:] if len(parsed_table) > 1 else []
|
||||||
|
|
||||||
|
lines_for_tts = []
|
||||||
|
if len(parsed_table) == 1:
|
||||||
|
# 只有一行
|
||||||
|
only_line_str = ", ".join(parsed_table[0])
|
||||||
|
lines_for_tts.append(f"单行表格:{only_line_str}")
|
||||||
|
else:
|
||||||
|
lines_for_tts.append(f"表头是:{', '.join(headers)}")
|
||||||
|
for i, row in enumerate(data_rows, start=1):
|
||||||
|
row_str_list = []
|
||||||
|
for col_index, cell_val in enumerate(row):
|
||||||
|
if col_index < len(headers):
|
||||||
|
row_str_list.append(f"{headers[col_index]} = {cell_val}")
|
||||||
|
else:
|
||||||
|
row_str_list.append(cell_val)
|
||||||
|
lines_for_tts.append(f"第 {i} 行:{', '.join(row_str_list)}")
|
||||||
|
|
||||||
|
return "\n".join(lines_for_tts) + "\n"
|
||||||
|
|
||||||
|
# 预编译所有正则表达式(按执行频率排序)
|
||||||
|
# 这里要把 replace_xxx 的静态方法放在最前定义,以便在列表里能正确引用它们。
|
||||||
|
REGEXES = [
|
||||||
|
(re.compile(r'```.*?```', re.DOTALL), ''), # 代码块
|
||||||
|
(re.compile(r'^#+\s*', re.MULTILINE), ''), # 标题
|
||||||
|
(re.compile(r'(\*\*|__)(.*?)\1'), r'\2'), # 粗体
|
||||||
|
(re.compile(r'(\*|_)(?=\S)(.*?)(?<=\S)\1'), r'\2'), # 斜体
|
||||||
|
(re.compile(r'!\[.*?\]\(.*?\)'), ''), # 图片
|
||||||
|
(re.compile(r'\[(.*?)\]\(.*?\)'), r'\1'), # 链接
|
||||||
|
(re.compile(r'^\s*>+\s*', re.MULTILINE), ''), # 引用
|
||||||
|
(
|
||||||
|
re.compile(r'(?P<table_block>(?:^[^\n]*\|[^\n]*\n)+)', re.MULTILINE),
|
||||||
|
_replace_table_block
|
||||||
|
),
|
||||||
|
(re.compile(r'^\s*[*+-]\s*', re.MULTILINE), '- '), # 列表
|
||||||
|
(re.compile(r'\$\$.*?\$\$', re.DOTALL), ''), # 块级公式
|
||||||
|
(
|
||||||
|
re.compile(r'(?<![A-Za-z0-9])\$([^\n$]+)\$(?![A-Za-z0-9])'),
|
||||||
|
_replace_inline_dollar
|
||||||
|
),
|
||||||
|
(re.compile(r'\n{2,}'), '\n'), # 多余空行
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clean_markdown(text: str) -> str:
|
||||||
|
"""
|
||||||
|
主入口方法:依序执行所有正则,移除或替换 Markdown 元素
|
||||||
|
"""
|
||||||
|
for regex, replacement in MarkdownCleaner.REGEXES:
|
||||||
|
text = regex.sub(replacement, text)
|
||||||
|
return text.strip()
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import random
|
||||||
|
import requests
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from config.logger import setup_logging
|
||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
GET_NEWS_FUNCTION_DESC = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_news",
|
||||||
|
"description": (
|
||||||
|
"获取最新新闻,随机选择一条新闻进行播报。"
|
||||||
|
"用户可以指定新闻类型,如社会新闻、科技新闻、国际新闻等。"
|
||||||
|
"如果没有指定,默认播报社会新闻。"
|
||||||
|
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "新闻类别,例如社会、科技、国际。可选参数,如果不提供则使用默认类别"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"
|
||||||
|
},
|
||||||
|
"lang": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["lang"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_news_from_rss(rss_url):
|
||||||
|
"""从RSS源获取新闻列表"""
|
||||||
|
try:
|
||||||
|
response = requests.get(rss_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# 解析XML
|
||||||
|
root = ET.fromstring(response.content)
|
||||||
|
|
||||||
|
# 查找所有item元素(新闻条目)
|
||||||
|
news_items = []
|
||||||
|
for item in root.findall('.//item'):
|
||||||
|
title = item.find('title').text if item.find('title') is not None else "无标题"
|
||||||
|
link = item.find('link').text if item.find('link') is not None else "#"
|
||||||
|
description = item.find('description').text if item.find('description') is not None else "无描述"
|
||||||
|
pubDate = item.find('pubDate').text if item.find('pubDate') is not None else "未知时间"
|
||||||
|
|
||||||
|
news_items.append({
|
||||||
|
'title': title,
|
||||||
|
'link': link,
|
||||||
|
'description': description,
|
||||||
|
'pubDate': pubDate
|
||||||
|
})
|
||||||
|
|
||||||
|
return news_items
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"获取RSS新闻失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_news_detail(url):
|
||||||
|
"""获取新闻详情页内容并总结"""
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.content, 'html.parser')
|
||||||
|
|
||||||
|
# 尝试提取正文内容 (这里的选择器需要根据实际网站结构调整)
|
||||||
|
content_div = soup.select_one('.content_desc, .content, article, .article-content')
|
||||||
|
if content_div:
|
||||||
|
paragraphs = content_div.find_all('p')
|
||||||
|
content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
|
||||||
|
return content
|
||||||
|
else:
|
||||||
|
# 如果找不到特定的内容区域,尝试获取所有段落
|
||||||
|
paragraphs = soup.find_all('p')
|
||||||
|
content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
|
||||||
|
return content[:2000] # 限制长度
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"获取新闻详情失败: {e}")
|
||||||
|
return "无法获取详细内容"
|
||||||
|
|
||||||
|
|
||||||
|
def map_category(category_text):
|
||||||
|
"""将用户输入的中文类别映射到配置文件中的类别键"""
|
||||||
|
if not category_text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件
|
||||||
|
category_map = {
|
||||||
|
# 社会新闻
|
||||||
|
"社会": "society",
|
||||||
|
"社会新闻": "society",
|
||||||
|
# 国际新闻
|
||||||
|
"国际": "world",
|
||||||
|
"国际新闻": "world",
|
||||||
|
# 财经新闻
|
||||||
|
"财经": "finance",
|
||||||
|
"财经新闻": "finance",
|
||||||
|
"金融": "finance",
|
||||||
|
"经济": "finance"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 转换为小写并去除空格
|
||||||
|
normalized_category = category_text.lower().strip()
|
||||||
|
|
||||||
|
# 返回映射结果,如果没有匹配项则返回原始输入
|
||||||
|
return category_map.get(normalized_category, category_text)
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('get_news', GET_NEWS_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
||||||
|
def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_CN"):
|
||||||
|
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
|
||||||
|
try:
|
||||||
|
# 如果detail为True,获取上一条新闻的详细内容
|
||||||
|
if detail:
|
||||||
|
if not hasattr(conn, 'last_news_link') or not conn.last_news_link or 'link' not in conn.last_news_link:
|
||||||
|
return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None)
|
||||||
|
|
||||||
|
link = conn.last_news_link.get('link')
|
||||||
|
title = conn.last_news_link.get('title', '未知标题')
|
||||||
|
|
||||||
|
if link == '#':
|
||||||
|
return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
|
||||||
|
|
||||||
|
logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}")
|
||||||
|
|
||||||
|
# 获取新闻详情
|
||||||
|
detail_content = fetch_news_detail(link)
|
||||||
|
|
||||||
|
if not detail_content or detail_content == "无法获取详细内容":
|
||||||
|
return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
|
||||||
|
|
||||||
|
# 构建详情报告
|
||||||
|
detail_report = (
|
||||||
|
f"根据下列数据,用{lang}回应用户的新闻详情查询请求:\n\n"
|
||||||
|
f"新闻标题: {title}\n"
|
||||||
|
f"详细内容: {detail_content}\n\n"
|
||||||
|
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
|
||||||
|
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ActionResponse(Action.REQLLM, detail_report, None)
|
||||||
|
|
||||||
|
# 否则,获取新闻列表并随机选择一条
|
||||||
|
# 从配置中获取RSS URL
|
||||||
|
rss_config = conn.config["plugins"]["get_news"]
|
||||||
|
default_rss_url = rss_config.get("default_rss_url", "https://www.chinanews.com.cn/rss/society.xml")
|
||||||
|
|
||||||
|
# 将用户输入的类别映射到配置中的类别键
|
||||||
|
mapped_category = map_category(category)
|
||||||
|
|
||||||
|
# 如果提供了类别,尝试从配置中获取对应的URL
|
||||||
|
rss_url = default_rss_url
|
||||||
|
if mapped_category and mapped_category in rss_config.get("category_urls", {}):
|
||||||
|
rss_url = rss_config["category_urls"][mapped_category]
|
||||||
|
|
||||||
|
logger.bind(tag=TAG).info(f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}")
|
||||||
|
|
||||||
|
# 获取新闻列表
|
||||||
|
news_items = fetch_news_from_rss(rss_url)
|
||||||
|
|
||||||
|
if not news_items:
|
||||||
|
return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None)
|
||||||
|
|
||||||
|
# 随机选择一条新闻
|
||||||
|
selected_news = random.choice(news_items)
|
||||||
|
|
||||||
|
# 保存当前新闻链接到连接对象,以便后续查询详情
|
||||||
|
if not hasattr(conn, 'last_news_link'):
|
||||||
|
conn.last_news_link = {}
|
||||||
|
conn.last_news_link = {
|
||||||
|
'link': selected_news.get('link', '#'),
|
||||||
|
'title': selected_news.get('title', '未知标题')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建新闻报告
|
||||||
|
news_report = (
|
||||||
|
f"根据下列数据,用{lang}回应用户的新闻查询请求:\n\n"
|
||||||
|
f"新闻标题: {selected_news['title']}\n"
|
||||||
|
f"发布时间: {selected_news['pubDate']}\n"
|
||||||
|
f"新闻内容: {selected_news['description']}\n"
|
||||||
|
f"(请以自然、流畅的方式向用户播报这条新闻,可以适当总结内容,"
|
||||||
|
f"直接读出新闻即可,不需要额外多余的内容。"
|
||||||
|
f"如果用户询问更多详情,告知用户可以说'请详细介绍这条新闻'获取更多内容)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ActionResponse(Action.REQLLM, news_report, None)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
|
||||||
|
return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
|
||||||
Reference in New Issue
Block a user