Compare commits

...
23 Commits
Author SHA1 Message Date
hrzandGitHub eaaea2b24c update:智控台添加版本号 (#793) 2025-04-15 01:46:36 +08:00
58d0aefae1 fix:修复manager下发配置的bug (#792)
* update: 优化系统prompt

* Update Deployment_all.md

更新部分的安装配置文档,后续需要修改 docker-compose.yaml

* Update docker-compose_all.yml

- 直接提供全量的部署配置,包括 mysql 与 Redis 实现真正意义上的一键配置。
- 将 MySQL 的数据内容本地挂载存储,需要备份迁移的时候,直接打包复制文件夹即可。MySQL 容器删除的时候也不会出现数据丢失的问题。

* Update Deployment_all.md

- 使用 docker compose 部署的时候,不应该每次更新的时候都把原来的 docker-compose 文件删除,再重新配置。
- 如果没有自己部署数据库需求的话,目前可以直接开箱即用,同步更新了 server 中的 Redis 与 MySQL 的镜像服务配置。

* Update Deployment_all.md

- MySQL 与 Redis 的安装是可选的

* 现在新版本的 docker 直接集成了 compose 命令,不需要手动安装与执行 docker-compose 命令了,提高兼容性

* fix:manager下发配置字符转数字bug

* update: 优化三方模块打印日志报错问题

* update: 修复Dify对接未过滤全量数据问题

* update:通过代理转发接口api

* fix:number熟知去掉小数点后的00

* update:优化docker启动

* update:优化参数读取

---------

Co-authored-by: haotian <haotian@codemao.cn>
Co-authored-by: expoli <31023767+expoli@users.noreply.github.com>
Co-authored-by: GoodyHao <865700600@qq.com>
2025-04-15 00:46:37 +08:00
hrz 11c9915a70 update:优化参数读取 2025-04-15 00:44:34 +08:00
hrz 6cf6b0d645 update:优化docker启动 2025-04-15 00:22:53 +08:00
hrzandGitHub 1980a39b73 Merge pull request #763 from GOODDAYDAY/main
update: 优化系统prompt & 优化三方模块打印日志报错问题 & 修复Dify未过滤全量数据问题
2025-04-14 23:09:43 +08:00
hrzandGitHub 8d3fbeeb22 Merge pull request #782 from expoli/main
[docker] 更新了 docker-compose
2025-04-14 22:50:50 +08:00
hrzandGitHub 2678644c2c Merge branch 'hot-fix' into main 2025-04-14 22:50:42 +08:00
hrz 25da9fe119 fix:number熟知去掉小数点后的00 2025-04-14 22:09:18 +08:00
hrz 03d89d22ec update:通过代理转发接口api 2025-04-14 21:53:52 +08:00
haotian 100cd25ffc Merge remote-tracking branch 'origin/main' 2025-04-14 21:44:06 +08:00
haotian 6284a74a6e update: 修复Dify对接未过滤全量数据问题 2025-04-14 21:43:55 +08:00
GoodyHaoandGitHub 978cd8ba8e Merge branch 'xinnan-tech:main' into main 2025-04-14 18:51:15 +08:00
haotian c76f28824c update: 优化三方模块打印日志报错问题 2025-04-14 18:49:24 +08:00
hrz 703ecfae40 fix:manager下发配置字符转数字bug 2025-04-14 18:30:58 +08:00
expoliandGitHub 6e4e9a002b 现在新版本的 docker 直接集成了 compose 命令,不需要手动安装与执行 docker-compose 命令了,提高兼容性 2025-04-14 14:18:07 +08:00
expoliandGitHub 91278b0b3d Update Deployment_all.md
- MySQL 与 Redis 的安装是可选的
2025-04-14 14:16:25 +08:00
expoliandGitHub ff9b3ed1a2 Update Deployment_all.md
- 使用 docker compose 部署的时候,不应该每次更新的时候都把原来的 docker-compose 文件删除,再重新配置。
- 如果没有自己部署数据库需求的话,目前可以直接开箱即用,同步更新了 server 中的 Redis 与 MySQL 的镜像服务配置。
2025-04-14 14:15:37 +08:00
expoliandGitHub 0c855d4ba8 Update docker-compose_all.yml
- 直接提供全量的部署配置,包括 mysql 与 Redis 实现真正意义上的一键配置。
- 将 MySQL 的数据内容本地挂载存储,需要备份迁移的时候,直接打包复制文件夹即可。MySQL 容器删除的时候也不会出现数据丢失的问题。
2025-04-14 14:06:30 +08:00
expoliandGitHub 9176e9d255 Update Deployment_all.md
更新部分的安装配置文档,后续需要修改 docker-compose.yaml
2025-04-14 14:00:31 +08:00
hrzandGitHub 2bab917b3e update:优化文档 (#775)
* update:添加设计公司致谢

* update:添加设计公司致谢

* update:优化文档

* update:优化文档

* update:优化文档

* update:优化文档
2025-04-14 11:31:52 +08:00
hrzandGitHub 8a4fca9bd5 update:优化文档 (#774)
* update:添加设计公司致谢

* update:添加设计公司致谢

* update:优化文档

* update:优化文档
2025-04-14 10:35:14 +08:00
hrzandGitHub 13288a6b3a update:优化文档
* update:添加设计公司致谢

* update:添加设计公司致谢

* update:优化文档
2025-04-14 10:26:34 +08:00
haotian dcb6d0feab update: 优化系统prompt 2025-04-13 18:46:04 +08:00
39 changed files with 348 additions and 319 deletions
+2 -1
View File
@@ -158,4 +158,5 @@ my_wakeup_words.mp3
main/manager-api/.vscode main/manager-api/.vscode
# Ignore webpack cache directory # Ignore webpack cache directory
main/manager-web/.webpack_cache/ main/manager-web/.webpack_cache/
main/xiaozhi-server/mysql
+45 -69
View File
@@ -145,42 +145,24 @@ server:
--- ---
## 部署方式 🚀 ## 部署文档
[![Banners](docs/images/banner2.png)](./docs/Deployment.md) 本项目提供两种部署方式,请根据您的具体需求选择:
### 部署文档 #### 🚀 部署方式选择
本项目支持`最简化`安装和`全模块`安装。请根据具体需求选择安装方式。 | 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 |
|---------|------|---------|---------|---------|
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
`最简化`适合在较低资源配置中运行,无需数据库。[请参考-最简化安装架构图](./docs/images/deploy1.png) > 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
`全模块`适合想体验完整功能,过程复杂。[请参考-全模块安装架构图](./docs/images/deploy2.png)
#### 1、 最简化安装
`最简化`安装包含:智能对话、IOT等功能,数据存储在`配置文件`。我们提供了两种安装教程,如果你决定使用最简化安装,可选择`Docker只运行Server``本地源码只运行Server`任意一种方式。
##### 1.1. Docker只运行Server
你可以使用编译好的docker镜像安装,[点击Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)。优点:减少环境配置的烦恼;缺点:版本可能有点旧,镜像文件较大。
##### 2.2. 本地源码只运行Server
你可以以本地源码的方式运行,[点击本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)。优点:可以随时更新最新代码;缺点:安装Conda环境较为复杂。
#### 2、 全模块安装
`全模块`安装包含:智能对话、IOT、OTA、智控台,数据存储在`数据库`中。我们提供了两种安装教程,如果你决定使用全模块安装,可选择`Docker运行全模块``本地源码运行全模块`任意一种方式。
##### 2.1. Docker运行全模块
你可以使用编译好的docker镜像安装,[点击Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97)。优点:减少环境配置的烦恼;缺点:版本可能有点旧,镜像文件较大。
##### 2.2. 本地源码运行全模块
你可以以本地源码的方式运行,[点击本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97)。优点:可以随时更新最新代码;缺点:安装Java、Node.js、Conda环境较为复杂。
```
智控台地址: https://2662r3426b.vicp.fun
OTA接口地址: htts://2662r3426b.vicp.fun/xiaozhi/ota/
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
```
--- ---
## 常见问题 ❓ ## 常见问题 ❓
@@ -188,54 +170,41 @@ server:
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。 如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
--- ---
## 产品生态 👬
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
- [小智安卓客户端](https://github.com/TOM88812/xiaozhi-android-client)
一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。现在是flutter版本,打通IOS、Android端。
- [小智电脑客户端](https://github.com/Huang-junsen/py-xiaozhi)
该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,依然能够体过代码体验小智 AI 的功能。主要功能包括 AI 语音交互、视觉多模态识别、IoT 设备集成、联网音乐播放、语音唤醒、自动对话模式、图形化界面、命令行模式、跨平台支持、音量控制、会话管理、加密音频传输、自动验证码处理等。
- [小智Java服务端](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
小智开源后端服务 Java 版本是一个基于 Java 的开源项目,它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。
---
## 功能清单 ✨ ## 功能清单 ✨
### 已实现 ✅ ### 已实现 ✅
- **通信协议** | 功能模块 | 描述 |
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。 |---------|------|
- **对话交互** | 通信协议 | 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互 |
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 | 对话交互 | 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 |
- **意图识别** | 意图识别 | 支持使用LLM意图识别、function call函数调用,减少硬编码意图判断 |
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断 | 多语言识别 | 支持国语、粤语、英语、日语、韩语(默认使用 FunASR) |
- **多语言识别** | LLM 模块 | 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口 |
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。 | TTS 模块 | 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求 |
- **LLM 模块** | 记忆功能 | 支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求 |
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。 | IOT功能 | 支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制 |
- **TTS 模块** | 智控台 | 提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理 |
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
- **IOT功能**
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
- **智控台**
提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理。
### 正在开发 🚧 ### 正在开发 🚧
- 多种心情模式 想了解具体开发计划进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入! 如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
--- ---
## 产品生态 👬
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
| 项目名称 | 项目地址 | 项目描述 |
|:---------------------|:--------|:--------|
| 小智安卓客户端 | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | 一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。<br/>现在是flutter版本,打通IOS、Android端。 |
| 小智电脑客户端 | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | 该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,<br/>依然能够体过代码体验小智 AI 的功能。 |
| 小智Java服务端 | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | 小智开源后端服务 Java 版本是一个基于 Java 的开源项目。<br/>它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
---
## 本项目支持的平台/组件列表 📋 ## 本项目支持的平台/组件列表 📋
### LLM 语言模型 ### LLM 语言模型
@@ -298,10 +267,17 @@ server:
## 鸣谢 🙏 ## 鸣谢 🙏
- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。 | Logo | 项目/公司 | 说明 |
- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。 |:---:|:---:|:---|
| <img src="./docs/images/logo_bailing.png" width="160"> | [百聆语音对话机器人](https://github.com/wwbin2017/bailing) | 本项目受[百聆语音对话机器人](https://github.com/wwbin2017/bailing)启发,并在其基础上实现 |
| <img src="./docs/images/logo_tenclass.png" width="160"> | [十方融海](https://www.tenclass.com/) | 感谢[十方融海](https://www.tenclass.com/)为小智生态制定了标准的通讯协议、多设备兼容性方案及高并发场景实践示范;为本项目提供了全链路技术文档支持 |
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [玄凤科技](https://github.com/Eric0308) | 感谢[玄凤科技](https://github.com/Eric0308)贡献函数调用框架、MCP通信协议及插件化调用机制的实现代码,通过标准化的指令调度体系与动态扩展能力,显著提升了前端设备(IoT)的交互效率和功能延展性 |
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [汇远设计](http://ui.kwd988.net/) | 感谢[汇远设计](http://ui.kwd988.net/)为本项目提供专业视觉解决方案,用其服务超千家企业的设计实战经验,赋能本项目产品用户体验 |
| <img src="./docs/images/logo_qinren.png" width="160"> | [西安勤人信息科技](https://www.029app.com/) | 感谢[西安勤人信息科技](https://www.029app.com/)深化本项目视觉体系,确保整体设计风格在多场景应用中的一致性和扩展性 |
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date"> <a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" /> <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" /> <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
+4 -1
View File
@@ -137,7 +137,10 @@ docker logs -f xiaozhi-esp32-server
``` ```
docker stop xiaozhi-esp32-server docker stop xiaozhi-esp32-server
docker rm xiaozhi-esp32-server docker rm xiaozhi-esp32-server
docker stop xiaozhi-esp32-server-web
docker rm xiaozhi-esp32-server-web
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
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
``` ```
5.3、重新按docker方式部署 5.3、重新按docker方式部署
@@ -221,7 +224,7 @@ python app.py
## 配置项目 ## 配置项目
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。 如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml` 如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制`data`,并重命名为`.config.yaml`
修改`xiaozhi-server``data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。 修改`xiaozhi-server``data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
+36 -68
View File
@@ -44,6 +44,8 @@ xiaozhi-server
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的 在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
`xiaozhi-server`中。 `xiaozhi-server`中。
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/docker-compose_all.yml` 下载。
下载完后,回到本教程继续往下。 下载完后,回到本教程继续往下。
##### 1.3.2 下载 config_from_api.yaml ##### 1.3.2 下载 config_from_api.yaml
@@ -53,6 +55,8 @@ xiaozhi-server
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的 在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml` `xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/config_from_api.yaml` 下载保存。
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示: 下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
``` ```
@@ -67,57 +71,37 @@ xiaozhi-server
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。 如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
## 2. 安装Mysql和Redis ## 2. 备份数据
### 2.1 安装Mysql数据库 如果你之前已经成功运行智控台,如果上面保存有你的密钥信息,请先从智控台上拷贝重要数据下来。因为升级过程中,有可能会覆盖原来的数据。
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
```sql
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
如果还没有MySQL,你可以通过docker安装mysql
```
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
```
### 2.2 安装Redis
如果还没有Redis,你可以通过docker安装redis
```
docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
```
## 3. 配置docker-compose_all.yml文件文件
`xiaozhi-server`目录,打开docker-compose_all.yml
1、修改`SPRING_DATASOURCE_DRUID_URL`里,把`192.168.1.25`修改成部署mysql的电脑局域网ip,如果是你本地,你要查看一下你本机的局域网ip是什么
2、修改`SPRING_DATA_REDIS_HOST`里,把`192.168.1.25`修改成部署redis的电脑局域网ip,如果是你本地,你要查看一下你本机的局域网ip是什么
3、确认一下mysql的用户名`SPRING_DATASOURCE_DRUID_USERNAME`和密码`SPRING_DATASOURCE_DRUID_PASSWORD`
## 4. 运行程序
确认mysql和redis是否运行正常,输入
```
docker ps
```
如果你能看到`xiaozhi-esp32-server-redis``xiaozhi-esp32-server-db`信息,就可以继续往下,如果看不到,说明你的`mysql``redis`没有安装或没有启动。需要继续回到上面的教程,看看哪一步漏了。
```
CONTAINER ID IMAGE COMMAND CREATED TATUS PORTS NAMES
xxx redis "xx" xxx xxx 0.0.0.0:6379->6379/tcp xiaozhi-esp32-server-redis
xxx mysql:latest "xx" xxx xxx 0.0.0.0:3306->3306/tcp, 33060/tcp xiaozhi-esp32-server-db
```
## 3. 清除历史版本镜像和容器
接下来打开命令行工具,使用`终端``命令行`工具 进入到你的`xiaozhi-server`,执行以下命令 接下来打开命令行工具,使用`终端``命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
``` ```
docker-compose -f docker-compose_all.yml up -d docker compose -f docker-compose_all.yml down
docker stop xiaozhi-esp32-server
docker rm xiaozhi-esp32-server
docker stop xiaozhi-esp32-server-web
docker rm xiaozhi-esp32-server-web
docker stop xiaozhi-esp32-server-db
docker rm xiaozhi-esp32-server-db
docker stop xiaozhi-esp32-server-redis
docker rm xiaozhi-esp32-server-redis
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
```
## 4. 运行程序
执行以下命令启动新版本容器
```
docker compose -f docker-compose_all.yml up -d
``` ```
执行完后,再执行以下命令,查看日志信息。 执行完后,再执行以下命令,查看日志信息。
@@ -150,17 +134,16 @@ http://localhost:8002/xiaozhi/doc.html
``` ```
manager-api: manager-api:
url: http://127.0.0.1:8002/xiaozhi url: http://127.0.0.1:8002/xiaozhi
secret: 你的server.secret值 secret: 你的server.secret值
``` ```
1、把你刚才从`智控台`复制过来的`server.secret``参数值`复制到`.config.yaml`文件里的`secret`里。 1、把你刚才从`智控台`复制过来的`server.secret``参数值`复制到`.config.yaml`文件里的`secret`里。
2、注意,把`url`改成下面的`http://xiaozhi-esp32-server-web/xiaozhi`
2、把`url`里的`127.0.0.1`改成你电脑局域网内的ip
类似这样的效果 类似这样的效果
``` ```
manager-api: manager-api:
url: http://192.168.1.25:8002/xiaozhi url: http://xiaozhi-esp32-server-web/xiaozhi
secret: 12345678-xxxx-xxxx-xxxx-123456789000 secret: 12345678-xxxx-xxxx-xxxx-123456789000
``` ```
@@ -202,24 +185,6 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。 接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
## 6. 版本升级操作
如果后期想升级版本,先备份你的模型密钥。
进入`xiaozhi-server`目录
5.1、执行以下命令
```
docker-compose down
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
```
5.2、删掉`docker-compose_all.yaml`文件
5.3、重新按1.1开始部署
# 方式二:本地源码运行全模块 # 方式二:本地源码运行全模块
## 1.安装MySQL数据库 ## 1.安装MySQL数据库
@@ -388,6 +353,9 @@ pip install -r requirements.txt
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api``server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。 `server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api``server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config_from_api.yaml`文件复制到`data`,并重命名为`.config.yaml`
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的: 复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
``` ```
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -171,7 +171,13 @@ public class ConfigServiceImpl implements ConfigService {
switch (param.getValueType().toLowerCase()) { switch (param.getValueType().toLowerCase()) {
case "number": case "number":
try { try {
current.put(lastKey, Double.parseDouble(value)); double doubleValue = Double.parseDouble(value);
// 如果数值是整数形式,则转换为Integer
if (doubleValue == (int) doubleValue) {
current.put(lastKey, (int) doubleValue);
} else {
current.put(lastKey, doubleValue);
}
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
current.put(lastKey, value); current.put(lastKey, value);
} }
@@ -76,6 +76,7 @@ public class ShiroConfig {
filterMap.put("/favicon.ico", "anon"); filterMap.put("/favicon.ico", "anon");
filterMap.put("/user/captcha", "anon"); filterMap.put("/user/captcha", "anon");
filterMap.put("/user/login", "anon"); filterMap.put("/user/login", "anon");
filterMap.put("/user/pub-config", "anon");
filterMap.put("/user/register", "anon"); filterMap.put("/user/register", "anon");
filterMap.put("/config/server-base", "anon"); filterMap.put("/config/server-base", "anon");
filterMap.put("/config/agent-models", "anon"); filterMap.put("/config/agent-models", "anon");
@@ -1,6 +1,8 @@
package xiaozhi.modules.security.controller; package xiaozhi.modules.security.controller;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -111,4 +113,12 @@ public class LoginController {
sysUserTokenService.changePassword(userId, passwordDTO); sysUserTokenService.changePassword(userId, passwordDTO);
return new Result<>(); return new Result<>();
} }
@GetMapping("/pub-config")
@Operation(summary = "公共配置")
public Result<Map<String, Object>> pubConfig() {
Map<String, Object> config = new HashMap<>();
config.put("version", "0.3.3");
return new Result<Map<String, Object>>().ok(config);
}
} }
+1 -4
View File
@@ -1,4 +1 @@
# 暂时使用群主的接口 VUE_APP_API_BASE_URL=/xiaozhi
# VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun/xiaozhi
# 如果本地开发,请使用以下接口
VUE_APP_API_BASE_URL=http://localhost:8002/xiaozhi
+17 -1
View File
@@ -106,7 +106,7 @@ export default {
}, },
// 修改用户状态 // 修改用户状态
changeUserStatus(status, userIds, successCallback) { changeUserStatus(status, userIds, successCallback) {
console.log(555,userIds) console.log(555, userIds)
RequestService.sendRequest() RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/users/changeStatus/${status}`) .url(`${getServiceUrl()}/admin/users/changeStatus/${status}`)
.method('put') .method('put')
@@ -122,4 +122,20 @@ export default {
}) })
}).send() }).send()
}, },
// 获取公共配置
getPubConfig(callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/pub-config`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('获取公共配置失败:', err);
RequestService.reAjaxFun(() => {
this.getPubConfig(callback);
});
}).send();
},
} }
@@ -0,0 +1,39 @@
<template>
<div class="copyright">
©2025 xiaozhi-esp32-server v{{ version }}
</div>
</template>
<script>
import Api from '@/apis/api';
export default {
name: 'VersionFooter',
data() {
return {
version: ''
}
},
mounted() {
this.getSystemVersion();
},
methods: {
getSystemVersion() {
const storedVersion = sessionStorage.getItem('systemVersion');
if (storedVersion) {
this.version = storedVersion;
return;
}
Api.user.getPubConfig(({ data }) => {
if (data.code === 0 && data.data.version) {
this.version = data.data.version;
sessionStorage.setItem('systemVersion', data.data.version);
}
});
}
}
}
</script>
<style scoped></style>
@@ -4,7 +4,8 @@
<el-main style="padding: 20px; display: flex; flex-direction: column;"> <el-main style="padding: 20px; display: flex; flex-direction: column;">
<div class="table-container"> <div class="table-container">
<h3 class="device-list-title">设备列表</h3> <h3 class="device-list-title">设备列表</h3>
<el-table ref="deviceTable" :data="paginatedDeviceList" @selection-change="handleSelectionChange" style="width: 100%; margin-top: 20px" border stripe> <el-table ref="deviceTable" :data="paginatedDeviceList" @selection-change="handleSelectionChange"
style="width: 100%; margin-top: 20px" border stripe>
<el-table-column type="selection" align="center" width="60"></el-table-column> <el-table-column type="selection" align="center" width="60"></el-table-column>
<el-table-column label="设备型号" prop="model" flex></el-table-column> <el-table-column label="设备型号" prop="model" flex></el-table-column>
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column> <el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
@@ -40,22 +41,17 @@
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="toggleAllSelection"> <el-button size="mini" type="primary" class="select-all-btn" @click="toggleAllSelection">
{{ isAllSelected ? '取消全选' : '全选' }} {{ isAllSelected ? '取消全选' : '全选' }}
</el-button> </el-button>
<el-button type="primary" size="mini" class="add-device-btn" @click="handleAddDevice"> <el-button type="primary" size="mini" class="add-device-btn" @click="handleAddDevice">
新增 新增
</el-button> </el-button>
</div> </div>
<div class="custom-pagination"> <div class="custom-pagination">
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button> <button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button> <button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
<button <button v-for="page in visiblePages" :key="page" class="pagination-btn"
v-for="page in visiblePages" :class="{ active: page === currentPage }" @click="goToPage(page)">
:key="page"
class="pagination-btn"
:class="{ active: page === currentPage }"
@click="goToPage(page)"
>
{{ page }} {{ page }}
</button> </button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
@@ -63,9 +59,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId" <AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
@refresh="fetchBindDevices(currentAgentId)" /> @refresh="fetchBindDevices(currentAgentId)" />
</el-main> </el-main>
@@ -82,7 +75,7 @@ export default {
data() { data() {
return { return {
addDeviceDialogVisible: false, addDeviceDialogVisible: false,
selectedDevices: [], selectedDevices: [],
isAllSelected: false, isAllSelected: false,
currentAgentId: this.$route.query.agentId || '', currentAgentId: this.$route.query.agentId || '',
currentPage: 1, currentPage: 1,
@@ -99,7 +92,7 @@ export default {
return this.deviceList.slice(start, end); return this.deviceList.slice(start, end);
}, },
pageCount() { pageCount() {
return Math.ceil(this.deviceList.length / this.pageSize); return Math.ceil(this.deviceList.length / this.pageSize);
}, },
visiblePages() { visiblePages() {
const pages = []; const pages = [];
@@ -346,6 +339,4 @@ export default {
color: white; color: white;
} }
} }
</style> </style>
+10 -34
View File
@@ -3,21 +3,15 @@
<HeaderBar /> <HeaderBar />
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">{{ modelTypeText }}</h2> <h2 class="page-title">{{ modelTypeText }}</h2>
<div class="action-group"> <div class="action-group">
<div class="search-group"> <div class="search-group">
<el-input <el-input placeholder="请输入模型名称查询" v-model="search" class="search-input" clearable
placeholder="请输入模型名称查询" @keyup.enter.native="handleSearch" style="width: 240px" />
v-model="search" <el-button class="btn-search" @click="handleSearch">
class="search-input" 搜索
clearable </el-button>
@keyup.enter.native="handleSearch" </div>
style="width: 240px"
/>
<el-button class="btn-search" @click="handleSearch">
搜索
</el-button>
</div>
</div> </div>
</div> </div>
@@ -93,9 +87,9 @@
<div class="table-footer"> <div class="table-footer">
<div class="batch-actions"> <div class="batch-actions">
<el-button size="mini" type="primary" @click="selectAll" > <el-button size="mini" type="primary" @click="selectAll">
{{ isAllSelected ? {{ isAllSelected ?
'取消全选' : '全选' }} '取消全选' : '全选' }}
</el-button> </el-button>
<el-button type="success" size="mini" @click="addModel" class="add-btn"> <el-button type="success" size="mini" @click="addModel" class="add-btn">
新增 新增
@@ -125,10 +119,6 @@
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" /> <TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" />
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" /> <AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
</div> </div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
</div> </div>
</template> </template>
@@ -639,20 +629,6 @@ export default {
gap: 8px; gap: 8px;
} }
.copyright {
text-align: center;
color: #979db1;
font-size: 12px;
font-weight: 400;
margin-top: auto;
padding: 30px 0 20px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
}
.title-wrapper { .title-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -64,8 +64,6 @@
<!-- 新增/编辑参数对话框 --> <!-- 新增/编辑参数对话框 -->
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit" <param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
@cancel="dialogVisible = false" /> @cancel="dialogVisible = false" />
<div class="copyright">©2025 xiaozhi-esp32-server</div>
</div> </div>
</template> </template>
@@ -382,20 +380,6 @@ export default {
} }
} }
.copyright {
text-align: center;
color: #979db1;
font-size: 12px;
font-weight: 400;
margin-top: auto;
padding: 30px 0 20px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
}
.custom-pagination { .custom-pagination {
display: flex; display: flex;
align-items: center; align-items: center;
+1 -17
View File
@@ -42,7 +42,7 @@
<div class="table_bottom"> <div class="table_bottom">
<div class="ctrl_btn"> <div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll"> <el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? '取消全选' : '全选' }} {{ isAllSelected ? '取消全选' : '全选' }}
</el-button> </el-button>
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button> <el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
<el-button size="mini" type="warning" @click="batchDisable"><i <el-button size="mini" type="warning" @click="batchDisable"><i
@@ -69,8 +69,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="copyright">©2025 xiaozhi-esp32-server</div>
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" /> <view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
</div> </div>
</template> </template>
@@ -465,20 +463,6 @@ export default {
color: black; color: black;
} }
.copyright {
text-align: center;
color: #979db1;
font-size: 12px;
font-weight: 400;
margin-top: auto;
padding: 30px 0 20px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
}
.custom-pagination { .custom-pagination {
display: flex; display: flex;
align-items: center; align-items: center;
+5 -4
View File
@@ -35,11 +35,11 @@
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" /> @deviceManage="handleDeviceManage" @delete="handleDeleteAgent" />
</div> </div>
</div> </div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" /> <AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
</el-main> </el-main>
<el-footer>
<version-footer />
</el-footer>
</div> </div>
</template> </template>
@@ -49,10 +49,11 @@ import Api from '@/apis/api';
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue'; import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
import DeviceItem from '@/components/DeviceItem.vue'; import DeviceItem from '@/components/DeviceItem.vue';
import HeaderBar from '@/components/HeaderBar.vue'; import HeaderBar from '@/components/HeaderBar.vue';
import VersionFooter from '@/components/VersionFooter.vue';
export default { export default {
name: 'HomePage', name: 'HomePage',
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar }, components: { DeviceItem, AddWisdomBodyDialog, HeaderBar, VersionFooter },
data() { data() {
return { return {
addDeviceDialogVisible: false, addDeviceDialogVisible: false,
+23 -5
View File
@@ -51,9 +51,7 @@
</div> </div>
</el-main> </el-main>
<el-footer> <el-footer>
<div class="copyright"> <version-footer />
©2025 xiaozhi-esp32-server
</div>
</el-footer> </el-footer>
</el-container> </el-container>
</div> </div>
@@ -61,11 +59,14 @@
<script> <script>
import Api from '@/apis/api'; import Api from '@/apis/api';
import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils'; import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
export default { export default {
name: 'login', name: 'login',
components: {
VersionFooter
},
data() { data() {
return { return {
activeName: "username", activeName: "username",
@@ -76,11 +77,13 @@ export default {
captchaId: '' captchaId: ''
}, },
captchaUuid: '', captchaUuid: '',
captchaUrl: '' captchaUrl: '',
version: ''
} }
}, },
mounted() { mounted() {
this.fetchCaptcha(); this.fetchCaptcha();
this.getSystemVersion();
}, },
methods: { methods: {
fetchCaptcha() { fetchCaptcha() {
@@ -144,6 +147,21 @@ export default {
goToRegister() { goToRegister() {
goToPage('/register') goToPage('/register')
},
getSystemVersion() {
const storedVersion = sessionStorage.getItem('systemVersion');
if (storedVersion) {
this.version = storedVersion;
return;
}
Api.user.getPubConfig(({ data }) => {
if (data.code === 0 && data.data.version) {
this.version = data.data.version;
sessionStorage.setItem('systemVersion', data.data.version);
}
});
} }
} }
} }
+5 -3
View File
@@ -72,9 +72,7 @@
<!-- 保持相同的页脚 --> <!-- 保持相同的页脚 -->
<el-footer> <el-footer>
<div class="copyright"> <version-footer />
©2025 xiaozhi-esp32-server
</div>
</el-footer> </el-footer>
</el-container> </el-container>
</div> </div>
@@ -82,10 +80,14 @@
<script> <script>
import Api from '@/apis/api'; import Api from '@/apis/api';
import VersionFooter from '@/components/VersionFooter.vue';
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils'; import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
export default { export default {
name: 'register', name: 'register',
components: {
VersionFooter
},
data() { data() {
return { return {
form: { form: {
@@ -81,9 +81,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="copyright">
©2025 xiaozhi-esp32-server
</div>
</el-main> </el-main>
</div> </div>
</template> </template>
+4
View File
@@ -15,6 +15,10 @@ module.exports = defineConfig({
devServer: { devServer: {
port: 8001, // 指定端口为 8001 port: 8001, // 指定端口为 8001
proxy: { proxy: {
'/xiaozhi': {
target: 'http://127.0.0.1:8002',
changeOrigin: true
}
}, },
client: { client: {
overlay: false, // 不显示 webpack 错误覆盖层 overlay: false, // 不显示 webpack 错误覆盖层
+14 -3
View File
@@ -3,7 +3,7 @@ import sys
from loguru import logger from loguru import logger
from config.config_loader import load_config from config.config_loader import load_config
SERVER_VERSION = "0.3.1" SERVER_VERSION = "0.3.3"
def get_module_abbreviation(module_name, module_dict): def get_module_abbreviation(module_name, module_dict):
@@ -25,6 +25,12 @@ def build_module_string(selected_module):
) )
def formatter(record):
"""为没有 tag 的日志添加默认值"""
record["extra"].setdefault("tag", record["name"])
return record["message"]
def setup_logging(): def setup_logging():
"""从配置文件中读取日志配置,并设置日志输出格式和级别""" """从配置文件中读取日志配置,并设置日志输出格式和级别"""
config = load_config() config = load_config()
@@ -56,9 +62,14 @@ def setup_logging():
logger.remove() logger.remove()
# 输出到控制台 # 输出到控制台
logger.add(sys.stdout, format=log_format, level=log_level) logger.add(sys.stdout, format=log_format, level=log_level, filter=formatter)
# 输出到文件 # 输出到文件
logger.add(os.path.join(log_dir, log_file), format=log_format_file, level=log_level) logger.add(
os.path.join(log_dir, log_file),
format=log_format_file,
level=log_level,
filter=formatter,
)
return logger return logger
+4 -6
View File
@@ -204,7 +204,7 @@ class ConnectionHandler:
self.headers.get("device-id", None), self.headers.get("device-id", None),
self.headers.get("client-id", None), self.headers.get("client-id", None),
) )
private_config["delete_audio"] = self.config["delete_audio"] private_config["delete_audio"] = bool(self.config.get("delete_audio", True))
self.logger.bind(tag=TAG).info(f"获取差异化配置成功: {private_config}") self.logger.bind(tag=TAG).info(f"获取差异化配置成功: {private_config}")
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}") self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
@@ -335,10 +335,8 @@ class ConnectionHandler:
def change_system_prompt(self, prompt): def change_system_prompt(self, prompt):
self.prompt = prompt self.prompt = prompt
# 找到原来的role==system,替换原来的系统提示 # 更新系统prompt至上下文
for m in self.dialogue.dialogue: self.dialogue.update_system_message(self.prompt)
if m.role == "system":
m.content = prompt
def chat(self, query): def chat(self, query):
@@ -702,7 +700,7 @@ 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_timeout = self.config.get("tts_timeout", 10) tts_timeout = int(self.config.get("tts_timeout", 10))
tts_file, text, text_index = future.result(timeout=tts_timeout) 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( self.logger.bind(tag=TAG).error(
@@ -71,8 +71,8 @@ async def no_voice_close_connect(conn):
conn.client_no_voice_last_time = time.time() * 1000 conn.client_no_voice_last_time = time.time() * 1000
else: else:
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
close_connection_no_voice_time = conn.config.get( close_connection_no_voice_time = int(
"close_connection_no_voice_time", 120 conn.config.get("close_connection_no_voice_time", 120)
) )
if ( if (
not conn.close_after_chat not conn.close_after_chat
@@ -58,7 +58,8 @@ class LLMProvider(LLMProviderBase):
self.session_conversation_map[session_id] = ( self.session_conversation_map[session_id] = (
conversation_id # 更新映射 conversation_id # 更新映射
) )
if event.get("answer"): # 过滤 message_replace 事件,此事件会全量推一次
if event.get("event") != "message_replace" and event.get("answer"):
yield event["answer"] yield event["answer"]
elif self.mode == "workflows/run": elif self.mode == "workflows/run":
for line in r.iter_lines(): for line in r.iter_lines():
@@ -73,7 +74,8 @@ class LLMProvider(LLMProviderBase):
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:])
if event.get("answer"): # 过滤 message_replace 事件,此事件会全量推一次
if event.get("event") != "message_replace" and event.get("answer"):
yield event["answer"] yield event["answer"]
except Exception as e: except Exception as e:
@@ -89,19 +89,19 @@ class TTSProvider(TTSProviderBase):
self.reference_audio = config.get("reference_audio", []) self.reference_audio = config.get("reference_audio", [])
self.reference_text = config.get("reference_text", []) self.reference_text = config.get("reference_text", [])
self.format = config.get("format", "wav") self.format = config.get("format", "wav")
self.channels = config.get("channels", 1) self.channels = int(config.get("channels", 1))
self.rate = config.get("rate", 44100) self.rate = int(config.get("rate", 44100))
self.api_key = config.get("api_key", "YOUR_API_KEY") self.api_key = config.get("api_key", "YOUR_API_KEY")
have_key = check_model_key("FishSpeech TTS", self.api_key) have_key = check_model_key("FishSpeech TTS", self.api_key)
if not have_key: if not have_key:
return return
self.normalize = config.get("normalize", True) self.normalize = config.get("normalize", True)
self.max_new_tokens = config.get("max_new_tokens", 1024) self.max_new_tokens = int(config.get("max_new_tokens", 1024))
self.chunk_length = config.get("chunk_length", 200) self.chunk_length = int(config.get("chunk_length", 200))
self.top_p = config.get("top_p", 0.7) self.top_p = float(config.get("top_p", 0.7))
self.repetition_penalty = config.get("repetition_penalty", 1.2) self.repetition_penalty = float(config.get("repetition_penalty", 1.2))
self.temperature = config.get("temperature", 0.7) self.temperature = float(config.get("temperature", 0.7))
self.streaming = config.get("streaming", False) self.streaming = bool(config.get("streaming", False))
self.use_memory_cache = config.get("use_memory_cache", "on") self.use_memory_cache = config.get("use_memory_cache", "on")
self.seed = config.get("seed") self.seed = config.get("seed")
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts") self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
@@ -10,6 +10,7 @@ from core.providers.tts.base import TTSProviderBase
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
class TTSProvider(TTSProviderBase): 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)
@@ -18,23 +19,26 @@ class TTSProvider(TTSProviderBase):
self.ref_audio_path = config.get("ref_audio_path") self.ref_audio_path = config.get("ref_audio_path")
self.prompt_text = config.get("prompt_text") self.prompt_text = config.get("prompt_text")
self.prompt_lang = config.get("prompt_lang", "zh") self.prompt_lang = config.get("prompt_lang", "zh")
self.top_k = config.get("top_k", 5) self.top_k = int(config.get("top_k", 5))
self.top_p = config.get("top_p", 1) self.top_p = float(config.get("top_p", 1))
self.temperature = config.get("temperature", 1) self.temperature = float(config.get("temperature", 1))
self.text_split_method = config.get("text_split_method", "cut0") self.text_split_method = config.get("text_split_method", "cut0")
self.batch_size = config.get("batch_size", 1) self.batch_size = int(config.get("batch_size", 1))
self.batch_threshold = config.get("batch_threshold", 0.75) self.batch_threshold = float(config.get("batch_threshold", 0.75))
self.split_bucket = config.get("split_bucket", True) self.split_bucket = bool(config.get("split_bucket", True))
self.return_fragment = config.get("return_fragment", False) self.return_fragment = bool(config.get("return_fragment", False))
self.speed_factor = config.get("speed_factor", 1.0) self.speed_factor = float(config.get("speed_factor", 1.0))
self.streaming_mode = config.get("streaming_mode", False) self.streaming_mode = bool(config.get("streaming_mode", False))
self.seed = config.get("seed", -1) self.seed = int(config.get("seed", -1))
self.parallel_infer = config.get("parallel_infer", True) self.parallel_infer = bool(config.get("parallel_infer", True))
self.repetition_penalty = config.get("repetition_penalty", 1.35) self.repetition_penalty = float(config.get("repetition_penalty", 1.35))
self.aux_ref_audio_paths = config.get("aux_ref_audio_paths", []) self.aux_ref_audio_paths = config.get("aux_ref_audio_paths", [])
def generate_filename(self, extension=".wav"): def generate_filename(self, extension=".wav"):
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}") return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
request_json = { request_json = {
@@ -56,7 +60,7 @@ class TTSProvider(TTSProviderBase):
"streaming_mode": self.streaming_mode, "streaming_mode": self.streaming_mode,
"seed": self.seed, "seed": self.seed,
"parallel_infer": self.parallel_infer, "parallel_infer": self.parallel_infer,
"repetition_penalty": self.repetition_penalty "repetition_penalty": self.repetition_penalty,
} }
resp = requests.post(self.url, json=request_json) resp = requests.post(self.url, json=request_json)
@@ -64,4 +68,6 @@ class TTSProvider(TTSProviderBase):
with open(output_file, "wb") as file: with open(output_file, "wb") as file:
file.write(resp.content) file.write(resp.content)
else: else:
logger.bind(tag=TAG).error(f"GPT_SoVITS_V2 TTS请求失败: {resp.status_code} - {resp.text}") logger.bind(tag=TAG).error(
f"GPT_SoVITS_V2 TTS请求失败: {resp.status_code} - {resp.text}"
)
@@ -8,6 +8,7 @@ from core.providers.tts.base import TTSProviderBase
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
class TTSProvider(TTSProviderBase): 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)
@@ -16,18 +17,20 @@ class TTSProvider(TTSProviderBase):
self.prompt_text = config.get("prompt_text") self.prompt_text = config.get("prompt_text")
self.prompt_language = config.get("prompt_language") self.prompt_language = config.get("prompt_language")
self.text_language = config.get("text_language", "audo") self.text_language = config.get("text_language", "audo")
self.top_k = config.get("top_k", 15) self.top_k = int(config.get("top_k", 15))
self.top_p = config.get("top_p", 1.0) self.top_p = float(config.get("top_p", 1.0))
self.temperature = config.get("temperature", 1.0) self.temperature = float(config.get("temperature", 1.0))
self.cut_punc = config.get("cut_punc","") self.cut_punc = config.get("cut_punc", "")
self.speed = config.get("speed", 1.0) self.speed = float(config.get("speed", 1.0))
self.inp_refs = config.get("inp_refs",[]) self.inp_refs = config.get("inp_refs", [])
self.sample_steps = config.get("sample_steps",32) self.sample_steps = int(config.get("sample_steps", 32))
self.if_sr = config.get("if_sr",False) self.if_sr = bool(config.get("if_sr", False))
def generate_filename(self, extension=".wav"): def generate_filename(self, extension=".wav"):
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}") return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
request_params = { request_params = {
@@ -51,4 +54,6 @@ class TTSProvider(TTSProviderBase):
with open(output_file, "wb") as file: with open(output_file, "wb") as file:
file.write(resp.content) file.write(resp.content)
else: else:
logger.bind(tag=TAG).error(f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}") logger.bind(tag=TAG).error(
f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}"
)
@@ -21,7 +21,7 @@ class TTSProvider(TTSProviderBase):
else: else:
self.voice = config.get("voice", "alloy") self.voice = config.get("voice", "alloy")
self.response_format = "wav" self.response_format = "wav"
self.speed = config.get("speed", 1.0) self.speed = float(config.get("speed", 1.0))
self.output_file = config.get("output_dir", "tmp/") self.output_file = config.get("output_dir", "tmp/")
check_model_key("TTS", self.api_key) check_model_key("TTS", self.api_key)
@@ -16,7 +16,7 @@ class TTSProvider(TTSProviderBase):
self.voice = config.get("voice") self.voice = config.get("voice")
self.response_format = config.get("response_format") self.response_format = config.get("response_format")
self.sample_rate = config.get("sample_rate") self.sample_rate = config.get("sample_rate")
self.speed = config.get("speed") self.speed = float(config.get("speed"))
self.gain = config.get("gain") self.gain = config.get("gain")
self.host = "api.siliconflow.cn" self.host = "api.siliconflow.cn"
@@ -19,7 +19,7 @@ class TTSProvider(TTSProviderBase):
if config.get("private_voice"): if config.get("private_voice"):
self.voice = config.get("private_voice") self.voice = config.get("private_voice")
else: else:
self.voice = config.get("voice") self.voice = int(config.get("voice"))
self.api_url = "https://tts.tencentcloudapi.com" # 正确的API端点 self.api_url = "https://tts.tencentcloudapi.com" # 正确的API端点
self.region = config.get("region") self.region = config.get("region")
self.output_file = config.get("output_dir") self.output_file = config.get("output_dir")
@@ -20,13 +20,13 @@ class TTSProvider(TTSProviderBase):
self.voice_id = int(config.get("voice_id", 1695)) self.voice_id = int(config.get("voice_id", 1695))
self.token = config.get("token") self.token = config.get("token")
self.to_lang = config.get("to_lang") self.to_lang = config.get("to_lang")
self.volume_change_dB = config.get("volume_change_dB", 0) self.volume_change_dB = int(config.get("volume_change_dB", 0))
self.speed_factor = config.get("speed_factor", 1) self.speed_factor = int(config.get("speed_factor", 1))
self.stream = config.get("stream", False) self.stream = bool(config.get("stream", False))
self.output_file = config.get("output_dir") self.output_file = config.get("output_dir")
self.pitch_factor = config.get("pitch_factor", 0) self.pitch_factor = int(config.get("pitch_factor", 0))
self.format = config.get("format", "mp3") self.format = config.get("format", "mp3")
self.emotion = config.get("emotion", 1) self.emotion = int(config.get("emotion", 1))
self.header = {"Content-Type": "application/json"} self.header = {"Content-Type": "application/json"}
def generate_filename(self, extension=".mp3"): def generate_filename(self, extension=".mp3"):
@@ -21,8 +21,8 @@ class VADProvider(VADProviderBase):
(get_speech_timestamps, _, _, _, _) = self.utils (get_speech_timestamps, _, _, _, _) = self.utils
self.decoder = opuslib_next.Decoder(16000, 1) self.decoder = opuslib_next.Decoder(16000, 1)
self.vad_threshold = config.get("threshold") self.vad_threshold = float(config.get("threshold", 0.5))
self.silence_threshold_ms = config.get("min_silence_duration_ms") self.silence_threshold_ms = int(config.get("min_silence_duration_ms", 1000))
def is_vad(self, conn, opus_packet): def is_vad(self, conn, opus_packet):
try: try:
+2 -2
View File
@@ -230,7 +230,7 @@ def initialize_modules(
modules["tts"] = tts.create_instance( modules["tts"] = tts.create_instance(
tts_type, tts_type,
config["TTS"][config["selected_module"]["TTS"]], config["TTS"][config["selected_module"]["TTS"]],
config["delete_audio"], bool(config.get("delete_audio", True)),
) )
logger.bind(tag=TAG).info(f"初始化组件: tts成功") logger.bind(tag=TAG).info(f"初始化组件: tts成功")
@@ -294,7 +294,7 @@ def initialize_modules(
modules["asr"] = asr.create_instance( modules["asr"] = asr.create_instance(
asr_type, asr_type,
config["ASR"][config["selected_module"]["ASR"]], config["ASR"][config["selected_module"]["ASR"]],
config["delete_audio"], bool(config.get("delete_audio", True)),
) )
logger.bind(tag=TAG).info(f"初始化组件: asr成功") logger.bind(tag=TAG).info(f"初始化组件: asr成功")
+1 -1
View File
@@ -25,7 +25,7 @@ class WebSocketServer:
async def start(self): async def start(self):
server_config = self.config["server"] server_config = self.config["server"]
host = server_config["ip"] host = server_config["ip"]
port = server_config["port"] port = int(server_config.get("port", 8000))
self.logger.bind(tag=TAG).info( self.logger.bind(tag=TAG).info(
"Server is running at ws://{}:{}/xiaozhi/v1/", get_local_ip(), port "Server is running at ws://{}:{}/xiaozhi/v1/", get_local_ip(), port
+48 -15
View File
@@ -1,25 +1,17 @@
# Docker安装全模块 # Docker安装全模块
# 1、安装mysql
# |- 如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
# |- 如果还没有MySQL,你可以通过docker安装mysql,执行以下一句话
# |- docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
# |- 记得修改下方SPRING_DATASOURCE_DRUID_URL的IPip不能写127.0.0.1或localhost,否则容器无法访问,要写你电脑局域网ip
# 2、安装redis
# |- 如果本机已经安装了Redis,看一下你安装的redis端口、密码,然后修改下方redis的地址和端口
# |- 如果还没有Redis,你可以通过docker安装redis,执行以下一句话
# |- docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
# |- 记得修改SPRING_DATA_REDIS_HOST的IPip不能写127.0.0.1或localhost,否则容器无法访问,要写你电脑局域网ip
version: '3' version: '3'
services: services:
# Server模块 # Server模块
xiaozhi-esp32-server: xiaozhi-esp32-server:
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
container_name: xiaozhi-esp32-server container_name: xiaozhi-esp32-server
depends_on:
- xiaozhi-esp32-server-db
- xiaozhi-esp32-server-redis
restart: always restart: always
networks:
- default
ports: ports:
# ws服务端 # ws服务端
- "8000:8000" - "8000:8000"
@@ -38,14 +30,55 @@ services:
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
container_name: xiaozhi-esp32-server-web container_name: xiaozhi-esp32-server-web
restart: always restart: always
networks:
- default
depends_on:
xiaozhi-esp32-server-db:
condition: service_healthy
xiaozhi-esp32-server-redis:
condition: service_healthy
ports: ports:
# 智控台 # 智控台
- "8002:8002" - "8002:8002"
environment: environment:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
##记得改mysql和redis IP 密码 ##记得改mysql和redis IP 密码
- SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://192.168.1.25:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai - SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://xiaozhi-esp32-server-db:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
- SPRING_DATASOURCE_DRUID_USERNAME=root - SPRING_DATASOURCE_DRUID_USERNAME=root
- SPRING_DATASOURCE_DRUID_PASSWORD=123456 - SPRING_DATASOURCE_DRUID_PASSWORD=123456
- SPRING_DATA_REDIS_HOST=192.168.1.25 - SPRING_DATA_REDIS_HOST=xiaozhi-esp32-server-redis
- SPRING_DATA_REDIS_PORT=6379 - SPRING_DATA_REDIS_PORT=6379
xiaozhi-esp32-server-db:
image: mysql:latest
container_name: xiaozhi-esp32-server-db
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
timeout: 45s
interval: 10s
retries: 10
restart: always
networks:
- default
expose:
- "3306:3306"
volumes:
- ./mysql/data:/var/lib/mysql
environment:
- TZ=Asia/Shanghai
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_DATABASE=xiaozhi_esp32_server
- MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
xiaozhi-esp32-server-redis:
image: redis
expose:
- 6379
container_name: xiaozhi-esp32-server-redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- default
networks:
default: