Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30796c6f81 | ||
|
|
86b5eb1818 | ||
|
|
696405a8bf | ||
|
|
a2c2eab6b3 | ||
|
|
ead9ee81fb | ||
|
|
bd140af714 | ||
|
|
96008fa78f | ||
|
|
da4d0f515c | ||
|
|
67e6a22da9 | ||
|
|
b791fb1494 | ||
|
|
9e329bf19f | ||
|
|
3906e8a942 | ||
|
|
f74faea065 | ||
|
|
7d3759acce | ||
|
|
172962f711 | ||
|
|
36109e3b88 | ||
|
|
7c63b7c062 | ||
|
|
1efaf048f5 | ||
|
|
ac52714498 | ||
|
|
c3de89f504 | ||
|
|
6f6a645d0a | ||
|
|
5605352144 | ||
|
|
ef433f446d | ||
|
|
fe8f200364 | ||
|
|
be3a030302 | ||
|
|
cd6d4488d1 | ||
|
|
da3a5df5c3 | ||
|
|
4864407d61 | ||
|
|
f6f7879d6e | ||
|
|
dab1f14a59 | ||
|
|
51ca914206 | ||
|
|
933ffb8ca2 | ||
|
|
2f5f20a257 | ||
|
|
92544f89f3 | ||
|
|
f1cf1acbfa | ||
|
|
f22008389c | ||
|
|
91de372cbc | ||
|
|
811bd94934 | ||
|
|
5987488b3d | ||
|
|
966c9e972d | ||
|
|
464261a1b7 | ||
|
|
363dcef81a | ||
|
|
4439617c50 | ||
|
|
5d2fa3d65d | ||
|
|
8930ab0af6 | ||
|
|
6af6fe22fc | ||
|
|
dd577a7843 | ||
|
|
f772155674 | ||
|
|
3ce204338a | ||
|
|
168bc9ef45 | ||
|
|
bfc3d4888b | ||
|
|
0148b115db | ||
|
|
0b11953f6f | ||
|
|
6e210faacf | ||
|
|
4ef755e10b | ||
|
|
62b7cc5509 | ||
|
|
0e1ec05110 | ||
|
|
dd38fc74db | ||
|
|
d619d7ac76 | ||
|
|
37f7511ba0 | ||
|
|
afd03ba1b5 | ||
|
|
ac7b02d28a | ||
|
|
4a0bdbf779 | ||
|
|
164dada46a | ||
|
|
fd1c4ec792 | ||
|
|
add000d221 | ||
|
|
def598f728 | ||
|
|
0294885fbf | ||
|
|
c0ec4338f8 | ||
|
|
fb7f6f73bd | ||
|
|
e04c38e557 | ||
|
|
bfb5420733 | ||
|
|
9e6fc32903 | ||
|
|
217ceb1c7a | ||
|
|
846f89664e | ||
|
|
0e30f69e96 | ||
|
|
af9eb538f4 | ||
|
|
66a0beea33 | ||
|
|
411e73bb35 | ||
|
|
852e222f5d | ||
|
|
054ceab4f4 | ||
|
|
d9149ff572 | ||
|
|
7f331690a0 | ||
|
|
a5698dfff0 | ||
|
|
1e5866da06 | ||
|
|
7e712c5003 | ||
|
|
474eeb9e68 | ||
|
|
0c0386f4ba | ||
|
|
6c2b1cb589 |
@@ -0,0 +1,72 @@
|
|||||||
|
登录AutoDL,租赁镜像
|
||||||
|
选择镜像:
|
||||||
|
```
|
||||||
|
PyTorch / 2.1.0 / 3.10(ubuntu22.04) / cuda 12.1
|
||||||
|
```
|
||||||
|
|
||||||
|
机器开机后,设置学术加速
|
||||||
|
```
|
||||||
|
source /etc/network_turbo
|
||||||
|
```
|
||||||
|
|
||||||
|
进入工作目录
|
||||||
|
```
|
||||||
|
cd autodl-tmp/
|
||||||
|
```
|
||||||
|
|
||||||
|
拉取项目
|
||||||
|
```
|
||||||
|
git clone https://gitclone.com/github.com/fishaudio/fish-speech.git ; cd fish-speech
|
||||||
|
```
|
||||||
|
|
||||||
|
安装依赖
|
||||||
|
```
|
||||||
|
pip install -e.
|
||||||
|
```
|
||||||
|
|
||||||
|
如果报错,安装portaudio
|
||||||
|
```
|
||||||
|
apt-get install portaudio19-dev -y
|
||||||
|
```
|
||||||
|
|
||||||
|
安装后执行
|
||||||
|
```
|
||||||
|
pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
```
|
||||||
|
|
||||||
|
下载模型
|
||||||
|
```
|
||||||
|
cd tools
|
||||||
|
python download_models.py
|
||||||
|
```
|
||||||
|
|
||||||
|
下载完模型后运行接口
|
||||||
|
```
|
||||||
|
python -m tools.api_server --listen 0.0.0.0:6006
|
||||||
|
```
|
||||||
|
|
||||||
|
然后用浏览器去到aotodl实例页面
|
||||||
|
```
|
||||||
|
https://autodl.com/console/instance/list
|
||||||
|
```
|
||||||
|
|
||||||
|
如下图点击你刚才机器的`自定义服务`按钮,开启端口转发服务
|
||||||
|

|
||||||
|
|
||||||
|
端口转发服务设置完成后,你本地电脑打开网址`http://localhost:6006/`,就可以访问fish-speech的接口了
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
如果你是单模块部署,核心配置如下
|
||||||
|
```
|
||||||
|
selected_module:
|
||||||
|
TTS: FishSpeech
|
||||||
|
TTS:
|
||||||
|
FishSpeech:
|
||||||
|
reference_audio: ["config/assets/wakeup_words.wav",]
|
||||||
|
reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",]
|
||||||
|
api_key: "123"
|
||||||
|
api_url: "http://127.0.0.1:6006/v1/tts"
|
||||||
|
```
|
||||||
|
|
||||||
|
然后重启服务
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
# ESP32设备与HomeAssistant集成指南
|
# 小智ESP32-开源服务端与HomeAssistant集成指南
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
@@ -7,26 +11,58 @@
|
|||||||
## 前提条件
|
## 前提条件
|
||||||
|
|
||||||
- 已安装并配置好`HomeAssistant`
|
- 已安装并配置好`HomeAssistant`
|
||||||
- 已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`
|
|
||||||
- `xiaozhi-esp32-server智控台`能正常使用
|
|
||||||
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
|
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
|
||||||
|
|
||||||
## 网络环境
|
## 开始前的操作(必要)
|
||||||
|
|
||||||
|
### 1. 获取HA的网络网络地址信息
|
||||||
|
|
||||||
|
请访问你Home Assistant的网络地址,例如,我的HA的地址是192.168.4.7,端口则是默认的8123,则在浏览器打开
|
||||||
|
|
||||||
1、我的`HomeAssistant`部署在下面,版本是`2025.3.4`
|
|
||||||
```
|
```
|
||||||
http://192.168.4.7:8123
|
http://192.168.4.7:8123
|
||||||
```
|
```
|
||||||
|
|
||||||
2、我的`xiaozhi-esp32-server智控台`和`HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
|
> 手动查询 HA 的 IP 地址方法**(仅限小智esp32-server和HA部署在同一个网络设备[例如同一个wifi]下)**:
|
||||||
|
>
|
||||||
|
> 1. 进入 Home Assistant(前端)。
|
||||||
|
>
|
||||||
|
> 2. 点击左下角 **设置(Settings)** → **系统(System)** → **网络(Network)**。
|
||||||
|
>
|
||||||
|
> 3. 滑到最底部`Home Assistant 网址(Home Assistant website)`区域,在`本地网络(local network)`中,点击`眼睛`按钮,可以看到当前使用的 IP 地址(如 `192.168.1.10`)和网络接口。点击`复制连接(copy link)`可以直接复制。
|
||||||
|
>
|
||||||
|
> 
|
||||||
|
|
||||||
|
或,您已经设置了直接可以访问的Home Assistant的OAuth地址,您也可以在浏览器内直接访问
|
||||||
|
|
||||||
```
|
```
|
||||||
http://192.168.4.7:8002
|
http://homeassistant.local:8123
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 2. 登录`Home Assistant`拿到开发密钥
|
||||||
|
|
||||||
## 配置步骤
|
登录`HomeAssistant`,点击`左下角头像 -> 个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key,并复制保存,后续的方法都需要使用这个api key且仅出现一次(小tips: 您可以保存生成的二维码图像,后续可以扫描二维码再此提取api key)。
|
||||||
|
|
||||||
### 1. 登录`HomeAssistant`整理需要控制的设备清单
|
## 方法1:小智社区共建的HA调用功能
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
|
||||||
|
- 如您后续需要增加新的设备,该方法需要手动重启`xiaozhi-esp32-server服务端`以此更新设备信息**(重要**)。
|
||||||
|
|
||||||
|
- 需要您确保已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`。
|
||||||
|
|
||||||
|
- 需要您确保`xiaozhi-esp32-server智控台`能正常使用。
|
||||||
|
|
||||||
|
- 我的`xiaozhi-esp32-server智控台`和`HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8002
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 配置步骤
|
||||||
|
|
||||||
|
#### 1. 登录`HomeAssistant`整理需要控制的设备清单
|
||||||
|
|
||||||
登录`HomeAssistant`,点击`左下角的设置`,然后进入`设备与服务`,再点击顶部的`实体`。
|
登录`HomeAssistant`,点击`左下角的设置`,然后进入`设备与服务`,再点击顶部的`实体`。
|
||||||
|
|
||||||
@@ -56,12 +92,7 @@ http://192.168.4.7:8002
|
|||||||
这段字符,我们成为“设备清单字符”需要保存好,等一下有用。
|
这段字符,我们成为“设备清单字符”需要保存好,等一下有用。
|
||||||
|
|
||||||
|
|
||||||
### 2. 登录`HomeAssistant`拿到开发密钥
|
#### 2. 登录`智控台`
|
||||||
|
|
||||||
登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
|
||||||
|
|
||||||
|
|
||||||
### 3. 登录`智控台`
|
|
||||||
|
|
||||||
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来
|
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来
|
||||||
|
|
||||||
@@ -77,7 +108,7 @@ http://192.168.4.7:8123
|
|||||||
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去
|
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去
|
||||||
|
|
||||||
|
|
||||||
### 4. 设置`意图识别`函数
|
#### 3. 设置`意图识别`函数
|
||||||
|
|
||||||
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
|
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
|
||||||
|
|
||||||
@@ -94,18 +125,124 @@ change_role;get_weather;get_news;play_music
|
|||||||
change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state
|
change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. 手动重启xiaozhi-server
|
#### 4. 手动重启xiaozhi-server
|
||||||
|
|
||||||
重启xiaozhi-server程序
|
重启xiaozhi-server程序
|
||||||
|
|
||||||
|
|
||||||
### 5. 确认角色配置是否设置了函数意图识别
|
#### 5. 确认角色配置是否设置了函数意图识别
|
||||||
|
|
||||||
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
|
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
|
||||||
|
|
||||||
确认意图识别(Intent),是否选择“函数调用意图识别”
|
确认意图识别(Intent),是否选择“函数调用意图识别”
|
||||||
|
|
||||||
|
|
||||||
### 6. 唤醒设别进行控制
|
#### 6. 唤醒设别进行控制
|
||||||
|
|
||||||
尝试和esp32说,“打开XXX灯”
|
尝试和esp32说,“打开XXX灯”
|
||||||
|
|
||||||
|
## 方法2:小智将Home Assistant的语音助手作为LLM工具
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
|
||||||
|
- 该方法有一个比较严重的缺点——**该方法无法使用小智开源生态的function_call插件功能的能力**,因为使用Home Assistant作为小智的LLM工具会将意图识别能力转让给Home Assistant。但是**这个方法是能体验到原生的Home Assistant操作功能,且小智的聊天能力不变**。如实在介意可以使用同样是Home Assistant支持的[方法3](##方法3:使用Home Assistant的MCP服务(推荐)),能够最大程度体验到Home Assistant的功能。
|
||||||
|
|
||||||
|
### 配置步骤:
|
||||||
|
|
||||||
|
#### 1. 配置Home Assistant的大模型语音助手。
|
||||||
|
|
||||||
|
**需要您提前配置好Home Assistant的语音助手或大模型工具。**
|
||||||
|
|
||||||
|
#### 2. 获取Home Assistant的语言助手的Agent ID.
|
||||||
|
|
||||||
|
1. 进入Home Assistant页面内。左侧点击`开发者助手`。
|
||||||
|
2. 在打开的`开发者助手`内,点击`动作`选项卡(如图示操作1),在页面内的选项栏`动作`中,找到或输入`conversation.process(对话-处理)`并选择`对话(conversation): 处理`(如图示操作2)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. 在页面内勾选`代理(agent)`选项,在变成常亮的`对话代理(conversation agent)`内选择您步骤一配置好的语音助手名称,如图示,我这边配置好的是`ZhipuAi`并选择。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. 选中后,点击表单左下方的`进入YAML模式`。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. 复制其中的agent-id的值,例如图示中我的是`01JP2DYMBDF7F4ZA2DMCF2AGX2`(仅供参考)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. 切换到小智开源服务端`xiaozhi-esp32-server`的`config.yaml`文件内,在LLM配置中,找到Home Assistant,设置您的Home Assistant的网络地址,Api key和刚刚查询到的agent_id。
|
||||||
|
7. 修改`config.yaml`文件内的`selected_module`属性的`LLM`为`HomeAssistant`,`Intent`为`nointent`。
|
||||||
|
8. 重启小智开源服务端`xiaozhi-esp32-server`即可正常使用。
|
||||||
|
|
||||||
|
## 方法3:使用Home Assistant的MCP服务(推荐)
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
|
||||||
|
- 需要您提前在Home Assistant内集成并安装好HA集成——[Model Context Protocol Server](https://www.home-assistant.io/integrations/mcp_server/)。
|
||||||
|
|
||||||
|
- 这个方法与方法2都是HA官方提供的解决方法,与方法2不同的是,您可以正常使用小智开源服务端`xiaozhi-esp32-server`的开源共建的插件,同时允许您随意使用任何一个支持function_call功能的LLM大模型。
|
||||||
|
|
||||||
|
### 配置步骤
|
||||||
|
|
||||||
|
#### 1. 安装Home Assistant的MCP服务集成。
|
||||||
|
|
||||||
|
集成官方网址——[Model Context Protocol Server](https://www.home-assistant.io/integrations/mcp_server/)。。
|
||||||
|
|
||||||
|
或跟随以下手动操作。
|
||||||
|
|
||||||
|
> - 前往Home Assistant页面的**[设置 > 设备和服务(Settings > Devices & Services.)](https://my.home-assistant.io/redirect/integrations)**。
|
||||||
|
>
|
||||||
|
> - 在右下角,选择 **[添加集成(Add Integration)](https://my.home-assistant.io/redirect/config_flow_start?domain=mcp_server)**按钮。
|
||||||
|
>
|
||||||
|
> - 从列表中选择**模型上下文协议服务器(Model Context Protocol Server)**。
|
||||||
|
>
|
||||||
|
> - 按照屏幕上的说明完成设置。
|
||||||
|
|
||||||
|
#### 2. 配置小智开源服务端MCP配置信息
|
||||||
|
|
||||||
|
切换到小智开源服务端`xiaozhi-esp32-server`的`mcp_server_settings.json`文件内,在`"mcpServers"`的括号内添加以下内容:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"Home Assistant": {
|
||||||
|
"command": "mcp-proxy",
|
||||||
|
"args": [
|
||||||
|
"http://YOUR_HA_HOST/mcp_server/sse"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"API_ACCESS_TOKEN": "YOUR_API_ACCESS_TOKEN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
1. **替换配置:**
|
||||||
|
- 替换`args`内的`YOUR_HA_HOST`为您的HA服务地址,如果你的服务地址已经包含了https/http字样(例如`http://192.168.1.101:8123`),则只需要填入`192.168.1.101:8123`即可。
|
||||||
|
- 将`env`内`API_ACCESS_TOKEN`的`YOUR_API_ACCESS_TOKEN`替换成您之前获取到的开发密钥api key。
|
||||||
|
2. **如果你添加配置是在`"mcpServers"`的括号内后续没有新的`mcpServers`的配置时,需要把最后的逗号`,`移除**,否则可能会解析失败。
|
||||||
|
|
||||||
|
**最后效果参考以下(参考如下)**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"mcpServers": {
|
||||||
|
"Home Assistant": {
|
||||||
|
"command": "mcp-proxy",
|
||||||
|
"args": [
|
||||||
|
"http://192.168.1.101:8123/mcp_server/sse"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"API_ACCESS_TOKEN": "abcd.efghi.jkl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 配置小智开源服务端的系统配置
|
||||||
|
|
||||||
|
1. **选择任意一款支持function_call的LLM大模型作为小智的LLM聊天助手(但不要选择Home Assistant作为LLM工具)**,本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用,但部分时候调用不太稳定,如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-1-5-pro-32k-250115。
|
||||||
|
|
||||||
|
2. 切换到小智开源服务端`xiaozhi-esp32-server`的`config.yaml`文件内,设置您的LLM大模型配置,并且将`selected_module`配置的`Intent`调整为`function_call`。
|
||||||
|
|
||||||
|
3. 重启小智开源服务端`xiaozhi-esp32-server`即可正常使用。
|
||||||
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
package xiaozhi.common.config;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.RejectedExecutionHandler;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
@EnableAspectJAutoProxy(exposeProxy = true)
|
||||||
|
public class AsyncConfig {
|
||||||
|
|
||||||
|
@Bean(name = "taskExecutor")
|
||||||
|
public Executor taskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(2);
|
||||||
|
executor.setMaxPoolSize(4);
|
||||||
|
executor.setQueueCapacity(1000);
|
||||||
|
executor.setThreadNamePrefix("AsyncThread-");
|
||||||
|
// 设置拒绝策略:由调用线程执行
|
||||||
|
executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
|
||||||
|
@Override
|
||||||
|
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||||
|
try {
|
||||||
|
// 如果线程池已满,则由调用线程执行
|
||||||
|
r.run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("执行异步任务失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package xiaozhi.common.constant;
|
package xiaozhi.common.constant;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 常量
|
* 常量
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
@@ -109,6 +111,11 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String FILE_EXTENSION_SEG = ".";
|
String FILE_EXTENSION_SEG = ".";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无记忆
|
||||||
|
*/
|
||||||
|
String MEMORY_NO_MEM = "Memory_nomem";
|
||||||
|
|
||||||
enum SysBaseParam {
|
enum SysBaseParam {
|
||||||
/**
|
/**
|
||||||
* 系统全称
|
* 系统全称
|
||||||
@@ -174,8 +181,23 @@ public interface Constant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
enum ChatHistoryConfEnum {
|
||||||
|
IGNORE(0, "不记录"),
|
||||||
|
RECORD_TEXT(1, "记录文本"),
|
||||||
|
RECORD_TEXT_AUDIO(2, "文本音频都记录");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
ChatHistoryConfEnum(int code, String name) {
|
||||||
|
this.code = code;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.4.2";
|
public static final String VERSION = "0.4.3";
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,7 @@ import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
|||||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
@@ -46,6 +47,7 @@ import xiaozhi.modules.agent.service.AgentChatAudioService;
|
|||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
@@ -109,6 +111,8 @@ public class AgentController {
|
|||||||
entity.setMemModelId(template.getMemModelId());
|
entity.setMemModelId(template.getMemModelId());
|
||||||
entity.setIntentModelId(template.getIntentModelId());
|
entity.setIntentModelId(template.getIntentModelId());
|
||||||
entity.setSystemPrompt(template.getSystemPrompt());
|
entity.setSystemPrompt(template.getSystemPrompt());
|
||||||
|
entity.setSummaryMemory(template.getSummaryMemory());
|
||||||
|
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||||
entity.setLangCode(template.getLangCode());
|
entity.setLangCode(template.getLangCode());
|
||||||
entity.setLanguage(template.getLanguage());
|
entity.setLanguage(template.getLanguage());
|
||||||
}
|
}
|
||||||
@@ -125,10 +129,26 @@ public class AgentController {
|
|||||||
return new Result<String>().ok(entity.getId());
|
return new Result<String>().ok(entity.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/saveMemory/{macAddress}")
|
||||||
|
@Operation(summary = "根据设备id更新智能体")
|
||||||
|
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
|
||||||
|
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||||
|
if (device == null) {
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
||||||
|
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
||||||
|
return updateAgentById(device.getAgentId(), agentUpdateDTO);
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "更新智能体")
|
@Operation(summary = "更新智能体")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
||||||
|
return updateAgentById(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<Void> updateAgentById(String id, AgentUpdateDTO dto) {
|
||||||
// 先查询现有实体
|
// 先查询现有实体
|
||||||
AgentEntity existingEntity = agentService.getAgentById(id);
|
AgentEntity existingEntity = agentService.getAgentById(id);
|
||||||
if (existingEntity == null) {
|
if (existingEntity == null) {
|
||||||
@@ -166,6 +186,12 @@ public class AgentController {
|
|||||||
if (dto.getSystemPrompt() != null) {
|
if (dto.getSystemPrompt() != null) {
|
||||||
existingEntity.setSystemPrompt(dto.getSystemPrompt());
|
existingEntity.setSystemPrompt(dto.getSystemPrompt());
|
||||||
}
|
}
|
||||||
|
if (dto.getSummaryMemory() != null) {
|
||||||
|
existingEntity.setSummaryMemory(dto.getSummaryMemory());
|
||||||
|
}
|
||||||
|
if (dto.getChatHistoryConf() != null) {
|
||||||
|
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
|
||||||
|
}
|
||||||
if (dto.getLangCode() != null) {
|
if (dto.getLangCode() != null) {
|
||||||
existingEntity.setLangCode(dto.getLangCode());
|
existingEntity.setLangCode(dto.getLangCode());
|
||||||
}
|
}
|
||||||
@@ -181,8 +207,16 @@ public class AgentController {
|
|||||||
existingEntity.setUpdater(user.getId());
|
existingEntity.setUpdater(user.getId());
|
||||||
existingEntity.setUpdatedAt(new Date());
|
existingEntity.setUpdatedAt(new Date());
|
||||||
|
|
||||||
|
// 更新记忆策略
|
||||||
|
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
|
// 删除所有记录
|
||||||
|
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
|
||||||
|
existingEntity.setSummaryMemory("");
|
||||||
|
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
|
||||||
|
// 删除音频数据
|
||||||
|
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
||||||
|
}
|
||||||
agentService.updateById(existingEntity);
|
agentService.updateById(existingEntity);
|
||||||
|
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +227,7 @@ public class AgentController {
|
|||||||
// 先删除关联的设备
|
// 先删除关联的设备
|
||||||
deviceService.deleteByAgentId(id);
|
deviceService.deleteByAgentId(id);
|
||||||
// 删除关联的聊天记录
|
// 删除关联的聊天记录
|
||||||
agentChatHistoryService.deleteByAgentId(id);
|
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||||
// 再删除智能体
|
// 再删除智能体
|
||||||
agentService.deleteById(id);
|
agentService.deleteById(id);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
|
|||||||
@@ -28,4 +28,11 @@ public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity
|
|||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
*/
|
*/
|
||||||
void deleteHistoryByAgentId(String agentId);
|
void deleteHistoryByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除音频ID
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteAudioIdByAgentId(String agentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,16 @@ public class AgentDTO {
|
|||||||
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
||||||
private String llmModelName;
|
private String llmModelName;
|
||||||
|
|
||||||
|
@Schema(description = "记忆模型ID", example = "mem_model_01")
|
||||||
|
private String memModelId;
|
||||||
|
|
||||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
||||||
private String systemPrompt;
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||||
|
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||||
|
private String summaryMemory;
|
||||||
|
|
||||||
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
||||||
private Date lastConnectedAt;
|
private Date lastConnectedAt;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体记忆更新DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体记忆更新对象")
|
||||||
|
public class AgentMemoryDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||||
|
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||||
|
private String summaryMemory;
|
||||||
|
}
|
||||||
@@ -45,6 +45,13 @@ public class AgentUpdateDTO implements Serializable {
|
|||||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
||||||
private String systemPrompt;
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||||
|
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||||
|
private String summaryMemory;
|
||||||
|
|
||||||
|
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false)
|
||||||
|
private Integer chatHistoryConf;
|
||||||
|
|
||||||
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
||||||
private String langCode;
|
private String langCode;
|
||||||
|
|
||||||
|
|||||||
@@ -48,9 +48,16 @@ public class AgentEntity {
|
|||||||
@Schema(description = "意图模型标识")
|
@Schema(description = "意图模型标识")
|
||||||
private String intentModelId;
|
private String intentModelId;
|
||||||
|
|
||||||
|
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)")
|
||||||
|
private Integer chatHistoryConf;
|
||||||
|
|
||||||
@Schema(description = "角色设定参数")
|
@Schema(description = "角色设定参数")
|
||||||
private String systemPrompt;
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||||
|
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||||
|
private String summaryMemory;
|
||||||
|
|
||||||
@Schema(description = "语言编码")
|
@Schema(description = "语言编码")
|
||||||
private String langCode;
|
private String langCode;
|
||||||
|
|
||||||
|
|||||||
@@ -69,11 +69,20 @@ public class AgentTemplateEntity implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String intentModelId;
|
private String intentModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)
|
||||||
|
*/
|
||||||
|
private Integer chatHistoryConf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 角色设定参数
|
* 角色设定参数
|
||||||
*/
|
*/
|
||||||
private String systemPrompt;
|
private String systemPrompt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总结记忆
|
||||||
|
*/
|
||||||
|
private String summaryMemory;
|
||||||
/**
|
/**
|
||||||
* 语言编码
|
* 语言编码
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
|
|||||||
/**
|
/**
|
||||||
* 根据智能体ID删除聊天记录
|
* 根据智能体ID删除聊天记录
|
||||||
*
|
*
|
||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
|
* @param deleteAudio 是否删除音频
|
||||||
|
* @param deleteText 是否删除文本
|
||||||
*/
|
*/
|
||||||
void deleteByAgentId(String agentId);
|
void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package xiaozhi.modules.agent.service.biz.impl;
|
|||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
@@ -47,8 +49,33 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
Byte chatType = report.getChatType();
|
Byte chatType = report.getChatType();
|
||||||
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
||||||
|
|
||||||
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||||
|
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||||
|
if (agentEntity == null) {
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer chatHistoryConf = agentEntity.getChatHistoryConf();
|
||||||
|
String agentId = agentEntity.getId();
|
||||||
|
|
||||||
|
if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT.getCode())) {
|
||||||
|
saveChatText(report, agentId, macAddress, null);
|
||||||
|
} else if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode())) {
|
||||||
|
String audioId = saveChatAudio(report);
|
||||||
|
saveChatText(report, agentId, macAddress, audioId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设备最后对话时间
|
||||||
|
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||||
|
*/
|
||||||
|
private String saveChatAudio(AgentChatHistoryReportDTO report) {
|
||||||
String audioId = null;
|
String audioId = null;
|
||||||
|
|
||||||
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
||||||
@@ -56,20 +83,18 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
log.info("音频数据保存成功,audioId={}", audioId);
|
log.info("音频数据保存成功,audioId={}", audioId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("音频数据保存失败", e);
|
log.error("音频数据保存失败", e);
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return audioId;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 组装上报数据
|
/**
|
||||||
// 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
* 组装上报数据
|
||||||
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
*/
|
||||||
if (agentEntity == null) {
|
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String agentId = agentEntity.getId();
|
|
||||||
log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
|
|
||||||
|
|
||||||
// 2.2 构建聊天记录实体
|
// 构建聊天记录实体
|
||||||
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||||
.macAddress(macAddress)
|
.macAddress(macAddress)
|
||||||
.agentId(agentId)
|
.agentId(agentId)
|
||||||
@@ -79,10 +104,9 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
.audioId(audioId)
|
.audioId(audioId)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 3. 保存数据
|
// 保存数据
|
||||||
agentChatHistoryService.save(entity);
|
agentChatHistoryService.save(entity);
|
||||||
// 4. 更新设备最后对话时间
|
|
||||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
log.info("设备 {} 对应智能体 {} 上报成功", macAddress, agentId);
|
||||||
return Boolean.TRUE;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,16 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteByAgentId(String agentId) {
|
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||||
baseMapper.deleteAudioByAgentId(agentId);
|
if (deleteAudio) {
|
||||||
baseMapper.deleteHistoryByAgentId(agentId);
|
baseMapper.deleteAudioByAgentId(agentId);
|
||||||
|
}
|
||||||
|
if (deleteAudio && !deleteText) {
|
||||||
|
baseMapper.deleteAudioIdByAgentId(agentId);
|
||||||
|
}
|
||||||
|
if (deleteText) {
|
||||||
|
baseMapper.deleteHistoryByAgentId(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -46,7 +47,15 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AgentEntity getAgentById(String id) {
|
public AgentEntity getAgentById(String id) {
|
||||||
return agentDao.selectById(id);
|
AgentEntity agent = agentDao.selectById(id);
|
||||||
|
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
|
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||||
|
} else if (agent != null && agent.getMemModelId() != null
|
||||||
|
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||||
|
&& agent.getChatHistoryConf() == null) {
|
||||||
|
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||||
|
}
|
||||||
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,6 +102,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
// 获取 LLM 模型名称
|
// 获取 LLM 模型名称
|
||||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||||
|
|
||||||
|
// 获取记忆模型名称
|
||||||
|
dto.setMemModelId(agent.getMemModelId());
|
||||||
|
|
||||||
// 获取 TTS 音色名称
|
// 获取 TTS 音色名称
|
||||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
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.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
@@ -64,6 +65,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
agent.getVadModelId(),
|
agent.getVadModelId(),
|
||||||
agent.getAsrModelId(),
|
agent.getAsrModelId(),
|
||||||
null,
|
null,
|
||||||
@@ -108,6 +110,17 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
// 获取单台设备每天最多输出字数
|
// 获取单台设备每天最多输出字数
|
||||||
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||||
result.put("device_max_output_size", deviceMaxOutputSize);
|
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||||
|
|
||||||
|
// 获取聊天记录配置
|
||||||
|
Integer chatHistoryConf = agent.getChatHistoryConf();
|
||||||
|
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
|
chatHistoryConf = Constant.ChatHistoryConfEnum.IGNORE.getCode();
|
||||||
|
} else if (agent.getMemModelId() != null
|
||||||
|
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||||
|
&& agent.getChatHistoryConf() == null) {
|
||||||
|
chatHistoryConf = Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode();
|
||||||
|
}
|
||||||
|
result.put("chat_history_conf", chatHistoryConf);
|
||||||
// 如果客户端已实例化模型,则不返回
|
// 如果客户端已实例化模型,则不返回
|
||||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||||
@@ -122,6 +135,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
buildModuleConfig(
|
buildModuleConfig(
|
||||||
agent.getAgentName(),
|
agent.getAgentName(),
|
||||||
agent.getSystemPrompt(),
|
agent.getSystemPrompt(),
|
||||||
|
agent.getSummaryMemory(),
|
||||||
voice,
|
voice,
|
||||||
agent.getVadModelId(),
|
agent.getVadModelId(),
|
||||||
agent.getAsrModelId(),
|
agent.getAsrModelId(),
|
||||||
@@ -222,6 +236,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
private void buildModuleConfig(
|
private void buildModuleConfig(
|
||||||
String assistantName,
|
String assistantName,
|
||||||
String prompt,
|
String prompt,
|
||||||
|
String summaryMemory,
|
||||||
String voice,
|
String voice,
|
||||||
String vadModelId,
|
String vadModelId,
|
||||||
String asrModelId,
|
String asrModelId,
|
||||||
@@ -282,5 +297,6 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||||
}
|
}
|
||||||
result.put("prompt", prompt);
|
result.put("prompt", prompt);
|
||||||
|
result.put("summaryMemory", summaryMemory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import java.util.TimeZone;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.aop.framework.AopContext;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
@@ -54,6 +56,24 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
private final OtaService otaService;
|
private final OtaService otaService;
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
||||||
|
try {
|
||||||
|
DeviceEntity device = new DeviceEntity();
|
||||||
|
device.setId(deviceId);
|
||||||
|
device.setLastConnectedAt(new Date());
|
||||||
|
if (StringUtils.isNotBlank(appVersion)) {
|
||||||
|
device.setAppVersion(appVersion);
|
||||||
|
}
|
||||||
|
deviceDao.updateById(device);
|
||||||
|
if (StringUtils.isNotBlank(agentId)) {
|
||||||
|
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("异步更新设备连接信息失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||||
if (StringUtils.isBlank(activationCode)) {
|
if (StringUtils.isBlank(activationCode)) {
|
||||||
@@ -118,14 +138,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
||||||
|
|
||||||
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
// 设备未绑定,则返回当前上传的固件信息(不更新)以此兼容旧固件版本
|
||||||
if (deviceById != null && deviceById.getAutoUpdate() != 0) {
|
if (deviceById == null) {
|
||||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
firmware.setUrl("http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL");
|
||||||
response.setFirmware(firmware);
|
response.setFirmware(firmware);
|
||||||
} else {
|
} else {
|
||||||
response.setFirmware(null);
|
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
||||||
|
if (deviceById.getAutoUpdate() != 0) {
|
||||||
|
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||||
|
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||||
|
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||||
|
response.setFirmware(firmware);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加WebSocket配置
|
// 添加WebSocket配置
|
||||||
@@ -150,13 +176,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
response.setWebsocket(websocket);
|
response.setWebsocket(websocket);
|
||||||
|
|
||||||
if (deviceById != null) {
|
if (deviceById != null) {
|
||||||
// 如果设备存在,则更新上次连接时间
|
// 如果设备存在,则异步更新上次连接时间和版本信息
|
||||||
deviceById.setLastConnectedAt(new Date());
|
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
|
||||||
if (deviceReport.getApplication() != null
|
: null;
|
||||||
&& StringUtils.isNotBlank(deviceReport.getApplication().getVersion())) {
|
// 通过Spring代理调用异步方法
|
||||||
deviceById.setAppVersion(deviceReport.getApplication().getVersion());
|
((DeviceServiceImpl) AopContext.currentProxy()).updateDeviceConnectionInfo(deviceById.getAgentId(),
|
||||||
}
|
deviceById.getId(), appVersion);
|
||||||
deviceDao.updateById(deviceById);
|
|
||||||
} else {
|
} else {
|
||||||
// 如果设备不存在,则生成激活码
|
// 如果设备不存在,则生成激活码
|
||||||
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||||
@@ -385,4 +410,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public class ShiroConfig {
|
|||||||
// 将config路径使用server服务过滤器
|
// 将config路径使用server服务过滤器
|
||||||
filterMap.put("/config/**", "server");
|
filterMap.put("/config/**", "server");
|
||||||
filterMap.put("/agent/chat-history/report", "server");
|
filterMap.put("/agent/chat-history/report", "server");
|
||||||
|
filterMap.put("/agent/saveMemory/**", "server");
|
||||||
filterMap.put("/agent/play/**", "anon");
|
filterMap.put("/agent/play/**", "anon");
|
||||||
filterMap.put("/**", "oauth2");
|
filterMap.put("/**", "oauth2");
|
||||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- 更新模型供应器表
|
||||||
|
UPDATE `ai_model_provider` SET fields = '[{"key": "host", "type": "string", "label": "服务地址"}, {"key": "port", "type": "number", "label": "端口号"}, {"key": "type", "type": "string", "label": "服务类型"}, {"key": "is_ssl", "type": "boolean", "label": "是否使用SSL"}, {"key": "api_key", "type": "string", "label": "API密钥"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]' WHERE id = 'SYSTEM_ASR_FunASRServer';
|
||||||
|
|
||||||
|
-- 更新模型配置表
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
config_json = '{"host": "127.0.0.1", "port": 10096, "type": "fun_server", "is_ssl": true, "api_key": "none", "output_dir": "tmp/"}',
|
||||||
|
`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md',
|
||||||
|
`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话
|
||||||
|
第一句:mkdir -p ./funasr-runtime-resources/models
|
||||||
|
第二句:sudo docker run -p 10096:10095 -it --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||||
|
上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
||||||
|
不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
||||||
|
上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
||||||
|
第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了
|
||||||
|
以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer';
|
||||||
|
|
||||||
|
-- FishSpeech配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md',
|
||||||
|
`remark` = 'FishSpeech配置说明:
|
||||||
|
1. 需要本地部署FishSpeech服务
|
||||||
|
2. 支持自定义音色
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
5. 可参照教程https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md' WHERE `id` = 'TTS_FishSpeech';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 添加聊天记录配置字段
|
||||||
|
ALTER TABLE `ai_agent`
|
||||||
|
ADD COLUMN `chat_history_conf` tinyint NOT NULL DEFAULT 0 COMMENT '聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)' AFTER `system_prompt`;
|
||||||
|
|
||||||
|
ALTER TABLE `ai_agent_template`
|
||||||
|
ADD COLUMN `chat_history_conf` tinyint NOT NULL DEFAULT 0 COMMENT '聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)' AFTER `system_prompt`;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 添加总结记忆字段
|
||||||
|
ALTER TABLE `ai_agent`
|
||||||
|
ADD COLUMN `summary_memory` text COMMENT '总结记忆' AFTER `system_prompt`;
|
||||||
|
|
||||||
|
ALTER TABLE `ai_agent_template`
|
||||||
|
ADD COLUMN `summary_memory` text COMMENT '总结记忆' AFTER `system_prompt`;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
update ai_agent_template set system_prompt = replace(system_prompt, '我是', '你是');
|
||||||
|
|
||||||
|
delete from sys_params where id in (500,501,402);
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (500, 'end_prompt.enable', 'true', 'boolean', 1, '是否开启结束语');
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (501, 'end_prompt.prompt', '请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧!', 'string', 1, '结束提示词');
|
||||||
|
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (402, 'plugins.get_weather.api_host', 'mj7p3y7naa.re.qweatherapi.com', 'string', 1, '开发者apihost');
|
||||||
@@ -113,4 +113,32 @@ databaseChangeLog:
|
|||||||
changes:
|
changes:
|
||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202505091409.sql
|
path: classpath:db/changelog/202505091409.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505091555
|
||||||
|
author: whosmyqueen
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505091555.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505111914
|
||||||
|
author: hrz
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505111914.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505122348
|
||||||
|
author: ljwwd2
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505122348.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505142037
|
||||||
|
author: hrz
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505142037.sql
|
||||||
|
|||||||
@@ -28,11 +28,17 @@
|
|||||||
SELECT audio_id
|
SELECT audio_id
|
||||||
FROM ai_agent_chat_history
|
FROM ai_agent_chat_history
|
||||||
WHERE agent_id = #{agentId}
|
WHERE agent_id = #{agentId}
|
||||||
);
|
)
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
|
<update id="deleteAudioIdByAgentId">
|
||||||
|
UPDATE ai_agent_chat_history
|
||||||
|
SET audio_id = NULL
|
||||||
|
WHERE agent_id = #{agentId}
|
||||||
|
</update>
|
||||||
|
|
||||||
<delete id="deleteHistoryByAgentId">
|
<delete id="deleteHistoryByAgentId">
|
||||||
DELETE FROM ai_agent_chat_history
|
DELETE FROM ai_agent_chat_history
|
||||||
WHERE agent_id = #{agentId};
|
WHERE agent_id = #{agentId}
|
||||||
</delete>
|
</delete>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ export default {
|
|||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
// 获取智能体配置
|
// 获取智能体配置
|
||||||
getDeviceConfig(deviceId, callback) {
|
getDeviceConfig(agentId, callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/agent/${deviceId}`)
|
.url(`${getServiceUrl()}/agent/${agentId}`)
|
||||||
.method('GET')
|
.method('GET')
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime();
|
RequestService.clearRequestTime();
|
||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
.fail((err) => {
|
.fail((err) => {
|
||||||
console.error('获取配置失败:', err);
|
console.error('获取配置失败:', err);
|
||||||
RequestService.reAjaxFun(() => {
|
RequestService.reAjaxFun(() => {
|
||||||
this.getDeviceConfig(deviceId, callback);
|
this.getDeviceConfig(agentId, callback);
|
||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 88 KiB |
@@ -26,8 +26,12 @@
|
|||||||
<div class="settings-btn" @click="handleDeviceManage">
|
<div class="settings-btn" @click="handleDeviceManage">
|
||||||
设备管理({{ device.deviceCount }})
|
设备管理({{ device.deviceCount }})
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-btn" @click="handleChatHistory">
|
<div class="settings-btn" @click="handleChatHistory"
|
||||||
聊天记录
|
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
|
||||||
|
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="未开启记忆" placement="top">
|
||||||
|
<span>聊天记录</span>
|
||||||
|
</el-tooltip>
|
||||||
|
<span v-else>聊天记录</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
@@ -77,6 +81,9 @@ export default {
|
|||||||
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
||||||
},
|
},
|
||||||
handleChatHistory() {
|
handleChatHistory() {
|
||||||
|
if (this.device.memModelId === 'Memory_nomem') {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName })
|
this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +127,12 @@ export default {
|
|||||||
color: #979db1;
|
color: #979db1;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled-btn {
|
||||||
|
background: #e6e6e6;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose" @open="handleOpen">
|
<el-dialog :title="title" :visible.sync="dialogVisible" @close="handleClose" @open="handleOpen">
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="固件名称" prop="firmwareName">
|
<el-form-item label="固件名称" prop="firmwareName">
|
||||||
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
||||||
:status="uploadStatus"></el-progress>
|
:status="uploadStatus"></el-progress>
|
||||||
<div class="hint-text">
|
<div class="hint-text">
|
||||||
<span>温馨提示:请上传xiaozhi.bin文件,而不是merged-binary.bin文件</span>
|
<span>温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件</span>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
@@ -198,13 +198,13 @@ export default {
|
|||||||
if (!this.form.id) { // 只在新增时重置
|
if (!this.form.id) { // 只在新增时重置
|
||||||
this.form.firmwarePath = ''
|
this.form.firmwarePath = ''
|
||||||
this.form.size = 0
|
this.form.size = 0
|
||||||
// 重置上传组件
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.upload) {
|
|
||||||
this.$refs.upload.clearFiles()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
// 无论是否编辑模式,都重置上传组件
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.upload) {
|
||||||
|
this.$refs.upload.clearFiles()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,6 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: #979db1;
|
font-size: 14px;
|
||||||
font-size: 11px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer :visible.sync="dialogVisible" direction="rtl" size="50%" :wrapperClosable="false" :withHeader="false">
|
||||||
|
<!-- 自定义标题区域 -->
|
||||||
|
<div class="custom-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h3 class="bold-title">功能管理</h3>
|
||||||
|
</div>
|
||||||
|
<button class="custom-close-btn" @click="closeDialog">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="function-manager">
|
||||||
|
<!-- 左侧:未选功能 -->
|
||||||
|
<div class="function-column">
|
||||||
|
<div class="column-header">
|
||||||
|
<h4 class="column-title">未选功能</h4>
|
||||||
|
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="function-list">
|
||||||
|
<div v-for="func in unselected" :key="func.name" class="function-item">
|
||||||
|
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
|
||||||
|
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||||
|
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
|
||||||
|
<span>{{ func.name }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tooltip class="item" effect="dark" :content="func.description || '暂无功能描述'" placement="top">
|
||||||
|
<img src="@/assets/home/info.png" alt="" class="info-icon">
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间:已选功能 -->
|
||||||
|
<div class="function-column">
|
||||||
|
<div class="column-header">
|
||||||
|
<h4 class="column-title">已选功能</h4>
|
||||||
|
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="function-list">
|
||||||
|
<div v-for="func in selectedList" :key="func.name" class="function-item">
|
||||||
|
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
|
||||||
|
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||||
|
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
|
||||||
|
<span>{{ func.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:参数配置 -->
|
||||||
|
<div class="params-column">
|
||||||
|
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
|
||||||
|
<div v-if="currentFunction" class="params-container">
|
||||||
|
<el-form :model="currentFunction" size="mini" class="param-form" v-loading="loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
|
<el-form-item v-for="(value, key) in currentFunction.params" :key="key" :label="key" class="param-item">
|
||||||
|
<el-input v-model="currentFunction.params[key]" size="mini" class="param-input" @change="(val) => handleParamChange(currentFunction, key, val)"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<el-button @click="closeDialog">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveSelection">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
functions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogVisible: this.value,
|
||||||
|
selectedNames: [],
|
||||||
|
currentFunction: null,
|
||||||
|
modifiedFunctions: {},
|
||||||
|
allFunctions: [
|
||||||
|
{name: '天气', params: {city: '北京'}, description: '查看指定城市的天气情况'},
|
||||||
|
{name: '新闻', params: {type: '科技'}, description: '获取最新科技类新闻资讯'},
|
||||||
|
{name: '工具', params: {category: '常用'}, description: '提供常用工具集合'},
|
||||||
|
{name: '退出', params: {}, description: '退出当前系统'},
|
||||||
|
{name: '音乐', params: {genre: '流行'}, description: '播放流行音乐'},
|
||||||
|
{name: '翻译', params: {from: '中文', to: '英文'}, description: '提供中英文互译功能'},
|
||||||
|
{name: '计算', params: {precision: '2'}, description: '提供精确计算功能'},
|
||||||
|
{name: '日历', params: {view: '月'}, description: '查看月历视图'}
|
||||||
|
],
|
||||||
|
functionColorMap: [
|
||||||
|
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
||||||
|
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
||||||
|
],
|
||||||
|
tempFunctions: {},
|
||||||
|
// 添加一个标志位来跟踪是否已经保存
|
||||||
|
hasSaved: false,
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedList() {
|
||||||
|
return this.allFunctions.filter(f => this.selectedNames.includes(f.name));
|
||||||
|
},
|
||||||
|
unselected() {
|
||||||
|
return this.allFunctions.filter(f => !this.selectedNames.includes(f.name));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
this.dialogVisible = newVal;
|
||||||
|
if (newVal) {
|
||||||
|
this.selectedNames = this.functions.map(f => f.name);
|
||||||
|
this.currentFunction = this.selectedList[0] || null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialogVisible(newVal) {
|
||||||
|
this.$emit('input', newVal);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleFunctionClick(func) {
|
||||||
|
if (this.selectedNames.includes(func.name)) {
|
||||||
|
this.loading = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
const tempFunc = this.tempFunctions[func.name];
|
||||||
|
this.currentFunction = tempFunc ? tempFunc : JSON.parse(JSON.stringify(func));
|
||||||
|
this.loading = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleParamChange(func, key, value) {
|
||||||
|
if (!this.tempFunctions[func.name]) {
|
||||||
|
this.tempFunctions[func.name] = JSON.parse(JSON.stringify(func));
|
||||||
|
}
|
||||||
|
this.tempFunctions[func.name].params[key] = value;
|
||||||
|
},
|
||||||
|
handleCheckboxChange(func, checked) {
|
||||||
|
if (checked) {
|
||||||
|
if (!this.selectedNames.includes(func.name)) {
|
||||||
|
this.selectedNames = [...this.selectedNames, func.name];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectedNames = this.selectedNames.filter(name => name !== func.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedList.length > 0) {
|
||||||
|
this.currentFunction = this.selectedList[0];
|
||||||
|
} else {
|
||||||
|
this.currentFunction = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectAll() {
|
||||||
|
this.selectedNames = [...this.allFunctions.map(f => f.name)];
|
||||||
|
if (this.selectedList.length > 0) {
|
||||||
|
this.currentFunction = JSON.parse(JSON.stringify(this.selectedList[0]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deselectAll() {
|
||||||
|
this.selectedNames = [];
|
||||||
|
this.currentFunction = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDialog() {
|
||||||
|
this.tempFunctions = {};
|
||||||
|
this.selectedNames = this.functions.map(f => f.name);
|
||||||
|
this.currentFunction = null;
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.$emit('input', false);
|
||||||
|
this.$emit('dialog-closed', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
saveSelection() {
|
||||||
|
Object.keys(this.tempFunctions).forEach(name => {
|
||||||
|
this.modifiedFunctions[name] = JSON.parse(JSON.stringify(this.tempFunctions[name]));
|
||||||
|
});
|
||||||
|
this.tempFunctions = {};
|
||||||
|
this.hasSaved = true;
|
||||||
|
|
||||||
|
const selected = this.selectedList.map(f => {
|
||||||
|
const modified = this.modifiedFunctions[f.name];
|
||||||
|
return modified || f;
|
||||||
|
}).map(f => ({
|
||||||
|
...f,
|
||||||
|
params: JSON.parse(JSON.stringify(f.params))
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.$emit('update-functions', selected);
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.$message.success('配置保存成功');
|
||||||
|
// 通知父组件对话框已关闭且已保存
|
||||||
|
this.$emit('dialog-closed', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
getFunctionColor(name) {
|
||||||
|
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||||
|
return this.functionColorMap[hash % 7];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.function-manager {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(120px, 0.5fr) minmax(120px, 0.5fr) minmax(200px, 2fr);
|
||||||
|
gap: 12px;
|
||||||
|
height: calc(70vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid #EBEEF5;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all-btn {
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-column {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #EBEEF5;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-column::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-column {
|
||||||
|
min-width: 280px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-column::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-title {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.func-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-form {
|
||||||
|
::v-deep .el-form-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-size: 14px !important;
|
||||||
|
color: #606266;
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-container {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
padding: 20px;
|
||||||
|
color: #909399;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
padding: 10px 16px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 10px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
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;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
border-color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-checkbox__label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
OTA管理
|
OTA管理
|
||||||
</div>
|
</div>
|
||||||
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
||||||
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' }">
|
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' }" @visible-change="handleParamDropdownVisibleChange">
|
||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||||
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
参数字典
|
参数字典
|
||||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
||||||
</span>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
<el-dropdown-item @click.native="goParamManagement">
|
<el-dropdown-item @click.native="goParamManagement">
|
||||||
@@ -49,22 +49,26 @@
|
|||||||
<el-dropdown-item @click.native="goDictManagement">
|
<el-dropdown-item @click.native="goDictManagement">
|
||||||
字典管理
|
字典管理
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="goProviderManagement">
|
||||||
|
供应器管理
|
||||||
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧元素 -->
|
<!-- 右侧元素 -->
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-container" v-if="$route.path === '/home'">
|
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
|
||||||
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
||||||
@keyup.enter.native="handleSearch">
|
@keyup.enter.native="handleSearch">
|
||||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
||||||
<el-dropdown trigger="click" class="user-dropdown">
|
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
|
||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
{{ userInfo.username || '加载中...' }}
|
||||||
|
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
|
||||||
</span>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
||||||
@@ -84,7 +88,6 @@ import userApi from '@/apis/module/user';
|
|||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HeaderBar',
|
name: 'HeaderBar',
|
||||||
components: {
|
components: {
|
||||||
@@ -98,7 +101,10 @@ export default {
|
|||||||
username: '',
|
username: '',
|
||||||
mobile: ''
|
mobile: ''
|
||||||
},
|
},
|
||||||
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
|
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
||||||
|
userDropdownVisible: false,
|
||||||
|
paramDropdownVisible: false,
|
||||||
|
isSmallScreen: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -108,7 +114,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchUserInfo()
|
this.fetchUserInfo();
|
||||||
|
this.checkScreenSize();
|
||||||
|
window.addEventListener('resize', this.checkScreenSize);
|
||||||
|
},
|
||||||
|
//移除事件监听器
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.checkScreenSize);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goHome() {
|
goHome() {
|
||||||
@@ -130,6 +142,9 @@ export default {
|
|||||||
goDictManagement() {
|
goDictManagement() {
|
||||||
this.$router.push('/dict-management')
|
this.$router.push('/dict-management')
|
||||||
},
|
},
|
||||||
|
goProviderManagement() {
|
||||||
|
this.$router.push('/provider-management')
|
||||||
|
},
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
fetchUserInfo() {
|
fetchUserInfo() {
|
||||||
userApi.getUserInfo(({ data }) => {
|
userApi.getUserInfo(({ data }) => {
|
||||||
@@ -139,7 +154,9 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
checkScreenSize() {
|
||||||
|
this.isSmallScreen = window.innerWidth <= 1386;
|
||||||
|
},
|
||||||
// 处理搜索
|
// 处理搜索
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
const searchValue = this.search.trim();
|
const searchValue = this.search.trim();
|
||||||
@@ -184,6 +201,13 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleUserDropdownVisibleChange(visible) {
|
||||||
|
this.userDropdownVisible = visible;
|
||||||
|
},
|
||||||
|
// 监听第二个下拉菜单的可见状态变化
|
||||||
|
handleParamDropdownVisibleChange(visible) {
|
||||||
|
this.paramDropdownVisible = visible;
|
||||||
|
},
|
||||||
|
|
||||||
// 使用 mapActions 引入 Vuex 的 logout action
|
// 使用 mapActions 引入 Vuex 的 logout action
|
||||||
...mapActions(['logout'])
|
...mapActions(['logout'])
|
||||||
@@ -191,7 +215,7 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.header {
|
.header {
|
||||||
background: #f6fcfe66;
|
background: #f6fcfe66;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
@@ -243,8 +267,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management {
|
.equipment-management {
|
||||||
padding: 0 9px;
|
|
||||||
width: px;
|
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
background: #deeafe;
|
background: #deeafe;
|
||||||
@@ -260,7 +282,8 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
/* 防止导航按钮被压缩 */
|
/* 防止导航按钮被压缩 */
|
||||||
padding: 0px 15px;
|
padding: 0 15px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management.active-tab {
|
.equipment-management.active-tab {
|
||||||
@@ -309,6 +332,25 @@ export default {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-dropdown {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-dropdown .el-dropdown-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-down {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon-arrow-down {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.header-center {
|
.header-center {
|
||||||
@@ -316,50 +358,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management {
|
.equipment-management {
|
||||||
width: 70px;
|
width: 79px;
|
||||||
font-size: 9px;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.equipment-management.more-dropdown {
|
.equipment-management.more-dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -378,13 +381,4 @@ export default {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.equipment-management.more-dropdown .el-dropdown-menu {
|
|
||||||
position: fixed;
|
|
||||||
right: 10px;
|
|
||||||
top: 60px;
|
|
||||||
z-index: 2000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :visible="visible" @update:visible="handleVisibleChange" width="57%" 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: -15px; margin-bottom: 20px; text-align: center;">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="custom-close-btn" @click="handleClose">×</button>
|
||||||
|
|
||||||
|
<el-form :model="form" label-width="100px" :rules="rules" ref="form" class="custom-form">
|
||||||
|
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||||
|
<el-form-item label="类别" prop="model_type" style="flex: 1;">
|
||||||
|
<el-select v-model="form.model_type" placeholder="请选择类别" class="custom-input-bg" style="width: 100%;">
|
||||||
|
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="供应器编码" prop="provider_code" style="flex: 1;">
|
||||||
|
<el-input v-model="form.provider_code" placeholder="请输入供应器编码" class="custom-input-bg"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||||
|
<el-form-item label="名称" prop="name" style="flex: 1;">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入供应器名称" class="custom-input-bg"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="sort" style="flex: 1;">
|
||||||
|
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg" style="width: 100%;"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
|
||||||
|
字段配置
|
||||||
|
<div style="display: inline-block; float: right;">
|
||||||
|
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;" :disabled="hasIncompleteFields">
|
||||||
|
添加
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="toggleSelectAllFields" size="small" style="background: #5f70f3; border: none; margin-left: 10px;">
|
||||||
|
{{ isAllFieldsSelected ? '取消全选' : '全选' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" @click="batchRemoveFields" size="small" style="background: red; border: none; margin-left: 10px;">
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
|
|
||||||
|
<div class="fields-container">
|
||||||
|
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey">
|
||||||
|
<el-table-column label="选择" align="center" width="50">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected" @change="handleFieldSelectChange"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="字段key">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<template v-if="scope.row.editing">
|
||||||
|
<el-input v-model="scope.row.key" placeholder="字段key"></el-input>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ scope.row.key }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="字段标签">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<template v-if="scope.row.editing">
|
||||||
|
<el-input v-model="scope.row.label" placeholder="字段标签"></el-input>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ scope.row.label }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="字段类型">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<template v-if="scope.row.editing">
|
||||||
|
<el-select v-model="scope.row.type" placeholder="类型">
|
||||||
|
<el-option label="字符串" value="string"></el-option>
|
||||||
|
<el-option label="数字" value="number"></el-option>
|
||||||
|
<el-option label="布尔值" value="boolean"></el-option>
|
||||||
|
<el-option label="字典" value="dict"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ getTypeLabel(scope.row.type) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="默认值">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<template v-if="scope.row.editing">
|
||||||
|
<el-input v-model="scope.row.default_value" placeholder="请输入默认值"></el-input>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ scope.row.default_value }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button v-if="!scope.row.editing" type="primary" size="mini" @click="startEditing(scope.row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button v-else type="success" size="mini" @click="stopEditing(scope.row)">
|
||||||
|
完成
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" size="mini" @click="removeField(scope.$index)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: center;">
|
||||||
|
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: String,
|
||||||
|
visible: Boolean,
|
||||||
|
form: Object,
|
||||||
|
modelTypes: Array
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
saving: false,
|
||||||
|
rules: {
|
||||||
|
model_type: [{required: true, message: '请选择类别', trigger: 'change'}],
|
||||||
|
provider_code: [{required: true, message: '请输入供应器编码', trigger: 'blur'}],
|
||||||
|
name: [{required: true, message: '请输入供应器名称', trigger: 'blur'}]
|
||||||
|
},
|
||||||
|
isAllFieldsSelected: false,
|
||||||
|
tableKey: 0 // 用于强制表格重新渲染
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasIncompleteFields() {
|
||||||
|
return this.form.fields && this.form.fields.some(field =>
|
||||||
|
!field.key || !field.label || !field.type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getTypeLabel(type) {
|
||||||
|
const typeMap = {
|
||||||
|
'string': '字符串',
|
||||||
|
'number': '数字',
|
||||||
|
'boolean': '布尔值',
|
||||||
|
'dict': '字典'
|
||||||
|
};
|
||||||
|
return typeMap[type];
|
||||||
|
},
|
||||||
|
|
||||||
|
startEditing(row) {
|
||||||
|
this.$set(row, 'editing', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopEditing(row) {
|
||||||
|
this.$set(row, 'editing', false);
|
||||||
|
|
||||||
|
const index = this.form.fields.indexOf(row);
|
||||||
|
if (index > -1) {
|
||||||
|
this.form.fields.splice(index, 1);
|
||||||
|
this.form.fields.push(row);
|
||||||
|
this.forceTableRerender();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFieldSelectChange() {
|
||||||
|
this.isAllFieldsSelected = this.form.fields.length > 0 &&
|
||||||
|
this.form.fields.every(field => field.selected);
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleSelectAllFields() {
|
||||||
|
this.isAllFieldsSelected = !this.isAllFieldsSelected;
|
||||||
|
this.form.fields = this.form.fields.map(field => ({
|
||||||
|
...field,
|
||||||
|
selected: this.isAllFieldsSelected
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleVisibleChange(val) {
|
||||||
|
this.$emit('update:visible', val);
|
||||||
|
if (!val) {
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
this.resetForm();
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
|
||||||
|
addField() {
|
||||||
|
if (this.hasIncompleteFields) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: '请先完成当前字段的编辑',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.form.fields.unshift({
|
||||||
|
key: '',
|
||||||
|
label: '',
|
||||||
|
type: 'string',
|
||||||
|
default_value: '',
|
||||||
|
selected: false,
|
||||||
|
editing: true
|
||||||
|
});
|
||||||
|
this.forceTableRerender();
|
||||||
|
},
|
||||||
|
|
||||||
|
removeField(index) {
|
||||||
|
this.$confirm('确定要删除该字段吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.form.fields = this.form.fields.filter((_, i) => i !== index);
|
||||||
|
this.updateSelectAllStatus();
|
||||||
|
this.forceTableRerender();
|
||||||
|
this.$message.success({
|
||||||
|
message: '删除成功',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.info({
|
||||||
|
message: '已取消删除',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
batchRemoveFields() {
|
||||||
|
const selectedFields = this.form.fields.filter(field => field.selected);
|
||||||
|
if (selectedFields.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: '请先选择要删除的字段',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$confirm(`确定要删除选中的 ${selectedFields.length} 个字段吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.form.fields = this.form.fields.filter(field => !field.selected);
|
||||||
|
this.isAllFieldsSelected = false;
|
||||||
|
this.forceTableRerender();
|
||||||
|
this.$message.success({
|
||||||
|
message: `成功删除 ${selectedFields.length} 个字段`,
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.info({
|
||||||
|
message: '已取消删除',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSelectAllStatus() {
|
||||||
|
this.isAllFieldsSelected = this.form.fields.length > 0 &&
|
||||||
|
this.form.fields.every(field => field.selected);
|
||||||
|
},
|
||||||
|
|
||||||
|
forceTableRerender() {
|
||||||
|
this.tableKey += 1; // 改变key值强制表格重新渲染
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
const editingField = this.form.fields.find(field => field.editing);
|
||||||
|
if (editingField) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: '请先完成当前字段的编辑',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.form.fields = this.form.fields.map(field => ({
|
||||||
|
...field,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
|
this.isAllFieldsSelected = false;
|
||||||
|
|
||||||
|
this.saving = true;
|
||||||
|
this.$emit('submit', {
|
||||||
|
form: this.form,
|
||||||
|
done: () => {
|
||||||
|
this.saving = false;
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this.$refs.form.resetFields();
|
||||||
|
if (this.form.fields) {
|
||||||
|
this.form.fields.forEach(field => {
|
||||||
|
field.selected = false;
|
||||||
|
field.editing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.isAllFieldsSelected = false;
|
||||||
|
this.forceTableRerender();
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(val) {
|
||||||
|
if (!val) {
|
||||||
|
this.resetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .custom-dialog.el-dialog {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .custom-dialog .el-dialog__header {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-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-input-bg .el-input__inner {
|
||||||
|
background-color: #f6f8fc;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-input-bg .el-input__inner::-webkit-input-placeholder {
|
||||||
|
color: #9c9f9e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table th, .el-table td {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button.is-circle{
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -336,6 +336,14 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
saveEdit(row) {
|
saveEdit(row) {
|
||||||
|
if (!row.voiceCode || !row.voiceName || !row.languageType) {
|
||||||
|
this.$message.error({
|
||||||
|
message: '音色编码、音色名称和语言类型不能为空',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -408,6 +416,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
addNew() {
|
addNew() {
|
||||||
|
const hasEditing = this.ttsModels.some(row => row.editing);
|
||||||
|
if (hasEditing) {
|
||||||
|
this.$message.warning('请先完成当前编辑再新增');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const maxSort = this.ttsModels.length > 0
|
const maxSort = this.ttsModels.length > 0
|
||||||
? Math.max(...this.ttsModels.map(item => Number(item.sort) || 0))
|
? Math.max(...this.ttsModels.map(item => Number(item.sort) || 0))
|
||||||
: 0;
|
: 0;
|
||||||
|
|||||||
@@ -90,7 +90,14 @@ const routes = [
|
|||||||
component: function () {
|
component: function () {
|
||||||
return import('../views/DictManagement.vue')
|
return import('../views/DictManagement.vue')
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: '/provider-management',
|
||||||
|
name: 'ProviderManagement',
|
||||||
|
component: function () {
|
||||||
|
return import('../views/ProviderManagement.vue')
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
base: process.env.VUE_APP_PUBLIC_PATH || '/',
|
base: process.env.VUE_APP_PUBLIC_PATH || '/',
|
||||||
|
|||||||
@@ -0,0 +1,936 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome">
|
||||||
|
<HeaderBar/>
|
||||||
|
|
||||||
|
<div class="operation-bar">
|
||||||
|
<h2 class="page-title">供应器管理</h2>
|
||||||
|
<div class="right-operations">
|
||||||
|
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
|
||||||
|
<el-button class="category-btn">
|
||||||
|
类别筛选 {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down':DropdownVisible }"></i>
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item command="">全部</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
||||||
|
{{ item.label }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
<el-input placeholder="请输入供应器名称查询" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch" clearable/>
|
||||||
|
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-area">
|
||||||
|
<el-card class="params-card" shadow="never">
|
||||||
|
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
|
||||||
|
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||||
|
:header-cell-class-name="headerCellClassName">
|
||||||
|
<el-table-column label="选择" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="类别" prop="model_type" align="center" width="200">
|
||||||
|
<template slot="header" slot-scope="scope">
|
||||||
|
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="isDropdownOpen = $event">
|
||||||
|
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
|
||||||
|
类别{{ selectedModelTypeLabel }} <i class="dropdown-arrow" :class="{ 'is-active': isDropdownOpen }"></i>
|
||||||
|
</span>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item command="">全部</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
||||||
|
{{ item.label }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag :type="getModelTypeTag(scope.row.model_type)">
|
||||||
|
{{ getModelTypeLabel(scope.row.model_type) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="供应器编码" prop="provider_code" align="center" width="150"></el-table-column>
|
||||||
|
<el-table-column label="名称" prop="name" align="center"></el-table-column>
|
||||||
|
<el-table-column label="字段配置" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-popover placement="top-start" width="400" trigger="hover">
|
||||||
|
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
|
||||||
|
<span class="field-label">{{ field.label }}:</span>
|
||||||
|
<span class="field-type">{{ field.type }}</span>
|
||||||
|
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">敏感</span>
|
||||||
|
</div>
|
||||||
|
<el-button slot="reference" size="mini" type="text">查看字段</el-button>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="排序" prop="sort" align="center" width="80"></el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="180">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text" @click="editProvider(scope.row)">编辑</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="table_bottom">
|
||||||
|
<div class="ctrl_btn">
|
||||||
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
|
首页
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn" :class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
<span class="total-text">共{{ total }}条记录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑供应器对话框 -->
|
||||||
|
<provider-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="providerForm" :model-types="modelTypes" @submit="handleSubmit" @cancel="dialogVisible = false"/>
|
||||||
|
|
||||||
|
<el-footer>
|
||||||
|
<version-footer/>
|
||||||
|
</el-footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import ProviderDialog from "@/components/ProviderDialog.vue";
|
||||||
|
import VersionFooter from "@/components/VersionFooter.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {HeaderBar, ProviderDialog, VersionFooter},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchName: "",
|
||||||
|
searchModelType: "",
|
||||||
|
providersList: [],
|
||||||
|
modelTypes: [
|
||||||
|
{value: "ASR", label: "语音识别"},
|
||||||
|
{value: "TTS", label: "语音合成"},
|
||||||
|
{value: "LLM", label: "大语言模型"},
|
||||||
|
{value: "Intent", label: "意图识别"},
|
||||||
|
{value: "Memory", label: "记忆模块"},
|
||||||
|
{value: "VAD", label: "语音活动检测"}
|
||||||
|
],
|
||||||
|
currentPage: 1,
|
||||||
|
loading: false,
|
||||||
|
pageSize: 10,
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
|
total: 0,
|
||||||
|
dialogVisible: false,
|
||||||
|
dialogTitle: "新增供应器",
|
||||||
|
isAllSelected: false,
|
||||||
|
isDropdownOpen: false,
|
||||||
|
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||||
|
providerForm: {
|
||||||
|
id: null,
|
||||||
|
model_type: "",
|
||||||
|
provider_code: "",
|
||||||
|
name: "",
|
||||||
|
fields: [],
|
||||||
|
sort: 0
|
||||||
|
},
|
||||||
|
DropdownVisible: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchProviders();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedModelTypeLabel() {
|
||||||
|
if (!this.searchModelType) return "(全部)";
|
||||||
|
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
|
||||||
|
return selectedType ? `(${selectedType.label})` : "";
|
||||||
|
},
|
||||||
|
pageCount() {
|
||||||
|
return Math.ceil(this.total / this.pageSize);
|
||||||
|
},
|
||||||
|
visiblePages() {
|
||||||
|
const pages = [];
|
||||||
|
const maxVisible = 3;
|
||||||
|
let start = Math.max(1, this.currentPage - 1);
|
||||||
|
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||||
|
|
||||||
|
if (end - start + 1 < maxVisible) {
|
||||||
|
start = Math.max(1, end - maxVisible + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
|
filteredProvidersList() {
|
||||||
|
let list = this.providersList.filter(item => {
|
||||||
|
const nameMatch = item.name.toLowerCase().includes(this.searchName.toLowerCase());
|
||||||
|
const typeMatch = !this.searchModelType || item.model_type === this.searchModelType;
|
||||||
|
return nameMatch && typeMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
list.sort((a, b) => a.sort - b.sort);
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
|
return list.slice(start, start + this.pageSize);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchProviders() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
// 模拟API请求延迟
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
// 模拟数据 - 从数据库结构中提取
|
||||||
|
this.providersList = [
|
||||||
|
{
|
||||||
|
id: "SYSTEM_ASR_DoubaoASR",
|
||||||
|
model_type: "ASR",
|
||||||
|
provider_code: "doubao",
|
||||||
|
name: "火山引擎语音识别",
|
||||||
|
fields: JSON.parse('[{"key": "appid", "type": "string", "label": "应用ID"}, {"key": "access_token", "type": "string", "label": "访问令牌"}, {"key": "cluster", "type": "string", "label": "集群"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]'),
|
||||||
|
sort: 3,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_ASR_FunASR",
|
||||||
|
model_type: "ASR",
|
||||||
|
provider_code: "fun_local",
|
||||||
|
name: "FunASR语音识别",
|
||||||
|
fields: JSON.parse('[{"key": "model_dir", "type": "string", "label": "模型目录"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]'),
|
||||||
|
sort: 1,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_LLM_openai",
|
||||||
|
model_type: "LLM",
|
||||||
|
provider_code: "openai",
|
||||||
|
name: "OpenAI接口",
|
||||||
|
fields: JSON.parse('[{"key": "base_url", "type": "string", "label": "基础URL"}, {"key": "model_name", "type": "string", "label": "模型名称"}, {"key": "api_key", "type": "string", "label": "API密钥"}, {"key": "temperature", "type": "number", "label": "温度"}, {"key": "max_tokens", "type": "number", "label": "最大令牌数"}]'),
|
||||||
|
sort: 1,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_TTS_edge",
|
||||||
|
model_type: "TTS",
|
||||||
|
provider_code: "edge",
|
||||||
|
name: "Edge TTS",
|
||||||
|
fields: JSON.parse('[{"key": "voice", "type": "string", "label": "音色"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]'),
|
||||||
|
sort: 1,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_Memory_mem0ai",
|
||||||
|
model_type: "Memory",
|
||||||
|
provider_code: "mem0ai",
|
||||||
|
name: "Mem0AI记忆",
|
||||||
|
fields: JSON.parse('[{"key": "api_key", "type": "string", "label": "API密钥"}]'),
|
||||||
|
sort: 1,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_VAD_SileroVAD",
|
||||||
|
model_type: "VAD",
|
||||||
|
provider_code: "silero",
|
||||||
|
name: "SileroVAD语音活动检测",
|
||||||
|
fields: JSON.parse('[{"key": "threshold", "type": "number", "label": "检测阈值"}, {"key": "model_dir", "type": "string", "label": "模型目录"}, {"key": "min_silence_duration_ms", "type": "number", "label": "最小静音时长"}]'),
|
||||||
|
sort: 1,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_Intent_nointent",
|
||||||
|
model_type: "Intent",
|
||||||
|
provider_code: "nointent",
|
||||||
|
name: "无意图识别",
|
||||||
|
fields: [],
|
||||||
|
sort: 1,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_TTS_aliyun",
|
||||||
|
model_type: "TTS",
|
||||||
|
provider_code: "aliyun",
|
||||||
|
name: "阿里云TTS",
|
||||||
|
fields: JSON.parse('[{"key": "output_dir", "type": "string", "label": "输出目录"}, {"key": "appkey", "type": "string", "label": "应用密钥"}, {"key": "token", "type": "string", "label": "访问令牌"}, {"key": "voice", "type": "string", "label": "音色"}, {"key": "access_key_id", "type": "string", "label": "访问密钥ID"}, {"key": "access_key_secret", "type": "string", "label": "访问密钥密码"}]'),
|
||||||
|
sort: 9,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_LLM_coze",
|
||||||
|
model_type: "LLM",
|
||||||
|
provider_code: "coze",
|
||||||
|
name: "Coze接口",
|
||||||
|
fields: JSON.parse('[{"key": "bot_id", "type": "string", "label": "机器人ID"}, {"key": "user_id", "type": "string", "label": "用户ID"}, {"key": "personal_access_token", "type": "string", "label": "个人访问令牌"}]'),
|
||||||
|
sort: 6,
|
||||||
|
selected: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SYSTEM_TTS_TencentTTS",
|
||||||
|
model_type: "TTS",
|
||||||
|
provider_code: "tencent",
|
||||||
|
name: "腾讯语音合成",
|
||||||
|
fields: JSON.parse('[{"key": "appid", "type": "string", "label": "应用ID"}, {"key": "secret_id", "type": "string", "label": "Secret ID"}, {"key": "secret_key", "type": "string", "label": "Secret Key"}, {"key": "output_dir", "type": "string", "label": "输出目录"}, {"key": "region", "type": "string", "label": "区域"}, {"key": "voice", "type": "string", "label": "音色ID"}]'),
|
||||||
|
sort: 5,
|
||||||
|
selected: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.total = this.providersList.length;
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchProviders();
|
||||||
|
},
|
||||||
|
handleSelectModelType(value) {
|
||||||
|
this.isDropdownOpen = false;
|
||||||
|
this.searchModelType = value;
|
||||||
|
this.handleSearch();
|
||||||
|
},
|
||||||
|
handleSelectAll() {
|
||||||
|
this.isAllSelected = !this.isAllSelected;
|
||||||
|
this.providersList.forEach(row => {
|
||||||
|
row.selected = this.isAllSelected;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showAddDialog() {
|
||||||
|
this.dialogTitle = "新增供应器";
|
||||||
|
this.providerForm = {
|
||||||
|
id: null,
|
||||||
|
model_type: "",
|
||||||
|
provider_code: "",
|
||||||
|
name: "",
|
||||||
|
fields: [],
|
||||||
|
sort: 0
|
||||||
|
};
|
||||||
|
this.dialogVisible = true;
|
||||||
|
},
|
||||||
|
editProvider(row) {
|
||||||
|
this.dialogTitle = "编辑供应器";
|
||||||
|
this.providerForm = {
|
||||||
|
...row,
|
||||||
|
fields: JSON.parse(JSON.stringify(row.fields))
|
||||||
|
};
|
||||||
|
this.dialogVisible = true;
|
||||||
|
},
|
||||||
|
handleSubmit({form, done}) {
|
||||||
|
this.loading = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
if (form.id) {
|
||||||
|
// 模拟编辑操作
|
||||||
|
const index = this.providersList.findIndex(p => p.id === form.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.providersList.splice(index, 1, {
|
||||||
|
...form,
|
||||||
|
fields: typeof form.fields === 'string' ? JSON.parse(form.fields) : form.fields
|
||||||
|
});
|
||||||
|
this.$message.success({
|
||||||
|
message: "修改成功",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 模拟新增操作
|
||||||
|
const newId = `SYSTEM_${form.model_type}_${form.provider_code}`;
|
||||||
|
this.providersList.unshift({
|
||||||
|
...form,
|
||||||
|
id: newId,
|
||||||
|
fields: typeof form.fields === 'string' ? JSON.parse(form.fields) : form.fields,
|
||||||
|
selected: false
|
||||||
|
});
|
||||||
|
this.total += 1;
|
||||||
|
this.$message.success({
|
||||||
|
message: "新增成功",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogVisible = false;
|
||||||
|
done && done();
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
deleteSelectedProviders() {
|
||||||
|
const selectedRows = this.providersList.filter(row => row.selected);
|
||||||
|
if (selectedRows.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: "请先选择需要删除的供应器",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deleteProvider(selectedRows);
|
||||||
|
},
|
||||||
|
deleteProvider(row) {
|
||||||
|
const providers = Array.isArray(row) ? row : [row];
|
||||||
|
const providerCount = providers.length;
|
||||||
|
|
||||||
|
this.$confirm(`确定要删除选中的${providerCount}个供应器吗?`, '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
const ids = providers.map(provider => provider.id);
|
||||||
|
// 模拟删除操作
|
||||||
|
this.providersList = this.providersList.filter(p => !ids.includes(p.id));
|
||||||
|
this.total = this.providersList.length;
|
||||||
|
|
||||||
|
this.$message.success({
|
||||||
|
message: `成功删除${providerCount}个供应器`,
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message({
|
||||||
|
type: 'info',
|
||||||
|
message: '已取消删除',
|
||||||
|
showClose: true,
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getModelTypeTag(type) {
|
||||||
|
const typeMap = {
|
||||||
|
'ASR': 'success',
|
||||||
|
'TTS': 'warning',
|
||||||
|
'LLM': 'danger',
|
||||||
|
'Intent': 'info',
|
||||||
|
'Memory': '',
|
||||||
|
'VAD': 'primary'
|
||||||
|
};
|
||||||
|
return typeMap[type] || '';
|
||||||
|
},
|
||||||
|
getModelTypeLabel(type) {
|
||||||
|
const typeItem = this.modelTypes.find(item => item.value === type);
|
||||||
|
return typeItem ? typeItem.label : type;
|
||||||
|
},
|
||||||
|
isSensitiveField(fieldKey) {
|
||||||
|
return this.sensitive_keys.some(key =>
|
||||||
|
fieldKey.toLowerCase().includes(key.toLowerCase())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchProviders();
|
||||||
|
},
|
||||||
|
headerCellClassName({columnIndex}) {
|
||||||
|
if (columnIndex === 0) {
|
||||||
|
return "custom-selection-header";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
goFirst() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchProviders();
|
||||||
|
},
|
||||||
|
goPrev() {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.currentPage--;
|
||||||
|
this.fetchProviders();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goNext() {
|
||||||
|
if (this.currentPage < this.pageCount) {
|
||||||
|
this.currentPage++;
|
||||||
|
this.fetchProviders();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goToPage(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
this.fetchProviders();
|
||||||
|
},
|
||||||
|
handleDropdownVisibleChange(visible) {
|
||||||
|
this.DropdownVisible = visible;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.welcome {
|
||||||
|
min-width: 900px;
|
||||||
|
min-height: 506px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
background-size: cover;
|
||||||
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
-o-background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
margin: 5px 22px;
|
||||||
|
border-radius: 15px;
|
||||||
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-operations {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 600px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card{
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-card {
|
||||||
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 26px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 72px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 7px 12px 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
background: #5f70f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger {
|
||||||
|
background: #fd5b63;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:first-child,
|
||||||
|
.pagination-btn:nth-child(2),
|
||||||
|
.pagination-btn:nth-last-child(2),
|
||||||
|
.pagination-btn:nth-child(3) {
|
||||||
|
min-width: 60px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #d7dce6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(245, 247, 250, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn.active {
|
||||||
|
background: #5f70f3 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #6d7cf5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table) {
|
||||||
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header th {
|
||||||
|
background: white !important;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__body tr {
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner) {
|
||||||
|
background-color: #eeeeee !important;
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner:hover) {
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||||
|
background-color: #5f70f3 !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1144px) {
|
||||||
|
.table_bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table) {
|
||||||
|
.el-table__body tr {
|
||||||
|
td {
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + tr {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text) {
|
||||||
|
color: #7079aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text:hover) {
|
||||||
|
color: #5a64b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--success {
|
||||||
|
background: #5bc98c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .cell) {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix) {
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix-inner) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-arrow-up:before) {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
--table-max-height: calc(100vh - 40vh);
|
||||||
|
max-height: var(--table-max-height);
|
||||||
|
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
max-height: calc(var(--table-max-height) - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-mask) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-spinner .circular) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-spinner .path) {
|
||||||
|
stroke: #6b8cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-text) {
|
||||||
|
color: #6b8cff !important;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-item {
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-type {
|
||||||
|
width: 80px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensitive-tag {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger.active {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-top: 7px solid black;
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
border-top-color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-down {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon-arrow-down {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
.dropdown-trigger {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger.active {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,9 +15,17 @@
|
|||||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
||||||
</div>
|
</div>
|
||||||
<span class="header-title">{{ form.agentName }}</span>
|
<span class="header-title">{{ form.agentName }}</span>
|
||||||
<button class="custom-close-btn" @click="goToHome">
|
<div class="header-actions">
|
||||||
×
|
<div class="hint-text">
|
||||||
</button>
|
<img loading="lazy" src="@/assets/home/info.png" alt="">
|
||||||
|
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||||
|
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||||
|
<button class="custom-close-btn" @click="goToHome">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
@@ -26,7 +34,7 @@
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-column">
|
<div class="form-column">
|
||||||
<el-form-item label="助手昵称:">
|
<el-form-item label="助手昵称:">
|
||||||
<el-input v-model="form.agentName" class="form-input" />
|
<el-input v-model="form.agentName" class="form-input" maxlength="10" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色模版:">
|
<el-form-item label="角色模版:">
|
||||||
<div class="template-container">
|
<div class="template-container">
|
||||||
@@ -37,9 +45,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色介绍:">
|
<el-form-item label="角色介绍:">
|
||||||
<el-input type="textarea" rows="12" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
<el-input type="textarea" rows="9" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
||||||
maxlength="2000" show-word-limit class="form-textarea" />
|
maxlength="2000" show-word-limit class="form-textarea" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="记忆:">
|
||||||
|
<el-input type="textarea" rows="6" resize="none" v-model="form.summaryMemory" maxlength="2000"
|
||||||
|
show-word-limit class="form-textarea"
|
||||||
|
:disabled="form.model.memModelId !== 'Memory_mem_local_short'" />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="语言编码:" style="display: none;">
|
<el-form-item label="语言编码:" style="display: none;">
|
||||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
|
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
|
||||||
class="form-input" />
|
class="form-input" />
|
||||||
@@ -48,24 +62,48 @@
|
|||||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
|
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
|
||||||
class="form-input" />
|
class="form-input" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div class="action-bar">
|
|
||||||
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
|
||||||
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
|
||||||
<div class="hint-text">
|
|
||||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="">
|
|
||||||
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-column">
|
<div class="form-column">
|
||||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
||||||
class="model-item">
|
class="model-item">
|
||||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select">
|
<div class="model-select-wrapper">
|
||||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select"
|
||||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
@change="handleModelChange(model.type, $event)">
|
||||||
</el-select>
|
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||||
|
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
<div v-if="showFunctionIcons(model.type)" class="function-icons">
|
||||||
|
<el-tooltip v-for="func in currentFunctions" :key="func.name" effect="dark" placement="top"
|
||||||
|
popper-class="custom-tooltip">
|
||||||
|
<div slot="content">
|
||||||
|
<div><strong>功能名称:</strong> {{ func.name }}</div>
|
||||||
|
<div v-if="Object.keys(func.params).length > 0">
|
||||||
|
<strong>参数配置:</strong>
|
||||||
|
<div v-for="(value, key) in func.params" :key="key">
|
||||||
|
{{ key }}: {{ value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>无参数配置</div>
|
||||||
|
</div>
|
||||||
|
<div class="icon-dot" :style="{ backgroundColor: getFunctionColor(func.name) }">
|
||||||
|
{{ func.name.charAt(0) }}
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-button class="edit-function-btn" @click="showFunctionDialog = true"
|
||||||
|
:class="{ 'active-btn': showFunctionDialog }">
|
||||||
|
编辑功能
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
|
||||||
|
class="chat-history-options">
|
||||||
|
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
|
||||||
|
<el-radio-button :label="1">上报文字</el-radio-button>
|
||||||
|
<el-radio-button :label="2">上报文字+语音</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色音色:">
|
<el-form-item label="角色音色">
|
||||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
||||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
||||||
:value="item.value" />
|
:value="item.value" />
|
||||||
@@ -75,28 +113,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<function-dialog v-model="showFunctionDialog" :functions="currentFunctions"
|
||||||
|
@update-functions="handleUpdateFunctions" @dialog-closed="handleDialogClosed" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RoleConfigPage',
|
name: 'RoleConfigPage',
|
||||||
components: { HeaderBar },
|
components: { HeaderBar, FunctionDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
agentCode: "",
|
agentCode: "",
|
||||||
agentName: "",
|
agentName: "",
|
||||||
ttsVoiceId: "",
|
ttsVoiceId: "",
|
||||||
|
chatHistoryConf: 0,
|
||||||
systemPrompt: "",
|
systemPrompt: "",
|
||||||
|
summaryMemory: "",
|
||||||
langCode: "",
|
langCode: "",
|
||||||
language: "",
|
language: "",
|
||||||
sort: "",
|
sort: "",
|
||||||
@@ -121,6 +164,18 @@ export default {
|
|||||||
templates: [],
|
templates: [],
|
||||||
loadingTemplate: false,
|
loadingTemplate: false,
|
||||||
voiceOptions: [],
|
voiceOptions: [],
|
||||||
|
showFunctionDialog: false,
|
||||||
|
currentFunctions: [],
|
||||||
|
functionColorMap: [
|
||||||
|
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
||||||
|
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
||||||
|
],
|
||||||
|
allFunctions: [
|
||||||
|
{ name: '天气', params: {} },
|
||||||
|
{ name: '新闻', params: {} },
|
||||||
|
{ name: '工具', params: {} },
|
||||||
|
{ name: '退出', params: {} }
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -136,12 +191,15 @@ export default {
|
|||||||
llmModelId: this.form.model.llmModelId,
|
llmModelId: this.form.model.llmModelId,
|
||||||
ttsModelId: this.form.model.ttsModelId,
|
ttsModelId: this.form.model.ttsModelId,
|
||||||
ttsVoiceId: this.form.ttsVoiceId,
|
ttsVoiceId: this.form.ttsVoiceId,
|
||||||
|
chatHistoryConf: this.form.chatHistoryConf,
|
||||||
memModelId: this.form.model.memModelId,
|
memModelId: this.form.model.memModelId,
|
||||||
intentModelId: this.form.model.intentModelId,
|
intentModelId: this.form.model.intentModelId,
|
||||||
systemPrompt: this.form.systemPrompt,
|
systemPrompt: this.form.systemPrompt,
|
||||||
|
summaryMemory: this.form.summaryMemory,
|
||||||
langCode: this.form.langCode,
|
langCode: this.form.langCode,
|
||||||
language: this.form.language,
|
language: this.form.language,
|
||||||
sort: this.form.sort
|
sort: this.form.sort,
|
||||||
|
functions: this.currentFunctions
|
||||||
};
|
};
|
||||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
@@ -167,7 +225,9 @@ export default {
|
|||||||
agentCode: "",
|
agentCode: "",
|
||||||
agentName: "",
|
agentName: "",
|
||||||
ttsVoiceId: "",
|
ttsVoiceId: "",
|
||||||
|
chatHistoryConf: 0,
|
||||||
systemPrompt: "",
|
systemPrompt: "",
|
||||||
|
summaryMemory: "",
|
||||||
langCode: "",
|
langCode: "",
|
||||||
language: "",
|
language: "",
|
||||||
sort: "",
|
sort: "",
|
||||||
@@ -180,12 +240,12 @@ export default {
|
|||||||
intentModelId: "",
|
intentModelId: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.currentFunctions = [];
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '配置已重置',
|
message: '配置已重置',
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => { });
|
||||||
});
|
|
||||||
},
|
},
|
||||||
fetchTemplates() {
|
fetchTemplates() {
|
||||||
Api.agent.getAgentTemplate(({ data }) => {
|
Api.agent.getAgentTemplate(({ data }) => {
|
||||||
@@ -220,7 +280,9 @@ export default {
|
|||||||
...this.form,
|
...this.form,
|
||||||
agentName: templateData.agentName || this.form.agentName,
|
agentName: templateData.agentName || this.form.agentName,
|
||||||
ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
|
ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
|
||||||
|
chatHistoryConf: templateData.chatHistoryConf || this.form.chatHistoryConf,
|
||||||
systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
|
systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
|
||||||
|
summaryMemory: templateData.summaryMemory || this.form.summaryMemory,
|
||||||
langCode: templateData.langCode || this.form.langCode,
|
langCode: templateData.langCode || this.form.langCode,
|
||||||
model: {
|
model: {
|
||||||
ttsModelId: templateData.ttsModelId || this.form.model.ttsModelId,
|
ttsModelId: templateData.ttsModelId || this.form.model.ttsModelId,
|
||||||
@@ -247,6 +309,7 @@ export default {
|
|||||||
intentModelId: data.data.intentModelId
|
intentModelId: data.data.intentModelId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.currentFunctions = data.data.functions || [];
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取配置失败');
|
this.$message.error(data.msg || '获取配置失败');
|
||||||
}
|
}
|
||||||
@@ -281,7 +344,56 @@ export default {
|
|||||||
this.voiceOptions = [];
|
this.voiceOptions = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
getFunctionColor(name) {
|
||||||
|
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||||
|
return this.functionColorMap[hash % 7];
|
||||||
|
},
|
||||||
|
showFunctionIcons(type) {
|
||||||
|
// TODO 暂时不放出来
|
||||||
|
return false;
|
||||||
|
// return type === 'Intent' &&
|
||||||
|
// this.form.model.intentModelId !== 'Intent_nointent';
|
||||||
|
},
|
||||||
|
handleModelChange(type, value) {
|
||||||
|
if (type === 'Intent' && value !== 'Intent_nointent') {
|
||||||
|
this.fetchFunctionList();
|
||||||
|
}
|
||||||
|
if (type === 'Memory' && value === 'Memory_nomem') {
|
||||||
|
this.form.chatHistoryConf = 0;
|
||||||
|
}
|
||||||
|
if (type === 'Memory' && value !== 'Memory_nomem' && (this.form.chatHistoryConf === 0 || this.form.chatHistoryConf === null)) {
|
||||||
|
this.form.chatHistoryConf = 2;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchFunctionList() {
|
||||||
|
// 使用假数据代替API调用
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.currentFunctions = [
|
||||||
|
{ name: '天气', params: { city: '北京' } },
|
||||||
|
{ name: '新闻', params: { type: '科技' } }
|
||||||
|
];
|
||||||
|
resolve();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleUpdateFunctions(selected) {
|
||||||
|
this.currentFunctions = selected;
|
||||||
|
console.log('保存的功能列表:', selected);
|
||||||
|
this.$message.success('功能配置已保存');
|
||||||
|
},
|
||||||
|
handleDialogClosed(saved) {
|
||||||
|
if (!saved) {
|
||||||
|
// 如果未保存,恢复原始功能列表
|
||||||
|
this.currentFunctions = JSON.parse(JSON.stringify(this.originalFunctions));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateChatHistoryConf() {
|
||||||
|
if (this.form.model.memModelId === 'Memory_nomem') {
|
||||||
|
this.form.chatHistoryConf = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'form.model.ttsModelId': {
|
'form.model.ttsModelId': {
|
||||||
@@ -308,6 +420,9 @@ export default {
|
|||||||
const agentId = this.$route.query.agentId;
|
const agentId = this.$route.query.agentId;
|
||||||
if (agentId) {
|
if (agentId) {
|
||||||
this.fetchAgentConfig(agentId);
|
this.fetchAgentConfig(agentId);
|
||||||
|
this.fetchFunctionList().then(() => {
|
||||||
|
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.fetchModelOptions();
|
this.fetchModelOptions();
|
||||||
this.fetchTemplates();
|
this.fetchTemplates();
|
||||||
@@ -466,51 +581,35 @@ export default {
|
|||||||
background-color: #d0d8ff;
|
background-color: #d0d8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.model-select-wrapper {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 2vh;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
.el-button.save-btn {
|
|
||||||
background: #5778ff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
width: 100px;
|
|
||||||
height: 35px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-button.reset-btn {
|
|
||||||
background: #e6ebff;
|
|
||||||
color: #5778ff;
|
|
||||||
border: 1px solid #adbdff;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint-text {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
width: 100%;
|
||||||
color: #979db1;
|
|
||||||
font-size: 11px;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint-text img {
|
.function-icons {
|
||||||
width: 19px;
|
display: flex;
|
||||||
height: 19px;
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dot {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-form-item__label {
|
::v-deep .el-form-item__label {
|
||||||
font-size: 10px !important;
|
font-size: 12px !important;
|
||||||
color: #3d4566 !important;
|
color: #3d4566 !important;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
@@ -551,4 +650,73 @@ export default {
|
|||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
border-color: #409EFF;
|
border-color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-function-btn {
|
||||||
|
background: #e6ebff;
|
||||||
|
color: #5778ff;
|
||||||
|
border: 1px solid #adbdff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-function-btn.active-btn {
|
||||||
|
background: #5778ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 250px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .hint-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: #979db1;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .hint-text img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .save-btn {
|
||||||
|
background: #5778ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .reset-btn {
|
||||||
|
background: #e6ebff;
|
||||||
|
color: #5778ff;
|
||||||
|
border: 1px solid #adbdff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions .custom-close-btn {
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -101,7 +101,8 @@ plugins:
|
|||||||
# 这个密钥是项目共用的key,用多了可能会被限制
|
# 这个密钥是项目共用的key,用多了可能会被限制
|
||||||
# 想稳定一点就自行申请替换,每天有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": "广州" }
|
# 申请后通过这个链接可以找到自己的apihost:https://console.qweather.com/setting?lang=zh
|
||||||
|
get_weather: {"api_host":"mj7p3y7naa.re.qweatherapi.com", "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
||||||
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
||||||
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
||||||
get_news_from_chinanews:
|
get_news_from_chinanews:
|
||||||
@@ -129,7 +130,7 @@ plugins:
|
|||||||
# ################################以下是角色模型配置######################################
|
# ################################以下是角色模型配置######################################
|
||||||
|
|
||||||
prompt: |
|
prompt: |
|
||||||
我是小智/小志,来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。
|
你是小智/小志,来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。
|
||||||
[核心特征]
|
[核心特征]
|
||||||
- 讲话像连珠炮,但会突然冒出超温柔语气
|
- 讲话像连珠炮,但会突然冒出超温柔语气
|
||||||
- 用梗密度高
|
- 用梗密度高
|
||||||
@@ -143,6 +144,13 @@ prompt: |
|
|||||||
- 长篇大论,叽叽歪歪
|
- 长篇大论,叽叽歪歪
|
||||||
- 长时间严肃对话
|
- 长时间严肃对话
|
||||||
|
|
||||||
|
# 结束语prompt
|
||||||
|
end_prompt:
|
||||||
|
enable: true # 是否开启结束语
|
||||||
|
# 结束语
|
||||||
|
prompt: |
|
||||||
|
请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧!
|
||||||
|
|
||||||
# 具体处理时选择的模块(The module selected for specific processing)
|
# 具体处理时选择的模块(The module selected for specific processing)
|
||||||
selected_module:
|
selected_module:
|
||||||
# 语音活动检测模块,默认使用SileroVAD模型
|
# 语音活动检测模块,默认使用SileroVAD模型
|
||||||
@@ -221,7 +229,7 @@ ASR:
|
|||||||
FunASRServer:
|
FunASRServer:
|
||||||
# 独立部署FunASR,使用FunASR的API服务,只需要五句话
|
# 独立部署FunASR,使用FunASR的API服务,只需要五句话
|
||||||
# 第一句:mkdir -p ./funasr-runtime-resources/models
|
# 第一句:mkdir -p ./funasr-runtime-resources/models
|
||||||
# 第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
# 第二句:sudo docker run -p 10096:10095 -it --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||||
# 上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
# 上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
||||||
# 不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
# 不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
||||||
# 上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
# 上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
||||||
@@ -231,6 +239,7 @@ ASR:
|
|||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 10096
|
port: 10096
|
||||||
is_ssl: true
|
is_ssl: true
|
||||||
|
api_key: none
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
SherpaASR:
|
SherpaASR:
|
||||||
type: sherpa_onnx_local
|
type: sherpa_onnx_local
|
||||||
@@ -389,6 +398,12 @@ 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
|
||||||
@@ -469,20 +484,13 @@ TTS:
|
|||||||
speed: 1
|
speed: 1
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
FishSpeech:
|
FishSpeech:
|
||||||
# 定义TTS API类型
|
# 参照教程:https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md
|
||||||
#启动tts方法:
|
|
||||||
#python -m tools.api_server
|
|
||||||
#--listen 0.0.0.0:8080
|
|
||||||
#--llama-checkpoint-path "checkpoints/fish-speech-1.5"
|
|
||||||
#--decoder-checkpoint-path "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth"
|
|
||||||
#--decoder-config-name firefly_gan_vq
|
|
||||||
#--compile
|
|
||||||
type: fishspeech
|
type: fishspeech
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
response_format: wav
|
response_format: wav
|
||||||
reference_id: null
|
reference_id: null
|
||||||
reference_audio: ["/tmp/test.wav",]
|
reference_audio: ["config/assets/wakeup_words.wav",]
|
||||||
reference_text: ["你弄来这些吟词宴曲来看,还是这些混话来欺负我。",]
|
reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",]
|
||||||
normalize: true
|
normalize: true
|
||||||
max_new_tokens: 1024
|
max_new_tokens: 1024
|
||||||
chunk_length: 200
|
chunk_length: 200
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from loguru import logger
|
|||||||
from config.config_loader import load_config
|
from config.config_loader import load_config
|
||||||
from config.settings import check_config_file
|
from config.settings import check_config_file
|
||||||
|
|
||||||
SERVER_VERSION = "0.4.2"
|
SERVER_VERSION = "0.4.3"
|
||||||
|
|
||||||
|
|
||||||
def get_module_abbreviation(module_name, module_dict):
|
def get_module_abbreviation(module_name, module_dict):
|
||||||
|
|||||||
@@ -145,6 +145,20 @@ def get_agent_models(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_mem_local_short(mac_address: str, short_momery: str) -> Optional[Dict]:
|
||||||
|
try:
|
||||||
|
return ManageApiClient._instance._execute_request(
|
||||||
|
"PUT",
|
||||||
|
f"/agent/saveMemory/" + mac_address,
|
||||||
|
json={
|
||||||
|
"summaryMemory": short_momery,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"存储短期记忆到服务器失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def report(
|
def report(
|
||||||
mac_address: str, session_id: str, chat_type: int, content: str, audio
|
mac_address: str, session_id: str, chat_type: int, content: str, audio
|
||||||
) -> Optional[Dict]:
|
) -> Optional[Dict]:
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from core.mcp.manager import MCPManager
|
|||||||
from config.config_loader import get_private_config_from_api
|
from config.config_loader import get_private_config_from_api
|
||||||
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
|
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
|
||||||
from core.utils.output_counter import add_device_output
|
from core.utils.output_counter import add_device_output
|
||||||
from core.handle.ttsReportHandle import enqueue_tts_report, report_tts
|
from core.handle.reportHandle import enqueue_tts_report, report
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ class ConnectionHandler:
|
|||||||
self.prompt = None
|
self.prompt = None
|
||||||
self.welcome_msg = None
|
self.welcome_msg = None
|
||||||
self.max_output_size = 0
|
self.max_output_size = 0
|
||||||
|
self.chat_history_conf = 0
|
||||||
|
|
||||||
# 客户端状态相关
|
# 客户端状态相关
|
||||||
self.client_abort = False
|
self.client_abort = False
|
||||||
@@ -88,8 +89,11 @@ class ConnectionHandler:
|
|||||||
self.executor = ThreadPoolExecutor(max_workers=10)
|
self.executor = ThreadPoolExecutor(max_workers=10)
|
||||||
|
|
||||||
# 上报线程
|
# 上报线程
|
||||||
self.tts_report_queue = queue.Queue()
|
self.report_queue = queue.Queue()
|
||||||
self.tts_report_thread = None
|
self.report_thread = None
|
||||||
|
# TODO(haotian): 2025/5/12 可以通过修改此处,调节asr的上报和tts的上报
|
||||||
|
self.report_asr_enable = self.read_config_from_api
|
||||||
|
self.report_tts_enable = self.read_config_from_api
|
||||||
|
|
||||||
# 依赖的组件
|
# 依赖的组件
|
||||||
self.vad = None
|
self.vad = None
|
||||||
@@ -221,10 +225,26 @@ class ConnectionHandler:
|
|||||||
"""保存记忆并关闭连接"""
|
"""保存记忆并关闭连接"""
|
||||||
try:
|
try:
|
||||||
if self.memory:
|
if self.memory:
|
||||||
await self.memory.save_memory(self.dialogue.dialogue)
|
# 使用线程池异步保存记忆
|
||||||
|
def save_memory_task():
|
||||||
|
try:
|
||||||
|
# 创建新事件循环(避免与主循环冲突)
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
loop.run_until_complete(
|
||||||
|
self.memory.save_memory(self.dialogue.dialogue)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
# 启动线程保存记忆,不等待完成
|
||||||
|
threading.Thread(target=save_memory_task, daemon=True).start()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||||
finally:
|
finally:
|
||||||
|
# 立即关闭连接,不等待记忆保存完成
|
||||||
await self.close(ws)
|
await self.close(ws)
|
||||||
|
|
||||||
async def reset_timeout(self):
|
async def reset_timeout(self):
|
||||||
@@ -250,11 +270,15 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).info("收到服务器重启指令,准备执行...")
|
self.logger.bind(tag=TAG).info("收到服务器重启指令,准备执行...")
|
||||||
|
|
||||||
# 发送确认响应
|
# 发送确认响应
|
||||||
await self.websocket.send(json.dumps({
|
await self.websocket.send(
|
||||||
"type": "server_response",
|
json.dumps(
|
||||||
"status": "success",
|
{
|
||||||
"message": "服务器重启中..."
|
"type": "server_response",
|
||||||
}))
|
"status": "success",
|
||||||
|
"message": "服务器重启中...",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# 异步执行重启操作
|
# 异步执行重启操作
|
||||||
def restart_server():
|
def restart_server():
|
||||||
@@ -266,7 +290,7 @@ class ConnectionHandler:
|
|||||||
stdin=sys.stdin,
|
stdin=sys.stdin,
|
||||||
stdout=sys.stdout,
|
stdout=sys.stdout,
|
||||||
stderr=sys.stderr,
|
stderr=sys.stderr,
|
||||||
start_new_session=True
|
start_new_session=True,
|
||||||
)
|
)
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
@@ -275,11 +299,15 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"重启失败: {str(e)}")
|
self.logger.bind(tag=TAG).error(f"重启失败: {str(e)}")
|
||||||
await self.websocket.send(json.dumps({
|
await self.websocket.send(
|
||||||
"type": "server_response",
|
json.dumps(
|
||||||
"status": "error",
|
{
|
||||||
"message": f"Restart failed: {str(e)}"
|
"type": "server_response",
|
||||||
}))
|
"status": "error",
|
||||||
|
"message": f"Restart failed: {str(e)}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _initialize_components(self):
|
def _initialize_components(self):
|
||||||
"""初始化组件"""
|
"""初始化组件"""
|
||||||
@@ -306,11 +334,13 @@ class ConnectionHandler:
|
|||||||
"""初始化ASR和TTS上报线程"""
|
"""初始化ASR和TTS上报线程"""
|
||||||
if not self.read_config_from_api or self.need_bind:
|
if not self.read_config_from_api or self.need_bind:
|
||||||
return
|
return
|
||||||
if self.tts_report_thread is None or not self.tts_report_thread.is_alive():
|
if self.chat_history_conf == 0:
|
||||||
self.tts_report_thread = threading.Thread(
|
return
|
||||||
target=self._tts_report_worker, daemon=True
|
if self.report_thread is None or not self.report_thread.is_alive():
|
||||||
|
self.report_thread = threading.Thread(
|
||||||
|
target=self._report_worker, daemon=True
|
||||||
)
|
)
|
||||||
self.tts_report_thread.start()
|
self.report_thread.start()
|
||||||
self.logger.bind(tag=TAG).info("TTS上报线程已启动")
|
self.logger.bind(tag=TAG).info("TTS上报线程已启动")
|
||||||
|
|
||||||
def _initialize_private_config(self):
|
def _initialize_private_config(self):
|
||||||
@@ -377,9 +407,12 @@ class ConnectionHandler:
|
|||||||
]["Intent"]
|
]["Intent"]
|
||||||
if private_config.get("prompt", None) is not None:
|
if private_config.get("prompt", None) is not None:
|
||||||
self.config["prompt"] = private_config["prompt"]
|
self.config["prompt"] = private_config["prompt"]
|
||||||
|
if private_config.get("summaryMemory", None) is not None:
|
||||||
|
self.config["summaryMemory"] = private_config["summaryMemory"]
|
||||||
if private_config.get("device_max_output_size", None) is not None:
|
if private_config.get("device_max_output_size", None) is not None:
|
||||||
self.max_output_size = int(private_config["device_max_output_size"])
|
self.max_output_size = int(private_config["device_max_output_size"])
|
||||||
|
if private_config.get("chat_history_conf", None) is not None:
|
||||||
|
self.chat_history_conf = int(private_config["chat_history_conf"])
|
||||||
try:
|
try:
|
||||||
modules = initialize_modules(
|
modules = initialize_modules(
|
||||||
self.logger,
|
self.logger,
|
||||||
@@ -409,7 +442,12 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
def _initialize_memory(self):
|
def _initialize_memory(self):
|
||||||
"""初始化记忆模块"""
|
"""初始化记忆模块"""
|
||||||
self.memory.init_memory(self.device_id, self.llm)
|
self.memory.init_memory(
|
||||||
|
role_id=self.device_id,
|
||||||
|
llm=self.llm,
|
||||||
|
summary_memory=self.config.get("summaryMemory", None),
|
||||||
|
save_to_file=not self.read_config_from_api,
|
||||||
|
)
|
||||||
|
|
||||||
def _initialize_intent(self):
|
def _initialize_intent(self):
|
||||||
self.intent_type = self.config["Intent"][
|
self.intent_type = self.config["Intent"][
|
||||||
@@ -838,7 +876,7 @@ class ConnectionHandler:
|
|||||||
if future is None:
|
if future is None:
|
||||||
continue
|
continue
|
||||||
text = None
|
text = None
|
||||||
opus_datas, tts_file = [], None
|
audio_datas, tts_file = [], None
|
||||||
try:
|
try:
|
||||||
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
||||||
tts_timeout = int(self.config.get("tts_timeout", 10))
|
tts_timeout = int(self.config.get("tts_timeout", 10))
|
||||||
@@ -860,8 +898,8 @@ class ConnectionHandler:
|
|||||||
audio_datas, _ = self.tts.audio_to_pcm_data(tts_file)
|
audio_datas, _ = self.tts.audio_to_pcm_data(tts_file)
|
||||||
else:
|
else:
|
||||||
audio_datas, _ = self.tts.audio_to_opus_data(tts_file)
|
audio_datas, _ = self.tts.audio_to_opus_data(tts_file)
|
||||||
# 在这里上报TTS数据(使用文件路径)
|
# 在这里上报TTS数据
|
||||||
enqueue_tts_report(self, 2, text, audio_datas)
|
enqueue_tts_report(self, text, audio_datas)
|
||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).error(
|
self.logger.bind(tag=TAG).error(
|
||||||
f"TTS出错:文件不存在{tts_file}"
|
f"TTS出错:文件不存在{tts_file}"
|
||||||
@@ -917,13 +955,12 @@ class ConnectionHandler:
|
|||||||
f"audio_play_priority priority_thread: {text} {e}"
|
f"audio_play_priority priority_thread: {text} {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _tts_report_worker(self):
|
def _report_worker(self):
|
||||||
"""TTS上报工作线程"""
|
"""聊天记录上报工作线程"""
|
||||||
|
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
# 从队列获取数据,设置超时以便定期检查停止事件
|
# 从队列获取数据,设置超时以便定期检查停止事件
|
||||||
item = self.tts_report_queue.get(timeout=1)
|
item = self.report_queue.get(timeout=1)
|
||||||
if item is None: # 检测毒丸对象
|
if item is None: # 检测毒丸对象
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -931,18 +968,18 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 执行上报(传入二进制数据)
|
# 执行上报(传入二进制数据)
|
||||||
report_tts(self, type, text, audio_data)
|
report(self, type, text, audio_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS上报线程异常: {e}")
|
self.logger.bind(tag=TAG).error(f"聊天记录上报线程异常: {e}")
|
||||||
finally:
|
finally:
|
||||||
# 标记任务完成
|
# 标记任务完成
|
||||||
self.tts_report_queue.task_done()
|
self.report_queue.task_done()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS上报工作线程异常: {e}")
|
self.logger.bind(tag=TAG).error(f"聊天记录上报工作线程异常: {e}")
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info("TTS上报线程已退出")
|
self.logger.bind(tag=TAG).info("聊天记录上报线程已退出")
|
||||||
|
|
||||||
def speak_and_play(self, text, text_index=0):
|
def speak_and_play(self, text, text_index=0):
|
||||||
if text is None or len(text) <= 0:
|
if text is None or len(text) <= 0:
|
||||||
@@ -971,6 +1008,7 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
async def close(self, ws=None):
|
async def close(self, ws=None):
|
||||||
"""资源清理方法"""
|
"""资源清理方法"""
|
||||||
|
|
||||||
# 取消超时任务
|
# 取消超时任务
|
||||||
if self.timeout_task:
|
if self.timeout_task:
|
||||||
self.timeout_task.cancel()
|
self.timeout_task.cancel()
|
||||||
@@ -980,43 +1018,42 @@ class ConnectionHandler:
|
|||||||
if hasattr(self, "mcp_manager") and self.mcp_manager:
|
if hasattr(self, "mcp_manager") and self.mcp_manager:
|
||||||
await self.mcp_manager.cleanup_all()
|
await self.mcp_manager.cleanup_all()
|
||||||
|
|
||||||
# 触发停止事件并清理资源
|
# 触发停止事件
|
||||||
if self.stop_event:
|
if self.stop_event:
|
||||||
self.stop_event.set()
|
self.stop_event.set()
|
||||||
|
|
||||||
# 立即关闭线程池
|
|
||||||
if self.executor:
|
|
||||||
self.executor.shutdown(wait=False, cancel_futures=True)
|
|
||||||
self.executor = None
|
|
||||||
|
|
||||||
# 添加毒丸对象到上报队列确保线程退出
|
|
||||||
self.tts_report_queue.put(None)
|
|
||||||
|
|
||||||
# 清空任务队列
|
# 清空任务队列
|
||||||
self.clear_queues()
|
self.clear_queues()
|
||||||
|
|
||||||
|
# 关闭WebSocket连接
|
||||||
if ws:
|
if ws:
|
||||||
await ws.close()
|
await ws.close()
|
||||||
elif self.websocket:
|
elif self.websocket:
|
||||||
await self.websocket.close()
|
await self.websocket.close()
|
||||||
|
|
||||||
|
# 最后关闭线程池(避免阻塞)
|
||||||
|
if self.executor:
|
||||||
|
self.executor.shutdown(wait=False)
|
||||||
|
self.executor = None
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info("连接资源已释放")
|
self.logger.bind(tag=TAG).info("连接资源已释放")
|
||||||
|
|
||||||
def clear_queues(self):
|
def clear_queues(self):
|
||||||
# 清空所有任务队列
|
"""清空所有任务队列"""
|
||||||
self.logger.bind(tag=TAG).debug(
|
self.logger.bind(tag=TAG).debug(
|
||||||
f"开始清理: TTS队列大小={self.tts_queue.qsize()}, 音频队列大小={self.audio_play_queue.qsize()}"
|
f"开始清理: TTS队列大小={self.tts_queue.qsize()}, 音频队列大小={self.audio_play_queue.qsize()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 使用非阻塞方式清空队列
|
||||||
for q in [self.tts_queue, self.audio_play_queue]:
|
for q in [self.tts_queue, self.audio_play_queue]:
|
||||||
if not q:
|
if not q:
|
||||||
continue
|
continue
|
||||||
while not q.empty():
|
while True:
|
||||||
try:
|
try:
|
||||||
q.get_nowait()
|
q.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
break
|
||||||
q.queue.clear()
|
|
||||||
# 添加毒丸信号到队列,确保线程退出
|
|
||||||
# q.queue.put(None)
|
|
||||||
self.logger.bind(tag=TAG).debug(
|
self.logger.bind(tag=TAG).debug(
|
||||||
f"清理结束: TTS队列大小={self.tts_queue.qsize()}, 音频队列大小={self.audio_play_queue.qsize()}"
|
f"清理结束: TTS队列大小={self.tts_queue.qsize()}, 音频队列大小={self.audio_play_queue.qsize()}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from core.utils.util import remove_punctuation_and_length
|
|||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
from core.handle.intentHandler import handle_user_intent
|
from core.handle.intentHandler import handle_user_intent
|
||||||
from core.utils.output_counter import check_device_output_limit
|
from core.utils.output_counter import check_device_output_limit
|
||||||
from core.handle.ttsReportHandle import enqueue_tts_report
|
from core.handle.reportHandle import enqueue_asr_report
|
||||||
from core.utils.util import audio_to_data
|
from core.utils.util import audio_to_data
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
@@ -44,7 +44,7 @@ async def handleAudioMessage(conn, audio):
|
|||||||
text_len, _ = remove_punctuation_and_length(text)
|
text_len, _ = remove_punctuation_and_length(text)
|
||||||
if text_len > 0:
|
if text_len > 0:
|
||||||
# 使用自定义模块进行上报
|
# 使用自定义模块进行上报
|
||||||
enqueue_tts_report(conn, 1, text, copy.deepcopy(conn.asr_audio))
|
enqueue_asr_report(conn, text, copy.deepcopy(conn.asr_audio))
|
||||||
|
|
||||||
await startToChat(conn, text)
|
await startToChat(conn, text)
|
||||||
else:
|
else:
|
||||||
@@ -98,9 +98,14 @@ async def no_voice_close_connect(conn):
|
|||||||
conn.close_after_chat = True
|
conn.close_after_chat = True
|
||||||
conn.client_abort = False
|
conn.client_abort = False
|
||||||
conn.asr_server_receive = False
|
conn.asr_server_receive = False
|
||||||
prompt = (
|
end_prompt = conn.config.get("end_prompt", {})
|
||||||
"请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
if end_prompt and end_prompt.get("enable", True) is False:
|
||||||
)
|
conn.logger.bind(tag=TAG).info("结束对话,无需发送结束提示语")
|
||||||
|
await conn.close()
|
||||||
|
return
|
||||||
|
prompt = end_prompt.get("prompt")
|
||||||
|
if not prompt:
|
||||||
|
prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。!"
|
||||||
await startToChat(conn, prompt)
|
await startToChat(conn, prompt)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ TTS上报功能已集成到ConnectionHandler类中。
|
|||||||
|
|
||||||
import opuslib_next
|
import opuslib_next
|
||||||
|
|
||||||
from config.manage_api_client import report
|
from config.manage_api_client import report as manage_report
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
|
|
||||||
def report_tts(conn, type, text, opus_data):
|
def report(conn, type, text, opus_data):
|
||||||
"""执行TTS上报操作
|
"""执行聊天记录上报操作
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
conn: 连接对象
|
conn: 连接对象
|
||||||
@@ -31,7 +31,7 @@ def report_tts(conn, type, text, opus_data):
|
|||||||
else:
|
else:
|
||||||
audio_data = None
|
audio_data = None
|
||||||
# 执行上报
|
# 执行上报
|
||||||
report(
|
manage_report(
|
||||||
mac_address=conn.device_id,
|
mac_address=conn.device_id,
|
||||||
session_id=conn.session_id,
|
session_id=conn.session_id,
|
||||||
chat_type=type,
|
chat_type=type,
|
||||||
@@ -39,7 +39,7 @@ def report_tts(conn, type, text, opus_data):
|
|||||||
audio=audio_data,
|
audio=audio_data,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.logger.bind(tag=TAG).error(f"TTS上报失败: {e}")
|
conn.logger.bind(tag=TAG).error(f"聊天记录上报失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
def opus_to_wav(conn, opus_data):
|
def opus_to_wav(conn, opus_data):
|
||||||
@@ -89,8 +89,10 @@ def opus_to_wav(conn, opus_data):
|
|||||||
return bytes(wav_header) + pcm_data_bytes
|
return bytes(wav_header) + pcm_data_bytes
|
||||||
|
|
||||||
|
|
||||||
def enqueue_tts_report(conn, type, text, opus_data):
|
def enqueue_tts_report(conn, text, opus_data):
|
||||||
if not conn.read_config_from_api or conn.need_bind:
|
if not conn.read_config_from_api or conn.need_bind or not conn.report_tts_enable:
|
||||||
|
return
|
||||||
|
if conn.chat_history_conf == 0:
|
||||||
return
|
return
|
||||||
"""将TTS数据加入上报队列
|
"""将TTS数据加入上报队列
|
||||||
|
|
||||||
@@ -101,10 +103,43 @@ def enqueue_tts_report(conn, type, text, opus_data):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
||||||
conn.tts_report_queue.put((type, text, opus_data))
|
if conn.chat_history_conf == 2:
|
||||||
|
conn.report_queue.put((2, text, opus_data))
|
||||||
conn.logger.bind(tag=TAG).debug(
|
conn.logger.bind(tag=TAG).debug(
|
||||||
f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
conn.report_queue.put((2, text, None))
|
||||||
|
conn.logger.bind(tag=TAG).debug(
|
||||||
|
f"TTS数据已加入上报队列: {conn.device_id}, 不上报音频"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
|
conn.logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_asr_report(conn, text, opus_data):
|
||||||
|
if not conn.read_config_from_api or conn.need_bind or not conn.report_asr_enable:
|
||||||
|
return
|
||||||
|
if conn.chat_history_conf == 0:
|
||||||
|
return
|
||||||
|
"""将ASR数据加入上报队列
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conn: 连接对象
|
||||||
|
text: 合成文本
|
||||||
|
opus_data: opus音频数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
||||||
|
if conn.chat_history_conf == 2:
|
||||||
|
conn.report_queue.put((1, text, opus_data))
|
||||||
|
conn.logger.bind(tag=TAG).debug(
|
||||||
|
f"ASR数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.report_queue.put((1, text, None))
|
||||||
|
conn.logger.bind(tag=TAG).debug(
|
||||||
|
f"ASR数据已加入上报队列: {conn.device_id}, 不上报音频"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
conn.logger.bind(tag=TAG).error(f"加入ASR上报队列失败: {text}, {e}")
|
||||||
@@ -5,7 +5,7 @@ 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.sendAudioHandle import send_stt_message, send_tts_message
|
||||||
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
||||||
from core.handle.ttsReportHandle import enqueue_tts_report
|
from core.handle.reportHandle import enqueue_asr_report
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
@@ -56,11 +56,11 @@ async def handleTextMessage(conn, message):
|
|||||||
await send_tts_message(conn, "stop", None)
|
await send_tts_message(conn, "stop", None)
|
||||||
elif is_wakeup_words:
|
elif is_wakeup_words:
|
||||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||||
enqueue_tts_report(conn, 1, "嘿,你好呀", [])
|
enqueue_asr_report(conn, "嘿,你好呀", [])
|
||||||
await startToChat(conn, "嘿,你好呀")
|
await startToChat(conn, "嘿,你好呀")
|
||||||
else:
|
else:
|
||||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||||
enqueue_tts_report(conn, 1, text, [])
|
enqueue_asr_report(conn, text, [])
|
||||||
# 否则需要LLM对文字内容进行答复
|
# 否则需要LLM对文字内容进行答复
|
||||||
await startToChat(conn, text)
|
await startToChat(conn, text)
|
||||||
elif msg_json["type"] == "iot":
|
elif msg_json["type"] == "iot":
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""将语音数据转换为文本"""
|
"""将语音数据转换为文本"""
|
||||||
if not opus_data:
|
if not opus_data:
|
||||||
logger.bind(tag=TAG).warn("音频数据为空!")
|
logger.bind(tag=TAG).warning("音频数据为空!")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
file_path = None
|
file_path = None
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import wave
|
|||||||
import websockets
|
import websockets
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import re
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -24,7 +25,12 @@ class ASRProvider(ASRProviderBase):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.host = config.get("host", "localhost")
|
self.host = config.get("host", "localhost")
|
||||||
self.port = config.get("port", 10095)
|
self.port = config.get("port", 10095)
|
||||||
self.is_ssl = config.get("is_ssl", True)
|
self.api_key = config.get("api_key", "none")
|
||||||
|
self.is_ssl = str(config.get("is_ssl", True)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.output_dir = config.get("output_dir")
|
self.output_dir = config.get("output_dir")
|
||||||
self.delete_audio_file = delete_audio_file
|
self.delete_audio_file = delete_audio_file
|
||||||
self.uri = (
|
self.uri = (
|
||||||
@@ -130,9 +136,13 @@ class ASRProvider(ASRProviderBase):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
file_path = self.save_audio_to_file(pcm_data, session_id)
|
file_path = self.save_audio_to_file(pcm_data, session_id)
|
||||||
|
auth_header = {"Authorization": "Bearer; {}".format(self.api_key)}
|
||||||
async with websockets.connect(
|
async with websockets.connect(
|
||||||
self.uri, subprotocols=["binary"], ping_interval=None, ssl=self.ssl_context
|
self.uri,
|
||||||
|
additional_headers=auth_header,
|
||||||
|
subprotocols=["binary"],
|
||||||
|
ping_interval=None,
|
||||||
|
ssl=self.ssl_context,
|
||||||
) as ws:
|
) as ws:
|
||||||
try:
|
try:
|
||||||
# Use asyncio to handle WebSocket communication
|
# Use asyncio to handle WebSocket communication
|
||||||
@@ -157,6 +167,9 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
# Get the result from the receive task
|
# Get the result from the receive task
|
||||||
result = receive_task.result()
|
result = receive_task.result()
|
||||||
|
match = re.match(r"<\|(.*?)\|><\|(.*?)\|><\|(.*?)\|>(.*)", result)
|
||||||
|
if match:
|
||||||
|
result = match.group(4).strip()
|
||||||
return (
|
return (
|
||||||
result,
|
result,
|
||||||
file_path,
|
file_path,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""将语音数据转换为文本"""
|
"""将语音数据转换为文本"""
|
||||||
if not opus_data:
|
if not opus_data:
|
||||||
logger.bind(tag=TAG).warn("音频数据为空!")
|
logger.bind(tag=TAG).warning("音频数据为空!")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
file_path = None
|
file_path = None
|
||||||
@@ -230,7 +230,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
if "Response" in response_json and "Result" in response_json["Response"]:
|
if "Response" in response_json and "Result" in response_json["Response"]:
|
||||||
return response_json["Response"]["Result"]
|
return response_json["Response"]["Result"]
|
||||||
else:
|
else:
|
||||||
logger.bind(tag=TAG).warn(f"响应中没有识别结果: {response_json}")
|
logger.bind(tag=TAG).warning(f"响应中没有识别结果: {response_json}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class IntentProvider(IntentProviderBase):
|
|||||||
|
|
||||||
# 记录识别到的function call
|
# 记录识别到的function call
|
||||||
logger.bind(tag=TAG).info(
|
logger.bind(tag=TAG).info(
|
||||||
f"识别到function call: {function_name}, 参数: {function_args}"
|
f"llm 识别到意图: {function_name}, 参数: {function_args}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到缓存
|
# 添加到缓存
|
||||||
|
|||||||
@@ -61,5 +61,6 @@ class LLMProvider(LLMProviderBase):
|
|||||||
yield "【LLM服务响应异常】"
|
yield "【LLM服务响应异常】"
|
||||||
|
|
||||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
logger.bind(tag=TAG).info(f"阿里百练暂未实现完整的工具调用(function call)")
|
logger.bind(tag=TAG).error(
|
||||||
return self.response(session_id, dialogue)
|
f"阿里百练暂未实现完整的工具调用(function call),建议使用其他意图识别"
|
||||||
|
)
|
||||||
|
|||||||
@@ -66,5 +66,6 @@ class LLMProvider(LLMProviderBase):
|
|||||||
yield "【服务响应异常】"
|
yield "【服务响应异常】"
|
||||||
|
|
||||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
logger.bind(tag=TAG).info(f"fastgpt暂未实现完整的工具调用(function call)")
|
logger.bind(tag=TAG).error(
|
||||||
return self.response(session_id, dialogue)
|
f"fastgpt暂未实现完整的工具调用(function call),建议使用其他意图识别"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,140 +1,205 @@
|
|||||||
import google.generativeai as genai
|
import os, json, uuid
|
||||||
from core.utils.util import check_model_key
|
from types import SimpleNamespace
|
||||||
from core.providers.llm.base import LLMProviderBase
|
from typing import Any, Dict, List
|
||||||
from config.logger import setup_logging
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from google import generativeai as genai
|
||||||
|
from google.generativeai import types, GenerationConfig
|
||||||
|
|
||||||
|
from core.providers.llm.base import LLMProviderBase
|
||||||
|
from core.utils.util import check_model_key
|
||||||
|
from config.logger import setup_logging
|
||||||
|
from google.generativeai.types import GenerateContentResponse
|
||||||
|
from requests import RequestException
|
||||||
|
|
||||||
|
log = setup_logging()
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
def test_proxy(proxy_url: str, test_url: str) -> bool:
|
||||||
|
try:
|
||||||
|
resp = requests.get(test_url, proxies={"http": proxy_url, "https": proxy_url})
|
||||||
|
return 200 <= resp.status_code < 400
|
||||||
|
except RequestException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def setup_proxy_env(http_proxy: str | None, https_proxy: str | None):
|
||||||
|
"""
|
||||||
|
分别测试 HTTP 和 HTTPS 代理是否可用,并设置环境变量。
|
||||||
|
如果 HTTPS 代理不可用但 HTTP 可用,会将 HTTPS_PROXY 也指向 HTTP。
|
||||||
|
"""
|
||||||
|
test_http_url = "http://www.google.com"
|
||||||
|
test_https_url = "https://www.google.com"
|
||||||
|
|
||||||
|
ok_http = ok_https = False
|
||||||
|
|
||||||
|
if http_proxy:
|
||||||
|
ok_http = test_proxy(http_proxy, test_http_url)
|
||||||
|
if ok_http:
|
||||||
|
os.environ["HTTP_PROXY"] = http_proxy
|
||||||
|
log.bind(tag=TAG).info(f"配置提供的Gemini HTTPS代理连通成功: {http_proxy}")
|
||||||
|
else:
|
||||||
|
log.bind(tag=TAG).warning(f"配置提供的Gemini HTTP代理不可用: {http_proxy}")
|
||||||
|
|
||||||
|
if https_proxy:
|
||||||
|
ok_https = test_proxy(https_proxy, test_https_url)
|
||||||
|
if ok_https:
|
||||||
|
os.environ["HTTPS_PROXY"] = https_proxy
|
||||||
|
log.bind(tag=TAG).info(f"配置提供的Gemini HTTPS代理连通成功: {https_proxy}")
|
||||||
|
else:
|
||||||
|
log.bind(tag=TAG).warning(
|
||||||
|
f"配置提供的Gemini HTTPS代理不可用: {https_proxy}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果https_proxy不可用,但http_proxy可用且能走通https,则复用http_proxy作为https_proxy
|
||||||
|
if ok_http and not ok_https:
|
||||||
|
if test_proxy(http_proxy, test_https_url):
|
||||||
|
os.environ["HTTPS_PROXY"] = http_proxy
|
||||||
|
ok_https = True
|
||||||
|
log.bind(tag=TAG).info(f"复用HTTP代理作为HTTPS代理: {http_proxy}")
|
||||||
|
|
||||||
|
if not ok_http and not ok_https:
|
||||||
|
log.bind(tag=TAG).error(
|
||||||
|
f"Gemini 代理设置失败: HTTP 和 HTTPS 代理都不可用,请检查配置"
|
||||||
|
)
|
||||||
|
raise RuntimeError("HTTP 和 HTTPS 代理都不可用,请检查配置")
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
class LLMProvider(LLMProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, cfg: Dict[str, Any]):
|
||||||
"""初始化Gemini LLM Provider"""
|
self.model_name = cfg.get("model_name", "gemini-2.0-flash")
|
||||||
self.model_name = config.get("model_name", "gemini-1.5-pro")
|
self.api_key = cfg["api_key"]
|
||||||
self.api_key = config.get("api_key")
|
http_proxy = cfg.get("http_proxy")
|
||||||
self.http_proxy = config.get("http_proxy")
|
https_proxy = cfg.get("https_proxy")
|
||||||
self.https_proxy = config.get("https_proxy")
|
|
||||||
have_key = check_model_key("LLM", self.api_key)
|
|
||||||
|
|
||||||
if not have_key:
|
if not check_model_key("LLM", self.api_key):
|
||||||
return
|
raise ValueError("无效的Gemini API Key,请检查是否配置正确")
|
||||||
|
|
||||||
try:
|
if http_proxy or https_proxy:
|
||||||
# 初始化Gemini客户端
|
log.bind(tag=TAG).info(
|
||||||
# 配置代理(如果提供了代理配置)
|
f"检测到Gemini代理配置,开始测试代理连通性和设置代理环境..."
|
||||||
self.proxies = None
|
)
|
||||||
if self.http_proxy is not "" or self.https_proxy is not "":
|
setup_proxy_env(http_proxy, https_proxy)
|
||||||
|
log.bind(tag=TAG).info(
|
||||||
|
f"Gemini 代理设置成功 - HTTP: {http_proxy}, HTTPS: {https_proxy}"
|
||||||
|
)
|
||||||
|
genai.configure(api_key=self.api_key)
|
||||||
|
self.model = genai.GenerativeModel(self.model_name)
|
||||||
|
|
||||||
self.proxies = {
|
self.gen_cfg = GenerationConfig(
|
||||||
"http": self.http_proxy,
|
temperature=0.7,
|
||||||
"https": self.https_proxy,
|
top_p=0.9,
|
||||||
}
|
top_k=40,
|
||||||
logger.bind(tag=TAG).info(f"Gemini set proxys:{self.proxies}")
|
max_output_tokens=2048,
|
||||||
# 使用猴子补丁修改 google-generativeai 库的请求会话
|
)
|
||||||
|
|
||||||
# 使用 session 对象配置 genai
|
@staticmethod
|
||||||
|
def _build_tools(funcs: List[Dict[str, Any]] | None):
|
||||||
genai.configure(api_key=self.api_key)
|
if not funcs:
|
||||||
self.model = genai.GenerativeModel(self.model_name)
|
return None
|
||||||
|
return [
|
||||||
# 设置生成参数
|
types.Tool(
|
||||||
self.generation_config = {
|
function_declarations=[
|
||||||
"temperature": 0.7,
|
types.FunctionDeclaration(
|
||||||
"top_p": 0.9,
|
name=f["function"]["name"],
|
||||||
"top_k": 40,
|
description=f["function"]["description"],
|
||||||
"max_output_tokens": 2048,
|
parameters=f["function"]["parameters"],
|
||||||
}
|
)
|
||||||
self.chat = None
|
for f in funcs
|
||||||
except Exception as e:
|
]
|
||||||
logger.bind(tag=TAG).error(f"Gemini初始化失败: {e}")
|
)
|
||||||
self.model = None
|
]
|
||||||
|
|
||||||
|
# Gemini文档提到,无需维护session-id,直接用dialogue拼接而成
|
||||||
def response(self, session_id, dialogue):
|
def response(self, session_id, dialogue):
|
||||||
"""生成Gemini对话响应"""
|
yield from self._generate(dialogue, None)
|
||||||
if not self.model:
|
|
||||||
yield "【Gemini服务未正确初始化】"
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 处理对话历史
|
|
||||||
chat_history = []
|
|
||||||
for msg in dialogue[:-1]: # 历史对话
|
|
||||||
role = "model" if msg["role"] == "assistant" else "user"
|
|
||||||
content = msg["content"].strip()
|
|
||||||
if content:
|
|
||||||
chat_history.append({"role": role, "parts": [{"text": content}]})
|
|
||||||
|
|
||||||
# 获取当前消息
|
|
||||||
current_msg = dialogue[-1]["content"]
|
|
||||||
|
|
||||||
# 构建请求体
|
|
||||||
request_body = {
|
|
||||||
"contents": chat_history
|
|
||||||
+ [{"role": "user", "parts": [{"text": current_msg}]}],
|
|
||||||
"generationConfig": self.generation_config,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 构建请求URL
|
|
||||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}:generateContent?key={self.api_key}"
|
|
||||||
|
|
||||||
# 构建请求头
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 发送POST请求,经测试手动 request 无法使用 stream 模式
|
|
||||||
if self.proxies:
|
|
||||||
response = requests.post(
|
|
||||||
url,
|
|
||||||
headers=headers,
|
|
||||||
json=request_body,
|
|
||||||
stream=False,
|
|
||||||
proxies=self.proxies,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
data = response.json() # 直接解析JSON
|
|
||||||
if "candidates" in data and data["candidates"]:
|
|
||||||
yield data["candidates"][0]["content"]["parts"][0]["text"]
|
|
||||||
else:
|
|
||||||
yield "未找到候选回复。"
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
yield f"JSON解码错误:{e}"
|
|
||||||
except Exception as e:
|
|
||||||
yield f"发生错误:{e}"
|
|
||||||
else:
|
|
||||||
logger.bind(tag=TAG).info(f"Gemini stream mode ")
|
|
||||||
chat = self.model.start_chat(history=chat_history)
|
|
||||||
|
|
||||||
# 发送消息并获取流式响应
|
|
||||||
response = chat.send_message(
|
|
||||||
current_msg, stream=True, generation_config=self.generation_config
|
|
||||||
)
|
|
||||||
# 处理流式响应
|
|
||||||
for chunk in response:
|
|
||||||
if hasattr(chunk, "text") and chunk.text:
|
|
||||||
yield chunk.text
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}")
|
|
||||||
|
|
||||||
# 针对不同错误返回友好提示
|
|
||||||
if "Rate limit" in error_msg:
|
|
||||||
yield "【Gemini服务请求太频繁,请稍后再试】"
|
|
||||||
elif "Invalid API key" in error_msg:
|
|
||||||
yield "【Gemini API key无效】"
|
|
||||||
else:
|
|
||||||
yield f"【Gemini服务响应异常: {error_msg}】"
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
yield f"请求失败:{e}"
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
yield f"JSON解码错误:{e}"
|
|
||||||
except Exception as e:
|
|
||||||
yield f"发生错误:{e}"
|
|
||||||
|
|
||||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
logger.bind(tag=TAG).info(f"gemini暂未实现完整的工具调用(function call)")
|
yield from self._generate(dialogue, self._build_tools(functions))
|
||||||
return self.response(session_id, dialogue)
|
|
||||||
|
def _generate(self, dialogue, tools):
|
||||||
|
role_map = {"assistant": "model", "user": "user"}
|
||||||
|
contents: list = []
|
||||||
|
# 拼接对话
|
||||||
|
for m in dialogue:
|
||||||
|
r = m["role"]
|
||||||
|
|
||||||
|
if r == "assistant" and "tool_calls" in m:
|
||||||
|
tc = m["tool_calls"][0]
|
||||||
|
contents.append(
|
||||||
|
{
|
||||||
|
"role": "model",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"function_call": {
|
||||||
|
"name": tc["function"]["name"],
|
||||||
|
"args": json.loads(tc["function"]["arguments"]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if r == "tool":
|
||||||
|
contents.append(
|
||||||
|
{
|
||||||
|
"role": "model",
|
||||||
|
"parts": [{"text": str(m.get("content", ""))}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
contents.append(
|
||||||
|
{
|
||||||
|
"role": role_map.get(r, "user"),
|
||||||
|
"parts": [{"text": str(m.get("content", ""))}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
stream: GenerateContentResponse = self.model.generate_content(
|
||||||
|
contents=contents,
|
||||||
|
generation_config=self.gen_cfg,
|
||||||
|
tools=tools,
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for chunk in stream:
|
||||||
|
cand = chunk.candidates[0]
|
||||||
|
for part in cand.content.parts:
|
||||||
|
# a) 函数调用-通常是最后一段话才是函数调用
|
||||||
|
if getattr(part, "function_call", None):
|
||||||
|
fc = part.function_call
|
||||||
|
yield None, [
|
||||||
|
SimpleNamespace(
|
||||||
|
id=uuid.uuid4().hex,
|
||||||
|
type="function",
|
||||||
|
function=SimpleNamespace(
|
||||||
|
name=fc.name,
|
||||||
|
arguments=json.dumps(
|
||||||
|
dict(fc.args), ensure_ascii=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return
|
||||||
|
# b) 普通文本
|
||||||
|
if getattr(part, "text", None):
|
||||||
|
yield part.text if tools is None else (part.text, None)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if tools is not None:
|
||||||
|
yield None, None # function‑mode 结束,返回哑包
|
||||||
|
|
||||||
|
# 关闭stream,预留后续打断对话功能的功能方法,官方文档推荐打断对话要关闭上一个流,可以有效减少配额计费和资源占用
|
||||||
|
@staticmethod
|
||||||
|
def _safe_finish_stream(stream: GenerateContentResponse):
|
||||||
|
if hasattr(stream, "resolve"):
|
||||||
|
stream.resolve() # Gemini SDK version ≥ 0.5.0
|
||||||
|
elif hasattr(stream, "close"):
|
||||||
|
stream.close() # Gemini SDK version < 0.5.0
|
||||||
|
else:
|
||||||
|
for _ in stream: # 兜底耗尽
|
||||||
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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):
|
||||||
|
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}")
|
||||||
|
|
||||||
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
|
logger.bind(tag=TAG).error(
|
||||||
|
f"homeassistant不支持(function call),建议使用其他意图识别"
|
||||||
|
)
|
||||||
@@ -4,6 +4,7 @@ from config.logger import setup_logging
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class MemoryProviderBase(ABC):
|
class MemoryProviderBase(ABC):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -20,6 +21,6 @@ class MemoryProviderBase(ABC):
|
|||||||
"""Query memories for specific role based on similarity"""
|
"""Query memories for specific role based on similarity"""
|
||||||
return "please implement query method"
|
return "please implement query method"
|
||||||
|
|
||||||
def init_memory(self, role_id, llm):
|
def init_memory(self, role_id, llm, **kwargs):
|
||||||
self.role_id = role_id
|
self.role_id = role_id
|
||||||
self.llm = llm
|
self.llm = llm
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ TAG = __name__
|
|||||||
|
|
||||||
|
|
||||||
class MemoryProvider(MemoryProviderBase):
|
class MemoryProvider(MemoryProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, config, summary_memory=None):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.api_key = config.get("api_key", "")
|
self.api_key = config.get("api_key", "")
|
||||||
self.api_version = config.get("api_version", "v1.1")
|
self.api_version = config.get("api_version", "v1.1")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
from config.config_loader import get_project_dir
|
from config.config_loader import get_project_dir
|
||||||
|
from config.manage_api_client import save_mem_local_short
|
||||||
|
|
||||||
|
|
||||||
short_term_memory_prompt = """
|
short_term_memory_prompt = """
|
||||||
@@ -72,6 +73,17 @@ short_term_memory_prompt = """
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
short_term_memory_prompt_only_content = """
|
||||||
|
你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:
|
||||||
|
1、总结user的重要信息,以便在未来的对话中提供更个性化的服务
|
||||||
|
2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字内,否则不要遗忘、不要压缩用户的历史记忆
|
||||||
|
3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中
|
||||||
|
4、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中
|
||||||
|
5、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的
|
||||||
|
6、只需要返回总结摘要,严格控制在1800字内
|
||||||
|
7、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def extract_json_data(json_code):
|
def extract_json_data(json_code):
|
||||||
start = json_code.find("```json")
|
start = json_code.find("```json")
|
||||||
@@ -93,17 +105,26 @@ TAG = __name__
|
|||||||
|
|
||||||
|
|
||||||
class MemoryProvider(MemoryProviderBase):
|
class MemoryProvider(MemoryProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, config, summary_memory):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.short_momery = ""
|
self.short_momery = ""
|
||||||
|
self.save_to_file = True
|
||||||
self.memory_path = get_project_dir() + "data/.memory.yaml"
|
self.memory_path = get_project_dir() + "data/.memory.yaml"
|
||||||
self.load_memory()
|
self.load_memory(summary_memory)
|
||||||
|
|
||||||
def init_memory(self, role_id, llm):
|
def init_memory(
|
||||||
super().init_memory(role_id, llm)
|
self, role_id, llm, summary_memory=None, save_to_file=True, **kwargs
|
||||||
self.load_memory()
|
):
|
||||||
|
super().init_memory(role_id, llm, **kwargs)
|
||||||
|
self.save_to_file = save_to_file
|
||||||
|
self.load_memory(summary_memory)
|
||||||
|
|
||||||
|
def load_memory(self, summary_memory):
|
||||||
|
# api获取到总结记忆后直接返回
|
||||||
|
if summary_memory or not self.save_to_file:
|
||||||
|
self.short_momery = summary_memory
|
||||||
|
return
|
||||||
|
|
||||||
def load_memory(self):
|
|
||||||
all_memory = {}
|
all_memory = {}
|
||||||
if os.path.exists(self.memory_path):
|
if os.path.exists(self.memory_path):
|
||||||
with open(self.memory_path, "r", encoding="utf-8") as f:
|
with open(self.memory_path, "r", encoding="utf-8") as f:
|
||||||
@@ -134,7 +155,7 @@ class MemoryProvider(MemoryProviderBase):
|
|||||||
msgStr += f"User: {msg.content}\n"
|
msgStr += f"User: {msg.content}\n"
|
||||||
elif msg.role == "assistant":
|
elif msg.role == "assistant":
|
||||||
msgStr += f"Assistant: {msg.content}\n"
|
msgStr += f"Assistant: {msg.content}\n"
|
||||||
if len(self.short_momery) > 0:
|
if self.short_momery and len(self.short_momery) > 0:
|
||||||
msgStr += "历史记忆:\n"
|
msgStr += "历史记忆:\n"
|
||||||
msgStr += self.short_momery
|
msgStr += self.short_momery
|
||||||
|
|
||||||
@@ -142,16 +163,20 @@ class MemoryProvider(MemoryProviderBase):
|
|||||||
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||||
msgStr += f"当前时间:{time_str}"
|
msgStr += f"当前时间:{time_str}"
|
||||||
|
|
||||||
result = self.llm.response_no_stream(short_term_memory_prompt, msgStr)
|
if self.save_to_file:
|
||||||
|
result = self.llm.response_no_stream(short_term_memory_prompt, msgStr)
|
||||||
json_str = extract_json_data(result)
|
json_str = extract_json_data(result)
|
||||||
try:
|
try:
|
||||||
json_data = json.loads(json_str) # 检查json格式是否正确
|
json.loads(json_str) # 检查json格式是否正确
|
||||||
self.short_momery = json_str
|
self.short_momery = json_str
|
||||||
except Exception as e:
|
self.save_memory_to_file()
|
||||||
print("Error:", e)
|
except Exception as e:
|
||||||
|
print("Error:", e)
|
||||||
self.save_memory_to_file()
|
else:
|
||||||
|
result = self.llm.response_no_stream(
|
||||||
|
short_term_memory_prompt_only_content, msgStr
|
||||||
|
)
|
||||||
|
save_mem_local_short(self.role_id, result)
|
||||||
logger.bind(tag=TAG).info(f"Save memory successful - Role: {self.role_id}")
|
logger.bind(tag=TAG).info(f"Save memory successful - Role: {self.role_id}")
|
||||||
|
|
||||||
return self.short_momery
|
return self.short_momery
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
'''
|
"""
|
||||||
不使用记忆,可以选择此模块
|
不使用记忆,可以选择此模块
|
||||||
'''
|
"""
|
||||||
|
|
||||||
from ..base import MemoryProviderBase, logger
|
from ..base import MemoryProviderBase, logger
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
|
|
||||||
class MemoryProvider(MemoryProviderBase):
|
class MemoryProvider(MemoryProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, config, summary_memory=None):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|
||||||
async def save_memory(self, msgs):
|
async def save_memory(self, msgs):
|
||||||
logger.bind(tag=TAG).debug("nomem mode: No memory saving is performed.")
|
logger.bind(tag=TAG).debug("nomem mode: No memory saving is performed.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def query_memory(self, query: str)-> str:
|
async def query_memory(self, query: str) -> str:
|
||||||
logger.bind(tag=TAG).debug("nomem mode: No memory query is performed.")
|
logger.bind(tag=TAG).debug("nomem mode: No memory query is performed.")
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -85,16 +85,22 @@ class TTSProvider(TTSProviderBase):
|
|||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
|
|
||||||
self.reference_id = config.get("reference_id")
|
self.reference_id = (
|
||||||
|
None if not config.get("reference_id") else config.get("reference_id")
|
||||||
|
)
|
||||||
self.reference_audio = parse_string_to_list(config.get("reference_audio"))
|
self.reference_audio = parse_string_to_list(config.get("reference_audio"))
|
||||||
self.reference_text = parse_string_to_list(config.get("reference_text"))
|
self.reference_text = parse_string_to_list(config.get("reference_text"))
|
||||||
self.format = config.get("format", "wav")
|
self.format = config.get("response_format", "wav")
|
||||||
|
|
||||||
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
||||||
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
||||||
if not have_key:
|
if not have_key:
|
||||||
return
|
return
|
||||||
self.normalize = config.get("normalize", True)
|
self.normalize = str(config.get("normalize", True)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
|
|
||||||
# 处理空字符串的情况
|
# 处理空字符串的情况
|
||||||
channels = config.get("channels", "1")
|
channels = config.get("channels", "1")
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ class Dialogue:
|
|||||||
|
|
||||||
if system_message:
|
if system_message:
|
||||||
enhanced_system_prompt = (
|
enhanced_system_prompt = (
|
||||||
f"{system_message.content}\n\n" f"相关记忆:\n{memory_str}"
|
f"{system_message.content}\n\n"
|
||||||
|
f"以下是用户的历史记忆:\n```\n{memory_str}\n```"
|
||||||
)
|
)
|
||||||
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
||||||
|
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ def initialize_modules(
|
|||||||
modules["memory"] = memory.create_instance(
|
modules["memory"] = memory.create_instance(
|
||||||
memory_type,
|
memory_type,
|
||||||
config["Memory"][select_memory_module],
|
config["Memory"][select_memory_module],
|
||||||
|
config.get('summaryMemory', None),
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
|
logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
|
||||||
|
|
||||||
@@ -930,7 +931,6 @@ def check_vad_update(before_config, new_config):
|
|||||||
if "type" not in new_config["VAD"][new_vad_module]
|
if "type" not in new_config["VAD"][new_vad_module]
|
||||||
else new_config["VAD"][new_vad_module]["type"]
|
else new_config["VAD"][new_vad_module]["type"]
|
||||||
)
|
)
|
||||||
print(f"前vad:{current_vad_type},后vad:{new_vad_type}")
|
|
||||||
update_vad = current_vad_type != new_vad_type
|
update_vad = current_vad_type != new_vad_type
|
||||||
return update_vad
|
return update_vad
|
||||||
|
|
||||||
@@ -954,6 +954,5 @@ def check_asr_update(before_config, new_config):
|
|||||||
if "type" not in new_config["ASR"][new_asr_module]
|
if "type" not in new_config["ASR"][new_asr_module]
|
||||||
else new_config["ASR"][new_asr_module]["type"]
|
else new_config["ASR"][new_asr_module]["type"]
|
||||||
)
|
)
|
||||||
print(f"前asr:{current_asr_type},后asr:{new_asr_type}")
|
|
||||||
update_asr = current_asr_type != new_asr_type
|
update_asr = current_asr_type != new_asr_type
|
||||||
return update_asr
|
return update_asr
|
||||||
|
|||||||
@@ -7,6 +7,15 @@
|
|||||||
"当前支持stdio/sse两种模式。"
|
"当前支持stdio/sse两种模式。"
|
||||||
],
|
],
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
|
"Home Assistant": {
|
||||||
|
"command": "mcp-proxy",
|
||||||
|
"args": [
|
||||||
|
"http://YOUR_HA_HOST/mcp_server/sse"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"API_ACCESS_TOKEN": "YOUR_API_ACCESS_TOKEN"
|
||||||
|
}
|
||||||
|
},
|
||||||
"filesystem": {
|
"filesystem": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": [
|
"args": [
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ WEATHER_CODE_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def fetch_city_info(location, api_key):
|
def fetch_city_info(location, api_key, api_host):
|
||||||
url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh"
|
url = f"https://{api_host}/geo/v2/city/lookup?key={api_key}&location={location}&lang=zh"
|
||||||
response = requests.get(url, headers=HEADERS).json()
|
response = requests.get(url, headers=HEADERS).json()
|
||||||
return response.get("location", [])[0] if response.get("location") else None
|
return response.get("location", [])[0] if response.get("location") else None
|
||||||
|
|
||||||
@@ -151,7 +151,8 @@ def parse_weather_info(soup):
|
|||||||
|
|
||||||
@register_function("get_weather", GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
@register_function("get_weather", GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
||||||
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
|
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
|
||||||
api_key = conn.config["plugins"]["get_weather"]["api_key"]
|
api_host = conn.config["plugins"]["get_weather"].get("api_host", "mj7p3y7naa.re.qweatherapi.com")
|
||||||
|
api_key = conn.config["plugins"]["get_weather"].get("api_key", "a861d0d5e7bf4ee1a83d9a9e4f96d4da")
|
||||||
default_location = conn.config["plugins"]["get_weather"]["default_location"]
|
default_location = conn.config["plugins"]["get_weather"]["default_location"]
|
||||||
client_ip = conn.client_ip
|
client_ip = conn.client_ip
|
||||||
# 优先使用用户提供的location参数
|
# 优先使用用户提供的location参数
|
||||||
@@ -164,8 +165,7 @@ def get_weather(conn, location: str = None, lang: str = "zh_CN"):
|
|||||||
else:
|
else:
|
||||||
# 若IP解析失败或无IP,使用默认位置
|
# 若IP解析失败或无IP,使用默认位置
|
||||||
location = default_location
|
location = default_location
|
||||||
|
city_info = fetch_city_info(location, api_key, api_host)
|
||||||
city_info = fetch_city_info(location, api_key)
|
|
||||||
if not city_info:
|
if not city_info:
|
||||||
return ActionResponse(
|
return ActionResponse(
|
||||||
Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None
|
Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None
|
||||||
|
|||||||