Compare commits

...
17 Commits
Author SHA1 Message Date
f9472627f4 Hot fix (#255)
* update:增加意图识别、记忆功能

* update:根据新版本更新教程文档

* update:更新配置说明

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-10 00:49:14 +08:00
69735207e6 update:根据新版本更新教程文档 (#252)
* update:增加意图识别、记忆功能

* update:根据新版本更新教程文档

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 22:33:45 +08:00
3b47f18a12 update:增加意图识别、记忆功能 (#251)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 21:59:15 +08:00
63f34e5a82 本地记忆+意图识别 (#250)
* 增加本地记忆功能,使用llm总结记忆

* update:增加统一非流式输出输出

* 增加意图识别内容,使用llm进行识别

* 初始化记忆模块

* 完善意图识别处理后的流程

* 通过使用function call实现意图识别

* update:优化意图识别的配置

* update:function call最优设置成doubao-pro-32k-functioncall-241028

---------

Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: hrz <1710360675@qq.com>
2025-03-09 21:33:45 +08:00
9c2b2a2dcc Block docker web (#248)
* update:docker智控台设置提示

* 修复启动报错问题 (#247)

修复启动时SyntaxWarning: invalid escape sequence '\]'
  half_width_punctuations = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'的报错问题

* 2025-3-8-iot状态更新 (#243)

* 添加 GPTSoVits-V3 TTS (#234)

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: vonweller <154695311+vonweller@users.noreply.github.com>
Co-authored-by: Jiao Haoyang <108573524+XuSenfeng@users.noreply.github.com>
Co-authored-by: Echo <yowfung@163.com>
2025-03-09 01:02:37 +08:00
baff979ab4 update: 增加docker 统一打包 (#232)
* update: 增加docker 统一打包

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

* update:测试新版镜像自动化编译

---------

Co-authored-by: hrz <1710360675@qq.com>
2025-03-08 21:11:30 +08:00
07fe330e4f Manager web (#239)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

* aliyunTTS常联token

* 新增aliyunTTS长期Token方式

* 获取用户信息和已绑设备

* update:验证码,登录,注册

* update:去掉依赖错误代码

* update:去除验证码重复服务类

* update:删除重复验证码服务类

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
Co-authored-by: Ken <ulxiping@qq.com>
2025-03-07 22:32:35 +08:00
f98c3efef7 Manager web (#238)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

* aliyunTTS常联token

* 新增aliyunTTS长期Token方式

* 获取用户信息和已绑设备

* update:验证码,登录,注册

* update:去掉依赖错误代码

* update:去除验证码重复服务类

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
Co-authored-by: Ken <ulxiping@qq.com>
2025-03-07 22:28:11 +08:00
8b74dec910 Manager web (#237)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

* aliyunTTS常联token

* 新增aliyunTTS长期Token方式

* 获取用户信息和已绑设备

* update:验证码,登录,注册

* update:去掉依赖错误代码

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
Co-authored-by: Ken <ulxiping@qq.com>
2025-03-07 21:19:41 +08:00
dc4b8a5002 perft:记忆 (#231)
1:优化异常处理

Co-authored-by: 李彬 <01780391@yto.net.cn>
2025-03-07 18:44:08 +08:00
+2 dd5fecdf5a Merge old pr (#235)
* 新增openai tts (#137)

On branch openai_tts
Changes to be committed:
	modified:   config.yaml
	new file:   core/providers/tts/openai_tts.py

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

* Update config.yaml (#201)

* 修改了coze的实现方式,使用了v3接口,修复了只能对话一次的bug

* 使用mem0ai api实现记忆功能

* Fix docker space (#179)

* update:调试docker编译缓存

* update:调试docker编译

* update:调试docker编译容量

* update:调试docker编译

---------

Co-authored-by: hrz <1710360675@qq.com>

* TTS处理增加index信息,完善TTS出错时的处理 (#177)

* TTS处理增加index信息,完善TTS出错时的处理

* 第一句语音index处理,歌曲播放index处理

* update:去掉了无用变量,修复continuebug

---------

Co-authored-by: hrz <1710360675@qq.com>

* update:更新demo (#180)

Co-authored-by: hrz <1710360675@qq.com>

* 优化chat中异步调用方式

* 记忆增加时间,以便大模型判断先后关系

* 记忆对时间戳排序,便于梳理前后关系

* add: 不跟踪music下的文件的更改 (#187)

* Mem0ai (#199)

* update:增加mem0ai包依赖

* update:文档增加mem0ai的描述

---------

Co-authored-by: hrz <1710360675@qq.com>

* Update config.yaml

* Update config.yaml

* Update config.yaml

* Update config.yaml

---------

Co-authored-by: strix214 <a1425699467@gmail.com>
Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: HonestQiao <honestqiao@163.com>
Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: TOM88812 <ttangxs0808@163.com>
Co-authored-by: 香草味的纳西妲 <151599587+NyaOH-Nahida@users.noreply.github.com>

* update:添加忽略文件

* feat: 增加fastgpt适配:支持think、variables参数 (#213)

* 修改了coze的实现方式,使用了v3接口,修复了只能对话一次的bug

* 使用mem0ai api实现记忆功能

* Fix docker space (#179)

* update:调试docker编译缓存

* update:调试docker编译

* update:调试docker编译容量

* update:调试docker编译

---------

Co-authored-by: hrz <1710360675@qq.com>

* TTS处理增加index信息,完善TTS出错时的处理 (#177)

* TTS处理增加index信息,完善TTS出错时的处理

* 第一句语音index处理,歌曲播放index处理

* update:去掉了无用变量,修复continuebug

---------

Co-authored-by: hrz <1710360675@qq.com>

* update:更新demo (#180)

Co-authored-by: hrz <1710360675@qq.com>

* 优化chat中异步调用方式

* 记忆增加时间,以便大模型判断先后关系

* 记忆对时间戳排序,便于梳理前后关系

* add: 不跟踪music下的文件的更改 (#187)

* Mem0ai (#199)

* update:增加mem0ai包依赖

* update:文档增加mem0ai的描述

---------

Co-authored-by: hrz <1710360675@qq.com>

* feat: 增加fastgpt适配:支持think、variables参数

---------

Co-authored-by: strix214 <a1425699467@gmail.com>
Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: HonestQiao <honestqiao@163.com>
Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>
Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: TOM88812 <ttangxs0808@163.com>
Co-authored-by: 香草味的纳西妲 <151599587+NyaOH-Nahida@users.noreply.github.com>
Co-authored-by: pt <ptsghs@163.com>

* update:合并最新代码

* update:合并群友pr

* update:openaitts 测试通过

* update:ACGNTTS测试完成

---------

Co-authored-by: Chris <119588753+Chris-websketch@users.noreply.github.com>
Co-authored-by: ikun441 <3497689533@qq.com>
Co-authored-by: strix214 <a1425699467@gmail.com>
Co-authored-by: 玄凤科技 <eric230308@gmail.com>
Co-authored-by: HonestQiao <honestqiao@163.com>
Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: TOM88812 <ttangxs0808@163.com>
Co-authored-by: 香草味的纳西妲 <151599587+NyaOH-Nahida@users.noreply.github.com>
Co-authored-by: ptisnoob <49125305+ptisnoob@users.noreply.github.com>
Co-authored-by: pt <ptsghs@163.com>
2025-03-07 18:25:18 +08:00
6c52754c8f fixed:目录错位 (#230)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-07 11:44:37 +08:00
ikun441andGitHub e1ebcfa865 Create ACGNTTS.py (#200)
Type:ACGNTTS
2025-03-07 10:42:04 +08:00
6464c81e72 Manager web (#227)
* update:增加前端设计图

* 前端代码优化

* update:底部信息纠正

* 增加:flyio

* update:去除org.quartz

* update:登陆功能

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: CGD <3030332422@qq.com>
2025-03-07 10:01:20 +08:00
0b7abfae3a update:优化docker时区 (#225)
Co-authored-by: hrz <1710360675@qq.com>
2025-03-07 01:20:39 +08:00
zhishengandGitHub 76c16ad16e fix manager-api bug (#223) 2025-03-06 23:31:23 +08:00
1f722477ce Fix docker config bug (#220)
* fixed:兼容旧版本docker

* update:增加前端设计图

* update:dockerfile

* fix:修复新版docker目录不对bug

* fix: 修复docker快速启动方式中无法读取配置的问题 #207 (#219)

Co-authored-by: 欣南科技 <huangrongzhuang@xin-nan.com>

---------

Co-authored-by: hrz <1710360675@qq.com>
Co-authored-by: yanyige <232218719@qq.com>
2025-03-06 13:12:32 +08:00
76 changed files with 4361 additions and 6482 deletions
+7
View File
@@ -0,0 +1,7 @@
.git
__pycache__
*.pyc
.env
Dockerfile
tmp/
data/
+22 -8
View File
@@ -7,30 +7,31 @@ on:
jobs: jobs:
release: release:
name: Release Docker image name: Release Docker images
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: write packages: write
contents: write contents: write
id-token: write id-token: write
issues: write issues: write
steps: steps:
- name: Check Disk Space - name: Check Disk Space
run: | run: |
df -h df -h
docker system df docker system df
- name: Clean up Docker resources - name: Clean up Docker resources
run: | run: |
docker system prune -af docker system prune -af
docker builder prune -af docker builder prune -af
- name: Check out the repo
- name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to the GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
@@ -42,13 +43,26 @@ jobs:
run: | run: |
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push Docker image # 构建 xiaozhi-server 镜像
id: build_push - name: Build and push xiaozhi-server
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile-server
push: true push: true
tags: | tags: |
ghcr.io/${{ github.repository }}:${{ env.VERSION }} ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:server_latest
platforms: linux/amd64,linux/arm64
# 构建 manager-api 镜像
- name: Build and push manager-web
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile-web
push: true
tags: |
ghcr.io/${{ github.repository }}:web_${{ env.VERSION }}
ghcr.io/${{ github.repository }}:web_latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
Executable → Regular
+1 -1
View File
@@ -23,7 +23,7 @@ RUN apt-get update && \
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
# 复制应用代码 # 复制应用代码
COPY main/xiaozhi-server/ . COPY main/xiaozhi-server .
# 启动应用 # 启动应用
CMD ["python", "app.py"] CMD ["python", "app.py"]
+40
View File
@@ -0,0 +1,40 @@
# 第一阶段:构建Vue前端
FROM node:18 as web-builder
WORKDIR /app
COPY main/manager-web/package*.json ./
RUN npm install
COPY main/manager-web .
RUN npm run build
# 第二阶段:构建Java后端
FROM maven:3-eclipse-temurin-21-alpine as api-builder
WORKDIR /app
COPY main/manager-api/pom.xml .
COPY main/manager-api/src ./src
RUN mvn clean package -Dmaven.test.skip=true
# 第三阶段:构建最终镜像
FROM eclipse-temurin:21-jdk-jammy
# 安装Nginx并清理缓存
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 配置Nginx
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
# 复制前端构建产物
COPY --from=web-builder /app/dist /usr/share/nginx/html
# 复制Java后端JAR包
COPY --from=api-builder /app/target/xiaozhi-esp32-api.jar /app/xiaozhi-esp32-api.jar
# 暴露端口
EXPOSE 8002
# 启动脚本
COPY docs/docker/start.sh /start.sh
RUN chmod +x /start.sh
CMD ["/start.sh"]
+66 -45
View File
@@ -129,55 +129,67 @@ server:
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
- **对话交互** - **对话交互**
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
- **意图识别**
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
- **多语言识别** - **多语言识别**
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。 支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
- **LLM 模块** - **LLM 模块**
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
- **TTS 模块** - **TTS 模块**
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
### 正在开发 🚧 ### 正在开发 🚧
- 对话记忆功能
- 多种心情模式 - 多种心情模式
- 智控台webui - 智控台webui
- iot功能
![图片](docs/images/webui.png) ![图片](docs/images/webui.png)
--- ---
## 本项目支持的平台/组件列表 📋 ## 本项目支持的平台/组件列表 📋
### LLM ### LLM 语言模型
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:------------------:|:---------------------:|:--------:|:-----------------------------------------------------------------:| |:---:|:------------------:|:---------------------:|:-----------:|:-----------------------------------------------------------------------------------------------------------------------:|
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) | | LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) | | LLM | DoubaoLLM | openai 接口调用 | 消耗 token | [点击申请密钥](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k&projectName=undefined) |
| LLM | 智谱(ChatGLMLLM | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) | | LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
| LLM | OllamaLLM | ollama 接口调用 | 免费/自定义 | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` | | LLM | 智谱(ChatGLMLLM | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
| LLM | DifyLLM | dify 接口调用 | 消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 | | LLM | OllamaLLM | ollama 接口调用 | 免费/消耗 token | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) | | LLM | DifyLLM | dify 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 | | LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 | | LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。 实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
--- ---
### TTS ### TTS 语音合成
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:| |:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:|
| TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 | | TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 |
| TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 | | TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 |
| TTS | AliyunTTS | 接口调用 | 消耗 token | [点击创建密钥](https://nls-portal.console.aliyun.com/applist) |
| TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav | | TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
| TTS | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) |
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav | | TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
| TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) |
| TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 |
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 | | TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
| TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 | | TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
| TTS | GPT_SOVITS_V3 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
| TTS | MinimaxTTS | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
--- ---
### VAD ### VAD 语音活动检测
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
@@ -185,7 +197,7 @@ server:
--- ---
### ASR ### ASR 语音识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:---:|:---------:|:----:|:----:|:--:| |:---:|:---------:|:----:|:----:|:--:|
@@ -194,11 +206,21 @@ server:
--- ---
### Memory ### Memory 记忆存储
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 | | 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:------:|:----:|:----:|:--:| |:------:|:---------------:|:----:|:--------:|:--:|
| Memory | mem0ai | 接口调用 | 免费 | | | Memory | mem0ai | 接口调用 | 100次/月额度 | |
| Memory | mem_local_short | 本地总结 | 免费 | |
---
### Intent 意图识别
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|:------:|:-------------:|:----:|:-------:|:---------------------:|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
--- ---
@@ -231,39 +253,18 @@ server:
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。 点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
编译成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。 烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
--- ---
## 常见问题 ❓ ## 常见问题 ❓
### 1、TTS 经常失败,经常超时 ⏰ ### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
### 2、我想通过小智控制电灯、空调、远程开关机等操作 💡
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
### 3、我说话很慢,停顿时小智老是抢话 🗣️
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
```yaml
VAD:
SileroVAD:
threshold: 0.5
model_dir: models/snakers4_silero-vad
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
```
### 4、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt` 建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件) 文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁 ### 2、为什么会出现“TTS 任务出错 文件不存在”?📁
建议:检查一下是否正确使用`conda` 安装了`libopus``ffmpeg`库。 建议:检查一下是否正确使用`conda` 安装了`libopus``ffmpeg`库。
@@ -274,7 +275,12 @@ conda install conda-forge::libopus
conda install conda-forge::ffmpeg conda install conda-forge::ffmpeg
``` ```
### 6、如何提高小智对话响应速度? ⚡ ### 3、TTS 经常失败,经常超时 ⏰
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
### 4、如何提高小智对话响应速度? ⚡
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。 本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺): 如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
@@ -305,7 +311,6 @@ LLM 性能排行:
|:-----------|:-----------|:--------| |:-----------|:-----------|:--------|
| AliLLM | 0.547s | 1.485s | | AliLLM | 0.547s | 1.485s |
| ChatGLMLLM | 0.677s | 3.057s | | ChatGLMLLM | 0.677s | 3.057s |
| OllamaLLM | 0.003s | 0.003s |
TTS 性能排行: TTS 性能排行:
@@ -332,6 +337,22 @@ TTS 性能排行:
- LLM`AliLLM` - LLM`AliLLM`
- TTS`DoubaoTTS` - TTS`DoubaoTTS`
### 5、我说话很慢,停顿时小智老是抢话 🗣️
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
```yaml
VAD:
SileroVAD:
threshold: 0.5
model_dir: models/snakers4_silero-vad
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
```
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
### 7、更多问题,可联系我们反馈 💬 ### 7、更多问题,可联系我们反馈 💬
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y` 我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`
+16 -1
View File
@@ -294,4 +294,19 @@ LLM:
这个信息很有用的,后面`编译esp32固件`需要用到。 这个信息很有用的,后面`编译esp32固件`需要用到。
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。 接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
以下是一些常见问题,供参考:
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
+9 -36
View File
@@ -8,41 +8,14 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
``` ```
2、编译docker镜像 2、编译docker镜像
``` ```
#进入xiaozhi-server目录 #进入项目根目录
cd main/xiaozhi-server/ # 编译server
# 普通编译 docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip . # 编译web
``` docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
3、测试本地镜像
```
docker stop xiaozhi-esp32-server
docker rm xiaozhi-esp32-server
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/data/.config.yaml:/opt/xiaozhi-esp32-server/config.yaml xiaozhi-esp32-server:local
docker logs -f xiaozhi-esp32-server
# 编译完成后,可以使用docker-compose启动项目
# docker-compose.yml你需要修改成自己编译的镜像版本
cd main/xiaozhi-server
docker-compose up -d
``` ```
5、发布腾讯云镜像
```
# amd64
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
# arm64
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
# 推送最新版本
docker manifest rm ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
docker manifest create ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64 ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64 --amend
docker manifest inspect ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
docker manifest push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
```
6、运行线上镜像
```
cd /Users/hrz/myworkspace/docker-java-env/thirddata/
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
docker logs -f xiaozhi-esp32-server
```
+26
View File
@@ -0,0 +1,26 @@
server {
listen 8002;
server_name localhost;
# 静态资源服务(Vue项目)
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# API反向代理(Java项目)
location /xiaozhi-esp32-api/ {
proxy_pass http://127.0.0.1:8003;
proxy_set_header Host $host;
proxy_cookie_path /api/ /;
proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie;
proxy_connect_timeout 10;
proxy_send_timeout 10;
proxy_read_timeout 10;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# 启动Java后端(docker内监听8003端口)
java -jar /app/xiaozhi-esp32-api.jar \
--server.port=8003 \
--spring.datasource.druid.url=${SPRING_DATASOURCE_DRUID_URL} \
--spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \
--spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \
--spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \
--spring.data.redis.port=${SPRING_DATA_REDIS_PORT} &
# 启动Nginx(前台运行保持容器存活)
nginx -g 'daemon off;'
+16
View File
@@ -87,3 +87,19 @@ https://espressif.github.io/esp-launchpad/
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。 打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。 翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
以下是一些常见问题,供参考:
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
Executable → Regular
View File
+1 -17
View File
@@ -23,9 +23,7 @@
<mybatisplus.version>3.5.5</mybatisplus.version> <mybatisplus.version>3.5.5</mybatisplus.version>
<hutool.version>5.8.24</hutool.version> <hutool.version>5.8.24</hutool.version>
<jsoup.version>1.19.1</jsoup.version> <jsoup.version>1.19.1</jsoup.version>
<jasypt.version>3.0.5</jasypt.version>
<knife4j.version>4.6.0</knife4j.version> <knife4j.version>4.6.0</knife4j.version>
<quartz.version>2.3.2</quartz.version>
<shiro.version>2.0.2</shiro.version> <shiro.version>2.0.2</shiro.version>
<captcha.version>1.6.2</captcha.version> <captcha.version>1.6.2</captcha.version>
<guava.version>33.0.0-jre</guava.version> <guava.version>33.0.0-jre</guava.version>
@@ -34,21 +32,6 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
<exclusions>
<exclusion>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
</exclusion>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP-java6</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency> <dependency>
<groupId>org.apache.shiro</groupId> <groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId> <artifactId>shiro-core</artifactId>
@@ -83,6 +66,7 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<!-- 验证码工具包 -->
<dependency> <dependency>
<groupId>com.github.whvcse</groupId> <groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId> <artifactId>easy-captcha</artifactId>
@@ -1,40 +1,40 @@
package xiaozhi.common.aspect; package xiaozhi.common.aspect;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* Redis切面处理类 * Redis切面处理类
* Copyright (c) 人人开源 All rights reserved. * Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io * Website: https://www.renren.io
*/ */
@Slf4j @Slf4j
@Aspect @Aspect
@Component @Component
public class RedisAspect { public class RedisAspect {
/** /**
* 是否开启redis缓存 true开启 false关闭 * 是否开启redis缓存 true开启 false关闭
*/ */
@Value("${renren.redis.open}") @Value("${renren.redis.open}")
private boolean open; private boolean open;
@Around("execution(* xiaozhi.common.redis.RedisUtils.*(..))") @Around("execution(* xiaozhi.common.redis.RedisUtils.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
Object result = null; Object result = null;
if (open) { if (open) {
try { try {
result = point.proceed(); result = point.proceed();
} catch (Exception e) { } catch (Exception e) {
log.error("redis error", e); log.error("redis error", e);
throw new RenException(ErrorCode.REDIS_ERROR); throw new RenException(ErrorCode.REDIS_ERROR);
} }
} }
return result; return result;
} }
} }
@@ -35,4 +35,11 @@ public interface ErrorCode {
int REDIS_ERROR = 10027; int REDIS_ERROR = 10027;
int JOB_ERROR = 10028; int JOB_ERROR = 10028;
int INVALID_SYMBOL = 10029; int INVALID_SYMBOL = 10029;
// 密码相关错误码
int PASSWORD_LENGTH_ERROR = 10030;
int PASSWORD_WEAK_ERROR = 10031;
int DEL_MYSELF_ERROR = 10032;
// 验证码错误
int VERIFICATION_CODE = 10033;
} }
@@ -1,108 +1,108 @@
package xiaozhi.common.service; package xiaozhi.common.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.Wrapper;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
/** /**
* 基础服务接口,所有Service接口都要继承 * 基础服务接口,所有Service接口都要继承
* Copyright (c) 人人开源 All rights reserved. * Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io * Website: https://www.renren.io
*/ */
public interface BaseService<T> { public interface BaseService<T> {
Class<T> currentModelClass(); Class<T> currentModelClass();
/** /**
* <p> * <p>
* 插入一条记录(选择字段,策略插入) * 插入一条记录(选择字段,策略插入)
* </p> * </p>
* *
* @param entity 实体对象 * @param entity 实体对象
*/ */
boolean insert(T entity); boolean insert(T entity);
/** /**
* <p> * <p>
* 插入(批量),该方法不支持 Oracle、SQL Server * 插入(批量),该方法不支持 Oracle、SQL Server
* </p> * </p>
* *
* @param entityList 实体对象集合 * @param entityList 实体对象集合
*/ */
boolean insertBatch(Collection<T> entityList); boolean insertBatch(Collection<T> entityList);
/** /**
* <p> * <p>
* 插入(批量),该方法不支持 Oracle、SQL Server * 插入(批量),该方法不支持 Oracle、SQL Server
* </p> * </p>
* *
* @param entityList 实体对象集合 * @param entityList 实体对象集合
* @param batchSize 插入批次数量 * @param batchSize 插入批次数量
*/ */
boolean insertBatch(Collection<T> entityList, int batchSize); boolean insertBatch(Collection<T> entityList, int batchSize);
/** /**
* <p> * <p>
* 根据 ID 选择修改 * 根据 ID 选择修改
* </p> * </p>
* *
* @param entity 实体对象 * @param entity 实体对象
*/ */
boolean updateById(T entity); boolean updateById(T entity);
/** /**
* <p> * <p>
* 根据 whereEntity 条件,更新记录 * 根据 whereEntity 条件,更新记录
* </p> * </p>
* *
* @param entity 实体对象 * @param entity 实体对象
* @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper} * @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
*/ */
boolean update(T entity, Wrapper<T> updateWrapper); boolean update(T entity, Wrapper<T> updateWrapper);
/** /**
* <p> * <p>
* 根据ID 批量更新 * 根据ID 批量更新
* </p> * </p>
* *
* @param entityList 实体对象集合 * @param entityList 实体对象集合
*/ */
boolean updateBatchById(Collection<T> entityList); boolean updateBatchById(Collection<T> entityList);
/** /**
* <p> * <p>
* 根据ID 批量更新 * 根据ID 批量更新
* </p> * </p>
* *
* @param entityList 实体对象集合 * @param entityList 实体对象集合
* @param batchSize 更新批次数量 * @param batchSize 更新批次数量
*/ */
boolean updateBatchById(Collection<T> entityList, int batchSize); boolean updateBatchById(Collection<T> entityList, int batchSize);
/** /**
* <p> * <p>
* 根据 ID 查询 * 根据 ID 查询
* </p> * </p>
* *
* @param id 主键ID * @param id 主键ID
*/ */
T selectById(Serializable id); T selectById(Serializable id);
/** /**
* <p> * <p>
* 根据 ID 删除 * 根据 ID 删除
* </p> * </p>
* *
* @param id 主键ID * @param id 主键ID
*/ */
boolean deleteById(Serializable id); boolean deleteById(Serializable id);
/** /**
* <p> * <p>
* 删除(根据ID 批量删除) * 删除(根据ID 批量删除)
* </p> * </p>
* *
* @param idList 主键ID列表 * @param idList 主键ID列表
*/ */
boolean deleteBatchIds(Collection<? extends Serializable> idList); boolean deleteBatchIds(Collection<? extends Serializable> idList);
} }
@@ -1,214 +1,214 @@
package xiaozhi.common.service.impl; package xiaozhi.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.enums.SqlMethod; import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.Constants; import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit; import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import org.apache.ibatis.binding.MapperMethod; import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory; import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
/** /**
* 基础服务类,所有Service都要继承 * 基础服务类,所有Service都要继承
* Copyright (c) 人人开源 All rights reserved. * Copyright (c) 人人开源 All rights reserved.
* Website: https://www.renren.io * Website: https://www.renren.io
*/ */
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> { public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> {
@Autowired @Autowired
protected M baseDao; protected M baseDao;
protected Log log = LogFactory.getLog(getClass()); protected Log log = LogFactory.getLog(getClass());
/** /**
* 获取分页对象 * 获取分页对象
* *
* @param params 分页查询参数 * @param params 分页查询参数
* @param defaultOrderField 默认排序字段 * @param defaultOrderField 默认排序字段
* @param isAsc 排序方式 * @param isAsc 排序方式
*/ */
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) { protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
//分页参数 //分页参数
long curPage = 1; long curPage = 1;
long limit = 10; long limit = 10;
if (params.get(Constant.PAGE) != null) { if (params.get(Constant.PAGE) != null) {
curPage = Long.parseLong((String) params.get(Constant.PAGE)); curPage = Long.parseLong((String) params.get(Constant.PAGE));
} }
if (params.get(Constant.LIMIT) != null) { if (params.get(Constant.LIMIT) != null) {
limit = Long.parseLong((String) params.get(Constant.LIMIT)); limit = Long.parseLong((String) params.get(Constant.LIMIT));
} }
//分页对象 //分页对象
Page<T> page = new Page<>(curPage, limit); Page<T> page = new Page<>(curPage, limit);
//分页参数 //分页参数
params.put(Constant.PAGE, page); params.put(Constant.PAGE, page);
//排序字段 //排序字段
String orderField = (String) params.get(Constant.ORDER_FIELD); String orderField = (String) params.get(Constant.ORDER_FIELD);
String order = (String) params.get(Constant.ORDER); String order = (String) params.get(Constant.ORDER);
//前端字段排序 //前端字段排序
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) { if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
if (Constant.ASC.equalsIgnoreCase(order)) { if (Constant.ASC.equalsIgnoreCase(order)) {
return page.addOrder(OrderItem.asc(orderField)); return page.addOrder(OrderItem.asc(orderField));
} else { } else {
return page.addOrder(OrderItem.desc(orderField)); return page.addOrder(OrderItem.desc(orderField));
} }
} }
//没有排序字段,则不排序 //没有排序字段,则不排序
if (StringUtils.isBlank(defaultOrderField)) { if (StringUtils.isBlank(defaultOrderField)) {
return page; return page;
} }
//默认排序 //默认排序
if (isAsc) { if (isAsc) {
page.addOrder(OrderItem.asc(defaultOrderField)); page.addOrder(OrderItem.asc(defaultOrderField));
} else { } else {
page.addOrder(OrderItem.desc(defaultOrderField)); page.addOrder(OrderItem.desc(defaultOrderField));
} }
return page; return page;
} }
protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target) { protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target) {
List<T> targetList = ConvertUtils.sourceToTarget(list, target); List<T> targetList = ConvertUtils.sourceToTarget(list, target);
return new PageData<>(targetList, total); return new PageData<>(targetList, total);
} }
protected <T> PageData<T> getPageData(IPage page, Class<T> target) { protected <T> PageData<T> getPageData(IPage page, Class<T> target) {
return getPageData(page.getRecords(), page.getTotal(), target); return getPageData(page.getRecords(), page.getTotal(), target);
} }
protected void paramsToLike(Map<String, Object> params, String... likes) { protected void paramsToLike(Map<String, Object> params, String... likes) {
for (String like : likes) { for (String like : likes) {
String val = (String) params.get(like); String val = (String) params.get(like);
if (StringUtils.isNotBlank(val)) { if (StringUtils.isNotBlank(val)) {
params.put(like, "%" + val + "%"); params.put(like, "%" + val + "%");
} else { } else {
params.put(like, null); params.put(like, null);
} }
} }
} }
/** /**
* <p> * <p>
* 判断数据库操作是否成功 * 判断数据库操作是否成功
* </p> * </p>
* <p> * <p>
* 注意!! 该方法为 Integer 判断,不可传入 int 基本类型 * 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
* </p> * </p>
* *
* @param result 数据库操作返回影响条数 * @param result 数据库操作返回影响条数
* @return boolean * @return boolean
*/ */
protected static boolean retBool(Integer result) { protected static boolean retBool(Integer result) {
return SqlHelper.retBool(result); return SqlHelper.retBool(result);
} }
protected Class<M> currentMapperClass() { protected Class<M> currentMapperClass() {
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0); return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
} }
@Override @Override
public Class<T> currentModelClass() { public Class<T> currentModelClass() {
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1); return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
} }
protected String getSqlStatement(SqlMethod sqlMethod) { protected String getSqlStatement(SqlMethod sqlMethod) {
return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod); return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean insert(T entity) { public boolean insert(T entity) {
return BaseServiceImpl.retBool(baseDao.insert(entity)); return BaseServiceImpl.retBool(baseDao.insert(entity));
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean insertBatch(Collection<T> entityList) { public boolean insertBatch(Collection<T> entityList) {
return insertBatch(entityList, 100); return insertBatch(entityList, 100);
} }
/** /**
* 批量插入 * 批量插入
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean insertBatch(Collection<T> entityList, int batchSize) { public boolean insertBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE); String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
} }
/** /**
* 执行批量操作 * 执行批量操作
*/ */
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) { protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer); return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean updateById(T entity) { public boolean updateById(T entity) {
return BaseServiceImpl.retBool(baseDao.updateById(entity)); return BaseServiceImpl.retBool(baseDao.updateById(entity));
} }
@Override @Override
public boolean update(T entity, Wrapper<T> updateWrapper) { public boolean update(T entity, Wrapper<T> updateWrapper) {
return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper)); return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean updateBatchById(Collection<T> entityList) { public boolean updateBatchById(Collection<T> entityList) {
return updateBatchById(entityList, 30); return updateBatchById(entityList, 30);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean updateBatchById(Collection<T> entityList, int batchSize) { public boolean updateBatchById(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID); String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> { return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>(); MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
param.put(Constants.ENTITY, entity); param.put(Constants.ENTITY, entity);
sqlSession.update(sqlStatement, param); sqlSession.update(sqlStatement, param);
}); });
} }
@Override @Override
public T selectById(Serializable id) { public T selectById(Serializable id) {
return baseDao.selectById(id); return baseDao.selectById(id);
} }
@Override @Override
public boolean deleteById(Serializable id) { public boolean deleteById(Serializable id) {
return SqlHelper.retBool(baseDao.deleteById(id)); return SqlHelper.retBool(baseDao.deleteById(id));
} }
@Override @Override
public boolean deleteBatchIds(Collection<? extends Serializable> idList) { public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
return SqlHelper.retBool(baseDao.deleteBatchIds(idList)); return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
} }
} }
@@ -1,59 +1,59 @@
package xiaozhi.modules.sys.dto; package xiaozhi.modules.sys.dto;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import xiaozhi.common.utils.DateUtils; import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.validator.group.AddGroup; import xiaozhi.common.validator.group.AddGroup;
import xiaozhi.common.validator.group.DefaultGroup; import xiaozhi.common.validator.group.DefaultGroup;
import xiaozhi.common.validator.group.UpdateGroup; import xiaozhi.common.validator.group.UpdateGroup;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null; import jakarta.validation.constraints.Null;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
/** /**
* 字典数据 * 字典数据
*/ */
@Data @Data
@Schema(description = "字典数据") @Schema(description = "字典数据")
public class SysDictDataDTO implements Serializable { public class SysDictDataDTO implements Serializable {
@Schema(description = "id") @Schema(description = "id")
@Null(message = "{id.null}", groups = AddGroup.class) @Null(message = "{id.null}", groups = AddGroup.class)
@NotNull(message = "{id.require}", groups = UpdateGroup.class) @NotNull(message = "{id.require}", groups = UpdateGroup.class)
private Long id; private Long id;
@Schema(description = "字典类型ID") @Schema(description = "字典类型ID")
@NotNull(message = "{sysdict.type.require}", groups = DefaultGroup.class) @NotNull(message = "{sysdict.type.require}", groups = DefaultGroup.class)
private Long dictTypeId; private Long dictTypeId;
@Schema(description = "字典标签") @Schema(description = "字典标签")
@NotBlank(message = "{sysdict.label.require}", groups = DefaultGroup.class) @NotBlank(message = "{sysdict.label.require}", groups = DefaultGroup.class)
private String dictLabel; private String dictLabel;
@Schema(description = "字典值") @Schema(description = "字典值")
private String dictValue; private String dictValue;
@Schema(description = "备注") @Schema(description = "备注")
private String remark; private String remark;
@Schema(description = "排序") @Schema(description = "排序")
@Min(value = 0, message = "{sort.number}", groups = DefaultGroup.class) @Min(value = 0, message = "{sort.number}", groups = DefaultGroup.class)
private Integer sort; private Integer sort;
@Schema(description = "创建时间") @Schema(description = "创建时间")
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
private Date createDate; private Date createDate;
@Schema(description = "更新时间") @Schema(description = "更新时间")
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
private Date updateDate; private Date updateDate;
} }
@@ -1,55 +1,55 @@
package xiaozhi.modules.sys.dto; package xiaozhi.modules.sys.dto;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import xiaozhi.common.utils.DateUtils; import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.validator.group.AddGroup; import xiaozhi.common.validator.group.AddGroup;
import xiaozhi.common.validator.group.DefaultGroup; import xiaozhi.common.validator.group.DefaultGroup;
import xiaozhi.common.validator.group.UpdateGroup; import xiaozhi.common.validator.group.UpdateGroup;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null; import jakarta.validation.constraints.Null;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
/** /**
* 字典类型 * 字典类型
*/ */
@Data @Data
@Schema(description = "字典类型") @Schema(description = "字典类型")
public class SysDictTypeDTO implements Serializable { public class SysDictTypeDTO implements Serializable {
@Schema(description = "id") @Schema(description = "id")
@Null(message = "{id.null}", groups = AddGroup.class) @Null(message = "{id.null}", groups = AddGroup.class)
@NotNull(message = "{id.require}", groups = UpdateGroup.class) @NotNull(message = "{id.require}", groups = UpdateGroup.class)
private Long id; private Long id;
@Schema(description = "字典类型") @Schema(description = "字典类型")
@NotBlank(message = "{sysdict.type.require}", groups = DefaultGroup.class) @NotBlank(message = "{sysdict.type.require}", groups = DefaultGroup.class)
private String dictType; private String dictType;
@Schema(description = "字典名称") @Schema(description = "字典名称")
@NotBlank(message = "{sysdict.name.require}", groups = DefaultGroup.class) @NotBlank(message = "{sysdict.name.require}", groups = DefaultGroup.class)
private String dictName; private String dictName;
@Schema(description = "备注") @Schema(description = "备注")
private String remark; private String remark;
@Schema(description = "排序") @Schema(description = "排序")
@Min(value = 0, message = "{sort.number}", groups = DefaultGroup.class) @Min(value = 0, message = "{sort.number}", groups = DefaultGroup.class)
private Integer sort; private Integer sort;
@Schema(description = "创建时间") @Schema(description = "创建时间")
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
private Date createDate; private Date createDate;
@Schema(description = "更新时间") @Schema(description = "更新时间")
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
private Date updateDate; private Date updateDate;
} }
@@ -1,52 +1,52 @@
package xiaozhi.modules.sys.dto; package xiaozhi.modules.sys.dto;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import xiaozhi.common.utils.DateUtils; import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.validator.group.AddGroup; import xiaozhi.common.validator.group.AddGroup;
import xiaozhi.common.validator.group.DefaultGroup; import xiaozhi.common.validator.group.DefaultGroup;
import xiaozhi.common.validator.group.UpdateGroup; import xiaozhi.common.validator.group.UpdateGroup;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null; import jakarta.validation.constraints.Null;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
/** /**
* 参数管理 * 参数管理
*/ */
@Data @Data
@Schema(description = "参数管理") @Schema(description = "参数管理")
public class SysParamsDTO implements Serializable { public class SysParamsDTO implements Serializable {
@Schema(description = "id") @Schema(description = "id")
@Null(message = "{id.null}", groups = AddGroup.class) @Null(message = "{id.null}", groups = AddGroup.class)
@NotNull(message = "{id.require}", groups = UpdateGroup.class) @NotNull(message = "{id.require}", groups = UpdateGroup.class)
private Long id; private Long id;
@Schema(description = "参数编码") @Schema(description = "参数编码")
@NotBlank(message = "{sysparams.paramcode.require}", groups = DefaultGroup.class) @NotBlank(message = "{sysparams.paramcode.require}", groups = DefaultGroup.class)
private String paramCode; private String paramCode;
@Schema(description = "参数值") @Schema(description = "参数值")
@NotBlank(message = "{sysparams.paramvalue.require}", groups = DefaultGroup.class) @NotBlank(message = "{sysparams.paramvalue.require}", groups = DefaultGroup.class)
private String paramValue; private String paramValue;
@Schema(description = "备注") @Schema(description = "备注")
private String remark; private String remark;
@Schema(description = "创建时间") @Schema(description = "创建时间")
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
private Date createDate; private Date createDate;
@Schema(description = "更新时间") @Schema(description = "更新时间")
@JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonProperty(access = JsonProperty.Access.READ_ONLY)
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
private Date updateDate; private Date updateDate;
} }
@@ -1,26 +1,30 @@
package xiaozhi.modules.sys.service; package xiaozhi.modules.sys.service;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.modules.sys.dto.SysDictTypeDTO; import xiaozhi.modules.sys.dto.SysDictTypeDTO;
import xiaozhi.modules.sys.entity.DictType; import xiaozhi.modules.sys.entity.DictType;
import xiaozhi.modules.sys.entity.SysDictTypeEntity; import xiaozhi.modules.sys.entity.SysDictTypeEntity;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* 数据字典 * 数据字典
*/ */
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> { public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
PageData<SysDictTypeDTO> page(Map<String, Object> params); PageData<SysDictTypeDTO> page(Map<String, Object> params);
SysDictTypeDTO get(Long id); SysDictTypeDTO get(Long id);
void save(SysDictTypeDTO dto); void save(SysDictTypeDTO dto);
void update(SysDictTypeDTO dto); void update(SysDictTypeDTO dto);
void delete(Long[] ids); void delete(Long[] ids);
List<DictType> getAllList();
List<DictType> getDictTypeList();
} }
@@ -0,0 +1,11 @@
package xiaozhi.modules.sys.service;
public interface TokenService {
/**
* 生成token
*
* @param userId
* @return
*/
String createToken(long userId);
}
@@ -1,102 +1,102 @@
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 com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils; 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.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.modules.sys.dao.SysDictDataDao; import xiaozhi.modules.sys.dao.SysDictDataDao;
import xiaozhi.modules.sys.dao.SysDictTypeDao; import xiaozhi.modules.sys.dao.SysDictTypeDao;
import xiaozhi.modules.sys.dto.SysDictTypeDTO; import xiaozhi.modules.sys.dto.SysDictTypeDTO;
import xiaozhi.modules.sys.entity.DictData; import xiaozhi.modules.sys.entity.DictData;
import xiaozhi.modules.sys.entity.DictType; import xiaozhi.modules.sys.entity.DictType;
import xiaozhi.modules.sys.entity.SysDictTypeEntity; import xiaozhi.modules.sys.entity.SysDictTypeEntity;
import xiaozhi.modules.sys.service.SysDictTypeService; import xiaozhi.modules.sys.service.SysDictTypeService;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* 字典类型 * 字典类型
*/ */
@AllArgsConstructor @AllArgsConstructor
@Service @Service
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity> implements SysDictTypeService { public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity> implements SysDictTypeService {
private final SysDictDataDao sysDictDataDao; private final SysDictDataDao sysDictDataDao;
@Override @Override
public PageData<SysDictTypeDTO> page(Map<String, Object> params) { public PageData<SysDictTypeDTO> page(Map<String, Object> params) {
IPage<SysDictTypeEntity> page = baseDao.selectPage( IPage<SysDictTypeEntity> page = baseDao.selectPage(
getPage(params, "sort", true), getPage(params, "sort", true),
getWrapper(params) getWrapper(params)
); );
return getPageData(page, SysDictTypeDTO.class); return getPageData(page, SysDictTypeDTO.class);
} }
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) { private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
String dictType = (String) params.get("dictType"); String dictType = (String) params.get("dictType");
String dictName = (String) params.get("dictName"); String dictName = (String) params.get("dictName");
QueryWrapper<SysDictTypeEntity> wrapper = new QueryWrapper<>(); QueryWrapper<SysDictTypeEntity> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(dictType), "dict_type", dictType); wrapper.like(StringUtils.isNotBlank(dictType), "dict_type", dictType);
wrapper.like(StringUtils.isNotBlank(dictName), "dict_name", dictName); wrapper.like(StringUtils.isNotBlank(dictName), "dict_name", dictName);
return wrapper; return wrapper;
} }
@Override @Override
public SysDictTypeDTO get(Long id) { public SysDictTypeDTO get(Long id) {
SysDictTypeEntity entity = baseDao.selectById(id); SysDictTypeEntity entity = baseDao.selectById(id);
return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class); return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void save(SysDictTypeDTO dto) { public void save(SysDictTypeDTO dto) {
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class); SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
insert(entity); insert(entity);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void update(SysDictTypeDTO dto) { public void update(SysDictTypeDTO dto) {
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class); SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
updateById(entity); updateById(entity);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void delete(Long[] ids) { public void delete(Long[] ids) {
//删除 //删除
deleteBatchIds(Arrays.asList(ids)); deleteBatchIds(Arrays.asList(ids));
} }
@Override @Override
public List<DictType> getAllList() { public List<DictType> getAllList() {
List<DictType> typeList = baseDao.getDictTypeList(); List<DictType> typeList = baseDao.getDictTypeList();
List<DictData> dataList = sysDictDataDao.getDictDataList(); List<DictData> dataList = sysDictDataDao.getDictDataList();
for (DictType type : typeList) { for (DictType type : typeList) {
for (DictData data : dataList) { for (DictData data : dataList) {
if (type.getId().equals(data.getDictTypeId())) { if (type.getId().equals(data.getDictTypeId())) {
type.getDataList().add(data); type.getDataList().add(data);
} }
} }
} }
return typeList; return typeList;
} }
@Override @Override
public List<DictType> getDictTypeList() { public List<DictType> getDictTypeList() {
return baseDao.getDictTypeList(); return baseDao.getDictTypeList();
} }
} }
@@ -0,0 +1,31 @@
package xiaozhi.modules.sys.service.impl;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.security.oauth2.TokenGenerator;
import xiaozhi.modules.sys.service.TokenService;
import java.util.Date;
@AllArgsConstructor
@Service
public class TokenServiceImpl implements TokenService {
private final RedisUtils redisUtils;
/**
* 3小时无操作过期
*/
private final static int EXPIRE = 60 * 60 * 3;
@Override
public String createToken(long userId) {
//生成一个token
String token = TokenGenerator.generateValue();
//当前时间
Date now = new Date();
//过期时间
Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
return token;
}
}
@@ -1,49 +1,44 @@
knife4j: knife4j:
production: false production: false
enable: true enable: true
basic: basic:
enable: false enable: false
username: renren username: renren
password: 2ZABCDEUgF password: 2ZABCDEUgF
setting: setting:
enableFooter: false enableFooter: false
jasypt: jasypt:
encryptor: encryptor:
password: P9Hx718z8L password: P9Hx718z8L
spring: spring:
datasource: datasource:
druid: druid:
#MySQL #MySQL
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true url: jdbc:mysql://localhost:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: root username: root
password: 123456 password: 123456
initial-size: 10 initial-size: 10
max-active: 100 max-active: 100
min-idle: 10 min-idle: 10
max-wait: 6000 max-wait: 6000
pool-prepared-statements: true pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20 max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000 time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000 min-evictable-idle-time-millis: 300000
test-while-idle: true test-while-idle: true
test-on-borrow: false test-on-borrow: false
test-on-return: false test-on-return: false
stat-view-servlet: stat-view-servlet:
enabled: true enabled: true
url-pattern: /druid/* url-pattern: /druid/*
login-username: admin login-username: admin
login-password: D7Xj810i1C login-password: D7Xj810i1C
filter: filter:
stat: stat:
log-slow-sql: true log-slow-sql: true
slow-sql-millis: 1000 slow-sql-millis: 1000
merge-sql: false merge-sql: false
wall: wall:
config: config:
multi-statement-allow: true multi-statement-allow: true
logging:
level:
org.flowable.engine.impl.persistence.entity.*: debug
org.flowable.task.service.impl.persistence.entity.*: debug
@@ -1,50 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.sys.dao.SysUserDao"> <mapper namespace="xiaozhi.modules.sys.dao.SysUserDao">
<select id="getList" resultType="xiaozhi.modules.sys.entity.SysUserEntity"> <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 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 from sys_user t1 where t1.super_admin = 0
<if test="username != null and username.trim() != ''"> <if test="username != null and username.trim() != ''">
and t1.username like #{username} and t1.username like #{username}
</if> </if>
<if test="deptId != null and deptId.trim() != ''"> <if test="deptId != null and deptId.trim() != ''">
and t1.dept_id = #{deptId} and t1.dept_id = #{deptId}
</if> </if>
<if test="gender != null and gender.trim() != ''"> <if test="gender != null and gender.trim() != ''">
and t1.gender = #{gender} and t1.gender = #{gender}
</if> </if>
<if test="deptIdList != null"> <if test="deptIdList != null">
and t1.dept_id in and t1.dept_id in
<foreach item="id" collection="deptIdList" open="(" separator="," close=")"> <foreach item="id" collection="deptIdList" open="(" separator="," close=")">
#{id} #{id}
</foreach> </foreach>
</if> </if>
</select> </select>
<select id="getById" resultType="xiaozhi.modules.sys.entity.SysUserEntity"> <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 select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName from sys_user t1
where t1.id = #{value} where t1.id = #{value}
</select> </select>
<select id="getByUsername" resultType="xiaozhi.modules.sys.entity.SysUserEntity"> <select id="getByUsername" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
select * from sys_user where username = #{value} select * from sys_user where username = #{value}
</select> </select>
<update id="updatePassword"> <update id="updatePassword">
update sys_user set password = #{newPassword} where id = #{id} update sys_user set password = #{newPassword} where id = #{id}
</update> </update>
<select id="getCountByDeptId" resultType="int"> <select id="getCountByDeptId" resultType="int">
select count(*) from sys_user where dept_id = #{value} select count(*) from sys_user where dept_id = #{value}
</select> </select>
<select id="getUserIdListByDeptId" resultType="Long"> <select id="getUserIdListByDeptId" resultType="Long">
select id from sys_user where dept_id in select id from sys_user where dept_id in
<foreach item="deptId" collection="list" open="(" separator="," close=")"> <foreach item="deptId" collection="list" open="(" separator="," close=")">
#{deptId} #{deptId}
</foreach> </foreach>
</select> </select>
</mapper> </mapper>
@@ -1,25 +0,0 @@
package xiaozhi;
import org.jasypt.encryption.StringEncryptor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* 单元测试
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DbEncTest {
@Autowired
StringEncryptor stringEncryptor;
@Test
public void jiami() {
System.out.println("username:" + stringEncryptor.encrypt("07e43e8d669fb946e31ccd4ef5f32c9f2287619c79b766a5985d2c99ad7b7c7e"));
System.out.println("password:" + stringEncryptor.encrypt("042e94093fd2c2765ea45cf13ddbfd38e93026df4b6d5e4206ea5ac90956d63ab73e8b82c6daf7829f9aea7e27e1db5bb0a90944c4c4985af44db0ef49c46d6ad6"));
}
}
@@ -1,27 +0,0 @@
package xiaozhi;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.sys.entity.SysUserEntity;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RedisTest {
@Resource
private RedisUtils redisUtils;
@Test
public void contextLoads() {
SysUserEntity user = new SysUserEntity();
user.setEmail("123456@qq.com");
redisUtils.set("user", user);
System.out.println(ToStringBuilder.reflectionToString(redisUtils.get("user")));
}
}
@@ -1,24 +0,0 @@
package xiaozhi.service;
import xiaozhi.modules.sys.dao.SysUserDao;
import xiaozhi.modules.sys.entity.SysUserEntity;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* 测试多数据源
*/
@Service
public class DynamicDataSourceTestService {
@Resource
private SysUserDao sysUserDao;
//@Transactional
public void updateUser(Long id) {
SysUserEntity user = new SysUserEntity();
user.setId(id);
user.setMobile("13500000000");
//sysUserDao.updateById(user);
System.out.println(sysUserDao.selectById(id));
}
}
+1434 -286
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -7,10 +7,13 @@
"build": "vue-cli-service build" "build": "vue-cli-service build"
}, },
"dependencies": { "dependencies": {
"axios": "^1.8.1",
"element-ui": "^2.15.14", "element-ui": "^2.15.14",
"flyio": "^0.6.14",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-router": "^3.5.1", "vue-axios": "^3.5.2",
"vue-router": "^3.6.5",
"vuex": "^3.6.2" "vuex": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
+24
View File
@@ -0,0 +1,24 @@
// 引入各个模块的请求
import user from './module/user.js'
/**
* 接口地址
* 在开发阶段,如果地址写的是相对路径,请与vue.config.js的devServer配置相结合,方便跨域请求
*
*/
const DEV_API_SERVICE = 'https://apifoxmock.com/m1/5931378-5618560-default'
/**
* 根据开发环境返回接口url
* @returns {string}
*/
export function getServiceUrl() {
return DEV_API_SERVICE
}
/** request服务封装 */
export default {
getServiceUrl,
user,
}
+132
View File
@@ -0,0 +1,132 @@
import {goToPage, showDanger, showWarning} from '../utils/index'
import Constant from '../utils/constant'
import Fly from 'flyio/dist/npm/fly';
const fly = new Fly()
// 设置超时
fly.config.timeout = 30000
/**
* Request服务封装
*/
export default {
sendRequest,
reAjaxFun,
clearRequestTime
}
function sendRequest() {
return {
_sucCallback: null,
_failCallback: null,
_method: 'GET',
_data: {},
_header: {'content-type': 'application/json; charset=utf-8'},
_url: '',
'send'() {
this._header.token = localStorage.getItem(Constant.STORAGE_KEY.TOKEN)
fly.request(this._url, this._data, {
method: this._method,
headers: this._header
}).then((res) => {
const error = httpHandlerError(res, this._failCallback)
if (error) {
return
}
if (this._sucCallback) {
this._sucCallback(res)
}
}).catch((res) => {
console.log(1111, res)
httpHandlerError(res, this._failCallback)
})
return this
},
'success'(callback) {
this._sucCallback = callback
return this
},
'fail'(callback) {
this._failCallback = callback
return this
},
'url'(url) {
if (url) {
url = url.replaceAll('$', '/')
}
this._url = url
return this
},
'data'(data) {
this._data = data
return this
},
'method'(method) {
this._method = method
return this
},
'header'(header) {
this._header = header
return this
},
'showLoading'(showLoading) {
this._showLoading = showLoading
return this
},
'async'(flag) {
this.async = flag
}
}
}
/**
* Info 请求完成后返回信息
* callBack 回调函数
* errTip 自定义错误信息
*/
function httpHandlerError(info, callBack) {
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
let networkError = false
if (info.status === 200) {
if (info.data.code === 'success' || info.data.code === 0) {
return networkError
} else if (info.data.code === 401) {
goToPage(Constant.PAGE.LOGIN, true)
return true
} else {
showDanger(info.data.msg)
return true
}
}
if (callBack) {
callBack(info)
} else {
showDanger(`网络请求出现了错误【${info.status}`)
}
return true
}
let requestTime = 0
let reAjaxSec = 2
function reAjaxFun(fn) {
let nowTimeSec = new Date().getTime() / 1000
if (requestTime === 0) {
requestTime = nowTimeSec
}
let ajaxIndex = parseInt((nowTimeSec - requestTime) / reAjaxSec)
if (ajaxIndex > 10) {
showWarning('似乎无法连接服务器')
} else {
showWarning('正在连接服务器(' + ajaxIndex + ')')
}
if (fn) {
setTimeout(() => {
fn()
}, reAjaxSec * 1000)
}
}
function clearRequestTime() {
requestTime = 0
}
+46
View File
@@ -0,0 +1,46 @@
import RequestService from '../httpRequest'
import {getServiceUrl} from '../api'
export default {
// 登录
login(loginForm, callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`).method('POST')
.data(loginForm)
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.login(loginForm, callback)
})
}).send()
},
// 获取用户信息
getUserInfo(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/info`).method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getUserInfo()
})
}).send()
},
// 获取设备信息
getHomeList(callback) {
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/device/bind`).method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail(() => {
RequestService.reAjaxFun(() => {
this.getUserInfo()
})
}).send()
},
}
+19
View File
@@ -0,0 +1,19 @@
const HAVE_NO_RESULT = '暂无'
export default {
HAVE_NO_RESULT, // 项目的配置信息
STORAGE_KEY: {
TOKEN: 'TOKEN',
PUBLIC_KEY: 'PUBLIC_KEY',
USER_TYPE: 'USER_TYPE'
},
Lang: {
'zh_cn': 'zh_cn', 'zh_tw': 'zh_tw', 'en': 'en'
},
FONT_SIZE: {
'big': 'big',
'normal': 'normal',
}, // 获取map中的某key
get(map, key) {
return map[key] || HAVE_NO_RESULT
}
}
+60
View File
@@ -0,0 +1,60 @@
export const toDate = (date) => {
return isDate(date) ? new Date(date) : null
}
export const isDate = (date) => {
if (date === null || date === undefined) return false
if (isNaN(new Date(date).getTime())) return false
return true
}
export const isDateObject = (val) => {
return val instanceof Date
}
export const formatAddDate = (date, format, addDay) => {
date = toDate(date)
if (!date) {
return ''
}
if (!addDay) {
date.setDate(date.getDate() + addDay)
}
return formatDateTool(date, format || 'yyyy-MM-dd HH:mm:ss')
}
export const formatDate = (date, format) => {
date = toDate(date)
if (!date) return ''
return formatDateTool(date, format || 'yyyy-MM-dd HH:mm:ss')
}
function formatDateTool(date, fmt) {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length)
)
}
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for (const k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
const str = o[k] + ''
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? str : padLeftZero(str)
)
}
}
return fmt
}
function padLeftZero(str) {
return ('00' + str).substr(str.length)
}
+135
View File
@@ -0,0 +1,135 @@
import router from '../router'
import Constant from '../utils/constant'
import { Message } from 'element-ui'
/**
* 判断用户是否登录
*/
export function checkUserLogin(fn) {
let token = localStorage.getItem(Constant.STORAGE_KEY.TOKEN)
let userType = localStorage.getItem(Constant.STORAGE_KEY.USER_TYPE)
if (isNull(token) || isNull(userType)) {
goToPage('console', true)
return
}
if (fn) {
fn()
}
}
/**
* 判断是否为空
* @param data
* @returns {boolean}
*/
export function isNull(data) {
if (data === undefined) {
return true
} else if (data === null) {
return true
} else if (typeof data === 'string' && (data.length === 0 || data === '' || data === 'undefined' || data === 'null')) {
return true
} else if ((data instanceof Array) && data.length === 0) {
return true
}
return false
}
/**
* 判断不为空
* @param data
* @returns {boolean}
*/
export function isNotNull(data) {
return !isNull(data)
}
/**
* 显示顶部红色通知
* @param msg
*/
export function showDanger(msg) {
if (isNull(msg)) {
return
}
Message({
message: msg,
type: 'error'
})
}
/**
* 显示顶部橙色通知
* @param msg
*/
export function showWarning(msg) {
if (isNull(msg)) {
return
}
Message({
message: msg,
type: 'warning'
});
}
/**
* 显示顶部绿色通知
* @param msg
*/
export function showSuccess(msg) {
Message({
message: msg,
type: 'success'
})
}
/**
* 页面跳转
* @param path
* @param isRepalce
*/
export function goToPage(path, isRepalce) {
if (isRepalce) {
router.replace(path)
} else {
router.push(path)
}
}
/**
* 获取当前vue页面名称
* @param path
* @param isRepalce
*/
export function getCurrentPage() {
let hash = location.hash.replace('#', '')
if (hash.indexOf('?') > 0) {
hash = hash.substring(0, hash.indexOf('?'))
}
return hash
}
/**
* 生成从[min,max]的随机数
* @param min
* @param max
* @returns {number}
*/
export function randomNum(min, max) {
return Math.round(Math.random() * (max - min) + min)
}
/**
* 获取uuid
*/
export function getUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
})
}
+79 -37
View File
@@ -3,7 +3,7 @@
<el-container style="height: 100%;"> <el-container style="height: 100%;">
<el-header class="header"> <el-header class="header">
<div style="display: flex;justify-content: space-between;"> <div style="display: flex;justify-content: space-between;">
<div style="display: flex;align-items: center;gap: 8px;"> <div style="display: flex;align-items: center;gap: 8px;margin-top: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" /> <img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" />
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" /> <img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" />
<div class="equipment-management" @click="settingDevice=false"> <div class="equipment-management" @click="settingDevice=false">
@@ -19,15 +19,16 @@
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" /> <img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
</div> </div>
</div> </div>
<div style="display: flex;align-items: center;gap: 8px;"> <div style="display: flex;align-items: center;gap: 8px;margin-top: 10px">
<div class="serach-box"> <div class="serach-box">
<el-input placeholder="输入名称搜索.." v-model="serach" /> <el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" />
<img src="@/assets/home/search.png" alt="" <img src="@/assets/home/search.png" alt=""
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" /> style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" />
</div> </div>
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" /> <img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
<div class="user-info"> <div class="user-info">
158 3632 4642</div> {{ userInfo.mobile }}
</div>
</div> </div>
</div> </div>
</el-header> </el-header>
@@ -56,10 +57,11 @@
</div> </div>
<div <div
style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: space-between;box-sizing: border-box;"> style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: space-between;box-sizing: border-box;">
<div class="device-item" v-for="(item,index) in 10" :key="index"> <div class="device-item" v-for="(item,index) in deviceList" :key="index">
<div style="display: flex;justify-content: space-between;"> <div style="display: flex;justify-content: space-between;">
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;"> <div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
CC:ba:97:11:a6:ac <!-- CC:ba:97:11:a6:ac-->
{{item.list[0]?.mac_address}}
</div> </div>
<div> <div>
<img src="@/assets/home/delete.png" alt="" <img src="@/assets/home/delete.png" alt=""
@@ -68,7 +70,7 @@
</div> </div>
</div> </div>
<div class="device-name"> <div class="device-name">
设备型号:esp32-s3-touch-amoled-1.8 设备型号:{{item.list[0]?.device_type}}
</div> </div>
<div style="display: flex;gap: 8px;align-items: center;"> <div style="display: flex;gap: 8px;align-items: center;">
<div class="settings-btn" @click="clickSettingDevice"> <div class="settings-btn" @click="clickSettingDevice">
@@ -77,30 +79,30 @@
声纹识别</div> 声纹识别</div>
<div class="settings-btn"> <div class="settings-btn">
历史对话</div> 历史对话</div>
<el-switch v-model="switchValue" inactive-text="OTA升级:" :width="32" <el-switch :value="item.list[0]?.ota_upgrade && true || false" inactive-text="OTA升级:" :width="32"
style="margin-left: auto;" /> style="margin-left: auto;" />
</div> </div>
<div class="version-info"> <div class="version-info">
<div>最近对话:6天前</div> <div>最近对话:{{item.list[0]?.recent_chat_time}}</div>
<div>APP版本:1.1.0</div> <div>APP版本:{{item.list[0]?.app_version}}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div v-show="settingDevice" style="border-radius: 20px;background: #fafcfe;"> <div v-show="settingDevice" style="border-radius: 18px;background: #fafcfe;">
<div <div
style="padding: 19px 30px;font-weight: 700;font-size: 24px;text-align: left;color: #3d4566;display: flex;gap: 16px;align-items: center;"> style="padding: 17px 27px;font-weight: 700;font-size: 21px;text-align: left;color: #3d4566;display: flex;gap: 14px;align-items: center;">
<div <div
style="width: 46px;height: 46px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;"> 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: 24px;height: 24px;" /> <img src="@/assets/home/setting-user.png" alt="" style="width: 21px;height: 21px;" />
</div> </div>
CC:ba:97:11:a6:ac CC:ba:97:11:a6:ac
</div> </div>
<div style="height: 1px;background: #e8f0ff;" /> <div style="height: 1px;background: #e8f0ff;" />
<el-form ref="form" :model="form" label-width="90px"> <el-form ref="form" :model="form" label-width="81px">
<div style="padding: 20px 30px;max-width: 990px;"> <div style="padding: 18px 28px;max-width: 890px;">
<el-form-item label="助手昵称:"> <el-form-item label="助手昵称:">
<div class="input-46"> <div class="input-46" style="width: 57.5%;">
<el-input v-model="form.name" /> <el-input v-model="form.name" />
</div> </div>
</el-form-item> </el-form-item>
@@ -119,7 +121,7 @@
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="角色音色:"> <el-form-item label="角色音色:">
<div style="display: flex;gap: 10px;align-items: center;"> <div style="display: flex;gap: 9px;align-items: center;">
<div class="input-46" style="flex:1.4;"> <div class="input-46" style="flex:1.4;">
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;"> <el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
<el-option v-for="item in options" :key="item.value" :label="item.label" <el-option v-for="item in options" :key="item.value" :label="item.label"
@@ -135,28 +137,28 @@
</el-form-item> </el-form-item>
<el-form-item label="角色介绍:"> <el-form-item label="角色介绍:">
<div class="textarea-box"> <div class="textarea-box">
<el-input type="textarea" rows="6" resize="none" placeholder="请输入内容" <el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
v-model="form.introduction" maxlength="2000" show-word-limit /> v-model="form.introduction" maxlength="2000" show-word-limit />
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="记忆体:"> <el-form-item label="记忆体:">
<div class="textarea-box"> <div class="textarea-box">
<el-input type="textarea" rows="6" resize="none" placeholder="请输入内容" <el-input type="textarea" rows="5.4" resize="none" placeholder="请输入内容"
v-model="form.prompt" maxlength="1000" /> v-model="form.prompt" maxlength="1000" />
<div class="prompt-bottom"> <div class="prompt-bottom">
<div style="display: flex;gap: 10px;align-items: center;"> <div style="display: flex;gap: 10px;align-items: center;">
<div style="color: #979db1;font-size: 14px;">当前记忆(每次对话后重新生成)</div> <div style="color: #979db1;font-size: 12px;">当前记忆(每次对话后重新生成)</div>
<div class="clear-btn"> <div class="clear-btn">
<i class="el-icon-delete-solid" style="font-size: 14px;" /> <i class="el-icon-delete-solid" style="font-size: 12px;" />
清除 清除
</div> </div>
</div> </div>
<div style="color: #979db1;font-size:14px;">{{form.prompt.length}}/1000</div> <div style="color: #979db1;font-size:12px;">{{form.prompt.length}}/1000</div>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="语言模型(内测):" class="lh-form-item"> <el-form-item label="语言模型(内测):" class="lh-form-item">
<div style="display: flex;gap: 10px;"> <div style="display: flex;gap: 9px;">
<div class="input-46" style="width: 100%;"> <div class="input-46" style="width: 100%;">
<el-select v-model="form.model" placeholder="请选择" 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" <el-option v-for="item in options" :key="item.value" :label="item.label"
@@ -172,23 +174,24 @@
</el-form-item> </el-form-item>
</div> </div>
</el-form> </el-form>
<div style="display: flex;padding: 20px;gap: 10px;align-items: center;"> <div style="display: flex;padding: 18px;gap: 9px;align-items: center;">
<div class="save-btn"> <div class="save-btn">
保存配置</div> 保存配置</div>
<div class="reset-btn"> <div class="reset-btn">
重制</div> 重制</div>
<div class="clear-text"> <div class="clear-text">
<img src="@/assets/home/red-info.png" alt="" style="width: 24px;height: 24px;" /> <img src="@/assets/home/red-info.png" alt="" style="width: 21px;height: 21px;" />
保存配置后,需要重启设备,新的配置才会生效。 保存配置后,需要重启设备,新的配置才会生效。
</div> </div>
</div> </div>
</div> </div>
<div <div
style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;"> style="font-size: 12px;font-weight: 400;margin-top: auto;padding-top: 30px;color: #979db1;">
©2025 xiaozhi-esp32-server</div> ©2025 xiaozhi-esp32-server
</div>
</el-main> </el-main>
</el-container> </el-container>
<el-dialog :visible.sync="addDeviceDialogVisible" width="480px" center> <el-dialog :visible.sync="addDeviceDialogVisible" width="400px" center>
<div <div
style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;;"> style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;;">
<div <div
@@ -220,6 +223,8 @@
<script> <script>
// @ is an alias to /src // @ is an alias to /src
import Api from '@/apis/api';
export default { export default {
name: 'home', name: 'home',
data() { data() {
@@ -242,8 +247,12 @@ export default {
}, { }, {
value: '选项2', value: '选项2',
label: '双皮奶' label: '双皮奶'
}] }],
} userInfo: {
mobile: '' // 初始化用户信息
},
deviceList:[]
};
}, },
methods: { methods: {
showAddDialog() { showAddDialog() {
@@ -251,10 +260,28 @@ export default {
}, },
clickSettingDevice() { clickSettingDevice() {
this.settingDevice = true this.settingDevice = true
},
// 获取用户信息
fetchUserInfo() {
Api.user.getUserInfo(({data}) => {
this.userInfo = data.data
});
},
// 获取已绑设备
getList(){
Api.user.getHomeList(({data})=>{
console.log(data.data)
this.deviceList = data.data
})
} }
},
mounted() {
this.fetchUserInfo(); // 组件加载时获取用户信息
this.getList()
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.welcome { .welcome {
min-width: 900px; min-width: 900px;
@@ -356,11 +383,14 @@ export default {
} }
.serach-box { .serach-box {
display: flex; display: flex;
width: 230px; width: 250px;
height: 30px; height: 30px;
border-radius: 15px; border-radius: 15px;
background-color: #e2f5f7; background-color: #f6fcfe66;
border: 1px solid #e4e6ef;
align-items: center; align-items: center;
padding: 0 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.user-info { .user-info {
font-weight: 600; font-weight: 600;
@@ -389,7 +419,7 @@ export default {
} }
.template-item { .template-item {
height: 35px; height: 35px;
width: 75px; width: 85px;
border-radius: 8px; border-radius: 8px;
background: #e6ebff; background: #e6ebff;
line-height: 35px; line-height: 35px;
@@ -461,7 +491,7 @@ export default {
} }
} }
.device-item { .device-item {
width: 341px; width: 345px;
border-radius: 15px; border-radius: 15px;
background: #fafcfe; background: #fafcfe;
padding: 22px; padding: 22px;
@@ -504,6 +534,7 @@ audio::-webkit-media-controls-panel {
line-height: 34px; line-height: 34px;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
font-size: 12px;
} }
.save-btn { .save-btn {
border-radius: 23px; border-radius: 23px;
@@ -526,6 +557,14 @@ audio::-webkit-media-controls-panel {
border: none !important; border: none !important;
padding: 15px; 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 { .el-textarea .el-input__count {
color: #979db1; color: #979db1;
font-size: 11px; font-size: 11px;
@@ -533,13 +572,14 @@ audio::-webkit-media-controls-panel {
background-color: transparent; background-color: transparent;
} }
.el-input__inner { .el-input__inner {
border: none; //border: none;
background-color: transparent; //background-color: transparent;
padding: 0 5px 0 15px; padding: 0 5px 0 15px;
border-radius: 8px;
} }
.input-46 .el-input__inner { .input-46 .el-input__inner {
padding: 0 15px; padding: 0 15px;
height: 46px; height: 38px;
} }
.lh-form-item { .lh-form-item {
.el-form-item__label { .el-form-item__label {
@@ -596,3 +636,5 @@ audio::-webkit-media-controls-panel {
} }
} }
</style> </style>
+69 -61
View File
@@ -3,97 +3,91 @@
<el-container style="height: 100%;"> <el-container style="height: 100%;">
<el-header> <el-header>
<div <div
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;"> style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" /> <img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/>
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" /> <img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/>
</div> </div>
</el-header> </el-header>
<el-main style="position: relative;"> <el-main style="position: relative;">
<div class="login-box"> <div class="login-box">
<div <div
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;"> style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;" /> <img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/>
<div class="login-text">登录</div> <div class="login-text">登录</div>
<div class="login-welcome"> <div class="login-welcome">
WELCOME TO LOG IN</div> WELCOME TO LOGIN
</div>
</div> </div>
<el-tabs v-model="activeName" @tab-click="handleClick"> <div style="padding: 0 30px;">
<el-tab-pane label="用户名" name="username"> <div class="input-box">
<div style="padding: 0 30px;"> <img src="@/assets/login/username.png" alt="" class="input-icon"/>
<div class="input-box"> <el-input v-model="form.username" placeholder="请输入用户名"/>
<img src="@/assets/login/username.png" alt="" class="input-icon" /> </div>
<el-input v-model="form.username" placeholder="请输入用户名" /> <div class="input-box">
</div> <img src="@/assets/login/password.png" alt="" class="input-icon"/>
<div class="input-box"> <el-input v-model="form.password" placeholder="请输入密码"/>
<img src="@/assets/login/password.png" alt="" class="input-icon" /> </div>
<el-input v-model="form.password" placeholder="请输入密码" /> <div class="input-box">
</div> <img src="@/assets/login/shield.png" alt="" class="input-icon"/>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;"> <el-input v-model="form.captcha" placeholder="请输入验证码"/>
<div style="cursor: pointer;">新用户注册</div> </div>
<div style="cursor: pointer;">忘记密码</div> <div
</div> style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
</div> <div style="cursor: pointer;">新用户注册</div>
</el-tab-pane> </div>
<el-tab-pane label="手机号" name="phone"> </div>
<div style="padding: 0 30px;"> <div class="login-btn" @click="login">登陆</div>
<div class="input-box">
<img src="@/assets/login/phone.png" alt="" class="input-icon" />
<el-input v-model="form.phoneNumber" placeholder="请输入手机号" />
<div style="width: 120px;flex-shrink: 0;">
<el-dropdown>
<span class="el-dropdown-link">
+86 中国大陆<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>黄金糕</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div style="padding: 0 12px 0 30px;margin-top: 20px;" class="input-box">
<img src="@/assets/login/shield.png" alt="" class="input-icon" />
<el-input v-model="form.phoneCode" placeholder="请输入验证码" />
<div class="code-send">
发送
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
<div class="login-btn">登陆</div>
<div style="font-size: 14px;color: #979db1;"> <div style="font-size: 14px;color: #979db1;">
登录即同意<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div> 登录即同意
<div style="display: inline-block;color: #5778FF;cursor: pointer;">用户协议</div>
<div style="display: inline-block;color: #5778FF;cursor: pointer;">隐私政策</div> <div style="display: inline-block;color: #5778FF;cursor: pointer;">隐私政策</div>
</div> </div>
</div> </div>
</el-main> </el-main>
<el-footer> <el-footer>
<div style="font-size: 12px;font-weight: 400;color: #979db1;"> <div style="font-size: 12px;font-weight: 400;color: #979db1;">
©2024 小智Al控制面板2.0粤ICP备2022121736号-2</div> ©2025 xiaozhi-esp32-server
</div>
</el-footer> </el-footer>
</el-container> </el-container>
</div> </div>
</template> </template>
<script> <script>
// @ is an alias to /src import Api from '@/apis/api'
import {isNull, showDanger, showSuccess, goToPage} from '@/utils'
export default { export default {
name: 'login', name: 'login',
data() { data() {
return { return {
activeName: "username", activeName: "username",
form:{ form: {
phoneNumber:'', username: '',
phoneCode:'', password: '',
username:'', captcha: ''
password:''
} }
} }
}, },
methods:{ methods: {
handleClick(){ login() {
if (isNull(this.form.username)) {
showDanger('用户名不能为空')
return
}
if (isNull(this.form.password)) {
showDanger('密码不能为空')
return
}
if (isNull(this.form.captcha)) {
showDanger('验证码不能为空')
return
}
Api.user.login(this.form, ({data}) => {
showSuccess('登陆成功!')
goToPage('/home')
})
} }
} }
} }
@@ -113,12 +107,14 @@ export default {
-o-background-size: cover; -o-background-size: cover;
/* 兼容老版本Opera浏览器 */ /* 兼容老版本Opera浏览器 */
} }
.login-text { .login-text {
font-weight: 700; font-weight: 700;
font-size: 32px; font-size: 32px;
text-align: left; text-align: left;
color: #3d4566; color: #3d4566;
} }
.login-welcome { .login-welcome {
font-weight: 400; font-weight: 400;
font-size: 9px; font-size: 9px;
@@ -127,6 +123,7 @@ export default {
align-self: flex-end; align-self: flex-end;
margin-bottom: 7px; margin-bottom: 7px;
} }
.login-box { .login-box {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -138,17 +135,20 @@ export default {
width: 450px; width: 450px;
box-sizing: border-box; box-sizing: border-box;
} }
.el-dropdown-link { .el-dropdown-link {
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
text-align: left; text-align: left;
color: #979db1; color: #979db1;
} }
.input-icon { .input-icon {
width: 19px; width: 19px;
height: 22px; height: 22px;
flex-shrink: 0; flex-shrink: 0;
} }
.login-btn { .login-btn {
height: 35px; height: 35px;
background: #5778ff; background: #5778ff;
@@ -160,6 +160,7 @@ export default {
line-height: 35px; line-height: 35px;
margin: 35px 15px 15px; margin: 35px 15px 15px;
} }
.code-send { .code-send {
width: 70px; width: 70px;
height: 32px; height: 32px;
@@ -172,6 +173,7 @@ export default {
flex-shrink: 0; flex-shrink: 0;
cursor: pointer; cursor: pointer;
} }
.input-box { .input-box {
display: flex; display: flex;
margin-top: 20px; margin-top: 20px;
@@ -183,10 +185,12 @@ export default {
padding: 0 15px; padding: 0 15px;
gap: 20px; gap: 20px;
} }
::v-deep { ::v-deep {
.el-tabs__nav-wrap::after { .el-tabs__nav-wrap::after {
height: 1px; height: 1px;
} }
.el-tabs__nav-wrap::before { .el-tabs__nav-wrap::before {
content: ""; content: "";
position: absolute; position: absolute;
@@ -197,18 +201,22 @@ export default {
background-color: #e4e7ed; background-color: #e4e7ed;
z-index: 1; z-index: 1;
} }
.el-tabs__item { .el-tabs__item {
height: 65px; height: 65px;
line-height: 65px; line-height: 65px;
font-weight: 700; font-weight: 700;
color: #3d4566; color: #3d4566;
} }
.el-tabs__item.is-active { .el-tabs__item.is-active {
color: #5778ff; color: #5778ff;
} }
.el-tabs__nav-scroll { .el-tabs__nav-scroll {
padding: 0 30px; padding: 0 30px;
} }
.el-input__inner { .el-input__inner {
border: none; border: none;
background-color: transparent; background-color: transparent;
-13
View File
@@ -1,13 +0,0 @@
.git
__pycache__
*.pyc
.env
Dockerfile
../docs/
tmp/
data/
LICENSE
README.md
README_en.md
manager/static
manager/static/webui/
-16
View File
@@ -1,16 +0,0 @@
# 第一阶段:构建 Python 依赖
FROM kalicyh/poetry:v3.10_xiaozhi AS builder
WORKDIR /app
# 同时拷贝本地环境.venv
COPY . .
# 检查是否有缺失
RUN poetry install --no-root
# 设置虚拟环境路径
ENV PATH="/app/.venv/bin:$PATH"
# 启动应用
ENTRYPOINT ["poetry", "run", "python"]
CMD ["app.py"]
-51
View File
@@ -1,51 +0,0 @@
# 第一阶段:前端构建
FROM node:18 AS frontend-builder
WORKDIR /app/web
# 配置npm使用淘宝源
RUN npm config set registry https://registry.npmmirror.com
COPY web/package*.json ./
# 安装axios依赖
RUN npm install axios
RUN npm install
COPY web .
RUN npm run build
# 第二阶段:构建Python依赖
FROM python:3.10-slim AS builder
WORKDIR /app
COPY requirements.txt .
# 优化apt安装
RUN pip install --no-cache-dir -r requirements.txt \
-i https://mirrors.aliyun.com/pypi/simple/
# 第三阶段:生产镜像
FROM python:3.10-slim
WORKDIR /opt/xiaozhi-esp32-server
# 优化apt安装
RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list && \
echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list && \
apt-get update && \
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 从构建阶段复制Python包和前端构建产物
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=frontend-builder /app/web/dist /opt/xiaozhi-esp32-server/manager/static/webui
# 复制应用代码
COPY . .
# 启动应用
CMD ["python", "app.py"]
+101 -18
View File
@@ -37,7 +37,8 @@ log:
data_dir: data data_dir: data
iot: iot:
Speaker: Speaker:
volume: 100 # 设置esp32的音量,范围0-100
volume: 80
xiaozhi: xiaozhi:
type: hello type: hello
version: 1 version: 1
@@ -64,13 +65,34 @@ CMD_exit:
# 具体处理时选择的模块(The module selected for specific processing) # 具体处理时选择的模块(The module selected for specific processing)
selected_module: selected_module:
ASR: FunASR # 语音活动检测模块,默认使用SileroVAD模型
VAD: SileroVAD VAD: SileroVAD
# 语音识别模块,默认使用FunASR本地模型
ASR: FunASR
# 将根据配置名称对应的type调用实际的LLM适配器 # 将根据配置名称对应的type调用实际的LLM适配器
LLM: ChatGLMLLM LLM: ChatGLMLLM
# TTS将根据配置名称对应的type调用实际的TTS适配器 # TTS将根据配置名称对应的type调用实际的TTS适配器
TTS: EdgeTTS TTS: EdgeTTS
Memory: mem0ai # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
Memory: nomem
# 意图识别模块,默认不开启。开启后,可以播放音乐、控制音量、识别退出指令
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
# 如果意图识别设置成 function_call,建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
Intent: nointent
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
Intent:
# 不使用意图识别
nointent:
# 不需要动
type: nointent
intent_llm:
# 不需要动
type: intent_llm
function_call:
# 不需要动
type: nointent
Memory: Memory:
mem0ai: mem0ai:
@@ -78,7 +100,13 @@ Memory:
# https://app.mem0.ai/dashboard/api-keys # https://app.mem0.ai/dashboard/api-keys
# 每月有1000次免费调用 # 每月有1000次免费调用
api_key: 你的mem0ai api key api_key: 你的mem0ai api key
nomem:
# 不想使用记忆功能,可以使用nomem
type: nomem
mem_local_short:
# 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器
type: mem_local_short
ASR: ASR:
FunASR: FunASR:
type: fun_local type: fun_local
@@ -105,6 +133,16 @@ LLM:
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
model_name: qwen-turbo model_name: qwen-turbo
api_key: 你的deepseek web key api_key: 你的deepseek web key
DoubaoLLM:
# 定义LLM API类型
type: openai
# 先开通服务,打开以下网址,开通的服务搜索Doubao-pro-32k,开通它
# 开通改地址:https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false
# 免费额度500000token
# 开通后,进入这里获取密钥:https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D
base_url: https://ark.cn-beijing.volces.com/api/v3
model_name: doubao-pro-32k-functioncall-241028
api_key: 你的doubao web key
DeepSeekLLM: DeepSeekLLM:
# 定义LLM API类型 # 定义LLM API类型
type: openai type: openai
@@ -158,6 +196,15 @@ LLM:
base_url: http://homeassistant.local:8123 base_url: http://homeassistant.local:8123
agent_id: conversation.chatgpt agent_id: conversation.chatgpt
api_key: 你的home assistant api访问令牌 api_key: 你的home assistant api访问令牌
FastgptLLM:
# 定义LLM API类型
type: fastgpt
# 如果使用fastgpt,配置文件里prompt(提示词)是无效的,需要在fastgpt控制台设置提示词
base_url: https://host/api/v1
api_key: fastgpt-xxx
variables:
k: "v"
k2: "v2"
TTS: TTS:
# 当前支持的type为edge、doubao,可自行适配 # 当前支持的type为edge、doubao,可自行适配
EdgeTTS: EdgeTTS:
@@ -249,6 +296,21 @@ TTS:
parallel_infer: true parallel_infer: true
repetition_penalty: 1.35 repetition_penalty: 1.35
aux_ref_audio_paths: [] aux_ref_audio_paths: []
GPT_SOVITS_V3:
type: gpt_sovits_v3
url: "http://127.0.0.1:9880/tts"
output_file: tmp/
text_lang: "auto"
ref_audio_path: "caixukun.wav"
prompt_lang: "zh"
prompt_text: ""
top_k: 5
top_p: 1
temperature: 1
sample_steps: 16
media_type: "wav"
streaming_mode: false
threshold: 30
MinimaxTTS: MinimaxTTS:
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息 # Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
# 平台地址:https://platform.minimaxi.com/ # 平台地址:https://platform.minimaxi.com/
@@ -296,8 +358,11 @@ TTS:
type: aliyun type: aliyun
output_file: tmp/ output_file: tmp/
appkey: 你的阿里云智能语音交互服务项目Appkey appkey: 你的阿里云智能语音交互服务项目Appkey
token: 你的阿里云智能语音交互服务AccessToken token: 你的阿里云智能语音交互服务AccessToken,临时的24小时,要长期用下方的access_key_idaccess_key_secret
voice: xiaoyun voice: xiaoyun
access_key_id: 你的阿里云账号access_key_id
access_key_secret: 你的阿里云账号access_key_secret
# 以下可不用设置,使用默认设置 # 以下可不用设置,使用默认设置
# format: wav # format: wav
# sample_rate: 16000 # sample_rate: 16000
@@ -309,13 +374,43 @@ TTS:
TTS302AI: TTS302AI:
# 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息 # 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息
# 获取api_keyn路径:https://dash.302.ai/apis/list # 获取api_keyn路径:https://dash.302.ai/apis/list
# 价格,$35/百万字符。火山原版¥450元/万字符 # 价格,$35/百万字符。火山原版¥450元/万字符
type: doubao type: doubao
api_url: https://api.302ai.cn/doubao/tts_hd api_url: https://api.302ai.cn/doubao/tts_hd
authorization: "Bearer " authorization: "Bearer "
voice: "zh_female_wanwanxiaohe_moon_bigtts" voice: "zh_female_wanwanxiaohe_moon_bigtts"
output_file: tmp/ output_file: tmp/
access_token: "你的302API密钥" access_token: "你的302API密钥"
ACGNTTS:
#在线网址:https://acgn.ttson.cn/
#token购买:www.ttson.cn
#开发相关疑问请提交至3497689533@qq.com
#角色id获取地址:ctrl+f快速检索角色——网站管理者不允许发布,可询问网站管理者:1069379506
#各参数意义见开发文档:https://www.yuque.com/alexuh/skmti9/wm6taqislegb02gd?singleDoc#
type: ttson
token: your_token
voice_id: 1695
speed_factor: 1
pitch_factor: 0
volume_change_dB: 0
to_lang: ZH
url: https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=
format: mp3
output_file: tmp/
emotion: 1
OpenAITTS:
# openai官方文本转语音服务,可支持全球大多数语种
type: openai
api_key: 你的openai api key
# 国内需要使用代理
api_url: https://api.openai.com/v1/audio/speech
# 可选tts-1或tts-1-hdtts-1速度更快tts-1-hd质量更好
model: tts-1
# 演讲者,可选alloy, echo, fable, onyx, nova, shimmer
voice: onyx
# 语速范围0.25-4.0
speed: 1
output_file: tmp/
# 模块测试配置 # 模块测试配置
module_test: module_test:
test_sentences: # 自定义测试语句 test_sentences: # 自定义测试语句
@@ -325,18 +420,6 @@ module_test:
# 本地音乐播放配置 # 本地音乐播放配置
music: music:
music_commands:
- "来一首歌"
- "唱一首歌"
- "播放音乐"
- "来点音乐"
- "背景音乐"
- "放首歌"
- "播放歌曲"
- "来点背景音乐"
- "我想听歌"
- "我要听歌"
- "放点音乐"
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
music_ext: # 音乐文件类型,p3格式效率最高 music_ext: # 音乐文件类型,p3格式效率最高
- ".mp3" - ".mp3"
@@ -0,0 +1,36 @@
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"]
}
}
}
]
+140 -7
View File
@@ -15,6 +15,7 @@ from core.utils.util import get_string_no_punctuation_or_emoji
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 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
@@ -27,7 +28,7 @@ class TTSException(RuntimeError):
class ConnectionHandler: class ConnectionHandler:
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _music, _memory): def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _music, _memory, _intent):
self.config = config self.config = config
self.logger = setup_logging() self.logger = setup_logging()
self.auth = AuthMiddleware(config) self.auth = AuthMiddleware(config)
@@ -55,6 +56,7 @@ class ConnectionHandler:
self.llm = _llm self.llm = _llm
self.tts = _tts self.tts = _tts
self.memory = _memory self.memory = _memory
self.intent = _intent
# vad相关变量 # vad相关变量
self.client_audio_buffer = bytes() self.client_audio_buffer = bytes()
@@ -88,6 +90,12 @@ class ConnectionHandler:
self.auth_code_gen = AuthCodeGenerator.get_instance() self.auth_code_gen = AuthCodeGenerator.get_instance()
self.is_device_verified = False # 添加设备验证状态标志 self.is_device_verified = False # 添加设备验证状态标志
self.music_handler = _music self.music_handler = _music
self.close_after_chat = False # 是否在聊天结束后关闭连接
self.use_function_call_mode = False
if self.config["selected_module"]["Intent"] == 'function_call':
self.use_function_call_mode = True
self.logger.bind(tag=TAG).info(f"use_function_call_mode:{self.use_function_call_mode}")
async def handle_connection(self, ws): async def handle_connection(self, ws):
try: try:
@@ -101,7 +109,8 @@ class ConnectionHandler:
await self.auth.authenticate(self.headers) await self.auth.authenticate(self.headers)
device_id = self.headers.get("device-id", None) device_id = self.headers.get("device-id", None)
self.memory.set_role_id(device_id) self.memory.init_memory(device_id, self.llm)
self.intent.set_llm(self.llm)
# Load private configuration if device_id is provided # Load private configuration if device_id is provided
bUsePrivateConfig = self.config.get("use_private_config", False) bUsePrivateConfig = self.config.get("use_private_config", False)
@@ -204,7 +213,6 @@ class ConnectionHandler:
return False return False
return not self.is_device_verified return not self.is_device_verified
def chat(self, query): def chat(self, query):
if self.isNeedAuth(): if self.isNeedAuth():
self.llm_finish_task = True self.llm_finish_task = True
@@ -213,6 +221,7 @@ class ConnectionHandler:
return True return True
self.dialogue.put(Message(role="user", content=query)) self.dialogue.put(Message(role="user", content=query))
response_message = [] response_message = []
processed_chars = 0 # 跟踪已处理的字符位置 processed_chars = 0 # 跟踪已处理的字符位置
try: try:
@@ -220,10 +229,10 @@ class ConnectionHandler:
# 使用带记忆的对话 # 使用带记忆的对话
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop) future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
memory_str = future.result() memory_str = future.result()
self.logger.bind(tag=TAG).info(f"记忆内容: {memory_str}") self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}")
llm_responses = self.llm.response( llm_responses = self.llm.response(
self.session_id, self.session_id,
self.dialogue.get_llm_dialogue_with_memory(memory_str) self.dialogue.get_llm_dialogue_with_memory(memory_str)
) )
except Exception as e: except Exception as e:
@@ -245,7 +254,7 @@ class ConnectionHandler:
current_text = full_text[processed_chars:] # 从未处理的位置开始 current_text = full_text[processed_chars:] # 从未处理的位置开始
# 查找最后一个有效标点 # 查找最后一个有效标点
punctuations = ("", "", "", "?", "!", ";", "", ":", "") punctuations = ("", "", "", "", "")
last_punct_pos = -1 last_punct_pos = -1
for punct in punctuations: for punct in punctuations:
pos = current_text.rfind(punct) pos = current_text.rfind(punct)
@@ -282,6 +291,119 @@ 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):
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
"""Chat with function calling for intent detection using streaming"""
if self.isNeedAuth():
self.llm_finish_task = True
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
future.result()
return True
self.dialogue.put(Message(role="user", content=query))
# Define intent functions
functions = get_functions()
response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
function_call_data = None # 存储function call数据
try:
start_time = time.time()
# 使用带记忆的对话
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
memory_str = future.result()
# self.logger.bind(tag=TAG).info(f"记忆内容: {memory_str}")
# 使用支持functions的streaming接口
llm_responses = self.llm.response_with_functions(
self.session_id,
self.dialogue.get_llm_dialogue_with_memory(memory_str),
functions=functions
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
return None
self.llm_finish_task = False
text_index = 0
# 处理流式响应
for response in llm_responses:
if response["type"] == "content":
content = response["content"]
response_message.append(content)
if self.client_abort:
break
end_time = time.time()
self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
# 处理文本分段和TTS逻辑
# 合并当前全部文本并处理未分割部分
full_text = "".join(response_message)
current_text = full_text[processed_chars:] # 从未处理的位置开始
# 查找最后一个有效标点
punctuations = ("", "", "", "", "")
last_punct_pos = -1
for punct in punctuations:
pos = current_text.rfind(punct)
if pos > last_punct_pos:
last_punct_pos = pos
# 找到分割点则处理
if last_punct_pos != -1:
segment_text_raw = current_text[:last_punct_pos + 1]
segment_text = get_string_no_punctuation_or_emoji(segment_text_raw)
if segment_text:
text_index += 1
self.recode_first_last_text(segment_text, text_index)
future = self.executor.submit(self.speak_and_play, segment_text, text_index)
self.tts_queue.put(future)
processed_chars += len(segment_text_raw) # 更新已处理字符位置
elif response["type"] == "function_call":
# Extract function call data
function_call_data = {
"name": response["function_call"]["function"]["name"],
"arguments": response["function_call"]["function"]["arguments"]
}
self.logger.bind(tag=TAG).info(f"Function call detected: {function_call_data}")
# 处理最后剩余的文本
full_text = "".join(response_message)
remaining_text = full_text[processed_chars:]
if remaining_text:
segment_text = get_string_no_punctuation_or_emoji(remaining_text)
if segment_text:
text_index += 1
self.recode_first_last_text(segment_text, text_index)
future = self.executor.submit(self.speak_and_play, segment_text, text_index)
self.tts_queue.put(future)
# 存储对话内容
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
# 处理function call
if function_call_data:
result = handle_llm_function_call(self, function_call_data)
if result.action == Action.RESPONSE:
text = result.response
text_index += 1
self.recode_first_last_text(text, text_index)
future = self.executor.submit(self.speak_and_play, text, text_index)
self.tts_queue.put(future)
self.llm_finish_task = True
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
return True
def _tts_priority_thread(self): def _tts_priority_thread(self):
while not self.stop_event.is_set(): while not self.stop_event.is_set():
text = None text = None
@@ -372,3 +494,14 @@ class ConnectionHandler:
self.client_have_voice_last_time = 0 self.client_have_voice_last_time = 0
self.client_voice_stop = False self.client_voice_stop = False
self.logger.bind(tag=TAG).debug("VAD states reset.") self.logger.bind(tag=TAG).debug("VAD states reset.")
def chat_and_close(self, text):
"""Chat with the user and then close the connection"""
try:
# Use the existing chat method
self.chat(text)
# After chat is complete, close the connection
self.close_after_chat = True
except Exception as e:
self.logger.bind(tag=TAG).error(f"Chat and close error: {str(e)}")
@@ -0,0 +1,170 @@
from config.logger import setup_logging
import json
from core.handle.sendAudioHandle import send_stt_message
from core.utils.dialogue import Message
from config.functionCallConfig import FunctionCallConfig
import asyncio
from enum import Enum
TAG = __name__
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):
"""
Handle user intent before starting chat
Args:
conn: Connection object
text: User's text input
Returns:
bool: True if intent was handled, False if should proceed to chat
"""
# 检查是否有明确的退出命令
if await check_direct_exit(conn, text):
return True
if conn.use_function_call_mode:
# 使用支持function calling的聊天方法,不再进行意图分析
return False
logger.bind(tag=TAG).info(f"分析用户意图: {text}")
# 使用LLM进行意图分析
intent = await analyze_intent_with_llm(conn, text)
if not intent:
return False
# 处理各种意图
return await process_intent_result(conn, intent, text)
async def check_direct_exit(conn, text):
"""检查是否有明确的退出命令"""
cmd_exit = conn.cmd_exit
for cmd in cmd_exit:
if text == cmd:
logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
await conn.close()
return True
return False
async def analyze_intent_with_llm(conn, text):
"""使用LLM分析用户意图"""
if not hasattr(conn, 'intent') or not conn.intent:
logger.bind(tag=TAG).warning("意图识别服务未初始化")
return None
# 创建对话历史记录
dialogue = conn.dialogue
dialogue.put(Message(role="user", content=text))
try:
intent_result = await conn.intent.detect_intent(dialogue.dialogue)
logger.bind(tag=TAG).info(f"意图识别结果: {intent_result}")
# 尝试解析JSON结果
try:
intent_data = json.loads(intent_result)
if "intent" in intent_data:
return intent_data["intent"]
except json.JSONDecodeError:
# 如果不是JSON格式,尝试直接获取意图文本
return intent_result.strip()
except Exception as e:
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
return None
async def process_intent_result(conn, intent, original_text):
"""处理意图识别结果"""
# 处理退出意图
if "结束聊天" in intent:
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
# 如果正在播放音乐,可以关了 TODO
# 如果是明确的离别意图,发送告别语并关闭连接
await send_stt_message(conn, original_text)
conn.executor.submit(conn.chat_and_close, original_text)
return True
# 处理播放音乐意图
if "播放音乐" in intent:
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}")
await conn.music_handler.handle_music_command(conn, intent)
return True
# 其他意图处理可以在这里扩展
# 默认返回False,表示继续常规聊天流程
return False
@@ -109,7 +109,48 @@ async def handleIotDescriptors(conn, descriptors):
default_iot_volume = conn.config["iot"]["Speaker"]["volume"] default_iot_volume = conn.config["iot"]["Speaker"]["volume"]
logger.bind(tag=TAG).info(f"服务端设置音量为{default_iot_volume}") logger.bind(tag=TAG).info(f"服务端设置音量为{default_iot_volume}")
await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": default_iot_volume}) await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": default_iot_volume})
async def handleIotStatus(conn, states):
"""
处理物联网状态
示例: [{
"name":"Speaker",
"state":{
"volume":100
}
}]
states: 状态列表
"""
for state in states:
for key, value in conn.iot_descriptors.items():
if key == state["name"]:
for property_item in value.properties:
# properties为字典列表, 记录各种属性
for k, v in state["state"].items():
# state为字典, 记录各种属性的值, 是需要记录的信息
if property_item["name"] == k:
# 检查一下属性是不是相同的
if type(v) != type(property_item["value"]):
logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
break
else:
property_item["value"] = v
logger.bind(tag=TAG).info(f"物联网状态更新: {key} , {property_item['name']} = {v}")
break
break
async def get_iot_status(conn, name, property_name):
"""
获取物联网状态
name: 设备名称 "Speaker"
property_name: 属性名称 "volume"
返回值: 属性值, 实际的属性有int, bool和str三种类型
"""
for key, value in conn.iot_descriptors.items():
if key == name:
for property_item in value.properties:
if property_item["name"] == property_name:
return property_item["value"]
return None
async def send_iot_conn(conn, name, method_name, parameters): async def send_iot_conn(conn, name, method_name, parameters):
""" """
+10 -24
View File
@@ -15,7 +15,7 @@ logger = setup_logging()
def _extract_song_name(text): def _extract_song_name(text):
"""从用户输入中提取歌名""" """从用户输入中提取歌名"""
for keyword in ["", "播放", "", ""]: for keyword in ["播放音乐"]:
if keyword in text: if keyword in text:
parts = text.split(keyword) parts = text.split(keyword)
if len(parts) > 1: if len(parts) > 1:
@@ -36,6 +36,7 @@ def _find_best_match(potential_song, music_files):
best_match = music_file best_match = music_file
return best_match return best_match
class MusicManager: class MusicManager:
def __init__(self, music_dir, music_ext): def __init__(self, music_dir, music_ext):
self.music_dir = Path(music_dir) self.music_dir = Path(music_dir)
@@ -55,23 +56,20 @@ class MusicManager:
music_files.append(str(file.relative_to(self.music_dir))) music_files.append(str(file.relative_to(self.music_dir)))
return music_files return music_files
class MusicHandler: class MusicHandler:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.music_related_keywords = []
if "music" in self.config: if "music" in self.config:
self.music_config = self.config["music"] self.music_config = self.config["music"]
self.music_dir = os.path.abspath( self.music_dir = os.path.abspath(
self.music_config.get("music_dir", "./music") # 默认路径修改 self.music_config.get("music_dir", "./music") # 默认路径修改
) )
self.music_related_keywords = self.music_config.get("music_commands", [])
self.music_ext = self.music_config.get("music_ext", (".mp3", ".wav", ".p3")) self.music_ext = self.music_config.get("music_ext", (".mp3", ".wav", ".p3"))
self.refresh_time = self.music_config.get("refresh_time", 60) self.refresh_time = self.music_config.get("refresh_time", 60)
else: else:
self.music_dir = os.path.abspath("./music") self.music_dir = os.path.abspath("./music")
self.music_related_keywords = ["来一首歌", "唱一首歌", "播放音乐", "来点音乐", "背景音乐", "放首歌",
"播放歌曲", "来点背景音乐", "我想听歌", "我要听歌", "放点音乐"]
self.music_ext = (".mp3", ".wav", ".p3") self.music_ext = (".mp3", ".wav", ".p3")
self.refresh_time = 60 self.refresh_time = 60
@@ -100,13 +98,9 @@ class MusicHandler:
logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}") logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}")
await self.play_local_music(conn, specific_file=best_match) await self.play_local_music(conn, specific_file=best_match)
return True return True
# 检查是否是通用播放音乐命令 # 检查是否是通用播放音乐命令
if any(cmd in clean_text for cmd in self.music_related_keywords): await self.play_local_music(conn)
await self.play_local_music(conn) return True
return True
return False
async def play_local_music(self, conn, specific_file=None): async def play_local_music(self, conn, specific_file=None):
"""播放本地音乐文件""" """播放本地音乐文件"""
@@ -117,26 +111,18 @@ class MusicHandler:
# 确保路径正确性 # 确保路径正确性
if specific_file: if specific_file:
music_path = os.path.join(self.music_dir, specific_file)
if not os.path.exists(music_path):
logger.bind(tag=TAG).error(f"指定的音乐文件不存在: {music_path}")
return
selected_music = specific_file selected_music = specific_file
music_path = os.path.join(self.music_dir, specific_file)
else: else:
if time.time() - self.scan_time > self.refresh_time:
# 刷新音乐文件列表
self.music_files = MusicManager(self.music_dir, self.music_ext).get_music_files()
self.scan_time = time.time()
logger.bind(tag=TAG).debug(f"刷新的音乐文件列表: {self.music_files}")
if not self.music_files: if not self.music_files:
logger.bind(tag=TAG).error("未找到MP3音乐文件") logger.bind(tag=TAG).error("未找到MP3音乐文件")
return return
selected_music = random.choice(self.music_files) selected_music = random.choice(self.music_files)
music_path = os.path.join(self.music_dir, selected_music) music_path = os.path.join(self.music_dir, selected_music)
if not os.path.exists(music_path):
logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}") if not os.path.exists(music_path):
return logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
return
text = f"正在播放{selected_music}" text = f"正在播放{selected_music}"
await send_stt_message(conn, text) await send_stt_message(conn, text)
conn.tts_first_text_index = 0 conn.tts_first_text_index = 0
@@ -2,6 +2,7 @@ from config.logger import setup_logging
import time import time
from core.utils.util import remove_punctuation_and_length from core.utils.util import remove_punctuation_and_length
from core.handle.sendAudioHandle import send_stt_message from core.handle.sendAudioHandle import send_stt_message
from core.handle.intentHandler import handle_user_intent
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -33,13 +34,7 @@ async def handleAudioMessage(conn, audio):
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)
logger.bind(tag=TAG).info(f"识别文本: {text}") logger.bind(tag=TAG).info(f"识别文本: {text}")
text_len, text_without_punctuation = remove_punctuation_and_length(text) text_len, _ = remove_punctuation_and_length(text)
if await conn.music_handler.handle_music_command(conn, text_without_punctuation):
conn.asr_server_receive = True
conn.asr_audio.clear()
return
if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
return
if text_len > 0: if text_len > 0:
await startToChat(conn, text) await startToChat(conn, text)
else: else:
@@ -48,20 +43,22 @@ async def handleAudioMessage(conn, audio):
conn.reset_vad_states() conn.reset_vad_states()
async def handleCMDMessage(conn, text):
cmd_exit = conn.cmd_exit
for cmd in cmd_exit:
if text == cmd:
logger.bind(tag=TAG).info("识别到明确的退出命令".format(text))
await conn.close()
return True
return False
async def startToChat(conn, text): async def startToChat(conn, text):
# 异步发送 stt 信息 # 首先进行意图分析
intent_handled = await handle_user_intent(conn, text)
if intent_handled:
# 如果意图已被处理,不再进行聊天
conn.asr_server_receive = True
return
# 意图未被处理,继续常规聊天流程
await send_stt_message(conn, text) await send_stt_message(conn, text)
conn.executor.submit(conn.chat, text) if conn.use_function_call_mode:
# 使用支持function calling的聊天方法
conn.executor.submit(conn.chat_with_function_calling, text)
else:
conn.executor.submit(conn.chat, text)
async def no_voice_close_connect(conn): async def no_voice_close_connect(conn):
@@ -7,14 +7,6 @@ from core.utils.util import remove_punctuation_and_length, get_string_no_punctua
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
async def isLLMWantToFinish(last_text):
_, last_text_without_punctuation = remove_punctuation_and_length(last_text)
if "再见" in last_text_without_punctuation or "拜拜" in last_text_without_punctuation:
return True
return False
async def sendAudioMessage(conn, audios, text, text_index=0): async def sendAudioMessage(conn, audios, text, text_index=0):
# 发送句子开始消息 # 发送句子开始消息
if text_index == conn.tts_first_text_index: if text_index == conn.tts_first_text_index:
@@ -46,10 +38,9 @@ async def sendAudioMessage(conn, audios, text, text_index=0):
# 发送结束消息(如果是最后一个文本) # 发送结束消息(如果是最后一个文本)
if conn.llm_finish_task and text_index == conn.tts_last_text_index: if conn.llm_finish_task and text_index == conn.tts_last_text_index:
await send_tts_message(conn, 'stop', None) await send_tts_message(conn, 'stop', None)
if await isLLMWantToFinish(text): if conn.close_after_chat:
await conn.close() await conn.close()
async def send_tts_message(conn, state, text=None): async def send_tts_message(conn, state, text=None):
"""发送 TTS 状态消息""" """发送 TTS 状态消息"""
message = { message = {
@@ -3,7 +3,7 @@ import json
from core.handle.abortHandle import handleAbortMessage from core.handle.abortHandle import handleAbortMessage
from core.handle.helloHandle import handleHelloMessage from core.handle.helloHandle import handleHelloMessage
from core.handle.receiveAudioHandle import startToChat from core.handle.receiveAudioHandle import startToChat
from core.handle.iotHandle import handleIotDescriptors from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -40,5 +40,7 @@ async def handleTextMessage(conn, message):
elif msg_json["type"] == "iot": elif msg_json["type"] == "iot":
if "descriptors" in msg_json: if "descriptors" in msg_json:
await handleIotDescriptors(conn, msg_json["descriptors"]) await handleIotDescriptors(conn, msg_json["descriptors"])
if "states" in msg_json:
await handleIotStatus(conn, msg_json["states"])
except json.JSONDecodeError: except json.JSONDecodeError:
await conn.websocket.send(message) await conn.websocket.send(message)
@@ -0,0 +1,33 @@
from abc import ABC, abstractmethod
from typing import List, Dict
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class IntentProviderBase(ABC):
def __init__(self, config):
self.config = config
self.intent_options = config.get("intent_options", {
"continue_chat": "继续聊天",
"end_chat": "结束聊天",
"play_music": "播放音乐"
})
def set_llm(self, llm):
self.llm = llm
logger.bind(tag=TAG).debug("Set LLM for intent provider")
@abstractmethod
async def detect_intent(self, dialogue_history: List[Dict]) -> str:
"""
检测用户最后一句话的意图
Args:
dialogue_history: 对话历史记录列表,每条记录包含role和content
Returns:
返回识别出的意图,格式为:
- "继续聊天"
- "结束聊天"
- "播放音乐 歌名""随机播放音乐"
"""
pass
@@ -0,0 +1,61 @@
from typing import List, Dict
from ..base import IntentProviderBase
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class IntentProvider(IntentProviderBase):
def __init__(self, config):
super().__init__(config)
self.llm = None
self.promot = self.get_intent_system_prompt()
def get_intent_system_prompt(self) -> str:
"""
根据配置的意图选项动态生成系统提示词
Returns:
格式化后的系统提示词
"""
intent_list = []
for key, value in self.intent_options.items():
if key == "play_music":
intent_list.append(f"{value} [歌名]")
else:
intent_list.append(value)
prompt = (
"你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
f"{', '.join(intent_list)}\n"
"如果是唱歌、听歌、播放音乐,请指定歌名,格式为'播放音乐 [识别出的歌名]'\n"
"如果听不出具体歌名,可以返回'随机播放音乐'\n"
"只需要返回意图结果的json,不要解释。"
"返回格式如下:\n"
"{intent: '用户意图'}"
)
return prompt
async def detect_intent(self, dialogue_history: List[Dict]) -> str:
if not self.llm:
raise ValueError("LLM provider not set")
# 构建用户最后一句话的提示
msgStr = ""
for msg in dialogue_history:
if msg.role == "user":
msgStr += f"User: {msg.content}\n"
elif msg.role== "assistant":
msgStr += f"Assistant: {msg.content}\n"
user_prompt = f"请分析用户的意图:\n{msgStr}"
# 使用LLM进行意图识别
intent = self.llm.response_no_stream(
system_prompt=self.promot,
user_prompt=user_prompt
)
logger.bind(tag=TAG).info(f"Detected intent: {intent}")
return intent.strip()
@@ -0,0 +1,18 @@
from ..base import IntentProviderBase
from typing import List, Dict
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class IntentProvider(IntentProviderBase):
async def detect_intent(self, dialogue_history: List[Dict]) -> str:
"""
默认的意图识别实现,始终返回继续聊天
Args:
dialogue_history: 对话历史记录列表
Returns:
固定返回"继续聊天"
"""
logger.bind(tag=TAG).debug("Using NoIntentProvider, always returning continue chat")
return self.intent_options["continue_chat"]
@@ -1,8 +1,38 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class LLMProviderBase(ABC): class LLMProviderBase(ABC):
@abstractmethod @abstractmethod
def response(self, session_id, dialogue): def response(self, session_id, dialogue):
"""LLM response generator""" """LLM response generator"""
pass pass
def response_no_stream(self, system_prompt, user_prompt):
try:
# 构造对话格式
dialogue = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
result = ""
for part in self.response("", dialogue):
result += part
return result
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
return "【LLM服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None):
"""
Default implementation for function calling (streaming)
This should be overridden by providers that support function calls
Returns: generator that yields either text tokens or a special function call token
"""
# For providers that don't support functions, just return regular response
for token in self.response(session_id, dialogue):
yield {"type": "content", "content": token}
@@ -0,0 +1,65 @@
import json
from config.logger import setup_logging
import requests
from core.providers.llm.base import LLMProviderBase
TAG = __name__
logger = setup_logging()
class LLMProvider(LLMProviderBase):
def __init__(self, config):
self.api_key = config["api_key"]
self.base_url = config.get("base_url")
self.detail = config.get("detail", False)
self.variables = config.get("variables", {})
def response(self, session_id, dialogue):
try:
# 取最后一条用户消息
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
# 发起流式请求
with requests.post(
f"{self.base_url}/chat/completions",
headers={"Authorization": f"Bearer {self.api_key}"},
json={
"stream": True,
"chatId": session_id,
"detail": self.detail,
"variables": self.variables,
"messages": [
{
"role": "user",
"content": last_msg["content"]
}
]
},
stream=True
) as r:
for line in r.iter_lines():
if line:
try:
if line.startswith(b'data: '):
if line[6:].decode('utf-8') == '[DONE]':
break
data = json.loads(line[6:])
if 'choices' in data and len(data['choices']) > 0:
delta = data['choices'][0].get('delta', {})
if delta and 'content' in delta and delta['content'] is not None:
content = delta['content']
if '<think>' in content:
continue
if '</think>' in content:
continue
yield content
except json.JSONDecodeError as e:
continue
except Exception as e:
continue
except Exception as e:
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
yield "【服务响应异常】"
@@ -1,25 +1,24 @@
from config.logger import setup_logging
import google.generativeai as genai import google.generativeai as genai
from core.utils.util import check_model_key
from core.providers.llm.base import LLMProviderBase from core.providers.llm.base import LLMProviderBase
TAG = __name__
logger = setup_logging()
class LLMProvider(LLMProviderBase): class LLMProvider(LLMProviderBase):
def __init__(self, config): def __init__(self, config):
"""初始化Gemini LLM Provider""" """初始化Gemini LLM Provider"""
self.model_name = config.get("model_name", "gemini-1.5-pro") self.model_name = config.get("model_name", "gemini-1.5-pro")
self.api_key = config.get("api_key") self.api_key = config.get("api_key")
if not self.api_key or "" in self.api_key: have_key = check_model_key("LLM", self.api_key)
logger.bind(tag=TAG).error("你还没配置Gemini LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
if not have_key:
return return
try: try:
# 初始化Gemini客户端 # 初始化Gemini客户端
genai.configure(api_key=self.api_key) genai.configure(api_key=self.api_key)
self.model = genai.GenerativeModel(self.model_name) self.model = genai.GenerativeModel(self.model_name)
# 设置生成参数 # 设置生成参数
self.generation_config = { self.generation_config = {
"temperature": 0.7, "temperature": 0.7,
@@ -55,7 +54,7 @@ class LLMProvider(LLMProviderBase):
# 创建新的聊天会话 # 创建新的聊天会话
chat = self.model.start_chat(history=chat_history) chat = self.model.start_chat(history=chat_history)
# 发送消息并获取流式响应 # 发送消息并获取流式响应
response = chat.send_message( response = chat.send_message(
current_msg, current_msg,
@@ -71,7 +70,7 @@ class LLMProvider(LLMProviderBase):
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}") logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}")
# 针对不同错误返回友好提示 # 针对不同错误返回友好提示
if "Rate limit" in error_msg: if "Rate limit" in error_msg:
yield "【Gemini服务请求太频繁,请稍后再试】" yield "【Gemini服务请求太频繁,请稍后再试】"
@@ -1,5 +1,6 @@
from config.logger import setup_logging from config.logger import setup_logging
import requests, json from openai import OpenAI
import json
from core.providers.llm.base import LLMProviderBase from core.providers.llm.base import LLMProviderBase
TAG = __name__ TAG = __name__
@@ -8,39 +9,73 @@ logger = setup_logging()
class LLMProvider(LLMProviderBase): class LLMProvider(LLMProviderBase):
def __init__(self, config): def __init__(self, config):
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
#如果没有v1,增加v1
if not self.base_url.endswith("/v1"):
self.base_url = f"{self.base_url}/v1"
self.client = OpenAI(
base_url=self.base_url,
api_key="ollama" # Ollama doesn't need an API key but OpenAI client requires one
)
def response(self, session_id, dialogue): def response(self, session_id, dialogue):
try: try:
# Convert dialogue format to Ollama format responses = self.client.chat.completions.create(
prompt = "" model=self.model_name,
for msg in dialogue: messages=dialogue,
if msg["role"] == "system":
prompt += f"System: {msg['content']}\n"
elif msg["role"] == "user":
prompt += f"User: {msg['content']}\n"
elif msg["role"] == "assistant":
prompt += f"Assistant: {msg['content']}\n"
# Make request to Ollama API
response = requests.post(
f"{self.base_url}/api/generate",
json={
"model": self.model_name,
"prompt": prompt,
"stream": True
},
stream=True stream=True
) )
for line in response.iter_lines(): for chunk in responses:
if line: try:
json_response = json.loads(line) delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
if "response" in json_response: content = delta.content if hasattr(delta, 'content') else ''
yield json_response["response"] if content:
yield content
except Exception as e:
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}") logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
yield "【Ollama服务响应异常】" yield "【Ollama服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None):
try:
stream = self.client.chat.completions.create(
model=self.model_name,
messages=dialogue,
stream=True,
tools=functions,
)
current_function_call = None
current_content = ""
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
current_content += delta.content
yield {"type": "content", "content": delta.content}
if delta.tool_calls:
tool_call = delta.tool_calls[0]
# Handle the function call data using proper attribute access
if not current_function_call:
current_function_call = {
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments
}
}
if current_function_call:
logger.bind(tag=TAG).debug(f"ollama Function call detected: {current_function_call}")
yield {"type": "function_call", "function_call": current_function_call}
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
yield {"type": "content", "content": f"【Ollama服务响应异常: {str(e)}"}
@@ -1,10 +1,7 @@
from config.logger import setup_logging
import openai import openai
from core.utils.util import check_model_key
from core.providers.llm.base import LLMProviderBase from core.providers.llm.base import LLMProviderBase
TAG = __name__
logger = setup_logging()
class LLMProvider(LLMProviderBase): class LLMProvider(LLMProviderBase):
def __init__(self, config): def __init__(self, config):
@@ -14,8 +11,7 @@ class LLMProvider(LLMProviderBase):
self.base_url = config.get("base_url") self.base_url = config.get("base_url")
else: else:
self.base_url = config.get("url") self.base_url = config.get("url")
if "" in self.api_key: check_model_key("LLM", self.api_key)
logger.bind(tag=TAG).error("你还没配置LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url) self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
def response(self, session_id, dialogue): def response(self, session_id, dialogue):
@@ -47,3 +43,41 @@ class LLMProvider(LLMProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"Error in response generation: {e}") logger.bind(tag=TAG).error(f"Error in response generation: {e}")
def response_with_functions(self, session_id, dialogue, functions=None):
try:
stream = self.client.chat.completions.create(
model=self.model_name,
messages=dialogue,
stream=True,
tools=functions,
)
current_function_call = None
current_content = ""
for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
current_content += delta.content
yield {"type": "content", "content": delta.content}
if delta.tool_calls:
tool_call = delta.tool_calls[0]
# Handle the function call data using proper attribute access
if not current_function_call:
current_function_call = {
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments
}
}
if current_function_call:
logger.bind(tag=TAG).debug(f"openai Function call detected: {current_function_call}")
yield {"type": "function_call", "function_call": current_function_call}
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
yield {"type": "content", "content": f"【OpenAI服务响应异常: {e}"}
@@ -8,6 +8,7 @@ class MemoryProviderBase(ABC):
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.role_id = None self.role_id = None
self.llm = None
@abstractmethod @abstractmethod
async def save_memory(self, msgs): async def save_memory(self, msgs):
@@ -19,5 +20,6 @@ class MemoryProviderBase(ABC):
"""Query memories for specific role based on similarity""" """Query memories for specific role based on similarity"""
return "please implement query method" return "please implement query method"
def set_role_id(self, role_id: str): def init_memory(self, role_id, llm):
self.role_id = role_id self.role_id = role_id
self.llm = llm
@@ -1,5 +1,8 @@
import traceback
from ..base import MemoryProviderBase, logger from ..base import MemoryProviderBase, logger
from mem0 import MemoryClient from mem0 import MemoryClient
from core.utils.util import check_model_key
TAG = __name__ TAG = __name__
@@ -8,13 +11,19 @@ class MemoryProvider(MemoryProviderBase):
super().__init__(config) super().__init__(config)
self.api_key = config.get("api_key", "") self.api_key = config.get("api_key", "")
self.api_version = config.get("api_version", "v1.1") self.api_version = config.get("api_version", "v1.1")
if len(self.api_key) == 0 or "" in self.api_key: have_key = check_model_key("Mem0ai", self.api_key)
logger.bind(tag=TAG).error("你还没配置Mem0ai的密钥,请在配置文件中配置密钥,否则无法提供记忆服务") if not have_key :
self.use_mem0 = False self.use_mem0 = False
return return
else: else:
self.use_mem0 = True self.use_mem0 = True
self.client = MemoryClient(api_key=self.api_key) try:
self.client = MemoryClient(api_key=self.api_key)
logger.bind(tag=TAG).info("成功连接到 Mem0ai 服务")
except Exception as e:
logger.bind(tag=TAG).error(f"连接到 Mem0ai 服务时发生错误: {str(e)}")
logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
self.use_mem0 = False
async def save_memory(self, msgs): async def save_memory(self, msgs):
if not self.use_mem0: if not self.use_mem0:
@@ -0,0 +1,156 @@
from ..base import MemoryProviderBase, logger
import time
import json
import os
import yaml
from core.utils.util import get_project_dir
short_term_memory_prompt = """
# 时空记忆编织者
## 核心使命
构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹
根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务
## 记忆法则
### 1. 三维度记忆评估(每次更新必执行)
| 维度 | 评估标准 | 权重分 |
|------------|---------------------------|--------|
| 时效性 | 信息新鲜度(按对话轮次) | 40% |
| 情感强度 | 含💖标记/重复提及次数 | 35% |
| 关联密度 | 与其他信息的连接数量 | 25% |
### 2. 动态更新机制
**名字变更处理示例:**
原始记忆:"曾用名": ["张三"], "现用名": "张三丰"
触发条件:当检测到「我叫X」「称呼我Y」等命名信号时
操作流程:
1. 将旧名移入"曾用名"列表
2. 记录命名时间轴:"2024-02-15 14:32:启用张三丰"
3. 在记忆立方追加:「从张三到张三丰的身份蜕变」
### 3. 空间优化策略
- **信息压缩术**:用符号体系提升密度
- ✅"张三丰[北/软工/🐱]"
- ❌"北京软件工程师,养猫"
- **淘汰预警**:当总字数≥900时触发
1. 删除权重分<60且3轮未提及的信息
2. 合并相似条目(保留时间戳最近的)
## 记忆结构
输出格式必须为可解析的json字符串,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容
```json
{
"时空档案": {
"身份图谱": {
"现用名": "",
"特征标记": []
},
"记忆立方": [
{
"事件": "入职新公司",
"时间戳": "2024-03-20",
"情感值": 0.9,
"关联项": ["下午茶"],
"保鲜期": 30
}
]
},
"关系网络": {
"高频话题": {"职场": 12},
"暗线联系": [""]
},
"待响应": {
"紧急事项": ["需立即处理的任务"],
"潜在关怀": ["可主动提供的帮助"]
},
"高光语录": [
"最打动人心的瞬间,强烈的情感表达,user的原话"
]
}
```
"""
def extract_json_data(json_code):
start = json_code.find("```json")
# 从start开始找到下一个```结束
end = json_code.find("```", start+1)
#print("start:", start, "end:", end)
if start == -1 or end == -1:
try:
jsonData = json.loads(json_code)
return json_code
except Exception as e:
print("Error:", e)
return ""
jsonData = json_code[start+7:end]
return jsonData
TAG = __name__
class MemoryProvider(MemoryProviderBase):
def __init__(self, config):
super().__init__(config)
self.short_momery = ""
self.memory_path = get_project_dir() + 'data/.memory.yaml'
self.load_memory()
def init_memory(self, role_id, llm):
super().init_memory(role_id, llm)
self.load_memory()
def load_memory(self):
all_memory = {}
if os.path.exists(self.memory_path):
with open(self.memory_path, 'r', encoding='utf-8') as f:
all_memory = yaml.safe_load(f) or {}
if self.role_id in all_memory:
self.short_momery = all_memory[self.role_id]
def save_memory_to_file(self):
all_memory = {}
if os.path.exists(self.memory_path):
with open(self.memory_path, 'r', encoding='utf-8') as f:
all_memory = yaml.safe_load(f) or {}
all_memory[self.role_id] = self.short_momery
with open(self.memory_path, 'w', encoding='utf-8') as f:
yaml.dump(all_memory, f, allow_unicode=True)
async def save_memory(self, msgs):
if self.llm is None:
logger.bind(tag=TAG).error("LLM is not set for memory provider")
return None
if len(msgs) < 2:
return None
msgStr = ""
for msg in msgs:
if msg.role == "user":
msgStr += f"User: {msg.content}\n"
elif msg.role== "assistant":
msgStr += f"Assistant: {msg.content}\n"
if len(self.short_momery) > 0:
msgStr+="历史记忆:\n"
msgStr+=self.short_momery
#当前时间
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
msgStr += f"当前时间:{time_str}"
result = self.llm.response_no_stream(short_term_memory_prompt, msgStr)
json_str = extract_json_data(result)
try:
json_data = json.loads(json_str) # 检查json格式是否正确
self.short_momery = json_str
except Exception as e:
print("Error:", e)
self.save_memory_to_file()
logger.bind(tag=TAG).info(f"Save memory successful - Role: {self.role_id}")
return self.short_momery
async def query_memory(self, query: str)-> str:
return self.short_momery
@@ -0,0 +1,18 @@
'''
不使用记忆,可以选择此模块
'''
from ..base import MemoryProviderBase, logger
TAG = __name__
class MemoryProvider(MemoryProviderBase):
def __init__(self, config):
super().__init__(config)
async def save_memory(self, msgs):
logger.bind(tag=TAG).debug("nomem mode: No memory saving is performed.")
return None
async def query_memory(self, query: str)-> str:
logger.bind(tag=TAG).debug("nomem mode: No memory query is performed.")
return ""
@@ -1,19 +1,94 @@
import os import os
import uuid import uuid
import json import json
import hmac
import hashlib
import base64
import requests import requests
from datetime import datetime from datetime import datetime
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
import http.client import http.client
import urllib.parse import urllib.parse
import time
import uuid
from urllib import parse
class AccessToken:
@staticmethod
def _encode_text(text):
encoded_text = parse.quote_plus(text)
return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
@staticmethod
def _encode_dict(dic):
keys = dic.keys()
dic_sorted = [(key, dic[key]) for key in sorted(keys)]
encoded_text = parse.urlencode(dic_sorted)
return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
@staticmethod
def create_token(access_key_id, access_key_secret):
parameters = {'AccessKeyId': access_key_id,
'Action': 'CreateToken',
'Format': 'JSON',
'RegionId': 'cn-shanghai',
'SignatureMethod': 'HMAC-SHA1',
'SignatureNonce': str(uuid.uuid1()),
'SignatureVersion': '1.0',
'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
'Version': '2019-02-28'}
# 构造规范化的请求字符串
query_string = AccessToken._encode_dict(parameters)
print('规范化的请求字符串: %s' % query_string)
# 构造待签名字符串
string_to_sign = 'GET' + '&' + AccessToken._encode_text('/') + '&' + AccessToken._encode_text(query_string)
print('待签名的字符串: %s' % string_to_sign)
# 计算签名
secreted_string = hmac.new(bytes(access_key_secret + '&', encoding='utf-8'),
bytes(string_to_sign, encoding='utf-8'),
hashlib.sha1).digest()
signature = base64.b64encode(secreted_string)
print('签名: %s' % signature)
# 进行URL编码
signature = AccessToken._encode_text(signature)
print('URL编码后的签名: %s' % signature)
# 调用服务
full_url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s' % (signature, query_string)
print('url: %s' % full_url)
# 提交HTTP GET请求
response = requests.get(full_url)
if response.ok:
root_obj = response.json()
key = 'Token'
if key in root_obj:
token = root_obj[key]['Id']
expire_time = root_obj[key]['ExpireTime']
return token, expire_time
print(response.text)
return None, None
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)
# 新增空值判断逻辑
access_key_id = config.get("access_key_id")
access_key_secret = config.get("access_key_secret")
if access_key_id and access_key_secret:
# 使用密钥对生成临时token
token, expire_time = AccessToken.create_token(access_key_id, access_key_secret)
else:
# 直接使用预生成的长期token
token = config.get("token")
expire_time = None
print('token: %s, expire time(s): %s' % (token, expire_time))
self.appkey = config.get("appkey") self.appkey = config.get("appkey")
self.token = config.get("token") self.token = token
self.format = config.get("format", "wav") self.format = config.get("format", "wav")
self.sample_rate = config.get("sample_rate", 16000) self.sample_rate = config.get("sample_rate", 16000)
self.voice = config.get("voice", "xiaoyun") self.voice = config.get("voice", "xiaoyun")
@@ -4,6 +4,7 @@ import json
import base64 import base64
import requests import requests
from datetime import datetime from datetime import datetime
from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
@@ -17,6 +18,7 @@ class TTSProvider(TTSProviderBase):
self.api_url = config.get("api_url") self.api_url = config.get("api_url")
self.authorization = config.get("authorization") self.authorization = config.get("authorization")
self.header = {"Authorization": f"{self.authorization}{self.access_token}"} self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
check_model_key("TTS", self.access_token)
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}")
@@ -1,4 +1,3 @@
import base64 import base64
import os import os
import uuid import uuid
@@ -9,6 +8,7 @@ from pydantic import BaseModel, Field, conint, model_validator
from typing_extensions import Annotated from typing_extensions import Annotated
from datetime import datetime from datetime import datetime
from typing import Literal from typing import Literal
from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
from config.logger import setup_logging from config.logger import setup_logging
@@ -24,7 +24,7 @@ class ServeReferenceAudio(BaseModel):
def decode_audio(cls, values): def decode_audio(cls, values):
audio = values.get("audio") audio = values.get("audio")
if ( if (
isinstance(audio, str) and len(audio) > 255 isinstance(audio, str) and len(audio) > 255
): # Check if audio is a string (Base64) ): # Check if audio is a string (Base64)
try: try:
values["audio"] = base64.b64decode(audio) values["audio"] = base64.b64decode(audio)
@@ -36,6 +36,7 @@ class ServeReferenceAudio(BaseModel):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"ServeReferenceAudio(text={self.text!r}, audio_size={len(self.audio)})" return f"ServeReferenceAudio(text={self.text!r}, audio_size={len(self.audio)})"
class ServeTTSRequest(BaseModel): class ServeTTSRequest(BaseModel):
text: str text: str
chunk_length: Annotated[int, conint(ge=100, le=300, strict=True)] = 200 chunk_length: Annotated[int, conint(ge=100, le=300, strict=True)] = 200
@@ -70,6 +71,7 @@ def audio_to_bytes(file_path):
wav = wav_file.read() wav = wav_file.read()
return wav return wav
def read_ref_text(ref_text): def read_ref_text(ref_text):
path = Path(ref_text) path = Path(ref_text)
if path.exists() and path.is_file(): if path.exists() and path.is_file():
@@ -77,31 +79,32 @@ def read_ref_text(ref_text):
return file.read() return file.read()
return ref_text return ref_text
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)
self.reference_id = config.get("reference_id") self.reference_id = config.get("reference_id")
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 = config.get("channels", 1)
self.rate = config.get("rate",44100) self.rate = config.get("rate", 44100)
self.api_key = config.get("api_key","YOUR_API_KEY") self.api_key = config.get("api_key", "YOUR_API_KEY")
if "" in self.api_key: have_key = check_model_key("FishSpeech TTS", self.api_key)
logger.bind(tag=TAG).error("你还没配置FishSpeech TTS的密钥,请在配置文件中配置密钥,否则无法正常工作") 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 = config.get("max_new_tokens", 1024)
self.chunk_length = config.get("chunk_length",200) self.chunk_length = config.get("chunk_length", 200)
self.top_p = config.get("top_p",0.7) self.top_p = config.get("top_p", 0.7)
self.repetition_penalty = config.get("repetition_penalty",1.2) self.repetition_penalty = config.get("repetition_penalty", 1.2)
self.temperature = config.get("temperature",0.7) self.temperature = config.get("temperature", 0.7)
self.streaming = config.get("streaming",False) self.streaming = 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")
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}")
@@ -148,11 +151,9 @@ class TTSProvider(TTSProviderBase):
with open(output_file, "wb") as audio_file: with open(output_file, "wb") as audio_file:
audio_file.write(audio_content) audio_file.write(audio_content)
else: else:
print(f"Request failed with status code {response.status_code}") print(f"Request failed with status code {response.status_code}")
print(response.json()) print(response.json())
@@ -0,0 +1,52 @@
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.text_lang = config.get("text_lang", "audo")
self.ref_audio_path = config.get("ref_audio_path")
self.prompt_lang = config.get("prompt_lang")
self.prompt_text = config.get("prompt_text")
self.top_k = config.get("top_k", 5)
self.top_p = config.get("top_p", 1)
self.temperature = config.get("temperature", 1)
self.sample_steps = config.get("sample_steps", 16)
self.media_type = config.get("media_type", "wav")
self.streaming_mode = config.get("streaming_mode", False)
self.threshold = config.get("threshold", 30)
def generate_filename(self, extension=".wav"):
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):
request_params = {
"text": text,
"text_lang": self.text_lang,
"ref_audio_path": self.ref_audio_path,
"prompt_lang": self.prompt_lang,
"prompt_text": self.prompt_text,
"top_k": self.top_k,
"top_p": self.top_p,
"temperature": self.temperature,
"sample_steps": self.sample_steps,
"media_type": self.media_type,
"streaming_mode": self.streaming_mode,
"threshold": self.threshold,
}
resp = requests.get(self.url, params=request_params)
if resp.status_code == 200:
with open(output_file, "wb") as file:
file.write(resp.content)
else:
logger.bind(tag=TAG).error(f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}")
@@ -0,0 +1,40 @@
import os
import uuid
import requests
from datetime import datetime
from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase
class TTSProvider(TTSProviderBase):
def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file)
self.api_key = config.get("api_key")
self.api_url = config.get("api_url", "https://api.openai.com/v1/audio/speech")
self.model = config.get("model", "tts-1")
self.voice = config.get("voice", "alloy")
self.response_format = "wav"
self.speed = config.get("speed", 1.0)
self.output_file = config.get("output_file", "tmp/")
check_model_key("TTS", self.api_key)
def generate_filename(self, extension=".wav"):
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):
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
data = {
"model": self.model,
"input": text,
"voice": self.voice,
"response_format": "wav",
"speed": self.speed
}
response = requests.post(self.api_url, json=data, headers=headers)
if response.status_code == 200:
with open(output_file, "wb") as audio_file:
audio_file.write(response.content)
else:
raise Exception(f"OpenAI TTS请求失败: {response.status_code} - {response.text}")
@@ -0,0 +1,64 @@
import os
import uuid
import json
import requests
import shutil
from datetime import datetime
from core.providers.tts.base import TTSProviderBase
class TTSProvider(TTSProviderBase):
def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file)
self.url = config.get("url", "https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=")
self.voice_id = config.get("voice_id", 1695)
self.token = config.get("token")
self.to_lang = config.get("to_lang")
self.volume_change_dB = config.get("volume_change_dB", 0)
self.speed_factor = config.get("speed_factor", 1)
self.stream = config.get("stream", False)
self.output_file = config.get("output_file")
self.pitch_factor = config.get("pitch_factor", 0)
self.format = config.get("format", "mp3")
self.emotion = config.get("emotion", 1)
self.header = {
"Content-Type": "application/json"
}
def generate_filename(self, extension=".mp3"):
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):
url = f'{self.url}{self.token}'
result = "firefly"
payload = json.dumps({
"to_lang": self.to_lang,
"text": text,
"emotion": self.emotion,
"format": self.format,
"volume_change_dB": self.volume_change_dB,
"voice_id": self.voice_id,
"pitch_factor": self.pitch_factor,
"speed_factor": self.speed_factor,
"token": self.token
})
resp = requests.request("POST", url, data=payload)
if resp.status_code != 200:
return None
resp_json = resp.json()
try:
result = resp_json['url'] + ':' + str(
resp_json[
'port']) + '/flashsummary/retrieveFileData?stream=True&token=' + self.token + '&voice_audio_path=' + \
resp_json['voice_path']
except Exception as e:
print("error:", e)
audio_content = requests.get(result)
with open(output_file, "wb") as f:
f.write(audio_content.content)
return True
voice_path = resp_json.get("voice_path")
des_path = output_file
shutil.move(voice_path, des_path)
@@ -26,6 +26,9 @@ class 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]]:
if memory_str is None or len(memory_str) == 0:
return self.get_llm_dialogue()
# 构建带记忆的对话 # 构建带记忆的对话
dialogue = [] dialogue = []
+17
View File
@@ -0,0 +1,17 @@
import os
import sys
from config.logger import setup_logging
import importlib
logger = setup_logging()
def create_instance(class_name, *args, **kwargs):
# 创建intent实例
if os.path.exists(os.path.join('core', 'providers', 'intent', class_name, f'{class_name}.py')):
lib_name = f'core.providers.intent.{class_name}.{class_name}'
if lib_name not in sys.modules:
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
return sys.modules[lib_name].IntentProvider(*args, **kwargs)
raise ValueError(f"不支持的intent类型: {class_name},请检查该配置的type是否设置正确")
+7 -29
View File
@@ -1,9 +1,9 @@
import os import os
import re
import json import json
import yaml import yaml
import socket import socket
import subprocess import subprocess
import logging
def get_project_dir(): def get_project_dir():
@@ -75,7 +75,7 @@ def get_string_no_punctuation_or_emoji(s):
def remove_punctuation_and_length(text): def remove_punctuation_and_length(text):
# 全角符号和半角符号的Unicode范围 # 全角符号和半角符号的Unicode范围
full_width_punctuations = '!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~' full_width_punctuations = '!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~'
half_width_punctuations = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~' half_width_punctuations = r'!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
space = ' ' # 半角空格 space = ' ' # 半角空格
full_width_space = ' ' # 全角空格 full_width_space = ' ' # 全角空格
@@ -87,35 +87,13 @@ def remove_punctuation_and_length(text):
return 0, "" return 0, ""
return len(result), result return len(result), result
def check_model_key(modelType, modelKey):
def check_password(password): if "" in modelKey:
""" logging.error("你还没配置" + modelType + "的密钥,请在配置文件中配置密钥,否则无法正常工作")
检查密码是否满足以下条件:
1. 密码长度大于八位。
2. 密码包含英文和数字。
3. 密码不能包含“xiaozhi”字符。
:param password: 要检查的密码
:return: 如果密码满足条件,则返回True;否则返回False。
"""
# 检查密码长度
if len(password) < 8:
return False return False
# 检查是否包含英文字符和数字
if not re.search(r'[A-Za-z]', password) or not re.search(r'[0-9]', password):
return False
# 检查是否包含“xiaozhi”字符
if "xiaozhi" in password:
return False
if "1234" in password:
return False
# 如果满足所有条件,则返回True
return True return True
def check_ffmpeg_installed(): def check_ffmpeg_installed():
ffmpeg_installed = False ffmpeg_installed = False
try: try:
@@ -140,4 +118,4 @@ def check_ffmpeg_installed():
error_msg += "\n建议您:\n" error_msg += "\n建议您:\n"
error_msg += "1、按照项目的安装文档,正确进入conda环境\n" error_msg += "1、按照项目的安装文档,正确进入conda环境\n"
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n" error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
raise ValueError(error_msg) raise ValueError(error_msg)
+11 -4
View File
@@ -4,7 +4,7 @@ from config.logger import setup_logging
from core.connection import ConnectionHandler from core.connection import ConnectionHandler
from core.handle.musicHandler import MusicHandler from core.handle.musicHandler import MusicHandler
from core.utils.util import get_local_ip from core.utils.util import get_local_ip
from core.utils import asr, vad, llm, tts, memory from core.utils import asr, vad, llm, tts, memory, intent
TAG = __name__ TAG = __name__
@@ -13,11 +13,11 @@ class WebSocketServer:
def __init__(self, config: dict): def __init__(self, config: dict):
self.config = config self.config = config
self.logger = setup_logging() self.logger = setup_logging()
self._vad, self._asr, self._llm, self._tts, self._music, self._memory = self._create_processing_instances() self._vad, self._asr, self._llm, self._tts, self._music, self._memory, self.intent = self._create_processing_instances()
self.active_connections = set() # 添加全局连接记录 self.active_connections = set() # 添加全局连接记录
def _create_processing_instances(self): def _create_processing_instances(self):
memory_cls_name = self.config["selected_module"].get("Memory", "mem0ai") # 默认使用mem0ai memory_cls_name = self.config["selected_module"].get("Memory", "nomem") # 默认使用nomem
has_memory_cfg = self.config.get("Memory") and memory_cls_name in self.config["Memory"] has_memory_cfg = self.config.get("Memory") and memory_cls_name in self.config["Memory"]
memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {} memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {}
@@ -52,6 +52,13 @@ class WebSocketServer:
), ),
MusicHandler(self.config), MusicHandler(self.config),
memory.create_instance(memory_cls_name, memory_cfg), memory.create_instance(memory_cls_name, memory_cfg),
intent.create_instance(
self.config["selected_module"]["Intent"]
if not 'type' in self.config["Intent"][self.config["selected_module"]["Intent"]]
else
self.config["Intent"][self.config["selected_module"]["Intent"]]["type"],
self.config["Intent"][self.config["selected_module"]["Intent"]]
),
) )
async def start(self): async def start(self):
@@ -71,7 +78,7 @@ class WebSocketServer:
async def _handle_connection(self, websocket): async def _handle_connection(self, websocket):
"""处理新连接,每次创建独立的ConnectionHandler""" """处理新连接,每次创建独立的ConnectionHandler"""
# 创建ConnectionHandler时传入当前server实例 # 创建ConnectionHandler时传入当前server实例
handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts, self._music, self._memory) handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts, self._music, self._memory, self.intent)
self.active_connections.add(handler) self.active_connections.add(handler)
try: try:
await handler.handle_connection(websocket) await handler.handle_connection(websocket)
+25 -5
View File
@@ -1,18 +1,38 @@
# 如果本机已经安装了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
# 如果你的mysql账号和密码有修改过,记得修改下方数据库的账号和密码
# 记得修改下方数据库的IPip不能写127.0.0.1或localhost,否则容器无法访问,要写你电脑局域网ip
version: '3' version: '3'
services: services:
xiaozhi-esp32-server: xiaozhi-esp32-server:
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
container_name: xiaozhi-esp32-server container_name: xiaozhi-esp32-server
restart: always restart: always
security_opt: security_opt:
- seccomp:unconfined - seccomp:unconfined
environment:
- TZ=Asia/Shanghai
ports: ports:
# ws服务端 # ws服务端
- "8000:8000" - "8000:8000"
# 管理后台
- "8002:8002"
volumes: volumes:
# 配置文件目录 # 配置文件目录
- ./data:/app/data - ./data:/opt/xiaozhi-esp32-server/data
# 模型文件挂接,很重要 # 模型文件挂接,很重要
- ./models/SenseVoiceSmall/model.pt:/app/models/SenseVoiceSmall/model.pt - ./models/SenseVoiceSmall/model.pt:/opt/xiaozhi-esp32-server/models/SenseVoiceSmall/model.pt
# #智控台还没开发好,还不能完全使用,会报很多错误,如果是非技术人员,请不要启用智控台服务
# xiaozhi-esp32-server-web:
# image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
# container_name: xiaozhi-esp32-server-web
# restart: always
# ports:
# - "8002:8002"
# environment:
# - TZ=Asia/Shanghai
# ##记得改mysql和redis IP 密码
# - SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://192.168.1.20:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
# - SPRING_DATASOURCE_DRUID_USERNAME=root
# - SPRING_DATASOURCE_DRUID_PASSWORD=123456
# - SPRING_DATA_REDIS_HOST=192.168.1.20
# - SPRING_DATA_REDIS_PORT=6379
-4854
View File
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
[tool.poetry]
name = "xiaozhi-esp32-server"
version = "0.1.0"
description = ""
authors = ["kalicyh <34980061+kaliCYH@users.noreply.github.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
pyyml = "0.0.2"
torch = "2.2.2"
silero-vad = "5.1.2"
websockets = "14.2"
numpy = "1.26.4"
pydub = "0.25.1"
funasr = "1.2.3"
torchaudio = "2.2.2"
openai = "1.61.0"
google-generativeai = "0.8.4"
edge-tts = "7.0.0"
httpx = "0.27.2"
aiohttp = "3.9.3"
aiohttp-cors = "0.7.0"
ormsgpack = "1.7.0"
ruamel-yaml = "0.18.10"
setuptools = "^75.8.0"
loguru = "^0.7.3"
opuslib-next = "^1.1.2"
fastapi = {extras = ["all"], version = "^0.115.8"}
uvicorn = "^0.34.0"
pyjwt = "^2.10.1"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
bcrypt = "^4.2.1"
sqlalchemy = "^2.0.38"
pymysql = "^1.1.1"
asyncpg = "^0.30.0"
onnxruntime = "1.19.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"