mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8376b6f0f2 | ||
|
|
490a66d3fe | ||
|
|
2e182626b9 | ||
|
|
50ecee99cc | ||
|
|
fc3f982309 | ||
|
|
b8e9aded6b | ||
|
|
55a464f1f6 | ||
|
|
72ec6beb99 | ||
|
|
895f3d9cb3 | ||
|
|
ae758da449 |
@@ -92,9 +92,11 @@
|
|||||||
- **硬件**:一套兼容 `xiaozhi-esp32`
|
- **硬件**:一套兼容 `xiaozhi-esp32`
|
||||||
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
|
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
|
||||||
|
|
||||||
- **电脑或服务器**:至少 4 核 CPU、8G 内存的电脑。
|
- **电脑或服务器**:建议 4 核 CPU、8G 内存的电脑。如果开启ASR也使用API,可运行在2核CPU、2G内存的服务器中。
|
||||||
- **固件编译**:请将本后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译`xiaozhi-esp32`固件并烧录到设备上。
|
- **固件编译**:请将本后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译`xiaozhi-esp32`固件并烧录到设备上。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
如果你没有esp32相关的硬件设备,但是非常想体验该项目,可以使用以下的项目让你的电脑、手机模拟成esp32设备。
|
如果你没有esp32相关的硬件设备,但是非常想体验该项目,可以使用以下的项目让你的电脑、手机模拟成esp32设备。
|
||||||
|
|
||||||
- [小智安卓端](https://github.com/TOM88812/xiaozhi-android-client)
|
- [小智安卓端](https://github.com/TOM88812/xiaozhi-android-client)
|
||||||
@@ -148,6 +150,8 @@ server:
|
|||||||
- 智控台webui
|
- 智控台webui
|
||||||
- iot功能
|
- iot功能
|
||||||
|
|
||||||
|
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
||||||
|
|
||||||

|

|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -210,10 +214,10 @@ server:
|
|||||||
|
|
||||||
### Memory 记忆存储
|
### Memory 记忆存储
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||||
|:------:|:---------------:|:----:|:--------:|:--:|
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
| Memory | mem0ai | 接口调用 | 100次/月额度 | |
|
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
||||||
| Memory | mem_local_short | 本地总结 | 免费 | |
|
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Executable
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# 本文件是用于一键自动下载本项目所需文件,自动创建好目录
|
||||||
|
# 所需条件(否则无法使用):
|
||||||
|
# 1、请确保你的环境可以正常访问 GitHub 否则无法下载脚本
|
||||||
|
#
|
||||||
|
# 检测操作系统类型
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*) OS=Linux;;
|
||||||
|
Darwin*) OS=Mac;;
|
||||||
|
CYGWIN*) OS=Windows;;
|
||||||
|
MINGW*) OS=Windows;;
|
||||||
|
MSYS*) OS=Windows;;
|
||||||
|
*) OS=UNKNOWN;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 设置颜色(Windows CMD 不支持,但不影响使用)
|
||||||
|
if [ "$OS" = "Windows" ]; then
|
||||||
|
GREEN=""
|
||||||
|
RED=""
|
||||||
|
NC=""
|
||||||
|
else
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${GREEN}开始安装小智服务端...${NC}"
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
echo "创建目录结构..."
|
||||||
|
mkdir -p xiaozhi-server/data xiaozhi-server/models/SenseVoiceSmall
|
||||||
|
cd xiaozhi-server || exit
|
||||||
|
|
||||||
|
# 根据操作系统选择下载命令
|
||||||
|
if [ "$OS" = "Windows" ]; then
|
||||||
|
DOWNLOAD_CMD="curl -L -o"
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="powershell -Command Invoke-WebRequest -Uri"
|
||||||
|
DOWNLOAD_CMD_SUFFIX="-OutFile"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="curl -L -o"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="wget -O"
|
||||||
|
else
|
||||||
|
echo "${RED}错误: 需要安装 curl 或 wget${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 下载语音识别模型
|
||||||
|
echo "下载语音识别模型..."
|
||||||
|
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
|
||||||
|
$DOWNLOAD_CMD "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" $DOWNLOAD_CMD_SUFFIX "models/SenseVoiceSmall/model.pt"
|
||||||
|
else
|
||||||
|
$DOWNLOAD_CMD "models/SenseVoiceSmall/model.pt" "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "${RED}模型下载失败。请手动从以下地址下载:${NC}"
|
||||||
|
echo "1. https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
|
||||||
|
echo "2. 百度网盘: https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg (提取码: qvna)"
|
||||||
|
echo "下载后请将文件放置在 models/SenseVoiceSmall/model.pt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 下载配置文件
|
||||||
|
echo "下载配置文件..."
|
||||||
|
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
|
||||||
|
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml" $DOWNLOAD_CMD_SUFFIX "docker-compose.yml"
|
||||||
|
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml" $DOWNLOAD_CMD_SUFFIX "data/.config.yaml"
|
||||||
|
else
|
||||||
|
$DOWNLOAD_CMD "docker-compose.yml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml"
|
||||||
|
$DOWNLOAD_CMD "data/.config.yaml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
echo "检查文件完整性..."
|
||||||
|
FILES_TO_CHECK="docker-compose.yml data/.config.yaml models/SenseVoiceSmall/model.pt"
|
||||||
|
ALL_FILES_EXIST=true
|
||||||
|
|
||||||
|
for FILE in $FILES_TO_CHECK; do
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "${RED}错误: $FILE 不存在${NC}"
|
||||||
|
ALL_FILES_EXIST=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ALL_FILES_EXIST" = false ]; then
|
||||||
|
echo "${RED}某些文件下载失败,请检查上述错误信息并手动下载缺失的文件。${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${GREEN}文件下载完成!${NC}"
|
||||||
|
echo "请编辑 data/.config.yaml 文件配置你的API密钥。"
|
||||||
|
echo "配置完成后,运行以下命令启动服务:"
|
||||||
|
echo "${GREEN}docker-compose up -d${NC}"
|
||||||
|
echo "查看日志请运行:"
|
||||||
|
echo "${GREEN}docker logs -f xiaozhi-esp32-server${NC}"
|
||||||
|
|
||||||
|
# 提示用户编辑配置文件
|
||||||
|
echo "\n${RED}重要提示:${NC}"
|
||||||
|
echo "1. 请确保编辑 data/.config.yaml 文件,配置必要的API密钥"
|
||||||
|
echo "2. 特别是 ChatGLM 和 mem0ai 的密钥必须配置"
|
||||||
|
echo "3. 配置完成后再启动 docker 服务"
|
||||||
+61
-16
@@ -1,3 +1,5 @@
|
|||||||
|
# 部署方案参考
|
||||||
|

