mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8f06f5718 | ||
|
|
c458a1b59a | ||
|
|
29d59b3974 | ||
|
|
27f267cb3d | ||
|
|
0a26ad8925 | ||
|
|
a8c6c7cb36 | ||
|
|
81fee53085 | ||
|
|
7c5f2d95a4 | ||
|
|
3d6bd0e23e | ||
|
|
8064e84276 | ||
|
|
f8ece2d984 | ||
|
|
442eef36ba | ||
|
|
7d737ad312 | ||
|
|
2b662d3a14 | ||
|
|
4f3fae81c1 | ||
|
|
7b266ad7c0 | ||
|
|
16601a67e3 | ||
|
|
224d59d50d | ||
|
|
f9472627f4 | ||
|
|
69735207e6 | ||
|
|
3b47f18a12 | ||
|
|
63f34e5a82 | ||
|
|
9c2b2a2dcc | ||
|
|
baff979ab4 | ||
|
|
07fe330e4f | ||
|
|
f98c3efef7 | ||
|
|
8b74dec910 | ||
|
|
dc4b8a5002 | ||
|
+2 |
dd5fecdf5a | ||
|
|
6c52754c8f | ||
|
|
e1ebcfa865 | ||
|
|
6464c81e72 | ||
|
|
0b7abfae3a | ||
|
|
76c16ad16e | ||
|
|
1f722477ce |
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
__pycache__
|
||||
*.pyc
|
||||
.env
|
||||
Dockerfile
|
||||
tmp/
|
||||
data/
|
||||
@@ -7,30 +7,31 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release Docker image
|
||||
name: Release Docker images
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: write
|
||||
id-token: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Check Disk Space
|
||||
run: |
|
||||
df -h
|
||||
docker system df
|
||||
|
||||
- name: Clean up Docker resources
|
||||
run: |
|
||||
docker system prune -af
|
||||
docker builder prune -af
|
||||
- name: Check out the repo
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
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
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -42,13 +43,26 @@ jobs:
|
||||
run: |
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build_push
|
||||
# 构建 xiaozhi-server 镜像
|
||||
- name: Build and push xiaozhi-server
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-server
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
|
||||
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
|
||||
Executable → Regular
+1
-1
@@ -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 main/xiaozhi-server/ .
|
||||
COPY main/xiaozhi-server .
|
||||
|
||||
# 启动应用
|
||||
CMD ["python", "app.py"]
|
||||
@@ -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"]
|
||||
@@ -100,6 +100,8 @@
|
||||
- [小智安卓端](https://github.com/TOM88812/xiaozhi-android-client)
|
||||
- [小智电脑端](https://github.com/Huang-junsen/py-xiaozhi)
|
||||
|
||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
||||
|
||||
---
|
||||
|
||||
## 警告 ⚠️
|
||||
@@ -129,55 +131,67 @@ server:
|
||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
||||
- **对话交互**
|
||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
||||
- **意图识别**
|
||||
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
|
||||
- **多语言识别**
|
||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
||||
- **LLM 模块**
|
||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
||||
- **TTS 模块**
|
||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
||||
- **记忆功能**
|
||||
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
|
||||
|
||||
### 正在开发 🚧
|
||||
|
||||
- 对话记忆功能
|
||||
- 多种心情模式
|
||||
- 智控台webui
|
||||
- iot功能
|
||||
|
||||

|
||||
---
|
||||
|
||||
## 本项目支持的平台/组件列表 📋
|
||||
|
||||
### LLM
|
||||
### LLM 语言模型
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:------------------:|:---------------------:|:--------:|:-----------------------------------------------------------------:|
|
||||
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
||||
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
|
||||
| LLM | 智谱(ChatGLMLLM) | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
||||
| LLM | OllamaLLM | ollama 接口调用 | 免费/自定义 | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
|
||||
| LLM | DifyLLM | dify 接口调用 | 消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
|
||||
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
||||
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:------------------:|:---------------------:|:-----------:|:-----------------------------------------------------------------------------------------------------------------------:|
|
||||
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
||||
| LLM | DoubaoLLM | openai 接口调用 | 消耗 token | [点击申请密钥](https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-pro-32k&projectName=undefined) |
|
||||
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
|
||||
| LLM | 智谱(ChatGLMLLM) | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
||||
| LLM | OllamaLLM | ollama 接口调用 | 免费/消耗 token | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
|
||||
| LLM | DifyLLM | dify 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
|
||||
| LLM | FastgptLLM | fastgpt 接口调用 | 免费/消耗 token | 本地化部署,注意配置提示词需在 Fastgpt 控制台设置 |
|
||||
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
||||
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
||||
|
||||
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||
|
||||
---
|
||||
|
||||
### TTS
|
||||
### TTS 语音合成
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:|
|
||||
| TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 |
|
||||
| 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 | TTS302AI | 接口调用 | 消耗 token | [点击创建密钥](https://dash.302.ai/apis/list) |
|
||||
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
|
||||
| TTS | ACGNTTS | 接口调用 | 消耗 token | [联系网站管理员购买密钥](www.ttson.cn) |
|
||||
| TTS | OpenAITTS | 接口调用 | 消耗 token | 境外使用,境外购买 |
|
||||
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
|
||||
| TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
|
||||
| TTS | GPT_SOVITS_V3 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
|
||||
| TTS | MinimaxTTS | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
|
||||
|
||||
---
|
||||
|
||||
### VAD
|
||||
### VAD 语音活动检测
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:---------:|:----:|:----:|:--:|
|
||||
@@ -185,7 +199,7 @@ server:
|
||||
|
||||
---
|
||||
|
||||
### ASR
|
||||
### ASR 语音识别
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:---:|:---------:|:----:|:----:|:--:|
|
||||
@@ -194,11 +208,21 @@ server:
|
||||
|
||||
---
|
||||
|
||||
### Memory
|
||||
### Memory 记忆存储
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:------:|:----:|:----:|:--:|
|
||||
| Memory | mem0ai | 接口调用 | 免费 | |
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:---------------:|:----:|:--------:|:--:|
|
||||
| Memory | mem0ai | 接口调用 | 100次/月额度 | |
|
||||
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||
|
||||
---
|
||||
|
||||
### Intent 意图识别
|
||||
|
||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
||||
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
||||
|
||||
---
|
||||
|
||||
@@ -208,14 +232,9 @@ server:
|
||||
|
||||
本项目支持以下三种部署方式,您可根据实际需求选择。
|
||||
|
||||
本项目的文档主要是`文字版本`的教程,如果你想要`视频版本`
|
||||
的教程,您可以学习一下[这个大佬的手把手教程](https://www.bilibili.com/video/BV1gePuejEvT)。
|
||||
|
||||
如果你能把`文字版本的教程`和`视频版本的教程`结合起来一起看,可以让你更快上手。
|
||||
|
||||
1. [Docker 快速部署](./docs/Deployment.md)
|
||||
|
||||
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。
|
||||
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。视频教程可参考[美女大佬教你Docker部署](https://www.bilibili.com/video/BV1RNQnYDE5t)
|
||||
|
||||
2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
|
||||
|
||||
@@ -225,45 +244,24 @@ server:
|
||||
|
||||
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
|
||||
|
||||
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。
|
||||
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。视频教程可参考[帅哥大佬教你源码部署](https://www.bilibili.com/video/BV1GvQWYZEd2)
|
||||
|
||||
### 二、[固件编译](./docs/firmware-build.md)
|
||||
|
||||
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
|
||||
|
||||
编译成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题 ❓
|
||||
|
||||
### 1、TTS 经常失败,经常超时 ⏰
|
||||
|
||||
建议:如果 `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、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
||||
### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
||||
|
||||
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
|
||||
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
|
||||
|
||||
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁
|
||||
### 2、为什么会出现“TTS 任务出错 文件不存在”?📁
|
||||
|
||||
建议:检查一下是否正确使用`conda` 安装了`libopus`和`ffmpeg`库。
|
||||
|
||||
@@ -274,7 +272,12 @@ conda install conda-forge::libopus
|
||||
conda install conda-forge::ffmpeg
|
||||
```
|
||||
|
||||
### 6、如何提高小智对话响应速度? ⚡
|
||||
### 3、TTS 经常失败,经常超时 ⏰
|
||||
|
||||
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
||||
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
||||
|
||||
### 4、如何提高小智对话响应速度? ⚡
|
||||
|
||||
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
|
||||
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
||||
@@ -305,7 +308,6 @@ LLM 性能排行:
|
||||
|:-----------|:-----------|:--------|
|
||||
| AliLLM | 0.547s | 1.485s |
|
||||
| ChatGLMLLM | 0.677s | 3.057s |
|
||||
| OllamaLLM | 0.003s | 0.003s |
|
||||
|
||||
TTS 性能排行:
|
||||
|
||||
@@ -332,6 +334,22 @@ TTS 性能排行:
|
||||
- LLM:`AliLLM`
|
||||
- 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、更多问题,可联系我们反馈 💬
|
||||
|
||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||
@@ -355,4 +373,4 @@ TTS 性能排行:
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
+20
-21
@@ -93,7 +93,7 @@ docker logs -f xiaozhi-esp32-server
|
||||
```
|
||||
docker stop xiaozhi-esp32-server
|
||||
docker rm xiaozhi-esp32-server
|
||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||
```
|
||||
|
||||
3、重新按docker方式部署
|
||||
@@ -117,7 +117,7 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
|
||||
-p 8000:8000 \
|
||||
-p 8002:8002 \
|
||||
-v ./:/app \
|
||||
kalicyh/poetry:v3.10_xiaozhi
|
||||
kalicyh/python:xiaozhi
|
||||
```
|
||||
|
||||
然后就和正常开发一样了
|
||||
@@ -127,29 +127,13 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
|
||||
在刚刚的打开的终端运行
|
||||
|
||||
```sh
|
||||
poetry install --no-root
|
||||
```
|
||||
|
||||
```sh
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends libopus0 ffmpeg
|
||||
```
|
||||
|
||||
速度慢可以尝试使用清华镜像
|
||||
|
||||
```sh
|
||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list
|
||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends libopus0 ffmpeg
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 6.运行项目
|
||||
|
||||
```sh
|
||||
poetry run python app.py
|
||||
python app.py
|
||||
```
|
||||
|
||||
# 方式三:本地源码运行
|
||||
@@ -294,4 +278,19 @@ LLM:
|
||||
|
||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[3、TTS 经常失败,经常超时](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[4、如何提高小智对话响应速度?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[5、我说话很慢,停顿时小智老是抢话](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# 致开发者的公开信
|
||||
|
||||
"春江水暖鸭先知,正是河豚欲上时!"
|
||||
|
||||
亲爱的朋友,今天,我怀着无比真挚的心情,向热爱AI技术与创新的你发出这封公开信。
|
||||
|
||||
我们的开源项目 **xiaozhi-esp32-server** 正处在发展的阶段,它承载着我们对智能硬件和低成本民用贾维斯解决方案的美好憧憬,也希望借助每一位开发者的智慧,共同开创技术的新局面。
|
||||
|
||||
## 项目目标
|
||||
|
||||
1. **民用低成本贾维斯解决方案**
|
||||
|
||||
2. **智能联动周边硬件的解决方案**
|
||||
|
||||
## 加入我们
|
||||
|
||||
我们热忱欢迎志同道合的朋友加入,共同为项目贡献力量。参与方式如下:
|
||||
|
||||
### 1、成为普通贡献者
|
||||
|
||||
Fork 项目,提交 PR,由开发者审核后合入主分支。
|
||||
|
||||
### 2、成为开发者
|
||||
|
||||
当你累计提交 3 次有效 PR 后,可以联系群主申请成为开发者,群主将邀请你加入独立的开发者群,共同探讨项目未来。
|
||||
|
||||
## 开发者开发流程
|
||||
|
||||
1. **创建新分支**
|
||||
每个功能点请以新分支方式开发,分支名称应简洁明了,让人一眼看出所实现的功能,避免功能撞车。
|
||||
|
||||
2. **提交 PR 审核**
|
||||
功能开发完成后,请在 GitHub 上提交 PR,由其他开发者审核,审核通过后合并入主分支。
|
||||
|
||||
## 功能点的来源
|
||||
|
||||
- **创新的点子与功能**
|
||||
你的每一个灵感都可能成为项目的突破点,欢迎大胆提出前沿的创意与功能建议。
|
||||
|
||||
- **代码优化**
|
||||
如果你发现代码中存在改进的空间,欢迎提交优化方案,让我们的代码更加高效、易读。
|
||||
|
||||
- **解决 Issues 问题**
|
||||
Issues 中有很多问题需要解决,我们一同完善、打造产品。
|
||||
|
||||
亲爱的开发者们,
|
||||
每一次提交、每一份建议,都是对我们共同梦想的加持。每一代人有每一代人的使命,让我们共同书写智能生活的新篇章!
|
||||
|
||||
期待与你携手前行,共创未来。
|
||||
|
||||
John,2025.3.11,广州
|
||||
+9
-36
@@ -8,41 +8,14 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
||||
```
|
||||
2、编译docker镜像
|
||||
```
|
||||
#进入xiaozhi-server目录
|
||||
cd main/xiaozhi-server/
|
||||
# 普通编译
|
||||
docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip .
|
||||
```
|
||||
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
|
||||
#进入项目根目录
|
||||
# 编译server
|
||||
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
|
||||
# 编译web
|
||||
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
|
||||
|
||||
# 编译完成后,可以使用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
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;'
|
||||
@@ -87,3 +87,19 @@ https://espressif.github.io/esp-launchpad/
|
||||
|
||||
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
|
||||
翻到:`方式二: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-)
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
# 项目目录介绍
|
||||
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
|
||||
|
||||
如果你会开发,我们非常欢迎您的加入。
|
||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](../docs/contributor_open_letter.md),欢迎归队!
|
||||
|
||||
```
|
||||
xiaozhi-esp32-server
|
||||
|
||||
Executable → Regular
@@ -23,9 +23,7 @@
|
||||
<mybatisplus.version>3.5.5</mybatisplus.version>
|
||||
<hutool.version>5.8.24</hutool.version>
|
||||
<jsoup.version>1.19.1</jsoup.version>
|
||||
<jasypt.version>3.0.5</jasypt.version>
|
||||
<knife4j.version>4.6.0</knife4j.version>
|
||||
<quartz.version>2.3.2</quartz.version>
|
||||
<shiro.version>2.0.2</shiro.version>
|
||||
<captcha.version>1.6.2</captcha.version>
|
||||
<guava.version>33.0.0-jre</guava.version>
|
||||
@@ -34,21 +32,6 @@
|
||||
</properties>
|
||||
|
||||
<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>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
@@ -83,6 +66,7 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- 验证码工具包 -->
|
||||
<dependency>
|
||||
<groupId>com.github.whvcse</groupId>
|
||||
<artifactId>easy-captcha</artifactId>
|
||||
|
||||
@@ -8,6 +8,6 @@ public class AdminApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AdminApplication.class, args);
|
||||
System.out.println("http://localhost:8002/xiaozhi-esp32-api/doc.html");
|
||||
System.out.println("http://localhost:8002/xiaozhi-esp32-api/api/v1/doc.html");
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,40 @@
|
||||
package xiaozhi.common.aspect;
|
||||
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Redis切面处理类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class RedisAspect {
|
||||
/**
|
||||
* 是否开启redis缓存 true开启 false关闭
|
||||
*/
|
||||
@Value("${renren.redis.open}")
|
||||
private boolean open;
|
||||
|
||||
@Around("execution(* xiaozhi.common.redis.RedisUtils.*(..))")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
Object result = null;
|
||||
if (open) {
|
||||
try {
|
||||
result = point.proceed();
|
||||
} catch (Exception e) {
|
||||
log.error("redis error", e);
|
||||
throw new RenException(ErrorCode.REDIS_ERROR);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
package xiaozhi.common.aspect;
|
||||
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Redis切面处理类
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class RedisAspect {
|
||||
/**
|
||||
* 是否开启redis缓存 true开启 false关闭
|
||||
*/
|
||||
@Value("${renren.redis.open}")
|
||||
private boolean open;
|
||||
|
||||
@Around("execution(* xiaozhi.common.redis.RedisUtils.*(..))")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
Object result = null;
|
||||
if (open) {
|
||||
try {
|
||||
result = point.proceed();
|
||||
} catch (Exception e) {
|
||||
log.error("redis error", e);
|
||||
throw new RenException(ErrorCode.REDIS_ERROR);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,17 +22,24 @@ public interface ErrorCode {
|
||||
int CAPTCHA_ERROR = 10007;
|
||||
int SUB_MENU_EXIST = 10008;
|
||||
int PASSWORD_ERROR = 10009;
|
||||
|
||||
int SUPERIOR_DEPT_ERROR = 10011;
|
||||
int SUPERIOR_MENU_ERROR = 10012;
|
||||
int DATA_SCOPE_PARAMS_ERROR = 10013;
|
||||
int DEPT_SUB_DELETE_ERROR = 10014;
|
||||
int DEPT_USER_DELETE_ERROR = 10015;
|
||||
|
||||
int UPLOAD_FILE_EMPTY = 10019;
|
||||
int TOKEN_NOT_EMPTY = 10020;
|
||||
int TOKEN_INVALID = 10021;
|
||||
int ACCOUNT_LOCK = 10022;
|
||||
|
||||
int OSS_UPLOAD_FILE_ERROR = 10024;
|
||||
|
||||
int REDIS_ERROR = 10027;
|
||||
int JOB_ERROR = 10028;
|
||||
int INVALID_SYMBOL = 10029;
|
||||
int PASSWORD_LENGTH_ERROR = 10030;
|
||||
int PASSWORD_WEAK_ERROR = 10031;
|
||||
int DEL_MYSELF_ERROR = 10032;
|
||||
}
|
||||
|
||||
@@ -1,108 +1,108 @@
|
||||
package xiaozhi.common.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 基础服务接口,所有Service接口都要继承
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public interface BaseService<T> {
|
||||
Class<T> currentModelClass();
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入一条记录(选择字段,策略插入)
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
*/
|
||||
boolean insert(T entity);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入(批量),该方法不支持 Oracle、SQL Server
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
*/
|
||||
boolean insertBatch(Collection<T> entityList);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入(批量),该方法不支持 Oracle、SQL Server
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 插入批次数量
|
||||
*/
|
||||
boolean insertBatch(Collection<T> entityList, int batchSize);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 选择修改
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
*/
|
||||
boolean updateById(T entity);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 whereEntity 条件,更新记录
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
* @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
|
||||
*/
|
||||
boolean update(T entity, Wrapper<T> updateWrapper);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据ID 批量更新
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
*/
|
||||
boolean updateBatchById(Collection<T> entityList);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据ID 批量更新
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 更新批次数量
|
||||
*/
|
||||
boolean updateBatchById(Collection<T> entityList, int batchSize);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 查询
|
||||
* </p>
|
||||
*
|
||||
* @param id 主键ID
|
||||
*/
|
||||
T selectById(Serializable id);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 删除
|
||||
* </p>
|
||||
*
|
||||
* @param id 主键ID
|
||||
*/
|
||||
boolean deleteById(Serializable id);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 删除(根据ID 批量删除)
|
||||
* </p>
|
||||
*
|
||||
* @param idList 主键ID列表
|
||||
*/
|
||||
boolean deleteBatchIds(Collection<? extends Serializable> idList);
|
||||
package xiaozhi.common.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 基础服务接口,所有Service接口都要继承
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public interface BaseService<T> {
|
||||
Class<T> currentModelClass();
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入一条记录(选择字段,策略插入)
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
*/
|
||||
boolean insert(T entity);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入(批量),该方法不支持 Oracle、SQL Server
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
*/
|
||||
boolean insertBatch(Collection<T> entityList);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 插入(批量),该方法不支持 Oracle、SQL Server
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 插入批次数量
|
||||
*/
|
||||
boolean insertBatch(Collection<T> entityList, int batchSize);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 选择修改
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
*/
|
||||
boolean updateById(T entity);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 whereEntity 条件,更新记录
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
* @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
|
||||
*/
|
||||
boolean update(T entity, Wrapper<T> updateWrapper);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据ID 批量更新
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
*/
|
||||
boolean updateBatchById(Collection<T> entityList);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据ID 批量更新
|
||||
* </p>
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 更新批次数量
|
||||
*/
|
||||
boolean updateBatchById(Collection<T> entityList, int batchSize);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 查询
|
||||
* </p>
|
||||
*
|
||||
* @param id 主键ID
|
||||
*/
|
||||
T selectById(Serializable id);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 根据 ID 删除
|
||||
* </p>
|
||||
*
|
||||
* @param id 主键ID
|
||||
*/
|
||||
boolean deleteById(Serializable id);
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 删除(根据ID 批量删除)
|
||||
* </p>
|
||||
*
|
||||
* @param idList 主键ID列表
|
||||
*/
|
||||
boolean deleteBatchIds(Collection<? extends Serializable> idList);
|
||||
}
|
||||
@@ -1,214 +1,214 @@
|
||||
package xiaozhi.common.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import org.apache.ibatis.binding.MapperMethod;
|
||||
import org.apache.ibatis.logging.Log;
|
||||
import org.apache.ibatis.logging.LogFactory;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* 基础服务类,所有Service都要继承
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> {
|
||||
@Autowired
|
||||
protected M baseDao;
|
||||
protected Log log = LogFactory.getLog(getClass());
|
||||
|
||||
/**
|
||||
* 获取分页对象
|
||||
*
|
||||
* @param params 分页查询参数
|
||||
* @param defaultOrderField 默认排序字段
|
||||
* @param isAsc 排序方式
|
||||
*/
|
||||
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
||||
//分页参数
|
||||
long curPage = 1;
|
||||
long limit = 10;
|
||||
|
||||
if (params.get(Constant.PAGE) != null) {
|
||||
curPage = Long.parseLong((String) params.get(Constant.PAGE));
|
||||
}
|
||||
if (params.get(Constant.LIMIT) != null) {
|
||||
limit = Long.parseLong((String) params.get(Constant.LIMIT));
|
||||
}
|
||||
|
||||
//分页对象
|
||||
Page<T> page = new Page<>(curPage, limit);
|
||||
|
||||
//分页参数
|
||||
params.put(Constant.PAGE, page);
|
||||
|
||||
//排序字段
|
||||
String orderField = (String) params.get(Constant.ORDER_FIELD);
|
||||
String order = (String) params.get(Constant.ORDER);
|
||||
|
||||
//前端字段排序
|
||||
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
|
||||
if (Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.asc(orderField));
|
||||
} else {
|
||||
return page.addOrder(OrderItem.desc(orderField));
|
||||
}
|
||||
}
|
||||
|
||||
//没有排序字段,则不排序
|
||||
if (StringUtils.isBlank(defaultOrderField)) {
|
||||
return page;
|
||||
}
|
||||
|
||||
//默认排序
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target) {
|
||||
List<T> targetList = ConvertUtils.sourceToTarget(list, target);
|
||||
|
||||
return new PageData<>(targetList, total);
|
||||
}
|
||||
|
||||
protected <T> PageData<T> getPageData(IPage page, Class<T> target) {
|
||||
return getPageData(page.getRecords(), page.getTotal(), target);
|
||||
}
|
||||
|
||||
protected void paramsToLike(Map<String, Object> params, String... likes) {
|
||||
for (String like : likes) {
|
||||
String val = (String) params.get(like);
|
||||
if (StringUtils.isNotBlank(val)) {
|
||||
params.put(like, "%" + val + "%");
|
||||
} else {
|
||||
params.put(like, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 判断数据库操作是否成功
|
||||
* </p>
|
||||
* <p>
|
||||
* 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
|
||||
* </p>
|
||||
*
|
||||
* @param result 数据库操作返回影响条数
|
||||
* @return boolean
|
||||
*/
|
||||
protected static boolean retBool(Integer result) {
|
||||
return SqlHelper.retBool(result);
|
||||
}
|
||||
|
||||
protected Class<M> currentMapperClass() {
|
||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> currentModelClass() {
|
||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||
}
|
||||
|
||||
protected String getSqlStatement(SqlMethod sqlMethod) {
|
||||
return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insert(T entity) {
|
||||
return BaseServiceImpl.retBool(baseDao.insert(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insertBatch(Collection<T> entityList) {
|
||||
return insertBatch(entityList, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insertBatch(Collection<T> entityList, int batchSize) {
|
||||
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
|
||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量操作
|
||||
*/
|
||||
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
|
||||
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateById(T entity) {
|
||||
return BaseServiceImpl.retBool(baseDao.updateById(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(T entity, Wrapper<T> updateWrapper) {
|
||||
return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateBatchById(Collection<T> entityList) {
|
||||
return updateBatchById(entityList, 30);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
||||
String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
|
||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
|
||||
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
|
||||
param.put(Constants.ENTITY, entity);
|
||||
sqlSession.update(sqlStatement, param);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public T selectById(Serializable id) {
|
||||
return baseDao.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteById(Serializable id) {
|
||||
return SqlHelper.retBool(baseDao.deleteById(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
|
||||
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
|
||||
}
|
||||
package xiaozhi.common.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import org.apache.ibatis.binding.MapperMethod;
|
||||
import org.apache.ibatis.logging.Log;
|
||||
import org.apache.ibatis.logging.LogFactory;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* 基础服务类,所有Service都要继承
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
* Website: https://www.renren.io
|
||||
*/
|
||||
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> {
|
||||
@Autowired
|
||||
protected M baseDao;
|
||||
protected Log log = LogFactory.getLog(getClass());
|
||||
|
||||
/**
|
||||
* 获取分页对象
|
||||
*
|
||||
* @param params 分页查询参数
|
||||
* @param defaultOrderField 默认排序字段
|
||||
* @param isAsc 排序方式
|
||||
*/
|
||||
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
||||
//分页参数
|
||||
long curPage = 1;
|
||||
long limit = 10;
|
||||
|
||||
if (params.get(Constant.PAGE) != null) {
|
||||
curPage = Long.parseLong((String) params.get(Constant.PAGE));
|
||||
}
|
||||
if (params.get(Constant.LIMIT) != null) {
|
||||
limit = Long.parseLong((String) params.get(Constant.LIMIT));
|
||||
}
|
||||
|
||||
//分页对象
|
||||
Page<T> page = new Page<>(curPage, limit);
|
||||
|
||||
//分页参数
|
||||
params.put(Constant.PAGE, page);
|
||||
|
||||
//排序字段
|
||||
String orderField = (String) params.get(Constant.ORDER_FIELD);
|
||||
String order = (String) params.get(Constant.ORDER);
|
||||
|
||||
//前端字段排序
|
||||
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
|
||||
if (Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.asc(orderField));
|
||||
} else {
|
||||
return page.addOrder(OrderItem.desc(orderField));
|
||||
}
|
||||
}
|
||||
|
||||
//没有排序字段,则不排序
|
||||
if (StringUtils.isBlank(defaultOrderField)) {
|
||||
return page;
|
||||
}
|
||||
|
||||
//默认排序
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target) {
|
||||
List<T> targetList = ConvertUtils.sourceToTarget(list, target);
|
||||
|
||||
return new PageData<>(targetList, total);
|
||||
}
|
||||
|
||||
protected <T> PageData<T> getPageData(IPage page, Class<T> target) {
|
||||
return getPageData(page.getRecords(), page.getTotal(), target);
|
||||
}
|
||||
|
||||
protected void paramsToLike(Map<String, Object> params, String... likes) {
|
||||
for (String like : likes) {
|
||||
String val = (String) params.get(like);
|
||||
if (StringUtils.isNotBlank(val)) {
|
||||
params.put(like, "%" + val + "%");
|
||||
} else {
|
||||
params.put(like, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 判断数据库操作是否成功
|
||||
* </p>
|
||||
* <p>
|
||||
* 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
|
||||
* </p>
|
||||
*
|
||||
* @param result 数据库操作返回影响条数
|
||||
* @return boolean
|
||||
*/
|
||||
protected static boolean retBool(Integer result) {
|
||||
return SqlHelper.retBool(result);
|
||||
}
|
||||
|
||||
protected Class<M> currentMapperClass() {
|
||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> currentModelClass() {
|
||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||
}
|
||||
|
||||
protected String getSqlStatement(SqlMethod sqlMethod) {
|
||||
return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insert(T entity) {
|
||||
return BaseServiceImpl.retBool(baseDao.insert(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insertBatch(Collection<T> entityList) {
|
||||
return insertBatch(entityList, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean insertBatch(Collection<T> entityList, int batchSize) {
|
||||
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
|
||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量操作
|
||||
*/
|
||||
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
|
||||
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateById(T entity) {
|
||||
return BaseServiceImpl.retBool(baseDao.updateById(entity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(T entity, Wrapper<T> updateWrapper) {
|
||||
return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateBatchById(Collection<T> entityList) {
|
||||
return updateBatchById(entityList, 30);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
||||
String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
|
||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
|
||||
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
|
||||
param.put(Constants.ENTITY, entity);
|
||||
sqlSession.update(sqlStatement, param);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public T selectById(Serializable id) {
|
||||
return baseDao.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteById(Serializable id) {
|
||||
return SqlHelper.retBool(baseDao.deleteById(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
|
||||
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
|
||||
}
|
||||
}
|
||||
Executable → Regular
@@ -69,6 +69,7 @@ public class ShiroConfig {
|
||||
filterMap.put("/captcha", "anon");
|
||||
filterMap.put("/favicon.ico", "anon");
|
||||
filterMap.put("/mobile/**", "anon");
|
||||
filterMap.put("/user/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
|
||||
+48
-16
@@ -2,41 +2,37 @@ package xiaozhi.modules.security.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.utils.PropertiesUtils;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
import xiaozhi.modules.security.dto.LoginDTO;
|
||||
import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.security.service.CaptchaService;
|
||||
import xiaozhi.modules.security.service.SysUserTokenService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* 登录控制层
|
||||
*/
|
||||
@Tag(name = "登录管理")
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@Tag(name = "登录管理")
|
||||
public class LoginController {
|
||||
private final SysUserService sysUserService;
|
||||
private final SysUserTokenService sysUserTokenService;
|
||||
private final CaptchaService captchaService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final SysParamsService sysParamsService;
|
||||
private final PropertiesUtils propertiesUtils;
|
||||
|
||||
@GetMapping("captcha")
|
||||
|
||||
@GetMapping("/captcha")
|
||||
@Operation(summary = "验证码")
|
||||
public void captcha(HttpServletResponse response, String uuid) throws IOException {
|
||||
//uuid不能为空
|
||||
@@ -46,10 +42,46 @@ public class LoginController {
|
||||
captchaService.create(response, uuid);
|
||||
}
|
||||
|
||||
@PostMapping("login")
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "登录")
|
||||
public Result login(HttpServletRequest request, @RequestBody LoginDTO login) {
|
||||
return sysUserTokenService.createToken(1L);
|
||||
public Result login( @RequestBody LoginDTO login) {
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||
if (!validate) {
|
||||
throw new RenException("验证码错误,请重新获取");
|
||||
}
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
// 判断用户是否存在
|
||||
if (userDTO == null) {
|
||||
throw new RenException("请检测用户和密码是否输入错误");
|
||||
}
|
||||
// 判断密码是否正确,不一样则进入if
|
||||
if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) {
|
||||
throw new RenException("请检测用户和密码是否输入错误");
|
||||
}
|
||||
return sysUserTokenService.createToken(userDTO.getId());
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "注册")
|
||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||
if (!validate) {
|
||||
throw new RenException("验证码错误,请重新获取");
|
||||
}
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
if (userDTO != null){
|
||||
throw new RenException("此手机号码已经注册过");
|
||||
}
|
||||
userDTO = new SysUserDTO();
|
||||
userDTO.setUsername(login.getUsername());
|
||||
userDTO.setPassword(login.getPassword());
|
||||
sysUserService.save(userDTO);
|
||||
return new Result<Void>();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import java.io.Serializable;
|
||||
@Schema(description = "登录表单")
|
||||
public class LoginDTO implements Serializable {
|
||||
|
||||
@Schema(description = "用户名", required = true)
|
||||
@Schema(description = "手机号码")
|
||||
@NotBlank(message = "{sysuser.username.require}")
|
||||
private String username;
|
||||
|
||||
@@ -27,6 +27,6 @@ public class LoginDTO implements Serializable {
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
@NotBlank(message = "{sysuser.uuid.require}")
|
||||
private String uuid;
|
||||
private String captchaId;
|
||||
|
||||
}
|
||||
@@ -80,7 +80,6 @@ public class SysUserController {
|
||||
//效验数据
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
String newPassword = dto.getNewPassword();
|
||||
String password = dto.getPassword();
|
||||
|
||||
//密码的强度
|
||||
if (newPassword == null || newPassword.length() < 8) {
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Null;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 字典数据
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "字典数据")
|
||||
public class SysDictDataDTO implements Serializable {
|
||||
|
||||
|
||||
@Schema(description = "id")
|
||||
@Null(message = "{id.null}", groups = AddGroup.class)
|
||||
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "字典类型ID")
|
||||
@NotNull(message = "{sysdict.type.require}", groups = DefaultGroup.class)
|
||||
private Long dictTypeId;
|
||||
|
||||
@Schema(description = "字典标签")
|
||||
@NotBlank(message = "{sysdict.label.require}", groups = DefaultGroup.class)
|
||||
private String dictLabel;
|
||||
|
||||
@Schema(description = "字典值")
|
||||
private String dictValue;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "排序")
|
||||
@Min(value = 0, message = "{sort.number}", groups = DefaultGroup.class)
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date updateDate;
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Null;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 字典数据
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "字典数据")
|
||||
public class SysDictDataDTO implements Serializable {
|
||||
|
||||
|
||||
@Schema(description = "id")
|
||||
@Null(message = "{id.null}", groups = AddGroup.class)
|
||||
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "字典类型ID")
|
||||
@NotNull(message = "{sysdict.type.require}", groups = DefaultGroup.class)
|
||||
private Long dictTypeId;
|
||||
|
||||
@Schema(description = "字典标签")
|
||||
@NotBlank(message = "{sysdict.label.require}", groups = DefaultGroup.class)
|
||||
private String dictLabel;
|
||||
|
||||
@Schema(description = "字典值")
|
||||
private String dictValue;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "排序")
|
||||
@Min(value = 0, message = "{sort.number}", groups = DefaultGroup.class)
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date updateDate;
|
||||
}
|
||||
@@ -1,55 +1,55 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Null;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "字典类型")
|
||||
public class SysDictTypeDTO implements Serializable {
|
||||
|
||||
|
||||
@Schema(description = "id")
|
||||
@Null(message = "{id.null}", groups = AddGroup.class)
|
||||
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "字典类型")
|
||||
@NotBlank(message = "{sysdict.type.require}", groups = DefaultGroup.class)
|
||||
private String dictType;
|
||||
|
||||
@Schema(description = "字典名称")
|
||||
@NotBlank(message = "{sysdict.name.require}", groups = DefaultGroup.class)
|
||||
private String dictName;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "排序")
|
||||
@Min(value = 0, message = "{sort.number}", groups = DefaultGroup.class)
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
private Date updateDate;
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Null;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "字典类型")
|
||||
public class SysDictTypeDTO implements Serializable {
|
||||
|
||||
|
||||
@Schema(description = "id")
|
||||
@Null(message = "{id.null}", groups = AddGroup.class)
|
||||
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "字典类型")
|
||||
@NotBlank(message = "{sysdict.type.require}", groups = DefaultGroup.class)
|
||||
private String dictType;
|
||||
|
||||
@Schema(description = "字典名称")
|
||||
@NotBlank(message = "{sysdict.name.require}", groups = DefaultGroup.class)
|
||||
private String dictName;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "排序")
|
||||
@Min(value = 0, message = "{sort.number}", groups = DefaultGroup.class)
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
private Date updateDate;
|
||||
}
|
||||
@@ -1,52 +1,52 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Null;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 参数管理
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "参数管理")
|
||||
public class SysParamsDTO implements Serializable {
|
||||
|
||||
|
||||
@Schema(description = "id")
|
||||
@Null(message = "{id.null}", groups = AddGroup.class)
|
||||
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "参数编码")
|
||||
@NotBlank(message = "{sysparams.paramcode.require}", groups = DefaultGroup.class)
|
||||
private String paramCode;
|
||||
|
||||
@Schema(description = "参数值")
|
||||
@NotBlank(message = "{sysparams.paramvalue.require}", groups = DefaultGroup.class)
|
||||
private String paramValue;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date updateDate;
|
||||
|
||||
}
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Null;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 参数管理
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "参数管理")
|
||||
public class SysParamsDTO implements Serializable {
|
||||
|
||||
|
||||
@Schema(description = "id")
|
||||
@Null(message = "{id.null}", groups = AddGroup.class)
|
||||
@NotNull(message = "{id.require}", groups = UpdateGroup.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "参数编码")
|
||||
@NotBlank(message = "{sysparams.paramcode.require}", groups = DefaultGroup.class)
|
||||
private String paramCode;
|
||||
|
||||
@Schema(description = "参数值")
|
||||
@NotBlank(message = "{sysparams.paramvalue.require}", groups = DefaultGroup.class)
|
||||
private String paramValue;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date updateDate;
|
||||
|
||||
}
|
||||
|
||||
+29
-25
@@ -1,26 +1,30 @@
|
||||
package xiaozhi.modules.sys.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据字典
|
||||
*/
|
||||
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
||||
|
||||
PageData<SysDictTypeDTO> page(Map<String, Object> params);
|
||||
|
||||
SysDictTypeDTO get(Long id);
|
||||
|
||||
void save(SysDictTypeDTO dto);
|
||||
|
||||
void update(SysDictTypeDTO dto);
|
||||
|
||||
void delete(Long[] ids);
|
||||
package xiaozhi.modules.sys.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据字典
|
||||
*/
|
||||
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
||||
|
||||
PageData<SysDictTypeDTO> page(Map<String, Object> params);
|
||||
|
||||
SysDictTypeDTO get(Long id);
|
||||
|
||||
void save(SysDictTypeDTO dto);
|
||||
|
||||
void update(SysDictTypeDTO dto);
|
||||
|
||||
void delete(Long[] ids);
|
||||
|
||||
List<DictType> getAllList();
|
||||
|
||||
List<DictType> getDictTypeList();
|
||||
}
|
||||
@@ -38,23 +38,6 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
||||
*/
|
||||
void updatePassword(Long id, String newPassword);
|
||||
|
||||
/**
|
||||
* 根据部门ID,查询用户数
|
||||
*/
|
||||
int getCountByDeptId(Long deptId);
|
||||
|
||||
/**
|
||||
* 根据部门ID,查询用户Id列表
|
||||
*/
|
||||
List<Long> getUserIdListByDeptId(List<Long> deptIdList);
|
||||
|
||||
/**
|
||||
* 删除用户缓存
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
void deleteUserCache(Long userId);
|
||||
|
||||
/**
|
||||
* 验证密码强度
|
||||
*
|
||||
@@ -63,5 +46,4 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
||||
*/
|
||||
boolean isStrongPassword(String newPassword);
|
||||
|
||||
String getName(Long creator);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package xiaozhi.modules.sys.service;
|
||||
|
||||
public interface TokenService {
|
||||
/**
|
||||
* 生成token
|
||||
*
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
String createToken(long userId);
|
||||
}
|
||||
+101
-101
@@ -1,102 +1,102 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||
import xiaozhi.modules.sys.entity.DictData;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity> implements SysDictTypeService {
|
||||
private final SysDictDataDao sysDictDataDao;
|
||||
|
||||
@Override
|
||||
public PageData<SysDictTypeDTO> page(Map<String, Object> params) {
|
||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
getWrapper(params)
|
||||
);
|
||||
|
||||
return getPageData(page, SysDictTypeDTO.class);
|
||||
}
|
||||
|
||||
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
||||
String dictType = (String) params.get("dictType");
|
||||
String dictName = (String) params.get("dictName");
|
||||
|
||||
QueryWrapper<SysDictTypeEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.like(StringUtils.isNotBlank(dictType), "dict_type", dictType);
|
||||
wrapper.like(StringUtils.isNotBlank(dictName), "dict_name", dictName);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysDictTypeDTO get(Long id) {
|
||||
SysDictTypeEntity entity = baseDao.selectById(id);
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(SysDictTypeDTO dto) {
|
||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||
|
||||
insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(SysDictTypeDTO dto) {
|
||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||
|
||||
updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long[] ids) {
|
||||
//删除
|
||||
deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DictType> getAllList() {
|
||||
List<DictType> typeList = baseDao.getDictTypeList();
|
||||
List<DictData> dataList = sysDictDataDao.getDictDataList();
|
||||
for (DictType type : typeList) {
|
||||
for (DictData data : dataList) {
|
||||
if (type.getId().equals(data.getDictTypeId())) {
|
||||
type.getDataList().add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return typeList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DictType> getDictTypeList() {
|
||||
return baseDao.getDictTypeList();
|
||||
}
|
||||
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||
import xiaozhi.modules.sys.entity.DictData;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity> implements SysDictTypeService {
|
||||
private final SysDictDataDao sysDictDataDao;
|
||||
|
||||
@Override
|
||||
public PageData<SysDictTypeDTO> page(Map<String, Object> params) {
|
||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
getWrapper(params)
|
||||
);
|
||||
|
||||
return getPageData(page, SysDictTypeDTO.class);
|
||||
}
|
||||
|
||||
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
||||
String dictType = (String) params.get("dictType");
|
||||
String dictName = (String) params.get("dictName");
|
||||
|
||||
QueryWrapper<SysDictTypeEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.like(StringUtils.isNotBlank(dictType), "dict_type", dictType);
|
||||
wrapper.like(StringUtils.isNotBlank(dictName), "dict_name", dictName);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysDictTypeDTO get(Long id) {
|
||||
SysDictTypeEntity entity = baseDao.selectById(id);
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(SysDictTypeDTO dto) {
|
||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||
|
||||
insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(SysDictTypeDTO dto) {
|
||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||
|
||||
updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long[] ids) {
|
||||
//删除
|
||||
deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DictType> getAllList() {
|
||||
List<DictType> typeList = baseDao.getDictTypeList();
|
||||
List<DictData> dataList = sysDictDataDao.getDictDataList();
|
||||
for (DictType type : typeList) {
|
||||
for (DictData data : dataList) {
|
||||
if (type.getId().equals(data.getDictTypeId())) {
|
||||
type.getDataList().add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
return typeList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DictType> getDictTypeList() {
|
||||
return baseDao.getDictTypeList();
|
||||
}
|
||||
|
||||
}
|
||||
+15
-36
@@ -1,25 +1,23 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -86,7 +84,13 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
entity.setPassword(password);
|
||||
|
||||
//保存用户
|
||||
entity.setSuperAdmin(SuperAdminEnum.NO.value());
|
||||
Long userCount = getUserCount();
|
||||
if (userCount == 0) {
|
||||
entity.setSuperAdmin(SuperAdminEnum.YES.value());
|
||||
} else {
|
||||
entity.setSuperAdmin(SuperAdminEnum.NO.value());
|
||||
}
|
||||
|
||||
insert(entity);
|
||||
}
|
||||
|
||||
@@ -139,23 +143,11 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
baseDao.updatePassword(id, newPassword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCountByDeptId(Long deptId) {
|
||||
return baseDao.getCountByDeptId(deptId);
|
||||
private Long getUserCount() {
|
||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||
return baseDao.selectCount(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getUserIdListByDeptId(List<Long> deptIdList) {
|
||||
return baseDao.getUserIdListByDeptId(deptIdList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteUserCache(Long userId) {
|
||||
// 删除缓存
|
||||
redisUtils.delete(RedisKeys.getUserInfoKey(userId));
|
||||
redisUtils.delete(RedisKeys.getDataScopeListKey(userId));
|
||||
redisUtils.delete(RedisKeys.getSysUserName(userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStrongPassword(String password) {
|
||||
@@ -165,17 +157,4 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
Matcher matcher = pattern.matcher(password);
|
||||
return matcher.matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Long id) {
|
||||
String name = (String) redisUtils.get(RedisKeys.getSysUserName(id));
|
||||
if (StringUtils.isBlank(name)) {
|
||||
SysUserEntity sysUserEntity = selectById(id);
|
||||
if (sysUserEntity != null) {
|
||||
redisUtils.set(RedisKeys.getSysUserName(id), sysUserEntity.getUsername());
|
||||
return sysUserEntity.getUsername();
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
production: false
|
||||
enable: true
|
||||
basic:
|
||||
enable: false
|
||||
username: renren
|
||||
password: 2ZABCDEUgF
|
||||
setting:
|
||||
enableFooter: false
|
||||
jasypt:
|
||||
encryptor:
|
||||
password: P9Hx718z8L
|
||||
spring:
|
||||
datasource:
|
||||
druid:
|
||||
#MySQL
|
||||
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
|
||||
username: root
|
||||
password: 123456
|
||||
initial-size: 10
|
||||
max-active: 100
|
||||
min-idle: 10
|
||||
max-wait: 6000
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 300000
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
url-pattern: /druid/*
|
||||
login-username: admin
|
||||
login-password: D7Xj810i1C
|
||||
filter:
|
||||
stat:
|
||||
log-slow-sql: true
|
||||
slow-sql-millis: 1000
|
||||
merge-sql: false
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.flowable.engine.impl.persistence.entity.*: debug
|
||||
org.flowable.task.service.impl.persistence.entity.*: debug
|
||||
knife4j:
|
||||
production: false
|
||||
enable: true
|
||||
basic:
|
||||
enable: false
|
||||
username: renren
|
||||
password: 2ZABCDEUgF
|
||||
setting:
|
||||
enableFooter: false
|
||||
jasypt:
|
||||
encryptor:
|
||||
password: P9Hx718z8L
|
||||
spring:
|
||||
datasource:
|
||||
druid:
|
||||
#MySQL
|
||||
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
|
||||
username: root
|
||||
password: 123456
|
||||
initial-size: 10
|
||||
max-active: 100
|
||||
min-idle: 10
|
||||
max-wait: 6000
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 300000
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
url-pattern: /druid/*
|
||||
login-username: admin
|
||||
login-password: D7Xj810i1C
|
||||
filter:
|
||||
stat:
|
||||
log-slow-sql: true
|
||||
slow-sql-millis: 1000
|
||||
merge-sql: false
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
@@ -7,7 +7,7 @@ server:
|
||||
min-spare: 30
|
||||
port: 8002
|
||||
servlet:
|
||||
context-path: /xiaozhi-esp32-api
|
||||
context-path: /xiaozhi-esp32-api/api/v1
|
||||
session:
|
||||
cookie:
|
||||
http-only: true
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 给用户表添加一个创建者
|
||||
ALTER TABLE sys_user ADD COLUMN creator BIGINT COMMENT '创建者';
|
||||
@@ -8,4 +8,11 @@ databaseChangeLog:
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/001create_sys.sql
|
||||
path: classpath:db/changelog/001create_sys.sql
|
||||
- changeSet:
|
||||
id: 202503101631
|
||||
author: zjy
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202503101631.sql
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
|
||||
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
||||
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
||||
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
||||
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
||||
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
||||
@@ -31,24 +30,6 @@
|
||||
10027=Redis\u670D\u52A1\u5F02\u5E38
|
||||
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u53C2\u6570\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528JSON\u683C\u5F0F
|
||||
10031=\u8BF7\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E
|
||||
10032=\u4EFB\u52A1\u5DF2\u88AB\u7B7E\u6536\uFF0C\u64CD\u4F5C\u5931\u8D25
|
||||
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u4E49
|
||||
10034=\u4E0A\u7EA7\u8282\u70B9\u4E0D\u5B58\u5728
|
||||
10035=\u9A73\u56DE
|
||||
10036=\u56DE\u9000
|
||||
10037=\u4EFB\u52A1\u6CA1\u6709\u5206\u7EC4\uFF0C\u65E0\u6CD5\u53D6\u6D88\u8BA4\u9886
|
||||
10038=\u4E0A\u7EA7\u533A\u57DF\u9009\u62E9\u9519\u8BEF
|
||||
10039=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u533A\u57DF
|
||||
10040=\u6D41\u7A0B\u5DF2\u6302\u8D77\uFF0C\u4E0D\u80FD\u542F\u52A8\u5B9E\u4F8B
|
||||
10041=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u9A73\u56DE
|
||||
10042=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u9A73\u56DE
|
||||
10043=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u7EC8\u6B62
|
||||
10044=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u7EC8\u6B62\u6D41\u7A0B
|
||||
10045=\u7EC8\u6B62
|
||||
10046=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u56DE\u9000
|
||||
10047=\u5B58\u5728\u591A\u4E2A\u5E76\u884C\u6267\u884C\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u56DE\u9000
|
||||
10048=\u767B\u5F55\u8D26\u53F7\uFF0C\u65E0\u6743\u5220\u9664
|
||||
10055=\u60A8\u7684\u5BC6\u7801\u957F\u5EA6\u4E0D\u591F8\u4F4D
|
||||
10056=\u60A8\u7684\u5BC6\u7801\u590D\u6742\u5EA6\u4E0D\u591F\uFF0C\u9700\u8981\u540C\u65F6\u5305\u542B\u6570\u5B57,\u5C0F\u5199\u82F1\u6587,\u5927\u5199\u82F1\u6587
|
||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5F31\u5BC6\u7801
|
||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||
|
||||
@@ -11,44 +11,24 @@
|
||||
10007=The verification code is incorrect
|
||||
10008=First delete submenu or button
|
||||
10009=The original password is incorrect
|
||||
10010=password error,you can try {0} times
|
||||
|
||||
10011=The superior department made a wrong choice
|
||||
10012=Upper menu cannot be for itself
|
||||
10013=Data permission interface, which can only be a Map type parameter.
|
||||
10014=Please delete the subordinate department first
|
||||
10015=Please delete the user under the department first
|
||||
10016=Deployment failed, no process
|
||||
10017=Model diagram is incorrect, please check
|
||||
10018=The export failed with the model ID {0}
|
||||
|
||||
10019=Please upload a file
|
||||
10020=token cannot be empty
|
||||
10021=token is invalid, please log in again
|
||||
10022=The account has been locked
|
||||
10023=Please upload zip, bar, bpmn, bpmn20.xml format file
|
||||
|
||||
10024=Failed to upload file {0}
|
||||
10025=Failed to send SMS {0}
|
||||
10026=Mail template does not exist
|
||||
|
||||
10027=Redis service exception
|
||||
10028=Timed task failed
|
||||
10029=Cannot contain illegal characters
|
||||
10030=The parameter format is incorrect. Please use JSON format.
|
||||
10031=Please complete the SMS configuration first.
|
||||
10032=Task has been signed and operation failed
|
||||
10033=Non-existent process definition
|
||||
10034=Superior node does not exist
|
||||
10035=Reject
|
||||
10036=Rollback
|
||||
10037=Tasks are not grouped and cannot be cancelled
|
||||
10038=Upper area selection error
|
||||
10039=Please delete the subordinate area first
|
||||
10040=The process has been suspended and the instance cannot be started
|
||||
10041=Multi-instance tasks cannot be rejected
|
||||
10042=Tasks in multiple processes cannot be rejected
|
||||
10043=Multi-instance tasks cannot be terminated
|
||||
10044=There are tasks in multiple processes that cannot terminate the process
|
||||
10045=END
|
||||
10046=Multi-instance tasks cannot be rolled back
|
||||
10047=There are multiple parallel tasks that cannot be rolled back
|
||||
10048 = Login account, no right to delete
|
||||
10055=Your password length is less than 8 digits
|
||||
10056=Your password is not complex enough and needs to include both numbers, lowercase English, and uppercase English
|
||||
|
||||
10030=The password is less than {0} digits.
|
||||
10031=The password must consist of numbers, uppercase and lowercase letters, and special characters at the same time
|
||||
10032=Exception in deleting this data
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
|
||||
401=\u672A\u6388\u6743
|
||||
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
|
||||
|
||||
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
|
||||
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
|
||||
@@ -11,44 +12,23 @@
|
||||
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
||||
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
||||
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
||||
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
||||
|
||||
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
||||
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
|
||||
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
|
||||
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
|
||||
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
|
||||
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
|
||||
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
|
||||
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
|
||||
|
||||
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
|
||||
10020=token\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
|
||||
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
|
||||
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||
|
||||
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
|
||||
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
|
||||
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||
|
||||
10027=Redis\u670D\u52A1\u5F02\u5E38
|
||||
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u53C2\u6570\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528JSON\u683C\u5F0F
|
||||
10031=\u8BF7\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E
|
||||
10032=\u4EFB\u52A1\u5DF2\u88AB\u7B7E\u6536\uFF0C\u64CD\u4F5C\u5931\u8D25
|
||||
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u4E49
|
||||
10034=\u4E0A\u7EA7\u8282\u70B9\u4E0D\u5B58\u5728
|
||||
10035=\u9A73\u56DE
|
||||
10036=\u56DE\u9000
|
||||
10037=\u4EFB\u52A1\u6CA1\u6709\u5206\u7EC4\uFF0C\u65E0\u6CD5\u53D6\u6D88\u8BA4\u9886
|
||||
10038=\u4E0A\u7EA7\u533A\u57DF\u9009\u62E9\u9519\u8BEF
|
||||
10039=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u533A\u57DF
|
||||
10040=\u6D41\u7A0B\u5DF2\u6302\u8D77\uFF0C\u4E0D\u80FD\u542F\u52A8\u5B9E\u4F8B
|
||||
10041=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u9A73\u56DE
|
||||
10042=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u9A73\u56DE
|
||||
10043=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u7EC8\u6B62
|
||||
10044=\u5B58\u5728\u591A\u4E2A\u5904\u7406\u4E2D\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u7EC8\u6B62\u6D41\u7A0B
|
||||
10045=\u7EC8\u6B62
|
||||
10046=\u591A\u5B9E\u4F8B\u4EFB\u52A1\u4E0D\u80FD\u56DE\u9000
|
||||
10047=\u5B58\u5728\u591A\u4E2A\u5E76\u884C\u6267\u884C\u7684\u4EFB\u52A1\uFF0C\u4E0D\u80FD\u56DE\u9000
|
||||
10048=\u767B\u5F55\u8D26\u53F7\uFF0C\u65E0\u6743\u5220\u9664
|
||||
10055=\u60A8\u7684\u5BC6\u7801\u957F\u5EA6\u4E0D\u591F8\u4F4D
|
||||
10056=\u60A8\u7684\u5BC6\u7801\u590D\u6742\u5EA6\u4E0D\u591F\uFF0C\u9700\u8981\u540C\u65F6\u5305\u542B\u6570\u5B57,\u5C0F\u5199\u82F1\u6587,\u5927\u5199\u82F1\u6587
|
||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
|
||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
||||
@@ -2,6 +2,7 @@
|
||||
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
|
||||
401=\u672A\u6388\u6B0A
|
||||
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6B0A\u9650
|
||||
|
||||
10001={0}\u4E0D\u80FD\u70BA\u7A7A
|
||||
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
|
||||
10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
|
||||
@@ -11,44 +12,23 @@
|
||||
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
|
||||
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
|
||||
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
|
||||
10010=\u8CEC\u865F\u6216\u5BC6\u78BC\u4E0D\u6B63\u78BA,\u60A8\u9084\u6709\u53EF\u4EE5\u5617\u8A66{0}\u6B21
|
||||
|
||||
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
|
||||
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
|
||||
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
|
||||
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
|
||||
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
|
||||
10016=\u90E8\u7F72\u5931\u6557\uFF0C\u6C92\u6709\u6D41\u7A0B
|
||||
10017=\u6A21\u578B\u5716\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u6AA2\u67E5
|
||||
10018=\u5C0E\u51FA\u5931\u6557\uFF0C\u6A21\u578BID\u70BA{0}
|
||||
|
||||
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
|
||||
10020=token\u4E0D\u80FD\u70BA\u7A7A
|
||||
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
|
||||
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
|
||||
10023=\u8ACB\u4E0A\u50B3zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
||||
|
||||
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
|
||||
10025=\u767C\u9001\u77ED\u4FE1\u5931\u6557{0}
|
||||
10026=\u90F5\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
||||
|
||||
10027=Redis\u670D\u52D9\u7570\u5E38
|
||||
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
|
||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
||||
10030=\u53C3\u6578\u683C\u5F0F\u4E0D\u6B63\u78BA\uFF0C\u8ACB\u4F7F\u7528JSON\u683C\u5F0F
|
||||
10031=\u8ACB\u5148\u5B8C\u6210\u77ED\u4FE1\u914D\u7F6E
|
||||
10032=\u4EFB\u52D9\u5DF2\u88AB\u7C3D\u6536\uFF0C\u64CD\u4F5C\u5931\u6557
|
||||
10033=\u4E0D\u5B58\u5728\u7684\u6D41\u7A0B\u5B9A\u7FA9
|
||||
10034=\u4E0A\u7D1A\u7BC0\u9EDE\u4E0D\u5B58\u5728
|
||||
10035=\u99C1\u56DE
|
||||
10036=\u56DE\u9000
|
||||
10037=\u4EFB\u52D9\u6C92\u6709\u5206\u7D44\uFF0C\u7121\u6CD5\u53D6\u6D88\u8A8D\u9818
|
||||
10038=\u4E0A\u7D1A\u5340\u57DF\u9078\u64C7\u932F\u8AA4
|
||||
10039=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u5340\u57DF
|
||||
10040=\u6D41\u7A0B\u5DF2\u639B\u8D77\uFF0C\u4E0D\u80FD\u555F\u52D5\u5BE6\u4F8B
|
||||
10041=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u99C1\u56DE
|
||||
10042=\u5B58\u5728\u591A\u500B\u8655\u7406\u4E2D\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u99C1\u56DE
|
||||
10043=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u7D42\u6B62
|
||||
10044=\u5B58\u5728\u591A\u500B\u8655\u7406\u4E2D\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u7D42\u6B62\u6D41\u7A0B
|
||||
10045=\u7D42\u6B62
|
||||
10046=\u591A\u5BE6\u4F8B\u4EFB\u52D9\u4E0D\u80FD\u56DE\u9000
|
||||
10047=\u5B58\u5728\u591A\u500B\u4E26\u884C\u57F7\u884C\u7684\u4EFB\u52D9\uFF0C\u4E0D\u80FD\u56DE\u9000
|
||||
10048=\u767B\u5165\u5E33\u865F\uFF0C\u7121\u6B0A\u5220\u9664
|
||||
10055=\u60A8\u7684\u5BC6\u78BC\u9577\u5EA6\u4E0D\u59208\u4F4D
|
||||
10056=\u60A8\u7684\u5BC6\u78BC\u5FA9\u96DC\u5EA6\u4E0D\u5920\uFF0C\u9700\u8981\u540C\u6642\u5305\u542B\u6578\u5B57,\u5C0F\u5BEB\u82F1\u6587,\u5927\u5BEB\u82F1\u6587
|
||||
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
||||
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
|
||||
10032=\u522A\u9664\u6B64\u6578\u64DA\u7570\u5E38
|
||||
@@ -1,9 +1,13 @@
|
||||
#\u7B80\u4F53\u4E2D\u6587
|
||||
id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
pid.require=\u4E0A\u7EA7ID\uFF0C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -11,99 +15,8 @@ sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
|
||||
sysuser.email.require=\u90AE\u7BB1\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
sysuser.mobile.require=\u624B\u673A\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.superadmin.range=\u8D85\u7EA7\u7BA1\u7406\u5458\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysmenu.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u83DC\u5355
|
||||
sysmenu.name.require=\u83DC\u5355\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysmenu.type.range=\u83DC\u5355\u7C7B\u578B\u53D6\u503C\u8303\u56F40~1
|
||||
sysmenu.openstyle.range=\u83DC\u5355\u6253\u5F00\u65B9\u5F0F\u53D6\u503C\u8303\u56F40~1
|
||||
|
||||
sysdept.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u90E8\u95E8
|
||||
sysdept.name.require=\u90E8\u95E8\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysrole.name.require=\u89D2\u8272\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
schedule.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
schedule.cron.require=cron\u8868\u8FBE\u5F0F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
schedule.bean.require=bean\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
oss.type.range=\u7C7B\u578B\u53D6\u503C\u8303\u56F41~5
|
||||
|
||||
aliyun.accesskeyid.require=\u963F\u91CC\u4E91AccessKeyId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.accesskeysecret.require=\u963F\u91CC\u4E91AccessKeySecret\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.signname.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.templatecode.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.domain.require=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.domain.url=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
aliyun.endPoint.require=\u963F\u91CC\u4E91EndPoint\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.bucketname.require=\u963F\u91CC\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
qcloud.appid.require=\u817E\u8BAF\u4E91AppId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.appkey.require=\u817E\u8BAF\u4E91AppKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.secretId.require=\u817E\u8BAF\u4E91SecretId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.secretkey.require=\u817E\u8BAF\u4E91SecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.signname.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.templateid.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.domain.require=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.domain.url=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
qcloud.bucketname.require=\u817E\u8BAF\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.region.require=\u6240\u5C5E\u5730\u533A\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
qiniu.domain.require=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.domain.url=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u95F4\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
fastdfs.domain.require=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
fastdfs.domain.url=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
|
||||
local.domain.require=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
local.domain.url=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
local.path.url=\u5B58\u50A8\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sms.platform.range=\u5E73\u53F0\u7C7B\u578B\u53D6\u503C\u8303\u56F41~2
|
||||
email.smtp.require=SMTP\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.port.require=\u7AEF\u53E3\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.username.require=\u90AE\u7BB1\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.password.require=\u90AE\u7BB1\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
mail.name.require=\u6A21\u677F\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
mail.subject.require=\u90AE\u4EF6\u4E3B\u9898\u4E0D\u80FD\u4E3A\u7A7A
|
||||
mail.content.require=\u90AE\u4EF6\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
model.name.require=\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
model.key.require=\u6A21\u578B\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
news.title.require=\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A
|
||||
news.content.require=\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A
|
||||
news.pubdate.require=\u53D1\u5E03\u65F6\u95F4\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
region.id.require=\u533A\u57DF\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
region.pid.require=\u4E0A\u7EA7\u533A\u57DF\u4E0D\u80FD\u4E3A\u7A7A
|
||||
region.name.require=\u533A\u57DF\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u4E49ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.bizRoute.require=\u4E1A\u52A1\u8DEF\u7531\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u4E49\u7248\u672C\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
ProcessStart.businessKey.require=\u4E1A\u52A1KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
@@ -1,9 +1,13 @@
|
||||
#English
|
||||
id.require=ID can not be empty
|
||||
id.null=ID has to be empty
|
||||
pid.require=Parent ID, cannot be empty
|
||||
|
||||
sort.number=The sort value cannot be less than 0
|
||||
|
||||
sysdict.type.require=The dictionary type cannot be empty
|
||||
sysdict.name.require=The dictionary name cannot be empty
|
||||
sysdict.label.require=Dictionary tag cannot be empty
|
||||
|
||||
sysparams.paramcode.require=Parameter encoding cannot be empty
|
||||
sysparams.paramvalue.require=Parameter values cannot be empty
|
||||
|
||||
@@ -11,100 +15,8 @@ sysuser.username.require=The username cannot be empty
|
||||
sysuser.password.require=The password cannot be empty
|
||||
sysuser.realname.require=The realname cannot be empty
|
||||
sysuser.gender.range=Gender ranges from 0 to 2
|
||||
sysuser.email.require=Mailbox cannot be empty
|
||||
sysuser.email.error=Incorrect email format
|
||||
sysuser.mobile.require=The phone number cannot be empty
|
||||
sysuser.deptId.require=Departments cannot be empty
|
||||
sysuser.superadmin.range=Super administrator values range from 0 to 1
|
||||
sysuser.status.range=State ranges from 0 to 1
|
||||
sysuser.captcha.require=The captcha cannot be empty
|
||||
sysuser.uuid.require=The unique identifier cannot be empty
|
||||
|
||||
sysmenu.pid.require=Please select superior menu
|
||||
sysmenu.name.require=Menu name cannot be empty
|
||||
sysmenu.type.range=Menu type ranges from 0 to 1
|
||||
sysmenu.openstyle.range=Menu open style ranges from 0 to 1
|
||||
|
||||
sysdept.pid.require=Please select superior department
|
||||
sysdept.name.require=The department name cannot be empty
|
||||
|
||||
sysrole.name.require=The role name cannot be empty
|
||||
|
||||
sysdict.type.require=The dictionary type cannot be empty
|
||||
sysdict.name.require=The dictionary name cannot be empty
|
||||
sysdict.label.require=Dictionary tag cannot be empty
|
||||
|
||||
schedule.status.range=Status ranges from 0 to 1
|
||||
schedule.cron.require=Cron expression cannot be empty
|
||||
schedule.bean.require=Bean name cannot be empty
|
||||
|
||||
oss.type.range=Type ranges from 1 to 5
|
||||
|
||||
aliyun.accesskeyid.require=Aliyun AccessKeyId cannot be empty
|
||||
aliyun.accesskeysecret.require=Aliyun AccessKeySecret cannot be empty
|
||||
aliyun.signname.require=Aliyun message signature cannot be empty
|
||||
aliyun.templatecode.require=Aliyun message template cannot be empty
|
||||
aliyun.domain.require=Aliyun bound domain name cannot be empty
|
||||
aliyun.domain.url=Aliyun binding domain name format is incorrect
|
||||
aliyun.endPoint.require=The aliyun EndPoint cannot be empty
|
||||
aliyun.bucketname.require=Aliyun BucketName cannot be empty
|
||||
|
||||
qcloud.appid.require=Tencent cloud AppId cannot be empty
|
||||
qcloud.appkey.require=Tencent cloud AppKey cannot be empty
|
||||
qcloud.secretId.require=Tencent cloud SecretId cannot be empty
|
||||
qcloud.secretkey.require=Tencent cloud SecretKey cannot be empty
|
||||
qcloud.signname.require=Tencent cloud SMS signature cannot be empty
|
||||
qcloud.templateid.require=Tencent cloud SMS template ID cannot be empty
|
||||
qcloud.domain.require=Tencent cloud bound domain name cannot be empty
|
||||
qcloud.domain.url=Tencent cloud binding domain name format is incorrect
|
||||
qcloud.bucketname.require=Tencent cloud BucketName cannot be empty
|
||||
qcloud.region.require=The region cannot be empty
|
||||
|
||||
qiniu.domain.require=Bound domain name cannot be empty
|
||||
qiniu.domain.url=Binding domain name format is incorrect
|
||||
qiniu.accesskey.require=The AccessKey cannot be empty
|
||||
qiniu.secretkey.require=The SecretKey of seven cows cannot be empty
|
||||
qiniu.bucketname.require=Space names cannot be empty
|
||||
qiniu.templateId.require=Template ID cannot be empty
|
||||
|
||||
fastdfs.domain.require=FastDFS bound domain name cannot be empty
|
||||
fastdfs.domain.url=FastDFS bound domain name format is incorrect
|
||||
|
||||
local.domain.require=Local upload bound domain name cannot be empty
|
||||
local.domain.url=The domain name bound for local upload is not formatted correctly
|
||||
local.path.url=The storage directory cannot be empty
|
||||
|
||||
minio.endPoint.require=Minio EndPoint cannot be empty
|
||||
minio.accesskey.require=Minio AccessKey cannot be empty
|
||||
minio.secretkey.require=Minio SecretKey cannot be empty
|
||||
minio.bucketname.require=Minio BucketName cannot be empty
|
||||
|
||||
sms.platform.range=Platform type ranges from 1 to 2
|
||||
|
||||
email.smtp.require=SMTP cannot be empty
|
||||
email.port.require=Port number cannot be empty
|
||||
email.username.require=The email account cannot be empty
|
||||
email.password.require=The password cannot be empty
|
||||
|
||||
mail.name.require=The template name cannot be empty
|
||||
mail.subject.require=Mail subject cannot be empty
|
||||
mail.content.require=Message text cannot be empty
|
||||
|
||||
model.name.require=The model name cannot be empty
|
||||
model.key.require=The model key cannot be empty
|
||||
|
||||
news.title.require=The title cannot be empty
|
||||
news.content.require=Content cannot be empty
|
||||
news.pubdate.require=The release time cannot be empty
|
||||
|
||||
region.id.require=Region ID cannot be empty
|
||||
region.pid.require=Upper area cannot be empty
|
||||
region.name.require=Region name cannot be empty
|
||||
|
||||
processBizRoute.procDefId.require=Process Definition ID cannot be empty
|
||||
processBizRoute.bizRoute.require=Service routing cannot be empty
|
||||
processBizRoute.procDefKey.require=Process Definition KEY cannot be empty
|
||||
processBizRoute.version.require=Process Definition Version number cannot be empty
|
||||
|
||||
ProcessStart.processDefinitionKey.require=Process Definition KEY cannot be empty
|
||||
ProcessStart.businessKey.require=Business KEY cannot be empty
|
||||
@@ -1,9 +1,13 @@
|
||||
#\u7B80\u4F53\u4E2D\u6587
|
||||
id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
pid.require=\u4E0A\u7EA7ID\uFF0C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -11,99 +15,9 @@ sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
|
||||
sysuser.email.require=\u90AE\u7BB1\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
sysuser.mobile.require=\u624B\u673A\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.superadmin.range=\u8D85\u7EA7\u7BA1\u7406\u5458\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysmenu.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u83DC\u5355
|
||||
sysmenu.name.require=\u83DC\u5355\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysmenu.type.range=\u83DC\u5355\u7C7B\u578B\u53D6\u503C\u8303\u56F40~1
|
||||
sysmenu.openstyle.range=\u83DC\u5355\u6253\u5F00\u65B9\u5F0F\u53D6\u503C\u8303\u56F40~1
|
||||
|
||||
sysdept.pid.require=\u8BF7\u9009\u62E9\u4E0A\u7EA7\u90E8\u95E8
|
||||
sysdept.name.require=\u90E8\u95E8\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysrole.name.require=\u89D2\u8272\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
schedule.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
schedule.cron.require=cron\u8868\u8FBE\u5F0F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
schedule.bean.require=bean\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
oss.type.range=\u7C7B\u578B\u53D6\u503C\u8303\u56F41~5
|
||||
|
||||
aliyun.accesskeyid.require=\u963F\u91CC\u4E91AccessKeyId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.accesskeysecret.require=\u963F\u91CC\u4E91AccessKeySecret\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.signname.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.templatecode.require=\u963F\u91CC\u4E91\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.domain.require=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.domain.url=\u963F\u91CC\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
aliyun.endPoint.require=\u963F\u91CC\u4E91EndPoint\u4E0D\u80FD\u4E3A\u7A7A
|
||||
aliyun.bucketname.require=\u963F\u91CC\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
qcloud.appid.require=\u817E\u8BAF\u4E91AppId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.appkey.require=\u817E\u8BAF\u4E91AppKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.secretId.require=\u817E\u8BAF\u4E91SecretId\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.secretkey.require=\u817E\u8BAF\u4E91SecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.signname.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u7B7E\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.templateid.require=\u817E\u8BAF\u4E91\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.domain.require=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.domain.url=\u817E\u8BAF\u4E91\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
qcloud.bucketname.require=\u817E\u8BAF\u4E91BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qcloud.region.require=\u6240\u5C5E\u5730\u533A\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
qiniu.domain.require=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.domain.url=\u4E03\u725B\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u95F4\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
fastdfs.domain.require=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
fastdfs.domain.url=FastDFS\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
|
||||
local.domain.require=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
local.domain.url=\u672C\u5730\u4E0A\u4F20\u7ED1\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
local.path.url=\u5B58\u50A8\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u4E3A\u7A7A
|
||||
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sms.platform.range=\u5E73\u53F0\u7C7B\u578B\u53D6\u503C\u8303\u56F41~2
|
||||
email.smtp.require=SMTP\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.port.require=\u7AEF\u53E3\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.username.require=\u90AE\u7BB1\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
email.password.require=\u90AE\u7BB1\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
mail.name.require=\u6A21\u677F\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
mail.subject.require=\u90AE\u4EF6\u4E3B\u9898\u4E0D\u80FD\u4E3A\u7A7A
|
||||
mail.content.require=\u90AE\u4EF6\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
model.name.require=\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
model.key.require=\u6A21\u578B\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
news.title.require=\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A
|
||||
news.content.require=\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A
|
||||
news.pubdate.require=\u53D1\u5E03\u65F6\u95F4\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
region.id.require=\u533A\u57DF\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
region.pid.require=\u4E0A\u7EA7\u533A\u57DF\u4E0D\u80FD\u4E3A\u7A7A
|
||||
region.name.require=\u533A\u57DF\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u4E49ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.bizRoute.require=\u4E1A\u52A1\u8DEF\u7531\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u4E49\u7248\u672C\u53F7\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u4E49KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
ProcessStart.businessKey.require=\u4E1A\u52A1KEY\u4E0D\u80FD\u4E3A\u7A7A
|
||||
@@ -1,110 +1,22 @@
|
||||
#\u7E41\u4F53\u4E2D\u6587
|
||||
id.require=ID\u4E0D\u80FD\u70BA\u7A7A
|
||||
id.null=ID\u5FC5\u9808\u70BA\u7A7A
|
||||
pid.require=\u4E0A\u7D1AID\uFF0C\u4E0D\u80FD\u70BA\u7A7A
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u65BC0
|
||||
#\u7B80\u4F53\u4E2D\u6587
|
||||
id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
|
||||
sysparams.paramcode.require=\u53C3\u6578\u7DE8\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysparams.paramvalue.require=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysuser.username.require=\u7528\u6236\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.password.require=\u5BC6\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.gender.range=\u6027\u5225\u53D6\u503C\u7BC4\u570D0~2
|
||||
sysuser.email.require=\u90F5\u7BB1\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.email.error=\u90F5\u7BB1\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
sysuser.mobile.require=\u624B\u6A5F\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.deptId.require=\u90E8\u9580\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.superadmin.range=\u8D85\u7D1A\u7BA1\u7406\u54E1\u53D6\u503C\u7BC4\u570D0~1
|
||||
sysuser.status.range=\u72C0\u614B\u53D6\u503C\u7BC4\u570D0~1
|
||||
sysuser.captcha.require=\u9A57\u8B49\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.uuid.require=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6807\u7B7E\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysmenu.pid.require=\u8ACB\u9078\u64C7\u4E0A\u7D1A\u83DC\u55AE
|
||||
sysmenu.name.require=\u83DC\u55AE\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysmenu.type.range=\u83DC\u55AE\u985E\u578B\u53D6\u503C\u7BC4\u570D0~1
|
||||
sysmenu.openstyle.range=\u83DC\u55AE\u6253\u958B\u65B9\u5F0F\u53D6\u503C\u7BC4\u570D0~1
|
||||
sysparams.paramcode.require=\u53C2\u6570\u7F16\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysparams.paramvalue.require=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
sysdept.pid.require=\u8ACB\u9078\u64C7\u4E0A\u7D1A\u90E8\u9580
|
||||
sysdept.name.require=\u90E8\u9580\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
sysrole.name.require=\u89D2\u8272\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysdict.label.require=\u5B57\u5178\u6A19\u7C64\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
schedule.status.range=\u72C0\u614B\u53D6\u503C\u7BC4\u570D0~1
|
||||
schedule.cron.require=cron\u8868\u9054\u5F0F\u4E0D\u80FD\u70BA\u7A7A
|
||||
schedule.bean.require=bean\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
oss.type.range=\u985E\u578B\u53D6\u503C\u7BC4\u570D1~5
|
||||
|
||||
aliyun.accesskeyid.require=\u963F\u91CC\u96F2AccessKeyId\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.accesskeysecret.require=\u963F\u91CC\u96F2AccessKeySecret\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.signname.require=\u963F\u91CC\u96F2\u77ED\u4FE1\u7C3D\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.templatecode.require=\u963F\u91CC\u96F2\u77ED\u4FE1\u6A21\u677F\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.domain.require=\u963F\u91CC\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.domain.url=\u963F\u91CC\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
aliyun.endPoint.require=\u963F\u91CC\u96F2EndPoint\u4E0D\u80FD\u70BA\u7A7A
|
||||
aliyun.bucketname.require=\u963F\u91CC\u96F2BucketName\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
qcloud.appid.require=\u9A30\u8A0A\u96F2AppId\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.appkey.require=\u9A30\u8A0A\u96F2AppKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.secretId.require=\u9A30\u8A0A\u96F2SecretId\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.secretkey.require=\u9A30\u8A0A\u96F2SecretKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.signname.require=\u9A30\u8A0A\u96F2\u77ED\u4FE1\u7C3D\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.templateid.require=\u9A30\u8A0A\u96F2\u77ED\u4FE1\u6A21\u677FID\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.domain.require=\u9A30\u8A0A\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.domain.url=\u9A30\u8A0A\u96F2\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
qcloud.bucketname.require=\u9A30\u8A0A\u96F2BucketName\u4E0D\u80FD\u70BA\u7A7A
|
||||
qcloud.region.require=\u6240\u5C6C\u5730\u5340\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
qiniu.domain.require=\u4E03\u725B\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
qiniu.domain.url=\u4E03\u725B\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
qiniu.accesskey.require=\u4E03\u725BAccessKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
qiniu.secretkey.require=\u4E03\u725BSecretKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
qiniu.bucketname.require=\u4E03\u725B\u7A7A\u9593\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
qiniu.templateId.require=\u4E03\u725B\u6A21\u677FID\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
fastdfs.domain.require=FastDFS\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
fastdfs.domain.url=FastDFS\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
|
||||
local.domain.require=\u672C\u5730\u4E0A\u50B3\u7D81\u5B9A\u7684\u57DF\u540D\u4E0D\u80FD\u70BA\u7A7A
|
||||
local.domain.url=\u672C\u5730\u4E0A\u50B3\u7D81\u5B9A\u7684\u57DF\u540D\u683C\u5F0F\u4E0D\u6B63\u78BA
|
||||
local.path.url=\u5B58\u5132\u76EE\u9304\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
minio.endPoint.require=Minio EndPoint\u4E0D\u80FD\u70BA\u7A7A
|
||||
minio.accesskey.require=Minio AccessKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
minio.secretkey.require=Minio SecretKey\u4E0D\u80FD\u70BA\u7A7A
|
||||
minio.bucketname.require=Minio BucketName\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
sms.platform.range=\u5E73\u53F0\u985E\u578B\u53D6\u503C\u7BC4\u570D1~2
|
||||
|
||||
email.smtp.require=SMTP\u4E0D\u80FD\u70BA\u7A7A
|
||||
email.port.require=\u7AEF\u53E3\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
email.username.require=\u90F5\u7BB1\u8CEC\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
email.password.require=\u90F5\u7BB1\u5BC6\u78BC\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
mail.name.require=\u6A21\u677F\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
mail.subject.require=\u90F5\u4EF6\u4E3B\u984C\u4E0D\u80FD\u70BA\u7A7A
|
||||
mail.content.require=\u90F5\u4EF6\u6B63\u6587\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
model.name.require=\u6A21\u578B\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
model.key.require=\u6A21\u578B\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
news.title.require=\u6A19\u984C\u4E0D\u80FD\u70BA\u7A7A
|
||||
news.content.require=\u5167\u5BB9\u4E0D\u80FD\u70BA\u7A7A
|
||||
news.pubdate.require=\u767C\u4F48\u6642\u9593\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
region.id.require=\u5340\u57DF\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
||||
region.pid.require=\u4E0A\u7D1A\u5340\u57DF\u4E0D\u80FD\u70BA\u7A7A
|
||||
region.name.require=\u5340\u57DF\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
processBizRoute.procDefId.require=\u6D41\u7A0B\u5B9A\u7FA9ID\u4E0D\u80FD\u70BA\u7A7A
|
||||
processBizRoute.bizRoute.require=\u696D\u52D9\u8DEF\u7531\u4E0D\u80FD\u70BA\u7A7A
|
||||
processBizRoute.procDefKey.require=\u6D41\u7A0B\u5B9A\u7FA9KEY\u4E0D\u80FD\u70BA\u7A7A
|
||||
processBizRoute.version.require=\u6D41\u7A0B\u5B9A\u7FA9\u7248\u672C\u865F\u4E0D\u80FD\u70BA\u7A7A
|
||||
|
||||
ProcessStart.processDefinitionKey.require=\u6D41\u7A0B\u5B9A\u7FA9KEY\u4E0D\u80FD\u70BA\u7A7A
|
||||
ProcessStart.businessKey.require=\u696D\u52D9KEY\u4E0D\u80FD\u70BA\u7A7A
|
||||
sysuser.username.require=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.password.require=\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.realname.require=\u59D3\u540D\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.gender.range=\u6027\u522B\u53D6\u503C\u8303\u56F40~2
|
||||
sysuser.email.error=\u90AE\u7BB1\u683C\u5F0F\u4E0D\u6B63\u786E
|
||||
sysuser.deptId.require=\u90E8\u95E8\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.status.range=\u72B6\u6001\u53D6\u503C\u8303\u56F40~1
|
||||
sysuser.captcha.require=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -1,50 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="xiaozhi.modules.sys.dao.SysUserDao">
|
||||
|
||||
<select id="getList" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName
|
||||
from sys_user t1 where t1.super_admin = 0
|
||||
<if test="username != null and username.trim() != ''">
|
||||
and t1.username like #{username}
|
||||
</if>
|
||||
<if test="deptId != null and deptId.trim() != ''">
|
||||
and t1.dept_id = #{deptId}
|
||||
</if>
|
||||
<if test="gender != null and gender.trim() != ''">
|
||||
and t1.gender = #{gender}
|
||||
</if>
|
||||
<if test="deptIdList != null">
|
||||
and t1.dept_id in
|
||||
<foreach item="id" collection="deptIdList" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="getById" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName from sys_user t1
|
||||
where t1.id = #{value}
|
||||
</select>
|
||||
|
||||
<select id="getByUsername" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select * from sys_user where username = #{value}
|
||||
</select>
|
||||
|
||||
<update id="updatePassword">
|
||||
update sys_user set password = #{newPassword} where id = #{id}
|
||||
</update>
|
||||
|
||||
<select id="getCountByDeptId" resultType="int">
|
||||
select count(*) from sys_user where dept_id = #{value}
|
||||
</select>
|
||||
|
||||
<select id="getUserIdListByDeptId" resultType="Long">
|
||||
select id from sys_user where dept_id in
|
||||
<foreach item="deptId" collection="list" open="(" separator="," close=")">
|
||||
#{deptId}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="xiaozhi.modules.sys.dao.SysUserDao">
|
||||
|
||||
<select id="getList" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName
|
||||
from sys_user t1 where t1.super_admin = 0
|
||||
<if test="username != null and username.trim() != ''">
|
||||
and t1.username like #{username}
|
||||
</if>
|
||||
<if test="deptId != null and deptId.trim() != ''">
|
||||
and t1.dept_id = #{deptId}
|
||||
</if>
|
||||
<if test="gender != null and gender.trim() != ''">
|
||||
and t1.gender = #{gender}
|
||||
</if>
|
||||
<if test="deptIdList != null">
|
||||
and t1.dept_id in
|
||||
<foreach item="id" collection="deptIdList" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="getById" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select t1.*, (select t2.name from sys_dept t2 where t2.id=t1.dept_id) deptName from sys_user t1
|
||||
where t1.id = #{value}
|
||||
</select>
|
||||
|
||||
<select id="getByUsername" resultType="xiaozhi.modules.sys.entity.SysUserEntity">
|
||||
select * from sys_user where username = #{value}
|
||||
</select>
|
||||
|
||||
<update id="updatePassword">
|
||||
update sys_user set password = #{newPassword} where id = #{id}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Generated
+1432
-527
File diff suppressed because it is too large
Load Diff
@@ -8,10 +8,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"element-ui": "^2.15.14",
|
||||
"flyio": "^0.6.14",
|
||||
"normalize.css": "^8.0.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2"
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2",
|
||||
"xiaozhi": "file:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -5,7 +5,7 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<title>小智-智控台</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
// 引入各个模块的请求
|
||||
import user from './module/user.js'
|
||||
|
||||
/**
|
||||
* 接口地址
|
||||
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
|
||||
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
|
||||
*
|
||||
*/
|
||||
const DEV_API_SERVICE = 'https://apifoxmock.com/m1/5931378-5618560-default'
|
||||
// 8002开发完成完成后使用这个
|
||||
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
|
||||
|
||||
/**
|
||||
* 根据开发环境返回接口url
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getServiceUrl() {
|
||||
return DEV_API_SERVICE
|
||||
}
|
||||
|
||||
|
||||
/** request服务封装 */
|
||||
export default {
|
||||
getServiceUrl,
|
||||
user,
|
||||
}
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
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: '',
|
||||
_responseType: undefined, // 新增响应类型字段
|
||||
'send'() {
|
||||
this._header.token = localStorage.getItem(Constant.STORAGE_KEY.TOKEN)
|
||||
|
||||
// 打印请求信息
|
||||
fly.request(this._url, this._data, {
|
||||
method: this._method,
|
||||
headers: this._header,
|
||||
responseType: this._responseType
|
||||
}).then((res) => {
|
||||
const error = httpHandlerError(res, this._failCallback);
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._sucCallback) {
|
||||
this._sucCallback(res)
|
||||
}
|
||||
}).catch((res) => {
|
||||
// 打印失败响应
|
||||
console.log(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
|
||||
},
|
||||
// 新增类型设置方法
|
||||
'type'(responseType) {
|
||||
this._responseType = responseType;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || info.data.code === undefined) {
|
||||
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
|
||||
}
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
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()
|
||||
},
|
||||
// 解绑设备
|
||||
unbindDevice(device_id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.unbindDevice(device_id, callback);
|
||||
});
|
||||
}).send()
|
||||
},
|
||||
// 绑定设备
|
||||
bindDevice(deviceCode, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/device/bind/${deviceCode}`)
|
||||
.method('POST')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('绑定设备失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.bindDevice(deviceCode, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取验证码
|
||||
getCaptcha(uuid, callback) {
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
|
||||
.method('GET')
|
||||
.type('blob')
|
||||
.header({
|
||||
'Content-Type': 'image/gif',
|
||||
'Pragma': 'No-cache',
|
||||
'Cache-Control': 'no-cache'
|
||||
})
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => { // 添加错误参数
|
||||
|
||||
}).send()
|
||||
},
|
||||
// 注册账号
|
||||
register(registerForm, callback) {
|
||||
RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/register`).method('POST')
|
||||
.data(registerForm)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail(() => {
|
||||
}).send()
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Welcome from '../views/welcome.vue'
|
||||
import Login from '../views/login.vue'
|
||||
import Register from '@/views/register.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
@@ -9,7 +10,7 @@ const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'welcome',
|
||||
component: Welcome
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
@@ -27,7 +28,17 @@ const routes = [
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "about" */ '../views/home.vue')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "about" */ '../views/register.vue')
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
const HAVE_NO_RESULT = '暂无'
|
||||
export default {
|
||||
HAVE_NO_RESULT, // 项目的配置信息
|
||||
PAGE: {
|
||||
LOGIN: '/login',
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
Executable
+60
@@ -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)
|
||||
}
|
||||
Executable
+135
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
|
||||
.welcome {
|
||||
min-width: 1200px;
|
||||
min-height: 675px;
|
||||
height: 100vh;
|
||||
background-image: url("@/assets/login/background.png");
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
/* 从顶部中心对齐 */
|
||||
-webkit-background-size: cover;
|
||||
/* 兼容老版本WebKit浏览器 */
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
}
|
||||
|
||||
.login-text {
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.login-welcome {
|
||||
font-weight: 400;
|
||||
font-size: 9px;
|
||||
text-align: left;
|
||||
color: #818cae;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 18%;
|
||||
background-color: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 35px 0;
|
||||
width: 450px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: #979db1;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
width: 19px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
height: 35px;
|
||||
background: #5778ff;
|
||||
border-radius: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
line-height: 35px;
|
||||
margin: 35px 15px 15px;
|
||||
}
|
||||
|
||||
.code-send {
|
||||
width: 70px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
background: #e6ebff;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #5778ff;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
background: #f6f8fb;
|
||||
border: 1px solid #e4e6ef;
|
||||
height: 40px;
|
||||
padding: 0 15px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #e4e7ed;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
font-weight: 700;
|
||||
color: #3d4566;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
color: #5778ff;
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-container style="height: 100%;">
|
||||
<el-header class="header">
|
||||
<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-ai.png" alt="" style="width: 70px;height: 13px;" />
|
||||
<div class="equipment-management" @click="settingDevice=false">
|
||||
@@ -19,15 +19,16 @@
|
||||
<img src="@/assets/home/close.png" alt="" style="width: 6px;height: 6px;" />
|
||||
</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">
|
||||
<el-input placeholder="输入名称搜索.." v-model="serach" />
|
||||
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" @keyup.enter.native="handleSearch" />
|
||||
<img src="@/assets/home/search.png" alt=""
|
||||
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" />
|
||||
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
|
||||
</div>
|
||||
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
|
||||
<div class="user-info">
|
||||
158 3632 4642</div>
|
||||
{{ userInfo.mobile }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
@@ -55,20 +56,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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 style="display: flex;justify-content: space-between;">
|
||||
style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: flex-start;box-sizing: border-box;">
|
||||
<div class="device-item" v-for="(item,index) in filteredDeviceList" :key="index">
|
||||
<div style="display: flex;justify-content: space-between; align-items: center; ">
|
||||
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
|
||||
CC:ba:97:11:a6:ac
|
||||
<!-- CC:ba:97:11:a6:ac-->
|
||||
{{item.list[0]?.mac_address}}
|
||||
</div>
|
||||
<div>
|
||||
<div style="display: flex;align-items: center;">
|
||||
<img src="@/assets/home/delete.png" alt=""
|
||||
style="width: 18px;height: 18px;margin-right: 8px;" />
|
||||
style="width: 18px;height: 18px;margin-right: 8px;" @click="unbindDevice(item.list[0]?.id)" />
|
||||
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-name">
|
||||
设备型号:esp32-s3-touch-amoled-1.8
|
||||
设备型号:{{item.list[0]?.device_type}}
|
||||
</div>
|
||||
<div style="display: flex;gap: 8px;align-items: center;">
|
||||
<div class="settings-btn" @click="clickSettingDevice">
|
||||
@@ -77,30 +79,30 @@
|
||||
声纹识别</div>
|
||||
<div class="settings-btn">
|
||||
历史对话</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;" />
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<div>最近对话:6天前</div>
|
||||
<div>APP版本:1.1.0</div>
|
||||
<div>最近对话:{{item.list[0]?.recent_chat_time}}</div>
|
||||
<div>APP版本:{{item.list[0]?.app_version}}</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
|
||||
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
|
||||
style="width: 46px;height: 46px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/setting-user.png" alt="" style="width: 24px;height: 24px;" />
|
||||
style="width: 41px;height: 41px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||
<img src="@/assets/home/setting-user.png" alt="" style="width: 21px;height: 21px;" />
|
||||
</div>
|
||||
CC:ba:97:11:a6:ac
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<el-form ref="form" :model="form" label-width="90px">
|
||||
<div style="padding: 20px 30px;max-width: 990px;">
|
||||
<el-form ref="form" :model="form" label-width="81px">
|
||||
<div style="padding: 18px 28px;max-width: 890px;">
|
||||
<el-form-item label="助手昵称:">
|
||||
<div class="input-46">
|
||||
<div class="input-46" style="width: 57.5%;">
|
||||
<el-input v-model="form.name" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -119,7 +121,7 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<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;">
|
||||
<el-select v-model="form.timbre" placeholder="请选择" style="width: 100%;">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
@@ -135,28 +137,28 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="角色介绍:">
|
||||
<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 />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="记忆体:">
|
||||
<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" />
|
||||
<div class="prompt-bottom">
|
||||
<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">
|
||||
<i class="el-icon-delete-solid" style="font-size: 14px;" />
|
||||
<i class="el-icon-delete-solid" style="font-size: 12px;" />
|
||||
清除
|
||||
</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>
|
||||
</el-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%;">
|
||||
<el-select v-model="form.model" placeholder="请选择" style="width: 100%;">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label"
|
||||
@@ -172,23 +174,24 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
</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>
|
||||
<div class="reset-btn">
|
||||
重制</div>
|
||||
<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
|
||||
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-container>
|
||||
<el-dialog :visible.sync="addDeviceDialogVisible" width="480px" center>
|
||||
<el-dialog :visible.sync="addDeviceDialogVisible" width="400px" center>
|
||||
<div
|
||||
style="margin: 0 20px 20px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;;">
|
||||
<div
|
||||
@@ -207,7 +210,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 0 20px;gap: 10px;">
|
||||
<div class="dialog-btn" @click="addDeviceDialogVisible=false">确定</div>
|
||||
<div class="dialog-btn" @click="addDevice">确定</div>
|
||||
<div class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="addDeviceDialogVisible=false">
|
||||
@@ -220,14 +223,18 @@
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
data() {
|
||||
return {
|
||||
serach: '',
|
||||
serach: '', // 搜索框输入内容
|
||||
deviceList: [], // 原始设备列表
|
||||
filteredDeviceList: [], // 过滤后的设备列表
|
||||
switchValue: false,
|
||||
addDeviceDialogVisible: false,
|
||||
deviceCode: "",
|
||||
deviceCode: "", // 设备验证码
|
||||
settingDevice: false,
|
||||
form: {
|
||||
name: "",
|
||||
@@ -242,19 +249,94 @@ export default {
|
||||
}, {
|
||||
value: '选项2',
|
||||
label: '双皮奶'
|
||||
}]
|
||||
}
|
||||
}],
|
||||
userInfo: {
|
||||
mobile: '' // 初始化用户信息
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showAddDialog() {
|
||||
this.addDeviceDialogVisible = true;
|
||||
this.addDeviceDialogVisible = true;
|
||||
},
|
||||
clickSettingDevice() {
|
||||
this.settingDevice = true
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
Api.user.getUserInfo(({data}) => {
|
||||
this.userInfo = data.data
|
||||
});
|
||||
},
|
||||
// 获取已绑设备
|
||||
getList(){
|
||||
Api.user.getHomeList(({data})=>{
|
||||
this.deviceList = data.data; // 保存原始设备列表
|
||||
this.filteredDeviceList = data.data; // 初始化过滤后的设备列表
|
||||
})
|
||||
},
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
if (this.serach.trim() === '') {
|
||||
// 如果搜索框为空,显示全部设备
|
||||
this.filteredDeviceList = this.deviceList;
|
||||
} else {
|
||||
// 过滤设备列表
|
||||
this.filteredDeviceList = this.deviceList.filter(device => {
|
||||
return (
|
||||
device.list[0]?.mac_address?.includes(this.serach) || // 匹配MAC地址
|
||||
device.list[0]?.device_type?.includes(this.serach) || // 匹配设备型号
|
||||
device.list[0]?.app_version?.includes(this.serach) // 匹配APP版本
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
// 解绑设备
|
||||
unbindDevice(device_id) {
|
||||
this.$confirm('确定要解绑该设备吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 调用解绑设备的接口
|
||||
Api.user.unbindDevice(device_id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('解绑成功');
|
||||
this.getList();
|
||||
} else {
|
||||
this.$message.error(data.msg || '解绑失败');
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// 用户取消操作
|
||||
this.$message.info('已取消解绑');
|
||||
});
|
||||
},
|
||||
// 添加设备
|
||||
addDevice() {
|
||||
if (!this.deviceCode) {
|
||||
this.$message.warning('请输入设备验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
Api.user.bindDevice(this.deviceCode, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('设备绑定成功');
|
||||
this.addDeviceDialogVisible = false;
|
||||
this.getList(); // 刷新设备列表
|
||||
} else {
|
||||
this.$message.error(data.msg || '设备绑定失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserInfo(); // 组件加载时获取用户信息
|
||||
this.getList(); // 初始化设备列表
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
@@ -356,11 +438,14 @@ export default {
|
||||
}
|
||||
.serach-box {
|
||||
display: flex;
|
||||
width: 230px;
|
||||
width: 250px;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background-color: #e2f5f7;
|
||||
background-color: #f6fcfe66;
|
||||
border: 1px solid #e4e6ef;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.user-info {
|
||||
font-weight: 600;
|
||||
@@ -389,7 +474,7 @@ export default {
|
||||
}
|
||||
.template-item {
|
||||
height: 35px;
|
||||
width: 75px;
|
||||
width: 85px;
|
||||
border-radius: 8px;
|
||||
background: #e6ebff;
|
||||
line-height: 35px;
|
||||
@@ -461,7 +546,7 @@ export default {
|
||||
}
|
||||
}
|
||||
.device-item {
|
||||
width: 341px;
|
||||
width: 345px;
|
||||
border-radius: 15px;
|
||||
background: #fafcfe;
|
||||
padding: 22px;
|
||||
@@ -504,6 +589,7 @@ audio::-webkit-media-controls-panel {
|
||||
line-height: 34px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.save-btn {
|
||||
border-radius: 23px;
|
||||
@@ -526,6 +612,14 @@ audio::-webkit-media-controls-panel {
|
||||
border: none !important;
|
||||
padding: 15px;
|
||||
}
|
||||
// 搜索输入框的样式调整
|
||||
.serach-box .el-input__inner {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0 5px 0 15px;
|
||||
font-size: 12px;
|
||||
color: #3d4566;
|
||||
}
|
||||
.el-textarea .el-input__count {
|
||||
color: #979db1;
|
||||
font-size: 11px;
|
||||
@@ -533,13 +627,14 @@ audio::-webkit-media-controls-panel {
|
||||
background-color: transparent;
|
||||
}
|
||||
.el-input__inner {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
//border: none;
|
||||
//background-color: transparent;
|
||||
padding: 0 5px 0 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.input-46 .el-input__inner {
|
||||
padding: 0 15px;
|
||||
height: 46px;
|
||||
height: 38px;
|
||||
}
|
||||
.lh-form-item {
|
||||
.el-form-item__label {
|
||||
@@ -596,3 +691,5 @@ audio::-webkit-media-controls-panel {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -3,217 +3,135 @@
|
||||
<el-container style="height: 100%;">
|
||||
<el-header>
|
||||
<div
|
||||
style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;" />
|
||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;" />
|
||||
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-ai.png" alt="" style="width: 70px;height: 13px;"/>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main style="position: relative;">
|
||||
<div class="login-box">
|
||||
<div
|
||||
style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;" />
|
||||
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;"/>
|
||||
<div class="login-text">登录</div>
|
||||
<div class="login-welcome">
|
||||
WELCOME TO LOG IN</div>
|
||||
WELCOME TO LOGIN
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick">
|
||||
<el-tab-pane label="用户名" name="username">
|
||||
<div style="padding: 0 30px;">
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/username.png" alt="" class="input-icon" />
|
||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon" />
|
||||
<el-input v-model="form.password" placeholder="请输入密码" />
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||
<div style="cursor: pointer;">新用户注册</div>
|
||||
<div style="cursor: pointer;">忘记密码?</div>
|
||||
</div>
|
||||
<div style="padding: 0 30px;">
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/username.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="手机号" name="phone">
|
||||
<div style="padding: 0 30px;">
|
||||
<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>
|
||||
<img v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
@click="fetchCaptcha"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-btn" @click="login">登陆</div>
|
||||
<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>
|
||||
</div>
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<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-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
|
||||
import Api from '@/apis/api';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
data() {
|
||||
return {
|
||||
activeName: "username",
|
||||
form:{
|
||||
phoneNumber:'',
|
||||
phoneCode:'',
|
||||
username:'',
|
||||
password:''
|
||||
}
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
captchaId: ''
|
||||
},
|
||||
captchaUuid: '',
|
||||
captchaUrl: ''
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
handleClick(){
|
||||
mounted() {
|
||||
this.fetchCaptcha();
|
||||
},
|
||||
methods: {
|
||||
fetchCaptcha() {
|
||||
this.captchaUuid = getUUID();
|
||||
|
||||
Api.user.getCaptcha(this.captchaUuid, (res) => {
|
||||
if (res.status === 200) {
|
||||
const blob = new Blob([res.data], {type: res.data.type});
|
||||
this.captchaUrl = URL.createObjectURL(blob);
|
||||
|
||||
} else {
|
||||
console.error('验证码加载异常:', error);
|
||||
showDanger('验证码加载失败,点击刷新')
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async login() {
|
||||
if (!this.form.username.trim()) { // 替换isNull校验
|
||||
showDanger('用户名不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.form.password.trim()) { // 替换isNull校验
|
||||
showDanger('密码不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.form.captcha.trim()) { // 替换isNull校验
|
||||
showDanger('验证码不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
this.form.captchaId = this.captchaUuid
|
||||
Api.user.login(this.form, ({data}) => {
|
||||
console.log(data)
|
||||
showSuccess('登陆成功!')
|
||||
goToPage('/home')
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.fetchCaptcha()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
goToRegister() {
|
||||
goToPage('/register')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.welcome {
|
||||
min-width: 1200px;
|
||||
min-height: 675px;
|
||||
height: 100vh;
|
||||
background-image: url("@/assets/login/background.png");
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
/* 从顶部中心对齐 */
|
||||
-webkit-background-size: cover;
|
||||
/* 兼容老版本WebKit浏览器 */
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
}
|
||||
.login-text {
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
text-align: left;
|
||||
color: #3d4566;
|
||||
}
|
||||
.login-welcome {
|
||||
font-weight: 400;
|
||||
font-size: 9px;
|
||||
text-align: left;
|
||||
color: #818cae;
|
||||
align-self: flex-end;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.login-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 18%;
|
||||
background-color: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 35px 0;
|
||||
width: 450px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.el-dropdown-link {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
color: #979db1;
|
||||
}
|
||||
.input-icon {
|
||||
width: 19px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.login-btn {
|
||||
height: 35px;
|
||||
background: #5778ff;
|
||||
border-radius: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
line-height: 35px;
|
||||
margin: 35px 15px 15px;
|
||||
}
|
||||
.code-send {
|
||||
width: 70px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
background: #e6ebff;
|
||||
line-height: 32px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #5778ff;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.input-box {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
background: #f6f8fb;
|
||||
border: 1px solid #e4e6ef;
|
||||
height: 40px;
|
||||
padding: 0 15px;
|
||||
gap: 20px;
|
||||
}
|
||||
::v-deep {
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
}
|
||||
.el-tabs__nav-wrap::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #e4e7ed;
|
||||
z-index: 1;
|
||||
}
|
||||
.el-tabs__item {
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
font-weight: 700;
|
||||
color: #3d4566;
|
||||
}
|
||||
.el-tabs__item.is-active {
|
||||
color: #5778ff;
|
||||
}
|
||||
.el-tabs__nav-scroll {
|
||||
padding: 0 30px;
|
||||
}
|
||||
.el-input__inner {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@import './auth.scss'; // 添加这行引用
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<el-container style="height: 100%;">
|
||||
<!-- 保持相同的头部 -->
|
||||
<el-header>
|
||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img src="@/assets/xiaozhi-logo.png" alt="" style="width: 45px;height: 45px;"/>
|
||||
<img src="@/assets/xiaozhi-ai.png" alt="" style="width: 70px;height: 13px;"/>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-main style="position: relative;">
|
||||
<div class="login-box">
|
||||
<!-- 修改标题部分 -->
|
||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||
<img src="@/assets/login/hi.png" alt="" style="width: 34px;height: 34px;"/>
|
||||
<div class="login-text">注册</div>
|
||||
<div class="login-welcome">
|
||||
WELCOME TO REGISTER
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 30px;">
|
||||
<!-- 用户名输入框 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/username.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.username" placeholder="请输入用户名"/>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
|
||||
</div>
|
||||
|
||||
<!-- 新增确认密码 -->
|
||||
<div class="input-box">
|
||||
<img src="@/assets/login/password.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.confirmPassword" type="password" placeholder="请确认密码"/>
|
||||
</div>
|
||||
|
||||
<!-- 验证码部分保持相同 -->
|
||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||
<img src="@/assets/login/shield.png" alt="" class="input-icon"/>
|
||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;"/>
|
||||
</div>
|
||||
<img v-if="captchaUrl"
|
||||
:src="captchaUrl"
|
||||
alt="验证码"
|
||||
style="width: 150px; height: 40px; cursor: pointer;"
|
||||
@click="fetchCaptcha"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 修改底部链接 -->
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||
<div style="cursor: pointer;" @click="goToLogin">已有账号?立即登录</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改按钮文本 -->
|
||||
<div class="login-btn" @click="register">立即注册</div>
|
||||
|
||||
<!-- 保持相同的协议声明 -->
|
||||
<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>
|
||||
</div>
|
||||
</el-main>
|
||||
|
||||
<!-- 保持相同的页脚 -->
|
||||
<el-footer>
|
||||
<div style="font-size: 12px;font-weight: 400;color: #979db1;">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {getUUID, goToPage, showDanger, showSuccess} from '@/utils'
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'register',
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
captcha: '',
|
||||
captchaId: ''
|
||||
},
|
||||
captchaUrl: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCaptcha();
|
||||
},
|
||||
methods: {
|
||||
// 复用验证码获取方法
|
||||
fetchCaptcha() {
|
||||
this.form.captchaId = getUUID();
|
||||
Api.user.getCaptcha(this.form.captchaId, (res) => {
|
||||
if (res.status === 200) {
|
||||
const blob = new Blob([res.data], {type: res.data.type});
|
||||
this.captchaUrl = URL.createObjectURL(blob);
|
||||
|
||||
} else {
|
||||
console.error('验证码加载异常:', error);
|
||||
showDanger('验证码加载失败,点击刷新');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 注册逻辑
|
||||
register() {
|
||||
if (!this.form.username.trim()) {
|
||||
showDanger('用户名不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.form.password.trim()) {
|
||||
showDanger('密码不能为空')
|
||||
return
|
||||
}
|
||||
if (this.form.password !== this.form.confirmPassword) {
|
||||
showDanger('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
if (!this.form.captcha.trim()) {
|
||||
showDanger('验证码不能为空')
|
||||
return
|
||||
}
|
||||
Api.user.register(this.form, ({data}) => {
|
||||
console.log(data)
|
||||
if (data.code === 0) {
|
||||
showSuccess('注册成功!')
|
||||
goToPage('/login')
|
||||
} else {
|
||||
showDanger(data.msg || '注册失败')
|
||||
this.fetchCaptcha()
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.fetchCaptcha()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
goToLogin() {
|
||||
goToPage('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './auth.scss'; // 修改为导入新建的SCSS文件
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
const { defineConfig } = require('@vue/cli-service');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
// 确保加载 .env 文件
|
||||
dotenv.config();
|
||||
|
||||
module.exports = defineConfig({
|
||||
devServer: {
|
||||
// Bug 修复:将代理配置为环境变量中定义的 API 基础 URL
|
||||
proxy: {
|
||||
'/xiaozhi-esp32-api': {
|
||||
target: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8002', // 后端 API 的基础 URL
|
||||
changeOrigin: true, // 允许跨域
|
||||
// pathRewrite: {
|
||||
// '^/api': '', // 路径重写
|
||||
// },
|
||||
},
|
||||
},
|
||||
client: {
|
||||
overlay: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
.git
|
||||
__pycache__
|
||||
*.pyc
|
||||
.env
|
||||
Dockerfile
|
||||
../docs/
|
||||
tmp/
|
||||
data/
|
||||
LICENSE
|
||||
README.md
|
||||
README_en.md
|
||||
manager/static
|
||||
manager/static/webui/
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -1,10 +1,27 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import signal
|
||||
from config.settings import load_config, check_config_file
|
||||
from core.websocket_server import WebSocketServer
|
||||
from core.utils.util import check_ffmpeg_installed
|
||||
|
||||
TAG = __name__
|
||||
|
||||
async def wait_for_exit():
|
||||
"""Windows 和 Linux 兼容的退出监听"""
|
||||
loop = asyncio.get_running_loop()
|
||||
stop_event = asyncio.Event()
|
||||
|
||||
if sys.platform == "win32":
|
||||
# Windows: 用 sys.stdin.read() 监听 Ctrl + C
|
||||
await loop.run_in_executor(None, sys.stdin.read)
|
||||
else:
|
||||
# Linux/macOS: 用 signal 监听 Ctrl + C
|
||||
def stop():
|
||||
stop_event.set()
|
||||
loop.add_signal_handler(signal.SIGINT, stop)
|
||||
loop.add_signal_handler(signal.SIGTERM, stop) # 支持 kill 进程
|
||||
await stop_event.wait()
|
||||
|
||||
async def main():
|
||||
check_config_file()
|
||||
@@ -16,11 +33,19 @@ async def main():
|
||||
ws_task = asyncio.create_task(ws_server.start())
|
||||
|
||||
try:
|
||||
# 等待 WebSocket 服务器运行
|
||||
await ws_task
|
||||
await wait_for_exit() # 监听退出信号
|
||||
except asyncio.CancelledError:
|
||||
print("任务被取消,清理资源中...")
|
||||
finally:
|
||||
ws_task.cancel()
|
||||
|
||||
try:
|
||||
await ws_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
print("服务器已关闭,程序退出。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("手动中断,程序终止。")
|
||||
|
||||
+104
-20
@@ -37,7 +37,8 @@ log:
|
||||
data_dir: data
|
||||
iot:
|
||||
Speaker:
|
||||
volume: 100
|
||||
# 设置esp32的音量,范围0-100
|
||||
volume: 80
|
||||
xiaozhi:
|
||||
type: hello
|
||||
version: 1
|
||||
@@ -64,13 +65,34 @@ CMD_exit:
|
||||
|
||||
# 具体处理时选择的模块(The module selected for specific processing)
|
||||
selected_module:
|
||||
ASR: FunASR
|
||||
# 语音活动检测模块,默认使用SileroVAD模型
|
||||
VAD: SileroVAD
|
||||
# 语音识别模块,默认使用FunASR本地模型
|
||||
ASR: FunASR
|
||||
# 将根据配置名称对应的type调用实际的LLM适配器
|
||||
LLM: ChatGLMLLM
|
||||
# TTS将根据配置名称对应的type调用实际的TTS适配器
|
||||
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:
|
||||
mem0ai:
|
||||
@@ -78,7 +100,13 @@ Memory:
|
||||
# https://app.mem0.ai/dashboard/api-keys
|
||||
# 每月有1000次免费调用
|
||||
api_key: 你的mem0ai api key
|
||||
|
||||
nomem:
|
||||
# 不想使用记忆功能,可以使用nomem
|
||||
type: nomem
|
||||
mem_local_short:
|
||||
# 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器
|
||||
type: mem_local_short
|
||||
|
||||
ASR:
|
||||
FunASR:
|
||||
type: fun_local
|
||||
@@ -105,6 +133,16 @@ LLM:
|
||||
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
model_name: qwen-turbo
|
||||
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:
|
||||
# 定义LLM API类型
|
||||
type: openai
|
||||
@@ -143,8 +181,9 @@ LLM:
|
||||
CozeLLM:
|
||||
# 定义LLM API类型
|
||||
type: coze
|
||||
bot_id: 你的bot_id
|
||||
user_id: 你的user_id
|
||||
# bot_id和user_id的内容写在引号之内
|
||||
bot_id: "你的bot_id"
|
||||
user_id: "你的user_id"
|
||||
personal_access_token: 你的coze个人令牌
|
||||
LMStudioLLM:
|
||||
# 定义LLM API类型
|
||||
@@ -158,6 +197,15 @@ LLM:
|
||||
base_url: http://homeassistant.local:8123
|
||||
agent_id: conversation.chatgpt
|
||||
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:
|
||||
# 当前支持的type为edge、doubao,可自行适配
|
||||
EdgeTTS:
|
||||
@@ -249,6 +297,21 @@ TTS:
|
||||
parallel_infer: true
|
||||
repetition_penalty: 1.35
|
||||
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:
|
||||
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
|
||||
# 平台地址:https://platform.minimaxi.com/
|
||||
@@ -296,8 +359,11 @@ TTS:
|
||||
type: aliyun
|
||||
output_file: tmp/
|
||||
appkey: 你的阿里云智能语音交互服务项目Appkey
|
||||
token: 你的阿里云智能语音交互服务AccessToken
|
||||
token: 你的阿里云智能语音交互服务AccessToken,临时的24小时,要长期用下方的access_key_id,access_key_secret
|
||||
voice: xiaoyun
|
||||
access_key_id: 你的阿里云账号access_key_id
|
||||
access_key_secret: 你的阿里云账号access_key_secret
|
||||
|
||||
# 以下可不用设置,使用默认设置
|
||||
# format: wav
|
||||
# sample_rate: 16000
|
||||
@@ -309,13 +375,43 @@ TTS:
|
||||
TTS302AI:
|
||||
# 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息
|
||||
# 获取api_keyn路径:https://dash.302.ai/apis/list
|
||||
# 价格,$35/百万字符。火山原版¥450元/万字符
|
||||
# 价格,$35/百万字符。火山原版¥450元/百万字符
|
||||
type: doubao
|
||||
api_url: https://api.302ai.cn/doubao/tts_hd
|
||||
authorization: "Bearer "
|
||||
voice: "zh_female_wanwanxiaohe_moon_bigtts"
|
||||
output_file: tmp/
|
||||
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-hd,tts-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:
|
||||
test_sentences: # 自定义测试语句
|
||||
@@ -325,18 +421,6 @@ module_test:
|
||||
|
||||
# 本地音乐播放配置
|
||||
music:
|
||||
music_commands:
|
||||
- "来一首歌"
|
||||
- "唱一首歌"
|
||||
- "播放音乐"
|
||||
- "来点音乐"
|
||||
- "背景音乐"
|
||||
- "放首歌"
|
||||
- "播放歌曲"
|
||||
- "来点背景音乐"
|
||||
- "我想听歌"
|
||||
- "我要听歌"
|
||||
- "放点音乐"
|
||||
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
|
||||
music_ext: # 音乐文件类型,p3格式效率最高
|
||||
- ".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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -11,10 +11,11 @@ import websockets
|
||||
from typing import Dict, Any
|
||||
from core.utils.dialogue import Message, Dialogue
|
||||
from core.handle.textHandle import handleTextMessage
|
||||
from core.utils.util import get_string_no_punctuation_or_emoji
|
||||
from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_from_string
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||
from core.handle.sendAudioHandle import sendAudioMessage
|
||||
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 core.auth import AuthMiddleware, AuthenticationError
|
||||
from core.utils.auth_code_gen import AuthCodeGenerator
|
||||
@@ -27,7 +28,7 @@ class TTSException(RuntimeError):
|
||||
|
||||
|
||||
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.logger = setup_logging()
|
||||
self.auth = AuthMiddleware(config)
|
||||
@@ -55,6 +56,7 @@ class ConnectionHandler:
|
||||
self.llm = _llm
|
||||
self.tts = _tts
|
||||
self.memory = _memory
|
||||
self.intent = _intent
|
||||
|
||||
# vad相关变量
|
||||
self.client_audio_buffer = bytes()
|
||||
@@ -88,6 +90,10 @@ class ConnectionHandler:
|
||||
self.auth_code_gen = AuthCodeGenerator.get_instance()
|
||||
self.is_device_verified = False # 添加设备验证状态标志
|
||||
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
|
||||
|
||||
async def handle_connection(self, ws):
|
||||
try:
|
||||
@@ -101,7 +107,8 @@ class ConnectionHandler:
|
||||
await self.auth.authenticate(self.headers)
|
||||
|
||||
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
|
||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||
@@ -204,7 +211,6 @@ class ConnectionHandler:
|
||||
return False
|
||||
return not self.is_device_verified
|
||||
|
||||
|
||||
def chat(self, query):
|
||||
if self.isNeedAuth():
|
||||
self.llm_finish_task = True
|
||||
@@ -213,6 +219,7 @@ class ConnectionHandler:
|
||||
return True
|
||||
|
||||
self.dialogue.put(Message(role="user", content=query))
|
||||
|
||||
response_message = []
|
||||
processed_chars = 0 # 跟踪已处理的字符位置
|
||||
try:
|
||||
@@ -220,10 +227,10 @@ class ConnectionHandler:
|
||||
# 使用带记忆的对话
|
||||
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}")
|
||||
|
||||
self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}")
|
||||
llm_responses = self.llm.response(
|
||||
self.session_id,
|
||||
self.session_id,
|
||||
self.dialogue.get_llm_dialogue_with_memory(memory_str)
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -245,7 +252,7 @@ class ConnectionHandler:
|
||||
current_text = full_text[processed_chars:] # 从未处理的位置开始
|
||||
|
||||
# 查找最后一个有效标点
|
||||
punctuations = ("。", "?", "!", "?", "!", ";", ";", ":", ":")
|
||||
punctuations = ("。", "?", "!", ";", ":")
|
||||
last_punct_pos = -1
|
||||
for punct in punctuations:
|
||||
pos = current_text.rfind(punct)
|
||||
@@ -282,6 +289,157 @@ class ConnectionHandler:
|
||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
||||
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 # 跟踪已处理的字符位置
|
||||
|
||||
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"对话记录: {self.dialogue.get_llm_dialogue_with_memory(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
|
||||
|
||||
# 处理流式响应
|
||||
tool_call_flag = False
|
||||
function_name = None
|
||||
function_id = None
|
||||
function_arguments = ""
|
||||
content_arguments = ""
|
||||
for response in llm_responses:
|
||||
content, tools_call = response
|
||||
if content is not None and len(content)>0:
|
||||
if len(response_message)<=0 and content=="```":
|
||||
tool_call_flag = True
|
||||
|
||||
if tools_call is not None:
|
||||
tool_call_flag = True
|
||||
if tools_call[0].id is not None:
|
||||
function_id = tools_call[0].id
|
||||
if tools_call[0].function.name is not None:
|
||||
function_name = tools_call[0].function.name
|
||||
if tools_call[0].function.arguments is not None:
|
||||
function_arguments += tools_call[0].function.arguments
|
||||
|
||||
if content is not None and len(content) > 0:
|
||||
if tool_call_flag:
|
||||
content_arguments+=content
|
||||
else:
|
||||
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) # 更新已处理字符位置
|
||||
|
||||
# 处理最后剩余的文本
|
||||
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)
|
||||
|
||||
# 存储对话内容
|
||||
if len(response_message)>0:
|
||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
||||
|
||||
# 处理function call
|
||||
if tool_call_flag:
|
||||
if function_id is None:
|
||||
a = extract_json_from_string(content_arguments)
|
||||
if a is not None:
|
||||
content_arguments_json = json.loads(a)
|
||||
function_name = content_arguments_json["function_name"]
|
||||
function_arguments = json.dumps(content_arguments_json["args"], ensure_ascii=False)
|
||||
function_id = str(uuid.uuid4().hex)
|
||||
else:
|
||||
return []
|
||||
function_arguments = json.loads(function_arguments)
|
||||
self.logger.bind(tag=TAG).info(f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}")
|
||||
function_call_data = {
|
||||
"name": function_name,
|
||||
"id": function_id,
|
||||
"arguments": function_arguments
|
||||
}
|
||||
result = handle_llm_function_call(self, function_call_data)
|
||||
self._handle_function_result(result, function_call_data, text_index+1)
|
||||
|
||||
self.llm_finish_task = True
|
||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
||||
|
||||
return True
|
||||
|
||||
def _handle_function_result(self, result, function_call_data, text_index):
|
||||
if result.action == Action.RESPONSE: # 直接回复前端
|
||||
text = result.response
|
||||
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.dialogue.put(Message(role="assistant", content=text))
|
||||
if result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||
text = result.response
|
||||
if result.action == Action.NOTFOUND:
|
||||
text = result.response
|
||||
|
||||
|
||||
|
||||
def _tts_priority_thread(self):
|
||||
while not self.stop_event.is_set():
|
||||
text = None
|
||||
@@ -372,3 +530,14 @@ class ConnectionHandler:
|
||||
self.client_have_voice_last_time = 0
|
||||
self.client_voice_stop = False
|
||||
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,167 @@
|
||||
from config.logger import setup_logging
|
||||
import json
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.utils.dialogue import Message
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
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
|
||||
|
||||
# 使用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):
|
||||
"""检查是否有明确的退出命令"""
|
||||
_, text = remove_punctuation_and_length(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
|
||||
try:
|
||||
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
|
||||
|
||||
# 尝试解析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"]
|
||||
logger.bind(tag=TAG).info(f"服务端设置音量为{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):
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = setup_logging()
|
||||
|
||||
def _extract_song_name(text):
|
||||
"""从用户输入中提取歌名"""
|
||||
for keyword in ["听", "播放", "放", "唱"]:
|
||||
for keyword in ["播放音乐"]:
|
||||
if keyword in text:
|
||||
parts = text.split(keyword)
|
||||
if len(parts) > 1:
|
||||
@@ -36,6 +36,7 @@ def _find_best_match(potential_song, music_files):
|
||||
best_match = music_file
|
||||
return best_match
|
||||
|
||||
|
||||
class MusicManager:
|
||||
def __init__(self, music_dir, music_ext):
|
||||
self.music_dir = Path(music_dir)
|
||||
@@ -55,23 +56,20 @@ class MusicManager:
|
||||
music_files.append(str(file.relative_to(self.music_dir)))
|
||||
return music_files
|
||||
|
||||
|
||||
class MusicHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.music_related_keywords = []
|
||||
|
||||
if "music" in self.config:
|
||||
self.music_config = self.config["music"]
|
||||
self.music_dir = os.path.abspath(
|
||||
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.refresh_time = self.music_config.get("refresh_time", 60)
|
||||
else:
|
||||
self.music_dir = os.path.abspath("./music")
|
||||
self.music_related_keywords = ["来一首歌", "唱一首歌", "播放音乐", "来点音乐", "背景音乐", "放首歌",
|
||||
"播放歌曲", "来点背景音乐", "我想听歌", "我要听歌", "放点音乐"]
|
||||
self.music_ext = (".mp3", ".wav", ".p3")
|
||||
self.refresh_time = 60
|
||||
|
||||
@@ -100,13 +98,9 @@ class MusicHandler:
|
||||
logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}")
|
||||
await self.play_local_music(conn, specific_file=best_match)
|
||||
return True
|
||||
|
||||
# 检查是否是通用播放音乐命令
|
||||
if any(cmd in clean_text for cmd in self.music_related_keywords):
|
||||
await self.play_local_music(conn)
|
||||
return True
|
||||
|
||||
return False
|
||||
await self.play_local_music(conn)
|
||||
return True
|
||||
|
||||
async def play_local_music(self, conn, specific_file=None):
|
||||
"""播放本地音乐文件"""
|
||||
@@ -117,26 +111,18 @@ class MusicHandler:
|
||||
|
||||
# 确保路径正确性
|
||||
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
|
||||
music_path = os.path.join(self.music_dir, specific_file)
|
||||
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:
|
||||
logger.bind(tag=TAG).error("未找到MP3音乐文件")
|
||||
return
|
||||
selected_music = random.choice(self.music_files)
|
||||
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}")
|
||||
return
|
||||
|
||||
if not os.path.exists(music_path):
|
||||
logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
|
||||
return
|
||||
text = f"正在播放{selected_music}"
|
||||
await send_stt_message(conn, text)
|
||||
conn.tts_first_text_index = 0
|
||||
|
||||
@@ -2,6 +2,7 @@ from config.logger import setup_logging
|
||||
import time
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.handle.intentHandler import handle_user_intent
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -33,13 +34,7 @@ async def handleAudioMessage(conn, audio):
|
||||
else:
|
||||
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
||||
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
||||
text_len, text_without_punctuation = 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
|
||||
text_len, _ = remove_punctuation_and_length(text)
|
||||
if text_len > 0:
|
||||
await startToChat(conn, text)
|
||||
else:
|
||||
@@ -48,20 +43,22 @@ async def handleAudioMessage(conn, audio):
|
||||
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):
|
||||
# 异步发送 stt 信息
|
||||
# 首先进行意图分析
|
||||
intent_handled = await handle_user_intent(conn, text)
|
||||
|
||||
if intent_handled:
|
||||
# 如果意图已被处理,不再进行聊天
|
||||
conn.asr_server_receive = True
|
||||
return
|
||||
|
||||
# 意图未被处理,继续常规聊天流程
|
||||
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):
|
||||
@@ -70,8 +67,9 @@ async def no_voice_close_connect(conn):
|
||||
else:
|
||||
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
||||
close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120)
|
||||
if no_voice_time > 1000 * close_connection_no_voice_time:
|
||||
if not conn.close_after_chat and no_voice_time > 1000 * close_connection_no_voice_time:
|
||||
conn.close_after_chat = True
|
||||
conn.client_abort = False
|
||||
conn.asr_server_receive = False
|
||||
prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜”为结尾"
|
||||
prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
||||
await startToChat(conn, prompt)
|
||||
|
||||
@@ -7,49 +7,49 @@ from core.utils.util import remove_punctuation_and_length, get_string_no_punctua
|
||||
TAG = __name__
|
||||
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):
|
||||
# 发送句子开始消息
|
||||
if text_index == conn.tts_first_text_index:
|
||||
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
||||
await send_tts_message(conn, "sentence_start", text)
|
||||
|
||||
# 初始化流控参数
|
||||
frame_duration = 60 # 毫秒
|
||||
start_time = time.perf_counter() # 使用高精度计时器
|
||||
play_position = 0 # 已播放的时长(毫秒)
|
||||
# 流控参数优化
|
||||
original_frame_duration = 60 # 原始帧时长(毫秒)
|
||||
adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
|
||||
total_frames = len(audios) # 获取总帧数
|
||||
compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
play_position = 0 # 已播放时长(毫秒)
|
||||
|
||||
for opus_packet in audios:
|
||||
if conn.client_abort:
|
||||
return
|
||||
|
||||
# 计算当前包的预期发送时间
|
||||
# 计算带加速因子的预期时间
|
||||
expected_time = start_time + (play_position / 1000)
|
||||
current_time = time.perf_counter()
|
||||
|
||||
# 等待直到预期时间
|
||||
# 流控等待(使用加速后的帧时长)
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# 发送音频包
|
||||
await conn.websocket.send(opus_packet)
|
||||
play_position += frame_duration # 更新播放位置
|
||||
play_position += adjusted_frame_duration # 使用调整后的帧时长
|
||||
|
||||
# 补偿因加速损失的时长
|
||||
if compensation > 0:
|
||||
await asyncio.sleep(compensation)
|
||||
|
||||
await send_tts_message(conn, "sentence_end", text)
|
||||
|
||||
# 发送结束消息(如果是最后一个文本)
|
||||
if conn.llm_finish_task and text_index == conn.tts_last_text_index:
|
||||
await send_tts_message(conn, 'stop', None)
|
||||
if await isLLMWantToFinish(text):
|
||||
if conn.close_after_chat:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def send_tts_message(conn, state, text=None):
|
||||
"""发送 TTS 状态消息"""
|
||||
message = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.handle.helloHandle import handleHelloMessage
|
||||
from core.handle.receiveAudioHandle import startToChat
|
||||
from core.handle.iotHandle import handleIotDescriptors
|
||||
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -40,5 +40,7 @@ async def handleTextMessage(conn, message):
|
||||
elif msg_json["type"] == "iot":
|
||||
if "descriptors" in msg_json:
|
||||
await handleIotDescriptors(conn, msg_json["descriptors"])
|
||||
if "states" in msg_json:
|
||||
await handleIotStatus(conn, msg_json["states"])
|
||||
except json.JSONDecodeError:
|
||||
await conn.websocket.send(message)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||
"""
|
||||
检测用户最后一句话的意图
|
||||
Args:
|
||||
dialogue_history: 对话历史记录列表,每条记录包含role和content
|
||||
Returns:
|
||||
返回识别出的意图,格式为:
|
||||
- "继续聊天"
|
||||
- "结束聊天"
|
||||
- "播放音乐 歌名" 或 "随机播放音乐"
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,109 @@
|
||||
from typing import List, Dict
|
||||
from ..base import IntentProviderBase
|
||||
from config.logger import setup_logging
|
||||
import re
|
||||
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 = []
|
||||
|
||||
"""
|
||||
"continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等",
|
||||
"end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
||||
"play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图"
|
||||
"""
|
||||
for key, value in self.intent_options.items():
|
||||
if key == "play_music":
|
||||
intent_list.append("3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图")
|
||||
elif key == "end_chat":
|
||||
intent_list.append("2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候")
|
||||
elif key == "continue_chat":
|
||||
intent_list.append("1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等")
|
||||
else:
|
||||
intent_list.append(value)
|
||||
|
||||
# "如果是唱歌、听歌、播放音乐,请指定歌名,格式为'播放音乐 [识别出的歌名]'。\n"
|
||||
# "如果听不出具体歌名,可以返回'随机播放音乐'。\n"
|
||||
# "只需要返回意图结果的json,不要解释。"
|
||||
# "返回格式如下:\n"
|
||||
prompt = (
|
||||
"你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类(使用<start>和<end>标志):\n"
|
||||
"<start>"
|
||||
f"{', '.join(intent_list)}"
|
||||
"<end>\n"
|
||||
"你需要按照以下的步骤处理用户的对话"
|
||||
"1. 思考出对话的意图是哪一类的"
|
||||
"2. 属于1和2的意图, 直接返回,返回格式如下:\n"
|
||||
"{intent: '用户意图'}\n"
|
||||
"3. 属于3的意图,则继续分析用户希望播放的音乐\n"
|
||||
"4. 如果无法识别出具体歌名,可以返回'随机播放音乐'\n"
|
||||
"{intent: '播放音乐 [获取的音乐名字]'}\n"
|
||||
"下面是几个处理的示例(思考的内容不返回, 只返回json部分, 无额外的内容)\n"
|
||||
"```"
|
||||
"用户: 你今天怎么样?\n"
|
||||
"思考(不返回): 用户发来的数据是一个问候语,属于继续聊天的意图, 是种类1, 种类1的需求是直接返回\n"
|
||||
"返回结果: {intent: '继续聊天'}\n"
|
||||
"```"
|
||||
"用户: 我今天有点累了, 我们明天再聊吧\n"
|
||||
"思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
|
||||
"返回结果: {intent: '结束聊天'}\n"
|
||||
"```"
|
||||
"用户: 我今天有点累了, 我们明天再聊吧\n"
|
||||
"思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
|
||||
"返回结果: {intent: '结束聊天'}\n"
|
||||
"```"
|
||||
"用户: 你可以播放一首中秋月给我听吗\n"
|
||||
"思考(不返回): 用户表达了想听音乐的续签,属于播放音乐的意图, 是种类3, 种类3的需求需要继续判断播放的音乐, 这里用户希望的歌曲名明确给出是中秋月\n"
|
||||
"返回结果: {intent: '播放音乐 [中秋月]'}\n"
|
||||
"```"
|
||||
"你现在可以使用的音乐的名称如下(使用<start>和<end>标志):\n"
|
||||
)
|
||||
return prompt
|
||||
|
||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text:str) -> str:
|
||||
if not self.llm:
|
||||
raise ValueError("LLM provider not set")
|
||||
|
||||
# 构建用户最后一句话的提示
|
||||
msgStr = ""
|
||||
|
||||
# 只使用最后两句即可
|
||||
if len(dialogue_history) >= 2:
|
||||
# 保证最少有两句话的时候处理
|
||||
msgStr += f"{dialogue_history[-2].role}: {dialogue_history[-2].content}\n"
|
||||
msgStr += f"{dialogue_history[-1].role}: {dialogue_history[-1].content}\n"
|
||||
|
||||
msgStr += f"User: {text}\n"
|
||||
user_prompt = f"当前的对话如下:\n{msgStr}"
|
||||
prompt_music = f"{self.promot}\n<start>{conn.music_handler.music_files}\n<end>"
|
||||
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
|
||||
# 使用LLM进行意图识别
|
||||
intent = self.llm.response_no_stream(
|
||||
system_prompt=prompt_music,
|
||||
user_prompt=user_prompt
|
||||
)
|
||||
# 使用正则表达式提取大括号中的内容
|
||||
# 使用正则表达式提取 {} 中的内容
|
||||
match = re.search(r'\{.*?\}', intent)
|
||||
if match:
|
||||
result = match.group(0) # 获取匹配到的内容(包含 {})
|
||||
print(result) # 输出:{intent: '播放音乐 [中秋月]'}
|
||||
intent = result
|
||||
else:
|
||||
intent = "{intent: '继续聊天'}"
|
||||
logger.bind(tag=TAG).info(f"Detected intent: {intent}")
|
||||
return intent.strip()
|
||||
@@ -0,0 +1,20 @@
|
||||
from ..base import IntentProviderBase
|
||||
from typing import List, Dict
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class IntentProvider(IntentProviderBase):
|
||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||
"""
|
||||
默认的意图识别实现,始终返回继续聊天
|
||||
Args:
|
||||
dialogue_history: 对话历史记录列表
|
||||
text: 本次对话记录
|
||||
Returns:
|
||||
固定返回"继续聊天"
|
||||
"""
|
||||
logger.bind(tag=TAG).debug("Using NoIntentProvider, always returning continue chat")
|
||||
return self.intent_options["continue_chat"]
|
||||
@@ -1,8 +1,38 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
class LLMProviderBase(ABC):
|
||||
@abstractmethod
|
||||
def response(self, session_id, dialogue):
|
||||
"""LLM response generator"""
|
||||
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}
|
||||
@@ -11,19 +11,31 @@ from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageContentType, Cha
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
self.personal_access_token = config.get("personal_access_token")
|
||||
self.bot_id = config.get("bot_id")
|
||||
self.user_id = config.get("user_id")
|
||||
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
||||
|
||||
def response(self, session_id, dialogue):
|
||||
coze_api_token = self.personal_access_token
|
||||
coze_api_base = COZE_CN_BASE_URL
|
||||
|
||||
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
|
||||
|
||||
|
||||
coze = Coze(auth=TokenAuth(token=coze_api_token), base_url=coze_api_base)
|
||||
conversation_id = self.session_conversation_map.get(session_id)
|
||||
|
||||
# 如果没有找到conversation_id,则创建新的对话
|
||||
if not conversation_id:
|
||||
conversation = coze.conversations.create(
|
||||
messages=[
|
||||
]
|
||||
)
|
||||
conversation_id = conversation.id
|
||||
self.session_conversation_map[session_id] = conversation_id # 更新映射
|
||||
|
||||
for event in coze.chat.stream(
|
||||
bot_id=self.bot_id,
|
||||
@@ -31,6 +43,7 @@ class LLMProvider(LLMProviderBase):
|
||||
additional_messages=[
|
||||
Message.build_user_question_text(last_msg["content"]),
|
||||
],
|
||||
conversation_id=conversation_id,
|
||||
):
|
||||
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
|
||||
print(event.message.content, end="", flush=True)
|
||||
|
||||
@@ -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
|
||||
from core.utils.util import check_model_key
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
"""初始化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")
|
||||
|
||||
if not self.api_key or "你" in self.api_key:
|
||||
logger.bind(tag=TAG).error("你还没配置Gemini LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
||||
|
||||
have_key = check_model_key("LLM", self.api_key)
|
||||
|
||||
if not have_key:
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
# 初始化Gemini客户端
|
||||
genai.configure(api_key=self.api_key)
|
||||
self.model = genai.GenerativeModel(self.model_name)
|
||||
|
||||
|
||||
# 设置生成参数
|
||||
self.generation_config = {
|
||||
"temperature": 0.7,
|
||||
@@ -55,7 +54,7 @@ class LLMProvider(LLMProviderBase):
|
||||
|
||||
# 创建新的聊天会话
|
||||
chat = self.model.start_chat(history=chat_history)
|
||||
|
||||
|
||||
# 发送消息并获取流式响应
|
||||
response = chat.send_message(
|
||||
current_msg,
|
||||
@@ -71,7 +70,7 @@ class LLMProvider(LLMProviderBase):
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}")
|
||||
|
||||
|
||||
# 针对不同错误返回友好提示
|
||||
if "Rate limit" in error_msg:
|
||||
yield "【Gemini服务请求太频繁,请稍后再试】"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from config.logger import setup_logging
|
||||
import requests, json
|
||||
from openai import OpenAI
|
||||
import json
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
|
||||
TAG = __name__
|
||||
@@ -8,39 +9,51 @@ logger = setup_logging()
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
|
||||
self.model_name = config.get("model_name")
|
||||
self.base_url = config.get("base_url", "http://localhost: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:
|
||||
# Convert dialogue format to Ollama format
|
||||
prompt = ""
|
||||
for msg in 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
|
||||
},
|
||||
responses = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=dialogue,
|
||||
stream=True
|
||||
)
|
||||
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
json_response = json.loads(line)
|
||||
if "response" in json_response:
|
||||
yield json_response["response"]
|
||||
|
||||
for chunk in responses:
|
||||
try:
|
||||
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||
content = delta.content if hasattr(delta, 'content') else ''
|
||||
if content:
|
||||
yield content
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
|
||||
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,
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||
|
||||
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
|
||||
from core.utils.util import check_model_key
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
@@ -14,8 +11,7 @@ class LLMProvider(LLMProviderBase):
|
||||
self.base_url = config.get("base_url")
|
||||
else:
|
||||
self.base_url = config.get("url")
|
||||
if "你" in self.api_key:
|
||||
logger.bind(tag=TAG).error("你还没配置LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
||||
check_model_key("LLM", self.api_key)
|
||||
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
|
||||
def response(self, session_id, dialogue):
|
||||
@@ -47,3 +43,19 @@ class LLMProvider(LLMProviderBase):
|
||||
|
||||
except Exception as 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,
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||
|
||||
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):
|
||||
self.config = config
|
||||
self.role_id = None
|
||||
self.llm = None
|
||||
|
||||
@abstractmethod
|
||||
async def save_memory(self, msgs):
|
||||
@@ -19,5 +20,6 @@ class MemoryProviderBase(ABC):
|
||||
"""Query memories for specific role based on similarity"""
|
||||
return "please implement query method"
|
||||
|
||||
def set_role_id(self, role_id: str):
|
||||
self.role_id = role_id
|
||||
def init_memory(self, role_id, llm):
|
||||
self.role_id = role_id
|
||||
self.llm = llm
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import traceback
|
||||
|
||||
from ..base import MemoryProviderBase, logger
|
||||
from mem0 import MemoryClient
|
||||
from core.utils.util import check_model_key
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -8,13 +11,19 @@ class MemoryProvider(MemoryProviderBase):
|
||||
super().__init__(config)
|
||||
self.api_key = config.get("api_key", "")
|
||||
self.api_version = config.get("api_version", "v1.1")
|
||||
if len(self.api_key) == 0 or "你" in self.api_key:
|
||||
logger.bind(tag=TAG).error("你还没配置Mem0ai的密钥,请在配置文件中配置密钥,否则无法提供记忆服务")
|
||||
have_key = check_model_key("Mem0ai", self.api_key)
|
||||
if not have_key :
|
||||
self.use_mem0 = False
|
||||
return
|
||||
else:
|
||||
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):
|
||||
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 uuid
|
||||
import json
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
|
||||
import http.client
|
||||
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):
|
||||
|
||||
|
||||
def __init__(self, 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.token = config.get("token")
|
||||
self.token = token
|
||||
self.format = config.get("format", "wav")
|
||||
self.sample_rate = config.get("sample_rate", 16000)
|
||||
self.voice = config.get("voice", "xiaoyun")
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import base64
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from core.utils.util import check_model_key
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
|
||||
|
||||
@@ -17,6 +18,7 @@ class TTSProvider(TTSProviderBase):
|
||||
self.api_url = config.get("api_url")
|
||||
self.authorization = config.get("authorization")
|
||||
self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
|
||||
check_model_key("TTS", self.access_token)
|
||||
|
||||
def generate_filename(self, extension=".wav"):
|
||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import base64
|
||||
import os
|
||||
import uuid
|
||||
@@ -9,6 +8,7 @@ from pydantic import BaseModel, Field, conint, model_validator
|
||||
from typing_extensions import Annotated
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from core.utils.util import check_model_key
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from config.logger import setup_logging
|
||||
|
||||
@@ -24,7 +24,7 @@ class ServeReferenceAudio(BaseModel):
|
||||
def decode_audio(cls, values):
|
||||
audio = values.get("audio")
|
||||
if (
|
||||
isinstance(audio, str) and len(audio) > 255
|
||||
isinstance(audio, str) and len(audio) > 255
|
||||
): # Check if audio is a string (Base64)
|
||||
try:
|
||||
values["audio"] = base64.b64decode(audio)
|
||||
@@ -36,6 +36,7 @@ class ServeReferenceAudio(BaseModel):
|
||||
def __repr__(self) -> str:
|
||||
return f"ServeReferenceAudio(text={self.text!r}, audio_size={len(self.audio)})"
|
||||
|
||||
|
||||
class ServeTTSRequest(BaseModel):
|
||||
text: str
|
||||
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()
|
||||
return wav
|
||||
|
||||
|
||||
def read_ref_text(ref_text):
|
||||
path = Path(ref_text)
|
||||
if path.exists() and path.is_file():
|
||||
@@ -77,31 +79,32 @@ def read_ref_text(ref_text):
|
||||
return file.read()
|
||||
return ref_text
|
||||
|
||||
|
||||
class TTSProvider(TTSProviderBase):
|
||||
|
||||
def __init__(self, config, delete_audio_file):
|
||||
super().__init__(config, delete_audio_file)
|
||||
|
||||
self.reference_id = config.get("reference_id")
|
||||
self.reference_audio = config.get("reference_audio",[])
|
||||
self.reference_text = config.get("reference_text",[])
|
||||
self.format = config.get("format","wav")
|
||||
self.channels = config.get("channels",1)
|
||||
self.rate = config.get("rate",44100)
|
||||
self.api_key = config.get("api_key","YOUR_API_KEY")
|
||||
if "你" in self.api_key:
|
||||
logger.bind(tag=TAG).error("你还没配置FishSpeech TTS的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
||||
self.reference_audio = config.get("reference_audio", [])
|
||||
self.reference_text = config.get("reference_text", [])
|
||||
self.format = config.get("format", "wav")
|
||||
self.channels = config.get("channels", 1)
|
||||
self.rate = config.get("rate", 44100)
|
||||
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
||||
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
||||
if not have_key:
|
||||
return
|
||||
self.normalize = config.get("normalize",True)
|
||||
self.max_new_tokens = config.get("max_new_tokens",1024)
|
||||
self.chunk_length = config.get("chunk_length",200)
|
||||
self.top_p = config.get("top_p",0.7)
|
||||
self.repetition_penalty = config.get("repetition_penalty",1.2)
|
||||
self.temperature = config.get("temperature",0.7)
|
||||
self.streaming = config.get("streaming",False)
|
||||
self.use_memory_cache = config.get("use_memory_cache","on")
|
||||
self.normalize = config.get("normalize", True)
|
||||
self.max_new_tokens = config.get("max_new_tokens", 1024)
|
||||
self.chunk_length = config.get("chunk_length", 200)
|
||||
self.top_p = config.get("top_p", 0.7)
|
||||
self.repetition_penalty = config.get("repetition_penalty", 1.2)
|
||||
self.temperature = config.get("temperature", 0.7)
|
||||
self.streaming = config.get("streaming", False)
|
||||
self.use_memory_cache = config.get("use_memory_cache", "on")
|
||||
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"):
|
||||
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:
|
||||
audio_file.write(audio_content)
|
||||
|
||||
|
||||
|
||||
|
||||
else:
|
||||
print(f"Request failed with status code {response.status_code}")
|
||||
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
|
||||
|
||||
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 = []
|
||||
|
||||
|
||||
@@ -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是否设置正确")
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import yaml
|
||||
import socket
|
||||
import subprocess
|
||||
import logging
|
||||
import re
|
||||
|
||||
|
||||
def get_project_dir():
|
||||
@@ -75,7 +76,7 @@ def get_string_no_punctuation_or_emoji(s):
|
||||
def remove_punctuation_and_length(text):
|
||||
# 全角符号和半角符号的Unicode范围
|
||||
full_width_punctuations = '!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~'
|
||||
half_width_punctuations = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
|
||||
half_width_punctuations = r'!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
|
||||
space = ' ' # 半角空格
|
||||
full_width_space = ' ' # 全角空格
|
||||
|
||||
@@ -87,35 +88,13 @@ def remove_punctuation_and_length(text):
|
||||
return 0, ""
|
||||
return len(result), result
|
||||
|
||||
|
||||
def check_password(password):
|
||||
"""
|
||||
检查密码是否满足以下条件:
|
||||
1. 密码长度大于八位。
|
||||
2. 密码包含英文和数字。
|
||||
3. 密码不能包含“xiaozhi”字符。
|
||||
|
||||
:param password: 要检查的密码
|
||||
:return: 如果密码满足条件,则返回True;否则返回False。
|
||||
"""
|
||||
# 检查密码长度
|
||||
if len(password) < 8:
|
||||
def check_model_key(modelType, modelKey):
|
||||
if "你" in modelKey:
|
||||
logging.error("你还没配置" + modelType + "的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
||||
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
|
||||
|
||||
|
||||
def check_ffmpeg_installed():
|
||||
ffmpeg_installed = False
|
||||
try:
|
||||
@@ -140,4 +119,12 @@ def check_ffmpeg_installed():
|
||||
error_msg += "\n建议您:\n"
|
||||
error_msg += "1、按照项目的安装文档,正确进入conda环境\n"
|
||||
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
|
||||
raise ValueError(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
def extract_json_from_string(input_string):
|
||||
"""提取字符串中的 JSON 部分"""
|
||||
pattern = r'(\{.*\})'
|
||||
match = re.search(pattern, input_string)
|
||||
if match:
|
||||
return match.group(1) # 返回提取的 JSON 字符串
|
||||
return None
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user