mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-24 08:03:53 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0f777b9b4 | ||
|
|
dfe3de14d8 | ||
|
|
abe234c7ce | ||
|
|
d7bf67fd0e | ||
|
|
8218996f04 | ||
|
|
d88559f5de | ||
|
|
f398115df3 | ||
|
|
82413fecab | ||
|
|
d5a5b24223 | ||
|
|
472dba2665 | ||
|
|
a5444b11b3 | ||
|
|
5a05a68524 | ||
|
|
36be61bbe1 | ||
|
|
ec6ab983f7 | ||
|
|
69630c2be0 | ||
|
|
8e605dd1d4 | ||
|
|
8bb0cf02ad | ||
|
|
60c054f457 | ||
|
|
5648ee974a | ||
|
|
36d1370720 | ||
|
|
7fe797d44d | ||
|
|
a1e959420d | ||
|
|
4c99b40257 | ||
|
|
ead0958d18 | ||
|
|
a1730b2d52 | ||
|
|
06cbffdea4 | ||
|
|
6cd88fb98f | ||
|
|
3ea1a2bf2f | ||
|
|
19fe2d2f03 |
@@ -51,7 +51,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/av114036381327149" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||||
</picture>
|
</picture>
|
||||||
@@ -95,6 +95,11 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -185,7 +190,6 @@ server:
|
|||||||
| LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
|
| LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
|
||||||
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
||||||
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
||||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
|
||||||
|
|
||||||
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||||
|
|
||||||
@@ -370,7 +374,41 @@ VAD:
|
|||||||
|
|
||||||
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||||
|
|
||||||
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
|
本项目,支持以工具调用的方式控制HomeAssistant设备
|
||||||
|
|
||||||
|
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`。
|
||||||
|
|
||||||
|
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`。
|
||||||
|
|
||||||
|
3、登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
||||||
|
|
||||||
|
在配置文件中,配置好你的home assistant的`devices`(被控制的设备)和`api_key`和`base_url`等信息。例如:
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
plugins
|
||||||
|
home_assistant:
|
||||||
|
devices:
|
||||||
|
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
|
||||||
|
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
|
||||||
|
base_url: http://你的homeassistant地址:8123
|
||||||
|
api_key: 你的home assistant api访问令牌
|
||||||
|
```
|
||||||
|
|
||||||
|
最后,允许function_call 插件在配置文件中启用`hass_get_state`(必须)、`hass_set_state`(必须)、`hass_play_music`(不想用ha听音乐可以不启动),例如:
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
Intent:
|
||||||
|
...
|
||||||
|
function_call:
|
||||||
|
type: nointent
|
||||||
|
functions:
|
||||||
|
- change_role
|
||||||
|
- get_weather
|
||||||
|
- get_news
|
||||||
|
- hass_get_state
|
||||||
|
- hass_set_state
|
||||||
|
- hass_play_music
|
||||||
|
```
|
||||||
|
|
||||||
### 7、更多问题,可联系我们反馈 💬
|
### 7、更多问题,可联系我们反馈 💬
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 289 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 338 KiB |
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true">
|
||||||
|
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="80px">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="音色编码" prop="voiceCode">
|
||||||
|
<el-input v-model="voiceForm.voiceCode" placeholder="请输入内容" class="compact-input"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="音色名称" prop="voiceName">
|
||||||
|
<el-input v-model="voiceForm.voiceName" placeholder="请输入内容" class="compact-input"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="语言类型" prop="languageType">
|
||||||
|
<el-input v-model="voiceForm.languageType" placeholder="请输入内容" class="compact-input"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="排序号" prop="sortNumber">
|
||||||
|
<el-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" placeholder="请输入内容" class="compact-textarea"
|
||||||
|
></el-input>
|
||||||
|
<div class="audio-controls">
|
||||||
|
<div class="audio-player">
|
||||||
|
<audio
|
||||||
|
:src="audioUrl"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
class="custom-audio"
|
||||||
|
></audio>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="mini" class="preview-btn">生成试听</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||||
|
<el-button type="primary" @click="handleClose">关闭</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'EditVoiceDialog',
|
||||||
|
props: {
|
||||||
|
showDialog: Boolean,
|
||||||
|
voiceData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
voiceCode: 'wawaxiaohe',
|
||||||
|
voiceName: '湾湾小何',
|
||||||
|
languageType: '中文',
|
||||||
|
sortNumber: 123
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogVisible: this.showDialog,
|
||||||
|
voiceForm: { ...this.voiceData },
|
||||||
|
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
|
||||||
|
generatedAudio: null,
|
||||||
|
rules: {
|
||||||
|
voiceCode: [{ required: true, message: '请输入音色编码', trigger: 'blur' }],
|
||||||
|
voiceName: [{ required: true, message: '请输入音色名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
showDialog(newVal) {
|
||||||
|
this.dialogVisible = newVal
|
||||||
|
if (newVal) this.voiceForm = { ...this.voiceData }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
this.dialogVisible = false
|
||||||
|
this.$emit('update:showDialog', false)
|
||||||
|
},
|
||||||
|
handleSave() {
|
||||||
|
this.$refs.voiceForm.validate(valid => {
|
||||||
|
if (valid) this.$emit('save', this.voiceForm)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.compact-dialog {
|
||||||
|
/deep/ .el-dialog__body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-number {
|
||||||
|
width: 100%;
|
||||||
|
/deep/ .el-input__inner {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-textarea {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
|
||||||
|
.preview-btn {
|
||||||
|
padding: 7px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
padding: 16px 20px 0;
|
||||||
|
text-align: right;
|
||||||
|
border-top: 1px solid #EBEEF5;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,13 +8,11 @@
|
|||||||
<img alt="" src="@/assets/home/equipment.png" style="width: 12px;height: 10px;"/>
|
<img alt="" src="@/assets/home/equipment.png" style="width: 12px;height: 10px;"/>
|
||||||
智能体管理
|
智能体管理
|
||||||
</div>
|
</div>
|
||||||
<div class="console">
|
<div class="equipment-management2" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||||
<i class="el-icon-s-grid" style="font-size: 10px;color: #979db1;"/>
|
用户管理
|
||||||
控制台
|
|
||||||
</div>
|
</div>
|
||||||
<div class="equipment-management2">
|
<div class="equipment-management2" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
|
||||||
设备管理
|
模型配置
|
||||||
<img alt="" src="@/assets/home/close.png" style="width: 6px;height: 6px;"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
|
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
|
||||||
@@ -64,6 +62,12 @@ export default {
|
|||||||
// 跳转到首页
|
// 跳转到首页
|
||||||
this.$router.push('/')
|
this.$router.push('/')
|
||||||
},
|
},
|
||||||
|
goUserManagement() {
|
||||||
|
this.$router.push('/user-management')
|
||||||
|
},
|
||||||
|
goModelConfig() {
|
||||||
|
this.$router.push('/model-config')
|
||||||
|
},
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
fetchUserInfo() {
|
fetchUserInfo() {
|
||||||
userApi.getUserInfo(({data}) => {
|
userApi.getUserInfo(({data}) => {
|
||||||
@@ -117,18 +121,24 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management2 {
|
.equipment-management2 {
|
||||||
width: 87px;
|
width: 82px;
|
||||||
height: 22px;
|
height: 24px;
|
||||||
border-radius: 11px;
|
border-radius: 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
color: #3d4566;
|
color: #3d4566;
|
||||||
margin-left: 2px;
|
margin-left: 1px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-management2.active-tab {
|
||||||
|
background: #5778ff !important;
|
||||||
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -168,28 +178,4 @@ export default {
|
|||||||
color: #3d4566;
|
color: #3d4566;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console {
|
|
||||||
width: 90px;
|
|
||||||
height: 22px;
|
|
||||||
border-radius: 11px;
|
|
||||||
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 9px;
|
|
||||||
color: #979db1;
|
|
||||||
font-weight: 400;
|
|
||||||
gap: 7px;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dropdown-link {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #5778ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon-arrow-down {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :visible.sync="dialogVisible" width="800px" center>
|
||||||
|
<el-form :model="form" ref="form" label-width="70px">
|
||||||
|
<el-row :gutter="20" class="form-row">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="模型编码">
|
||||||
|
<el-input v-model="form.code" placeholder="请输入内容" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="模型名称">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入内容" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 供应商 -->
|
||||||
|
<el-row :gutter="20" class="form-row">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="供应器">
|
||||||
|
<el-select v-model="form.supplier" placeholder="请选择">
|
||||||
|
<el-option label="openai" value="openai" />
|
||||||
|
<el-option label="dify" value="dify" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span class="el-form-item__label" style="width: 70px;">设成默认</span>
|
||||||
|
<el-switch v-model="form.isDefault" style="margin-right: 20px;" />
|
||||||
|
<span class="el-form-item__label" style="width: 70px;">是否启用</span>
|
||||||
|
<el-switch v-model="form.isEnable" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 文档 -->
|
||||||
|
<el-row :gutter="20" class="form-row">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="文档地址">
|
||||||
|
<el-input v-model="form.docUrl" placeholder="请输入内容" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="排序号">
|
||||||
|
<el-input-number v-model="form.sort" :min="1" :max="999" controls-position="right" placeholder="123"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 备注 -->
|
||||||
|
<el-form-item label="备注" class="form-row">
|
||||||
|
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="vertical-fields">
|
||||||
|
<el-form-item label="接口地址">
|
||||||
|
<el-input v-model="form.apiUrl" placeholder="请输入base_url" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模型名称">
|
||||||
|
<el-input v-model="form.modelName" placeholder="请输入model_name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密钥信息">
|
||||||
|
<el-input v-model="form.apiKey" placeholder="请输入api_key" show-password/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "ModelConfigDialog",
|
||||||
|
props: {
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
configData: { type: Object, default: () => ({}) }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogVisible: this.visible,
|
||||||
|
form: {
|
||||||
|
code: "",
|
||||||
|
name: "",
|
||||||
|
supplier: "",
|
||||||
|
isDefault: true,
|
||||||
|
isEnable: true,
|
||||||
|
docUrl: "",
|
||||||
|
sort: 123,
|
||||||
|
remark: "",
|
||||||
|
apiUrl: "",
|
||||||
|
modelName: "",
|
||||||
|
apiKey: ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dialogVisible(val) {
|
||||||
|
this.$emit('update:visible', val);
|
||||||
|
},
|
||||||
|
visible(val) {
|
||||||
|
this.dialogVisible = val;
|
||||||
|
if (val) this.form = { ...this.form, ...this.configData };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSave() {
|
||||||
|
this.$emit("submit", this.form);
|
||||||
|
this.dialogVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
margin-top: -30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.el-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.form-row {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-fields {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
margin-top: -10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.el-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :visible.sync="visible" width="80%" @close="handleClose">
|
||||||
|
<el-row class="main-container">
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
|
||||||
|
<el-menu-item index="EdgeTTS">EdgeTTS</el-menu-item>
|
||||||
|
<el-menu-item index="DoubaoTTS">DoubaoTTS</el-menu-item>
|
||||||
|
<el-menu-item index="TTS302AI">TTS302AI</el-menu-item>
|
||||||
|
<el-menu-item index="CosyVoiceSiliconflow">CosyVoiceSiliconflow</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="20">
|
||||||
|
<div class="search-operate">
|
||||||
|
<el-input placeholder="请输入音色名称查询" v-model="searchQuery" style="width: 300px; margin-right: 10px;"/>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button type="primary" plain @click="handleAddVoice">添加音色</el-button>
|
||||||
|
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="filteredTtsModels" style="width: 100%" border stripe header-row-class-name="table-header">
|
||||||
|
<el-table-column label="音色编码" prop="voiceCode" width="150" align="center"></el-table-column>
|
||||||
|
<el-table-column label="音色名称" prop="voiceName" width="180" align="center"></el-table-column>
|
||||||
|
<el-table-column label="语言类型" prop="languageType" width="120" align="center"></el-table-column>
|
||||||
|
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text" @click="editVoice(scope.row)" style="color: #409EFF; margin-right: 15px;">修改</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="deleteVoice(scope.row)" style="color: #F56C6C;">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination :current-page="currentPage" :page-size="pageSize" :total="total" layout="prev, pager, next" prev-text="<" next-text=">"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleClose" size="medium">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="handleImportExport" size="medium">导入导出配置</el-button>
|
||||||
|
</div>
|
||||||
|
<EditVoiceDialog :showDialog="editDialogVisible" :voiceData="editVoiceData" @update:showDialog="editDialogVisible = $event" @saveVoice="handleSaveEditedVoice"/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EditVoiceDialog from "@/components/EditVoiceDialog.vue";
|
||||||
|
export default {
|
||||||
|
components: { EditVoiceDialog },
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeModel: 'EdgeTTS',
|
||||||
|
searchQuery: '',
|
||||||
|
editDialogVisible: false,
|
||||||
|
editVoiceData: {},
|
||||||
|
ttsModels: [
|
||||||
|
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||||
|
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||||
|
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||||
|
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||||
|
],
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 4,
|
||||||
|
total: 20
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredTtsModels() {
|
||||||
|
return this.ttsModels.filter(model =>
|
||||||
|
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
},
|
||||||
|
handleModelSelect(index) {
|
||||||
|
this.activeModel = index;
|
||||||
|
// 可根据选中模型加载对应数据
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
// 搜索
|
||||||
|
},
|
||||||
|
handleAddVoice() {
|
||||||
|
// 添加音色
|
||||||
|
},
|
||||||
|
handleBatchDelete() {
|
||||||
|
// 批量删除
|
||||||
|
},
|
||||||
|
editVoice(voice) {
|
||||||
|
this.editVoiceData = { ...voice };
|
||||||
|
this.editDialogVisible = true;
|
||||||
|
},
|
||||||
|
handleSaveEditedVoice(voiceForm) {
|
||||||
|
const index = this.ttsModels.findIndex(item => item.voiceCode === voiceForm.voiceCode);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.ttsModels.splice(index, 1, voiceForm);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteVoice(voice) {
|
||||||
|
// 删除
|
||||||
|
},
|
||||||
|
handleImportExport() {
|
||||||
|
// 导入导出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-menu {
|
||||||
|
border-right: 1px solid #ebeef5;
|
||||||
|
height: calc(100vh - 300px);
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-operate {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
padding: 20px 20px 0;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .table-header th {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table--border td, ::v-deep .el-table--border th {
|
||||||
|
border-right: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-pagination {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-pagination .btn-prev, ::v-deep .el-pagination .btn-next {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-pagination .btn-prev:hover, ::v-deep .el-pagination .btn-next:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-pagination .el-pager li {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-pagination .el-pager li:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-pagination .el-pager li.active {
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #409EFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -39,15 +39,30 @@ const routes = [
|
|||||||
return import('../views/register.vue')
|
return import('../views/register.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 新增设备管理页面路由
|
// 设备管理页面路由
|
||||||
{
|
{
|
||||||
path: '/device-management',
|
path: '/device-management',
|
||||||
name: 'DeviceManagement',
|
name: 'DeviceManagement',
|
||||||
component: function () {
|
component: function () {
|
||||||
return import('../views/DeviceManagement.vue')
|
return import('../views/DeviceManagement.vue')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// 添加用户管理路由
|
||||||
|
{
|
||||||
|
path: '/user-management',
|
||||||
|
name: 'UserManagement',
|
||||||
|
component: function () {
|
||||||
|
return import('../views/UserManagement.vue')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/model-config',
|
||||||
|
name: 'ModelConfig',
|
||||||
|
component: function () {
|
||||||
|
return import('../views/ModelConfig.vue')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
<el-table :data="deviceList" style="width: 100%; margin-top: 20px" border stripe>
|
<el-table :data="deviceList" style="width: 100%; margin-top: 20px" border stripe>
|
||||||
<el-table-column label="设备型号" prop="model" flex></el-table-column>
|
<el-table-column label="设备型号" prop="model" flex></el-table-column>
|
||||||
<el-table-column label="固件版本" prop="firmwareVersion" width="140"></el-table-column>
|
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
|
||||||
<el-table-column label="Mac地址" prop="macAddress" width="220"></el-table-column>
|
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
|
||||||
<el-table-column label="绑定时间" prop="bindTime" width="260"></el-table-column>
|
<el-table-column label="绑定时间" prop="bindTime" width="200"></el-table-column>
|
||||||
<el-table-column label="最近对话" prop="lastConversation" width="100"></el-table-column>
|
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
|
||||||
<el-table-column label="备注" width="220">
|
<el-table-column label="备注" width="180">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini" @blur="stopEditRemark(scope.$index)"></el-input>
|
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini" @blur="stopEditRemark(scope.$index)"></el-input>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
@@ -85,7 +85,7 @@ export default {
|
|||||||
remark: '生产环境设备',
|
remark: '生产环境设备',
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
otaSwitch: false
|
otaSwitch: false
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome">
|
||||||
|
<HeaderBar />
|
||||||
|
<div class="model-tabs-container">
|
||||||
|
<el-tabs v-model="activeTab" class="model-tabs" >
|
||||||
|
<el-tab-pane label="语音活动检测模型(VAD)" name="vad"></el-tab-pane>
|
||||||
|
<el-tab-pane label="语音识别(ASR)" name="asr"></el-tab-pane>
|
||||||
|
<el-tab-pane label="大语言模型(LLM)" name="llm"></el-tab-pane>
|
||||||
|
<el-tab-pane label="意图识别模型(Intent)" name="intent"></el-tab-pane>
|
||||||
|
<el-tab-pane label="语音合成模型(TTS)" name="tts"></el-tab-pane>
|
||||||
|
<el-tab-pane label="记忆模型(Memory)" name="memory"></el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<div class="import-export-btn">
|
||||||
|
<el-button v-if="activeTab === 'tts'" type="primary" @click="ttsDialogVisible = true" style="margin-right: 10px;">模型设置</el-button>
|
||||||
|
<el-button size="small" @click="handleImportExport">导入导出配置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
||||||
|
|
||||||
|
<el-card class="model-card" shadow="always">
|
||||||
|
<div class="model-search-operate" style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||||
|
<el-input placeholder="请输入模型名称查询" v-model="search" style="width: 300px; margin-right: 10px" />
|
||||||
|
<el-button @click="handleSearch">查询</el-button>
|
||||||
|
<el-button type="primary" @click="addModel">增加模型</el-button>
|
||||||
|
<el-button type="danger" @click="batchDelete">批量删除</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="modelList" style="width: 100%;" border stripe>
|
||||||
|
<el-table-column label="模型编码" prop="code"></el-table-column>
|
||||||
|
<el-table-column label="模型名称" prop="candidateName"></el-table-column>
|
||||||
|
<el-table-column label="是否应用">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="scope.row.isApplied ? 'success' : 'danger'">
|
||||||
|
{{ scope.row.isApplied ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="供应商名称" prop="supplier"></el-table-column>
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" @click="editModel(scope.row)" style="margin-right: 10px;">修改</el-button>
|
||||||
|
<el-button size="mini" type="danger" @click="deleteModel(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-sizes="[5, 10, 15]"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
|
||||||
|
©2025 xiaozhi-esp32-server
|
||||||
|
</div>
|
||||||
|
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
|
||||||
|
<TtsModel :visible.sync="ttsDialogVisible" />
|
||||||
|
</el-main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import ModelEditDialog from "@/components/ModelEditDialog.vue";
|
||||||
|
import TtsModel from "@/components/TtsModel.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { HeaderBar, ModelEditDialog, TtsModel },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: 'vad',
|
||||||
|
search: '',
|
||||||
|
editDialogVisible: false,
|
||||||
|
editModelData: {},
|
||||||
|
ttsDialogVisible: false,
|
||||||
|
modelList: [
|
||||||
|
{ code: 'AILLM', candidateName: '阿里百炼', isApplied: true, supplier: 'openai' },
|
||||||
|
{ code: 'DoubaoLLM', candidateName: '豆包大模型', isApplied: true, supplier: 'openai' },
|
||||||
|
{ code: 'DeepSeekLLM', candidateName: '深度求索', isApplied: true, supplier: 'openai' },
|
||||||
|
{ code: 'DifyLLM', candidateName: 'DifChat', isApplied: true, supplier: 'dify' },
|
||||||
|
],
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 4,
|
||||||
|
total: 20
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 查询
|
||||||
|
handleSearch() {
|
||||||
|
console.log('查询:', this.search);
|
||||||
|
},
|
||||||
|
// 批量删除
|
||||||
|
batchDelete() {
|
||||||
|
console.log('批量删除');
|
||||||
|
},
|
||||||
|
// 增加
|
||||||
|
addModel() {
|
||||||
|
console.log('增加模型');
|
||||||
|
},
|
||||||
|
// 修改
|
||||||
|
editModel(model) {
|
||||||
|
this.editModelData = {
|
||||||
|
code: model.code,
|
||||||
|
name: model.candidateName,
|
||||||
|
supplier: model.supplier,
|
||||||
|
};
|
||||||
|
this.editDialogVisible = true;
|
||||||
|
},
|
||||||
|
// 删除
|
||||||
|
deleteModel(model) {
|
||||||
|
console.log('删除:', model);
|
||||||
|
},
|
||||||
|
handleCurrentChange(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
console.log('当前页码:', page);
|
||||||
|
},
|
||||||
|
// 导入导出
|
||||||
|
handleImportExport() {
|
||||||
|
console.log('导入导出');
|
||||||
|
},
|
||||||
|
handleModelSave(formData) {
|
||||||
|
// 处理保存
|
||||||
|
console.log('保存的模型数据:', formData);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.welcome {
|
||||||
|
min-width: 900px;
|
||||||
|
min-height: 506px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-image: url("@/assets/home/background.png");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
-o-background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-search-operate {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-search-operate > * {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header th {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-tabs-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-tabs {
|
||||||
|
flex-grow: 0;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-export-btn {
|
||||||
|
margin-right: 40px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-tabs {
|
||||||
|
margin-left: 70px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome">
|
||||||
|
<HeaderBar />
|
||||||
|
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
||||||
|
<el-card class="user-card" shadow="always" >
|
||||||
|
<div class="user-search-operate" style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||||
|
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" style="width: 300px; margin-right: 10px" />
|
||||||
|
<el-button @click="handleSearch">查询</el-button>
|
||||||
|
<el-button type="danger" @click="batchDelete">批量删除</el-button>
|
||||||
|
<el-button type="danger" @click="batchDisable">批量禁用</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="userList" style="width: 100%;" border stripe>
|
||||||
|
<el-table-column label="用户Id" prop="userId"></el-table-column>
|
||||||
|
<el-table-column label="手机号码" prop="phone"></el-table-column>
|
||||||
|
<el-table-column label="Status" prop="status"></el-table-column>
|
||||||
|
<el-table-column label="设备数量" prop="deviceCount"></el-table-column>
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" @click="resetPassword(scope.row)">重置密码</el-button>
|
||||||
|
<el-button size="mini"
|
||||||
|
v-if="scope.row.status === '正常'"
|
||||||
|
@click="disableUser(scope.row)">禁用</el-button>
|
||||||
|
<el-button size="mini"
|
||||||
|
v-if="scope.row.status === '禁用'"
|
||||||
|
@click="restoreUser(scope.row)">恢复</el-button>
|
||||||
|
<el-button size="mini" @click="deleteUser(scope.row)" style="color: #ff4949">删除用户</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-sizes="[5, 10, 15]"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
|
||||||
|
©2025 xiaozhi-esp32-server
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { HeaderBar },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchPhone: '',
|
||||||
|
userList: [
|
||||||
|
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 10 },
|
||||||
|
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 9 },
|
||||||
|
{ userId: '123456', phone: '13800138000', status: '正常', deviceCount: 7 },
|
||||||
|
{ userId: '123456', phone: '13800138000', status: '禁用', deviceCount: 7 }
|
||||||
|
],
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 4,
|
||||||
|
total: 20
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSearch() {
|
||||||
|
// 模拟搜索逻辑
|
||||||
|
console.log('执行查询,搜索号码:', this.searchPhone);
|
||||||
|
},
|
||||||
|
batchDelete() {
|
||||||
|
console.log('执行批量删除操作');
|
||||||
|
},
|
||||||
|
batchDisable() {
|
||||||
|
console.log('执行批量禁用操作');
|
||||||
|
},
|
||||||
|
resetPassword(row) {
|
||||||
|
console.log('重置用户密码,用户:', row);
|
||||||
|
},
|
||||||
|
disableUser(row) {
|
||||||
|
row.status = '禁用';
|
||||||
|
console.log('禁用用户:', row);
|
||||||
|
},
|
||||||
|
restoreUser(row) {
|
||||||
|
row.status = '正常';
|
||||||
|
console.log('恢复用户:', row);
|
||||||
|
},
|
||||||
|
deleteUser(row) {
|
||||||
|
console.log('删除用户:', row);
|
||||||
|
},
|
||||||
|
handleCurrentChange(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
console.log('当前页码:', page);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.welcome {
|
||||||
|
min-width: 900px;
|
||||||
|
min-height: 506px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-image: url("@/assets/home/background.png");
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
-o-background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search-operate {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search-operate > * {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header th {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -125,11 +125,11 @@ export default {
|
|||||||
],
|
],
|
||||||
models: [
|
models: [
|
||||||
{ label: '大语言模型(LLM)', key: 'llm' },
|
{ label: '大语言模型(LLM)', key: 'llm' },
|
||||||
{ label: '语音转文本模型(ASR)', key: 'asr' },
|
{ label: '语音识别(ASR)', key: 'asr' },
|
||||||
{ label: '语音活动检测模型(VAD)', key: 'vad' },
|
{ label: '语音活动检测模型(VAD)', key: 'vad' },
|
||||||
{ label: '语音生成模型(TTS)', key: 'tts' },
|
{ label: '语音合成模型(TTS)', key: 'tts' },
|
||||||
{ label: '意图分类模型(Intent)', key: 'intent' },
|
{ label: '意图识别模型(Intent)', key: 'intent' },
|
||||||
{ label: '记忆增强模型(Memory)', key: 'memory' }
|
{ label: '记忆模型(Memory)', key: 'memory' }
|
||||||
],
|
],
|
||||||
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长']
|
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长']
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ prompt: |
|
|||||||
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||||
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
||||||
现在我正在和你进行语音聊天,我们开始吧。
|
现在我正在和你进行语音聊天,我们开始吧。
|
||||||
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
|
|
||||||
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
||||||
delete_audio: true
|
delete_audio: true
|
||||||
|
|
||||||
@@ -99,6 +99,12 @@ Intent:
|
|||||||
- change_role
|
- change_role
|
||||||
- get_weather
|
- get_weather
|
||||||
- get_news
|
- get_news
|
||||||
|
# play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放
|
||||||
|
# 如果用了hass_play_music,就不要开启play_music,两者只留一个
|
||||||
|
- play_music
|
||||||
|
#- hass_get_state
|
||||||
|
#- hass_set_state
|
||||||
|
#- hass_play_music
|
||||||
|
|
||||||
# 插件的基础配置
|
# 插件的基础配置
|
||||||
plugins:
|
plugins:
|
||||||
@@ -115,6 +121,20 @@ plugins:
|
|||||||
society: "https://www.chinanews.com.cn/rss/society.xml"
|
society: "https://www.chinanews.com.cn/rss/society.xml"
|
||||||
world: "https://www.chinanews.com.cn/rss/world.xml"
|
world: "https://www.chinanews.com.cn/rss/world.xml"
|
||||||
finance: "https://www.chinanews.com.cn/rss/finance.xml"
|
finance: "https://www.chinanews.com.cn/rss/finance.xml"
|
||||||
|
home_assistant:
|
||||||
|
devices:
|
||||||
|
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
|
||||||
|
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
|
||||||
|
base_url: http://homeassistant.local:8123
|
||||||
|
api_key: 你的home assistant api访问令牌
|
||||||
|
play_music:
|
||||||
|
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
|
||||||
|
music_ext: # 音乐文件类型,p3格式效率最高
|
||||||
|
- ".mp3"
|
||||||
|
- ".wav"
|
||||||
|
- ".p3"
|
||||||
|
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
|
||||||
|
|
||||||
|
|
||||||
Memory:
|
Memory:
|
||||||
mem0ai:
|
mem0ai:
|
||||||
@@ -165,6 +185,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
|
||||||
@@ -229,12 +261,6 @@ LLM:
|
|||||||
model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载
|
model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载
|
||||||
url: http://localhost:1234/v1 # LM Studio服务地址
|
url: http://localhost:1234/v1 # LM Studio服务地址
|
||||||
api_key: lm-studio # LM Studio服务的固定API Key
|
api_key: lm-studio # LM Studio服务的固定API Key
|
||||||
HomeAssistant:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: homeassistant
|
|
||||||
base_url: http://homeassistant.local:8123
|
|
||||||
agent_id: conversation.chatgpt
|
|
||||||
api_key: 你的home assistant api访问令牌
|
|
||||||
FastgptLLM:
|
FastgptLLM:
|
||||||
# 定义LLM API类型
|
# 定义LLM API类型
|
||||||
type: fastgpt
|
type: fastgpt
|
||||||
@@ -488,20 +514,3 @@ module_test:
|
|||||||
- "你好,请介绍一下你自己"
|
- "你好,请介绍一下你自己"
|
||||||
- "What's the weather like today?"
|
- "What's the weather like today?"
|
||||||
- "请用100字概括量子计算的基本原理和应用前景"
|
- "请用100字概括量子计算的基本原理和应用前景"
|
||||||
|
|
||||||
# 本地音乐播放配置
|
|
||||||
music:
|
|
||||||
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
|
|
||||||
music_ext: # 音乐文件类型,p3格式效率最高
|
|
||||||
- ".mp3"
|
|
||||||
- ".wav"
|
|
||||||
- ".p3"
|
|
||||||
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
|
|
||||||
|
|
||||||
# 以下配置在小于等于0.0.9版本中的docker容器中可用
|
|
||||||
# 0.0.9以后的新版本源码部署已经无法奏效
|
|
||||||
manager:
|
|
||||||
enabled: false
|
|
||||||
ip: 0.0.0.0
|
|
||||||
port: 8002
|
|
||||||
use_private_config: false
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class ConnectionHandler:
|
|||||||
self.prompt = self.prompt + f"\n我在:{self.client_ip_info}"
|
self.prompt = self.prompt + f"\n我在:{self.client_ip_info}"
|
||||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
self.dialogue.put(Message(role="system", content=self.prompt))
|
||||||
|
|
||||||
self.func_handler = FunctionHandler(self.config)
|
self.func_handler = FunctionHandler(self)
|
||||||
|
|
||||||
def change_system_prompt(self, prompt):
|
def change_system_prompt(self, prompt):
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import asyncio
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import json
|
import json
|
||||||
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
|
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
|
||||||
|
from plugins_func.functions.hass_init import append_devices_to_prompt
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class FunctionHandler:
|
class FunctionHandler:
|
||||||
def __init__(self, config):
|
def __init__(self, conn):
|
||||||
self.config = config
|
self.conn = conn
|
||||||
|
self.config = conn.config
|
||||||
self.function_registry = FunctionRegistry()
|
self.function_registry = FunctionRegistry()
|
||||||
self.register_nessary_functions()
|
self.register_nessary_functions()
|
||||||
self.register_config_functions()
|
self.register_config_functions()
|
||||||
@@ -26,9 +26,10 @@ class FunctionHandler:
|
|||||||
func_names = ",".join(surport_plugins)
|
func_names = ",".join(surport_plugins)
|
||||||
for function_desc in self.functions_desc:
|
for function_desc in self.functions_desc:
|
||||||
if function_desc["function"]["name"] == "plugin_loader":
|
if function_desc["function"]["name"] == "plugin_loader":
|
||||||
function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names)
|
function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]",
|
||||||
|
func_names)
|
||||||
break
|
break
|
||||||
|
|
||||||
def upload_functions_desc(self):
|
def upload_functions_desc(self):
|
||||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||||
|
|
||||||
@@ -47,7 +48,6 @@ class FunctionHandler:
|
|||||||
def register_nessary_functions(self):
|
def register_nessary_functions(self):
|
||||||
"""注册必要的函数"""
|
"""注册必要的函数"""
|
||||||
self.function_registry.register_function("handle_exit_intent")
|
self.function_registry.register_function("handle_exit_intent")
|
||||||
self.function_registry.register_function("play_music")
|
|
||||||
self.function_registry.register_function("plugin_loader")
|
self.function_registry.register_function("plugin_loader")
|
||||||
self.function_registry.register_function("get_time")
|
self.function_registry.register_function("get_time")
|
||||||
self.function_registry.register_function("raise_and_lower_the_volume")
|
self.function_registry.register_function("raise_and_lower_the_volume")
|
||||||
@@ -57,6 +57,9 @@ class FunctionHandler:
|
|||||||
for func in self.config["Intent"]["function_call"].get("functions", []):
|
for func in self.config["Intent"]["function_call"].get("functions", []):
|
||||||
self.function_registry.register_function(func)
|
self.function_registry.register_function(func)
|
||||||
|
|
||||||
|
"""home assistant需要初始化提示词"""
|
||||||
|
append_devices_to_prompt(self.conn)
|
||||||
|
|
||||||
def get_function(self, name):
|
def get_function(self, name):
|
||||||
return self.function_registry.get_function(name)
|
return self.function_registry.get_function(name)
|
||||||
|
|
||||||
@@ -81,4 +84,4 @@ class FunctionHandler:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -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服务响应异常】"
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import requests
|
|
||||||
from requests.exceptions import RequestException
|
|
||||||
from config.logger import setup_logging
|
|
||||||
from core.providers.llm.base import LLMProviderBase
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
|
||||||
def __init__(self, config):
|
|
||||||
self.agent_id = config.get("agent_id") # 对应 agent_id
|
|
||||||
self.api_key = config.get("api_key")
|
|
||||||
self.base_url = config.get("base_url", config.get("url")) # 默认使用 base_url
|
|
||||||
self.api_url = f"{self.base_url}/api/conversation/process" # 拼接完整的 API URL
|
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
print(dialogue)
|
|
||||||
try:
|
|
||||||
# home assistant语音助手自带意图,无需使用xiaozhi ai自带的,只需要把用户说的话传递给home assistant即可
|
|
||||||
|
|
||||||
# 提取最后一个 role 为 'user' 的 content
|
|
||||||
input_text = None
|
|
||||||
if isinstance(dialogue, list): # 确保 dialogue 是一个列表
|
|
||||||
# 逆序遍历,找到最后一个 role 为 'user' 的消息
|
|
||||||
for message in reversed(dialogue):
|
|
||||||
if message.get("role") == "user": # 找到 role 为 'user' 的消息
|
|
||||||
input_text = message.get("content", "")
|
|
||||||
break # 找到后立即退出循环
|
|
||||||
|
|
||||||
# 构造请求数据
|
|
||||||
payload = {
|
|
||||||
"text": input_text,
|
|
||||||
"agent_id": self.agent_id,
|
|
||||||
"conversation_id": session_id # 使用 session_id 作为 conversation_id
|
|
||||||
}
|
|
||||||
# 设置请求头
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 发起 POST 请求
|
|
||||||
response = requests.post(self.api_url, json=payload, headers=headers)
|
|
||||||
|
|
||||||
# 检查请求是否成功
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
# 解析返回数据
|
|
||||||
data = response.json()
|
|
||||||
speech = data.get("response", {}).get("speech", {}).get("plain", {}).get("speech", "")
|
|
||||||
|
|
||||||
# 返回生成的内容
|
|
||||||
if speech:
|
|
||||||
yield speech
|
|
||||||
else:
|
|
||||||
logger.bind(tag=TAG).warning("API 返回数据中没有 speech 内容")
|
|
||||||
|
|
||||||
except RequestException as e:
|
|
||||||
logger.bind(tag=TAG).error(f"HTTP 请求错误: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"生成响应时出错: {e}")
|
|
||||||
@@ -35,6 +35,15 @@ class Dialogue:
|
|||||||
self.getMessages(m, dialogue)
|
self.getMessages(m, dialogue)
|
||||||
return dialogue
|
return dialogue
|
||||||
|
|
||||||
|
def update_system_message(self, new_content: str):
|
||||||
|
"""更新或添加系统消息"""
|
||||||
|
# 查找第一个系统消息
|
||||||
|
system_msg = next((msg for msg in self.dialogue if msg.role == "system"), None)
|
||||||
|
if system_msg:
|
||||||
|
system_msg.content = new_content
|
||||||
|
else:
|
||||||
|
self.put(Message(role="system", content=new_content))
|
||||||
|
|
||||||
def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]:
|
def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]:
|
||||||
if memory_str is None or len(memory_str) == 0:
|
if memory_str is None or len(memory_str) == 0:
|
||||||
return self.get_llm_dialogue()
|
return self.get_llm_dialogue()
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||||
|
from config.logger import setup_logging
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
hass_get_state_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "hass_get_state",
|
||||||
|
"description": "获取homeassistant里设备的状态,包括灯光亮度,媒体播放器的音量,设备的暂停、继续操作",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entity_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要操作的设备id,homeassistant里的entity_id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["entity_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def hass_get_state(conn, entity_id=''):
|
||||||
|
try:
|
||||||
|
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
handle_hass_get_state(conn, entity_id),
|
||||||
|
conn.loop
|
||||||
|
)
|
||||||
|
ha_response = future.result()
|
||||||
|
return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_hass_get_state(conn, entity_id):
|
||||||
|
HASS_CACHE = initialize_hass_handler(conn)
|
||||||
|
api_key = HASS_CACHE['api_key']
|
||||||
|
base_url = HASS_CACHE['base_url']
|
||||||
|
url = f"{base_url}/api/states/{entity_id}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()['state']
|
||||||
|
else:
|
||||||
|
return f"切换失败,错误码: {response.status_code}"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from config.logger import setup_logging
|
||||||
|
from core.utils.util import check_model_key
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
HASS_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
|
def append_devices_to_prompt(conn):
|
||||||
|
if conn.use_function_call_mode:
|
||||||
|
funcs = conn.config["Intent"]["function_call"].get("functions", [])
|
||||||
|
if "hass_get_state" in funcs or "hass_get_state" in funcs:
|
||||||
|
prompt = "下面是我家智能设备,可以通过homeassistant控制\n"
|
||||||
|
devices = conn.config["plugins"]["home_assistant"].get("devices", [])
|
||||||
|
if len(devices) == 0:
|
||||||
|
return
|
||||||
|
for device in devices:
|
||||||
|
prompt += device + "\n"
|
||||||
|
conn.prompt += prompt
|
||||||
|
# 更新提示词
|
||||||
|
conn.dialogue.update_system_message(conn.prompt)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_hass_handler(conn):
|
||||||
|
global HASS_CACHE
|
||||||
|
if HASS_CACHE == {}:
|
||||||
|
if conn.use_function_call_mode:
|
||||||
|
funcs = conn.config["Intent"]["function_call"].get("functions", [])
|
||||||
|
if "hass_get_state" in funcs or "hass_get_state" in funcs:
|
||||||
|
HASS_CACHE['base_url'] = conn.config["plugins"]["home_assistant"].get("base_url")
|
||||||
|
HASS_CACHE['api_key'] = conn.config["plugins"]["home_assistant"].get("api_key")
|
||||||
|
|
||||||
|
check_model_key("home_assistant", HASS_CACHE['api_key'])
|
||||||
|
return HASS_CACHE
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||||
|
from config.logger import setup_logging
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
hass_play_music_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "hass_play_music",
|
||||||
|
"description": "用户想听音乐、有声书的时候使用,在房间的媒体播放器(media_player)里播放对应音频",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"media_content_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random"
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["media_content_id", "entity_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('hass_play_music', hass_play_music_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def hass_play_music(conn, entity_id='', media_content_id='random'):
|
||||||
|
try:
|
||||||
|
# 执行音乐播放命令
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
handle_hass_play_music(conn, entity_id, media_content_id),
|
||||||
|
conn.loop
|
||||||
|
)
|
||||||
|
ha_response = future.result()
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=ha_response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_hass_play_music(conn, entity_id, media_content_id):
|
||||||
|
HASS_CACHE = initialize_hass_handler(conn)
|
||||||
|
api_key = HASS_CACHE['api_key']
|
||||||
|
base_url = HASS_CACHE['base_url']
|
||||||
|
url = f"{base_url}/api/services/music_assistant/play_media"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"entity_id": entity_id,
|
||||||
|
"media_id": media_content_id
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return f"正在播放{media_content_id}的音乐"
|
||||||
|
else:
|
||||||
|
return f"音乐播放失败,错误码: {response.status_code}"
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||||
|
from config.logger import setup_logging
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
hass_set_state_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "hass_set_state",
|
||||||
|
"description": "设置homeassistant里设备的状态,包括开、关,调整灯光亮度,调整播放器的音量,设备的暂停、继续、静音操作",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加>音量:,volume_up降低音量:volume_down,设置音量:volume_set,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute"
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"type": "int",
|
||||||
|
"description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%"
|
||||||
|
},
|
||||||
|
"is_muted": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type"]
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要操作的设备id,homeassistant里的entity_id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["state", "entity_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('hass_set_state', hass_set_state_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def hass_set_state(conn, entity_id='', state={}):
|
||||||
|
try:
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
handle_hass_set_state(conn, entity_id, state),
|
||||||
|
conn.loop
|
||||||
|
)
|
||||||
|
ha_response = future.result()
|
||||||
|
return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_hass_set_state(conn, entity_id, state):
|
||||||
|
HASS_CACHE = initialize_hass_handler(conn)
|
||||||
|
api_key = HASS_CACHE['api_key']
|
||||||
|
base_url = HASS_CACHE['base_url']
|
||||||
|
'''
|
||||||
|
state = { "type":"brightness_up","input":"80","is_muted":"true"}
|
||||||
|
'''
|
||||||
|
domains = entity_id.split(".")
|
||||||
|
if len(domains) > 1:
|
||||||
|
domain = domains[0]
|
||||||
|
else:
|
||||||
|
return "执行失败,错误的设备id"
|
||||||
|
action = ''
|
||||||
|
arg = ''
|
||||||
|
value = ''
|
||||||
|
if state['type'] == 'turn_on':
|
||||||
|
description = "设备已打开"
|
||||||
|
if domain == "cover":
|
||||||
|
action = "open_cover"
|
||||||
|
elif domain == "vacuum":
|
||||||
|
action = "start"
|
||||||
|
else:
|
||||||
|
action = "turn_on"
|
||||||
|
elif state['type'] == 'turn_off':
|
||||||
|
description = "设备已关闭"
|
||||||
|
if domain == 'cover':
|
||||||
|
action = "close_cover"
|
||||||
|
elif domain == 'vacuum':
|
||||||
|
action = "stop"
|
||||||
|
else:
|
||||||
|
action = "turn_off"
|
||||||
|
elif state['type'] == 'brightness_up':
|
||||||
|
description = "灯光已调亮"
|
||||||
|
action = 'turn_on'
|
||||||
|
arg = 'brightness_step_pct'
|
||||||
|
value = 10
|
||||||
|
elif state['type'] == 'brightness_down':
|
||||||
|
description = "灯光已调暗"
|
||||||
|
action = 'turn_on'
|
||||||
|
arg = 'brightness_step_pct'
|
||||||
|
value = -10
|
||||||
|
elif state['type'] == 'brightness_value':
|
||||||
|
description = f"亮度已调整到{state['input']}"
|
||||||
|
action = 'turn_on'
|
||||||
|
arg = 'brightness_pct'
|
||||||
|
value = state['input']
|
||||||
|
elif state['type'] == 'volume_up':
|
||||||
|
description = "音量已调大"
|
||||||
|
action = state['type']
|
||||||
|
elif state['type'] == 'volume_down':
|
||||||
|
description = "音量已调小"
|
||||||
|
action = state['type']
|
||||||
|
elif state['type'] == 'volume_set':
|
||||||
|
description = f"音量已调整到{state['input']}"
|
||||||
|
action = state['type']
|
||||||
|
arg = 'volume_level'
|
||||||
|
value = state['input']
|
||||||
|
elif state['type'] == 'volume_mute':
|
||||||
|
description = f"设备已静音"
|
||||||
|
action = state['type']
|
||||||
|
arg = 'is_volume_muted'
|
||||||
|
value = state['is_muted']
|
||||||
|
elif state['type'] == 'pause':
|
||||||
|
description = f"设备已暂停"
|
||||||
|
action = state['type']
|
||||||
|
if domain == 'media_player':
|
||||||
|
action = 'media_pause'
|
||||||
|
if domain == 'cover':
|
||||||
|
action = 'stop_cover'
|
||||||
|
if domain == 'vacuum':
|
||||||
|
action = 'pause'
|
||||||
|
elif state['type'] == 'continue':
|
||||||
|
description = f"设备已继续"
|
||||||
|
if domain == 'media_player':
|
||||||
|
action = 'media_play'
|
||||||
|
if domain == 'vacuum':
|
||||||
|
action = 'start'
|
||||||
|
else:
|
||||||
|
return f"{domain} {state.type}功能尚未支持"
|
||||||
|
|
||||||
|
if arg == '':
|
||||||
|
data = {
|
||||||
|
"entity_id": entity_id,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
"entity_id": entity_id,
|
||||||
|
arg: value
|
||||||
|
}
|
||||||
|
url = f"{base_url}/api/services/{domain}/{action}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=data)
|
||||||
|
logger.bind(tag=TAG).info(f"设置状态:url:{url},return_code:{response.status_code}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return description
|
||||||
|
else:
|
||||||
|
return f"设置失败,错误码: {response.status_code}"
|
||||||
@@ -21,13 +21,13 @@ play_music_function_desc = {
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "play_music",
|
"name": "play_music",
|
||||||
"description": "唱歌、听歌、播放音乐方法。比如用户说播放音乐,参数为:random,比如用户说播放两只老虎,参数为:两只老虎",
|
"description": "唱歌、听歌、播放音乐的方法。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"song_name": {
|
"song_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "歌曲名称,如果没有指定具体歌名则为'random'"
|
"description": "歌曲名称,如果用户没有指定具体歌名则为'random', 明确指定的时返回音乐的名字 示例: ```用户:播放两只老虎\n参数:两只老虎``` ```用户:播放音乐 \n参数:random ```"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["song_name"]
|
"required": ["song_name"]
|
||||||
@@ -112,8 +112,8 @@ def get_music_files(music_dir, music_ext):
|
|||||||
def initialize_music_handler(conn):
|
def initialize_music_handler(conn):
|
||||||
global MUSIC_CACHE
|
global MUSIC_CACHE
|
||||||
if MUSIC_CACHE == {}:
|
if MUSIC_CACHE == {}:
|
||||||
if "music" in conn.config:
|
if "play_music" in conn.config["plugins"]:
|
||||||
MUSIC_CACHE["music_config"] = conn.config["music"]
|
MUSIC_CACHE["music_config"] = conn.config["plugins"]["play_music"]
|
||||||
MUSIC_CACHE["music_dir"] = os.path.abspath(
|
MUSIC_CACHE["music_dir"] = os.path.abspath(
|
||||||
MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
|
MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user