Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
497def8055 | ||
|
|
62be26ff47 | ||
|
|
6a81d80778 | ||
|
|
60c38a8dea | ||
|
|
4e0934debf | ||
|
|
17126303fa | ||
|
|
98fa95190c | ||
|
|
3a2261107c | ||
|
|
b1b64abed9 | ||
|
|
05356b6652 | ||
|
|
cbd0c6615d | ||
|
|
90e389efa2 | ||
|
|
00a0b26797 | ||
|
|
69b5ffe3bf | ||
|
|
31539294eb | ||
|
|
d1a2307134 | ||
|
|
97b777f1b2 | ||
|
|
a63137abc9 | ||
|
|
3ca06f8d50 | ||
|
|
e5b0e202be | ||
|
|
c66385d1f7 | ||
|
|
b0304fd875 | ||
|
|
5bfc3efc9c | ||
|
|
62845cfcf1 | ||
|
|
5c536e72df | ||
|
|
7cfece1592 | ||
|
|
f393b2c9ba | ||
|
|
164f2e5472 | ||
|
|
1434ecbc2e | ||
|
|
c2e443d4ac | ||
|
|
c079125458 | ||
|
|
4c1d7b34b9 | ||
|
|
53772b3fe3 | ||
|
|
28d3c40835 | ||
|
|
a01faecd0c | ||
|
|
a1b071e38e | ||
|
|
571622a0f4 | ||
|
|
4ab84d7209 | ||
|
|
30a938e348 | ||
|
|
f6f4384e42 | ||
|
|
911b11211e | ||
|
|
8bb596c74f | ||
|
|
d0fdeb33e0 | ||
|
|
ae7f41032e | ||
|
|
f30cb6badb | ||
|
|
97a9c1e08a | ||
|
|
1e699c027d | ||
|
|
9bd731729d | ||
|
|
dc17eccf76 | ||
|
|
3649a33afb | ||
|
|
d76bcc1366 | ||
|
|
2987f1c680 | ||
|
|
ab93ee0406 | ||
|
|
69dd933a7d | ||
|
|
5440de0453 | ||
|
|
f2e130c70e | ||
|
|
4c845c26f7 | ||
|
|
a43acca375 | ||
|
|
635520aa85 | ||
|
|
fce9759e93 | ||
|
|
18243cd4b0 | ||
|
|
75a6e51e68 | ||
|
|
312edd4b15 | ||
|
|
be3e67b2c1 | ||
|
|
a5765f81fe | ||
|
|
4bee92abb0 | ||
|
|
d9dcb90745 | ||
|
|
fbd25353d5 | ||
|
|
44c3e73577 | ||
|
|
9cbad1a448 | ||
|
|
3a3ddbc2bc | ||
|
|
63a3c33df9 | ||
|
|
32d7400f7d | ||
|
|
38191f976b | ||
|
|
c380e0e110 | ||
|
|
2199b8eca7 | ||
|
|
88455604a2 | ||
|
|
63eb69091f | ||
|
|
fc668810bd | ||
|
|
2773e3a48a | ||
|
|
18a03f6ccc | ||
|
|
e9348f5512 | ||
|
|
ce65226ea4 | ||
|
|
a3b3079b63 | ||
|
|
22de4bb13b | ||
|
|
eef7bb0cad | ||
|
|
8f1fbbdeef | ||
|
|
f3562e091f | ||
|
|
2001258ef4 | ||
|
|
52a66ea304 | ||
|
|
21ddb1541f | ||
|
|
a3ec6d79b0 | ||
|
|
bb8c315ffc | ||
|
|
f2ae398d3f | ||
|
|
02e33f68b4 | ||
|
|
8c4c9d88da | ||
|
|
49cb98a606 | ||
|
|
32ab716f0d | ||
|
|
fe24529fce | ||
|
|
b59c097bca | ||
|
|
6d6414eb62 | ||
|
|
5b1c20d625 | ||
|
|
af6c8ab935 | ||
|
|
4acb5f924a | ||
|
|
b1b99f1066 | ||
|
|
004ee6f143 | ||
|
|
19455f89d3 | ||
|
|
18d98e3bd0 | ||
|
|
af038b7fc8 | ||
|
|
4a9297dc65 | ||
|
|
49fefb41a6 | ||
|
|
2fa64dfdd0 | ||
|
|
e0f777b9b4 | ||
|
|
dfe3de14d8 | ||
|
|
abe234c7ce | ||
|
|
d7bf67fd0e | ||
|
|
8218996f04 | ||
|
|
ed91e5d607 | ||
|
|
0bf0b0d381 | ||
|
|
d88559f5de | ||
|
|
f398115df3 | ||
|
|
82413fecab | ||
|
|
d5a5b24223 | ||
|
|
472dba2665 | ||
|
|
a5444b11b3 | ||
|
|
5a05a68524 | ||
|
|
36be61bbe1 | ||
|
|
ec6ab983f7 | ||
|
|
69630c2be0 | ||
|
|
8e605dd1d4 | ||
|
|
8bb0cf02ad | ||
|
|
60c054f457 | ||
|
|
5648ee974a | ||
|
|
dc4968f961 | ||
|
|
36d1370720 | ||
|
|
7ead5537f6 | ||
|
|
7fe797d44d | ||
|
|
a1e959420d | ||
|
|
4c99b40257 | ||
|
|
ead0958d18 | ||
|
|
220af8068e | ||
|
|
a1730b2d52 | ||
|
|
1a1d6c0835 | ||
|
|
06cbffdea4 | ||
|
|
c45e1faf42 | ||
|
|
732d42f174 | ||
|
|
524fde99f8 | ||
|
|
649246dc6a | ||
|
|
6c7ac25388 | ||
|
|
cb1d6ddeec | ||
|
|
58313889ee | ||
|
|
5ebc62a084 | ||
|
|
fcba384a15 | ||
|
|
3ea95ffa48 | ||
|
|
898fd36eca | ||
|
|
6cd88fb98f | ||
|
|
1ad93ce1fd | ||
|
|
2041513676 | ||
|
|
3ea1a2bf2f | ||
|
|
2b81ebca8e | ||
|
|
de9809ca5a | ||
|
|
8504c181c0 | ||
|
|
19fe2d2f03 |
@@ -145,6 +145,7 @@ tmp
|
|||||||
.history
|
.history
|
||||||
.DS_Store
|
.DS_Store
|
||||||
main/xiaozhi-server/data
|
main/xiaozhi-server/data
|
||||||
|
main/xiaozhi-server/config/assets/wakeup_words.*
|
||||||
main/manager-web/node_modules
|
main/manager-web/node_modules
|
||||||
.config.yaml
|
.config.yaml
|
||||||
.secrets.yaml
|
.secrets.yaml
|
||||||
@@ -154,3 +155,4 @@ main/manager-web/node_modules
|
|||||||
# model files
|
# model files
|
||||||
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||||
main/xiaozhi-server/models/sherpa-onnx*
|
main/xiaozhi-server/models/sherpa-onnx*
|
||||||
|
my_wakeup_words.mp3
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY main/xiaozhi-server/requirements.txt .
|
COPY main/xiaozhi-server/requirements.txt .
|
||||||
|
|
||||||
# 优化apt安装
|
# 安装Python依赖
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# 第三阶段:生产镜像
|
# 第二阶段:生产镜像
|
||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
|
|
||||||
WORKDIR /opt/xiaozhi-esp32-server
|
WORKDIR /opt/xiaozhi-esp32-server
|
||||||
|
|
||||||
# 优化apt安装
|
# 安装系统依赖
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
|
|||||||
@@ -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、更多问题,可联系我们反馈 💬
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 298 KiB |
@@ -0,0 +1,84 @@
|
|||||||
|
package xiaozhi.modules.admin.contrloler;
|
||||||
|
|
||||||
|
|
||||||
|
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.sys.dto.AdminPageUserDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
|
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员控制层
|
||||||
|
*
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-25
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin")
|
||||||
|
@Tag(name = "管理员管理")
|
||||||
|
public class AdminController {
|
||||||
|
private final SysUserService sysUserService;
|
||||||
|
|
||||||
|
@GetMapping("/users")
|
||||||
|
@Operation(summary = "分页查找用户")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = "mobile", description = "用户手机号码", required = true),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||||
|
})
|
||||||
|
public Result<PageData<AdminPageUserVO>> pageUser(
|
||||||
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
AdminPageUserDTO dto = new AdminPageUserDTO();
|
||||||
|
dto.setMobile((String) params.get("mobile"));
|
||||||
|
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||||
|
dto.setPage((String) params.get("pages"));
|
||||||
|
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||||
|
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/users/{id}")
|
||||||
|
@Operation(summary = "重置密码")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> update(
|
||||||
|
@PathVariable Long id) {
|
||||||
|
String password = sysUserService.resetPassword(id);
|
||||||
|
return new Result<String>().ok(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/users/{id}")
|
||||||
|
@Operation(summary = "用户删除")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> delete(@PathVariable Long id) {
|
||||||
|
sysUserService.delete(new Long[]{id});
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/device/all")
|
||||||
|
@Operation(summary = "分页查找设备")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = "keywords", description = "用户手机号码", required = true),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||||
|
})
|
||||||
|
public Result<Void> pageDevice(
|
||||||
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
//TODO 等设备功能模块写好
|
||||||
|
return new Result<Void>().error(600,"等设备功能模块写好");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package xiaozhi.modules.agent.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 jakarta.validation.Valid;
|
||||||
|
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.user.UserDetail;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Tag(name = "智能体管理")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/agent")
|
||||||
|
public class AgentController {
|
||||||
|
private final AgentService agentService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "获取用户智能体列表")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<AgentEntity>> getUserAgents() {
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
List<AgentEntity> agents = agentService.getUserAgents(user.getId());
|
||||||
|
return new Result<List<AgentEntity>>().ok(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
@Operation(summary = "智能体列表(管理员)")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||||
|
})
|
||||||
|
public Result<PageData<AgentEntity>> adminAgentList(
|
||||||
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
PageData<AgentEntity> page = agentService.adminAgentList(params);
|
||||||
|
return new Result<PageData<AgentEntity>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取智能体详情")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<AgentEntity> getAgentById(@PathVariable("id") String id) {
|
||||||
|
AgentEntity agent = agentService.getAgentById(id);
|
||||||
|
return new Result<AgentEntity>().ok(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "创建智能体")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||||
|
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
||||||
|
|
||||||
|
// 设置用户ID和创建者信息
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
entity.setUserId(user.getId());
|
||||||
|
entity.setCreator(user.getId());
|
||||||
|
entity.setCreatedAt(new Date());
|
||||||
|
|
||||||
|
// ID、智能体编码和排序会在Service层自动生成
|
||||||
|
agentService.insert(entity);
|
||||||
|
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
@Operation(summary = "更新智能体")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> update(@RequestBody @Valid AgentUpdateDTO dto) {
|
||||||
|
// 先查询现有实体
|
||||||
|
AgentEntity existingEntity = agentService.getAgentById(dto.getId());
|
||||||
|
if (existingEntity == null) {
|
||||||
|
return new Result<Void>().error("智能体不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只更新提供的非空字段
|
||||||
|
if (dto.getAgentName() != null) {
|
||||||
|
existingEntity.setAgentName(dto.getAgentName());
|
||||||
|
}
|
||||||
|
if (dto.getAgentCode() != null) {
|
||||||
|
existingEntity.setAgentCode(dto.getAgentCode());
|
||||||
|
}
|
||||||
|
if (dto.getAsrModelId() != null) {
|
||||||
|
existingEntity.setAsrModelId(dto.getAsrModelId());
|
||||||
|
}
|
||||||
|
if (dto.getVadModelId() != null) {
|
||||||
|
existingEntity.setVadModelId(dto.getVadModelId());
|
||||||
|
}
|
||||||
|
if (dto.getLlmModelId() != null) {
|
||||||
|
existingEntity.setLlmModelId(dto.getLlmModelId());
|
||||||
|
}
|
||||||
|
if (dto.getTtsModelId() != null) {
|
||||||
|
existingEntity.setTtsModelId(dto.getTtsModelId());
|
||||||
|
}
|
||||||
|
if (dto.getTtsVoiceId() != null) {
|
||||||
|
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
|
||||||
|
}
|
||||||
|
if (dto.getMemModelId() != null) {
|
||||||
|
existingEntity.setMemModelId(dto.getMemModelId());
|
||||||
|
}
|
||||||
|
if (dto.getIntentModelId() != null) {
|
||||||
|
existingEntity.setIntentModelId(dto.getIntentModelId());
|
||||||
|
}
|
||||||
|
if (dto.getSystemPrompt() != null) {
|
||||||
|
existingEntity.setSystemPrompt(dto.getSystemPrompt());
|
||||||
|
}
|
||||||
|
if (dto.getLangCode() != null) {
|
||||||
|
existingEntity.setLangCode(dto.getLangCode());
|
||||||
|
}
|
||||||
|
if (dto.getLanguage() != null) {
|
||||||
|
existingEntity.setLanguage(dto.getLanguage());
|
||||||
|
}
|
||||||
|
if (dto.getSort() != null) {
|
||||||
|
existingEntity.setSort(dto.getSort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置更新者信息
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
existingEntity.setUpdater(user.getId());
|
||||||
|
existingEntity.setUpdatedAt(new Date());
|
||||||
|
|
||||||
|
agentService.updateById(existingEntity);
|
||||||
|
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "删除智能体")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> delete(@PathVariable String id) {
|
||||||
|
agentService.deleteById(id);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import xiaozhi.common.dao.BaseDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AgentDao extends BaseDao<AgentEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体创建DTO
|
||||||
|
* 专用于新增智能体,不包含id、agentCode和sort字段,这些字段由系统自动生成/设置默认值
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体创建对象")
|
||||||
|
public class AgentCreateDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "智能体名称", example = "客服助手")
|
||||||
|
@NotBlank(message = "智能体名称不能为空")
|
||||||
|
private String agentName;
|
||||||
|
|
||||||
|
@Schema(description = "语音识别模型标识", example = "asr_model_01")
|
||||||
|
private String asrModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音活动检测标识", example = "vad_model_01")
|
||||||
|
private String vadModelId;
|
||||||
|
|
||||||
|
@Schema(description = "大语言模型标识", example = "llm_model_01")
|
||||||
|
private String llmModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音合成模型标识", example = "tts_model_01")
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色标识", example = "voice_01")
|
||||||
|
private String ttsVoiceId;
|
||||||
|
|
||||||
|
@Schema(description = "记忆模型标识", example = "mem_model_01")
|
||||||
|
private String memModelId;
|
||||||
|
|
||||||
|
@Schema(description = "意图模型标识", example = "intent_model_01")
|
||||||
|
private String intentModelId;
|
||||||
|
|
||||||
|
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题")
|
||||||
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "语言编码", example = "zh_CN")
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Schema(description = "交互语种", example = "中文")
|
||||||
|
private String language;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体更新DTO
|
||||||
|
* 专用于更新智能体,id字段是必需的,用于标识要更新的智能体
|
||||||
|
* 其他字段均为非必填,只更新提供的字段
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体更新对象")
|
||||||
|
public class AgentUpdateDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "智能体唯一标识", example = "a1b2c3d4e5f6", required = true)
|
||||||
|
@NotBlank(message = "智能体ID不能为空")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "智能体编码", example = "AGT_1234567890", required = false)
|
||||||
|
private String agentCode;
|
||||||
|
|
||||||
|
@Schema(description = "智能体名称", example = "客服助手", required = false)
|
||||||
|
private String agentName;
|
||||||
|
|
||||||
|
@Schema(description = "语音识别模型标识", example = "asr_model_02", required = false)
|
||||||
|
private String asrModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音活动检测标识", example = "vad_model_02", required = false)
|
||||||
|
private String vadModelId;
|
||||||
|
|
||||||
|
@Schema(description = "大语言模型标识", example = "llm_model_02", required = false)
|
||||||
|
private String llmModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色标识", example = "voice_02", required = false)
|
||||||
|
private String ttsVoiceId;
|
||||||
|
|
||||||
|
@Schema(description = "记忆模型标识", example = "mem_model_02", required = false)
|
||||||
|
private String memModelId;
|
||||||
|
|
||||||
|
@Schema(description = "意图模型标识", example = "intent_model_02", required = false)
|
||||||
|
private String intentModelId;
|
||||||
|
|
||||||
|
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
||||||
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Schema(description = "交互语种", example = "中文", required = false)
|
||||||
|
private String language;
|
||||||
|
|
||||||
|
@Schema(description = "排序", example = "1", required = false)
|
||||||
|
private Integer sort;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("ai_agent")
|
||||||
|
@Schema(description = "智能体信息")
|
||||||
|
public class AgentEntity {
|
||||||
|
@Schema(description = "智能体唯一标识")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "所属用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "智能体编码")
|
||||||
|
private String agentCode;
|
||||||
|
|
||||||
|
@Schema(description = "智能体名称")
|
||||||
|
private String agentName;
|
||||||
|
|
||||||
|
@Schema(description = "语音识别模型标识")
|
||||||
|
private String asrModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音活动检测标识")
|
||||||
|
private String vadModelId;
|
||||||
|
|
||||||
|
@Schema(description = "大语言模型标识")
|
||||||
|
private String llmModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音合成模型标识")
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色标识")
|
||||||
|
private String ttsVoiceId;
|
||||||
|
|
||||||
|
@Schema(description = "记忆模型标识")
|
||||||
|
private String memModelId;
|
||||||
|
|
||||||
|
@Schema(description = "意图模型标识")
|
||||||
|
private String intentModelId;
|
||||||
|
|
||||||
|
@Schema(description = "角色设定参数")
|
||||||
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "语言编码")
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Schema(description = "交互语种")
|
||||||
|
private String language;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface AgentService extends BaseService<AgentEntity> {
|
||||||
|
/**
|
||||||
|
* 根据用户ID获取智能体列表
|
||||||
|
*/
|
||||||
|
List<AgentEntity> getUserAgents(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员获取所有智能体列表(分页)
|
||||||
|
*/
|
||||||
|
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体详情
|
||||||
|
*/
|
||||||
|
AgentEntity getAgentById(String id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.modules.agent.dao.AgentDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||||
|
private final AgentDao agentDao;
|
||||||
|
|
||||||
|
public AgentServiceImpl(AgentDao agentDao) {
|
||||||
|
this.agentDao = agentDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AgentEntity> getUserAgents(Long userId) {
|
||||||
|
QueryWrapper<AgentEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
return agentDao.selectList(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
|
IPage<AgentEntity> page = agentDao.selectPage(
|
||||||
|
getPage(params, "sort", true),
|
||||||
|
new QueryWrapper<>()
|
||||||
|
);
|
||||||
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AgentEntity getAgentById(String id) {
|
||||||
|
return agentDao.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean insert(AgentEntity entity) {
|
||||||
|
// 如果ID为空,自动生成一个UUID作为ID
|
||||||
|
if (entity.getId() == null || entity.getId().trim().isEmpty()) {
|
||||||
|
entity.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果智能体编码为空,自动生成一个带前缀的编码
|
||||||
|
if (entity.getAgentCode() == null || entity.getAgentCode().trim().isEmpty()) {
|
||||||
|
entity.setAgentCode("AGT_" + System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果排序字段为空,设置默认值0
|
||||||
|
if (entity.getSort() == null) {
|
||||||
|
entity.setSort(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.insert(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
package xiaozhi.modules.model.controller;public class ModelConfigController {
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package xiaozhi.modules.model.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
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.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
|
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||||
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/models")
|
||||||
|
@Tag(name = "模型配置")
|
||||||
|
public class ModelController {
|
||||||
|
|
||||||
|
private final ModelProviderService modelProviderService;
|
||||||
|
|
||||||
|
private final ModelConfigService modelConfigService;
|
||||||
|
|
||||||
|
@GetMapping("/models/names")
|
||||||
|
@Operation(summary = "获取所有模型名称")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<List<String>> getModelNames(@RequestParam String modelType,
|
||||||
|
@RequestParam(required = false) String modelName) {
|
||||||
|
List<String> modelNameList = modelConfigService.getModelCodeList(modelType, modelName);
|
||||||
|
return new Result<List<String>>().ok(modelNameList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{modelType}/provideTypes")
|
||||||
|
@Operation(summary = "获取模型供应器列表")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<List<ModelProviderDTO>> getModelProviderList(@PathVariable String modelType) {
|
||||||
|
List<ModelProviderDTO> modelProviderDTOS = modelProviderService.getListByModelType(modelType);
|
||||||
|
return new Result<List<ModelProviderDTO>>().ok(modelProviderDTOS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{modelType}/{provideCode}/fields")
|
||||||
|
@Operation(summary = "获取模型供应器字段")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<List<String>> getModelProviderFields(@PathVariable String modelType, @PathVariable String provideCode) {
|
||||||
|
List<String> fieldList = modelProviderService.getFieldList(modelType, provideCode);
|
||||||
|
return new Result<List<String>>().ok(fieldList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping("/models/list")
|
||||||
|
@Operation(summary = "获取模型配置列表")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<PageData<ModelConfigDTO>> getModelConfigList(@RequestParam String modelType,
|
||||||
|
@RequestParam(required = false) String modelName,
|
||||||
|
@RequestParam(required = false, defaultValue = "0") Integer page,
|
||||||
|
@RequestParam(required = false,defaultValue = "10") Integer limit) {
|
||||||
|
PageData<ModelConfigDTO> pageList = modelConfigService.getPageList(modelType, modelName, page, limit);
|
||||||
|
return new Result<PageData<ModelConfigDTO>>().ok(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("/models/{modelType}/{provideCode}")
|
||||||
|
@Operation(summary = "新增模型配置")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<ModelConfigDTO> addModelConfig(@PathVariable String modelType,
|
||||||
|
@PathVariable String provideCode,
|
||||||
|
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||||
|
ModelConfigDTO modelConfigDTO = modelConfigService.add(modelType, provideCode, modelConfigBodyDTO);
|
||||||
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PutMapping("/models/{modelType}/{provideCode}/{id}")
|
||||||
|
@Operation(summary = "编辑模型配置")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<ModelConfigDTO> editModelConfig(@PathVariable String modelType,
|
||||||
|
@PathVariable String provideCode,
|
||||||
|
@PathVariable String id,
|
||||||
|
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||||
|
ModelConfigDTO modelConfigDTO = modelConfigService.edit(modelType, provideCode, id, modelConfigBodyDTO);
|
||||||
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@DeleteMapping("/models/{modelType}/{provideCode}/{id}")
|
||||||
|
@Operation(summary = "删除模型配置")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> deleteModelConfig(@PathVariable String modelType, @PathVariable String provideCode, @PathVariable String id) {
|
||||||
|
modelConfigService.delete(modelType, provideCode, id);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/models/{modelName}/voices")
|
||||||
|
@Operation(summary = "获取模型音色")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<String>> getVoiceList(@PathVariable String modelName,
|
||||||
|
@RequestParam(required = false) String voiceName) {
|
||||||
|
|
||||||
|
List<String> voiceList = modelConfigService.getVoiceList(modelName, voiceName);
|
||||||
|
return new Result<List<String>>().ok(voiceList);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package xiaozhi.modules.model.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import xiaozhi.common.dao.BaseDao;
|
||||||
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ModelConfigDao extends BaseDao<ModelConfigEntity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get model_code list
|
||||||
|
*/
|
||||||
|
List<String> getModelCodeList(@Param("modelType") String modelType, @Param("modelName") String modelName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package xiaozhi.modules.model.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import xiaozhi.common.dao.BaseDao;
|
||||||
|
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ModelProviderDao extends BaseDao<ModelProviderEntity> {
|
||||||
|
|
||||||
|
List<String> getFieldList(@Param("modelType") String modelType, @Param("provideCode") String provideCode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package xiaozhi.modules.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "模型供应器/商")
|
||||||
|
public class ModelConfigBodyDTO {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// @Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||||
|
// private String modelType;
|
||||||
|
//
|
||||||
|
@Schema(description = "模型编码(如AliLLM、DoubaoTTS)")
|
||||||
|
private String modelCode;
|
||||||
|
|
||||||
|
@Schema(description = "模型名称")
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
@Schema(description = "是否默认配置(0否 1是)")
|
||||||
|
private Integer isDefault;
|
||||||
|
|
||||||
|
@Schema(description = "是否启用")
|
||||||
|
private Integer isEnabled;
|
||||||
|
|
||||||
|
@Schema(description = "模型配置(JSON格式)")
|
||||||
|
private String configJson;
|
||||||
|
|
||||||
|
@Schema(description = "官方文档链接")
|
||||||
|
private String docLink;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package xiaozhi.modules.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "模型供应器/商")
|
||||||
|
public class ModelConfigDTO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||||
|
private String modelType;
|
||||||
|
|
||||||
|
@Schema(description = "模型编码(如AliLLM、DoubaoTTS)")
|
||||||
|
private String modelCode;
|
||||||
|
|
||||||
|
@Schema(description = "模型名称")
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
@Schema(description = "是否默认配置(0否 1是)")
|
||||||
|
private Integer isDefault;
|
||||||
|
|
||||||
|
@Schema(description = "是否启用")
|
||||||
|
private Integer isEnabled;
|
||||||
|
|
||||||
|
@Schema(description = "模型配置(JSON格式)")
|
||||||
|
private String configJson;
|
||||||
|
|
||||||
|
@Schema(description = "官方文档链接")
|
||||||
|
private String docLink;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package xiaozhi.modules.model.dto;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "模型供应器/商")
|
||||||
|
public class ModelProviderDTO implements Serializable {
|
||||||
|
//
|
||||||
|
// @Schema(description = "主键")
|
||||||
|
// private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||||
|
private String modelType;
|
||||||
|
|
||||||
|
@Schema(description = "供应器类型")
|
||||||
|
private String providerCode;
|
||||||
|
|
||||||
|
@Schema(description = "供应器名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "供应器字段列表(JSON格式)")
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private String fields;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createDate;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package xiaozhi.modules.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("ai_model_config")
|
||||||
|
@Schema(description = "模型配置表")
|
||||||
|
public class ModelConfigEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||||
|
private String modelType;
|
||||||
|
|
||||||
|
@Schema(description = "模型编码(如AliLLM、DoubaoTTS)")
|
||||||
|
private String modelCode;
|
||||||
|
|
||||||
|
@Schema(description = "模型名称")
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
@Schema(description = "是否默认配置(0否 1是)")
|
||||||
|
private Integer isDefault;
|
||||||
|
|
||||||
|
@Schema(description = "是否启用")
|
||||||
|
private Integer isEnabled;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
@Schema(description = "模型配置(JSON格式)")
|
||||||
|
private String configJson;
|
||||||
|
|
||||||
|
@Schema(description = "官方文档链接")
|
||||||
|
private String docLink;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createDate;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package xiaozhi.modules.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("ai_model_provider")
|
||||||
|
@Schema(description = "模型供应器表")
|
||||||
|
public class ModelProviderEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||||
|
private String modelType;
|
||||||
|
|
||||||
|
@Schema(description = "供应器类型,如 openai、")
|
||||||
|
private String providerCode;
|
||||||
|
|
||||||
|
@Schema(description = "供应器名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "供应器字段列表(JSON格式)")
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private String fields;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createDate;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.model.service;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
|
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ModelConfigService {
|
||||||
|
|
||||||
|
List<String> getModelCodeList(String modelType, String modelName);
|
||||||
|
|
||||||
|
PageData<ModelConfigDTO> getPageList(String modelType, String modelName, Integer page, Integer limit);
|
||||||
|
|
||||||
|
ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO);
|
||||||
|
|
||||||
|
ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO);
|
||||||
|
|
||||||
|
void delete(String modelType, String provideCode, String id);
|
||||||
|
|
||||||
|
List<String> getVoiceList(String modelName, String voiceName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package xiaozhi.modules.model.service;
|
||||||
|
|
||||||
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
|
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface ModelProviderService {
|
||||||
|
|
||||||
|
// List<String> getModelNames(String modelType, String modelName);
|
||||||
|
|
||||||
|
List<ModelProviderDTO> getListByModelType(String modelType);
|
||||||
|
|
||||||
|
ModelProviderDTO add(ModelProviderEntity modelProviderEntity);
|
||||||
|
|
||||||
|
ModelProviderDTO edit(ModelProviderEntity modelProviderEntity);
|
||||||
|
|
||||||
|
void delete();
|
||||||
|
|
||||||
|
List<ModelProviderDTO> getList(String modelType, String provideCode);
|
||||||
|
|
||||||
|
List<String> getFieldList(String modelType, String provideCode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package xiaozhi.modules.model.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||||
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
|
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||||
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, ModelConfigEntity> implements ModelConfigService {
|
||||||
|
|
||||||
|
private final ModelConfigDao modelConfigDao;
|
||||||
|
private final ModelProviderService modelProviderService;
|
||||||
|
private final TimbreService timbreService;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ModelConfigServiceImpl.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getModelCodeList(String modelType, String modelName) {
|
||||||
|
return modelConfigDao.getModelCodeList(modelType, modelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<ModelConfigDTO> getPageList(String modelType, String modelName, Integer page, Integer limit) {
|
||||||
|
Map<String, Object> params = new HashMap<String, Object>();
|
||||||
|
params.put(Constant.PAGE, page);
|
||||||
|
params.put(Constant.LIMIT, limit);
|
||||||
|
IPage<ModelConfigEntity> modelConfigEntityIPage = modelConfigDao.selectPage(
|
||||||
|
getPage(params, "sort", true),
|
||||||
|
new QueryWrapper<ModelConfigEntity>()
|
||||||
|
.eq("model_type", modelType)
|
||||||
|
.like(StringUtils.isNotBlank(modelName), "model_name", modelName)
|
||||||
|
);
|
||||||
|
return getPageData(modelConfigEntityIPage, ModelConfigDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||||
|
// 先验证有没有供应器
|
||||||
|
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
|
||||||
|
throw new RenException("modelType和provideCode不能为空");
|
||||||
|
}
|
||||||
|
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||||
|
if (CollectionUtil.isEmpty(providerList)) {
|
||||||
|
throw new RenException("供应器不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再保存供应器提供的模型
|
||||||
|
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||||
|
modelConfigEntity.setModelType(modelType);
|
||||||
|
modelConfigDao.insert(modelConfigEntity);
|
||||||
|
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||||
|
// 先验证有没有供应器
|
||||||
|
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
|
||||||
|
throw new RenException("modelType和provideCode不能为空");
|
||||||
|
}
|
||||||
|
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||||
|
if (CollectionUtil.isEmpty(providerList)) {
|
||||||
|
throw new RenException("供应器不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再更新供应器提供的模型
|
||||||
|
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||||
|
modelConfigEntity.setId(id);
|
||||||
|
modelConfigEntity.setModelType(modelType);
|
||||||
|
modelConfigDao.updateById(modelConfigEntity);
|
||||||
|
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String modelType, String provideCode, String id) {
|
||||||
|
// 先验证有没有供应器
|
||||||
|
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
|
||||||
|
throw new RenException("modelType和provideCode不能为空");
|
||||||
|
}
|
||||||
|
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||||
|
if (CollectionUtil.isEmpty(providerList)) {
|
||||||
|
throw new RenException("供应器不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
modelConfigDao.deleteById(Long.getLong(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getVoiceList(String modelName, String voiceName) {
|
||||||
|
QueryWrapper<ModelConfigEntity> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("model_name", StringUtils.isBlank(modelName) ? "" : modelName);
|
||||||
|
queryWrapper.eq("model_type", "TTS");
|
||||||
|
List<ModelConfigEntity> modelConfigEntities = modelConfigDao.selectList(queryWrapper);
|
||||||
|
if (CollectionUtil.isEmpty(modelConfigEntities)) {
|
||||||
|
logger.warn("没有找到模型配置信息");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ModelConfigEntity modelConfigEntity = modelConfigEntities.get(0);
|
||||||
|
String id = modelConfigEntity.getId();
|
||||||
|
|
||||||
|
return timbreService.getVoiceNames(id, voiceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package xiaozhi.modules.model.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.modules.model.dao.ModelProviderDao;
|
||||||
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
|
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||||
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao, ModelProviderEntity> implements ModelProviderService {
|
||||||
|
|
||||||
|
private final ModelProviderDao modelProviderDao;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ModelProviderDTO> getListByModelType(String modelType) {
|
||||||
|
|
||||||
|
QueryWrapper<ModelProviderEntity> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("model_type", StringUtils.isBlank(modelType) ? "" : modelType);
|
||||||
|
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
|
||||||
|
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModelProviderDTO add(ModelProviderEntity modelProviderEntity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModelProviderDTO edit(ModelProviderEntity modelProviderEntity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ModelProviderDTO> getList(String modelType, String provideCode) {
|
||||||
|
QueryWrapper<ModelProviderEntity> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("model_type", StringUtils.isBlank(modelType) ? "" : modelType);
|
||||||
|
queryWrapper.eq("provide_code", StringUtils.isBlank(provideCode) ? "" : provideCode);
|
||||||
|
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
|
||||||
|
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getFieldList(String modelType, String provideCode) {
|
||||||
|
return modelProviderDao.getFieldList(modelType, provideCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.apache.shiro.authc.AuthenticationException;
|
import org.apache.shiro.authc.AuthenticationException;
|
||||||
import org.apache.shiro.authc.AuthenticationToken;
|
import org.apache.shiro.authc.AuthenticationToken;
|
||||||
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
|
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
@@ -24,12 +26,15 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
public class Oauth2Filter extends AuthenticatingFilter {
|
public class Oauth2Filter extends AuthenticatingFilter {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Oauth2Filter.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
|
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
|
||||||
//获取请求token
|
//获取请求token
|
||||||
String token = getRequestToken((HttpServletRequest) request);
|
String token = getRequestToken((HttpServletRequest) request);
|
||||||
|
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
|
logger.warn("createToken:token is empty");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +54,15 @@ public class Oauth2Filter extends AuthenticatingFilter {
|
|||||||
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
|
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
|
||||||
//获取请求token,如果token不存在,直接返回401
|
//获取请求token,如果token不存在,直接返回401
|
||||||
String token = getRequestToken((HttpServletRequest) request);
|
String token = getRequestToken((HttpServletRequest) request);
|
||||||
|
|
||||||
|
// TODO 调试接口,临时取消登录限制,需要 token 参数的除外
|
||||||
|
if (true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
|
logger.warn("onAccessDenied:token is empty");
|
||||||
|
|
||||||
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||||
httpResponse.setContentType("application/json;charset=utf-8");
|
httpResponse.setContentType("application/json;charset=utf-8");
|
||||||
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
|
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
@@ -73,13 +86,14 @@ public class Oauth2Filter extends AuthenticatingFilter {
|
|||||||
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
||||||
try {
|
try {
|
||||||
//处理登录失败的异常
|
//处理登录失败的异常
|
||||||
|
logger.error("onLoginFailure:登录失败!", e);
|
||||||
Throwable throwable = e.getCause() == null ? e : e.getCause();
|
Throwable throwable = e.getCause() == null ? e : e.getCause();
|
||||||
Result r = new Result().error(ErrorCode.UNAUTHORIZED, throwable.getMessage());
|
Result r = new Result().error(ErrorCode.UNAUTHORIZED, throwable.getMessage());
|
||||||
|
|
||||||
String json = JsonUtils.toJsonString(r);
|
String json = JsonUtils.toJsonString(r);
|
||||||
httpResponse.getWriter().print(json);
|
httpResponse.getWriter().print(json);
|
||||||
} catch (IOException e1) {
|
} catch (IOException e1) {
|
||||||
|
logger.error("onLoginFailure:登录失败! msg:{}", e1.getMessage(), e1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import com.google.common.cache.Cache;
|
|||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import com.wf.captcha.SpecCaptcha;
|
import com.wf.captcha.SpecCaptcha;
|
||||||
import com.wf.captcha.base.Captcha;
|
import com.wf.captcha.base.Captcha;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||||
import xiaozhi.modules.security.service.CaptchaService;
|
import xiaozhi.modules.security.service.CaptchaService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -29,6 +32,7 @@ public class CaptchaServiceImpl implements CaptchaService {
|
|||||||
* Local Cache 5分钟过期
|
* Local Cache 5分钟过期
|
||||||
*/
|
*/
|
||||||
Cache<String, String> localCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(5, TimeUnit.MINUTES).build();
|
Cache<String, String> localCache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(5, TimeUnit.MINUTES).build();
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Oauth2Realm.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void create(HttpServletResponse response, String uuid) throws IOException {
|
public void create(HttpServletResponse response, String uuid) throws IOException {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package xiaozhi.modules.sys.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 AdminPageUserDTO {
|
||||||
|
|
||||||
|
|
||||||
|
@Schema(description = "手机号码")
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
@Schema(description = "页数")
|
||||||
|
private String page;
|
||||||
|
|
||||||
|
@Schema(description = "显示列数")
|
||||||
|
private String limit;
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package xiaozhi.modules.sys.service;
|
package xiaozhi.modules.sys.service;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
|
||||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,5 +22,30 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
|||||||
|
|
||||||
void delete(Long[] ids);
|
void delete(Long[] ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证是否允许修改密码更改
|
||||||
|
* @param userId 用户id
|
||||||
|
* @param passwordDTO 验证密码的参数
|
||||||
|
*/
|
||||||
void changePassword(Long userId, PasswordDTO passwordDTO);
|
void changePassword(Long userId, PasswordDTO passwordDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接修改密码,不需要验证
|
||||||
|
* @param userId 用户id
|
||||||
|
* @param password 密码
|
||||||
|
*/
|
||||||
|
void changePasswordDirectly(Long userId, String password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置密码
|
||||||
|
* @param userId 用户id
|
||||||
|
* @return 随机生成符合规范的密码
|
||||||
|
*/
|
||||||
|
String resetPassword(Long userId);
|
||||||
|
/**
|
||||||
|
* 管理员分页用户信息
|
||||||
|
* @param dto 分页查找参数
|
||||||
|
* @return 用户列表分页数据
|
||||||
|
*/
|
||||||
|
PageData<AdminPageUserVO> page(AdminPageUserDTO dto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
package xiaozhi.modules.sys.service.impl;
|
package xiaozhi.modules.sys.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.security.password.PasswordUtils;
|
import xiaozhi.modules.security.password.PasswordUtils;
|
||||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
|
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
|
||||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
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 xiaozhi.modules.sys.service.SysUserService;
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
|
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -80,9 +85,11 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
public void delete(Long[] ids) {
|
public void delete(Long[] ids) {
|
||||||
//删除用户
|
//删除用户
|
||||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||||
|
//TODO 除了要删除用户还要删除用户关联的设备,对话,智能体。等此3个功能完善在添加
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void changePassword(Long userId, PasswordDTO passwordDTO) {
|
public void changePassword(Long userId, PasswordDTO passwordDTO) {
|
||||||
SysUserEntity sysUserEntity = sysUserDao.selectById(userId);
|
SysUserEntity sysUserEntity = sysUserDao.selectById(userId);
|
||||||
|
|
||||||
@@ -107,11 +114,50 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
updateById(sysUserEntity);
|
updateById(sysUserEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void changePasswordDirectly(Long userId, String password) {
|
||||||
|
SysUserEntity sysUserEntity = new SysUserEntity();
|
||||||
|
sysUserEntity.setId(userId);
|
||||||
|
sysUserEntity.setPassword(PasswordUtils.encode(password));
|
||||||
|
updateById(sysUserEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public String resetPassword(Long userId) {
|
||||||
|
String password = generatePassword();
|
||||||
|
changePasswordDirectly(userId,password);
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
private Long getUserCount() {
|
private Long getUserCount() {
|
||||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||||
return baseDao.selectCount(queryWrapper);
|
return baseDao.selectCount(queryWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<AdminPageUserVO> page(AdminPageUserDTO dto) {
|
||||||
|
Map<String, Object> params = new HashMap<String, Object>();
|
||||||
|
params.put(Constant.PAGE, dto.getPage());
|
||||||
|
params.put(Constant.LIMIT,dto.getLimit());
|
||||||
|
IPage<SysUserEntity> page = baseDao.selectPage(
|
||||||
|
getPage(params, "id", true),
|
||||||
|
//定义查询条件
|
||||||
|
new QueryWrapper<SysUserEntity>()
|
||||||
|
//必须按照手机号码查找
|
||||||
|
.eq(StringUtils.isNotBlank(dto.getMobile()),"username",dto.getMobile()));
|
||||||
|
List<AdminPageUserVO> list = page.getRecords().stream().map(user -> {
|
||||||
|
AdminPageUserVO adminPageUserVO = new AdminPageUserVO();
|
||||||
|
adminPageUserVO.setUserid(user.getId().toString());
|
||||||
|
adminPageUserVO.setMobile(user.getUsername());
|
||||||
|
//TODO 2. 等设备功能写好,获取对应数据
|
||||||
|
adminPageUserVO.setDeviceCount("0");
|
||||||
|
return adminPageUserVO;
|
||||||
|
}).toList();
|
||||||
|
return new PageData<>(list, page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isStrongPassword(String password) {
|
private boolean isStrongPassword(String password) {
|
||||||
// 弱密码的正则表达式
|
// 弱密码的正则表达式
|
||||||
@@ -120,4 +166,19 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
Matcher matcher = pattern.matcher(password);
|
Matcher matcher = pattern.matcher(password);
|
||||||
return matcher.matches();
|
return matcher.matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()";
|
||||||
|
private static final Random random = new Random();
|
||||||
|
/**
|
||||||
|
* 生成随机密码
|
||||||
|
* @return 随机生成的密码
|
||||||
|
*/
|
||||||
|
private String generatePassword(){
|
||||||
|
StringBuilder password = new StringBuilder();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
int randomIndex = random.nextInt(CHARACTERS.length());
|
||||||
|
password.append(CHARACTERS.charAt(randomIndex));
|
||||||
|
}
|
||||||
|
return password.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员分页展示用户的VO
|
||||||
|
* @ zjy
|
||||||
|
* @since 2025-3-25
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AdminPageUserVO {
|
||||||
|
|
||||||
|
@Schema(description = "设备数量")
|
||||||
|
private String deviceCount;
|
||||||
|
|
||||||
|
@Schema(description = "手机号码")
|
||||||
|
private String mobile;
|
||||||
|
|
||||||
|
@Schema(description = "用户id")
|
||||||
|
private String userid;
|
||||||
|
}
|
||||||
@@ -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,53 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色的业务层的定义
|
||||||
|
* @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);
|
||||||
|
|
||||||
|
List<String> getVoiceNames(String ttsModelId, String voiceName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package xiaozhi.modules.timbre.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
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.entity.TimbreEntity;
|
||||||
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色的业务层的实现
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
||||||
|
|
||||||
|
private final TimbreDao timbreDao;
|
||||||
|
|
||||||
|
@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));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getVoiceNames(String ttsModelId, String voiceName) {
|
||||||
|
QueryWrapper<TimbreEntity> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("ttsModelId", StringUtils.isBlank(ttsModelId) ? "" : ttsModelId);
|
||||||
|
if (StringUtils.isNotBlank(voiceName)) {
|
||||||
|
queryWrapper.like("name", voiceName);
|
||||||
|
}
|
||||||
|
List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper);
|
||||||
|
if (CollectionUtil.isEmpty(timbreEntities)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timbreEntities.stream().map(TimbreEntity::getName).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理是不是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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ spring:
|
|||||||
database: 0
|
database: 0
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 6379
|
port: 6379
|
||||||
password: # 密码(默认为空)
|
password: # 密码(默认为空)
|
||||||
timeout: 6000ms # 连接超时时长(毫秒)
|
timeout: 6000ms # 连接超时时长(毫秒)
|
||||||
lettuce:
|
lettuce:
|
||||||
pool:
|
pool:
|
||||||
@@ -39,7 +39,6 @@ spring:
|
|||||||
min-idle: 5 # 连接池中的最小空闲连接
|
min-idle: 5 # 连接池中的最小空闲连接
|
||||||
main:
|
main:
|
||||||
allow-bean-definition-overriding: true
|
allow-bean-definition-overriding: true
|
||||||
|
|
||||||
knife4j:
|
knife4j:
|
||||||
enable: true
|
enable: true
|
||||||
basic:
|
basic:
|
||||||
|
|||||||
@@ -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,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<mapper namespace="xiaozhi.modules.model.dao.ModelConfigDao">
|
||||||
|
<!-- 获取模型供应器字段 -->
|
||||||
|
<select id="getModelCodeList" resultType="String">
|
||||||
|
select model_name from ai_model_config where model_type = #{modelType}
|
||||||
|
<if test="modelName != null">and model_name = #{modelName}</if>
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
|
<mapper namespace="xiaozhi.modules.model.dao.ModelProviderDao">
|
||||||
|
<!-- 获取模型供应器字段 -->
|
||||||
|
<select id="getFieldList" resultType="string">
|
||||||
|
select fields from ai_model_provider where model_type = #{modelType} and provider_code = #{providerCode};
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// 引入各个模块的请求
|
// 引入各个模块的请求
|
||||||
import user from './module/user.js'
|
import user from './module/user.js'
|
||||||
|
import admin from './module/admin.js'
|
||||||
/**
|
/**
|
||||||
* 接口地址
|
* 接口地址
|
||||||
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
|
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import RequestService from '../httpRequest'
|
||||||
|
import {getServiceUrl} from '../api'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 用户列表
|
||||||
|
getUserList(callback) {
|
||||||
|
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/admin/users`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ import {getServiceUrl} from '../api'
|
|||||||
export default {
|
export default {
|
||||||
// 登录
|
// 登录
|
||||||
login(loginForm, callback) {
|
login(loginForm, callback) {
|
||||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`)
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/login`)
|
||||||
.method('POST')
|
.method('POST')
|
||||||
.data(loginForm)
|
.data(loginForm)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
@@ -18,51 +19,6 @@ export default {
|
|||||||
})
|
})
|
||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
// 获取设备信息
|
|
||||||
getHomeList(callback) {
|
|
||||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`)
|
|
||||||
.method('GET')
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime()
|
|
||||||
callback(res)
|
|
||||||
})
|
|
||||||
.fail(() => {
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.getUserInfo()
|
|
||||||
})
|
|
||||||
}).send()
|
|
||||||
},
|
|
||||||
// 解绑设备
|
|
||||||
unbindDevice(device_id, callback) {
|
|
||||||
RequestService.sendRequest()
|
|
||||||
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
|
|
||||||
.method('PUT')
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime();
|
|
||||||
callback(res);
|
|
||||||
})
|
|
||||||
.fail(() => {
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.unbindDevice(device_id, callback);
|
|
||||||
});
|
|
||||||
}).send()
|
|
||||||
},
|
|
||||||
// 绑定设备
|
|
||||||
bindDevice(deviceCode, callback) {
|
|
||||||
RequestService.sendRequest()
|
|
||||||
.url(`${getServiceUrl()}/api/v1/user/device/bind/${deviceCode}`)
|
|
||||||
.method('POST')
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime();
|
|
||||||
callback(res);
|
|
||||||
})
|
|
||||||
.fail((err) => {
|
|
||||||
console.error('绑定设备失败:', err);
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.bindDevice(deviceCode, callback);
|
|
||||||
});
|
|
||||||
}).send();
|
|
||||||
},
|
|
||||||
// 获取验证码
|
// 获取验证码
|
||||||
getCaptcha(uuid, callback) {
|
getCaptcha(uuid, callback) {
|
||||||
|
|
||||||
@@ -71,9 +27,9 @@ export default {
|
|||||||
.method('GET')
|
.method('GET')
|
||||||
.type('blob')
|
.type('blob')
|
||||||
.header({
|
.header({
|
||||||
'Content-Type': 'image/gif',
|
'Content-Type': 'image/gif',
|
||||||
'Pragma': 'No-cache',
|
'Pragma': 'No-cache',
|
||||||
'Cache-Control': 'no-cache'
|
'Cache-Control': 'no-cache'
|
||||||
})
|
})
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime();
|
RequestService.clearRequestTime();
|
||||||
@@ -85,7 +41,9 @@ export default {
|
|||||||
},
|
},
|
||||||
// 注册账号
|
// 注册账号
|
||||||
register(registerForm, callback) {
|
register(registerForm, callback) {
|
||||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/register`).method('POST')
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/register`)
|
||||||
|
.method('POST')
|
||||||
.data(registerForm)
|
.data(registerForm)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
@@ -112,53 +70,6 @@ export default {
|
|||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
// 获取设备配置
|
|
||||||
getDeviceConfig(device_id, callback) {
|
|
||||||
RequestService.sendRequest()
|
|
||||||
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
|
|
||||||
.method('GET')
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime();
|
|
||||||
callback(res);
|
|
||||||
})
|
|
||||||
.fail((err) => {
|
|
||||||
console.error('获取配置失败:', err);
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.getDeviceConfig(device_id, callback);
|
|
||||||
});
|
|
||||||
}).send();
|
|
||||||
},
|
|
||||||
// 获取所有模型名称
|
|
||||||
getModelNames(callback) {
|
|
||||||
RequestService.sendRequest()
|
|
||||||
.url(`${getServiceUrl()}/api/v1/models/names`)
|
|
||||||
.method('GET')
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime();
|
|
||||||
callback(res);
|
|
||||||
})
|
|
||||||
.fail(() => {
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.getModelNames(callback);
|
|
||||||
});
|
|
||||||
}).send();
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取模型音色
|
|
||||||
getModelVoices(modelName, callback) {
|
|
||||||
RequestService.sendRequest()
|
|
||||||
.url(`${getServiceUrl()}/api/v1/models/${modelName}/voices`)
|
|
||||||
.method('GET')
|
|
||||||
.success((res) => {
|
|
||||||
RequestService.clearRequestTime();
|
|
||||||
callback(res);
|
|
||||||
})
|
|
||||||
.fail(() => {
|
|
||||||
RequestService.reAjaxFun(() => {
|
|
||||||
this.getModelVoices(modelName, callback);
|
|
||||||
});
|
|
||||||
}).send();
|
|
||||||
},
|
|
||||||
// 获取智能体列表
|
// 获取智能体列表
|
||||||
getAgentList(callback) {
|
getAgentList(callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
@@ -173,8 +84,8 @@ export default {
|
|||||||
this.getAgentList(callback);
|
this.getAgentList(callback);
|
||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
|
// 用户信息获取
|
||||||
getUserInfo(callback) {
|
getUserInfo(callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/info`)
|
.url(`${getServiceUrl()}/api/v1/user/info`)
|
||||||
@@ -195,7 +106,7 @@ export default {
|
|||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/agent`)
|
.url(`${getServiceUrl()}/api/v1/user/agent`)
|
||||||
.method('POST')
|
.method('POST')
|
||||||
.data({ name: agentName })
|
.data({name: agentName})
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime();
|
RequestService.clearRequestTime();
|
||||||
callback(res);
|
callback(res);
|
||||||
@@ -206,5 +117,136 @@ export default {
|
|||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
|
// 删除智能体
|
||||||
|
deleteAgent(agentId, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
|
||||||
|
.method('DELETE')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.deleteAgent(agentId, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 修改用户密码
|
||||||
|
changePassword(oldPassword, newPassword, successCallback, errorCallback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/change-password`)
|
||||||
|
.method('PUT')
|
||||||
|
.data({
|
||||||
|
old_password: oldPassword,
|
||||||
|
new_password: newPassword,
|
||||||
|
})
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
successCallback(res);
|
||||||
|
})
|
||||||
|
.fail((error) => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.changePassword(oldPassword, newPassword, successCallback, errorCallback);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
},
|
||||||
|
// 获取智能体配置
|
||||||
|
getDeviceConfig(deviceId, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/agent/${deviceId}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取配置失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDeviceConfig(deviceId, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 配置智能体
|
||||||
|
updateAgentConfig(agentId, configData, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/agent/${agentId}`)
|
||||||
|
.method('PUT')
|
||||||
|
.data(configData)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.updateAgentConfig(agentId, configData, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 已绑设备
|
||||||
|
getAgentBindDevices(agentId, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取设备列表失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getAgentBindDevices(agentId, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 解绑设备
|
||||||
|
unbindDevice(device_id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
|
||||||
|
.method('PUT')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('解绑设备失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.unbindDevice(device_id, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 绑定设备
|
||||||
|
bindDevice(agentId, code, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`)
|
||||||
|
.method('POST')
|
||||||
|
.data({ code })
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('绑定设备失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.bindDevice(agentId, code, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 新增方法:获取智能体模板
|
||||||
|
getAgentTemplate(templateName, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/agent/templateId`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取模板失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getAgentTemplate(templateName, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -33,16 +33,46 @@
|
|||||||
export default {
|
export default {
|
||||||
name: 'AddDeviceDialog',
|
name: 'AddDeviceDialog',
|
||||||
props: {
|
props: {
|
||||||
visible: { type: Boolean, required: true }
|
visible: { type: Boolean, required: true },
|
||||||
|
agentId: { type: String, required: true }
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return { deviceCode: "" }
|
return {
|
||||||
|
deviceCode: "",
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
confirm() {
|
confirm() {
|
||||||
this.$emit('update:visible', false)
|
if (!/^\d{6}$/.test(this.deviceCode)) {
|
||||||
this.$emit('added', this.deviceCode)
|
this.$message.error('请输入6位数字验证码');
|
||||||
this.deviceCode = ""
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
import('@/apis/module/user').then(({ default: userApi }) => {
|
||||||
|
userApi.bindDevice(
|
||||||
|
this.agentId,
|
||||||
|
this.deviceCode, ({data}) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.$emit('refresh');
|
||||||
|
this.$message.success('设备绑定成功');
|
||||||
|
this.closeDialog();
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg || '绑定失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).catch((err) => {
|
||||||
|
this.loading = false;
|
||||||
|
console.error('API模块加载失败:', err);
|
||||||
|
this.$message.error('绑定服务不可用');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
closeDialog() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
this.deviceCode = '';
|
||||||
|
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('update:visible', false)
|
this.$emit('update:visible', false)
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="visible"
|
||||||
|
width="975px"
|
||||||
|
center
|
||||||
|
custom-class="custom-dialog"
|
||||||
|
:show-close="false"
|
||||||
|
class="center-dialog"
|
||||||
|
|
||||||
|
>
|
||||||
|
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||||
|
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
|
添加模型
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<button class="custom-close-btn" @click="handleClose">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 模型信息部分 -->
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
|
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span style="margin-right: 8px;">是否启用</span>
|
||||||
|
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span style="margin-right: 8px;">设为默认</span>
|
||||||
|
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
|
|
||||||
|
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||||
|
<!-- 第一行:模型名称和模型编码 -->
|
||||||
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
|
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.modelName"
|
||||||
|
placeholder="请输入模型名称"
|
||||||
|
class="custom-input-bg"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.modelCode"
|
||||||
|
placeholder="请输入模型编码"
|
||||||
|
class="custom-input-bg"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二行:供应器和排序号 -->
|
||||||
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
|
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.supplier"
|
||||||
|
placeholder="请选择"
|
||||||
|
class="custom-select custom-input-bg"
|
||||||
|
style="width: 100%;"
|
||||||
|
>
|
||||||
|
<el-option label="硅基流动" value="硅基流动"></el-option>
|
||||||
|
<el-option label="智脑科技" value="智脑科技"></el-option>
|
||||||
|
<el-option label="云智科技" value="云智科技"></el-option>
|
||||||
|
<el-option label="其他" value="其他"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.sort"
|
||||||
|
placeholder="请输入排序号"
|
||||||
|
class="custom-input-bg"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文档地址 -->
|
||||||
|
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.docLink"
|
||||||
|
placeholder="请输入文档地址"
|
||||||
|
class="custom-input-bg"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 备注 -->
|
||||||
|
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="请输入模型备注"
|
||||||
|
class="custom-input-bg"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 调用信息部分 -->
|
||||||
|
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
||||||
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 15px;"></div>
|
||||||
|
|
||||||
|
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||||
|
<!-- 第一行:模型名称和接口地址 -->
|
||||||
|
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
||||||
|
<el-form-item label="模型名称" prop="param1" style="flex: 0.5; margin-bottom: 0;">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.configJson.param1"
|
||||||
|
placeholder="请输入model_name"
|
||||||
|
class="custom-input-bg"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="接口地址" prop="param2" style="flex: 1; margin-bottom: 0;">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.configJson.param2"
|
||||||
|
placeholder="请输入base_url"
|
||||||
|
class="custom-input-bg"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 秘钥信息 -->
|
||||||
|
<el-form-item label="秘钥信息" prop="apiKey">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.configJson.apiKey"
|
||||||
|
placeholder="请输入api_key"
|
||||||
|
show-password
|
||||||
|
class="custom-input-bg"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 保存按钮 -->
|
||||||
|
<div style="display: flex;justify-content: center;">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="confirm"
|
||||||
|
class="save-btn"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AddModelDialog',
|
||||||
|
props: {
|
||||||
|
visible: {type: Boolean, required: true},
|
||||||
|
modelType: {type: String, required: true}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
modelName: '',
|
||||||
|
modelCode: '',
|
||||||
|
supplier: '',
|
||||||
|
sort: 1,
|
||||||
|
docLink: '',
|
||||||
|
remark: '',
|
||||||
|
isEnabled: true,
|
||||||
|
isDefault: true,
|
||||||
|
configJson: {
|
||||||
|
param1: '',
|
||||||
|
param2: '',
|
||||||
|
apiKey: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
confirm() {
|
||||||
|
if (!this.formData.modelName || !this.formData.modelCode || !this.formData.supplier ||
|
||||||
|
!this.formData.configJson.param1 || !this.formData.configJson.param2 || !this.formData.configJson.apiKey) {
|
||||||
|
this.$message.error('请填写所有必填字段');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('confirm', {
|
||||||
|
...this.formData,
|
||||||
|
provideType: this.formData.supplier
|
||||||
|
});
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
this.resetForm();
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
this.formData = {
|
||||||
|
modelName: '',
|
||||||
|
modelCode: '',
|
||||||
|
supplier: '',
|
||||||
|
sort: 1,
|
||||||
|
docLink: '',
|
||||||
|
remark: '',
|
||||||
|
isEnabled: true,
|
||||||
|
isDefault: true,
|
||||||
|
configJson: {
|
||||||
|
param1: '',
|
||||||
|
param2: '',
|
||||||
|
apiKey: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.resetForm();
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom-dialog {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
padding-bottom: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dialog .el-dialog__header {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-dialog {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-dialog .el-dialog {
|
||||||
|
margin: 4% 0 auto !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #cfcfcf;
|
||||||
|
background: none;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: lighter;
|
||||||
|
color: #cfcfcf;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
border-color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select .el-input__suffix {
|
||||||
|
background: #e6e8ea;
|
||||||
|
right: 6px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select .el-input__suffix-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select .el-icon-arrow-up:before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-top: 7px solid #c0c4cc;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-form .el-form-item {
|
||||||
|
margin-bottom: 20px; /* 统一设置所有表单项的间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-form .el-form-item__label {
|
||||||
|
color: #3d4566;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-form .el-form-item.prop-remark .el-form-item__label {
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改placeholder颜色 */
|
||||||
|
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
|
||||||
|
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
|
||||||
|
color: #9c9f9e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框背景色 */
|
||||||
|
.custom-input-bg .el-input__inner,
|
||||||
|
.custom-input-bg .el-textarea__inner {
|
||||||
|
background-color: #f6f8fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background: #e6f0fd;
|
||||||
|
color: #237ff4;
|
||||||
|
border: 1px solid #b3d1ff;
|
||||||
|
width: 150px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
background: linear-gradient(to right, #237ff4, #9c40d5);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 修改开关样式 */
|
||||||
|
.custom-switch .el-switch__core {
|
||||||
|
border-radius: 20px;
|
||||||
|
height: 23px;
|
||||||
|
background-color: #c0ccda;
|
||||||
|
width: 35px;
|
||||||
|
padding: 0 20px; /* 调整左右内边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-switch .el-switch__core:after {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
background-color: white;
|
||||||
|
top: 3px;
|
||||||
|
left: 4px;
|
||||||
|
transition: all .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-switch.is-checked .el-switch__core {
|
||||||
|
border-color: #b5bcf0;
|
||||||
|
background-color: #cfd7fa;
|
||||||
|
padding: 0 20px; /* 确保启用状态也有相同的间隔 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-switch.is-checked .el-switch__core:after {
|
||||||
|
left: 100%;
|
||||||
|
margin-left: -18px;
|
||||||
|
background-color: #1b47ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 调整flex布局的gap */
|
||||||
|
[style*="display: flex"] {
|
||||||
|
gap: 20px; /* 扩大flex项间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整输入框高度 */
|
||||||
|
.custom-input-bg .el-input__inner {
|
||||||
|
height: 32px; /* 固定输入框高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :visible.sync="visible" width="400px" center>
|
||||||
|
<div
|
||||||
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
|
<div
|
||||||
|
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||||
|
<img src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||||
|
</div>
|
||||||
|
修改密码
|
||||||
|
</div>
|
||||||
|
<div style="height: 1px;background: #e8f0ff;"/>
|
||||||
|
<div style="margin: 22px 15px;">
|
||||||
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||||
|
<div style="color: red;display: inline-block;">*</div>
|
||||||
|
旧密码:
|
||||||
|
</div>
|
||||||
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
|
<el-input placeholder="请输入旧密码" v-model="oldPassword" type="password" show-password/>
|
||||||
|
</div>
|
||||||
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||||
|
<div style="color: red;display: inline-block;">*</div>
|
||||||
|
新密码:
|
||||||
|
</div>
|
||||||
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
|
<el-input placeholder="请输入新密码" v-model="newPassword" type="password" show-password/>
|
||||||
|
</div>
|
||||||
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||||
|
<div style="color: red;display: inline-block;">*</div>
|
||||||
|
确认新密码:
|
||||||
|
</div>
|
||||||
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
|
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||||
|
<div class="dialog-btn" @click="confirm">
|
||||||
|
确定
|
||||||
|
</div>
|
||||||
|
<div class="dialog-btn"
|
||||||
|
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||||
|
@click="cancel">
|
||||||
|
取消
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import userApi from '@/apis/module/user';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChangePasswordDialog',
|
||||||
|
props: {
|
||||||
|
visible: {type: Boolean, required: true}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
oldPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
confirmNewPassword: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
confirm() {
|
||||||
|
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
|
||||||
|
this.$message.error('请填写所有字段');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.newPassword !== this.confirmNewPassword) {
|
||||||
|
this.$message.error('两次输入的新密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.newPassword === this.oldPassword) {
|
||||||
|
this.$message.error('新密码不能与旧密码相同');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接调用修改密码接口
|
||||||
|
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.error(res.msg || '密码修改失败');
|
||||||
|
} else {
|
||||||
|
this.$message.success('密码修改成功');
|
||||||
|
this.$emit('confirm', res);
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
}, (err) => {
|
||||||
|
this.$message.error(err.msg || '密码修改失败');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
this.resetForm();
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
this.oldPassword = "";
|
||||||
|
this.newPassword = "";
|
||||||
|
this.confirmNewPassword = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.input-46 {
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
background: #f6f8fb;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 23px;
|
||||||
|
background: #5778ff;
|
||||||
|
height: 40px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-dialog {
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-dialog__headerbtn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-dialog__body {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-dialog__header {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<img src="@/assets/home/delete.png" alt=""
|
<img src="@/assets/home/delete.png" alt=""
|
||||||
style="width: 18px;height: 18px;margin-right: 10px;" />
|
style="width: 18px;height: 18px;margin-right: 10px;" @click.stop="handleDelete" />
|
||||||
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
音色模型:{{ device.ttsVoiceName }}
|
音色模型:{{ device.ttsVoiceName }}
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;gap: 10px;align-items: center;">
|
<div style="display: flex;gap: 10px;align-items: center;">
|
||||||
<div class="settings-btn" @click="$emit('configure')">
|
<div class="settings-btn" @click="handleConfigure">
|
||||||
配置角色
|
配置角色
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-btn">
|
<div class="settings-btn">
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="settings-btn">
|
<div class="settings-btn">
|
||||||
历史对话
|
历史对话
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-btn" @click="$emit('deviceManage')">
|
<div class="settings-btn" @click="handleDeviceManage">
|
||||||
设备管理
|
设备管理
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,6 +44,17 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return { switchValue: false }
|
return { switchValue: false }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleDelete() {
|
||||||
|
this.$emit('delete', this.device.agentId)
|
||||||
|
},
|
||||||
|
handleConfigure() {
|
||||||
|
this.$router.push({ path: '/role-config', query: { agentId: this.device.agentId } });
|
||||||
|
},
|
||||||
|
handleDeviceManage() {
|
||||||
|
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,50 +1,77 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-header class="header">
|
<el-header class="header">
|
||||||
<div style="display: flex;justify-content: space-between;margin-top: 6px; ">
|
<div class="header-container">
|
||||||
<div style="display: flex;align-items: center;gap: 10px;">
|
<!-- 左侧元素 -->
|
||||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 42px;height: 42px;" />
|
<div class="header-left">
|
||||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 58px;height: 12px;" />
|
<img alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
|
||||||
<div class="equipment-management" @click="goHome">
|
<img alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
|
||||||
<img src="@/assets/home/equipment.png" alt="" style="width: 12px;height: 10px;" />
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间导航菜单 -->
|
||||||
|
<div class="header-center">
|
||||||
|
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
||||||
|
<img alt="" src="@/assets/header/roboot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||||
智能体管理
|
智能体管理
|
||||||
</div>
|
</div>
|
||||||
<div class="console">
|
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||||
<i class="el-icon-s-grid" style="font-size: 10px;color: #979db1;" />
|
<img alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||||
控制台
|
用户管理
|
||||||
</div>
|
</div>
|
||||||
<div class="equipment-management2">
|
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
|
||||||
设备管理
|
<img alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||||
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
|
模型配置
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;align-items: center;gap: 7px; margin-top: 2px;">
|
|
||||||
<div class="serach-box">
|
<!-- 右侧元素 -->
|
||||||
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" @keyup.enter.native="handleSearch" />
|
<div class="header-right">
|
||||||
<img src="@/assets/home/search.png" alt=""
|
<div class="search-container">
|
||||||
style="width: 14px;height: 14px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
|
<el-input
|
||||||
</div>
|
v-model="serach"
|
||||||
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
|
placeholder="输入名称搜索.."
|
||||||
<div class="user-info">
|
class="custom-search-input"
|
||||||
{{ userInfo.mobile || '加载中...' }}
|
@keyup.enter.native="handleSearch"
|
||||||
|
>
|
||||||
|
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||||
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
|
<img alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
|
||||||
|
<el-dropdown trigger="click" class="user-dropdown">
|
||||||
|
<span class="el-dropdown-link">
|
||||||
|
{{ userInfo.mobile || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||||
|
</span>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item icon="el-icon-plus" @click.native="">个人中心</el-dropdown-item>
|
||||||
|
<el-dropdown-item icon="el-icon-circle-plus" @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
||||||
|
<el-dropdown-item icon="el-icon-circle-plus-outline" @click.native="">退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码弹窗 -->
|
||||||
|
<ChangePasswordDialog :visible.sync="isChangePasswordDialogVisible"/>
|
||||||
</el-header>
|
</el-header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import userApi from '@/apis/module/user'
|
import userApi from '@/apis/module/user';
|
||||||
|
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HeaderBar',
|
name: 'HeaderBar',
|
||||||
|
components: {
|
||||||
|
ChangePasswordDialog
|
||||||
|
},
|
||||||
props: ['devices'], // 接收父组件设备列表
|
props: ['devices'], // 接收父组件设备列表
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serach: '',
|
serach: '',
|
||||||
userInfo: {
|
userInfo: {
|
||||||
|
username: '',
|
||||||
mobile: ''
|
mobile: ''
|
||||||
}
|
},
|
||||||
|
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -55,6 +82,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}) => {
|
||||||
@@ -71,7 +104,7 @@ export default {
|
|||||||
// 当搜索内容为空时,显示原始完整列表
|
// 当搜索内容为空时,显示原始完整列表
|
||||||
filteredDevices = this.$parent.originalDevices;
|
filteredDevices = this.$parent.originalDevices;
|
||||||
} else {
|
} else {
|
||||||
// 过滤逻辑
|
// 过滤逻辑
|
||||||
filteredDevices = this.devices.filter(device => {
|
filteredDevices = this.devices.filter(device => {
|
||||||
return device.agentName.includes(searchValue) ||
|
return device.agentName.includes(searchValue) ||
|
||||||
device.ttsModelName.includes(searchValue) ||
|
device.ttsModelName.includes(searchValue) ||
|
||||||
@@ -80,96 +113,179 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('search-result', filteredDevices);
|
this.$emit('search-result', filteredDevices);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 显示修改密码弹窗
|
||||||
|
showChangePasswordDialog() {
|
||||||
|
this.isChangePasswordDialogVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.equipment-management,
|
.header {
|
||||||
.equipment-management2 {
|
background: #f6fcfe66;
|
||||||
cursor: pointer;
|
border: 1px solid #fff;
|
||||||
|
height: 53px !important;
|
||||||
|
min-width: 900px; /* 设置最小宽度防止过度压缩 */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-img {
|
||||||
|
width: 58px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 25px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-width: 300px;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management {
|
.equipment-management {
|
||||||
width: 82px;
|
width: 82px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #5778ff;
|
background: #deeafe;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 7px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
font-weight: 500;
|
||||||
|
|
||||||
.equipment-management2 {
|
|
||||||
width: 87px;
|
|
||||||
height: 22px;
|
|
||||||
border-radius: 11px;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: 400;
|
|
||||||
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;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0; /* 防止导航按钮被压缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.equipment-management.active-tab {
|
||||||
background: #f6fcfe66;
|
background: #5778ff !important;
|
||||||
border: 1px solid #fff;
|
color: #fff !important;
|
||||||
height: 53px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.serach-box {
|
.equipment-management img {
|
||||||
display: flex;
|
width: 15px;
|
||||||
width: 220px;
|
height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
margin-right: 15px;
|
||||||
|
min-width: 150px;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search-input >>> .el-input__inner {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
background-color: #f6fcfe66;
|
background-color: #e2e5f8;
|
||||||
border: 1px solid #e4e6ef;
|
border: 1px solid #e4e6ef;
|
||||||
align-items: center;
|
padding-left: 15px;
|
||||||
padding: 0 7px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.serach-box /deep/ .el-input__inner {
|
|
||||||
border-radius: 15px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
padding-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: -0.02px;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
text-align: left;
|
width: 100%;
|
||||||
color: #3d4566;
|
|
||||||
}
|
}
|
||||||
.console {
|
|
||||||
width: 90px;
|
.search-icon {
|
||||||
height: 22px;
|
cursor: pointer;
|
||||||
border-radius: 11px;
|
color: #909399;
|
||||||
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
|
margin-right: 8px;
|
||||||
display: flex;
|
font-size: 14px;
|
||||||
align-items: center;
|
line-height: 30px;
|
||||||
justify-content: center;
|
}
|
||||||
font-size: 9px;
|
|
||||||
color: #979db1;
|
.avatar-img {
|
||||||
font-weight: 400;
|
width: 21px;
|
||||||
gap: 7px;
|
height: 21px;
|
||||||
margin-left: 15px;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.header-center {
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-management {
|
||||||
|
width: 70px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.search-container {
|
||||||
|
margin-right: 10px;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.header-left {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-container {
|
||||||
|
max-width: 145px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search-input >>> .el-input__inner {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.search-container {
|
||||||
|
max-width: 120px;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</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({
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<h3 class="device-list-title">设备列表</h3>
|
<h3 class="device-list-title">设备列表</h3>
|
||||||
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
|
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
|
||||||
+ 添加设备
|
+ 添加设备
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-table :data="deviceList" style="width: 100%; margin-top: 20px" border stripe>
|
<el-table :data="paginatedDeviceList" 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>
|
||||||
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
||||||
@@ -31,17 +31,27 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="80">
|
<el-table-column label="操作" width="80">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row)" style="color: #ff4949">
|
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)" style="color: #ff4949">
|
||||||
解绑
|
解绑
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
class="pagination"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-sizes="[5, 10, 20, 50]"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="deviceList.length"
|
||||||
|
></el-pagination>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
|
<div style="font-size: 12px; font-weight: 400; margin-top: auto; padding-top: 30px; color: #979db1;">
|
||||||
©2025 xiaozhi-esp32-server
|
©2025 xiaozhi-esp32-server
|
||||||
</div>
|
</div>
|
||||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" @added="handleDeviceAdded" />
|
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId" @refresh="fetchBindDevices(currentAgentId)" />
|
||||||
</el-main>
|
</el-main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -49,49 +59,37 @@
|
|||||||
<script>
|
<script>
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {HeaderBar, AddDeviceDialog },
|
components: {HeaderBar, AddDeviceDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
deviceList: [
|
currentAgentId: this.$route.query.agentId || '',
|
||||||
{
|
currentPage: 1,
|
||||||
model: 'xingzhi-cube-0.96oled-wifi',
|
pageSize: 5,
|
||||||
firmwareVersion: '1.4.6',
|
deviceList: [],
|
||||||
macAddress: 'fc:01:2c:c5:d5:7c',
|
loading: false,
|
||||||
bindTime: '2025-03-10 18:16:21',
|
userApi: null,
|
||||||
lastConversation: '6 天前',
|
|
||||||
remark: '',
|
|
||||||
isEdit: false,
|
|
||||||
otaSwitch: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: 'xingzhi-board-1.3tft-ble',
|
|
||||||
firmwareVersion: '2.1.0',
|
|
||||||
macAddress: 'ac:12:3d:e7:f8:9a',
|
|
||||||
bindTime: '2025-03-12 09:30:15',
|
|
||||||
lastConversation: '4 天前',
|
|
||||||
remark: '测试设备',
|
|
||||||
isEdit: false,
|
|
||||||
otaSwitch: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: 'xingzhi-kit-0.91oled-4g',
|
|
||||||
firmwareVersion: '1.8.3',
|
|
||||||
macAddress: 'bc:45:6f:1e:2d:3c',
|
|
||||||
bindTime: '2025-03-15 14:22:08',
|
|
||||||
lastConversation: '2 天前',
|
|
||||||
remark: '生产环境设备',
|
|
||||||
isEdit: false,
|
|
||||||
otaSwitch: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
paginatedDeviceList() {
|
||||||
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
|
const end = start + this.pageSize;
|
||||||
|
return this.deviceList.slice(start, end);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const agentId = this.$route.query.agentId;
|
||||||
|
import('@/apis/module/user').then(({ default: userApi }) => {
|
||||||
|
this.userApi = userApi;
|
||||||
|
if (agentId) {
|
||||||
|
this.fetchBindDevices(agentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleAddDevice() {
|
handleAddDevice() {
|
||||||
// 添加设备逻辑
|
|
||||||
this.addDeviceDialogVisible = true;
|
this.addDeviceDialogVisible = true;
|
||||||
},
|
},
|
||||||
startEditRemark(index, row) {
|
startEditRemark(index, row) {
|
||||||
@@ -100,13 +98,57 @@ export default {
|
|||||||
stopEditRemark(index) {
|
stopEditRemark(index) {
|
||||||
this.deviceList[index].isEdit = false;
|
this.deviceList[index].isEdit = false;
|
||||||
},
|
},
|
||||||
handleUnbind(device) {
|
handleUnbind(device_id) {
|
||||||
// 解绑逻辑
|
if (!this.userApi) {
|
||||||
console.log('解绑设备', device);
|
this.$message.error('功能模块加载失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$confirm('确认要解绑该设备吗?', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.userApi.unbindDevice(device_id, ({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.$message.success('设备解绑成功');
|
||||||
|
this.fetchBindDevices(this.$route.query.agentId);
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg || '设备解绑失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
handleDeviceAdded(deviceCode) {
|
handleSizeChange(val) {
|
||||||
console.log('添加的智慧体名称:', deviceCode);
|
this.pageSize = val;
|
||||||
},
|
},
|
||||||
|
handleCurrentChange(val) {
|
||||||
|
this.currentPage = val;
|
||||||
|
},
|
||||||
|
fetchBindDevices(agentId) {
|
||||||
|
this.loading = true;
|
||||||
|
import('@/apis/module/user').then(({ default: userApi }) => {
|
||||||
|
userApi.getAgentBindDevices(agentId, ({ data }) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.deviceList = data.data[0].list.map(device => ({
|
||||||
|
device_id: device.id,
|
||||||
|
model: device.device_type,
|
||||||
|
firmwareVersion: device.app_version,
|
||||||
|
macAddress: device.mac_address,
|
||||||
|
lastConversation: device.recent_chat_time,
|
||||||
|
remark: '',
|
||||||
|
isEdit: false,
|
||||||
|
otaSwitch: device.ota_upgrade === 1
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg || '获取设备列表失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('模块加载失败:', error);
|
||||||
|
this.$message.error('功能模块加载失败');
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -166,4 +208,8 @@ export default {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,401 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome">
|
||||||
|
<HeaderBar />
|
||||||
|
|
||||||
|
<div class="operation-bar">
|
||||||
|
|
||||||
|
<div class="right-operations">
|
||||||
|
<el-button v-if="activeTab === 'tts'" type="primary" plain size="small" @click="ttsDialogVisible = true">
|
||||||
|
语音设置
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" plain size="small" @click="handleImport">
|
||||||
|
导入配置
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" plain size="small" @click="handleExport">
|
||||||
|
导出配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 主体内容 -->
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<div class="content-panel">
|
||||||
|
<!-- 左侧导航 -->
|
||||||
|
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
|
||||||
|
style="background-size: cover; background-position: center;">
|
||||||
|
<el-menu-item index="vad">
|
||||||
|
<span class="menu-text">语言活动检测</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="asr">
|
||||||
|
<span class="menu-text">语音识别</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="llm">
|
||||||
|
<span class="menu-text">大语言模型</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="intent">
|
||||||
|
<span class="menu-text">意图识别</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="tts">
|
||||||
|
<span class="menu-text">语音合成</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="memory">
|
||||||
|
<span class="menu-text">记忆</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
<!-- 右侧内容 -->
|
||||||
|
<div class="content-area">
|
||||||
|
<div class="title-bar">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<h2 class="model-title">大语言模型(LLM)</h2>
|
||||||
|
<el-button type="primary" size="small" @click="addModel" class="add-btn">
|
||||||
|
添加
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="action-group">
|
||||||
|
<div class="search-group">
|
||||||
|
<el-input placeholder="请输入模型名称查询" v-model="search" size="small" class="search-input" clearable/>
|
||||||
|
<el-button type="primary" size="small" class="search-btn" @click="handleSearch">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :header-cell-style="{background: 'transparent'}" :data="modelList" border class="data-table" header-row-class-name="table-header" >
|
||||||
|
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||||
|
<el-table-column label="模型名称" prop="candidateName" align="center"></el-table-column>
|
||||||
|
<el-table-column label="模型编码" prop="code" align="center"></el-table-column>
|
||||||
|
<el-table-column label="提供商" prop="supplier" align="center"></el-table-column>
|
||||||
|
<el-table-column label="是否启用" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-switch v-model="scope.row.isApplied" class="custom-switch" :active-color="null" :inactive-color="null"/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="table-footer">
|
||||||
|
<div class="batch-actions">
|
||||||
|
<el-button size="mini" @click="selectAll">全选</el-button>
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination @current-change="handleCurrentChange" background :current-page="currentPage" :page-size="pageSize" layout="prev, pager, next" :total="total"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
|
||||||
|
<TtsModel :visible.sync="ttsDialogVisible" />
|
||||||
|
<AddModelDialog :visible.sync="addDialogVisible" @confirm="handleAddConfirm"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="copyright">
|
||||||
|
©2025 xiaozhi-esp32-server
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import ModelEditDialog from "@/components/ModelEditDialog.vue";
|
||||||
|
import TtsModel from "@/components/TtsModel.vue";
|
||||||
|
import AddModelDialog from "@/components/AddModelDialog.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
addDialogVisible: false,
|
||||||
|
activeTab: 'llm',
|
||||||
|
search: '',
|
||||||
|
editDialogVisible: false,
|
||||||
|
editModelData: {},
|
||||||
|
ttsDialogVisible: false,
|
||||||
|
modelList: [
|
||||||
|
{ code: 'DeepSeek', candidateName: '深度求索', isApplied: true, supplier: '硅基流动' },
|
||||||
|
{ code: 'SmartAssist', candidateName: '智能助手', isApplied: false, supplier: '智脑科技' },
|
||||||
|
{ code: 'CogEngine', candidateName: '认知引擎', isApplied: true, supplier: '云智科技' },
|
||||||
|
],
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 4,
|
||||||
|
total: 20
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleMenuSelect(index) {
|
||||||
|
this.activeTab = index;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
console.log('查询:', this.search);
|
||||||
|
},
|
||||||
|
batchDelete() {
|
||||||
|
console.log('批量删除');
|
||||||
|
},
|
||||||
|
addModel() {
|
||||||
|
this.addDialogVisible = true;
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
handleImport() {
|
||||||
|
console.log('导入配置');
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
console.log('导出配置');
|
||||||
|
},
|
||||||
|
handleModelSave(formData) {
|
||||||
|
console.log('保存的模型数据:', formData);
|
||||||
|
},
|
||||||
|
selectAll() {
|
||||||
|
console.log('全选');
|
||||||
|
},
|
||||||
|
handleAddConfirm(newModel) {
|
||||||
|
console.log('新增模型数据:', newModel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
::v-deep .el-table tr{
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.welcome {
|
||||||
|
min-width: 900px;
|
||||||
|
min-height: 506px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
background-size: cover;
|
||||||
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
-o-background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
margin: 5px 60px;
|
||||||
|
background-image: url("@/assets/home/background.png");
|
||||||
|
border-radius: 15px;
|
||||||
|
min-height: 600px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-panel {
|
||||||
|
width: 18%;
|
||||||
|
min-width: 200px;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid #ebeef5;
|
||||||
|
background-size: cover;
|
||||||
|
background: url("../assets/model/model.png") no-repeat center;
|
||||||
|
padding: 16px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-panel .el-menu-item {
|
||||||
|
height: 48px;
|
||||||
|
line-height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 12px !important;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 8px 12px 8px auto;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-panel .el-menu-item.is-active {
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409EFF;
|
||||||
|
border-right: 3px solid #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 600px;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-title {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
background: linear-gradient(135deg, #6B8CFF, #A966FF);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: transparent !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table /deep/ .el-table__row {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header th {
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-footer {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
text-align: center;
|
||||||
|
color: #979db1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 30px 0 20px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
color: #409EFF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
color: #F56C6C !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background: #87CEFA;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
.title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.batch-actions .el-button:first-child {
|
||||||
|
background: linear-gradient(135deg, #409EFF, #6B8CFF);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-actions .el-button:first-child:hover {
|
||||||
|
background: linear-gradient(135deg, #3A8EE6, #5A7CFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table th /deep/ .el-table__cell {
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome">
|
||||||
|
<HeaderBar />
|
||||||
|
<el-main class="main" style="padding: 20px; display: flex; flex-direction: column;">
|
||||||
|
<div class="top-area">
|
||||||
|
<div class="page-title">用户管理</div>
|
||||||
|
<div class="page-search">
|
||||||
|
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input" />
|
||||||
|
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||||
|
<!-- <el-button type="danger" @click="batchDelete">批量删除</el-button>
|
||||||
|
<el-button type="danger" @click="batchDisable">批量禁用</el-button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card class="user-card" shadow="never">
|
||||||
|
<!-- <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" class="transparent-table" :header-cell-class-name="headerCellClassName">
|
||||||
|
<el-table-column label="选择" type="selection" width="55"></el-table-column>
|
||||||
|
<el-table-column label="用户Id" prop="user_id"></el-table-column>
|
||||||
|
<el-table-column label="手机号码" prop="mobile"></el-table-column>
|
||||||
|
<el-table-column label="设备数量" prop="device_count"></el-table-column>
|
||||||
|
<el-table-column label="状态" prop="status"></el-table-column>
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text" @click="resetPassword(scope.row)" style="color: #989fdd">重置密码</el-button>
|
||||||
|
<el-button size="mini" type="text"
|
||||||
|
v-if="scope.row.status === '正常'"
|
||||||
|
@click="disableUser(scope.row)">禁用</el-button>
|
||||||
|
<el-button size="mini" type="text"
|
||||||
|
v-if="scope.row.status === '禁用'"
|
||||||
|
@click="restoreUser(scope.row)">恢复</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="deleteUser(scope.row)" style="color: #989fdd">删除用户</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="table_bottom">
|
||||||
|
<div class="ctrl_btn">
|
||||||
|
<el-button size="mini" type="primary" style="width: 72px; background: #5f70f3">全选</el-button>
|
||||||
|
<el-button size="mini" type="success" icon="el-icon-circle-check" style="background: #5bc98c">启用</el-button>
|
||||||
|
<el-button size="mini" type="warning" style="color: black; background: #f6d075"><i class="el-icon-remove-outline rotated-icon"></i>禁用</el-button>
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-delete" style="background: #fd5b63">删除</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-sizes="[5, 10, 15]"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<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";
|
||||||
|
import Api from '@/apis/api';
|
||||||
|
import adminApi from '@/apis/module/admin';
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
adminApi.getUserList(({data}) => {
|
||||||
|
//mock偶尔会返回-1导致出错,又会返回两个list,所以这里只取第一个
|
||||||
|
this.userList = data.data[0].list;
|
||||||
|
console.log('用户列表:', this.userList);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
headerCellClassName({ column, columnIndex }) {
|
||||||
|
if (columnIndex === 0) {
|
||||||
|
return 'custom-selection-header'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
$table-bg-color: #ecf1fd;
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 20px; display: flex; flex-direction: column;
|
||||||
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||||
|
}
|
||||||
|
.top-area {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.page-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.btn-search {
|
||||||
|
margin-left: 10px;
|
||||||
|
background: linear-gradient(to right, #5778ff, #c793f3);
|
||||||
|
width: 100px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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: $table-bg-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
//box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
.ctrl_btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.rotated-icon {
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
margin-right: 4px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
background: $table-bg-color;
|
||||||
|
|
||||||
|
&.transparent-table {
|
||||||
|
.el-table__header th {
|
||||||
|
background: $table-bg-color !important;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__body tr {
|
||||||
|
background-color: $table-bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 300px;
|
||||||
|
margin-right: 10px;
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
background-color: transparent;
|
||||||
|
&:focus {
|
||||||
|
border-color: #409eff; // 保持聚焦状态下的边框颜色
|
||||||
|
}
|
||||||
|
//文字颜色
|
||||||
|
&::placeholder {
|
||||||
|
color: #606266;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-selection-header) {
|
||||||
|
.el-checkbox {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '选择';
|
||||||
|
display: inline-block;
|
||||||
|
color: black;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -31,7 +31,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;">
|
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: flex-start;box-sizing: border-box;">
|
||||||
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item" @configure="goToRoleConfig" @deviceManage="handleDeviceManage" />
|
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item"
|
||||||
|
@configure="goToRoleConfig"
|
||||||
|
@deviceManage="handleDeviceManage"
|
||||||
|
@delete="handleDeleteAgent"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
||||||
@@ -83,16 +87,37 @@ export default {
|
|||||||
fetchAgentList() {
|
fetchAgentList() {
|
||||||
import('@/apis/module/user').then(({ default: userApi }) => {
|
import('@/apis/module/user').then(({ default: userApi }) => {
|
||||||
userApi.getAgentList(({data}) => {
|
userApi.getAgentList(({data}) => {
|
||||||
this.originalDevices = data.data;
|
this.originalDevices = data.data.map(item => ({
|
||||||
this.devices = data.data;
|
...item,
|
||||||
|
agentId: item.id // 字段映射
|
||||||
|
}));
|
||||||
|
this.devices = this.originalDevices;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// 搜索更新智能体列表
|
// 搜索更新智能体列表
|
||||||
handleSearchResult(filteredList) {
|
handleSearchResult(filteredList) {
|
||||||
this.devices = filteredList; // 更新设备列表
|
this.devices = filteredList; // 更新设备列表
|
||||||
|
},
|
||||||
|
// 删除智能体
|
||||||
|
handleDeleteAgent(agentId) {
|
||||||
|
this.$confirm('确定要删除该智能体吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
import('@/apis/module/user').then(({ default: userApi }) => {
|
||||||
|
userApi.deleteAgent(agentId, (res) => {
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
this.$message.success('删除成功');
|
||||||
|
this.fetchAgentList(); // 刷新列表
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.data.msg || '删除失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<!-- 公共头部 -->
|
|
||||||
<HeaderBar/>
|
<HeaderBar/>
|
||||||
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
||||||
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
||||||
@@ -10,19 +9,19 @@
|
|||||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||||
<img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
|
<img src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;"/>
|
||||||
</div>
|
</div>
|
||||||
{{ deviceMac }}
|
{{ form.agentName }}
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;"/>
|
<div style="height: 1px;background: #e8f0ff;"/>
|
||||||
<el-form ref="form" :model="form" label-width="72px">
|
<el-form ref="form" :model="form" label-width="72px">
|
||||||
<div style="padding: 16px 24px;max-width: 792px;">
|
<div style="padding: 16px 24px;max-width: 792px;">
|
||||||
<el-form-item label="助手昵称:">
|
<el-form-item label="助手昵称:">
|
||||||
<div class="input-46" style="width: 100%; max-width: 412px;">
|
<div class="input-46" style="width: 100%; max-width: 412px;">
|
||||||
<el-input v-model="form.name"/>
|
<el-input v-model="form.agentName"/>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色模版:">
|
<el-form-item label="角色模版:">
|
||||||
<div style="display: flex;gap: 8px;">
|
<div style="display: flex;gap: 8px;">
|
||||||
<div v-for="template in templates" :key="template" class="template-item" @click="selectTemplate(template)">
|
<div v-for="template in templates" :key="template" class="template-item" :class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
||||||
{{ template }}
|
{{ template }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
<el-form-item label="角色音色:">
|
<el-form-item label="角色音色:">
|
||||||
<div style="display: flex;gap: 8px;align-items: center;">
|
<div style="display: flex;gap: 8px;align-items: center;">
|
||||||
<div class="input-46" style="flex:1.4;">
|
<div class="input-46" style="flex:1.4;">
|
||||||
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
|
<el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
|
||||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||||
:value="item.value">
|
:value="item.value">
|
||||||
</el-option>
|
</el-option>
|
||||||
@@ -45,14 +44,14 @@
|
|||||||
<el-form-item label="角色介绍:">
|
<el-form-item label="角色介绍:">
|
||||||
<div class="textarea-box">
|
<div class="textarea-box">
|
||||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
|
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
|
||||||
v-model="form.introduction" maxlength="2000" show-word-limit/>
|
v-model="form.systemPrompt" maxlength="2000" show-word-limit/>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="记忆体:">
|
<el-form-item label="记忆体:">
|
||||||
<div class="textarea-box">
|
<div class="textarea-box">
|
||||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
|
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容"
|
||||||
v-model="form.prompt" maxlength="1000"/>
|
v-model="form.langCode" maxlength="1000"/>
|
||||||
<div class="prompt-bottom">
|
<div class="prompt-bottom" @click="clearMemory">
|
||||||
<div style="display: flex;gap: 8px;align-items: center;">
|
<div style="display: flex;gap: 8px;align-items: center;">
|
||||||
<div style="color: #979db1;font-size: 11px;">当前记忆(每次对话后重新生成)</div>
|
<div style="color: #979db1;font-size: 11px;">当前记忆(每次对话后重新生成)</div>
|
||||||
<div class="clear-btn">
|
<div class="clear-btn">
|
||||||
@@ -60,7 +59,7 @@
|
|||||||
清除
|
清除
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="color: #979db1;font-size:11px;">{{ form.prompt.length }}/1000</div>
|
<div style="color: #979db1;font-size:11px;">{{ form.langCode.length }}/1000</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -104,41 +103,65 @@ export default {
|
|||||||
components: {HeaderBar},
|
components: {HeaderBar},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
deviceMac: 'CC:ba:97:11:a6:ac',
|
|
||||||
form: {
|
form: {
|
||||||
name: "",
|
agentCode: "",
|
||||||
timbre: "",
|
agentName: "",
|
||||||
introduction: "",
|
ttsVoiceId: "",
|
||||||
prompt: "",
|
systemPrompt: "",
|
||||||
|
langCode: "",
|
||||||
|
language: "",
|
||||||
|
sort: "",
|
||||||
model: {
|
model: {
|
||||||
tts: "",
|
ttsModelId: "",
|
||||||
vad: "",
|
vadModelId: "",
|
||||||
asr: "",
|
asrModelId: "",
|
||||||
llm: "",
|
llmModelId: "",
|
||||||
memory:"",
|
memModelId: "",
|
||||||
intent:""
|
intentModelId: "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
{ value: '选项1', label: '黄金糕' },
|
{value: '选项1', label: '黄金糕'},
|
||||||
{ value: '选项2', label: '双皮奶' }
|
{value: '选项2', label: '双皮奶'}
|
||||||
],
|
],
|
||||||
models: [
|
models: [
|
||||||
{ label: '大语言模型(LLM)', key: 'llm' },
|
{label: '大语言模型(LLM)', key: 'llmModelId'},
|
||||||
{ label: '语音转文本模型(ASR)', key: 'asr' },
|
{label: '语音识别(ASR)', key: 'asrModelId'},
|
||||||
{ label: '语音活动检测模型(VAD)', key: 'vad' },
|
{label: '语音活动检测模型(VAD)', key: 'vadModelId'},
|
||||||
{ label: '语音生成模型(TTS)', key: 'tts' },
|
{label: '语音合成模型(TTS)', key: 'ttsModelId'},
|
||||||
{ label: '意图分类模型(Intent)', key: 'intent' },
|
{label: '意图识别模型(Intent)', key: 'intentModelId'},
|
||||||
{ label: '记忆增强模型(Memory)', key: 'memory' }
|
{label: '记忆模型(Memory)', key: 'memModelId'}
|
||||||
],
|
],
|
||||||
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长']
|
templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长'],
|
||||||
|
loadingTemplate: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
saveConfig() {
|
saveConfig() {
|
||||||
// 此处写保存配置逻辑
|
const configData = {
|
||||||
this.$message.success('配置已保存')
|
agentCode: this.form.agentCode,
|
||||||
|
agentName: this.form.agentName,
|
||||||
|
asrModelId: this.form.model.asrModelId,
|
||||||
|
vadModelId: this.form.model.vadModelId,
|
||||||
|
llmModelId: this.form.model.llmModelId,
|
||||||
|
ttsModelId: this.form.model.ttsModelId,
|
||||||
|
ttsVoiceId: this.form.ttsVoiceId,
|
||||||
|
memModelId: this.form.model.memModelId,
|
||||||
|
intentModelId: this.form.model.intentModelId,
|
||||||
|
systemPrompt: this.form.systemPrompt,
|
||||||
|
langCode: this.form.langCode,
|
||||||
|
language: this.form.language,
|
||||||
|
sort: this.form.sort
|
||||||
|
};
|
||||||
|
import('@/apis/module/user').then(({default: userApi}) => {
|
||||||
|
userApi.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.$message.success('配置保存成功');
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg || '配置保存失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
resetConfig() {
|
resetConfig() {
|
||||||
this.$confirm('确定要重置配置吗?', '提示', {
|
this.$confirm('确定要重置配置吗?', '提示', {
|
||||||
@@ -148,20 +171,99 @@ export default {
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
// 重置表单
|
// 重置表单
|
||||||
this.form = {
|
this.form = {
|
||||||
name: "",
|
agentCode: "",
|
||||||
timbre: "",
|
agentName: "",
|
||||||
introduction: "",
|
ttsVoiceId: "",
|
||||||
prompt: "",
|
systemPrompt: "",
|
||||||
model: ""
|
langCode: "",
|
||||||
|
language: "",
|
||||||
|
sort: "",
|
||||||
|
model: {
|
||||||
|
ttsModelId: "",
|
||||||
|
vadModelId: "",
|
||||||
|
asrModelId: "",
|
||||||
|
llmModelId: "",
|
||||||
|
memModelId: "",
|
||||||
|
intentModelId: "",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.$message.success('配置已重置')
|
this.$message.success('配置已重置')
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// 处理选择模板的逻辑
|
selectTemplate(templateName) {
|
||||||
selectTemplate(template) {
|
if (this.loadingTemplate) return;
|
||||||
this.form.name = template;
|
|
||||||
this.$message.success(`已选择模板:${template}`);
|
this.loadingTemplate = true;
|
||||||
|
import('@/apis/module/user').then(({default: userApi}) => {
|
||||||
|
userApi.getAgentTemplate(
|
||||||
|
{templateName},
|
||||||
|
(response) => {
|
||||||
|
this.loadingTemplate = false;
|
||||||
|
if (response.data.code === 0 && response.data.data.length > 0) {
|
||||||
|
this.applyTemplateData(response.data.data[0]);
|
||||||
|
this.$message.success(`「${templateName}」模板已应用`);
|
||||||
|
} else {
|
||||||
|
this.$message.warning(`未找到「${templateName}」模板`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).catch((error) => {
|
||||||
|
this.loadingTemplate = false;
|
||||||
|
this.$message.error('模板加载失败');
|
||||||
|
console.error('接口异常:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyTemplateData(templateData) {
|
||||||
|
this.form = {
|
||||||
|
...this.form,
|
||||||
|
agentName: templateData.agentName || this.form.agentName,
|
||||||
|
ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
|
||||||
|
systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
|
||||||
|
langCode: templateData.langCode || this.form.langCode,
|
||||||
|
model: {
|
||||||
|
ttsModelId: templateData.ttsModelId || this.form.model.ttsModelId,
|
||||||
|
vadModelId: templateData.vadModelId || this.form.model.vadModelId,
|
||||||
|
asrModelId: templateData.asrModelId || this.form.model.asrModelId,
|
||||||
|
llmModelId: templateData.llmModelId || this.form.model.llmModelId,
|
||||||
|
memModelId: templateData.memModelId || this.form.model.memModelId,
|
||||||
|
intentModelId: templateData.intentModelId || this.form.model.intentModelId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fetchAgentConfig(agentId) {
|
||||||
|
import('@/apis/module/user').then(({default: userApi}) => {
|
||||||
|
userApi.getDeviceConfig(agentId, ({data}) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.form = {
|
||||||
|
...this.form,
|
||||||
|
...data.data,
|
||||||
|
model: {
|
||||||
|
ttsModelId: data.data.ttsModelId,
|
||||||
|
vadModelId: data.data.vadModelId,
|
||||||
|
asrModelId: data.data.asrModelId,
|
||||||
|
llmModelId: data.data.llmModelId,
|
||||||
|
memModelId: data.data.memModelId,
|
||||||
|
intentModelId: data.data.intentModelId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg || '获取配置失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 清空记忆体内容
|
||||||
|
clearMemory() {
|
||||||
|
this.form.langCode = "";
|
||||||
|
this.$message.success("记忆体已清空");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const agentId = this.$route.query.agentId;
|
||||||
|
console.log('agentId2222',agentId);
|
||||||
|
if (agentId) {
|
||||||
|
this.fetchAgentConfig(agentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ server:
|
|||||||
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
|
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
|
||||||
log:
|
log:
|
||||||
# 设置控制台输出的日志格式,时间、日志级别、标签、消息
|
# 设置控制台输出的日志格式,时间、日志级别、标签、消息
|
||||||
log_format: "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>"
|
log_format: "<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>"
|
||||||
# 设置日志文件输出的格式,时间、日志级别、标签、消息
|
# 设置日志文件输出的格式,时间、日志级别、标签、消息
|
||||||
log_format_simple: "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}"
|
log_format_file: "{time:YYYY-MM-DD HH:mm:ss} - {version}_{selected_module} - {name} - {level} - {extra[tag]} - {message}"
|
||||||
# 设置日志等级:INFO、DEBUG
|
# 设置日志等级:INFO、DEBUG
|
||||||
log_level: INFO
|
log_level: INFO
|
||||||
# 设置日志路径
|
# 设置日志路径
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -57,6 +57,14 @@ delete_audio: true
|
|||||||
close_connection_no_voice_time: 120
|
close_connection_no_voice_time: 120
|
||||||
# TTS请求超时时间(秒)
|
# TTS请求超时时间(秒)
|
||||||
tts_timeout: 10
|
tts_timeout: 10
|
||||||
|
# 开启唤醒词加速
|
||||||
|
enable_wakeup_words_response_cache: true
|
||||||
|
# 开场是否回复唤醒词
|
||||||
|
enable_greeting: true
|
||||||
|
# 说完话是否开启提示音
|
||||||
|
enable_stop_tts_notify: false
|
||||||
|
# 说完话是否开启提示音,音效地址
|
||||||
|
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
|
||||||
|
|
||||||
CMD_exit:
|
CMD_exit:
|
||||||
- "退出"
|
- "退出"
|
||||||
@@ -74,11 +82,12 @@ selected_module:
|
|||||||
TTS: EdgeTTS
|
TTS: EdgeTTS
|
||||||
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
|
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
|
||||||
Memory: nomem
|
Memory: nomem
|
||||||
# 意图识别模块,默认不开启。开启后,可以播放音乐、控制音量、识别退出指令
|
# 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令。
|
||||||
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
|
# 不想开通意图识别,就设置成:nointent
|
||||||
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
|
# 意图识别可使用intent_llm,如果你的LLM是DifyLLM或CozeLLM,建议使用这个。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
|
||||||
# 如果意图识别设置成 function_call,建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
# 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
|
||||||
Intent: nointent
|
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
||||||
|
Intent: function_call
|
||||||
|
|
||||||
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
|
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
|
||||||
Intent:
|
Intent:
|
||||||
@@ -89,15 +98,26 @@ Intent:
|
|||||||
intent_llm:
|
intent_llm:
|
||||||
# 不需要动type
|
# 不需要动type
|
||||||
type: intent_llm
|
type: intent_llm
|
||||||
|
# 配备意图识别独立的思考模型
|
||||||
|
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
|
||||||
|
# 如果你的selected_module.LLM选择了DifyLLM或CozeLLM,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
|
||||||
|
llm: ChatGLMLLM
|
||||||
function_call:
|
function_call:
|
||||||
# 不需要动type
|
# 不需要动type
|
||||||
type: nointent
|
type: function_call
|
||||||
# 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
|
||||||
|
# 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:
|
||||||
@@ -106,6 +126,28 @@ 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"
|
||||||
|
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:
|
||||||
@@ -156,6 +198,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
|
||||||
@@ -220,12 +274,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
|
||||||
@@ -235,6 +283,18 @@ LLM:
|
|||||||
variables:
|
variables:
|
||||||
k: "v"
|
k: "v"
|
||||||
k2: "v2"
|
k2: "v2"
|
||||||
|
XinferenceLLM:
|
||||||
|
# 定义LLM API类型
|
||||||
|
type: xinference
|
||||||
|
# Xinference服务地址和模型名称
|
||||||
|
model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型
|
||||||
|
base_url: http://localhost:9997 # Xinference服务地址
|
||||||
|
XinferenceSmallLLM:
|
||||||
|
# 定义轻量级LLM API类型,用于意图识别
|
||||||
|
type: xinference
|
||||||
|
# Xinference服务地址和模型名称
|
||||||
|
model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别
|
||||||
|
base_url: http://localhost:9997 # Xinference服务地址
|
||||||
TTS:
|
TTS:
|
||||||
# 当前支持的type为edge、doubao,可自行适配
|
# 当前支持的type为edge、doubao,可自行适配
|
||||||
EdgeTTS:
|
EdgeTTS:
|
||||||
@@ -480,19 +540,15 @@ module_test:
|
|||||||
- "What's the weather like today?"
|
- "What's the weather like today?"
|
||||||
- "请用100字概括量子计算的基本原理和应用前景"
|
- "请用100字概括量子计算的基本原理和应用前景"
|
||||||
|
|
||||||
# 本地音乐播放配置
|
# 唤醒词,用于识别唤醒词还是讲话内容
|
||||||
music:
|
wakeup_words:
|
||||||
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
|
|
||||||
|
|||||||
@@ -3,12 +3,24 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.settings import load_config
|
from config.settings import load_config
|
||||||
|
|
||||||
|
SERVER_VERSION = "0.1.16"
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
log_config = config["log"]
|
log_config = config["log"]
|
||||||
log_format = log_config.get("log_format", "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>")
|
log_format = log_config.get("log_format", "<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>")
|
||||||
log_format_simple = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}")
|
log_format_file = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}")
|
||||||
|
|
||||||
|
selected_module = config.get("selected_module")
|
||||||
|
selected_module_str = ''.join([value[0] + value[1] for key, value in selected_module.items()])
|
||||||
|
|
||||||
|
log_format = log_format.replace("{version}", SERVER_VERSION)
|
||||||
|
log_format = log_format.replace("{selected_module}", selected_module_str)
|
||||||
|
log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
|
||||||
|
log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
|
||||||
|
|
||||||
|
|
||||||
log_level = log_config.get("log_level", "INFO")
|
log_level = log_config.get("log_level", "INFO")
|
||||||
log_dir = log_config.get("log_dir", "tmp")
|
log_dir = log_config.get("log_dir", "tmp")
|
||||||
log_file = log_config.get("log_file", "server.log")
|
log_file = log_config.get("log_file", "server.log")
|
||||||
@@ -24,6 +36,6 @@ def setup_logging():
|
|||||||
logger.add(sys.stdout, format=log_format, level=log_level)
|
logger.add(sys.stdout, format=log_format, level=log_level)
|
||||||
|
|
||||||
# 输出到文件
|
# 输出到文件
|
||||||
logger.add(os.path.join(log_dir, log_file), format=log_format_simple, level=log_level)
|
logger.add(os.path.join(log_dir, log_file), format=log_format_file, level=log_level)
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import traceback
|
|||||||
import threading
|
import threading
|
||||||
import websockets
|
import websockets
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import plugins_func.loadplugins
|
from plugins_func.loadplugins import auto_import_modules
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.utils.dialogue import Message, Dialogue
|
from core.utils.dialogue import Message, Dialogue
|
||||||
from core.handle.textHandle import handleTextMessage
|
from core.handle.textHandle import handleTextMessage
|
||||||
@@ -18,13 +18,16 @@ from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
|||||||
from core.handle.sendAudioHandle import sendAudioMessage
|
from core.handle.sendAudioHandle import sendAudioMessage
|
||||||
from core.handle.receiveAudioHandle import handleAudioMessage
|
from core.handle.receiveAudioHandle import handleAudioMessage
|
||||||
from core.handle.functionHandler import FunctionHandler
|
from core.handle.functionHandler import FunctionHandler
|
||||||
from plugins_func.register import Action
|
from plugins_func.register import Action, ActionResponse
|
||||||
from config.private_config import PrivateConfig
|
from config.private_config import PrivateConfig
|
||||||
from core.auth import AuthMiddleware, AuthenticationError
|
from core.auth import AuthMiddleware, AuthenticationError
|
||||||
from core.utils.auth_code_gen import AuthCodeGenerator
|
from core.utils.auth_code_gen import AuthCodeGenerator
|
||||||
|
from core.mcp.manager import MCPManager
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
|
auto_import_modules('plugins_func.functions')
|
||||||
|
|
||||||
|
|
||||||
class TTSException(RuntimeError):
|
class TTSException(RuntimeError):
|
||||||
pass
|
pass
|
||||||
@@ -98,6 +101,8 @@ class ConnectionHandler:
|
|||||||
self.use_function_call_mode = False
|
self.use_function_call_mode = False
|
||||||
if self.config["selected_module"]["Intent"] == 'function_call':
|
if self.config["selected_module"]["Intent"] == 'function_call':
|
||||||
self.use_function_call_mode = True
|
self.use_function_call_mode = True
|
||||||
|
|
||||||
|
self.mcp_manager = MCPManager(self)
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
async def handle_connection(self, ws):
|
||||||
try:
|
try:
|
||||||
@@ -109,11 +114,15 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
# 进行认证
|
# 进行认证
|
||||||
await self.auth.authenticate(self.headers)
|
await self.auth.authenticate(self.headers)
|
||||||
|
|
||||||
device_id = self.headers.get("device-id", None)
|
device_id = self.headers.get("device-id", None)
|
||||||
self.memory.init_memory(device_id, self.llm)
|
|
||||||
self.intent.set_llm(self.llm)
|
|
||||||
|
|
||||||
|
# 认证通过,继续处理
|
||||||
|
self.websocket = ws
|
||||||
|
self.session_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
self.welcome_msg = self.config["xiaozhi"]
|
||||||
|
self.welcome_msg["session_id"] = self.session_id
|
||||||
|
await self.websocket.send(json.dumps(self.welcome_msg))
|
||||||
# Load private configuration if device_id is provided
|
# Load private configuration if device_id is provided
|
||||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||||
self.logger.bind(tag=TAG).info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
|
self.logger.bind(tag=TAG).info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
|
||||||
@@ -141,43 +150,32 @@ class ConnectionHandler:
|
|||||||
self.private_config = None
|
self.private_config = None
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# 认证通过,继续处理
|
|
||||||
self.websocket = ws
|
|
||||||
self.session_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
self.welcome_msg = self.config["xiaozhi"]
|
|
||||||
self.welcome_msg["session_id"] = self.session_id
|
|
||||||
await self.websocket.send(json.dumps(self.welcome_msg))
|
|
||||||
|
|
||||||
# 异步初始化
|
# 异步初始化
|
||||||
await self.loop.run_in_executor(None, self._initialize_components)
|
self.executor.submit(self._initialize_components)
|
||||||
|
|
||||||
# tts 消化线程
|
# tts 消化线程
|
||||||
tts_priority = threading.Thread(target=self._tts_priority_thread, daemon=True)
|
self.tts_priority_thread = threading.Thread(target=self._tts_priority_thread, daemon=True)
|
||||||
tts_priority.start()
|
self.tts_priority_thread.start()
|
||||||
|
|
||||||
# 音频播放 消化线程
|
# 音频播放 消化线程
|
||||||
audio_play_priority = threading.Thread(target=self._audio_play_priority_thread, daemon=True)
|
self.audio_play_priority_thread = threading.Thread(target=self._audio_play_priority_thread, daemon=True)
|
||||||
audio_play_priority.start()
|
self.audio_play_priority_thread.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for message in self.websocket:
|
async for message in self.websocket:
|
||||||
await self._route_message(message)
|
await self._route_message(message)
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
self.logger.bind(tag=TAG).info("客户端断开连接")
|
self.logger.bind(tag=TAG).info("客户端断开连接")
|
||||||
await self.close()
|
|
||||||
|
|
||||||
except AuthenticationError as e:
|
except AuthenticationError as e:
|
||||||
self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}")
|
self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}")
|
||||||
await ws.close()
|
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
stack_trace = traceback.format_exc()
|
stack_trace = traceback.format_exc()
|
||||||
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
||||||
await ws.close()
|
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
await self.memory.save_memory(self.dialogue.dialogue)
|
await self.memory.save_memory(self.dialogue.dialogue)
|
||||||
|
await self.close(ws)
|
||||||
|
|
||||||
async def _route_message(self, message):
|
async def _route_message(self, message):
|
||||||
"""消息路由"""
|
"""消息路由"""
|
||||||
@@ -187,16 +185,53 @@ class ConnectionHandler:
|
|||||||
await handleAudioMessage(self, message)
|
await handleAudioMessage(self, message)
|
||||||
|
|
||||||
def _initialize_components(self):
|
def _initialize_components(self):
|
||||||
|
"""加载提示词"""
|
||||||
self.prompt = self.config["prompt"]
|
self.prompt = self.config["prompt"]
|
||||||
if self.private_config:
|
if self.private_config:
|
||||||
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
||||||
|
|
||||||
self.client_ip_info = get_ip_info(self.client_ip)
|
|
||||||
self.logger.bind(tag=TAG).info(f"Client ip info: {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)
|
||||||
|
|
||||||
|
"""加载记忆"""
|
||||||
|
device_id = self.headers.get("device-id", None)
|
||||||
|
self.memory.init_memory(device_id, self.llm)
|
||||||
|
|
||||||
|
"""为意图识别设置LLM,优先使用专用LLM"""
|
||||||
|
# 检查是否配置了专用的意图识别LLM
|
||||||
|
intent_llm_name = self.config["Intent"]["intent_llm"]["llm"]
|
||||||
|
|
||||||
|
# 记录开始初始化意图识别LLM的时间
|
||||||
|
intent_llm_init_start = time.time()
|
||||||
|
|
||||||
|
if not self.use_function_call_mode and intent_llm_name and intent_llm_name in self.config["LLM"]:
|
||||||
|
# 如果配置了专用LLM,则创建独立的LLM实例
|
||||||
|
from core.utils import llm as llm_utils
|
||||||
|
intent_llm_config = self.config["LLM"][intent_llm_name]
|
||||||
|
intent_llm_type = intent_llm_config.get("type", intent_llm_name)
|
||||||
|
intent_llm = llm_utils.create_instance(intent_llm_type, intent_llm_config)
|
||||||
|
self.logger.bind(tag=TAG).info(f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}")
|
||||||
|
|
||||||
|
self.intent.set_llm(intent_llm)
|
||||||
|
else:
|
||||||
|
# 否则使用主LLM
|
||||||
|
self.intent.set_llm(self.llm)
|
||||||
|
self.logger.bind(tag=TAG).info("意图识别使用主LLM")
|
||||||
|
|
||||||
|
# 记录意图识别LLM初始化耗时
|
||||||
|
intent_llm_init_time = time.time() - intent_llm_init_start
|
||||||
|
self.logger.bind(tag=TAG).info(f"意图识别LLM初始化完成,耗时: {intent_llm_init_time:.4f}秒")
|
||||||
|
|
||||||
|
"""加载位置信息"""
|
||||||
|
self.client_ip_info = get_ip_info(self.client_ip)
|
||||||
|
if self.client_ip_info is not None and "city" in self.client_ip_info:
|
||||||
|
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
|
||||||
|
self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
|
||||||
|
self.dialogue.update_system_message(self.prompt)
|
||||||
|
|
||||||
|
"""加载MCP工具"""
|
||||||
|
asyncio.run_coroutine_threadsafe(self.mcp_manager.initialize_servers(), self.loop)
|
||||||
|
|
||||||
def change_system_prompt(self, prompt):
|
def change_system_prompt(self, prompt):
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
@@ -316,8 +351,9 @@ class ConnectionHandler:
|
|||||||
self.dialogue.put(Message(role="user", content=query))
|
self.dialogue.put(Message(role="user", content=query))
|
||||||
|
|
||||||
# Define intent functions
|
# Define intent functions
|
||||||
functions = self.func_handler.get_functions()
|
functions = None
|
||||||
|
if hasattr(self, 'func_handler'):
|
||||||
|
functions = self.func_handler.get_functions()
|
||||||
response_message = []
|
response_message = []
|
||||||
processed_chars = 0 # 跟踪已处理的字符位置
|
processed_chars = 0 # 跟踪已处理的字符位置
|
||||||
|
|
||||||
@@ -351,6 +387,9 @@ class ConnectionHandler:
|
|||||||
content_arguments = ""
|
content_arguments = ""
|
||||||
for response in llm_responses:
|
for response in llm_responses:
|
||||||
content, tools_call = response
|
content, tools_call = response
|
||||||
|
if "content" in response:
|
||||||
|
content = response["content"]
|
||||||
|
tools_call = None
|
||||||
if content is not None and len(content) > 0:
|
if content is not None and len(content) > 0:
|
||||||
if len(response_message) <= 0 and (content == "```" or "<tool_call>" in content):
|
if len(response_message) <= 0 and (content == "```" or "<tool_call>" in content):
|
||||||
tool_call_flag = True
|
tool_call_flag = True
|
||||||
@@ -429,8 +468,14 @@ class ConnectionHandler:
|
|||||||
"id": function_id,
|
"id": function_id,
|
||||||
"arguments": function_arguments
|
"arguments": function_arguments
|
||||||
}
|
}
|
||||||
result = self.func_handler.handle_llm_function_call(self, function_call_data)
|
|
||||||
self._handle_function_result(result, function_call_data, text_index + 1)
|
# 处理MCP工具调用
|
||||||
|
if self.mcp_manager.is_mcp_tool(function_name):
|
||||||
|
result = self._handle_mcp_tool_call(function_call_data)
|
||||||
|
else:
|
||||||
|
# 处理系统函数
|
||||||
|
result = self.func_handler.handle_llm_function_call(self, function_call_data)
|
||||||
|
self._handle_function_result(result, function_call_data, text_index+1)
|
||||||
|
|
||||||
# 处理最后剩余的文本
|
# 处理最后剩余的文本
|
||||||
full_text = "".join(response_message)
|
full_text = "".join(response_message)
|
||||||
@@ -452,6 +497,42 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _handle_mcp_tool_call(self, function_call_data):
|
||||||
|
function_arguments = function_call_data["arguments"]
|
||||||
|
function_name = function_call_data["name"]
|
||||||
|
try:
|
||||||
|
args_dict = function_arguments
|
||||||
|
if isinstance(function_arguments, str):
|
||||||
|
try:
|
||||||
|
args_dict = json.loads(function_arguments)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.logger.bind(tag=TAG).error(f"无法解析 function_arguments: {function_arguments}")
|
||||||
|
return ActionResponse(action=Action.REQLLM, result="参数解析失败", response="")
|
||||||
|
|
||||||
|
tool_result = asyncio.run_coroutine_threadsafe(self.mcp_manager.execute_tool(
|
||||||
|
function_name,
|
||||||
|
args_dict
|
||||||
|
), self.loop).result()
|
||||||
|
# meta=None content=[TextContent(type='text', text='北京当前天气:\n温度: 21°C\n天气: 晴\n湿度: 6%\n风向: 西北 风\n风力等级: 5级', annotations=None)] isError=False
|
||||||
|
content_text = ""
|
||||||
|
if tool_result is not None and tool_result.content is not None:
|
||||||
|
for content in tool_result.content:
|
||||||
|
content_type = content.type
|
||||||
|
if content_type == "text":
|
||||||
|
content_text = content.text
|
||||||
|
elif content_type == "image":
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(content_text) > 0:
|
||||||
|
return ActionResponse(action=Action.REQLLM, result=content_text, response="")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}")
|
||||||
|
return ActionResponse(action=Action.REQLLM, result="工具调用出错", response="")
|
||||||
|
|
||||||
|
return ActionResponse(action=Action.REQLLM, result="工具调用出错", response="")
|
||||||
|
|
||||||
|
|
||||||
def _handle_function_result(self, result, function_call_data, text_index):
|
def _handle_function_result(self, result, function_call_data, text_index):
|
||||||
if result.action == Action.RESPONSE: # 直接回复前端
|
if result.action == Action.RESPONSE: # 直接回复前端
|
||||||
text = result.response
|
text = result.response
|
||||||
@@ -492,7 +573,12 @@ class ConnectionHandler:
|
|||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
text = None
|
text = None
|
||||||
try:
|
try:
|
||||||
future = self.tts_queue.get()
|
try:
|
||||||
|
future = self.tts_queue.get(timeout=1)
|
||||||
|
except queue.Empty:
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
break
|
||||||
|
continue
|
||||||
if future is None:
|
if future is None:
|
||||||
continue
|
continue
|
||||||
text = None
|
text = None
|
||||||
@@ -533,7 +619,12 @@ class ConnectionHandler:
|
|||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
text = None
|
text = None
|
||||||
try:
|
try:
|
||||||
opus_datas, text, text_index = self.audio_play_queue.get()
|
try:
|
||||||
|
opus_datas, text, text_index = self.audio_play_queue.get(timeout=1)
|
||||||
|
except queue.Empty:
|
||||||
|
if self.stop_event.is_set():
|
||||||
|
break
|
||||||
|
continue
|
||||||
future = asyncio.run_coroutine_threadsafe(sendAudioMessage(self, opus_datas, text, text_index),
|
future = asyncio.run_coroutine_threadsafe(sendAudioMessage(self, opus_datas, text, text_index),
|
||||||
self.loop)
|
self.loop)
|
||||||
future.result()
|
future.result()
|
||||||
@@ -563,16 +654,43 @@ class ConnectionHandler:
|
|||||||
self.tts_first_text_index = text_index
|
self.tts_first_text_index = text_index
|
||||||
self.tts_last_text_index = text_index
|
self.tts_last_text_index = text_index
|
||||||
|
|
||||||
async def close(self):
|
async def close(self, ws=None):
|
||||||
"""资源清理方法"""
|
"""资源清理方法"""
|
||||||
|
# 清理MCP资源
|
||||||
|
await self.mcp_manager.cleanup_all()
|
||||||
|
|
||||||
# 清理其他资源
|
# 触发停止事件并清理资源
|
||||||
self.stop_event.set()
|
if self.stop_event:
|
||||||
self.executor.shutdown(wait=False)
|
self.stop_event.set()
|
||||||
if self.websocket:
|
|
||||||
|
# 立即关闭线程池
|
||||||
|
if self.executor:
|
||||||
|
self.executor.shutdown(wait=False, cancel_futures=True)
|
||||||
|
self.executor = None
|
||||||
|
|
||||||
|
# 清空任务队列
|
||||||
|
self._clear_queues()
|
||||||
|
|
||||||
|
if ws:
|
||||||
|
await ws.close()
|
||||||
|
elif self.websocket:
|
||||||
await self.websocket.close()
|
await self.websocket.close()
|
||||||
self.logger.bind(tag=TAG).info("连接资源已释放")
|
self.logger.bind(tag=TAG).info("连接资源已释放")
|
||||||
|
|
||||||
|
def _clear_queues(self):
|
||||||
|
# 清空所有任务队列
|
||||||
|
for q in [self.tts_queue, self.audio_play_queue]:
|
||||||
|
if not q:
|
||||||
|
continue
|
||||||
|
while not q.empty():
|
||||||
|
try:
|
||||||
|
q.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
q.queue.clear()
|
||||||
|
# 添加毒丸信号到队列,确保线程退出
|
||||||
|
# q.queue.put(None)
|
||||||
|
|
||||||
def reset_vad_states(self):
|
def reset_vad_states(self):
|
||||||
self.client_audio_buffer = bytes()
|
self.client_audio_buffer = bytes()
|
||||||
self.client_have_voice = False
|
self.client_have_voice = False
|
||||||
|
|||||||
@@ -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,16 +48,19 @@ 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("get_lunar")
|
||||||
|
self.function_registry.register_function("handle_device")
|
||||||
|
|
||||||
def register_config_functions(self):
|
def register_config_functions(self):
|
||||||
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||||
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 +85,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
|
||||||
|
|||||||
@@ -1,8 +1,74 @@
|
|||||||
import json
|
import json
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
|
from core.utils.util import remove_punctuation_and_length
|
||||||
|
import shutil
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
WAKEUP_CONFIG = {"dir": "config/assets/", "file_name": "wakeup_words", "create_time": time.time(), "refresh_time": 10,
|
||||||
|
"words": ["很高兴见到你", "你好啊", "我们又见面了", "最近可好?", "很高兴再次和你谈话", "在干嘛"]}
|
||||||
|
|
||||||
|
|
||||||
async def handleHelloMessage(conn):
|
async def handleHelloMessage(conn):
|
||||||
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
||||||
|
|
||||||
|
|
||||||
|
async def checkWakeupWords(conn, text):
|
||||||
|
enable_wakeup_words_response_cache = conn.config["enable_wakeup_words_response_cache"]
|
||||||
|
"""是否开启唤醒词加速"""
|
||||||
|
if not enable_wakeup_words_response_cache:
|
||||||
|
return False
|
||||||
|
"""检查是否是唤醒词"""
|
||||||
|
_, text = remove_punctuation_and_length(text)
|
||||||
|
if text in conn.config.get("wakeup_words"):
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
conn.tts_first_text_index = 0
|
||||||
|
conn.tts_last_text_index = 0
|
||||||
|
conn.llm_finish_task = True
|
||||||
|
|
||||||
|
file = getWakeupWordFile(WAKEUP_CONFIG["file_name"])
|
||||||
|
if file is None:
|
||||||
|
asyncio.create_task(wakeupWordsResponse(conn))
|
||||||
|
return False
|
||||||
|
opus_packets, duration = conn.tts.audio_to_opus_data(file)
|
||||||
|
conn.audio_play_queue.put((opus_packets, text, 0))
|
||||||
|
if time.time() - WAKEUP_CONFIG["create_time"] > WAKEUP_CONFIG["refresh_time"]:
|
||||||
|
asyncio.create_task(wakeupWordsResponse(conn))
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def getWakeupWordFile(file_name):
|
||||||
|
for file in os.listdir(WAKEUP_CONFIG["dir"]):
|
||||||
|
if file.startswith("my_" + file_name):
|
||||||
|
"""避免缓存文件是一个空文件"""
|
||||||
|
if os.stat(f"config/assets/{file}").st_size > (5 * 1024):
|
||||||
|
return f"config/assets/{file}"
|
||||||
|
|
||||||
|
"""查找config/assets/目录下名称为wakeup_words的文件"""
|
||||||
|
for file in os.listdir(WAKEUP_CONFIG["dir"]):
|
||||||
|
if file.startswith(file_name):
|
||||||
|
return f"config/assets/{file}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def wakeupWordsResponse(conn):
|
||||||
|
"""唤醒词响应"""
|
||||||
|
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
||||||
|
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
|
||||||
|
tts_file = await asyncio.to_thread(conn.tts.to_tts, result)
|
||||||
|
if tts_file is not None and os.path.exists(tts_file):
|
||||||
|
file_type = os.path.splitext(tts_file)[1]
|
||||||
|
if file_type:
|
||||||
|
file_type = file_type.lstrip('.')
|
||||||
|
old_file = getWakeupWordFile("my_" + WAKEUP_CONFIG["file_name"])
|
||||||
|
if old_file is not None:
|
||||||
|
os.remove(old_file)
|
||||||
|
"""将文件挪到"wakeup_words.mp3"""
|
||||||
|
shutil.move(tts_file, WAKEUP_CONFIG["dir"] + "my_" + WAKEUP_CONFIG["file_name"] + "." + file_type)
|
||||||
|
WAKEUP_CONFIG["create_time"] = time.time()
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ from config.logger import setup_logging
|
|||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
|
from core.handle.helloHandle import checkWakeupWords
|
||||||
from core.utils.util import remove_punctuation_and_length
|
from core.utils.util import remove_punctuation_and_length
|
||||||
|
from core.utils.dialogue import Message
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -12,15 +15,19 @@ async def handle_user_intent(conn, text):
|
|||||||
# 检查是否有明确的退出命令
|
# 检查是否有明确的退出命令
|
||||||
if await check_direct_exit(conn, text):
|
if await check_direct_exit(conn, text):
|
||||||
return True
|
return True
|
||||||
|
# 检查是否是唤醒词
|
||||||
|
if await checkWakeupWords(conn, text):
|
||||||
|
return True
|
||||||
|
|
||||||
if conn.use_function_call_mode:
|
if conn.use_function_call_mode:
|
||||||
# 使用支持function calling的聊天方法,不再进行意图分析
|
# 使用支持function calling的聊天方法,不再进行意图分析
|
||||||
return False
|
return False
|
||||||
# 使用LLM进行意图分析
|
# 使用LLM进行意图分析
|
||||||
intent = await analyze_intent_with_llm(conn, text)
|
intent_result = await analyze_intent_with_llm(conn, text)
|
||||||
if not intent:
|
if not intent_result:
|
||||||
return False
|
return False
|
||||||
# 处理各种意图
|
# 处理各种意图
|
||||||
return await process_intent_result(conn, intent, text)
|
return await process_intent_result(conn, intent_result, text)
|
||||||
|
|
||||||
|
|
||||||
async def check_direct_exit(conn, text):
|
async def check_direct_exit(conn, text):
|
||||||
@@ -45,52 +52,65 @@ async def analyze_intent_with_llm(conn, text):
|
|||||||
dialogue = conn.dialogue
|
dialogue = conn.dialogue
|
||||||
try:
|
try:
|
||||||
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
|
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
|
||||||
# 尝试解析JSON结果
|
return intent_result
|
||||||
try:
|
|
||||||
intent_data = json.loads(intent_result)
|
|
||||||
if "intent" in intent_data:
|
|
||||||
return intent_data["intent"]
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# 如果不是JSON格式,尝试直接获取意图文本
|
|
||||||
return intent_result.strip()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
|
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def process_intent_result(conn, intent, original_text):
|
async def process_intent_result(conn, intent_result, original_text):
|
||||||
"""处理意图识别结果"""
|
"""处理意图识别结果"""
|
||||||
# 处理退出意图
|
try:
|
||||||
if "结束聊天" in intent:
|
# 尝试将结果解析为JSON
|
||||||
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
|
intent_data = json.loads(intent_result)
|
||||||
# 如果是明确的离别意图,发送告别语并关闭连接
|
|
||||||
await send_stt_message(conn, original_text)
|
|
||||||
conn.executor.submit(conn.chat_and_close, original_text)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 处理播放音乐意图
|
# 检查是否有function_call
|
||||||
if "播放音乐" in intent:
|
if "function_call" in intent_data:
|
||||||
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}")
|
# 直接从意图识别获取了function_call
|
||||||
# 调用play_music函数来播放音乐
|
logger.bind(tag=TAG).info(f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}")
|
||||||
song_name = extract_text_in_brackets(intent)
|
function_name = intent_data["function_call"]["name"]
|
||||||
function_id = str(uuid.uuid4().hex)
|
if function_name == "continue_chat":
|
||||||
function_name = "play_music"
|
return False
|
||||||
function_arguments = '{ "song_name": "' + song_name + '" }'
|
function_args = None
|
||||||
|
if "arguments" in intent_data["function_call"]:
|
||||||
|
function_args = intent_data["function_call"]["arguments"]
|
||||||
|
# 确保参数是字符串格式的JSON
|
||||||
|
if isinstance(function_args, dict):
|
||||||
|
function_args = json.dumps(function_args)
|
||||||
|
|
||||||
function_call_data = {
|
function_call_data = {
|
||||||
"name": function_name,
|
"name": function_name,
|
||||||
"id": function_id,
|
"id": str(uuid.uuid4().hex),
|
||||||
"arguments": function_arguments
|
"arguments": function_args
|
||||||
}
|
}
|
||||||
conn.func_handler.handle_llm_function_call(conn, function_call_data)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 其他意图处理可以在这里扩展
|
await send_stt_message(conn, original_text)
|
||||||
|
|
||||||
# 默认返回False,表示继续常规聊天流程
|
# 使用executor执行函数调用和结果处理
|
||||||
return False
|
def process_function_call():
|
||||||
|
conn.dialogue.put(Message(role="user", content=original_text))
|
||||||
|
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
|
||||||
|
if result and function_name != 'play_music':
|
||||||
|
# 获取当前最新的文本索引
|
||||||
|
text = result.response
|
||||||
|
if text is None:
|
||||||
|
text = result.result
|
||||||
|
if text is not None:
|
||||||
|
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
|
||||||
|
conn.recode_first_last_text(text, text_index)
|
||||||
|
future = conn.executor.submit(conn.speak_and_play, text, text_index)
|
||||||
|
conn.llm_finish_task = True
|
||||||
|
conn.tts_queue.put(future)
|
||||||
|
conn.dialogue.put(Message(role="assistant", content=text))
|
||||||
|
|
||||||
|
# 将函数执行放在线程池中
|
||||||
|
conn.executor.submit(process_function_call)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def extract_text_in_brackets(s):
|
def extract_text_in_brackets(s):
|
||||||
@@ -106,4 +126,4 @@ def extract_text_in_brackets(s):
|
|||||||
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
|
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
|
||||||
return s[left_bracket_index + 1:right_bracket_index]
|
return s[left_bracket_index + 1:right_bracket_index]
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -7,40 +7,15 @@ from core.utils.util import remove_punctuation_and_length, get_string_no_punctua
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
async def sendAudioMessage(conn, audios, text, text_index=0):
|
async def sendAudioMessage(conn, audios, text, text_index=0):
|
||||||
# 发送句子开始消息
|
# 发送句子开始消息
|
||||||
if text_index == conn.tts_first_text_index:
|
if text_index == conn.tts_first_text_index:
|
||||||
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
||||||
await send_tts_message(conn, "sentence_start", text)
|
await send_tts_message(conn, "sentence_start", text)
|
||||||
|
|
||||||
# 流控参数优化
|
# 播放音频
|
||||||
original_frame_duration = 60 # 原始帧时长(毫秒)
|
await sendAudio(conn, audios)
|
||||||
adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
|
|
||||||
total_frames = len(audios) # 获取总帧数
|
|
||||||
compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒)
|
|
||||||
|
|
||||||
start_time = time.perf_counter()
|
|
||||||
play_position = 0 # 已播放时长(毫秒)
|
|
||||||
|
|
||||||
for opus_packet in audios:
|
|
||||||
if conn.client_abort:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 计算带加速因子的预期时间
|
|
||||||
expected_time = start_time + (play_position / 1000)
|
|
||||||
current_time = time.perf_counter()
|
|
||||||
|
|
||||||
# 流控等待(使用加速后的帧时长)
|
|
||||||
delay = expected_time - current_time
|
|
||||||
if delay > 0:
|
|
||||||
await asyncio.sleep(delay)
|
|
||||||
|
|
||||||
await conn.websocket.send(opus_packet)
|
|
||||||
play_position += adjusted_frame_duration # 使用调整后的帧时长
|
|
||||||
|
|
||||||
# 补偿因加速损失的时长
|
|
||||||
if compensation > 0:
|
|
||||||
await asyncio.sleep(compensation)
|
|
||||||
|
|
||||||
await send_tts_message(conn, "sentence_end", text)
|
await send_tts_message(conn, "sentence_end", text)
|
||||||
|
|
||||||
@@ -50,6 +25,38 @@ async def sendAudioMessage(conn, audios, text, text_index=0):
|
|||||||
if conn.close_after_chat:
|
if conn.close_after_chat:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
|
# 播放音频
|
||||||
|
async def sendAudio(conn, audios):
|
||||||
|
# 流控参数优化
|
||||||
|
frame_duration = 60 # 帧时长(毫秒),匹配 Opus 编码
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
play_position = 0
|
||||||
|
|
||||||
|
# 预缓冲:发送前 3 帧
|
||||||
|
pre_buffer = min(3, len(audios))
|
||||||
|
for i in range(pre_buffer):
|
||||||
|
await conn.websocket.send(audios[i])
|
||||||
|
conn.logger.bind(tag=TAG).debug(f"预缓冲帧 {i}, 时间: {(time.perf_counter() - start_time) * 1000:.2f}ms")
|
||||||
|
|
||||||
|
# 正常播放剩余帧
|
||||||
|
for opus_packet in audios[pre_buffer:]:
|
||||||
|
if conn.client_abort:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 计算预期发送时间
|
||||||
|
expected_time = start_time + (play_position / 1000)
|
||||||
|
current_time = time.perf_counter()
|
||||||
|
delay = expected_time - current_time
|
||||||
|
if delay > 0:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
|
await conn.websocket.send(opus_packet)
|
||||||
|
conn.logger.bind(tag=TAG).debug(f"发送帧,位置: {play_position}ms, 实际间隔: {(time.perf_counter() - current_time) * 1000:.2f}ms")
|
||||||
|
|
||||||
|
play_position += frame_duration
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def send_tts_message(conn, state, text=None):
|
async def send_tts_message(conn, state, text=None):
|
||||||
"""发送 TTS 状态消息"""
|
"""发送 TTS 状态消息"""
|
||||||
message = {
|
message = {
|
||||||
@@ -60,10 +67,20 @@ async def send_tts_message(conn, state, text=None):
|
|||||||
if text is not None:
|
if text is not None:
|
||||||
message["text"] = text
|
message["text"] = text
|
||||||
|
|
||||||
await conn.websocket.send(json.dumps(message))
|
# TTS播放结束
|
||||||
if state == "stop":
|
if state == "stop":
|
||||||
|
# 播放提示音
|
||||||
|
tts_notify = conn.config.get("enable_stop_tts_notify", False)
|
||||||
|
if tts_notify:
|
||||||
|
stop_tts_notify_voice = conn.config.get("stop_tts_notify_voice", "config/assets/tts_notify.mp3")
|
||||||
|
audios, duration = conn.tts.audio_to_opus_data(stop_tts_notify_voice)
|
||||||
|
await sendAudio(conn, audios)
|
||||||
|
# 清除服务端讲话状态
|
||||||
conn.clearSpeakStatus()
|
conn.clearSpeakStatus()
|
||||||
|
|
||||||
|
# 发送消息到客户端
|
||||||
|
await conn.websocket.send(json.dumps(message))
|
||||||
|
|
||||||
|
|
||||||
async def send_stt_message(conn, text):
|
async def send_stt_message(conn, text):
|
||||||
"""发送 STT 状态消息"""
|
"""发送 STT 状态消息"""
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ from config.logger import setup_logging
|
|||||||
import json
|
import json
|
||||||
from core.handle.abortHandle import handleAbortMessage
|
from core.handle.abortHandle import handleAbortMessage
|
||||||
from core.handle.helloHandle import handleHelloMessage
|
from core.handle.helloHandle import handleHelloMessage
|
||||||
|
from core.utils.util import remove_punctuation_and_length
|
||||||
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
||||||
|
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
|
||||||
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
@@ -38,7 +40,21 @@ async def handleTextMessage(conn, message):
|
|||||||
conn.client_have_voice = False
|
conn.client_have_voice = False
|
||||||
conn.asr_audio.clear()
|
conn.asr_audio.clear()
|
||||||
if "text" in msg_json:
|
if "text" in msg_json:
|
||||||
await startToChat(conn, msg_json["text"])
|
text = msg_json["text"]
|
||||||
|
_, text = remove_punctuation_and_length(text)
|
||||||
|
|
||||||
|
# 识别是否是唤醒词
|
||||||
|
is_wakeup_words = text in conn.config.get("wakeup_words")
|
||||||
|
# 是否开启唤醒词回复
|
||||||
|
enable_greeting = conn.config.get("enable_greeting", True)
|
||||||
|
|
||||||
|
if is_wakeup_words and not enable_greeting:
|
||||||
|
# 如果是唤醒词,且关闭了唤醒词回复,就不用回答
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
await send_tts_message(conn, "stop", None)
|
||||||
|
else:
|
||||||
|
# 否则需要LLM对文字内容进行答复
|
||||||
|
await startToChat(conn, text)
|
||||||
elif msg_json["type"] == "iot":
|
elif msg_json["type"] == "iot":
|
||||||
if "descriptors" in msg_json:
|
if "descriptors" in msg_json:
|
||||||
await handleIotDescriptors(conn, msg_json["descriptors"])
|
await handleIotDescriptors(conn, msg_json["descriptors"])
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
|
import os, shutil
|
||||||
|
from mcp import ClientSession, StdioServerParameters
|
||||||
|
from mcp.client.stdio import stdio_client
|
||||||
|
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
|
||||||
|
class MCPClient:
|
||||||
|
def __init__(self, config):
|
||||||
|
# Initialize session and client objects
|
||||||
|
self.session: Optional[ClientSession] = None
|
||||||
|
self.exit_stack = AsyncExitStack()
|
||||||
|
self.logger = setup_logging()
|
||||||
|
self.config = config
|
||||||
|
self.tolls = []
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
args = self.config.get("args", [])
|
||||||
|
|
||||||
|
command = (
|
||||||
|
shutil.which("npx")
|
||||||
|
if self.config["command"] == "npx"
|
||||||
|
else self.config["command"]
|
||||||
|
)
|
||||||
|
|
||||||
|
env={**os.environ}
|
||||||
|
if self.config.get("env"):
|
||||||
|
env.update(self.config["env"])
|
||||||
|
|
||||||
|
server_params = StdioServerParameters(
|
||||||
|
command=command,
|
||||||
|
args=args,
|
||||||
|
env=env
|
||||||
|
)
|
||||||
|
|
||||||
|
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
||||||
|
self.stdio, self.write = stdio_transport
|
||||||
|
time_out_delta = timedelta(seconds=15)
|
||||||
|
self.session = await self.exit_stack.enter_async_context(ClientSession(read_stream=self.stdio, write_stream=self.write, read_timeout_seconds=time_out_delta))
|
||||||
|
|
||||||
|
await self.session.initialize()
|
||||||
|
|
||||||
|
# List available tools
|
||||||
|
response = await self.session.list_tools()
|
||||||
|
tools = response.tools
|
||||||
|
self.tools = tools
|
||||||
|
self.logger.bind(tag=TAG).info(f"Connected to server with tools:{[tool.name for tool in tools]}")
|
||||||
|
|
||||||
|
def has_tool(self, tool_name):
|
||||||
|
return any(tool.name == tool_name for tool in self.tools)
|
||||||
|
|
||||||
|
def get_available_tools(self):
|
||||||
|
available_tools = [{"type": "function", "function":{
|
||||||
|
"name": tool.name,
|
||||||
|
"description": tool.description,
|
||||||
|
"parameters": tool.inputSchema
|
||||||
|
} } for tool in self.tools]
|
||||||
|
|
||||||
|
return available_tools
|
||||||
|
|
||||||
|
async def call_tool(self, tool_name: str, tool_args: dict):
|
||||||
|
self.logger.bind(tag=TAG).info(f"MCPClient Calling tool {tool_name} with args: {tool_args}")
|
||||||
|
try:
|
||||||
|
response = await self.session.call_tool(tool_name, tool_args)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"Error calling tool {tool_name}: {e}")
|
||||||
|
from types import SimpleNamespace
|
||||||
|
error_content = SimpleNamespace(
|
||||||
|
type='text',
|
||||||
|
text=f"Error calling tool {tool_name}: {e}"
|
||||||
|
)
|
||||||
|
error_response = SimpleNamespace(
|
||||||
|
content=[error_content],
|
||||||
|
isError=True
|
||||||
|
)
|
||||||
|
return error_response
|
||||||
|
self.logger.bind(tag=TAG).info(f"MCPClient Response from tool {tool_name}: {response}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
"""Clean up resources"""
|
||||||
|
await self.exit_stack.aclose()
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""MCP服务管理器"""
|
||||||
|
import os, json
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from .MCPClient import MCPClient
|
||||||
|
from config.logger import setup_logging
|
||||||
|
from core.utils.util import get_project_dir
|
||||||
|
from plugins_func.register import register_function, ActionResponse, Action, ToolType
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
|
||||||
|
class MCPManager:
|
||||||
|
"""管理多个MCP服务的集中管理器"""
|
||||||
|
|
||||||
|
def __init__(self,conn) -> None:
|
||||||
|
"""
|
||||||
|
初始化MCP管理器
|
||||||
|
"""
|
||||||
|
self.conn = conn
|
||||||
|
self.logger = setup_logging()
|
||||||
|
self.config_path = get_project_dir() + 'data/.mcp_server_settings.json'
|
||||||
|
if os.path.exists(self.config_path) == False:
|
||||||
|
self.config_path = ""
|
||||||
|
self.logger.bind(tag=TAG).warning(f"请检查mcp服务配置文件:data/.mcp_server_settings.json")
|
||||||
|
self.client: Dict[str, MCPClient] = {}
|
||||||
|
self.tools = []
|
||||||
|
|
||||||
|
def load_config(self) -> Dict[str, Any]:
|
||||||
|
"""加载MCP服务配置
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 服务配置字典
|
||||||
|
"""
|
||||||
|
if len(self.config_path) == 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
return config.get('mcpServers', {})
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"Error loading MCP config from {self.config_path}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def initialize_servers(self) -> None:
|
||||||
|
"""初始化所有MCP服务"""
|
||||||
|
config = self.load_config()
|
||||||
|
for name, srv_config in config.items():
|
||||||
|
if not srv_config.get("command"):
|
||||||
|
self.logger.bind(tag=TAG).warning(f"Skipping server {name}: command not specified")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = MCPClient(srv_config)
|
||||||
|
await client.initialize()
|
||||||
|
self.client[name] = client
|
||||||
|
self.logger.bind(tag=TAG).info(f"Initialized MCP client: {name}")
|
||||||
|
client_tools = client.get_available_tools()
|
||||||
|
self.tools.extend(client_tools)
|
||||||
|
for tool in client_tools:
|
||||||
|
func_name = "mcp_"+tool["function"]["name"]
|
||||||
|
register_function(func_name, tool, ToolType.MCP_CLIENT)(self.execute_tool)
|
||||||
|
self.conn.func_handler.function_registry.register_function(func_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"Failed to initialize MCP server {name}: {e}")
|
||||||
|
self.conn.func_handler.upload_functions_desc()
|
||||||
|
|
||||||
|
def get_all_tools(self) -> List[Dict[str, Any]]:
|
||||||
|
"""获取所有服务的工具function定义
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: 所有工具的function定义列表
|
||||||
|
"""
|
||||||
|
return self.tools
|
||||||
|
|
||||||
|
def is_mcp_tool(self, tool_name: str) -> bool:
|
||||||
|
"""检查是否是MCP工具
|
||||||
|
Args:
|
||||||
|
tool_name: 工具名称
|
||||||
|
Returns:
|
||||||
|
bool: 是否是MCP工具
|
||||||
|
"""
|
||||||
|
for tool in self.tools:
|
||||||
|
if tool.get("function") != None and tool["function"].get("name") == tool_name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
||||||
|
"""执行工具调用
|
||||||
|
Args:
|
||||||
|
tool_name: 工具名称
|
||||||
|
arguments: 工具参数
|
||||||
|
Returns:
|
||||||
|
Any: 工具执行结果
|
||||||
|
Raises:
|
||||||
|
ValueError: 工具未找到时抛出
|
||||||
|
"""
|
||||||
|
self.logger.bind(tag=TAG).info(f"Executing tool {tool_name} with arguments: {arguments}")
|
||||||
|
for client in self.client.values():
|
||||||
|
if client.has_tool(tool_name):
|
||||||
|
return await client.call_tool(tool_name, arguments)
|
||||||
|
|
||||||
|
raise ValueError(f"Tool {tool_name} not found in any MCP server")
|
||||||
|
|
||||||
|
async def cleanup_all(self) -> None:
|
||||||
|
for name, client in self.client.items():
|
||||||
|
try:
|
||||||
|
await client.cleanup()
|
||||||
|
self.logger.bind(tag=TAG).info(f"Cleaned up MCP client: {name}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"Error cleaning up MCP client {name}: {e}")
|
||||||
|
self.client.clear()
|
||||||
@@ -10,14 +10,21 @@ class IntentProviderBase(ABC):
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.intent_options = config.get("intent_options", {
|
self.intent_options = config.get("intent_options", {
|
||||||
|
"handle_exit_intent": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
||||||
|
"play_music": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
|
||||||
|
"get_weather": "查询天气, 用户希望查询某个地点的天气情况",
|
||||||
|
"get_news": "查询新闻, 用户希望查询最新新闻或特定类型的新闻",
|
||||||
|
"get_lunar": "用于获取今天的阴历/农历和黄历信息",
|
||||||
|
"get_time": "获取今天日期或者当前时间信息",
|
||||||
"continue_chat": "继续聊天",
|
"continue_chat": "继续聊天",
|
||||||
"end_chat": "结束聊天",
|
|
||||||
"play_music": "播放音乐"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def set_llm(self, llm):
|
def set_llm(self, llm):
|
||||||
self.llm = llm
|
self.llm = llm
|
||||||
logger.bind(tag=TAG).debug("Set LLM for intent provider")
|
# 获取模型名称和类型信息
|
||||||
|
model_name = getattr(llm, 'model_name', str(llm.__class__.__name__))
|
||||||
|
# 记录更详细的日志
|
||||||
|
logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}")
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||||
@@ -30,5 +37,6 @@ class IntentProviderBase(ABC):
|
|||||||
- "继续聊天"
|
- "继续聊天"
|
||||||
- "结束聊天"
|
- "结束聊天"
|
||||||
- "播放音乐 歌名" 或 "随机播放音乐"
|
- "播放音乐 歌名" 或 "随机播放音乐"
|
||||||
|
- "查询天气 地点名" 或 "查询天气 [当前位置]"
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from ..base import IntentProviderBase
|
||||||
|
from typing import List, Dict
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
class IntentProvider(IntentProviderBase):
|
||||||
|
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||||
|
"""
|
||||||
|
默认的意图识别实现,始终返回继续聊天
|
||||||
|
Args:
|
||||||
|
dialogue_history: 对话历史记录列表
|
||||||
|
text: 本次对话记录
|
||||||
|
Returns:
|
||||||
|
固定返回"继续聊天"
|
||||||
|
"""
|
||||||
|
logger.bind(tag=TAG).debug("Using functionCallProvider, always returning continue chat")
|
||||||
|
return self.intent_options["continue_chat"]
|
||||||
@@ -3,6 +3,9 @@ from ..base import IntentProviderBase
|
|||||||
from plugins_func.functions.play_music import initialize_music_handler
|
from plugins_func.functions.play_music import initialize_music_handler
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -13,6 +16,10 @@ class IntentProvider(IntentProviderBase):
|
|||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.llm = None
|
self.llm = None
|
||||||
self.promot = self.get_intent_system_prompt()
|
self.promot = self.get_intent_system_prompt()
|
||||||
|
# 添加缓存管理
|
||||||
|
self.intent_cache = {} # 缓存意图识别结果
|
||||||
|
self.cache_expiry = 600 # 缓存有效期10分钟
|
||||||
|
self.cache_max_size = 100 # 最多缓存100个意图
|
||||||
|
|
||||||
def get_intent_system_prompt(self) -> str:
|
def get_intent_system_prompt(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -22,62 +29,119 @@ class IntentProvider(IntentProviderBase):
|
|||||||
"""
|
"""
|
||||||
intent_list = []
|
intent_list = []
|
||||||
|
|
||||||
"""
|
|
||||||
"continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等",
|
|
||||||
"end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
|
||||||
"play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图"
|
|
||||||
"""
|
|
||||||
for key, value in self.intent_options.items():
|
|
||||||
if key == "play_music":
|
|
||||||
intent_list.append("3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图")
|
|
||||||
elif key == "end_chat":
|
|
||||||
intent_list.append("2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候")
|
|
||||||
elif key == "continue_chat":
|
|
||||||
intent_list.append("1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等")
|
|
||||||
else:
|
|
||||||
intent_list.append(value)
|
|
||||||
|
|
||||||
# "如果是唱歌、听歌、播放音乐,请指定歌名,格式为'播放音乐 [识别出的歌名]'。\n"
|
|
||||||
# "如果听不出具体歌名,可以返回'随机播放音乐'。\n"
|
|
||||||
# "只需要返回意图结果的json,不要解释。"
|
|
||||||
# "返回格式如下:\n"
|
|
||||||
prompt = (
|
prompt = (
|
||||||
"你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类(使用<start>和<end>标志):\n"
|
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
|
||||||
"<start>"
|
"<start>"
|
||||||
f"{', '.join(intent_list)}"
|
f"{', '.join(intent_list)}"
|
||||||
"<end>\n"
|
"<end>\n"
|
||||||
"你需要按照以下的步骤处理用户的对话"
|
"处理步骤:"
|
||||||
"1. 思考出对话的意图是哪一类的"
|
"1. 思考意图类型,生成function_call格式"
|
||||||
"2. 属于1和2的意图, 直接返回,返回格式如下:\n"
|
"\n\n"
|
||||||
"{intent: '用户意图'}\n"
|
"返回格式示例:\n"
|
||||||
"3. 属于3的意图,则继续分析用户希望播放的音乐\n"
|
"1. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n"
|
||||||
"4. 如果无法识别出具体歌名,可以返回'随机播放音乐'\n"
|
"2. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n"
|
||||||
"{intent: '播放音乐 [获取的音乐名字]'}\n"
|
"3. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
|
||||||
"下面是几个处理的示例(思考的内容不返回, 只返回json部分, 无额外的内容)\n"
|
"4. 结束对话意图: {\"function_call\": {\"name\": \"handle_exit_intent\", \"arguments\": {\"say_goodbye\": \"goodbye\"}}}\n"
|
||||||
"```"
|
"5. 获取当天日期时间: {\"function_call\": {\"name\": \"get_time\"}}\n"
|
||||||
|
"6. 获取当前黄历意图: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
|
||||||
|
"7. 继续聊天意图: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||||
|
"\n"
|
||||||
|
"注意:\n"
|
||||||
|
"- 播放音乐:无歌名时,song_name设为\"random\"\n"
|
||||||
|
"- 查询天气:无地点时,location设为null\n"
|
||||||
|
"- 查询新闻:无类别时,category设为null;查询详情时,detail设为true\n"
|
||||||
|
"- 如果没有明显的意图,应按照继续聊天意图处理\n"
|
||||||
|
"- 只返回纯JSON,不要任何其他内容\n"
|
||||||
|
"\n"
|
||||||
|
"示例分析:\n"
|
||||||
|
"```\n"
|
||||||
|
"用户: 你好小智\n"
|
||||||
|
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||||
|
"```\n"
|
||||||
|
"```\n"
|
||||||
"用户: 你今天怎么样?\n"
|
"用户: 你今天怎么样?\n"
|
||||||
"思考(不返回): 用户发来的数据是一个问候语,属于继续聊天的意图, 是种类1, 种类1的需求是直接返回\n"
|
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||||
"返回结果: {intent: '继续聊天'}\n"
|
"```\n"
|
||||||
"```"
|
"```\n"
|
||||||
"用户: 我今天有点累了, 我们明天再聊吧\n"
|
"用户: 现在是几号了?现在几点了?\n"
|
||||||
"思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
|
"返回: {\"function_call\": {\"name\": \"get_time\"}}\n"
|
||||||
"返回结果: {intent: '结束聊天'}\n"
|
"```\n"
|
||||||
"```"
|
"```\n"
|
||||||
"用户: 我今天有点累了, 我们明天再聊吧\n"
|
"用户: 今天农历是多少?\n"
|
||||||
"思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
|
"返回: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
|
||||||
"返回结果: {intent: '结束聊天'}\n"
|
"```\n"
|
||||||
"```"
|
"```\n"
|
||||||
"用户: 你可以播放一首中秋月给我听吗\n"
|
"用户: 我们明天再聊吧\n"
|
||||||
"思考(不返回): 用户表达了想听音乐的续签,属于播放音乐的意图, 是种类3, 种类3的需求需要继续判断播放的音乐, 这里用户希望的歌曲名明确给出是中秋月\n"
|
"返回: {\"function_call\": {\"name\": \"handle_exit_intent\"}}\n"
|
||||||
"返回结果: {intent: '播放音乐 [中秋月]'}\n"
|
"```\n"
|
||||||
"```"
|
"```\n"
|
||||||
"你现在可以使用的音乐的名称如下(使用<start>和<end>标志):\n"
|
"用户: 播放中秋月\n"
|
||||||
|
"返回: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"中秋月\"}}}\n"
|
||||||
|
"```\n"
|
||||||
|
"```\n"
|
||||||
|
"用户: 北京天气怎么样\n"
|
||||||
|
"返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"北京\", \"lang\": \"zh_CN\"}}}\n"
|
||||||
|
"```\n"
|
||||||
|
"```\n"
|
||||||
|
"用户: 今天天气怎么样\n"
|
||||||
|
"返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": null, \"lang\": \"zh_CN\"}}}\n"
|
||||||
|
"```\n"
|
||||||
|
"```\n"
|
||||||
|
"用户: 播报财经新闻\n"
|
||||||
|
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"财经\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
|
||||||
|
"```\n"
|
||||||
|
"```\n"
|
||||||
|
"用户: 有什么最新新闻\n"
|
||||||
|
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": null, \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
|
||||||
|
"```\n"
|
||||||
|
"```\n"
|
||||||
|
"用户: 详细介绍一下这条新闻\n"
|
||||||
|
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}\n"
|
||||||
|
"```\n"
|
||||||
|
"可用的音乐名称:\n"
|
||||||
)
|
)
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
|
def clean_cache(self):
|
||||||
|
"""清理过期缓存"""
|
||||||
|
now = time.time()
|
||||||
|
# 找出过期键
|
||||||
|
expired_keys = [k for k, v in self.intent_cache.items() if now - v['timestamp'] > self.cache_expiry]
|
||||||
|
for key in expired_keys:
|
||||||
|
del self.intent_cache[key]
|
||||||
|
|
||||||
|
# 如果缓存太大,移除最旧的条目
|
||||||
|
if len(self.intent_cache) > self.cache_max_size:
|
||||||
|
# 按时间戳排序并保留最新的条目
|
||||||
|
sorted_items = sorted(self.intent_cache.items(), key=lambda x: x[1]['timestamp'])
|
||||||
|
for key, _ in sorted_items[:len(sorted_items) - self.cache_max_size]:
|
||||||
|
del self.intent_cache[key]
|
||||||
|
|
||||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||||
if not self.llm:
|
if not self.llm:
|
||||||
raise ValueError("LLM provider not set")
|
raise ValueError("LLM provider not set")
|
||||||
|
|
||||||
|
# 记录整体开始时间
|
||||||
|
total_start_time = time.time()
|
||||||
|
|
||||||
|
# 打印使用的模型信息
|
||||||
|
model_info = getattr(self.llm, 'model_name', str(self.llm.__class__.__name__))
|
||||||
|
logger.bind(tag=TAG).info(f"使用意图识别模型: {model_info}")
|
||||||
|
|
||||||
|
# 计算缓存键
|
||||||
|
cache_key = hashlib.md5(text.encode()).hexdigest()
|
||||||
|
|
||||||
|
# 检查缓存
|
||||||
|
if cache_key in self.intent_cache:
|
||||||
|
cache_entry = self.intent_cache[cache_key]
|
||||||
|
# 检查缓存是否过期
|
||||||
|
if time.time() - cache_entry['timestamp'] <= self.cache_expiry:
|
||||||
|
cache_time = time.time() - total_start_time
|
||||||
|
logger.bind(tag=TAG).info(f"使用缓存的意图: {cache_key} -> {cache_entry['intent']}, 耗时: {cache_time:.4f}秒")
|
||||||
|
return cache_entry['intent']
|
||||||
|
|
||||||
|
# 清理缓存
|
||||||
|
self.clean_cache()
|
||||||
|
|
||||||
# 构建用户最后一句话的提示
|
# 构建用户最后一句话的提示
|
||||||
msgStr = ""
|
msgStr = ""
|
||||||
@@ -94,18 +158,78 @@ class IntentProvider(IntentProviderBase):
|
|||||||
music_file_names = music_config["music_file_names"]
|
music_file_names = music_config["music_file_names"]
|
||||||
prompt_music = f"{self.promot}\n<start>{music_file_names}\n<end>"
|
prompt_music = f"{self.promot}\n<start>{music_file_names}\n<end>"
|
||||||
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
|
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
|
||||||
|
|
||||||
|
# 记录预处理完成时间
|
||||||
|
preprocess_time = time.time() - total_start_time
|
||||||
|
logger.bind(tag=TAG).debug(f"意图识别预处理耗时: {preprocess_time:.4f}秒")
|
||||||
|
|
||||||
# 使用LLM进行意图识别
|
# 使用LLM进行意图识别
|
||||||
|
llm_start_time = time.time()
|
||||||
|
logger.bind(tag=TAG).info(f"开始LLM意图识别调用, 模型: {model_info}")
|
||||||
|
|
||||||
intent = self.llm.response_no_stream(
|
intent = self.llm.response_no_stream(
|
||||||
system_prompt=prompt_music,
|
system_prompt=prompt_music,
|
||||||
user_prompt=user_prompt
|
user_prompt=user_prompt
|
||||||
)
|
)
|
||||||
# 使用正则表达式提取大括号中的内容
|
|
||||||
# 使用正则表达式提取 {} 中的内容
|
# 记录LLM调用完成时间
|
||||||
match = re.search(r'\{.*?\}', intent)
|
llm_time = time.time() - llm_start_time
|
||||||
|
logger.bind(tag=TAG).info(f"LLM意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}秒")
|
||||||
|
|
||||||
|
# 记录后处理开始时间
|
||||||
|
postprocess_start_time = time.time()
|
||||||
|
|
||||||
|
# 清理和解析响应
|
||||||
|
intent = intent.strip()
|
||||||
|
# 尝试提取JSON部分
|
||||||
|
match = re.search(r'\{.*\}', intent, re.DOTALL)
|
||||||
if match:
|
if match:
|
||||||
result = match.group(0)
|
intent = match.group(0)
|
||||||
intent = result
|
|
||||||
else:
|
# 记录总处理时间
|
||||||
intent = "{intent: '继续聊天'}"
|
total_time = time.time() - total_start_time
|
||||||
logger.bind(tag=TAG).info(f"Detected intent: {intent}")
|
logger.bind(tag=TAG).info(f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'")
|
||||||
return intent.strip()
|
|
||||||
|
# 尝试解析为JSON
|
||||||
|
try:
|
||||||
|
intent_data = json.loads(intent)
|
||||||
|
# 如果包含function_call,则格式化为适合处理的格式
|
||||||
|
if "function_call" in intent_data:
|
||||||
|
function_data = intent_data["function_call"]
|
||||||
|
function_name = function_data.get("name")
|
||||||
|
function_args = function_data.get("arguments", {})
|
||||||
|
|
||||||
|
# 记录识别到的function call
|
||||||
|
logger.bind(tag=TAG).info(f"识别到function call: {function_name}, 参数: {function_args}")
|
||||||
|
|
||||||
|
# 添加到缓存
|
||||||
|
self.intent_cache[cache_key] = {
|
||||||
|
'intent': intent,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 后处理时间
|
||||||
|
postprocess_time = time.time() - postprocess_start_time
|
||||||
|
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒")
|
||||||
|
|
||||||
|
# 确保返回完全序列化的JSON字符串
|
||||||
|
return intent
|
||||||
|
else:
|
||||||
|
# 添加到缓存
|
||||||
|
self.intent_cache[cache_key] = {
|
||||||
|
'intent': intent,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 后处理时间
|
||||||
|
postprocess_time = time.time() - postprocess_start_time
|
||||||
|
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒")
|
||||||
|
|
||||||
|
# 返回普通意图
|
||||||
|
return intent
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# 后处理时间
|
||||||
|
postprocess_time = time.time() - postprocess_start_time
|
||||||
|
logger.bind(tag=TAG).error(f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}秒")
|
||||||
|
# 如果解析失败,默认返回继续聊天意图
|
||||||
|
return "{\"intent\": \"继续聊天\"}"
|
||||||
|
|||||||
@@ -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}")
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from config.logger import setup_logging
|
||||||
|
from openai import OpenAI
|
||||||
|
import json
|
||||||
|
from core.providers.llm.base import LLMProviderBase
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
class LLMProvider(LLMProviderBase):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.model_name = config.get("model_name")
|
||||||
|
self.base_url = config.get("base_url", "http://localhost:9997")
|
||||||
|
# Initialize OpenAI client with Xinference base URL
|
||||||
|
# 如果没有v1,增加v1
|
||||||
|
if not self.base_url.endswith("/v1"):
|
||||||
|
self.base_url = f"{self.base_url}/v1"
|
||||||
|
|
||||||
|
logger.bind(tag=TAG).info(f"Initializing Xinference LLM provider with model: {self.model_name}, base_url: {self.base_url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client = OpenAI(
|
||||||
|
base_url=self.base_url,
|
||||||
|
api_key="xinference" # Xinference has a similar setup to Ollama where it doesn't need an actual key
|
||||||
|
)
|
||||||
|
logger.bind(tag=TAG).info("Xinference client initialized successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"Error initializing Xinference client: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def response(self, session_id, dialogue):
|
||||||
|
try:
|
||||||
|
logger.bind(tag=TAG).debug(f"Sending request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}")
|
||||||
|
responses = self.client.chat.completions.create(
|
||||||
|
model=self.model_name,
|
||||||
|
messages=dialogue,
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
is_active=True
|
||||||
|
for chunk in responses:
|
||||||
|
try:
|
||||||
|
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||||
|
content = delta.content if hasattr(delta, 'content') else ''
|
||||||
|
if content:
|
||||||
|
if '<think>' in content:
|
||||||
|
is_active = False
|
||||||
|
content = content.split('<think>')[0]
|
||||||
|
if '</think>' in content:
|
||||||
|
is_active = True
|
||||||
|
content = content.split('</think>')[-1]
|
||||||
|
if is_active:
|
||||||
|
yield content
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"Error in Xinference response generation: {e}")
|
||||||
|
yield "【Xinference服务响应异常】"
|
||||||
|
|
||||||
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
|
try:
|
||||||
|
logger.bind(tag=TAG).debug(f"Sending function call request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}")
|
||||||
|
if functions:
|
||||||
|
logger.bind(tag=TAG).debug(f"Function calls enabled with: {[f.get('function', {}).get('name') for f in functions]}")
|
||||||
|
|
||||||
|
stream = self.client.chat.completions.create(
|
||||||
|
model=self.model_name,
|
||||||
|
messages=dialogue,
|
||||||
|
stream=True,
|
||||||
|
tools=functions,
|
||||||
|
)
|
||||||
|
|
||||||
|
for chunk in stream:
|
||||||
|
delta = chunk.choices[0].delta
|
||||||
|
content = delta.content
|
||||||
|
tool_calls = delta.tool_calls
|
||||||
|
|
||||||
|
if content:
|
||||||
|
yield content, tool_calls
|
||||||
|
elif tool_calls:
|
||||||
|
yield None, tool_calls
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"Error in Xinference function call: {e}")
|
||||||
|
yield {"type": "content", "content": f"【Xinference服务响应异常: {str(e)}】"}
|
||||||
@@ -39,22 +39,22 @@ class AccessToken:
|
|||||||
'Version': '2019-02-28'}
|
'Version': '2019-02-28'}
|
||||||
# 构造规范化的请求字符串
|
# 构造规范化的请求字符串
|
||||||
query_string = AccessToken._encode_dict(parameters)
|
query_string = AccessToken._encode_dict(parameters)
|
||||||
print('规范化的请求字符串: %s' % query_string)
|
# print('规范化的请求字符串: %s' % query_string)
|
||||||
# 构造待签名字符串
|
# 构造待签名字符串
|
||||||
string_to_sign = 'GET' + '&' + AccessToken._encode_text('/') + '&' + AccessToken._encode_text(query_string)
|
string_to_sign = 'GET' + '&' + AccessToken._encode_text('/') + '&' + AccessToken._encode_text(query_string)
|
||||||
print('待签名的字符串: %s' % string_to_sign)
|
# print('待签名的字符串: %s' % string_to_sign)
|
||||||
# 计算签名
|
# 计算签名
|
||||||
secreted_string = hmac.new(bytes(access_key_secret + '&', encoding='utf-8'),
|
secreted_string = hmac.new(bytes(access_key_secret + '&', encoding='utf-8'),
|
||||||
bytes(string_to_sign, encoding='utf-8'),
|
bytes(string_to_sign, encoding='utf-8'),
|
||||||
hashlib.sha1).digest()
|
hashlib.sha1).digest()
|
||||||
signature = base64.b64encode(secreted_string)
|
signature = base64.b64encode(secreted_string)
|
||||||
print('签名: %s' % signature)
|
# print('签名: %s' % signature)
|
||||||
# 进行URL编码
|
# 进行URL编码
|
||||||
signature = AccessToken._encode_text(signature)
|
signature = AccessToken._encode_text(signature)
|
||||||
print('URL编码后的签名: %s' % signature)
|
# print('URL编码后的签名: %s' % signature)
|
||||||
# 调用服务
|
# 调用服务
|
||||||
full_url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s' % (signature, query_string)
|
full_url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s' % (signature, query_string)
|
||||||
print('url: %s' % full_url)
|
# print('url: %s' % full_url)
|
||||||
# 提交HTTP GET请求
|
# 提交HTTP GET请求
|
||||||
response = requests.get(full_url)
|
response = requests.get(full_url)
|
||||||
if response.ok:
|
if response.ok:
|
||||||
@@ -64,7 +64,7 @@ class AccessToken:
|
|||||||
token = root_obj[key]['Id']
|
token = root_obj[key]['Id']
|
||||||
expire_time = root_obj[key]['ExpireTime']
|
expire_time = root_obj[key]['ExpireTime']
|
||||||
return token, expire_time
|
return token, expire_time
|
||||||
print(response.text)
|
# print(response.text)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
@@ -75,37 +75,80 @@ class TTSProvider(TTSProviderBase):
|
|||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
|
|
||||||
# 新增空值判断逻辑
|
# 新增空值判断逻辑
|
||||||
access_key_id = config.get("access_key_id")
|
self.access_key_id = config.get("access_key_id")
|
||||||
access_key_secret = config.get("access_key_secret")
|
self.access_key_secret = config.get("access_key_secret")
|
||||||
if access_key_id and access_key_secret:
|
|
||||||
# 使用密钥对生成临时token
|
|
||||||
token, expire_time = AccessToken.create_token(access_key_id, access_key_secret)
|
|
||||||
else:
|
|
||||||
# 直接使用预生成的长期token
|
|
||||||
token = config.get("token")
|
|
||||||
expire_time = None
|
|
||||||
|
|
||||||
print('token: %s, expire time(s): %s' % (token, expire_time))
|
|
||||||
|
|
||||||
self.appkey = config.get("appkey")
|
self.appkey = config.get("appkey")
|
||||||
self.token = token
|
|
||||||
self.format = config.get("format", "wav")
|
self.format = config.get("format", "wav")
|
||||||
self.sample_rate = config.get("sample_rate", 16000)
|
self.sample_rate = config.get("sample_rate", 16000)
|
||||||
self.voice = config.get("voice", "xiaoyun")
|
self.voice = config.get("voice", "xiaoyun")
|
||||||
self.volume = config.get("volume", 50)
|
self.volume = config.get("volume", 50)
|
||||||
self.speech_rate = config.get("speech_rate", 0)
|
self.speech_rate = config.get("speech_rate", 0)
|
||||||
self.pitch_rate = config.get("pitch_rate", 0)
|
self.pitch_rate = config.get("pitch_rate", 0)
|
||||||
|
|
||||||
self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
|
self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
|
||||||
self.api_url = f"https://{self.host}/stream/v1/tts"
|
self.api_url = f"https://{self.host}/stream/v1/tts"
|
||||||
self.header = {
|
self.header = {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.access_key_id and self.access_key_secret:
|
||||||
|
# 使用密钥对生成临时token
|
||||||
|
self._refresh_token()
|
||||||
|
else:
|
||||||
|
# 直接使用预生成的长期token
|
||||||
|
self.token = config.get("token")
|
||||||
|
self.expire_time = None
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_token(self):
|
||||||
|
"""刷新Token并记录过期时间"""
|
||||||
|
if self.access_key_id and self.access_key_secret:
|
||||||
|
self.token, expire_time_str = AccessToken.create_token(
|
||||||
|
self.access_key_id,
|
||||||
|
self.access_key_secret
|
||||||
|
)
|
||||||
|
if not expire_time_str:
|
||||||
|
raise ValueError("无法获取有效的Token过期时间")
|
||||||
|
|
||||||
|
try:
|
||||||
|
#统一转换为字符串处理
|
||||||
|
expire_str = str(expire_time_str).strip()
|
||||||
|
|
||||||
|
if expire_str.isdigit():
|
||||||
|
expire_time = datetime.fromtimestamp(int(expire_str))
|
||||||
|
else:
|
||||||
|
expire_time = datetime.strptime(
|
||||||
|
expire_str,
|
||||||
|
"%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
)
|
||||||
|
self.expire_time = expire_time.timestamp() - 60
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"无效的过期时间格式: {expire_str}") from e
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.expire_time = None
|
||||||
|
|
||||||
|
if not self.token:
|
||||||
|
raise ValueError("无法获取有效的访问Token")
|
||||||
|
|
||||||
|
def _is_token_expired(self):
|
||||||
|
"""检查Token是否过期"""
|
||||||
|
if not self.expire_time:
|
||||||
|
return False # 长期Token不过期
|
||||||
|
# 新增调试日志
|
||||||
|
# current_time = time.time()
|
||||||
|
# remaining = self.expire_time - current_time
|
||||||
|
# print(f"Token过期检查: 当前时间 {datetime.fromtimestamp(current_time)} | "
|
||||||
|
# f"过期时间 {datetime.fromtimestamp(self.expire_time)} | "
|
||||||
|
# f"剩余 {remaining:.2f}秒")
|
||||||
|
return time.time() > self.expire_time
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
|
if self._is_token_expired():
|
||||||
|
logger.warning("Token已过期,正在自动刷新...")
|
||||||
|
self._refresh_token()
|
||||||
request_json = {
|
request_json = {
|
||||||
"appkey": self.appkey,
|
"appkey": self.appkey,
|
||||||
"token": self.token,
|
"token": self.token,
|
||||||
@@ -118,9 +161,12 @@ class TTSProvider(TTSProviderBase):
|
|||||||
"pitch_rate": self.pitch_rate
|
"pitch_rate": self.pitch_rate
|
||||||
}
|
}
|
||||||
|
|
||||||
print(self.api_url, json.dumps(request_json, ensure_ascii=False))
|
# print(self.api_url, json.dumps(request_json, ensure_ascii=False))
|
||||||
try:
|
try:
|
||||||
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
|
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
|
||||||
|
if resp.status_code == 401: # Token过期特殊处理
|
||||||
|
self._refresh_token()
|
||||||
|
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
|
||||||
# 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的
|
# 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的
|
||||||
if resp.headers['Content-Type'].startswith('audio/'):
|
if resp.headers['Content-Type'].startswith('audio/'):
|
||||||
with open(output_file, 'wb') as f:
|
with open(output_file, 'wb') as f:
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -34,7 +36,7 @@ class TTSProviderBase(ABC):
|
|||||||
|
|
||||||
return tmp_file
|
return tmp_file
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).info(f"Failed to generate TTS file: {e}")
|
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -14,5 +14,14 @@ class TTSProvider(TTSProviderBase):
|
|||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
communicate = edge_tts.Communicate(text, voice=self.voice) # Use your preferred voice
|
communicate = edge_tts.Communicate(text, voice=self.voice)
|
||||||
await communicate.save(output_file)
|
# 确保目录存在并创建空文件
|
||||||
|
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||||
|
with open(output_file, 'wb') as f:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 流式写入音频数据
|
||||||
|
with open(output_file, 'ab') as f: # 改为追加模式避免覆盖
|
||||||
|
async for chunk in communicate.stream():
|
||||||
|
if chunk["type"] == "audio": # 只处理音频数据块
|
||||||
|
f.write(chunk["data"])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -73,9 +73,7 @@ def get_ip_info(ip_addr):
|
|||||||
resp = requests.get(url).json()
|
resp = requests.get(url).json()
|
||||||
|
|
||||||
ip_info = {
|
ip_info = {
|
||||||
"city": resp.get("cityName"),
|
"city": resp.get("cityName")
|
||||||
"region": resp.get("regionName"),
|
|
||||||
"country": resp.get("countryName")
|
|
||||||
}
|
}
|
||||||
return ip_info
|
return ip_info
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||