|
||||||
# 方式一:docker快速部署
|
# 方式一:docker快速部署
|
||||||
|
|
||||||
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||||
@@ -6,7 +8,45 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
|||||||
|
|
||||||
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
||||||
|
|
||||||
## 2. 创建目录
|
如果你已经安装好docker,你可以[1.1使用懒人脚本](#11-懒人脚本)自动帮你下载所需的文件和配置文件,你可以使用docker[1.2手动部署](#12-手动部署)。
|
||||||
|
|
||||||
|
### 1.1 懒人脚本
|
||||||
|
|
||||||
|
你可以使用以下命令一键下载并执行部署脚本:
|
||||||
|
请确保你的环境可以正常访问 GitHub 否则无法下载脚本。
|
||||||
|
```bash
|
||||||
|
curl -L -o docker-setup.sh https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您的电脑是windows系统,请使用使用 Git Bash、WSL、PowerShell 或 CMD 运行以下命令:
|
||||||
|
```bash
|
||||||
|
# Git Bash 或 WSL
|
||||||
|
sh docker-setup.sh
|
||||||
|
# PowerShell 或 CMD
|
||||||
|
.\docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您的电脑是linux 或者 macos 系统,请使用终端运行以下命令:
|
||||||
|
```bash
|
||||||
|
chmod +x docker-setup.sh
|
||||||
|
./docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会自动完成以下操作:
|
||||||
|
> 1. 创建必要的目录结构
|
||||||
|
> 2. 下载语音识别模型
|
||||||
|
> 3. 下载配置文件
|
||||||
|
> 4. 检查文件完整性
|
||||||
|
>
|
||||||
|
> 执行完成后,请按照提示配置 API 密钥。
|
||||||
|
|
||||||
|
当你一切顺利完成以上操作后,继续操作[配置项目文件](#3-配置项目文件)
|
||||||
|
|
||||||
|
### 1.2 手动部署
|
||||||
|
|
||||||
|
如果懒人脚本无法正常运行,请按本章节1.2进行手动部署。
|
||||||
|
|
||||||
|
#### 1.2.1 创建目录
|
||||||
|
|
||||||
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
||||||
|
|
||||||
@@ -21,14 +61,18 @@ xiaozhi-server
|
|||||||
├─ SenseVoiceSmall
|
├─ SenseVoiceSmall
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. 下载语音识别模型文件
|
#### 1.2.2 下载语音识别模型文件
|
||||||
|
|
||||||
你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
|
你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
|
||||||
[跳转到下载语音识别模型文件](#模型文件)
|
[跳转到下载语音识别模型文件](#模型文件)
|
||||||
|
|
||||||
下载完后,回到本教程。
|
下载完后,回到本教程。
|
||||||
|
|
||||||
## 3. 下载docker-compose.yaml
|
#### 1.2.3 下载配置文件
|
||||||
|
|
||||||
|
你需要下载两个配置文件:`docker-compose.yaml` 和 `config.yaml`。需要从项目仓库下载这两个文件。
|
||||||
|
|
||||||
|
##### 1.2.3.1 下载 docker-compose.yaml
|
||||||
|
|
||||||
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
|
||||||
|
|
||||||
@@ -37,7 +81,7 @@ xiaozhi-server
|
|||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
## 3. 下载配置文件
|
##### 1.2.3.2 下载 config.yaml
|
||||||
|
|
||||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||||
|
|
||||||
@@ -58,14 +102,14 @@ xiaozhi-server
|
|||||||
|
|
||||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
## 4. 配置项目文件
|
## 3. 配置项目文件
|
||||||
|
|
||||||
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
||||||
[跳转到配置项目文件](#配置项目)
|
[跳转到配置项目文件](#配置项目)
|
||||||
|
|
||||||
配置完项目文件后,回到本教程继续往下。
|
配置完项目文件后,回到本教程继续往下。
|
||||||
|
|
||||||
## 5. 执行docker命令
|
## 4. 执行docker命令
|
||||||
|
|
||||||
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
@@ -81,14 +125,14 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
|
|
||||||
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
||||||
|
|
||||||
## 6.版本升级操作
|
## 5. 版本升级操作
|
||||||
|
|
||||||
如果后期想升级版本,可以这么操作
|
如果后期想升级版本,可以这么操作
|
||||||
|
|
||||||
1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
|
5.1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
|
||||||
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
|
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
|
||||||
|
|
||||||
2、执行以下命令
|
5.2、执行以下命令
|
||||||
|
|
||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
docker stop xiaozhi-esp32-server
|
||||||
@@ -96,7 +140,7 @@ docker rm xiaozhi-esp32-server
|
|||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
3、重新按docker方式部署
|
5.3、重新按docker方式部署
|
||||||
|
|
||||||
# 方式二:借助Docker环境运行部署
|
# 方式二:借助Docker环境运行部署
|
||||||
|
|
||||||
@@ -217,20 +261,23 @@ python app.py
|
|||||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
||||||
|
|
||||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的两个配置。
|
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
||||||
|
|
||||||
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||||
- 默认的记忆层`mem0ai`,你需要配置密钥,因为他们的API,虽然有免费额度,但是仍要去[官网](https://app.mem0.ai/dashboard/api-keys)注册密钥,才能启动。
|
|
||||||
|
|
||||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
||||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
||||||
|
|
||||||
```
|
```
|
||||||
selected_module:
|
selected_module:
|
||||||
ASR: FunASR
|
|
||||||
VAD: SileroVAD
|
VAD: SileroVAD
|
||||||
|
ASR: FunASR
|
||||||
LLM: ChatGLMLLM
|
LLM: ChatGLMLLM
|
||||||
TTS: EdgeTTS
|
TTS: EdgeTTS
|
||||||
|
# 默认不开启记忆,如需开启请看配置文件里的描述
|
||||||
|
Memory: nomem
|
||||||
|
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
||||||
|
Intent: nointent
|
||||||
```
|
```
|
||||||
|
|
||||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
||||||
@@ -249,8 +296,6 @@ LLM:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
有些服务,比如如果你使用`Dify`、`豆包的TTS`,是需要密钥的,记得在配置文件加上哦!
|
|
||||||
|
|
||||||
## 模型文件
|
## 模型文件
|
||||||
|
|
||||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
@@ -293,4 +338,4 @@ LLM:
|
|||||||
|
|
||||||
[5、我说话很慢,停顿时小智老是抢话](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
[5、我说话很慢,停顿时小智老是抢话](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||||
|
|
||||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||||
@@ -2,10 +2,27 @@
|
|||||||
|
|
||||||
"春江水暖鸭先知,正是河豚欲上时!"
|
"春江水暖鸭先知,正是河豚欲上时!"
|
||||||
|
|
||||||
亲爱的朋友,今天,我怀着无比真挚的心情,向热爱AI技术与创新的你发出这封公开信。
|
亲爱的朋友,我是John,是一名普通公司里的Java程序员,今天,我怀着无比真挚的心情,向热爱AI技术与创新的你发出这封公开信。
|
||||||
|
|
||||||
我们的开源项目 **xiaozhi-esp32-server** 正处在发展的阶段,它承载着我们对智能硬件和低成本民用贾维斯解决方案的美好憧憬,也希望借助每一位开发者的智慧,共同开创技术的新局面。
|
半年前我看到很多优秀的项目,比如`Dify`、`Chat2DB`等人工智能相关的项目,我在想,我要是能参与这些项目多好,可惜“报国无门,空打十年代码”。
|
||||||
|
|
||||||
|
我是2025年初刷到虾哥团队的视频,我非常好奇他是怎么实现的,我想复刻他们的后端服务,打造一个低成本民用贾维斯。很可惜现在做的作品依然只是一个人工智障,它并发低、没有灵魂,响应很慢,bug很多。
|
||||||
|
|
||||||
|
虾哥团队是我们学习的对象,我很想拥有像虾哥团队一样智能的小智后端服务。但是我也能理解虾哥不开源的决定。“一花独放不是春,百花齐放春满园”,人工智能遍地开花的时代,也许就在我们这代实现,我们可以用自己的双手,实现低成本民用贾维斯。我个人认为,他能实现的,我们也能实现,只是时间问题而已,我称之为“我们的取经之路”。
|
||||||
|
|
||||||
|
那么这条取经之路,我们会遇到什么困难?我想应该不少于八十一难。这一路必然会出现各种妖怪,当然也有神仙暗中帮助我们,也有人加入取经队伍。
|
||||||
|
|
||||||
|
以上内容,如果你觉得好笑。那我也觉得非常的幸运。我能够在你人生3万多天里博你笑五秒,也算是为你做了一次贡献。
|
||||||
|
|
||||||
|
民用低成本贾维斯这个想法,会失败吗,我不知道,但是我们普通人的一生,这种失败不是很常见吗?
|
||||||
|
|
||||||
|
未来,有一点是可以确定的,就一定会有人完全复刻虾哥团队的功能,实现民用低成本贾维斯。这个项目会是我们吗?
|
||||||
|
|
||||||
|
期待与你携手前行,共创未来。
|
||||||
|
|
||||||
|
John,2025.3.11,广州
|
||||||
|
|
||||||
|
# 附 开发贡献指南
|
||||||
## 项目目标
|
## 项目目标
|
||||||
|
|
||||||
1. **民用低成本贾维斯解决方案**
|
1. **民用低成本贾维斯解决方案**
|
||||||
@@ -14,11 +31,11 @@
|
|||||||
|
|
||||||
## 加入我们
|
## 加入我们
|
||||||
|
|
||||||
我们热忱欢迎志同道合的朋友加入,共同为项目贡献力量。参与方式如下:
|
我们热忱欢迎志同道合的朋友加入,共同为项目贡献力量。您可在[这个链接](https://github.com/users/xinnan-tech/projects/3)查看我们近期要实现的功能,功能列表中还没指派相关人员处理的,正是急需您的参与。参与方式如下:
|
||||||
|
|
||||||
### 1、成为普通贡献者
|
### 1、成为普通贡献者
|
||||||
|
|
||||||
Fork 项目,提交 PR,由开发者审核后合入主分支。
|
Fork 项目,,提交 PR,由开发者审核后合入主分支。
|
||||||
|
|
||||||
### 2、成为开发者
|
### 2、成为开发者
|
||||||
|
|
||||||
@@ -31,21 +48,3 @@ Fork 项目,提交 PR,由开发者审核后合入主分支。
|
|||||||
|
|
||||||
2. **提交 PR 审核**
|
2. **提交 PR 审核**
|
||||||
功能开发完成后,请在 GitHub 上提交 PR,由其他开发者审核,审核通过后合并入主分支。
|
功能开发完成后,请在 GitHub 上提交 PR,由其他开发者审核,审核通过后合并入主分支。
|
||||||
|
|
||||||
## 功能点的来源
|
|
||||||
|
|
||||||
- **创新的点子与功能**
|
|
||||||
你的每一个灵感都可能成为项目的突破点,欢迎大胆提出前沿的创意与功能建议。
|
|
||||||
|
|
||||||
- **代码优化**
|
|
||||||
如果你发现代码中存在改进的空间,欢迎提交优化方案,让我们的代码更加高效、易读。
|
|
||||||
|
|
||||||
- **解决 Issues 问题**
|
|
||||||
Issues 中有很多问题需要解决,我们一同完善、打造产品。
|
|
||||||
|
|
||||||
亲爱的开发者们,
|
|
||||||
每一次提交、每一份建议,都是对我们共同梦想的加持。每一代人有每一代人的使命,让我们共同书写智能生活的新篇章!
|
|
||||||
|
|
||||||
期待与你携手前行,共创未来。
|
|
||||||
|
|
||||||
John,2025.3.11,广州
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 576 KiB |
@@ -78,6 +78,8 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String TOKEN_HEADER = "token";
|
String TOKEN_HEADER = "token";
|
||||||
|
|
||||||
|
String AUTHORIZATION = "Authorization";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径分割符
|
* 路径分割符
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -42,4 +42,5 @@ public interface ErrorCode {
|
|||||||
int PASSWORD_LENGTH_ERROR = 10030;
|
int PASSWORD_LENGTH_ERROR = 10030;
|
||||||
int PASSWORD_WEAK_ERROR = 10031;
|
int PASSWORD_WEAK_ERROR = 10031;
|
||||||
int DEL_MYSELF_ERROR = 10032;
|
int DEL_MYSELF_ERROR = 10032;
|
||||||
|
int DEVICE_CAPTCHA_ERROR = 10033;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,60 +21,9 @@ public class RedisKeys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录用户Key
|
* 未注册设备验证码Key
|
||||||
*/
|
*/
|
||||||
public static String getSecurityUserKey(Long id) {
|
public static String getDeviceCaptchaKey(String captcha) {
|
||||||
return "sys:security:user:" + id;
|
return "sys:device:captcha:" + captcha;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统日志Key
|
|
||||||
*/
|
|
||||||
public static String getSysLogKey() {
|
|
||||||
return "sys:log";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统资源Key
|
|
||||||
*/
|
|
||||||
public static String getSysResourceKey() {
|
|
||||||
return "sys:resource";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户菜单导航Key
|
|
||||||
*/
|
|
||||||
public static String getUserMenuNavKey(Long userId) {
|
|
||||||
return "sys:user:nav:" + userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户权限标识Key
|
|
||||||
*/
|
|
||||||
public static String getUserPermissionsKey(Long userId) {
|
|
||||||
return "sys:user:permissions:" + userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户登陆错误次数
|
|
||||||
*
|
|
||||||
* @param username
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getUserLoginErrorCountKey(String username) {
|
|
||||||
return "sys:user:login:error:" + username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getUserInfoKey(Long userId) {
|
|
||||||
return "sys:user:" + userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getDataScopeListKey(Long userId) {
|
|
||||||
return "sys:user:data:scope:" + userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getSysUserName(Long id) {
|
|
||||||
return "sys:user:name" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.common.user.UserDetail;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/device")
|
||||||
|
@Tag(name = "设备管理")
|
||||||
|
|
||||||
|
public class DeviceController {
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("/bind/{deviceCode}")
|
||||||
|
@Operation(summary = "绑定设备")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<DeviceEntity> bindDevice(@PathVariable String deviceCode) {
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
|
||||||
|
String deviceHeaders = (String) redisUtils.get(RedisKeys.getDeviceCaptchaKey(deviceCode));
|
||||||
|
if (StringUtils.isBlank(deviceHeaders)) {
|
||||||
|
return new Result<DeviceEntity>().error(ErrorCode.DEVICE_CAPTCHA_ERROR);
|
||||||
|
}
|
||||||
|
DeviceHeaderDTO deviceHeader = JsonUtils.parseObject(deviceHeaders.getBytes(), DeviceHeaderDTO.class);
|
||||||
|
DeviceEntity device = deviceService.bindDevice(user.getId(), deviceHeader);
|
||||||
|
return new Result<DeviceEntity>().ok(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/bind")
|
||||||
|
@Operation(summary = "获取已绑定设备")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<DeviceEntity>> getUserDevices() {
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId());
|
||||||
|
return new Result<List<DeviceEntity>>().ok(devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/unbind")
|
||||||
|
@Operation(summary = "解绑设备")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result unbindDevice(@RequestBody DeviceUnBindDTO unDeviveBind) {
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
deviceService.unbindDevice(user.getId(), unDeviveBind.getDeviceId());
|
||||||
|
return new Result();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
@Operation(summary = "设备列表(管理员)")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||||
|
})
|
||||||
|
public Result<PageData<DeviceEntity>> adminDeviceList(
|
||||||
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
PageData<DeviceEntity> page = deviceService.adminDeviceList(params);
|
||||||
|
return new Result<PageData<DeviceEntity>>().ok(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package xiaozhi.modules.device.dao;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "设备连接头信息")
|
||||||
|
public class DeviceHeaderDTO {
|
||||||
|
|
||||||
|
@Schema(description = "设备ID")
|
||||||
|
private String deviceId;
|
||||||
|
|
||||||
|
@Schema(description = "协议版本号")
|
||||||
|
private Long protocolVersion;
|
||||||
|
|
||||||
|
@Schema(description = "认证信息")
|
||||||
|
private String authorization;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备解绑表单
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "设备解绑表单")
|
||||||
|
public class DeviceUnBindDTO implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "设备ID")
|
||||||
|
@NotBlank(message = "设备ID不能为空")
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package xiaozhi.modules.device.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("ai_device")
|
||||||
|
@Schema(description = "设备信息")
|
||||||
|
public class DeviceEntity {
|
||||||
|
@Schema(description = "设备ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "关联用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "MAC地址")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
@Schema(description = "最后连接时间")
|
||||||
|
private Date lastConnectedAt;
|
||||||
|
|
||||||
|
@Schema(description = "自动更新开关(0关闭/1开启)")
|
||||||
|
private Integer autoUpdate;
|
||||||
|
|
||||||
|
@Schema(description = "设备硬件型号")
|
||||||
|
private String board;
|
||||||
|
|
||||||
|
@Schema(description = "设备别名")
|
||||||
|
private String alias;
|
||||||
|
|
||||||
|
@Schema(description = "智能体ID")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
@Schema(description = "固件版本号")
|
||||||
|
private String appVersion;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createDate;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package xiaozhi.modules.device.service;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface DeviceService {
|
||||||
|
DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader);
|
||||||
|
|
||||||
|
List<DeviceEntity> getUserDevices(Long userId);
|
||||||
|
|
||||||
|
void unbindDevice(Long userId, Long deviceId);
|
||||||
|
|
||||||
|
PageData<DeviceEntity> adminDeviceList(Map<String, Object> params);
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.modules.device.dao.DeviceDao;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceHeaderDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||||
|
private final DeviceDao deviceDao;
|
||||||
|
|
||||||
|
// 添加构造函数来初始化 deviceMapper
|
||||||
|
public DeviceServiceImpl(DeviceDao deviceDao) {
|
||||||
|
this.deviceDao = deviceDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceEntity bindDevice(Long userId, DeviceHeaderDTO deviceHeader) {
|
||||||
|
DeviceEntity device = new DeviceEntity();
|
||||||
|
device.setUserId(userId);
|
||||||
|
device.setMacAddress(deviceHeader.getDeviceId());
|
||||||
|
device.setCreateDate(new Date());
|
||||||
|
deviceDao.insert(device);
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DeviceEntity> getUserDevices(Long userId) {
|
||||||
|
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
return deviceDao.selectList(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unbindDevice(Long userId, Long deviceId) {
|
||||||
|
deviceDao.deleteById(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<DeviceEntity> adminDeviceList(Map<String, Object> params) {
|
||||||
|
IPage<DeviceEntity> page = deviceDao.selectPage(
|
||||||
|
getPage(params, "sort", true),
|
||||||
|
new QueryWrapper<>()
|
||||||
|
);
|
||||||
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -58,17 +58,18 @@ public class ShiroConfig {
|
|||||||
filters.put("oauth2", new Oauth2Filter());
|
filters.put("oauth2", new Oauth2Filter());
|
||||||
shiroFilter.setFilters(filters);
|
shiroFilter.setFilters(filters);
|
||||||
|
|
||||||
|
//添加Shiro的内置过滤器
|
||||||
|
/*anon:无需认证就可以访问
|
||||||
|
authc:必须认证了才能让问
|
||||||
|
user:必须拥有,记住我功能,才能访问
|
||||||
|
perms:拥有对某个资源的权限才能访问
|
||||||
|
role:拥有某个角色权限才能访问*/
|
||||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||||
filterMap.put("/webjars/**", "anon");
|
filterMap.put("/webjars/**", "anon");
|
||||||
filterMap.put("/druid/**", "anon");
|
filterMap.put("/druid/**", "anon");
|
||||||
filterMap.put("/login", "anon");
|
|
||||||
filterMap.put("/publicKey", "anon");
|
|
||||||
filterMap.put("/v3/api-docs/**", "anon");
|
filterMap.put("/v3/api-docs/**", "anon");
|
||||||
filterMap.put("/doc.html", "anon");
|
filterMap.put("/doc.html", "anon");
|
||||||
filterMap.put("/sys/oss/download/**", "anon");
|
|
||||||
filterMap.put("/captcha", "anon");
|
|
||||||
filterMap.put("/favicon.ico", "anon");
|
filterMap.put("/favicon.ico", "anon");
|
||||||
filterMap.put("/mobile/**", "anon");
|
|
||||||
filterMap.put("/user/**", "anon");
|
filterMap.put("/user/**", "anon");
|
||||||
filterMap.put("/**", "oauth2");
|
filterMap.put("/**", "oauth2");
|
||||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||||
|
|||||||
+24
-1
@@ -4,11 +4,16 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.page.TokenDTO;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.AssertUtils;
|
import xiaozhi.common.validator.AssertUtils;
|
||||||
|
import xiaozhi.modules.security.dao.SysUserTokenDao;
|
||||||
import xiaozhi.modules.security.dto.LoginDTO;
|
import xiaozhi.modules.security.dto.LoginDTO;
|
||||||
import xiaozhi.modules.security.password.PasswordUtils;
|
import xiaozhi.modules.security.password.PasswordUtils;
|
||||||
import xiaozhi.modules.security.service.CaptchaService;
|
import xiaozhi.modules.security.service.CaptchaService;
|
||||||
@@ -31,6 +36,7 @@ public class LoginController {
|
|||||||
private final SysUserTokenService sysUserTokenService;
|
private final SysUserTokenService sysUserTokenService;
|
||||||
private final CaptchaService captchaService;
|
private final CaptchaService captchaService;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
|
||||||
|
|
||||||
@GetMapping("/captcha")
|
@GetMapping("/captcha")
|
||||||
@Operation(summary = "验证码")
|
@Operation(summary = "验证码")
|
||||||
@@ -44,7 +50,7 @@ public class LoginController {
|
|||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
@Operation(summary = "登录")
|
@Operation(summary = "登录")
|
||||||
public Result login( @RequestBody LoginDTO login) {
|
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
|
||||||
// 验证是否正确输入验证码
|
// 验证是否正确输入验证码
|
||||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
@@ -84,4 +90,21 @@ public class LoginController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/info")
|
||||||
|
@Operation(summary = "用户信息获取")
|
||||||
|
public Result<SysUserDTO> info(@RequestHeader("Authorization")String authorization) {
|
||||||
|
logger.info("the authorization:{}", authorization);
|
||||||
|
|
||||||
|
String token;
|
||||||
|
if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) {
|
||||||
|
throw new RenException(ErrorCode.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
token = authorization.replace("Bearer ", "");
|
||||||
|
if (StringUtils.isBlank(token)) {
|
||||||
|
throw new RenException(ErrorCode.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
SysUserDTO sysUserDTO = sysUserTokenService.getUserByToken(token);
|
||||||
|
Result result = new Result<SysUserDTO>();
|
||||||
|
return result.ok(sysUserDTO);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
|
|||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.utils.HttpContextUtils;
|
import xiaozhi.common.utils.HttpContextUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
@@ -89,12 +90,22 @@ public class Oauth2Filter extends AuthenticatingFilter {
|
|||||||
* 获取请求的token
|
* 获取请求的token
|
||||||
*/
|
*/
|
||||||
private String getRequestToken(HttpServletRequest httpRequest) {
|
private String getRequestToken(HttpServletRequest httpRequest) {
|
||||||
|
String token;
|
||||||
//从header中获取token
|
//从header中获取token
|
||||||
String token = httpRequest.getHeader(Constant.TOKEN_HEADER);
|
String authorization = httpRequest.getHeader(Constant.AUTHORIZATION);
|
||||||
|
if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) {
|
||||||
|
throw new RenException(ErrorCode.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
token = authorization.replace("Bearer ", "");
|
||||||
|
|
||||||
//如果header中不存在token,则从参数中获取token
|
//如果header中不存在token,则从参数中获取token
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
token = httpRequest.getParameter(Constant.TOKEN_HEADER);
|
authorization = httpRequest.getParameter(Constant.AUTHORIZATION);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) {
|
||||||
|
throw new RenException(ErrorCode.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
token = authorization.replace("Bearer ", "");
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
package xiaozhi.modules.security.oauth2;
|
package xiaozhi.modules.security.oauth2;
|
||||||
|
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
|
||||||
import xiaozhi.common.user.UserDetail;
|
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
|
||||||
import xiaozhi.common.utils.MessageUtils;
|
|
||||||
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
|
||||||
import xiaozhi.modules.security.service.ShiroService;
|
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.apache.shiro.authc.*;
|
import org.apache.shiro.authc.*;
|
||||||
import org.apache.shiro.authz.AuthorizationInfo;
|
import org.apache.shiro.authz.AuthorizationInfo;
|
||||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||||
import org.apache.shiro.realm.AuthorizingRealm;
|
import org.apache.shiro.realm.AuthorizingRealm;
|
||||||
import org.apache.shiro.subject.PrincipalCollection;
|
import org.apache.shiro.subject.PrincipalCollection;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.user.UserDetail;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.common.utils.MessageUtils;
|
||||||
|
import xiaozhi.modules.security.controller.LoginController;
|
||||||
|
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
||||||
|
import xiaozhi.modules.security.service.ShiroService;
|
||||||
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +33,8 @@ public class Oauth2Realm extends AuthorizingRealm {
|
|||||||
@Resource
|
@Resource
|
||||||
private ShiroService shiroService;
|
private ShiroService shiroService;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Oauth2Realm.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(AuthenticationToken token) {
|
public boolean supports(AuthenticationToken token) {
|
||||||
return token instanceof Oauth2Token;
|
return token instanceof Oauth2Token;
|
||||||
@@ -45,6 +50,13 @@ public class Oauth2Realm extends AuthorizingRealm {
|
|||||||
//用户权限列表
|
//用户权限列表
|
||||||
Set<String> permsSet = shiroService.getUserPermissions(user);
|
Set<String> permsSet = shiroService.getUserPermissions(user);
|
||||||
|
|
||||||
|
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||||
|
permsSet.add("sys:role:superAdmin");
|
||||||
|
permsSet.add("sys:role:normal");
|
||||||
|
} else {
|
||||||
|
permsSet.add("sys:role:normal");
|
||||||
|
}
|
||||||
|
|
||||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
|
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
|
||||||
info.setStringPermissions(permsSet);
|
info.setStringPermissions(permsSet);
|
||||||
return info;
|
return info;
|
||||||
@@ -75,6 +87,11 @@ public class Oauth2Realm extends AuthorizingRealm {
|
|||||||
userDetail.setToken(accessToken);
|
userDetail.setToken(accessToken);
|
||||||
|
|
||||||
//账号锁定
|
//账号锁定
|
||||||
|
if (userDetail.getStatus() == null) {
|
||||||
|
logger.error("账号状态异常,status 不能为空");
|
||||||
|
throw new DisabledAccountException(MessageUtils.getMessage(ErrorCode.ACCOUNT_DISABLE));
|
||||||
|
}
|
||||||
|
|
||||||
if (userDetail.getStatus() == 0) {
|
if (userDetail.getStatus() == 0) {
|
||||||
throw new LockedAccountException(MessageUtils.getMessage(ErrorCode.ACCOUNT_LOCK));
|
throw new LockedAccountException(MessageUtils.getMessage(ErrorCode.ACCOUNT_LOCK));
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -1,9 +1,11 @@
|
|||||||
package xiaozhi.modules.security.service;
|
package xiaozhi.modules.security.service;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.page.TokenDTO;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
||||||
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -19,7 +21,9 @@ public interface SysUserTokenService extends BaseService<SysUserTokenEntity> {
|
|||||||
*
|
*
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
*/
|
*/
|
||||||
Result createToken(Long userId);
|
Result<TokenDTO> createToken(Long userId);
|
||||||
|
|
||||||
|
SysUserDTO getUserByToken(String token);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出
|
* 退出
|
||||||
|
|||||||
+23
-1
@@ -1,6 +1,9 @@
|
|||||||
package xiaozhi.modules.security.service.impl;
|
package xiaozhi.modules.security.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.TokenDTO;
|
import xiaozhi.common.page.TokenDTO;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.HttpContextUtils;
|
import xiaozhi.common.utils.HttpContextUtils;
|
||||||
@@ -10,18 +13,23 @@ import xiaozhi.modules.security.entity.SysUserTokenEntity;
|
|||||||
import xiaozhi.modules.security.oauth2.TokenGenerator;
|
import xiaozhi.modules.security.oauth2.TokenGenerator;
|
||||||
import xiaozhi.modules.security.service.SysUserTokenService;
|
import xiaozhi.modules.security.service.SysUserTokenService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
@Service
|
@Service
|
||||||
public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, SysUserTokenEntity> implements SysUserTokenService {
|
public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, SysUserTokenEntity> implements SysUserTokenService {
|
||||||
|
|
||||||
|
private final SysUserService sysUserService;
|
||||||
/**
|
/**
|
||||||
* 12小时后过期
|
* 12小时后过期
|
||||||
*/
|
*/
|
||||||
private final static int EXPIRE = 3600 * 12;
|
private final static int EXPIRE = 3600 * 12;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result createToken(Long userId) {
|
public Result<TokenDTO> createToken(Long userId) {
|
||||||
//用户token
|
//用户token
|
||||||
String token;
|
String token;
|
||||||
|
|
||||||
@@ -70,6 +78,20 @@ public class SysUserTokenServiceImpl extends BaseServiceImpl<SysUserTokenDao, Sy
|
|||||||
return new Result().ok(tokenDTO);
|
return new Result().ok(tokenDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SysUserDTO getUserByToken(String token) {
|
||||||
|
SysUserTokenEntity userToken = baseDao.getByToken(token);
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
if (userToken.getExpireDate().before(now)) {
|
||||||
|
throw new RenException(ErrorCode.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUserDTO userDTO = sysUserService.getByUserId(userToken.getUserId());
|
||||||
|
userDTO.setPassword("");
|
||||||
|
return userDTO;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void logout(Long userId) {
|
public void logout(Long userId) {
|
||||||
Date expireDate = DateUtil.offsetMinute(new Date(), -1);
|
Date expireDate = DateUtil.offsetMinute(new Date(), -1);
|
||||||
|
|||||||
+2
-140
@@ -1,34 +1,9 @@
|
|||||||
package xiaozhi.modules.sys.controller;
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
import xiaozhi.common.annotation.LogOperation;
|
|
||||||
import xiaozhi.common.constant.Constant;
|
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
|
||||||
import xiaozhi.common.exception.RenException;
|
|
||||||
import xiaozhi.common.page.PageData;
|
|
||||||
import xiaozhi.common.user.UserDetail;
|
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
|
||||||
import xiaozhi.common.utils.Result;
|
|
||||||
import xiaozhi.common.validator.AssertUtils;
|
|
||||||
import xiaozhi.common.validator.ValidatorUtils;
|
|
||||||
import xiaozhi.common.validator.group.AddGroup;
|
|
||||||
import xiaozhi.common.validator.group.DefaultGroup;
|
|
||||||
import xiaozhi.common.validator.group.UpdateGroup;
|
|
||||||
import xiaozhi.modules.security.password.PasswordUtils;
|
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
|
||||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
|
||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
|
||||||
import xiaozhi.modules.sys.service.SysUserService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameters;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户管理
|
* 用户管理
|
||||||
@@ -38,118 +13,5 @@ import java.util.Map;
|
|||||||
@RequestMapping("/sys/user")
|
@RequestMapping("/sys/user")
|
||||||
@Tag(name = "用户管理")
|
@Tag(name = "用户管理")
|
||||||
public class SysUserController {
|
public class SysUserController {
|
||||||
private final SysUserService sysUserService;
|
|
||||||
|
|
||||||
@GetMapping("page")
|
|
||||||
@Operation(summary = "分页")
|
|
||||||
@Parameters({
|
|
||||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
|
||||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
|
||||||
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段"),
|
|
||||||
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)"),
|
|
||||||
@Parameter(name = "username", description = "用户名"),
|
|
||||||
@Parameter(name = "gender", description = "性别"),
|
|
||||||
@Parameter(name = "deptId", description = "部门ID")
|
|
||||||
})
|
|
||||||
@RequiresPermissions("sys:user:page")
|
|
||||||
public Result<PageData<SysUserDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
|
||||||
PageData<SysUserDTO> page = sysUserService.page(params);
|
|
||||||
|
|
||||||
return new Result<PageData<SysUserDTO>>().ok(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("{id}")
|
|
||||||
@Operation(summary = "信息")
|
|
||||||
@RequiresPermissions("sys:user:info")
|
|
||||||
public Result<SysUserDTO> get(@PathVariable("id") Long id) {
|
|
||||||
SysUserDTO data = sysUserService.get(id);
|
|
||||||
return new Result<SysUserDTO>().ok(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("info")
|
|
||||||
@Operation(summary = "登录用户信息")
|
|
||||||
public Result<SysUserDTO> info() {
|
|
||||||
SysUserDTO data = ConvertUtils.sourceToTarget(SecurityUser.getUser(), SysUserDTO.class);
|
|
||||||
return new Result<SysUserDTO>().ok(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("password")
|
|
||||||
@Operation(summary = "修改密码")
|
|
||||||
@LogOperation("修改密码")
|
|
||||||
public Result password(@RequestBody PasswordDTO dto) {
|
|
||||||
//效验数据
|
|
||||||
ValidatorUtils.validateEntity(dto);
|
|
||||||
String newPassword = dto.getNewPassword();
|
|
||||||
|
|
||||||
//密码的强度
|
|
||||||
if (newPassword == null || newPassword.length() < 8) {
|
|
||||||
return new Result().error(ErrorCode.PASSWORD_LENGTH_ERROR);
|
|
||||||
}
|
|
||||||
if (!sysUserService.isStrongPassword(newPassword)) {
|
|
||||||
return new Result().error(ErrorCode.PASSWORD_WEAK_ERROR);
|
|
||||||
}
|
|
||||||
UserDetail user = SecurityUser.getUser();
|
|
||||||
//原密码不正确
|
|
||||||
if (!PasswordUtils.matches(dto.getPassword(), user.getPassword())) {
|
|
||||||
return new Result().error(ErrorCode.PASSWORD_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
sysUserService.updatePassword(user.getId(), dto.getNewPassword());
|
|
||||||
|
|
||||||
return new Result();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@Operation(summary = "保存")
|
|
||||||
@LogOperation("保存")
|
|
||||||
@RequiresPermissions("sys:user:save")
|
|
||||||
public Result save(@RequestBody SysUserDTO dto) {
|
|
||||||
//效验数据
|
|
||||||
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
|
||||||
|
|
||||||
sysUserService.save(dto);
|
|
||||||
|
|
||||||
return new Result();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping
|
|
||||||
@Operation(summary = "修改")
|
|
||||||
@LogOperation("修改")
|
|
||||||
@RequiresPermissions("sys:user:update")
|
|
||||||
public Result update(@RequestBody SysUserDTO dto) {
|
|
||||||
//效验数据
|
|
||||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
|
||||||
|
|
||||||
sysUserService.update(dto);
|
|
||||||
|
|
||||||
return new Result();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("app")
|
|
||||||
@Operation(summary = "修改")
|
|
||||||
@LogOperation("修改")
|
|
||||||
@RequiresPermissions("sys:user:update")
|
|
||||||
public Result updateUserInfo(@RequestBody SysUserDTO dto) {
|
|
||||||
sysUserService.updateUserInfo(dto);
|
|
||||||
|
|
||||||
return new Result();
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping
|
|
||||||
@Operation(summary = "删除")
|
|
||||||
@LogOperation("删除")
|
|
||||||
@RequiresPermissions("sys:user:delete")
|
|
||||||
public Result delete(@RequestBody Long[] ids) {
|
|
||||||
//效验数据
|
|
||||||
AssertUtils.isArrayEmpty(ids, "id");
|
|
||||||
|
|
||||||
List<Long> idList = Arrays.asList(ids);
|
|
||||||
if (idList.contains(SecurityUser.getUserId())) {
|
|
||||||
throw new RenException(ErrorCode.DEL_MYSELF_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
sysUserService.deleteBatchIds(idList);
|
|
||||||
|
|
||||||
return new Result();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
package xiaozhi.modules.sys.dao;
|
package xiaozhi.modules.sys.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统用户
|
* 系统用户
|
||||||
@@ -14,21 +10,4 @@ import java.util.Map;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface SysUserDao extends BaseDao<SysUserEntity> {
|
public interface SysUserDao extends BaseDao<SysUserEntity> {
|
||||||
|
|
||||||
List<SysUserEntity> getList(Map<String, Object> params);
|
|
||||||
|
|
||||||
SysUserEntity getById(Long id);
|
|
||||||
|
|
||||||
SysUserEntity getByUsername(String username);
|
|
||||||
|
|
||||||
int updatePassword(@Param("id") Long id, @Param("newPassword") String newPassword);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据部门ID,查询用户数
|
|
||||||
*/
|
|
||||||
int getCountByDeptId(Long deptId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据部门ID,查询用户ID列表
|
|
||||||
*/
|
|
||||||
List<Long> getUserIdListByDeptId(List<Long> deptIdList);
|
|
||||||
}
|
}
|
||||||
@@ -24,30 +24,6 @@ public class SysUserEntity extends BaseEntity {
|
|||||||
* 密码
|
* 密码
|
||||||
*/
|
*/
|
||||||
private String password;
|
private String password;
|
||||||
/**
|
|
||||||
* 姓名
|
|
||||||
*/
|
|
||||||
private String realName;
|
|
||||||
/**
|
|
||||||
* 头像
|
|
||||||
*/
|
|
||||||
private String headUrl;
|
|
||||||
/**
|
|
||||||
* 性别 0:男 1:女 2:保密
|
|
||||||
*/
|
|
||||||
private Integer gender;
|
|
||||||
/**
|
|
||||||
* 邮箱
|
|
||||||
*/
|
|
||||||
private String email;
|
|
||||||
/**
|
|
||||||
* 手机号
|
|
||||||
*/
|
|
||||||
private String mobile;
|
|
||||||
/**
|
|
||||||
* 部门ID
|
|
||||||
*/
|
|
||||||
private Long deptId;
|
|
||||||
/**
|
/**
|
||||||
* 超级管理员 0:否 1:是
|
* 超级管理员 0:否 1:是
|
||||||
*/
|
*/
|
||||||
@@ -56,6 +32,11 @@ public class SysUserEntity extends BaseEntity {
|
|||||||
* 状态 0:停用 1:正常
|
* 状态 0:停用 1:正常
|
||||||
*/
|
*/
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
/**
|
||||||
|
* 更新者
|
||||||
|
*/
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Date createDate;
|
||||||
/**
|
/**
|
||||||
* 更新者
|
* 更新者
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,49 +1,21 @@
|
|||||||
package xiaozhi.modules.sys.service;
|
package xiaozhi.modules.sys.service;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统用户
|
* 系统用户
|
||||||
*/
|
*/
|
||||||
public interface SysUserService extends BaseService<SysUserEntity> {
|
public interface SysUserService extends BaseService<SysUserEntity> {
|
||||||
|
|
||||||
PageData<SysUserDTO> page(Map<String, Object> params);
|
|
||||||
|
|
||||||
List<SysUserDTO> list(Map<String, Object> params);
|
|
||||||
|
|
||||||
SysUserDTO get(Long id);
|
|
||||||
|
|
||||||
SysUserDTO getByUsername(String username);
|
SysUserDTO getByUsername(String username);
|
||||||
|
|
||||||
|
SysUserDTO getByUserId(Long userId);
|
||||||
|
|
||||||
void save(SysUserDTO dto);
|
void save(SysUserDTO dto);
|
||||||
|
|
||||||
void update(SysUserDTO dto);
|
|
||||||
|
|
||||||
void updateUserInfo(SysUserDTO dto);
|
|
||||||
|
|
||||||
void delete(Long[] ids);
|
void delete(Long[] ids);
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改密码
|
|
||||||
*
|
|
||||||
* @param id 用户ID
|
|
||||||
* @param newPassword 新密码
|
|
||||||
*/
|
|
||||||
void updatePassword(Long id, String newPassword);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证密码强度
|
|
||||||
*
|
|
||||||
* @param newPassword
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
boolean isStrongPassword(String newPassword);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-80
@@ -1,15 +1,11 @@
|
|||||||
package xiaozhi.modules.sys.service.impl;
|
package xiaozhi.modules.sys.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.security.password.PasswordUtils;
|
import xiaozhi.modules.security.password.PasswordUtils;
|
||||||
@@ -21,7 +17,6 @@ import xiaozhi.modules.sys.service.SysUserService;
|
|||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -32,43 +27,27 @@ import java.util.regex.Pattern;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Service
|
@Service
|
||||||
public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService {
|
public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService {
|
||||||
private final RedisUtils redisUtils;
|
private final SysUserDao sysUserDao;
|
||||||
|
|
||||||
@Override
|
|
||||||
public PageData<SysUserDTO> page(Map<String, Object> params) {
|
|
||||||
//转换成like
|
|
||||||
paramsToLike(params, "username");
|
|
||||||
|
|
||||||
//分页
|
|
||||||
IPage<SysUserEntity> page = getPage(params, "t1.create_date", false);
|
|
||||||
|
|
||||||
//查询
|
|
||||||
List<SysUserEntity> list = baseDao.getList(params);
|
|
||||||
|
|
||||||
return getPageData(list, page.getTotal(), SysUserDTO.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<SysUserDTO> list(Map<String, Object> params) {
|
|
||||||
|
|
||||||
List<SysUserEntity> entityList = baseDao.getList(params);
|
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entityList, SysUserDTO.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SysUserDTO get(Long id) {
|
|
||||||
SysUserEntity entity = baseDao.getById(id);
|
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entity, SysUserDTO.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysUserDTO getByUsername(String username) {
|
public SysUserDTO getByUsername(String username) {
|
||||||
SysUserEntity entity = baseDao.getByUsername(username);
|
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("username", username);
|
||||||
|
List<SysUserEntity> users = sysUserDao.selectList(queryWrapper);
|
||||||
|
if (users == null || users.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
SysUserEntity entity = users.getFirst();
|
||||||
return ConvertUtils.sourceToTarget(entity, SysUserDTO.class);
|
return ConvertUtils.sourceToTarget(entity, SysUserDTO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SysUserDTO getByUserId(Long userId) {
|
||||||
|
SysUserEntity sysUserEntity = sysUserDao.selectById(userId);
|
||||||
|
|
||||||
|
return ConvertUtils.sourceToTarget(sysUserEntity, SysUserDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void save(SysUserDTO dto) {
|
public void save(SysUserDTO dto) {
|
||||||
@@ -90,44 +69,11 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
} else {
|
} else {
|
||||||
entity.setSuperAdmin(SuperAdminEnum.NO.value());
|
entity.setSuperAdmin(SuperAdminEnum.NO.value());
|
||||||
}
|
}
|
||||||
|
entity.setStatus(1);
|
||||||
|
|
||||||
insert(entity);
|
insert(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void update(SysUserDTO dto) {
|
|
||||||
SysUserEntity entity = ConvertUtils.sourceToTarget(dto, SysUserEntity.class);
|
|
||||||
|
|
||||||
//密码加密
|
|
||||||
if (StringUtils.isBlank(dto.getPassword())) {
|
|
||||||
entity.setPassword(null);
|
|
||||||
} else {
|
|
||||||
//密码强度
|
|
||||||
if (!isStrongPassword(entity.getPassword())) {
|
|
||||||
throw new RenException(ErrorCode.PASSWORD_WEAK_ERROR);
|
|
||||||
}
|
|
||||||
String password = PasswordUtils.encode(entity.getPassword());
|
|
||||||
entity.setPassword(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
//更新用户
|
|
||||||
updateById(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void updateUserInfo(SysUserDTO dto) {
|
|
||||||
SysUserEntity entity = selectById(dto.getId());
|
|
||||||
entity.setHeadUrl(dto.getHeadUrl());
|
|
||||||
entity.setRealName(dto.getRealName());
|
|
||||||
entity.setGender(dto.getGender());
|
|
||||||
entity.setMobile(dto.getMobile());
|
|
||||||
entity.setEmail(dto.getEmail());
|
|
||||||
|
|
||||||
updateById(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void delete(Long[] ids) {
|
||||||
@@ -135,22 +81,13 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public void updatePassword(Long id, String newPassword) {
|
|
||||||
newPassword = PasswordUtils.encode(newPassword);
|
|
||||||
|
|
||||||
baseDao.updatePassword(id, newPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getUserCount() {
|
private Long getUserCount() {
|
||||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||||
return baseDao.selectCount(queryWrapper);
|
return baseDao.selectCount(queryWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
private boolean isStrongPassword(String password) {
|
||||||
public boolean isStrongPassword(String password) {
|
|
||||||
// 弱密码的正则表达式
|
// 弱密码的正则表达式
|
||||||
String weakPasswordRegex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$";
|
String weakPasswordRegex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$";
|
||||||
Pattern pattern = Pattern.compile(weakPasswordRegex);
|
Pattern pattern = Pattern.compile(weakPasswordRegex);
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
-- 给用户表添加一个创建者
|
|
||||||
ALTER TABLE sys_user ADD COLUMN creator BIGINT COMMENT '创建者';
|
|
||||||
+3
-4
@@ -13,10 +13,10 @@ CREATE TABLE sys_user (
|
|||||||
status tinyint COMMENT '状态 0:停用 1:正常',
|
status tinyint COMMENT '状态 0:停用 1:正常',
|
||||||
create_date datetime COMMENT '创建时间',
|
create_date datetime COMMENT '创建时间',
|
||||||
updater bigint COMMENT '更新者',
|
updater bigint COMMENT '更新者',
|
||||||
|
creator bigint COMMENT '创建者',
|
||||||
update_date datetime COMMENT '更新时间',
|
update_date datetime COMMENT '更新时间',
|
||||||
primary key (id),
|
primary key (id),
|
||||||
unique key uk_username (username),
|
unique key uk_username (username)
|
||||||
key idx_create_date (create_date)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户';
|
||||||
|
|
||||||
-- 系统用户Token
|
-- 系统用户Token
|
||||||
@@ -45,8 +45,7 @@ create table sys_params
|
|||||||
updater bigint COMMENT '更新者',
|
updater bigint COMMENT '更新者',
|
||||||
update_date datetime COMMENT '更新时间',
|
update_date datetime COMMENT '更新时间',
|
||||||
primary key (id),
|
primary key (id),
|
||||||
unique key uk_param_code (param_code),
|
unique key uk_param_code (param_code)
|
||||||
key idx_create_date (create_date)
|
|
||||||
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='参数管理';
|
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='参数管理';
|
||||||
|
|
||||||
-- 字典类型
|
-- 字典类型
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
-- 模型供应器表
|
||||||
|
DROP TABLE IF EXISTS `ai_model_provider`;
|
||||||
|
CREATE TABLE `ai_model_provider` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '主键',
|
||||||
|
`model_type` VARCHAR(20) COMMENT '模型类型(Memory/ASR/VAD/LLM/TTS)',
|
||||||
|
`provider_code` VARCHAR(50) COMMENT '供应器类型',
|
||||||
|
`name` VARCHAR(50) COMMENT '供应器名称',
|
||||||
|
`fields` JSON COMMENT '供应器字段列表(JSON格式)',
|
||||||
|
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
|
||||||
|
`creator` BIGINT COMMENT '创建者',
|
||||||
|
`create_date` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者',
|
||||||
|
`update_date` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_ai_model_provider_model_type` (`model_type`) COMMENT '创建模型类型的索引,用于快速查找特定类型下的所有供应器信息'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模型配置表';
|
||||||
|
|
||||||
|
-- 模型配置表
|
||||||
|
DROP TABLE IF EXISTS `ai_model_config`;
|
||||||
|
CREATE TABLE `ai_model_config` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '主键',
|
||||||
|
`model_type` VARCHAR(20) COMMENT '模型类型(Memory/ASR/VAD/LLM/TTS)',
|
||||||
|
`model_code` VARCHAR(50) COMMENT '模型编码(如AliLLM、DoubaoTTS)',
|
||||||
|
`model_name` VARCHAR(50) COMMENT '模型名称',
|
||||||
|
`is_default` TINYINT(1) DEFAULT 0 COMMENT '是否默认配置(0否 1是)',
|
||||||
|
`is_enabled` TINYINT(1) DEFAULT 0 COMMENT '是否启用',
|
||||||
|
`config_json` JSON COMMENT '模型配置(JSON格式)',
|
||||||
|
`doc_link` VARCHAR(200) COMMENT '官方文档链接',
|
||||||
|
`remark` VARCHAR(255) COMMENT '备注',
|
||||||
|
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
|
||||||
|
`creator` BIGINT COMMENT '创建者',
|
||||||
|
`create_date` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者',
|
||||||
|
`update_date` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_ai_model_config_model_type` (`model_type`) COMMENT '创建模型类型的索引,用于快速查找特定类型下的所有配置信息'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模型配置表';
|
||||||
|
|
||||||
|
-- TTS 音色表
|
||||||
|
DROP TABLE IF EXISTS `ai_tts_voice`;
|
||||||
|
CREATE TABLE `ai_tts_voice` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '主键',
|
||||||
|
`tts_model_id` VARCHAR(32) COMMENT '对应 TTS 模型主键',
|
||||||
|
`name` VARCHAR(20) COMMENT '音色名称',
|
||||||
|
`tts_voice` VARCHAR(50) COMMENT '音色编码',
|
||||||
|
`languages` VARCHAR(50) COMMENT '语言',
|
||||||
|
`voice_demo` VARCHAR(500) DEFAULT NULL COMMENT '音色 Demo',
|
||||||
|
`remark` VARCHAR(255) COMMENT '备注',
|
||||||
|
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
|
||||||
|
`creator` BIGINT COMMENT '创建者',
|
||||||
|
`create_date` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者',
|
||||||
|
`update_date` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_ai_tts_voice_tts_model_id` (`tts_model_id`) COMMENT '创建 TTS 模型主键的索引,用于快速查找对应模型的音色信息'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='TTS 音色表';
|
||||||
|
|
||||||
|
-- 智能体配置模板表
|
||||||
|
DROP TABLE IF EXISTS `ai_agent_template`;
|
||||||
|
CREATE TABLE `ai_agent_template` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '智能体唯一标识',
|
||||||
|
`agent_code` VARCHAR(36) COMMENT '智能体编码',
|
||||||
|
`agent_name` VARCHAR(64) COMMENT '智能体名称',
|
||||||
|
`asr_model_id` VARCHAR(32) COMMENT '语音识别模型标识',
|
||||||
|
`vad_model_id` VARCHAR(64) COMMENT '语音活动检测标识',
|
||||||
|
`llm_model_id` VARCHAR(32) COMMENT '大语言模型标识',
|
||||||
|
`tts_model_id` VARCHAR(32) COMMENT '语音合成模型标识',
|
||||||
|
`tts_voice_id` VARCHAR(32) COMMENT '音色标识',
|
||||||
|
`mem_model_id` VARCHAR(32) COMMENT '记忆模型标识',
|
||||||
|
`intent_model_id` VARCHAR(32) COMMENT '意图模型标识',
|
||||||
|
`system_prompt` TEXT COMMENT '角色设定参数',
|
||||||
|
`lang_code` VARCHAR(10) COMMENT '语言编码',
|
||||||
|
`language` VARCHAR(10) COMMENT '交互语种',
|
||||||
|
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
|
||||||
|
`creator` BIGINT COMMENT '创建者 ID',
|
||||||
|
`created_at` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者 ID',
|
||||||
|
`updated_at` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置模板表';
|
||||||
|
|
||||||
|
-- 智能体配置表
|
||||||
|
DROP TABLE IF EXISTS `ai_agent`;
|
||||||
|
CREATE TABLE `ai_agent` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '智能体唯一标识',
|
||||||
|
`user_id` BIGINT COMMENT '所属用户 ID',
|
||||||
|
`agent_code` VARCHAR(36) COMMENT '智能体编码',
|
||||||
|
`agent_name` VARCHAR(64) COMMENT '智能体名称',
|
||||||
|
`asr_model_id` VARCHAR(32) COMMENT '语音识别模型标识',
|
||||||
|
`vad_model_id` VARCHAR(64) COMMENT '语音活动检测标识',
|
||||||
|
`llm_model_id` VARCHAR(32) COMMENT '大语言模型标识',
|
||||||
|
`tts_model_id` VARCHAR(32) COMMENT '语音合成模型标识',
|
||||||
|
`tts_voice_id` VARCHAR(32) COMMENT '音色标识',
|
||||||
|
`mem_model_id` VARCHAR(32) COMMENT '记忆模型标识',
|
||||||
|
`intent_model_id` VARCHAR(32) COMMENT '意图模型标识',
|
||||||
|
`system_prompt` TEXT COMMENT '角色设定参数',
|
||||||
|
`lang_code` VARCHAR(10) COMMENT '语言编码',
|
||||||
|
`language` VARCHAR(10) COMMENT '交互语种',
|
||||||
|
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
|
||||||
|
`creator` BIGINT COMMENT '创建者 ID',
|
||||||
|
`created_at` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者 ID',
|
||||||
|
`updated_at` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_ai_agent_user_id` (`user_id`) COMMENT '创建用户的索引,用于快速查找用户下的智能体信息'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置表';
|
||||||
|
|
||||||
|
-- 设备信息表
|
||||||
|
DROP TABLE IF EXISTS `ai_device`;
|
||||||
|
CREATE TABLE `ai_device` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '设备唯一标识',
|
||||||
|
`user_id` BIGINT COMMENT '关联用户 ID',
|
||||||
|
`mac_address` VARCHAR(50) COMMENT 'MAC 地址',
|
||||||
|
`last_connected_at` DATETIME COMMENT '最后连接时间',
|
||||||
|
`auto_update` TINYINT UNSIGNED DEFAULT 0 COMMENT '自动更新开关(0 关闭/1 开启)',
|
||||||
|
`board` VARCHAR(50) COMMENT '设备硬件型号',
|
||||||
|
`alias` VARCHAR(64) DEFAULT NULL COMMENT '设备别名',
|
||||||
|
`agent_id` VARCHAR(32) COMMENT '智能体 ID',
|
||||||
|
`app_version` VARCHAR(20) COMMENT '固件版本号',
|
||||||
|
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序',
|
||||||
|
`creator` BIGINT COMMENT '创建者',
|
||||||
|
`create_date` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者',
|
||||||
|
`update_date` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_ai_device_created_at` (`mac_address`) COMMENT '创建mac的索引,用于快速查找设备信息'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备信息表';
|
||||||
|
|
||||||
|
-- 声纹识别表
|
||||||
|
DROP TABLE IF EXISTS `ai_voiceprint`;
|
||||||
|
CREATE TABLE `ai_voiceprint` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '声纹唯一标识',
|
||||||
|
`name` VARCHAR(64) COMMENT '声纹名称',
|
||||||
|
`user_id` BIGINT COMMENT '用户 ID(关联用户表)',
|
||||||
|
`agent_id` VARCHAR(32) COMMENT '关联智能体 ID',
|
||||||
|
`agent_code` VARCHAR(36) COMMENT '关联智能体编码',
|
||||||
|
`agent_name` VARCHAR(36) COMMENT '关联智能体名称',
|
||||||
|
`description` VARCHAR(255) COMMENT '声纹描述',
|
||||||
|
`embedding` LONGTEXT COMMENT '声纹特征向量(JSON 数组格式)',
|
||||||
|
`memory` TEXT COMMENT '关联记忆数据',
|
||||||
|
`sort` INT UNSIGNED DEFAULT 0 COMMENT '排序权重',
|
||||||
|
`creator` BIGINT COMMENT '创建者 ID',
|
||||||
|
`created_at` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者 ID',
|
||||||
|
`updated_at` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声纹识别表';
|
||||||
|
|
||||||
|
-- 对话历史表
|
||||||
|
DROP TABLE IF EXISTS `ai_chat_history`;
|
||||||
|
CREATE TABLE `ai_chat_history` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '对话编号',
|
||||||
|
`user_id` BIGINT COMMENT '用户编号',
|
||||||
|
`agent_id` VARCHAR(32) DEFAULT NULL COMMENT '聊天角色',
|
||||||
|
`device_id` VARCHAR(32) DEFAULT NULL COMMENT '设备编号',
|
||||||
|
`message_count` INT COMMENT '信息汇总',
|
||||||
|
`creator` BIGINT COMMENT '创建者',
|
||||||
|
`create_date` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者',
|
||||||
|
`update_date` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话历史表';
|
||||||
|
|
||||||
|
-- 对话信息表
|
||||||
|
DROP TABLE IF EXISTS `ai_chat_message`;
|
||||||
|
CREATE TABLE `ai_chat_message` (
|
||||||
|
`id` VARCHAR(32) NOT NULL COMMENT '对话记录唯一标识',
|
||||||
|
`user_id` BIGINT COMMENT '用户唯一标识',
|
||||||
|
`chat_id` VARCHAR(64) COMMENT '对话历史 ID',
|
||||||
|
`role` ENUM('user', 'assistant') COMMENT '角色(用户或助理)',
|
||||||
|
`content` TEXT COMMENT '对话内容',
|
||||||
|
`prompt_tokens` INT UNSIGNED DEFAULT 0 COMMENT '提示令牌数',
|
||||||
|
`total_tokens` INT UNSIGNED DEFAULT 0 COMMENT '总令牌数',
|
||||||
|
`completion_tokens` INT UNSIGNED DEFAULT 0 COMMENT '完成令牌数',
|
||||||
|
`prompt_ms` INT UNSIGNED DEFAULT 0 COMMENT '提示耗时(毫秒)',
|
||||||
|
`total_ms` INT UNSIGNED DEFAULT 0 COMMENT '总耗时(毫秒)',
|
||||||
|
`completion_ms` INT UNSIGNED DEFAULT 0 COMMENT '完成耗时(毫秒)',
|
||||||
|
`creator` BIGINT COMMENT '创建者',
|
||||||
|
`create_date` DATETIME COMMENT '创建时间',
|
||||||
|
`updater` BIGINT COMMENT '更新者',
|
||||||
|
`update_date` DATETIME COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
INDEX `idx_ai_chat_message_user_id_chat_id_role` (`user_id`, `chat_id`) COMMENT '用户 ID、聊天会话 ID 和角色的联合索引,用于快速检索对话记录',
|
||||||
|
INDEX `idx_ai_chat_message_created_at` (`create_date`) COMMENT '创建时间的索引,用于按时间排序或检索对话记录'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话信息表';
|
||||||
@@ -3,16 +3,16 @@
|
|||||||
# 每次对数据表进行改动时,只允许新建新对changeSet,不允许对上一个changeSet配置及文件进行修改
|
# 每次对数据表进行改动时,只允许新建新对changeSet,不允许对上一个changeSet配置及文件进行修改
|
||||||
databaseChangeLog:
|
databaseChangeLog:
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 001create_sys
|
id: 202503141335
|
||||||
author: John
|
author: John
|
||||||
changes:
|
changes:
|
||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/001create_sys.sql
|
path: classpath:db/changelog/202503141335.sql
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 202503101631
|
id: 202503141346
|
||||||
author: zjy
|
author: czc
|
||||||
changes:
|
changes:
|
||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202503101631.sql
|
path: classpath:db/changelog/202503141346.sql
|
||||||
@@ -33,3 +33,4 @@
|
|||||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||||
10031=\u5F31\u5BC6\u7801
|
10031=\u5F31\u5BC6\u7801
|
||||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||||
|
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||||
|
|||||||
@@ -32,3 +32,4 @@
|
|||||||
10030=The password is less than {0} digits.
|
10030=The password is less than {0} digits.
|
||||||
10031=The password must consist of numbers, uppercase and lowercase letters, and special characters at the same time
|
10031=The password must consist of numbers, uppercase and lowercase letters, and special characters at the same time
|
||||||
10032=Exception in deleting this data
|
10032=Exception in deleting this data
|
||||||
|
10033=Device verification code error
|
||||||
@@ -31,4 +31,5 @@
|
|||||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||||
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
|
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
|
||||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||||
|
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
||||||
@@ -31,4 +31,5 @@
|
|||||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||||
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||||
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
|
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
|
||||||
10032=\u522A\u9664\u6B64\u6578\u64DA\u7570\u5E38
|
10032=\u522A\u9664\u6B64\u6578\u64DA\u7570\u5E38
|
||||||
|
10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
|
||||||
@@ -3,37 +3,4 @@
|
|||||||
|
|
||||||
<mapper namespace="xiaozhi.modules.sys.dao.SysUserDao">
|
<mapper namespace="xiaozhi.modules.sys.dao.SysUserDao">
|
||||||
|
|
||||||
<select id="getList" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
|
||||||
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName
|
|
||||||
from sys_user t1 where t1.super_admin = 0
|
|
||||||
<if test="username != null and username.trim() != ''">
|
|
||||||
and t1.username like #{username}
|
|
||||||
</if>
|
|
||||||
<if test="deptId != null and deptId.trim() != ''">
|
|
||||||
and t1.dept_id = #{deptId}
|
|
||||||
</if>
|
|
||||||
<if test="gender != null and gender.trim() != ''">
|
|
||||||
and t1.gender = #{gender}
|
|
||||||
</if>
|
|
||||||
<if test="deptIdList != null">
|
|
||||||
and t1.dept_id in
|
|
||||||
<foreach item="id" collection="deptIdList" open="(" separator="," close=")">
|
|
||||||
#{id}
|
|
||||||
</foreach>
|
|
||||||
</if>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getById" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
|
||||||
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName from sys_user t1
|
|
||||||
where t1.id = #{value}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="getByUsername" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
|
||||||
select * from sys_user where username = #{value}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<update id="updatePassword">
|
|
||||||
update sys_user set password = #{newPassword} where id = #{id}
|
|
||||||
</update>
|
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -26,4 +26,7 @@ nav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {goToPage, showDanger, showWarning} from '../utils/index'
|
import {goToPage, showDanger, showWarning, isNotNull} from '../utils/index'
|
||||||
import Constant from '../utils/constant'
|
import Constant from '../utils/constant'
|
||||||
import Fly from 'flyio/dist/npm/fly';
|
import Fly from 'flyio/dist/npm/fly';
|
||||||
|
import store from '../store/index'
|
||||||
|
|
||||||
const fly = new Fly()
|
const fly = new Fly()
|
||||||
// 设置超时
|
// 设置超时
|
||||||
@@ -25,7 +26,9 @@ function sendRequest() {
|
|||||||
_url: '',
|
_url: '',
|
||||||
_responseType: undefined, // 新增响应类型字段
|
_responseType: undefined, // 新增响应类型字段
|
||||||
'send'() {
|
'send'() {
|
||||||
this._header.token = localStorage.getItem(Constant.STORAGE_KEY.TOKEN)
|
if(isNotNull(store.getters.getToken)){
|
||||||
|
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
|
||||||
|
}
|
||||||
|
|
||||||
// 打印请求信息
|
// 打印请求信息
|
||||||
fly.request(this._url, this._data, {
|
fly.request(this._url, this._data, {
|
||||||
@@ -43,7 +46,7 @@ function sendRequest() {
|
|||||||
}
|
}
|
||||||
}).catch((res) => {
|
}).catch((res) => {
|
||||||
// 打印失败响应
|
// 打印失败响应
|
||||||
console.log(res)
|
console.log('catch', res)
|
||||||
httpHandlerError(res, this._failCallback)
|
httpHandlerError(res, this._failCallback)
|
||||||
})
|
})
|
||||||
return this
|
return this
|
||||||
@@ -97,6 +100,7 @@ function sendRequest() {
|
|||||||
*/
|
*/
|
||||||
// 在错误处理函数中添加日志
|
// 在错误处理函数中添加日志
|
||||||
function httpHandlerError(info, callBack) {
|
function httpHandlerError(info, callBack) {
|
||||||
|
console.log('httpHandlerError', info)
|
||||||
|
|
||||||
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
|
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
|
||||||
let networkError = false
|
let networkError = false
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {getServiceUrl} from '../api'
|
|||||||
export default {
|
export default {
|
||||||
// 登录
|
// 登录
|
||||||
login(loginForm, callback) {
|
login(loginForm, callback) {
|
||||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`).method('POST')
|
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`)
|
||||||
|
.method('POST')
|
||||||
.data(loginForm)
|
.data(loginForm)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
@@ -19,7 +20,8 @@ export default {
|
|||||||
},
|
},
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
getUserInfo(callback) {
|
getUserInfo(callback) {
|
||||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`).method('GET')
|
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`)
|
||||||
|
.method('GET')
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
callback(res)
|
callback(res)
|
||||||
@@ -32,7 +34,8 @@ export default {
|
|||||||
},
|
},
|
||||||
// 获取设备信息
|
// 获取设备信息
|
||||||
getHomeList(callback) {
|
getHomeList(callback) {
|
||||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`).method('GET')
|
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`)
|
||||||
|
.method('GET')
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
callback(res)
|
callback(res)
|
||||||
@@ -76,14 +79,14 @@ export default {
|
|||||||
},
|
},
|
||||||
// 获取验证码
|
// 获取验证码
|
||||||
getCaptcha(uuid, callback) {
|
getCaptcha(uuid, callback) {
|
||||||
|
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
|
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
|
||||||
.method('GET')
|
.method('GET')
|
||||||
.type('blob')
|
.type('blob')
|
||||||
.header({
|
.header({
|
||||||
'Content-Type': 'image/gif',
|
'Content-Type': 'image/gif',
|
||||||
'Pragma': 'No-cache',
|
'Pragma': 'No-cache',
|
||||||
'Cache-Control': 'no-cache'
|
'Cache-Control': 'no-cache'
|
||||||
})
|
})
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
@@ -91,7 +94,7 @@ export default {
|
|||||||
callback(res);
|
callback(res);
|
||||||
})
|
})
|
||||||
.fail((err) => { // 添加错误参数
|
.fail((err) => { // 添加错误参数
|
||||||
|
|
||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
// 注册账号
|
// 注册账号
|
||||||
@@ -105,4 +108,70 @@ export default {
|
|||||||
.fail(() => {
|
.fail(() => {
|
||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 保存设备配置
|
||||||
|
saveDeviceConfig(device_id, configData, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
|
||||||
|
.method('PUT')
|
||||||
|
.data(configData)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('保存配置失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.saveDeviceConfig(device_id, configData, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 获取设备配置
|
||||||
|
getDeviceConfig(device_id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取配置失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDeviceConfig(device_id, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 获取所有模型名称
|
||||||
|
getModelNames(callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/models/names`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getModelNames(callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取模型音色
|
||||||
|
getModelVoices(modelName, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/models/${modelName}/voices`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getModelVoices(modelName, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :visible.sync="visible" width="480px" center>
|
||||||
|
<div style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
|
<div style="width: 36px;height: 36px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||||
|
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" />
|
||||||
|
</div>
|
||||||
|
添加设备
|
||||||
|
</div>
|
||||||
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
|
<div style="margin: 30px 20px;">
|
||||||
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||||
|
<div style="color: red;display: inline-block;">*</div>验证码:
|
||||||
|
</div>
|
||||||
|
<div class="input-46" style="margin-top: 10px;">
|
||||||
|
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;margin: 0 20px;gap: 10px;">
|
||||||
|
<div class="dialog-btn" @click="confirm">
|
||||||
|
确定
|
||||||
|
</div>
|
||||||
|
<div class="dialog-btn"
|
||||||
|
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||||
|
@click="cancel">
|
||||||
|
取消
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AddDeviceDialog',
|
||||||
|
props: {
|
||||||
|
visible: { type: Boolean, required: true }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { deviceCode: "" }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
confirm() {
|
||||||
|
this.$emit('update:visible', false)
|
||||||
|
this.$emit('added', this.deviceCode)
|
||||||
|
this.deviceCode = ""
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.$emit('update:visible', false)
|
||||||
|
this.deviceCode = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
.input-46 {
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
background: #f6f8fb;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 23px;
|
||||||
|
background: #5778ff;
|
||||||
|
height: 46px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 46px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div class="device-item">
|
||||||
|
<div style="display: flex;justify-content: space-between;">
|
||||||
|
<div style="font-weight: 700;font-size: 24px;text-align: left;color: #3d4566;">
|
||||||
|
{{ device.mac }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img src="@/assets/home/delete.png" alt=""
|
||||||
|
style="width: 24px;height: 24px;margin-right: 10px;" />
|
||||||
|
<img src="@/assets/home/info.png" alt="" style="width: 24px;height: 24px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="device-name">
|
||||||
|
设备型号:{{ device.model }}
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;gap: 10px;align-items: center;">
|
||||||
|
<div class="settings-btn" @click="$emit('configure')">
|
||||||
|
配置角色
|
||||||
|
</div>
|
||||||
|
<div class="settings-btn">
|
||||||
|
声纹识别
|
||||||
|
</div>
|
||||||
|
<div class="settings-btn">
|
||||||
|
历史对话
|
||||||
|
</div>
|
||||||
|
<el-switch v-model="switchValue" inactive-text="OTA升级:" :width="42"
|
||||||
|
style="margin-left: auto;" />
|
||||||
|
</div>
|
||||||
|
<div class="version-info">
|
||||||
|
<div>最近对话:{{ device.lastConversation }}</div>
|
||||||
|
<div>APP版本:{{ device.appVersion }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DeviceItem',
|
||||||
|
props: {
|
||||||
|
device: { type: Object, required: true }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { switchValue: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.device-item {
|
||||||
|
width: 455px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #fafcfe;
|
||||||
|
padding: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.device-name {
|
||||||
|
margin: 10px 0 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3d4566;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #5778ff;
|
||||||
|
background: #e6ebff;
|
||||||
|
width: 76px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #979db1;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<el-header class="header">
|
||||||
|
<div style="display: flex;justify-content: space-between;">
|
||||||
|
<div style="display: flex;align-items: center;gap: 10px;">
|
||||||
|
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 56px;height: 56px;" />
|
||||||
|
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 78px;height: 16px;" />
|
||||||
|
<div class="equipment-management" @click="goHome">
|
||||||
|
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" />
|
||||||
|
设备管理
|
||||||
|
</div>
|
||||||
|
<div class="console">
|
||||||
|
<i class="el-icon-s-grid" style="font-size: 14px;color: #979db1;" />
|
||||||
|
控制台
|
||||||
|
</div>
|
||||||
|
<div class="equipment-management2">
|
||||||
|
设备管理
|
||||||
|
<img src="@/assets/home/close.png" alt="" style="width: 8px;height: 8px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex;align-items: center;gap: 10px;">
|
||||||
|
<div class="serach-box">
|
||||||
|
<el-input placeholder="输入名称搜索.." v-model="serach" />
|
||||||
|
<img src="@/assets/home/search.png" alt=""
|
||||||
|
style="width: 16px;height: 16px;margin-right: 15px;cursor: pointer;" />
|
||||||
|
</div>
|
||||||
|
<img src="@/assets/home/avatar.png" alt="" style="width: 28px;height: 28px;" />
|
||||||
|
<div class="user-info">
|
||||||
|
158 3632 4642
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HeaderBar',
|
||||||
|
data() {
|
||||||
|
return { serach: '' }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goHome() {
|
||||||
|
// 跳转到首页
|
||||||
|
this.$router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.equipment-management,
|
||||||
|
.equipment-management2 {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-management {
|
||||||
|
width: 110px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #5778ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-management2 {
|
||||||
|
width: 116px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #979db1;
|
||||||
|
font-weight: 400;
|
||||||
|
gap: 10px;
|
||||||
|
color: #3d4566;
|
||||||
|
margin-left: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #f6fcfe66;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serach-box {
|
||||||
|
display: flex;
|
||||||
|
width: 306px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: #e2f5f7;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
letter-spacing: -0.02px;
|
||||||
|
text-align: left;
|
||||||
|
color: #3d4566;
|
||||||
|
}
|
||||||
|
.console {
|
||||||
|
width: 120px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #979db1;
|
||||||
|
font-weight: 400;
|
||||||
|
gap: 10px;
|
||||||
|
color: #979db1;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'HelloWorld',
|
|
||||||
props: {
|
|
||||||
msg: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped lang="scss">
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import Welcome from '../views/welcome.vue'
|
|
||||||
import Login from '../views/login.vue'
|
|
||||||
import Register from '@/views/register.vue'
|
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
@@ -10,33 +7,36 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'welcome',
|
name: 'welcome',
|
||||||
component: Login
|
component: function () {
|
||||||
|
return import('../views/login.vue')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/role-config',
|
||||||
|
name: 'RoleConfig',
|
||||||
|
component: function () {
|
||||||
|
return import('../views/roleConfig.vue')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
// route level code-splitting
|
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
|
||||||
// which is lazy-loaded when the route is visited.
|
|
||||||
component: function () {
|
component: function () {
|
||||||
return import(/* webpackChunkName: "about" */ '../views/login.vue')
|
return import('../views/login.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/home',
|
path: '/home',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: function () {
|
component: function () {
|
||||||
return import(/* webpackChunkName: "about" */ '../views/home.vue')
|
return import('../views/home.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/register',
|
path: '/register',
|
||||||
name: 'Register',
|
name: 'Register',
|
||||||
// route level code-splitting
|
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
|
||||||
// which is lazy-loaded when the route is visited.
|
|
||||||
component: function () {
|
component: function () {
|
||||||
return import(/* webpackChunkName: "about" */ '../views/register.vue')
|
return import('../views/register.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
|
import Constant from '../utils/constant'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
|
token: ''
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
getToken(state) {
|
||||||
|
if (!state.token) {
|
||||||
|
state.token = localStorage.getItem('token')
|
||||||
|
}
|
||||||
|
return state.token
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
setToken(state, token) {
|
||||||
|
state.token = token
|
||||||
|
localStorage.token = token
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,346 +1,88 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<el-container style="height: 100%;">
|
<!-- 公共头部 -->
|
||||||
<el-header class="header">
|
<HeaderBar />
|
||||||
<div style="display: flex;justify-content: space-between;">
|
<el-main style="padding: 20px;display: flex;flex-direction: column;">
|
||||||
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px;">
|
<div>
|
||||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" />
|
<!-- 首页内容 -->
|
||||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" />
|
|
||||||
<div class="equipment-management" @click="settingDevice=false">
|
|
||||||
<img src="@/assets/home/equipment.png" alt="" style="width: 12px;height: 11px;" />
|
|
||||||
设备管理
|
|
||||||
</div>
|
|
||||||
<div class="console">
|
|
||||||
<i class="el-icon-s-grid" style="font-size: 11px;color: #979db1;" />
|
|
||||||
控制台
|
|
||||||
</div>
|
|
||||||
<div class="equipment-management2">
|
|
||||||
设备管理
|
|
||||||
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px">
|
|
||||||
<div class="serach-box">
|
|
||||||
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" @keyup.enter.native="handleSearch" />
|
|
||||||
<img src="@/assets/home/search.png" alt=""
|
|
||||||
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
|
|
||||||
</div>
|
|
||||||
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
|
|
||||||
<div class="user-info">
|
|
||||||
{{ userInfo.mobile }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-header>
|
|
||||||
<el-main style="padding: 15px;display: flex;flex-direction: column;">
|
|
||||||
<div v-show="!settingDevice">
|
|
||||||
<div class="add-device">
|
<div class="add-device">
|
||||||
<div class="add-device-bg">
|
<div class="add-device-bg">
|
||||||
<div class="hellow-text" style="margin-top: 23px;">
|
<div class="hellow-text" style="margin-top: 30px;">
|
||||||
您好,小智</div>
|
您好,小智
|
||||||
<div class="hellow-text">让我们度过<div style="display: inline-block;color: #5778FF;">
|
</div>
|
||||||
|
<div class="hellow-text">
|
||||||
|
让我们度过
|
||||||
|
<div style="display: inline-block;color: #5778FF;">
|
||||||
美好的一天!
|
美好的一天!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hi-hint">
|
<div class="hi-hint">
|
||||||
Hello, Let's have a wonderful day!</div>
|
Hello, Let's have a wonderful day!
|
||||||
|
</div>
|
||||||
<div class="add-device-btn" @click="showAddDialog">
|
<div class="add-device-btn" @click="showAddDialog">
|
||||||
<div class="left-add">
|
<div class="left-add">
|
||||||
添加设备
|
添加设备
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 17px;height: 10px;background: #5778ff;margin-left: -8px;" />
|
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||||
<div class="right-add">
|
<div class="right-add">
|
||||||
<i class="el-icon-right" style="font-size: 23px;color: #fff;" />
|
<i class="el-icon-right" style="font-size: 30px;color: #fff;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div style="display: flex;flex-wrap: wrap;margin-top: 20px;gap: 20px;justify-content: space-between;box-sizing: border-box;">
|
||||||
style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: flex-start;box-sizing: border-box;">
|
<DeviceItem v-for="(item,index) in devices" :key="index" :device="item" @configure="goToRoleConfig" />
|
||||||
<div class="device-item" v-for="(item,index) in filteredDeviceList" :key="index">
|
|
||||||
<div style="display: flex;justify-content: space-between; align-items: center; ">
|
|
||||||
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
|
|
||||||
<!-- CC:ba:97:11:a6:ac-->
|
|
||||||
{{item.list[0]?.mac_address}}
|
|
||||||
</div>
|
|
||||||
<div style="display: flex;align-items: center;">
|
|
||||||
<img src="@/assets/home/delete.png" alt=""
|
|
||||||
style="width: 18px;height: 18px;margin-right: 8px;" @click="unbindDevice(item.list[0]?.id)" />
|
|
||||||
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="device-name">
|
|
||||||
设备型号:{{item.list[0]?.device_type}}
|
|
||||||
</div>
|
|
||||||
<div style="display: flex;gap: 8px;align-items: center;">
|
|
||||||
<div class="settings-btn" @click="clickSettingDevice">
|
|
||||||
配置角色</div>
|
|
||||||
<div class="settings-btn">
|
|
||||||
声纹识别</div>
|
|
||||||
<div class="settings-btn">
|
|
||||||
历史对话</div>
|
|
||||||
<el-switch :value="item.list[0]?.ota_upgrade && true || false" inactive-text="OTA升级:" :width="32"
|
|
||||||
style="margin-left: auto;" />
|
|
||||||
</div>
|
|
||||||
<div class="version-info">
|
|
||||||
<div>最近对话:{{item.list[0]?.recent_chat_time}}</div>
|
|
||||||
<div>APP版本:{{item.list[0]?.app_version}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="settingDevice" style="border-radius: 18px;background: #fafcfe;">
|
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
||||||
<div
|
|
||||||
style="padding: 17px 27px;font-weight: 700;font-size: 21px;text-align: left;color: #3d4566;display: flex;gap: 14px;align-items: center;">
|
|
||||||
<div
|
|
||||||
style="width: 41px;height: 41px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
|
||||||
<img src="@/assets/home/setting-user.png" alt="" style="width: 21px;height: 21px;" />
|
|
||||||
</div>
|
|
||||||
CC:ba:97:11:a6:ac
|
|
||||||
</div>
|
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
|
||||||
<el-form ref="form" :model="form" label-width="81px">
|
|
||||||
<div style="padding: 18px 28px;max-width: 890px;">
|
|
||||||
<el-form-item label="助手昵称:">
|
|
||||||
<div class="input-46" style="width: 57.5%;">
|
|
||||||
<el-input v-model="form.name" />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色模版:">
|
|
||||||
<div style="display: flex;gap: 10px;">
|
|
||||||
<div class="template-item">
|
|
||||||
台湾女友</div>
|
|
||||||
<div class="template-item">
|
|
||||||
土豆子</div>
|
|
||||||
<div class="template-item">
|
|
||||||
英语老师</div>
|
|
||||||
<div class="template-item">
|
|
||||||
好奇小男孩</div>
|
|
||||||
<div class="template-item">
|
|
||||||
汪汪队队长</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色音色:">
|
|
||||||
<div style="display: flex;gap: 9px;align-items: center;">
|
|
||||||
<div class="input-46" style="flex:1.4;">
|
|
||||||
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
|
|
||||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
|
||||||
:value="item.value">
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
<div class="audio-box">
|
|
||||||
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
|
|
||||||
style="height: 100%;width: 100%;" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色介绍:">
|
|
||||||
<div class="textarea-box">
|
|
||||||
<el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
|
|
||||||
v-model="form.introduction" maxlength="2000" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="记忆体:">
|
|
||||||
<div class="textarea-box">
|
|
||||||
<el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
|
|
||||||
v-model="form.prompt" maxlength="1000" />
|
|
||||||
<div class="prompt-bottom">
|
|
||||||
<div style="display: flex;gap: 10px;align-items: center;">
|
|
||||||
<div style="color: #979db1;font-size: 12px;">当前记忆(每次对话后重新生成)</div>
|
|
||||||
<div class="clear-btn">
|
|
||||||
<i class="el-icon-delete-solid" style="font-size: 12px;" />
|
|
||||||
清除
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="color: #979db1;font-size:12px;">{{form.prompt.length}}/1000</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="语言模型(内测):" class="lh-form-item">
|
|
||||||
<div style="display: flex;gap: 9px;">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-select v-model="form.model" placeholder="请选择" style="width: 100%;">
|
|
||||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
|
||||||
:value="item.value">
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="" class="lh-form-item">
|
|
||||||
<div style="color: #979db1;text-align: left;">除了“Qwen
|
|
||||||
实时”,其他模型通常会增加约1秒的延迟。改变模型后,建议清空记忆体,以免影响体验。</div>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
</el-form>
|
|
||||||
<div style="display: flex;padding: 18px;gap: 9px;align-items: center;">
|
|
||||||
<div class="save-btn">
|
|
||||||
保存配置</div>
|
|
||||||
<div class="reset-btn">
|
|
||||||
重制</div>
|
|
||||||
<div class="clear-text">
|
|
||||||
<img src="@/assets/home/red-info.png" alt="" style="width: 21px;height: 21px;" />
|
|
||||||
保存配置后,需要重启设备,新的配置才会生效。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
|
||||||
©2025 xiaozhi-esp32-server
|
©2025 xiaozhi-esp32-server
|
||||||
</div>
|
</div>
|
||||||
|
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" @added="handleDeviceAdded" />
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
|
||||||
<el-dialog :visible.sync="addDeviceDialogVisible" width="400px" center>
|
|
||||||
<div
|
|
||||||
style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;;">
|
|
||||||
<div
|
|
||||||
style="width: 36px;height: 36px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
|
||||||
<img src="@/assets/home/equipment.png" alt="" style="width: 16px;height: 14px;" />
|
|
||||||
</div>
|
|
||||||
添加设备
|
|
||||||
</div>
|
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
|
||||||
<div style="margin: 30px 20px;">
|
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
|
||||||
<div style="color: red;display: inline-block;">*</div>验证码:
|
|
||||||
</div>
|
|
||||||
<div class="input-46" style="margin-top: 10px;">
|
|
||||||
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex;margin: 0 20px;gap: 10px;">
|
|
||||||
<div class="dialog-btn" @click="addDevice">确定</div>
|
|
||||||
<div class="dialog-btn"
|
|
||||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
|
||||||
@click="addDeviceDialogVisible=false">
|
|
||||||
取消</div>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// @ is an alias to /src
|
import DeviceItem from '@/components/DeviceItem.vue'
|
||||||
|
import AddDeviceDialog from '@/components/AddDeviceDialog.vue'
|
||||||
import Api from '@/apis/api';
|
import HeaderBar from '@/components/HeaderBar.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'home',
|
name: 'HomePage',
|
||||||
|
components: { DeviceItem, AddDeviceDialog, HeaderBar },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serach: '', // 搜索框输入内容
|
|
||||||
deviceList: [], // 原始设备列表
|
|
||||||
filteredDeviceList: [], // 过滤后的设备列表
|
|
||||||
switchValue: false,
|
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
deviceCode: "", // 设备验证码
|
// 此处模拟设备列表(10条数据)
|
||||||
settingDevice: false,
|
devices: Array.from({ length: 10 }, (_, i) => ({
|
||||||
form: {
|
id: i,
|
||||||
name: "",
|
mac: 'CC:ba:97:11:a6:ac',
|
||||||
timbre: "",
|
model: 'esp32-s3-touch-amoled-1.8',
|
||||||
introduction: "",
|
lastConversation: '6天前',
|
||||||
prompt: "",
|
appVersion: '1.1.0'
|
||||||
model: ""
|
}))
|
||||||
},
|
}
|
||||||
options: [{
|
|
||||||
value: '选项1',
|
|
||||||
label: '黄金糕'
|
|
||||||
}, {
|
|
||||||
value: '选项2',
|
|
||||||
label: '双皮奶'
|
|
||||||
}],
|
|
||||||
userInfo: {
|
|
||||||
mobile: '' // 初始化用户信息
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.addDeviceDialogVisible = true;
|
this.addDeviceDialogVisible = true
|
||||||
},
|
},
|
||||||
clickSettingDevice() {
|
goToRoleConfig() {
|
||||||
this.settingDevice = true
|
// 点击配置角色后跳转到角色配置页
|
||||||
|
this.$router.push('/role-config')
|
||||||
},
|
},
|
||||||
// 获取用户信息
|
handleDeviceAdded(deviceCode) {
|
||||||
fetchUserInfo() {
|
// 根据需要处理添加设备后逻辑,比如刷新设备列表等
|
||||||
Api.user.getUserInfo(({data}) => {
|
console.log('设备验证码:', deviceCode)
|
||||||
this.userInfo = data.data
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 获取已绑设备
|
|
||||||
getList(){
|
|
||||||
Api.user.getHomeList(({data})=>{
|
|
||||||
this.deviceList = data.data; // 保存原始设备列表
|
|
||||||
this.filteredDeviceList = data.data; // 初始化过滤后的设备列表
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// 处理搜索
|
|
||||||
handleSearch() {
|
|
||||||
if (this.serach.trim() === '') {
|
|
||||||
// 如果搜索框为空,显示全部设备
|
|
||||||
this.filteredDeviceList = this.deviceList;
|
|
||||||
} else {
|
|
||||||
// 过滤设备列表
|
|
||||||
this.filteredDeviceList = this.deviceList.filter(device => {
|
|
||||||
return (
|
|
||||||
device.list[0]?.mac_address?.includes(this.serach) || // 匹配MAC地址
|
|
||||||
device.list[0]?.device_type?.includes(this.serach) || // 匹配设备型号
|
|
||||||
device.list[0]?.app_version?.includes(this.serach) // 匹配APP版本
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 解绑设备
|
|
||||||
unbindDevice(device_id) {
|
|
||||||
this.$confirm('确定要解绑该设备吗?', '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(() => {
|
|
||||||
// 调用解绑设备的接口
|
|
||||||
Api.user.unbindDevice(device_id, ({ data }) => {
|
|
||||||
if (data.code === 0) {
|
|
||||||
this.$message.success('解绑成功');
|
|
||||||
this.getList();
|
|
||||||
} else {
|
|
||||||
this.$message.error(data.msg || '解绑失败');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(() => {
|
|
||||||
// 用户取消操作
|
|
||||||
this.$message.info('已取消解绑');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// 添加设备
|
|
||||||
addDevice() {
|
|
||||||
if (!this.deviceCode) {
|
|
||||||
this.$message.warning('请输入设备验证码');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Api.user.bindDevice(this.deviceCode, ({ data }) => {
|
|
||||||
if (data.code === 0) {
|
|
||||||
this.$message.success('设备绑定成功');
|
|
||||||
this.addDeviceDialogVisible = false;
|
|
||||||
this.getList(); // 刷新设备列表
|
|
||||||
} else {
|
|
||||||
this.$message.error(data.msg || '设备绑定失败');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fetchUserInfo(); // 组件加载时获取用户信息
|
|
||||||
this.getList(); // 初始化设备列表
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped>
|
||||||
.welcome {
|
.welcome {
|
||||||
min-width: 900px;
|
min-width: 1200px;
|
||||||
min-height: 506px;
|
min-height: 675px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-image: url("@/assets/home/background.png");
|
background-image: url("@/assets/home/background.png");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@@ -352,59 +94,18 @@ export default {
|
|||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
/* 兼容老版本Opera浏览器 */
|
/* 兼容老版本Opera浏览器 */
|
||||||
}
|
}
|
||||||
.equipment-management,
|
|
||||||
.equipment-management2 {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.equipment-management {
|
|
||||||
width: 83px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #5778ff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.equipment-management2 {
|
|
||||||
width: 87px;
|
|
||||||
height: 23px;
|
|
||||||
border-radius: 11px;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 9px;
|
|
||||||
font-weight: 400;
|
|
||||||
gap: 8px;
|
|
||||||
color: #3d4566;
|
|
||||||
margin-left: 5px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
background: #f6fcfe66;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
}
|
|
||||||
.add-device {
|
.add-device {
|
||||||
height: 195px;
|
height: 260px;
|
||||||
border-radius: 15px;
|
border-radius: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
269.62deg,
|
269.62deg,
|
||||||
#e0e6fd 0%,
|
#e0e6fd 0%,
|
||||||
#cce7ff 49.69%,
|
#cce7ff 49.69%,
|
||||||
#d3d3fe 100%
|
#d3d3fe 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.audio-box {
|
|
||||||
flex: 1;
|
|
||||||
height: 35px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e4e6ef;
|
|
||||||
}
|
|
||||||
.add-device-bg {
|
.add-device-bg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -421,275 +122,51 @@ export default {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* 兼容老版本Opera浏览器 */
|
/* 兼容老版本Opera浏览器 */
|
||||||
.hellow-text {
|
.hellow-text {
|
||||||
margin-left: 75px;
|
margin-left: 100px;
|
||||||
color: #3d4566;
|
color: #3d4566;
|
||||||
font-size: 33px;
|
font-size: 44px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hi-hint {
|
.hi-hint {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 9px;
|
font-size: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #818cae;
|
color: #818cae;
|
||||||
margin-left: 75px;
|
margin-left: 100px;
|
||||||
margin-top: 5px;
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.serach-box {
|
|
||||||
display: flex;
|
|
||||||
width: 250px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 15px;
|
|
||||||
background-color: #f6fcfe66;
|
|
||||||
border: 1px solid #e4e6ef;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 10px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.user-info {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: -0.02px;
|
|
||||||
text-align: left;
|
|
||||||
color: #3d4566;
|
|
||||||
}
|
|
||||||
.clear-btn {
|
|
||||||
width: 45px;
|
|
||||||
height: 18px;
|
|
||||||
background: #fd8383;
|
|
||||||
border-radius: 9px;
|
|
||||||
line-height: 18px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.clear-text {
|
|
||||||
color: #979db1;
|
|
||||||
font-size: 11px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
.template-item {
|
|
||||||
height: 35px;
|
|
||||||
width: 85px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #e6ebff;
|
|
||||||
line-height: 35px;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: center;
|
|
||||||
color: #5778ff;
|
|
||||||
}
|
|
||||||
.prompt-bottom {
|
|
||||||
margin-bottom: 4px;display: flex;justify-content: space-between;padding: 0 15px;align-items: center;
|
|
||||||
}
|
|
||||||
.input-35 {
|
|
||||||
border: 1px solid #e4e6ef;
|
|
||||||
background: #f6f8fb;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
.console {
|
|
||||||
width: 90px;
|
|
||||||
height: 23px;
|
|
||||||
border-radius: 11px;
|
|
||||||
background: radial-gradient(50% 50% at 50% 50%, #fff 0%, #e8f0ff 100%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 9px;
|
|
||||||
color: #979db1;
|
|
||||||
font-weight: 400;
|
|
||||||
gap: 8px;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
.dialog-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
flex: 1;
|
|
||||||
border-radius: 17px;
|
|
||||||
background: #5778ff;
|
|
||||||
height: 34px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #fff;
|
|
||||||
line-height: 34px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.add-device-btn {
|
.add-device-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: 75px;
|
margin-left: 100px;
|
||||||
margin-top: 15px;
|
margin-top: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.left-add {
|
.left-add {
|
||||||
width: 105px;
|
width: 140px;
|
||||||
height: 34px;
|
height: 46px;
|
||||||
border-radius: 17px;
|
border-radius: 23px;
|
||||||
background: #5778ff;
|
background: #5778ff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 11px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 34px;
|
line-height: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-add {
|
.right-add {
|
||||||
width: 34px;
|
width: 46px;
|
||||||
height: 34px;
|
height: 46px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #5778ff;
|
background: #5778ff;
|
||||||
margin-left: -6px;
|
margin-left: -8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.device-item {
|
</style>
|
||||||
width: 345px;
|
|
||||||
border-radius: 15px;
|
|
||||||
background: #fafcfe;
|
|
||||||
padding: 22px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.device-name {
|
|
||||||
margin: 8px 0 10px;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #3d4566;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
audio::-webkit-media-controls-panel {
|
|
||||||
background-color: #fafcfe; /* 设置音频面板的控制按钮背景颜色为透明 */
|
|
||||||
}
|
|
||||||
.settings-btn {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #5778ff;
|
|
||||||
background: #e6ebff;
|
|
||||||
width: 57px;
|
|
||||||
height: 21px;
|
|
||||||
line-height: 21px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.version-info {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 15px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #979db1;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
.save-btn,
|
|
||||||
.reset-btn {
|
|
||||||
width: 105px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 17px;
|
|
||||||
line-height: 34px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.save-btn {
|
|
||||||
border-radius: 23px;
|
|
||||||
background: #5778ff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.reset-btn {
|
|
||||||
border: 1px solid #adbdff;
|
|
||||||
background: #e6ebff;
|
|
||||||
color: #5778ff;
|
|
||||||
}
|
|
||||||
.textarea-box {
|
|
||||||
border: 1px solid #e4e6ef;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f6f8fb;
|
|
||||||
}
|
|
||||||
::v-deep {
|
|
||||||
.textarea-box .el-textarea__inner {
|
|
||||||
background-color: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
// 搜索输入框的样式调整
|
|
||||||
.serach-box .el-input__inner {
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0 5px 0 15px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #3d4566;
|
|
||||||
}
|
|
||||||
.el-textarea .el-input__count {
|
|
||||||
color: #979db1;
|
|
||||||
font-size: 11px;
|
|
||||||
right: 15px;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
.el-input__inner {
|
|
||||||
//border: none;
|
|
||||||
//background-color: transparent;
|
|
||||||
padding: 0 5px 0 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
.input-46 .el-input__inner {
|
|
||||||
padding: 0 15px;
|
|
||||||
height: 38px;
|
|
||||||
}
|
|
||||||
.lh-form-item {
|
|
||||||
.el-form-item__label {
|
|
||||||
line-height: 17px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-form-item__label {
|
|
||||||
line-height: 34px;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 11px;
|
|
||||||
text-align: left;
|
|
||||||
color: #3d4566;
|
|
||||||
}
|
|
||||||
.el-switch__core {
|
|
||||||
height: 21px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.el-switch {
|
|
||||||
line-height: 28px;
|
|
||||||
}
|
|
||||||
.el-switch.is-checked .el-switch__core::after {
|
|
||||||
left: calc(100% - 4px);
|
|
||||||
}
|
|
||||||
.el-switch__label {
|
|
||||||
color: #3d4566;
|
|
||||||
}
|
|
||||||
.el-switch__core:after {
|
|
||||||
left: 4px;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
.el-dialog__headerbtn .el-dialog__close {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.el-dialog__header,
|
|
||||||
.el-dialog__footer {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.el-dialog--center .el-dialog__body {
|
|
||||||
padding: 24px 0;
|
|
||||||
}
|
|
||||||
.el-dialog {
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: column !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
position: absolute !important;
|
|
||||||
top: 50% !important;
|
|
||||||
left: 50% !important;
|
|
||||||
transform: translate(-50%, -50%) !important;
|
|
||||||
overflow-y: scroll !important;
|
|
||||||
max-height: 100vh !important;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
@@ -87,18 +87,22 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchCaptcha() {
|
fetchCaptcha() {
|
||||||
this.captchaUuid = getUUID();
|
if (this.$store.getters.getToken) {
|
||||||
|
goToPage('/home')
|
||||||
|
} else {
|
||||||
|
this.captchaUuid = getUUID();
|
||||||
|
|
||||||
Api.user.getCaptcha(this.captchaUuid, (res) => {
|
Api.user.getCaptcha(this.captchaUuid, (res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const blob = new Blob([res.data], {type: res.data.type});
|
const blob = new Blob([res.data], {type: res.data.type});
|
||||||
this.captchaUrl = URL.createObjectURL(blob);
|
this.captchaUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error('验证码加载异常:', error);
|
console.error('验证码加载异常:', error);
|
||||||
showDanger('验证码加载失败,点击刷新')
|
showDanger('验证码加载失败,点击刷新')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async login() {
|
async login() {
|
||||||
@@ -119,8 +123,12 @@ export default {
|
|||||||
Api.user.login(this.form, ({data}) => {
|
Api.user.login(this.form, ({data}) => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
showSuccess('登陆成功!')
|
showSuccess('登陆成功!')
|
||||||
|
|
||||||
|
this.$store.commit('setToken', JSON.stringify(data.data))
|
||||||
|
|
||||||
goToPage('/home')
|
goToPage('/home')
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.fetchCaptcha()
|
this.fetchCaptcha()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome">
|
||||||
|
<!-- 公共头部 -->
|
||||||
|
<HeaderBar/>
|
||||||
|
<el-main style="padding: 20px;display: flex;flex-direction: column;">
|
||||||
|
<div style="border-radius: 20px;background: #fafcfe;">
|
||||||
|
<div
|
||||||
|
style="padding: 19px 30px;font-weight: 700;font-size: 24px;text-align: left;color: #3d4566;display: flex;gap: 16px;align-items: center;">
|
||||||
|
<div
|
||||||
|
style="width: 46px;height: 46px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||||
|
<img src="@/assets/home/setting-user.png" alt="" style="width: 24px;height: 24px;"/>
|
||||||
|
</div>
|
||||||
|
{{ deviceMac }}
|
||||||
|
</div>
|
||||||
|
<div style="height: 1px;background: #e8f0ff;"/>
|
||||||
|
<el-form ref="form" :model="form" label-width="90px">
|
||||||
|
<div style="padding: 20px 30px;max-width: 990px;">
|
||||||
|
<el-form-item label="助手昵称:">
|
||||||
|
<div class="input-46">
|
||||||
|
<el-input v-model="form.name"/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色模版:">
|
||||||
|
<div style="display: flex;gap: 10px;">
|
||||||
|
<div class="template-item">
|
||||||
|
台湾女友
|
||||||
|
</div>
|
||||||
|
<div class="template-item">
|
||||||
|
土豆子
|
||||||
|
</div>
|
||||||
|
<div class="template-item">
|
||||||
|
英语老师
|
||||||
|
</div>
|
||||||
|
<div class="template-item">
|
||||||
|
好奇小男孩
|
||||||
|
</div>
|
||||||
|
<div class="template-item">
|
||||||
|
汪汪队队长
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色音色:">
|
||||||
|
<div style="display: flex;gap: 10px;align-items: center;">
|
||||||
|
<div class="input-46" style="flex:1.4;">
|
||||||
|
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
|
||||||
|
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||||
|
:value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="audio-box">
|
||||||
|
<audio src="http://music.163.com/song/media/outer/url?id=447925558.mp3" controls
|
||||||
|
style="height: 100%;width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色介绍:">
|
||||||
|
<div class="textarea-box">
|
||||||
|
<el-input type="textarea" rows="6" resize="none" placeholder="请输入内容"
|
||||||
|
v-model="form.introduction" maxlength="2000" show-word-limit/>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="记忆体:">
|
||||||
|
<div class="textarea-box">
|
||||||
|
<el-input type="textarea" rows="6" resize="none" placeholder="请输入内容"
|
||||||
|
v-model="form.prompt" maxlength="1000"/>
|
||||||
|
<div class="prompt-bottom">
|
||||||
|
<div style="display: flex;gap: 10px;align-items: center;">
|
||||||
|
<div style="color: #979db1;font-size: 14px;">当前记忆(每次对话后重新生成)</div>
|
||||||
|
<div class="clear-btn">
|
||||||
|
<i class="el-icon-delete-solid" style="font-size: 14px;"/>
|
||||||
|
清除
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="color: #979db1;font-size:14px;">{{ form.prompt.length }}/1000</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="语言模型(内测):" class="lh-form-item">
|
||||||
|
<div style="display: flex;gap: 10px;">
|
||||||
|
<div class="input-46" style="width: 100%;">
|
||||||
|
<el-select v-model="form.model" placeholder="请选择" style="width: 100%;">
|
||||||
|
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||||
|
:value="item.value">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="" class="lh-form-item">
|
||||||
|
<div style="color: #979db1;text-align: left;">除了“Qwen
|
||||||
|
实时”,其他模型通常会增加约1秒的延迟。改变模型后,建议清空记忆体,以免影响体验。
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
<div style="display: flex;padding: 20px;gap: 10px;align-items: center;">
|
||||||
|
<div class="save-btn" @click="saveConfig">
|
||||||
|
保存配置
|
||||||
|
</div>
|
||||||
|
<div class="reset-btn" @click="resetConfig">
|
||||||
|
重制
|
||||||
|
</div>
|
||||||
|
<div class="clear-text">
|
||||||
|
<img src="@/assets/home/red-info.png" alt="" style="width: 24px;height: 24px;"/>
|
||||||
|
保存配置后,需要重启设备,新的配置才会生效。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
|
||||||
|
©2025 xiaozhi-esp32-server
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RoleConfigPage',
|
||||||
|
components: {HeaderBar},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
deviceMac: 'CC:ba:97:11:a6:ac',
|
||||||
|
form: {
|
||||||
|
name: "",
|
||||||
|
timbre: "",
|
||||||
|
introduction: "",
|
||||||
|
prompt: "",
|
||||||
|
model: ""
|
||||||
|
},
|
||||||
|
options: [{
|
||||||
|
value: '选项1',
|
||||||
|
label: '黄金糕'
|
||||||
|
}, {
|
||||||
|
value: '选项2',
|
||||||
|
label: '双皮奶'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
saveConfig() {
|
||||||
|
// 此处写保存配置逻辑
|
||||||
|
this.$message.success('配置已保存')
|
||||||
|
},
|
||||||
|
resetConfig() {
|
||||||
|
this.$confirm('确定要重置配置吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
// 重置表单
|
||||||
|
this.form = {
|
||||||
|
name: "",
|
||||||
|
timbre: "",
|
||||||
|
introduction: "",
|
||||||
|
prompt: "",
|
||||||
|
model: ""
|
||||||
|
}
|
||||||
|
this.$message.success('配置已重置')
|
||||||
|
}).catch(() => {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.welcome {
|
||||||
|
min-width: 1200px;
|
||||||
|
min-height: 675px;
|
||||||
|
height: 100vh;
|
||||||
|
background-image: url("@/assets/home/background.png");
|
||||||
|
background-size: cover;
|
||||||
|
/* 确保背景图像覆盖整个元素 */
|
||||||
|
background-position: center;
|
||||||
|
/* 从顶部中心对齐 */
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
/* 兼容老版本WebKit浏览器 */
|
||||||
|
-o-background-size: cover;
|
||||||
|
/* 兼容老版本Opera浏览器 */
|
||||||
|
}
|
||||||
|
.audio-box {
|
||||||
|
flex: 1;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
width: 60px;
|
||||||
|
height: 24px;
|
||||||
|
background: #fd8383;
|
||||||
|
border-radius: 12px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-text {
|
||||||
|
color: #979db1;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-item {
|
||||||
|
height: 46px;
|
||||||
|
width: 100px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #e6ebff;
|
||||||
|
line-height: 46px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
color: #5778ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-bottom {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-46 {
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
background: #f6f8fb;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn,
|
||||||
|
.reset-btn {
|
||||||
|
width: 140px;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 23px;
|
||||||
|
line-height: 46px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
border-radius: 23px;
|
||||||
|
background: #5778ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
border: 1px solid #adbdff;
|
||||||
|
background: #e6ebff;
|
||||||
|
color: #5778ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-box {
|
||||||
|
border: 1px solid #e4e6ef;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f6f8fb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -51,13 +51,15 @@ xiaozhi:
|
|||||||
prompt: |
|
prompt: |
|
||||||
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||||
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
||||||
当前时间是:{date_time},现在我正在和你进行语音聊天,我们开始吧。
|
现在我正在和你进行语音聊天,我们开始吧。
|
||||||
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
|
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
|
||||||
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
||||||
delete_audio: true
|
delete_audio: true
|
||||||
|
|
||||||
# 没有语音输入多久后断开连接(秒),默认2分钟,即120秒
|
# 没有语音输入多久后断开连接(秒),默认2分钟,即120秒
|
||||||
close_connection_no_voice_time: 120
|
close_connection_no_voice_time: 120
|
||||||
|
# TTS请求超时时间(秒)
|
||||||
|
tts_timeout: 10
|
||||||
|
|
||||||
CMD_exit:
|
CMD_exit:
|
||||||
- "退出"
|
- "退出"
|
||||||
@@ -85,14 +87,20 @@ selected_module:
|
|||||||
Intent:
|
Intent:
|
||||||
# 不使用意图识别
|
# 不使用意图识别
|
||||||
nointent:
|
nointent:
|
||||||
# 不需要动
|
# 不需要动type
|
||||||
type: nointent
|
type: nointent
|
||||||
intent_llm:
|
intent_llm:
|
||||||
# 不需要动
|
# 不需要动type
|
||||||
type: intent_llm
|
type: intent_llm
|
||||||
function_call:
|
function_call:
|
||||||
# 不需要动
|
# 不需要动type
|
||||||
type: nointent
|
type: nointent
|
||||||
|
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
|
||||||
|
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
|
||||||
|
# 下面是加载查天气、角色切换的插件示例
|
||||||
|
functions:
|
||||||
|
- change_role
|
||||||
|
- get_weather
|
||||||
|
|
||||||
Memory:
|
Memory:
|
||||||
mem0ai:
|
mem0ai:
|
||||||
@@ -298,20 +306,24 @@ TTS:
|
|||||||
repetition_penalty: 1.35
|
repetition_penalty: 1.35
|
||||||
aux_ref_audio_paths: []
|
aux_ref_audio_paths: []
|
||||||
GPT_SOVITS_V3:
|
GPT_SOVITS_V3:
|
||||||
|
# 定义TTS API类型 GPT-SoVITS-v3lora-20250228
|
||||||
|
#启动tts方法:
|
||||||
|
#python api.py
|
||||||
type: gpt_sovits_v3
|
type: gpt_sovits_v3
|
||||||
url: "http://127.0.0.1:9880/tts"
|
url: "http://127.0.0.1:9880"
|
||||||
output_file: tmp/
|
output_file: tmp/
|
||||||
text_lang: "auto"
|
text_language: "auto"
|
||||||
ref_audio_path: "caixukun.wav"
|
refer_wav_path: "caixukun.wav"
|
||||||
prompt_lang: "zh"
|
prompt_language: "zh"
|
||||||
prompt_text: ""
|
prompt_text: ""
|
||||||
top_k: 5
|
top_k: 15
|
||||||
top_p: 1
|
top_p: 1.0
|
||||||
temperature: 1
|
temperature: 1.0
|
||||||
sample_steps: 16
|
cut_punc: ""
|
||||||
media_type: "wav"
|
speed: 1.0
|
||||||
streaming_mode: false
|
inp_refs: []
|
||||||
threshold: 30
|
sample_steps: 32
|
||||||
|
if_sr: false
|
||||||
MinimaxTTS:
|
MinimaxTTS:
|
||||||
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
|
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
|
||||||
# 平台地址:https://platform.minimaxi.com/
|
# 平台地址:https://platform.minimaxi.com/
|
||||||
@@ -412,6 +424,21 @@ TTS:
|
|||||||
# 语速范围0.25-4.0
|
# 语速范围0.25-4.0
|
||||||
speed: 1
|
speed: 1
|
||||||
output_file: tmp/
|
output_file: tmp/
|
||||||
|
CustomTTS:
|
||||||
|
# 自定义的TTS接口服务,请求参数可自定义
|
||||||
|
# 要求接口使用GET方式请求,并返回音频文件
|
||||||
|
type: custom
|
||||||
|
url: "http://127.0.0.1:9880/tts"
|
||||||
|
params: # 自定义请求参数
|
||||||
|
# text: "{prompt_text}" # {prompt_text}会被替换为实际的提示词内容
|
||||||
|
# speaker: jok老师
|
||||||
|
# speed: 1
|
||||||
|
# foo: bar
|
||||||
|
# testabc: 123456
|
||||||
|
headers: # 自定义请求头
|
||||||
|
# Authorization: Bearer xxxx
|
||||||
|
format: wav # 接口返回的音频格式
|
||||||
|
output_file: tmp/
|
||||||
# 模块测试配置
|
# 模块测试配置
|
||||||
module_test:
|
module_test:
|
||||||
test_sentences: # 自定义测试语句
|
test_sentences: # 自定义测试语句
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
FunctionCallConfig = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "handle_exit_intent",
|
|
||||||
"description": "当用户想结束对话或需要退出系统时调用",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"say_goodbye": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "和用户友好结束对话的告别语"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "play_music",
|
|
||||||
"description": "唱歌、听歌、播放音乐方法。比如用户说播放音乐,参数为:random,比如用户说播放两只老虎,参数为:两只老虎",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"song_name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "歌曲名称,如果没有指定具体歌名则为'random'"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["song_name"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -15,10 +15,12 @@ from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_fro
|
|||||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||||
from core.handle.sendAudioHandle import sendAudioMessage
|
from core.handle.sendAudioHandle import sendAudioMessage
|
||||||
from core.handle.receiveAudioHandle import handleAudioMessage
|
from core.handle.receiveAudioHandle import handleAudioMessage
|
||||||
from core.handle.intentHandler import Action, get_functions, handle_llm_function_call
|
from core.handle.functionHandler import FunctionHandler
|
||||||
|
from plugins_func.register import Action
|
||||||
from config.private_config import PrivateConfig
|
from config.private_config import PrivateConfig
|
||||||
from core.auth import AuthMiddleware, AuthenticationError
|
from core.auth import AuthMiddleware, AuthenticationError
|
||||||
from core.utils.auth_code_gen import AuthCodeGenerator
|
from core.utils.auth_code_gen import AuthCodeGenerator
|
||||||
|
import plugins_func.loadplugins
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
@@ -94,6 +96,8 @@ class ConnectionHandler:
|
|||||||
self.use_function_call_mode = False
|
self.use_function_call_mode = False
|
||||||
if self.config["selected_module"]["Intent"] == 'function_call':
|
if self.config["selected_module"]["Intent"] == 'function_call':
|
||||||
self.use_function_call_mode = True
|
self.use_function_call_mode = True
|
||||||
|
|
||||||
|
self.func_handler = FunctionHandler(self.config)
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
async def handle_connection(self, ws):
|
||||||
try:
|
try:
|
||||||
@@ -185,11 +189,14 @@ class ConnectionHandler:
|
|||||||
self.prompt = self.config["prompt"]
|
self.prompt = self.config["prompt"]
|
||||||
if self.private_config:
|
if self.private_config:
|
||||||
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
||||||
# 赋予LLM时间观念
|
|
||||||
if "{date_time}" in self.prompt:
|
|
||||||
date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
|
||||||
self.prompt = self.prompt.replace("{date_time}", date_time)
|
|
||||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
self.dialogue.put(Message(role="system", content=self.prompt))
|
||||||
|
|
||||||
|
def change_system_prompt(self, prompt):
|
||||||
|
self.prompt = prompt
|
||||||
|
# 找到原来的role==system,替换原来的系统提示
|
||||||
|
for m in self.dialogue.dialogue:
|
||||||
|
if m.role == "system":
|
||||||
|
m.content = prompt
|
||||||
|
|
||||||
async def _check_and_broadcast_auth_code(self):
|
async def _check_and_broadcast_auth_code(self):
|
||||||
"""检查设备绑定状态并广播认证码"""
|
"""检查设备绑定状态并广播认证码"""
|
||||||
@@ -289,7 +296,7 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def chat_with_function_calling(self, query):
|
def chat_with_function_calling(self, query, tool_call = False):
|
||||||
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
|
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
|
||||||
"""Chat with function calling for intent detection using streaming"""
|
"""Chat with function calling for intent detection using streaming"""
|
||||||
if self.isNeedAuth():
|
if self.isNeedAuth():
|
||||||
@@ -297,11 +304,12 @@ class ConnectionHandler:
|
|||||||
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
|
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
|
||||||
future.result()
|
future.result()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.dialogue.put(Message(role="user", content=query))
|
if not tool_call:
|
||||||
|
self.dialogue.put(Message(role="user", content=query))
|
||||||
|
|
||||||
# Define intent functions
|
# Define intent functions
|
||||||
functions = get_functions()
|
functions = self.func_handler.get_functions()
|
||||||
|
|
||||||
response_message = []
|
response_message = []
|
||||||
processed_chars = 0 # 跟踪已处理的字符位置
|
processed_chars = 0 # 跟踪已处理的字符位置
|
||||||
@@ -337,7 +345,7 @@ class ConnectionHandler:
|
|||||||
for response in llm_responses:
|
for response in llm_responses:
|
||||||
content, tools_call = response
|
content, tools_call = response
|
||||||
if content is not None and len(content)>0:
|
if content is not None and len(content)>0:
|
||||||
if len(response_message)<=0 and content=="```":
|
if len(response_message)<=0 and (content=="```" or "<tool_call>" in content):
|
||||||
tool_call_flag = True
|
tool_call_flag = True
|
||||||
|
|
||||||
if tools_call is not None:
|
if tools_call is not None:
|
||||||
@@ -385,7 +393,38 @@ class ConnectionHandler:
|
|||||||
self.tts_queue.put(future)
|
self.tts_queue.put(future)
|
||||||
processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
||||||
|
|
||||||
# 处理最后剩余的文本
|
# 处理function call
|
||||||
|
if tool_call_flag:
|
||||||
|
bHasError = False
|
||||||
|
if function_id is None:
|
||||||
|
a = extract_json_from_string(content_arguments)
|
||||||
|
if a is not None:
|
||||||
|
try:
|
||||||
|
content_arguments_json = json.loads(a)
|
||||||
|
function_name = content_arguments_json["name"]
|
||||||
|
function_arguments = json.dumps(content_arguments_json["arguments"], ensure_ascii=False)
|
||||||
|
function_id = str(uuid.uuid4().hex)
|
||||||
|
except Exception as e:
|
||||||
|
bHasError = True
|
||||||
|
response_message.append(a)
|
||||||
|
else:
|
||||||
|
bHasError = True
|
||||||
|
response_message.append(content_arguments)
|
||||||
|
if bHasError:
|
||||||
|
self.logger.bind(tag=TAG).error(f"function call error: {content_arguments}")
|
||||||
|
else:
|
||||||
|
function_arguments = json.loads(function_arguments)
|
||||||
|
if not bHasError:
|
||||||
|
self.logger.bind(tag=TAG).info(f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}")
|
||||||
|
function_call_data = {
|
||||||
|
"name": function_name,
|
||||||
|
"id": function_id,
|
||||||
|
"arguments": function_arguments
|
||||||
|
}
|
||||||
|
result = self.func_handler.handle_llm_function_call(self, function_call_data)
|
||||||
|
self._handle_function_result(result, function_call_data, text_index+1)
|
||||||
|
|
||||||
|
# 处理最后剩余的文本
|
||||||
full_text = "".join(response_message)
|
full_text = "".join(response_message)
|
||||||
remaining_text = full_text[processed_chars:]
|
remaining_text = full_text[processed_chars:]
|
||||||
if remaining_text:
|
if remaining_text:
|
||||||
@@ -400,27 +439,6 @@ class ConnectionHandler:
|
|||||||
if len(response_message)>0:
|
if len(response_message)>0:
|
||||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
||||||
|
|
||||||
# 处理function call
|
|
||||||
if tool_call_flag:
|
|
||||||
if function_id is None:
|
|
||||||
a = extract_json_from_string(content_arguments)
|
|
||||||
if a is not None:
|
|
||||||
content_arguments_json = json.loads(a)
|
|
||||||
function_name = content_arguments_json["function_name"]
|
|
||||||
function_arguments = json.dumps(content_arguments_json["args"], ensure_ascii=False)
|
|
||||||
function_id = str(uuid.uuid4().hex)
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
function_arguments = json.loads(function_arguments)
|
|
||||||
self.logger.bind(tag=TAG).info(f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}")
|
|
||||||
function_call_data = {
|
|
||||||
"name": function_name,
|
|
||||||
"id": function_id,
|
|
||||||
"arguments": function_arguments
|
|
||||||
}
|
|
||||||
result = handle_llm_function_call(self, function_call_data)
|
|
||||||
self._handle_function_result(result, function_call_data, text_index+1)
|
|
||||||
|
|
||||||
self.llm_finish_task = True
|
self.llm_finish_task = True
|
||||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
||||||
|
|
||||||
@@ -434,7 +452,20 @@ class ConnectionHandler:
|
|||||||
self.tts_queue.put(future)
|
self.tts_queue.put(future)
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
if result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
if result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||||
text = result.response
|
|
||||||
|
text = result.result
|
||||||
|
if text is not None and len(text) > 0:
|
||||||
|
function_id = function_call_data["id"]
|
||||||
|
function_name = function_call_data["name"]
|
||||||
|
function_arguments = function_call_data["arguments"]
|
||||||
|
self.dialogue.put(Message(role='assistant',
|
||||||
|
tool_calls=[{"id": function_id,
|
||||||
|
"function": {"arguments": function_arguments,"name": function_name},
|
||||||
|
"type": 'function',
|
||||||
|
"index": 0}]))
|
||||||
|
|
||||||
|
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
|
||||||
|
self.chat_with_function_calling(text, tool_call=True)
|
||||||
if result.action == Action.NOTFOUND:
|
if result.action == Action.NOTFOUND:
|
||||||
text = result.response
|
text = result.response
|
||||||
|
|
||||||
@@ -451,7 +482,8 @@ class ConnectionHandler:
|
|||||||
opus_datas, text_index, tts_file = [], 0, None
|
opus_datas, text_index, tts_file = [], 0, None
|
||||||
try:
|
try:
|
||||||
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
||||||
tts_file, text, text_index = future.result(timeout=10)
|
tts_timeout = self.config.get("tts_timeout", 10)
|
||||||
|
tts_file, text, text_index = future.result(timeout=tts_timeout)
|
||||||
if text is None or len(text) <= 0:
|
if text is None or len(text) <= 0:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS出错:{text_index}: tts text is empty")
|
self.logger.bind(tag=TAG).error(f"TTS出错:{text_index}: tts text is empty")
|
||||||
elif tts_file is None:
|
elif tts_file is None:
|
||||||
@@ -459,7 +491,7 @@ class ConnectionHandler:
|
|||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}")
|
self.logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}")
|
||||||
if os.path.exists(tts_file):
|
if os.path.exists(tts_file):
|
||||||
opus_datas, duration = self.tts.wav_to_opus_data(tts_file)
|
opus_datas, duration = self.tts.audio_to_opus_data(tts_file)
|
||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}")
|
self.logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}")
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import asyncio
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from config.logger import setup_logging
|
||||||
|
import json
|
||||||
|
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionHandler:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.function_registry = FunctionRegistry()
|
||||||
|
self.register_nessary_functions()
|
||||||
|
self.register_config_functions()
|
||||||
|
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||||
|
func_names = self.current_support_functions()
|
||||||
|
self.modify_plugin_loader_des(func_names)
|
||||||
|
|
||||||
|
def modify_plugin_loader_des(self, func_names):
|
||||||
|
if "plugin_loader" not in func_names:
|
||||||
|
return
|
||||||
|
# 可编辑的列表中去掉plugin_loader
|
||||||
|
surport_plugins = [func for func in func_names if func != "plugin_loader"]
|
||||||
|
func_names = ",".join(surport_plugins)
|
||||||
|
for function_desc in self.functions_desc:
|
||||||
|
if function_desc["function"]["name"] == "plugin_loader":
|
||||||
|
function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names)
|
||||||
|
break
|
||||||
|
|
||||||
|
def upload_functions_desc(self):
|
||||||
|
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||||
|
|
||||||
|
def current_support_functions(self):
|
||||||
|
func_names = []
|
||||||
|
for func in self.functions_desc:
|
||||||
|
func_names.append(func["function"]["name"])
|
||||||
|
# 打印当前支持的函数列表
|
||||||
|
logger.bind(tag=TAG).info(f"当前支持的函数列表: {func_names}")
|
||||||
|
return func_names
|
||||||
|
|
||||||
|
def get_functions(self):
|
||||||
|
"""获取功能调用配置"""
|
||||||
|
return self.functions_desc
|
||||||
|
|
||||||
|
def register_nessary_functions(self):
|
||||||
|
"""注册必要的函数"""
|
||||||
|
self.function_registry.register_function("handle_exit_intent")
|
||||||
|
self.function_registry.register_function("play_music")
|
||||||
|
self.function_registry.register_function("plugin_loader")
|
||||||
|
self.function_registry.register_function("get_time")
|
||||||
|
|
||||||
|
def register_config_functions(self):
|
||||||
|
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||||
|
for func in self.config["Intent"]["function_call"].get("functions", []):
|
||||||
|
self.function_registry.register_function(func)
|
||||||
|
|
||||||
|
def get_function(self, name):
|
||||||
|
return self.function_registry.get_function(name)
|
||||||
|
|
||||||
|
def handle_llm_function_call(self, conn, function_call_data):
|
||||||
|
try:
|
||||||
|
function_name = function_call_data["name"]
|
||||||
|
funcItem = self.get_function(function_name)
|
||||||
|
if not funcItem:
|
||||||
|
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
|
||||||
|
func = funcItem.func
|
||||||
|
arguments = function_call_data["arguments"]
|
||||||
|
arguments = json.loads(arguments) if arguments else {}
|
||||||
|
logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}")
|
||||||
|
if funcItem.type == ToolType.SYSTEM_CTL:
|
||||||
|
return func(conn, **arguments)
|
||||||
|
elif funcItem.type == ToolType.WAIT:
|
||||||
|
return func(**arguments)
|
||||||
|
elif funcItem.type == ToolType.CHANGE_SYS_PROMPT:
|
||||||
|
return func(conn, **arguments)
|
||||||
|
else:
|
||||||
|
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -1,80 +1,12 @@
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import json
|
import json
|
||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
from core.utils.dialogue import Message
|
|
||||||
from core.utils.util import remove_punctuation_and_length
|
from core.utils.util import remove_punctuation_and_length
|
||||||
from config.functionCallConfig import FunctionCallConfig
|
|
||||||
import asyncio
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class Action(Enum):
|
|
||||||
NOTFOUND = (0, "没有找到函数")
|
|
||||||
NONE = (1, "啥也不干")
|
|
||||||
RESPONSE = (2, "直接回复")
|
|
||||||
REQLLM = (3, "调用函数后再请求llm生成回复")
|
|
||||||
|
|
||||||
def __init__(self, code, message):
|
|
||||||
self.code = code
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class ActionResponse:
|
|
||||||
def __init__(self, action: Action, result, response):
|
|
||||||
self.action = action # 动作类型
|
|
||||||
self.result = result # 动作产生的结果
|
|
||||||
self.response = response # 直接回复的内容
|
|
||||||
|
|
||||||
|
|
||||||
def get_functions():
|
|
||||||
"""获取功能调用配置"""
|
|
||||||
return FunctionCallConfig
|
|
||||||
|
|
||||||
|
|
||||||
def handle_llm_function_call(conn, function_call_data):
|
|
||||||
try:
|
|
||||||
function_name = function_call_data["name"]
|
|
||||||
|
|
||||||
if function_name == "handle_exit_intent":
|
|
||||||
# 处理退出意图
|
|
||||||
try:
|
|
||||||
say_goodbye = json.loads(function_call_data["arguments"]).get("say_goodbye", "再见")
|
|
||||||
conn.close_after_chat = True
|
|
||||||
logger.bind(tag=TAG).info(f"退出意图已处理:{say_goodbye}")
|
|
||||||
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=say_goodbye)
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"处理退出意图错误: {e}")
|
|
||||||
|
|
||||||
elif function_name == "play_music":
|
|
||||||
# 处理音乐播放意图
|
|
||||||
try:
|
|
||||||
song_name = "random"
|
|
||||||
arguments = function_call_data["arguments"]
|
|
||||||
if arguments is not None and len(arguments) > 0:
|
|
||||||
args = json.loads(arguments)
|
|
||||||
song_name = args.get("song_name", "random")
|
|
||||||
music_intent = f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
|
|
||||||
|
|
||||||
# 执行音乐播放命令
|
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
|
||||||
conn.music_handler.handle_music_command(conn, music_intent),
|
|
||||||
conn.loop
|
|
||||||
)
|
|
||||||
future.result()
|
|
||||||
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response="还想听什么歌?")
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
|
||||||
else:
|
|
||||||
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="没有找到对应的函数处理相对于的功能呢,你可以需要添加预设的对应函数处理呢")
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_user_intent(conn, text):
|
async def handle_user_intent(conn, text):
|
||||||
"""
|
"""
|
||||||
Handle user intent before starting chat
|
Handle user intent before starting chat
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class MusicHandler:
|
|||||||
if music_path.endswith(".p3"):
|
if music_path.endswith(".p3"):
|
||||||
opus_packets, duration = p3.decode_opus_from_file(music_path)
|
opus_packets, duration = p3.decode_opus_from_file(music_path)
|
||||||
else:
|
else:
|
||||||
opus_packets, duration = conn.tts.wav_to_opus_data(music_path)
|
opus_packets, duration = conn.tts.audio_to_opus_data(music_path)
|
||||||
conn.audio_play_queue.put((opus_packets, selected_music, 0))
|
conn.audio_play_queue.put((opus_packets, selected_music, 0))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ async def handleAudioMessage(conn, audio):
|
|||||||
# 如果本次没有声音,本段也没声音,就把声音丢弃了
|
# 如果本次没有声音,本段也没声音,就把声音丢弃了
|
||||||
if have_voice == False and conn.client_have_voice == False:
|
if have_voice == False and conn.client_have_voice == False:
|
||||||
await no_voice_close_connect(conn)
|
await no_voice_close_connect(conn)
|
||||||
conn.asr_audio.clear()
|
conn.asr_audio.append(audio)
|
||||||
|
conn.asr_audio = conn.asr_audio[-5:] # 保留最新的5帧音频内容,解决ASR句首丢字问题
|
||||||
return
|
return
|
||||||
conn.client_no_voice_last_time = 0.0
|
conn.client_no_voice_last_time = 0.0
|
||||||
conn.asr_audio.append(audio)
|
conn.asr_audio.append(audio)
|
||||||
@@ -29,7 +30,7 @@ async def handleAudioMessage(conn, audio):
|
|||||||
conn.client_abort = False
|
conn.client_abort = False
|
||||||
conn.asr_server_receive = False
|
conn.asr_server_receive = False
|
||||||
# 音频太短了,无法识别
|
# 音频太短了,无法识别
|
||||||
if len(conn.asr_audio) < 3:
|
if len(conn.asr_audio) < 10:
|
||||||
conn.asr_server_receive = True
|
conn.asr_server_receive = True
|
||||||
else:
|
else:
|
||||||
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ class LLMProvider(LLMProviderBase):
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.api_key = config["api_key"]
|
self.api_key = config["api_key"]
|
||||||
self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip('/')
|
self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip('/')
|
||||||
|
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
def response(self, session_id, dialogue):
|
||||||
try:
|
try:
|
||||||
# 取最后一条用户消息
|
# 取最后一条用户消息
|
||||||
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
|
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
|
||||||
|
conversation_id = self.session_conversation_map.get(session_id)
|
||||||
|
|
||||||
# 发起流式请求
|
# 发起流式请求
|
||||||
with requests.post(
|
with requests.post(
|
||||||
@@ -24,13 +26,18 @@ class LLMProvider(LLMProviderBase):
|
|||||||
"query": last_msg["content"],
|
"query": last_msg["content"],
|
||||||
"response_mode": "streaming",
|
"response_mode": "streaming",
|
||||||
"user": session_id,
|
"user": session_id,
|
||||||
"inputs": {}
|
"inputs": {},
|
||||||
|
"conversation_id": conversation_id
|
||||||
},
|
},
|
||||||
stream=True
|
stream=True
|
||||||
) as r:
|
) as r:
|
||||||
for line in r.iter_lines():
|
for line in r.iter_lines():
|
||||||
if line.startswith(b'data: '):
|
if line.startswith(b'data: '):
|
||||||
event = json.loads(line[6:])
|
event = json.loads(line[6:])
|
||||||
|
# 如果没有找到conversation_id,则获取此次conversation_id
|
||||||
|
if not conversation_id:
|
||||||
|
conversation_id = event.get('conversation_id')
|
||||||
|
self.session_conversation_map[session_id] = conversation_id # 更新映射
|
||||||
if event.get('answer'):
|
if event.get('answer'):
|
||||||
yield event['answer']
|
yield event['answer']
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ class LLMProvider(LLMProviderBase):
|
|||||||
self.model_name = config.get("model_name")
|
self.model_name = config.get("model_name")
|
||||||
self.base_url = config.get("base_url", "http://localhost:11434")
|
self.base_url = config.get("base_url", "http://localhost:11434")
|
||||||
# Initialize OpenAI client with Ollama base URL
|
# Initialize OpenAI client with Ollama base URL
|
||||||
#如果没有v1,增加v1
|
# 如果没有v1,增加v1
|
||||||
if not self.base_url.endswith("/v1"):
|
if not self.base_url.endswith("/v1"):
|
||||||
self.base_url = f"{self.base_url}/v1"
|
self.base_url = f"{self.base_url}/v1"
|
||||||
|
|
||||||
self.client = OpenAI(
|
self.client = OpenAI(
|
||||||
base_url=self.base_url,
|
base_url=self.base_url,
|
||||||
api_key="ollama" # Ollama doesn't need an API key but OpenAI client requires one
|
api_key="ollama" # Ollama doesn't need an API key but OpenAI client requires one
|
||||||
@@ -28,13 +28,20 @@ class LLMProvider(LLMProviderBase):
|
|||||||
messages=dialogue,
|
messages=dialogue,
|
||||||
stream=True
|
stream=True
|
||||||
)
|
)
|
||||||
|
is_active=True
|
||||||
for chunk in responses:
|
for chunk in responses:
|
||||||
try:
|
try:
|
||||||
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||||
content = delta.content if hasattr(delta, 'content') else ''
|
content = delta.content if hasattr(delta, 'content') else ''
|
||||||
if content:
|
if content:
|
||||||
yield content
|
if '<think>' in content:
|
||||||
|
is_active = False
|
||||||
|
content = content.split('<think>')[0]
|
||||||
|
if '</think>' in content:
|
||||||
|
is_active = True
|
||||||
|
content = content.split('</think>')[-1]
|
||||||
|
if is_active:
|
||||||
|
yield content
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
|
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
|
||||||
|
|
||||||
@@ -50,10 +57,10 @@ class LLMProvider(LLMProviderBase):
|
|||||||
stream=True,
|
stream=True,
|
||||||
tools=functions,
|
tools=functions,
|
||||||
)
|
)
|
||||||
|
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
||||||
yield {"type": "content", "content": f"【Ollama服务响应异常: {str(e)}】"}
|
yield {"type": "content", "content": f"【Ollama服务响应异常: {str(e)}】"}
|
||||||
|
|||||||
@@ -41,19 +41,20 @@ class TTSProviderBase(ABC):
|
|||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def wav_to_opus_data(self, wav_file_path):
|
def audio_to_opus_data(self, audio_file_path):
|
||||||
# 使用pydub加载PCM文件
|
"""音频文件转换为Opus编码"""
|
||||||
# 获取文件后缀名
|
# 获取文件后缀名
|
||||||
file_type = os.path.splitext(wav_file_path)[1]
|
file_type = os.path.splitext(audio_file_path)[1]
|
||||||
if file_type:
|
if file_type:
|
||||||
file_type = file_type.lstrip('.')
|
file_type = file_type.lstrip('.')
|
||||||
audio = AudioSegment.from_file(wav_file_path, format=file_type)
|
audio = AudioSegment.from_file(audio_file_path, format=file_type)
|
||||||
|
|
||||||
|
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
|
||||||
|
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
|
||||||
|
|
||||||
|
# 音频时长(秒)
|
||||||
duration = len(audio) / 1000.0
|
duration = len(audio) / 1000.0
|
||||||
|
|
||||||
# 转换为单声道和16kHz采样率(确保与编码器匹配)
|
|
||||||
audio = audio.set_channels(1).set_frame_rate(16000)
|
|
||||||
|
|
||||||
# 获取原始PCM数据(16位小端)
|
# 获取原始PCM数据(16位小端)
|
||||||
raw_data = audio.raw_data
|
raw_data = audio.raw_data
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import requests
|
||||||
|
from config.logger import setup_logging
|
||||||
|
from datetime import datetime
|
||||||
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
class TTSProvider(TTSProviderBase):
|
||||||
|
def __init__(self, config, delete_audio_file):
|
||||||
|
super().__init__(config, delete_audio_file)
|
||||||
|
self.url = config.get("url")
|
||||||
|
self.headers = config.get("headers", {})
|
||||||
|
self.params = config.get("params")
|
||||||
|
self.format = config.get("format", "wav")
|
||||||
|
self.output_file = config.get("output_file", "tmp/")
|
||||||
|
|
||||||
|
def generate_filename(self):
|
||||||
|
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}.{self.format}")
|
||||||
|
|
||||||
|
async def text_to_speak(self, text, output_file):
|
||||||
|
request_params = {}
|
||||||
|
for k, v in self.params.items():
|
||||||
|
if isinstance(v, str) and "{prompt_text}" in v:
|
||||||
|
v = v.replace("{prompt_text}", text)
|
||||||
|
request_params[k] = v
|
||||||
|
|
||||||
|
resp = requests.get(self.url, params=request_params, headers=self.headers)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
with open(output_file, "wb") as file:
|
||||||
|
file.write(resp.content)
|
||||||
|
else:
|
||||||
|
logger.bind(tag=TAG).error(f"Custom TTS请求失败: {resp.status_code} - {resp.text}")
|
||||||
@@ -12,17 +12,18 @@ 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.url = config.get("url")
|
self.url = config.get("url")
|
||||||
self.text_lang = config.get("text_lang", "audo")
|
self.refer_wav_path = config.get("refer_wav_path")
|
||||||
self.ref_audio_path = config.get("ref_audio_path")
|
|
||||||
self.prompt_lang = config.get("prompt_lang")
|
|
||||||
self.prompt_text = config.get("prompt_text")
|
self.prompt_text = config.get("prompt_text")
|
||||||
self.top_k = config.get("top_k", 5)
|
self.prompt_language = config.get("prompt_language")
|
||||||
self.top_p = config.get("top_p", 1)
|
self.text_language = config.get("text_language", "audo")
|
||||||
self.temperature = config.get("temperature", 1)
|
self.top_k = config.get("top_k", 15)
|
||||||
self.sample_steps = config.get("sample_steps", 16)
|
self.top_p = config.get("top_p", 1.0)
|
||||||
self.media_type = config.get("media_type", "wav")
|
self.temperature = config.get("temperature", 1.0)
|
||||||
self.streaming_mode = config.get("streaming_mode", False)
|
self.cut_punc = config.get("cut_punc","")
|
||||||
self.threshold = config.get("threshold", 30)
|
self.speed = config.get("speed", 1.0)
|
||||||
|
self.inp_refs = config.get("inp_refs",[])
|
||||||
|
self.sample_steps = config.get("inp_refs",32)
|
||||||
|
self.if_sr = config.get("if_sr",False)
|
||||||
|
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
@@ -30,18 +31,19 @@ class TTSProvider(TTSProviderBase):
|
|||||||
|
|
||||||
async def text_to_speak(self, text, output_file):
|
async def text_to_speak(self, text, output_file):
|
||||||
request_params = {
|
request_params = {
|
||||||
"text": text,
|
"refer_wav_path": self.refer_wav_path,
|
||||||
"text_lang": self.text_lang,
|
|
||||||
"ref_audio_path": self.ref_audio_path,
|
|
||||||
"prompt_lang": self.prompt_lang,
|
|
||||||
"prompt_text": self.prompt_text,
|
"prompt_text": self.prompt_text,
|
||||||
|
"prompt_language": self.prompt_language,
|
||||||
|
"text": text,
|
||||||
|
"text_language": self.text_language,
|
||||||
"top_k": self.top_k,
|
"top_k": self.top_k,
|
||||||
"top_p": self.top_p,
|
"top_p": self.top_p,
|
||||||
"temperature": self.temperature,
|
"temperature": self.temperature,
|
||||||
|
"cut_punc": self.cut_punc,
|
||||||
|
"speed": self.speed,
|
||||||
|
"inp_refs": self.inp_refs,
|
||||||
"sample_steps": self.sample_steps,
|
"sample_steps": self.sample_steps,
|
||||||
"media_type": self.media_type,
|
"if_sr": self.if_sr,
|
||||||
"streaming_mode": self.streaming_mode,
|
|
||||||
"threshold": self.threshold,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = requests.get(self.url, params=request_params)
|
resp = requests.get(self.url, params=request_params)
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
class Message:
|
class Message:
|
||||||
def __init__(self, role: str, content: str = None, uniq_id: str = None):
|
def __init__(self, role: str, content: str = None, uniq_id: str = None, tool_calls = None, tool_call_id=None):
|
||||||
self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4())
|
self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4())
|
||||||
self.role = role
|
self.role = role
|
||||||
self.content = content
|
self.content = content
|
||||||
|
self.tool_calls = tool_calls
|
||||||
|
self.tool_call_id = tool_call_id
|
||||||
|
|
||||||
|
|
||||||
class Dialogue:
|
class Dialogue:
|
||||||
@@ -19,10 +21,18 @@ class Dialogue:
|
|||||||
def put(self, message: Message):
|
def put(self, message: Message):
|
||||||
self.dialogue.append(message)
|
self.dialogue.append(message)
|
||||||
|
|
||||||
|
def getMessages(self, m, dialogue):
|
||||||
|
if m.tool_calls is not None:
|
||||||
|
dialogue.append({"role": m.role, "tool_calls": m.tool_calls})
|
||||||
|
elif m.role == "tool":
|
||||||
|
dialogue.append({"role": m.role, "tool_call_id": m.tool_call_id, "content": m.content})
|
||||||
|
else:
|
||||||
|
dialogue.append({"role": m.role, "content": m.content})
|
||||||
|
|
||||||
def get_llm_dialogue(self) -> List[Dict[str, str]]:
|
def get_llm_dialogue(self) -> List[Dict[str, str]]:
|
||||||
dialogue = []
|
dialogue = []
|
||||||
for m in self.dialogue:
|
for m in self.dialogue:
|
||||||
dialogue.append({"role": m.role, "content": m.content})
|
self.getMessages(m, dialogue)
|
||||||
return dialogue
|
return dialogue
|
||||||
|
|
||||||
def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]:
|
def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]:
|
||||||
@@ -46,8 +56,8 @@ class Dialogue:
|
|||||||
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
||||||
|
|
||||||
# 添加用户和助手的对话
|
# 添加用户和助手的对话
|
||||||
for msg in self.dialogue:
|
for m in self.dialogue:
|
||||||
if msg.role != "system": # 跳过原始的系统消息
|
if m.role != "system": # 跳过原始的系统消息
|
||||||
dialogue.append({"role": msg.role, "content": msg.content})
|
self.getMessages(m, dialogue)
|
||||||
|
|
||||||
return dialogue
|
return dialogue
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
import yaml
|
import yaml
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class WebSocketServer:
|
|||||||
host = server_config["ip"]
|
host = server_config["ip"]
|
||||||
port = server_config["port"]
|
port = server_config["port"]
|
||||||
selected_module = self.config.get("selected_module")
|
selected_module = self.config.get("selected_module")
|
||||||
self.logger.bind(tag=TAG).info(f"selected_module: {selected_module}")
|
self.logger.bind(tag=TAG).info(f"selected_module values: {', '.join(selected_module.values())}")
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
|
self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
|
||||||
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
|
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
from plugins_func.register import register_function,ToolType, ActionResponse, Action
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
prompts = {
|
||||||
|
"英语老师":"""我是一个叫{{assistant_name}}(Lily)的英语老师,我会讲中文和英文,发音标准。
|
||||||
|
如果你没有英文名,我会给你起一个英文名。
|
||||||
|
我会讲地道的美式英语,我的任务是帮助你练习口语。
|
||||||
|
我会使用简单的英语词汇和语法,让你学起来很轻松。
|
||||||
|
我会用中文和英文混合的方式回复你,如果你喜欢,我可以全部用英语回复。
|
||||||
|
我每次不会说很多内容,会很简短,因为我要引导我的学生多说多练。
|
||||||
|
如果你问和英语学习无关的问题,我会拒绝回答。""",
|
||||||
|
"机车女友":"""我是一个叫{{assistant_name}}的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||||
|
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
||||||
|
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。""",
|
||||||
|
"好奇小男孩":"""我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
|
||||||
|
尽管我年纪尚小,但就像一个小小的知识宝库,儿童读物里的知识我都如数家珍。
|
||||||
|
从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情。
|
||||||
|
我不仅爱看书,还喜欢亲自动手做实验,探索自然界的奥秘。
|
||||||
|
无论是仰望星空的夜晚,还是在花园里观察小虫子的日子,每一天对我来说都是新的冒险。
|
||||||
|
我希望能与你一同踏上探索这个神奇世界的旅程,分享发现的乐趣,解决遇到的难题,一起用好奇心和智慧去揭开那些未知的面纱。
|
||||||
|
无论是去了解远古的文明,还是去探讨未来的科技,我相信我们能一起找到答案,甚至提出更多有趣的问题。"""
|
||||||
|
}
|
||||||
|
change_role_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "change_role",
|
||||||
|
"description": "当用户想切换角色/模型性格/助手名字时调用,可选的角色有:[机车女友,英语老师,好奇小男孩]",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"role_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "要切换的角色名字"
|
||||||
|
},
|
||||||
|
"role":{
|
||||||
|
"type": "string",
|
||||||
|
"description": "要切换的角色的职业"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["role","role_name"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@register_function('change_role', change_role_function_desc, ToolType.CHANGE_SYS_PROMPT)
|
||||||
|
def change_role(conn, role: str, role_name: str):
|
||||||
|
"""切换角色"""
|
||||||
|
if role not in prompts:
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="切换角色失败", response="不支持的角色")
|
||||||
|
new_prompt = prompts[role].replace("{{assistant_name}}", role_name)
|
||||||
|
conn.change_system_prompt(new_prompt)
|
||||||
|
logger.bind(tag=TAG).info(f"准备切换角色:{role},角色名字:{role_name}")
|
||||||
|
res = f"切换角色成功,我是{role}{role_name}"
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="切换角色已处理", response=res)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
|
||||||
|
get_time_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_time",
|
||||||
|
"description": "获取当前时间、日期、星期几",
|
||||||
|
"parameters": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('get_time', get_time_function_desc, ToolType.WAIT)
|
||||||
|
def get_time():
|
||||||
|
"""
|
||||||
|
获取当前时间、日期、星期几
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
current_time = now.strftime("%H:%M:%S")
|
||||||
|
current_date = now.strftime("%Y-%m-%d")
|
||||||
|
current_weekday = now.strftime("%A")
|
||||||
|
response_text = f"当前日期: {current_date},当前时间: {current_time},星期: {current_weekday}"
|
||||||
|
|
||||||
|
return ActionResponse(Action.REQLLM, response_text, None)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from plugins_func.register import register_function,ToolType, ActionResponse, Action
|
||||||
|
|
||||||
|
|
||||||
|
get_weather_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"description": "获取某个地点的天气,用户应先提供一个位置,比如用户说杭州天气,参数为:zhejiang/hangzhou,比如用户说北京天气怎么样,参数为:beijing/beijing。如果用户只问天气怎么样,参数是:guangdong/guangzhou",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"city": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "城市,zhejiang/hangzhou"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"city"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@register_function('get_weather', get_weather_function_desc, ToolType.WAIT)
|
||||||
|
def get_weather(city: str):
|
||||||
|
"""
|
||||||
|
"获取某个地点的天气,用户应先提供一个位置,\n比如用户说杭州天气,参数为:zhejiang/hangzhou,\n\n比如用户说北京天气怎么样,参数为:beijing/beijing",
|
||||||
|
city : 城市,zhejiang/hangzhou
|
||||||
|
"""
|
||||||
|
url = "https://tianqi.moji.com/weather/china/"+city
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
if response.status_code!=200:
|
||||||
|
return ActionResponse(Action.REQLLM, None, "请求失败")
|
||||||
|
soup = BeautifulSoup(response.text, "html.parser")
|
||||||
|
weather = soup.find('meta', attrs={'name':'description'})["content"]
|
||||||
|
weather = weather.replace("墨迹天气", "")
|
||||||
|
return ActionResponse(Action.REQLLM, weather, None)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from plugins_func.register import register_function,ToolType, ActionResponse, Action
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
handle_exit_intent_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "handle_exit_intent",
|
||||||
|
"description": "当用户想结束对话或需要退出系统时调用",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"say_goodbye": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "和用户友好结束对话的告别语"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["say_goodbye"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@register_function('handle_exit_intent', handle_exit_intent_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def handle_exit_intent(conn, say_goodbye: str):
|
||||||
|
# 处理退出意图
|
||||||
|
try:
|
||||||
|
conn.close_after_chat = True
|
||||||
|
logger.bind(tag=TAG).info(f"退出意图已处理:{say_goodbye}")
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=say_goodbye)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理退出意图错误: {e}")
|
||||||
|
return ActionResponse(action=Action.NONE, result="退出意图处理失败", response="")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from plugins_func.register import register_function,ToolType, ActionResponse, Action
|
||||||
|
from config.logger import setup_logging
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
play_music_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "play_music",
|
||||||
|
"description": "唱歌、听歌、播放音乐方法。比如用户说播放音乐,参数为:random,比如用户说播放两只老虎,参数为:两只老虎",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"song_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "歌曲名称,如果没有指定具体歌名则为'random'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["song_name"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('play_music', play_music_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def play_music(conn, song_name: str):
|
||||||
|
try:
|
||||||
|
music_intent = f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
|
||||||
|
|
||||||
|
# 执行音乐播放命令
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
conn.music_handler.handle_music_command(conn, music_intent),
|
||||||
|
conn.loop
|
||||||
|
)
|
||||||
|
future.result()
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response="还想听什么歌?")
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
from plugins_func.register import register_function,ToolType, ActionResponse, Action
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
plugin_loader_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "plugin_loader",
|
||||||
|
"description": "当用户想加载或卸载插件/function时,调用此函数:支持的插件列表为[plugins]",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"oper": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "load or unload"
|
||||||
|
},
|
||||||
|
"name":{
|
||||||
|
"type": "string",
|
||||||
|
"description": "要加载或卸载的插件名字"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["oper","name"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@register_function('plugin_loader', plugin_loader_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def plugin_loader(conn, oper: str, name: str):
|
||||||
|
"""插件加载"""
|
||||||
|
if oper not in ["load", "unload"]:
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="插件操作失败", response="不支持的操作")
|
||||||
|
|
||||||
|
cur_support = conn.func_handler.current_support_functions()
|
||||||
|
if oper == "load":
|
||||||
|
if name in cur_support:
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="插件加载失败", response=f"{name}插件已加载,无需重复加载")
|
||||||
|
func = conn.func_handler.function_registry.register_function(name)
|
||||||
|
if not func:
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="插件加载失败", response="插件未找到")
|
||||||
|
res = f"{name}插件加载成功"
|
||||||
|
else:
|
||||||
|
if name not in cur_support:
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="插件卸载失败", response=f"{name}插件未加载")
|
||||||
|
bOK = conn.func_handler.function_registry.unregister_function(name)
|
||||||
|
if not bOK:
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="插件卸载失败", response="插件未找到")
|
||||||
|
res = f"{name}插件卸载成功"
|
||||||
|
conn.func_handler.upload_functions_desc()
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="插件操作成功", response=res)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import importlib
|
||||||
|
import pkgutil
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
def auto_import_modules(package_name):
|
||||||
|
"""
|
||||||
|
自动导入指定包内的所有模块。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_name (str): 包的名称,如 'functions'。
|
||||||
|
"""
|
||||||
|
# 获取包的路径
|
||||||
|
package = importlib.import_module(package_name)
|
||||||
|
package_path = package.__path__
|
||||||
|
|
||||||
|
# 遍历包内的所有模块
|
||||||
|
for _, module_name, _ in pkgutil.iter_modules(package_path):
|
||||||
|
# 导入模块
|
||||||
|
full_module_name = f"{package_name}.{module_name}"
|
||||||
|
importlib.import_module(full_module_name)
|
||||||
|
#logger.bind(tag=TAG).info(f"模块 '{full_module_name}' 已加载")
|
||||||
|
|
||||||
|
auto_import_modules('plugins_func.functions')
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from config.logger import setup_logging
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
class ToolType(Enum):
|
||||||
|
NONE = (1, "调用完工具后,不做其他操作")
|
||||||
|
WAIT = (2, "调用工具,等待函数返回")
|
||||||
|
CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责")
|
||||||
|
SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数")
|
||||||
|
|
||||||
|
def __init__(self, code, message):
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class Action(Enum):
|
||||||
|
NOTFOUND = (0, "没有找到函数")
|
||||||
|
NONE = (1, "啥也不干")
|
||||||
|
RESPONSE = (2, "直接回复")
|
||||||
|
REQLLM = (3, "调用函数后再请求llm生成回复")
|
||||||
|
|
||||||
|
def __init__(self, code, message):
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
class ActionResponse:
|
||||||
|
def __init__(self, action: Action, result, response):
|
||||||
|
self.action = action # 动作类型
|
||||||
|
self.result = result # 动作产生的结果
|
||||||
|
self.response = response # 直接回复的内容
|
||||||
|
|
||||||
|
class FunctionItem:
|
||||||
|
def __init__(self, name, description, func, type):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.func = func
|
||||||
|
self.type = type
|
||||||
|
|
||||||
|
# 初始化函数注册字典
|
||||||
|
all_function_registry = {}
|
||||||
|
|
||||||
|
def register_function(name, desc, type=None):
|
||||||
|
"""注册函数到函数注册字典的装饰器"""
|
||||||
|
def decorator(func):
|
||||||
|
all_function_registry[name] = FunctionItem(name, desc, func, type)
|
||||||
|
logger.bind(tag=TAG).debug(f"函数 '{name}' 已加载,可以注册使用")
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
class FunctionRegistry:
|
||||||
|
def __init__(self):
|
||||||
|
self.function_registry = {}
|
||||||
|
self.logger = setup_logging()
|
||||||
|
|
||||||
|
def register_function(self, name):
|
||||||
|
# 查找all_function_registry中是否有对应的函数
|
||||||
|
func = all_function_registry.get(name)
|
||||||
|
if not func:
|
||||||
|
self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到")
|
||||||
|
return None
|
||||||
|
self.function_registry[name] = func
|
||||||
|
self.logger.bind(tag=TAG).info(f"函数 '{name}' 注册成功")
|
||||||
|
return func
|
||||||
|
|
||||||
|
def unregister_function(self, name):
|
||||||
|
# 注销函数,检测是否存在
|
||||||
|
if name not in self.function_registry:
|
||||||
|
self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到")
|
||||||
|
return False
|
||||||
|
self.function_registry.pop(name, None)
|
||||||
|
self.logger.bind(tag=TAG).info(f"函数 '{name}' 注销成功")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_function(self, name):
|
||||||
|
return self.function_registry.get(name)
|
||||||
|
|
||||||
|
def get_all_functions(self):
|
||||||
|
return self.function_registry
|
||||||
|
|
||||||
|
def get_all_function_desc(self):
|
||||||
|
return [func.description for _, func in self.function_registry.items()]
|
||||||
@@ -18,4 +18,5 @@ ruamel.yaml==0.18.10
|
|||||||
loguru==0.7.3
|
loguru==0.7.3
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
cozepy==0.12.0
|
cozepy==0.12.0
|
||||||
mem0ai==0.1.62
|
mem0ai==0.1.62
|
||||||
|
bs4==0.0.2
|
||||||
Reference in New Issue
Block a user