Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf08a91191 | ||
|
|
f0e353cc1a | ||
|
|
456b1eddcc | ||
|
|
5e08f05437 | ||
|
|
9f9227bae4 | ||
|
|
76f47bce37 | ||
|
|
e1f54b30dc | ||
|
|
fc4e7e6c05 | ||
|
|
7aaa79b644 | ||
|
|
9b6f894be2 | ||
|
|
48f7ef2023 | ||
|
|
54692859fd | ||
|
|
1719244015 | ||
|
|
48f8c6c5b7 | ||
|
|
f0815e595b | ||
|
|
4f829017ed | ||
|
|
d2f8f05acb | ||
|
|
e8b1156bfd | ||
|
|
bf748773c8 | ||
|
|
a05b25ae90 | ||
|
|
3ceb3feccf | ||
|
|
9e9af2a031 | ||
|
|
b50c7dbed4 | ||
|
|
233e1ee0a6 | ||
|
|
805b40ec76 | ||
|
|
86ca3e5be0 | ||
|
|
bb2143d9a0 | ||
|
|
ef662e0ccf | ||
|
|
30708a59f1 | ||
|
|
b0262cfe72 | ||
|
|
259886c1a8 | ||
|
|
f1b4b33727 | ||
|
|
38e8b690bd | ||
|
|
9a52b43e09 | ||
|
|
5e35cfc44a | ||
|
|
ac0ec6dfe6 | ||
|
|
361defc348 | ||
|
|
6ac8947168 | ||
|
|
5655930d12 | ||
|
|
18acc37324 | ||
|
|
e9155d719e | ||
|
|
16e77b02c2 | ||
|
|
adc1902649 | ||
|
|
1cc470bc5a | ||
|
|
b3f6efc7d0 | ||
|
|
d91fbf3ff4 | ||
|
|
e552593474 | ||
|
|
1f5a78f458 | ||
|
|
f490c15282 | ||
|
|
90a31debd1 | ||
|
|
416145a90e | ||
|
|
6285786282 | ||
|
|
f9f6a69c89 | ||
|
|
61da60bdf1 | ||
|
|
2d5ef3011a | ||
|
|
63dab6d224 | ||
|
|
38dd76dfb3 | ||
|
|
fe642de526 | ||
|
|
f4b7eefc38 | ||
|
|
5bbbad02b4 | ||
|
|
bd85dd5faf | ||
|
|
d91ba3d219 | ||
|
|
7992c40815 | ||
|
|
574b01db97 | ||
|
|
2d6049b7bd | ||
|
|
96d0f2e6f9 | ||
|
|
b46bd4d0dd | ||
|
|
244c76793c | ||
|
|
411fc46189 | ||
|
|
48427e330f | ||
|
|
5ddc2451c1 | ||
|
|
e357cad2bb | ||
|
|
80be8b2eaa | ||
|
|
9a5aaea8ca | ||
|
|
7d585615a9 | ||
|
|
3f3375dcac | ||
|
|
f555eae653 | ||
|
|
e805a6e037 | ||
|
|
3ed82ed7bb | ||
|
|
d638e512c9 | ||
|
|
204b966935 | ||
|
|
8318ba5e55 | ||
|
|
d39c595828 | ||
|
|
04154162c6 | ||
|
|
47f8e0f04c | ||
|
|
7e4ada5f4b | ||
|
|
b0b4f87848 | ||
|
|
d9085b82bf | ||
|
|
8e5770ff59 | ||
|
|
beb420c69d | ||
|
|
1d8b898924 | ||
|
|
b5b4c5ced7 | ||
|
|
b4e9d77895 | ||
|
|
21a08b3c7a | ||
|
|
6977d6bf73 | ||
|
|
39f81dd0c0 | ||
|
|
b3191f5e0b | ||
|
|
78f2f21142 | ||
|
|
497fc1d1ae | ||
|
|
d83e1c6fb2 | ||
|
|
c5218d485a | ||
|
|
19cbd3b8da | ||
|
|
c0c5630bfb | ||
|
|
969b5fcfbb | ||
|
|
0739fa2195 | ||
|
|
3c62adab3c | ||
|
|
edc601c35e | ||
|
|
951370eee7 | ||
|
|
fa81f4a150 | ||
|
|
e6a90c40fb | ||
|
|
2e0a1c00fa | ||
|
|
109754fac9 | ||
|
|
c3fbff3d7a | ||
|
|
6dd967fe24 | ||
|
|
f0bfdba944 | ||
|
|
7f76b5f4ee | ||
|
|
35f4d8f15a | ||
|
|
783f17e9e8 | ||
|
|
9e305c2840 | ||
|
|
78fa5d3110 | ||
|
|
d57482d966 | ||
|
|
3d7c2aff4b | ||
|
|
3757c5dde7 | ||
|
|
13c0a966ec | ||
|
|
d987619946 | ||
|
|
8fb9498fe6 | ||
|
|
b8f64a6693 | ||
|
|
e31bb047bf | ||
|
|
6897b813a6 | ||
|
|
83f86406b5 | ||
|
|
dde237ff0d | ||
|
|
6a4887e252 | ||
|
|
d945b94910 | ||
|
|
bc7dcf791d | ||
|
|
66c71fe3b7 | ||
|
|
16a7aeec3b | ||
|
|
94605345e5 | ||
|
|
ec9fa32c81 | ||
|
|
264aa15281 | ||
|
|
180a6ff824 | ||
|
|
5904ee1571 | ||
|
|
bc99341f85 | ||
|
|
8b9174b67e | ||
|
|
6c04a6cf69 | ||
|
|
14a63c4b97 | ||
|
|
098d13a34b | ||
|
|
488f247744 | ||
|
|
c74dbf03cd | ||
|
|
1f836c3235 | ||
|
|
bd2e2e77d5 | ||
|
|
6282ef14e8 | ||
|
|
558f23688f | ||
|
|
541f2de599 | ||
|
|
68116254cd | ||
|
|
77ff4599ea | ||
|
|
652f5a3247 | ||
|
|
9b6e57b143 | ||
|
|
bdc19256bf | ||
|
|
de5eaf4467 | ||
|
|
ca884833d1 | ||
|
|
9fc164a69d | ||
|
|
0da2da83a5 | ||
|
|
bfdfa44edd | ||
|
|
ed1cfe5eb6 | ||
|
|
b696f3f5db | ||
|
|
7a0cf5ef9a | ||
|
|
d1b7cb6a36 | ||
|
|
b21d699472 | ||
|
|
029fadd778 | ||
|
|
215d50235b | ||
|
|
000e676397 | ||
|
|
208c045d3d | ||
|
|
ee20f59a8e | ||
|
|
925cf3dabb | ||
|
|
0049d0e926 | ||
|
|
56959808a9 | ||
|
|
8667a9680e | ||
|
|
8c7d129089 | ||
|
|
a3a9b98a1d | ||
|
|
f2f66579cb | ||
|
|
1354cf2a87 | ||
|
|
15f5592429 | ||
|
|
dc69094f00 | ||
|
|
8ad3783a36 | ||
|
|
15d5dd8e92 | ||
|
|
b07edb977b | ||
|
|
1509fc2039 | ||
|
|
2877bb8faa | ||
|
|
a83410cb49 | ||
|
|
4ff9f2235b | ||
|
|
1bc611f097 | ||
|
|
4e0a3941ff | ||
|
|
4e885dac3e | ||
|
|
eafd131a22 | ||
|
|
289ce3716b | ||
|
|
2242070bdc | ||
|
|
ee83cea11c | ||
|
|
6e571a43d8 | ||
|
|
466a0245a9 | ||
|
|
d9ce5e4ce7 | ||
|
|
6b0dd291c2 | ||
|
|
4ff77d0d7e | ||
|
|
e00da5d8d2 | ||
|
|
1a525a59ab | ||
|
|
d5581c4236 | ||
|
|
4bf950574c | ||
|
|
0b9e434c4b | ||
|
|
c4b1466643 | ||
|
|
488842029b | ||
|
|
1b0ce59371 | ||
|
|
97bea89204 | ||
|
|
0892dfc601 | ||
|
|
20d66ff401 | ||
|
|
e2fda532d1 | ||
|
|
68035af7b3 | ||
|
|
ee25555f0e | ||
|
|
e528510f5c | ||
|
|
afa0527b4c | ||
|
|
dd7d23498f | ||
|
|
eaaea2b24c | ||
|
|
58d0aefae1 | ||
|
|
11c9915a70 | ||
|
|
6cf6b0d645 | ||
|
|
1980a39b73 | ||
|
|
8d3fbeeb22 | ||
|
|
2678644c2c | ||
|
|
25da9fe119 | ||
|
|
03d89d22ec | ||
|
|
100cd25ffc | ||
|
|
6284a74a6e | ||
|
|
978cd8ba8e | ||
|
|
c76f28824c | ||
|
|
703ecfae40 | ||
|
|
b15c59aa48 | ||
|
|
98a4ce701c | ||
|
|
2fc0de8f4e | ||
|
|
66fd551d68 | ||
|
|
6e4e9a002b | ||
|
|
91278b0b3d | ||
|
|
ff9b3ed1a2 | ||
|
|
0c855d4ba8 | ||
|
|
9176e9d255 | ||
|
|
2bab917b3e | ||
|
|
b80086a349 | ||
|
|
8a4fca9bd5 | ||
|
|
13288a6b3a | ||
|
|
57ed552f43 | ||
|
|
5a4d65986d | ||
|
|
427b3bb774 | ||
|
|
4912f385c6 | ||
|
|
dcb6d0feab | ||
|
|
de8c762d79 | ||
|
|
056659304a | ||
|
|
525bf18a03 | ||
|
|
7714c3f611 | ||
|
|
768b9fb7c1 | ||
|
|
db4f028955 | ||
|
|
e3928ffbcc | ||
|
|
db5159bd5e | ||
|
|
2626e1ebf0 | ||
|
|
5d69ba0796 | ||
|
|
c39ad97b8e | ||
|
|
7c73f060ed | ||
|
|
f0aa7df3b0 | ||
|
|
5882dec23a | ||
|
|
835c9deb28 | ||
|
|
a4ecc7ac7f | ||
|
|
526a195e4d | ||
|
|
5e77560313 | ||
|
|
1abacfe90b | ||
|
|
d042c589df | ||
|
|
9f0f2870d0 | ||
|
|
9dd445b0ce | ||
|
|
8e1cc6e75b | ||
|
|
a36887d885 | ||
|
|
e300d04502 | ||
|
|
3d8afac9e1 | ||
|
|
81d2571f8f | ||
|
|
e4a0d2c56c | ||
|
|
55be189772 | ||
|
|
56352478fe | ||
|
|
f16d0406ea | ||
|
|
9d1871dc80 | ||
|
|
137f29e9e5 | ||
|
|
0e81a165e4 | ||
|
|
a73bcc05bc | ||
|
|
cd97018c4d | ||
|
|
a42ed5c388 | ||
|
|
f4bd80b5f9 | ||
|
|
498542f8be | ||
|
|
1aaa31ceb0 | ||
|
|
c9eb640510 | ||
|
|
766a706250 | ||
|
|
6fd63870f0 | ||
|
|
0be5350a1c | ||
|
|
48a5489c52 | ||
|
|
fe0fb9a884 | ||
|
|
91a2846794 | ||
|
|
625c9c2e40 | ||
|
|
710b338e6d | ||
|
|
024468adbd | ||
|
|
e4d173ad28 | ||
|
|
f4218b032a | ||
|
|
b87ebc6e1c | ||
|
|
d901b0ed34 | ||
|
|
519340dbf8 | ||
|
|
a7f6a6cc16 | ||
|
|
a0e56757d8 | ||
|
|
a618c2a2c6 | ||
|
|
07d8f783e0 | ||
|
|
623bfe32f7 | ||
|
|
6862735aa0 | ||
|
|
5461794a1e | ||
|
|
d9cd9acd27 | ||
|
|
b2788ac7a0 | ||
|
|
d4f6e25f92 | ||
|
|
17bcaba026 | ||
|
|
aed4e6b1e4 | ||
|
|
50897e97af | ||
|
|
e57137b101 | ||
|
|
3370ecb2ef |
@@ -9,26 +9,24 @@ assignees: ''
|
|||||||
## 🐛 问题描述
|
## 🐛 问题描述
|
||||||
<!-- 清晰简洁地描述问题是什么 -->
|
<!-- 清晰简洁地描述问题是什么 -->
|
||||||
|
|
||||||
## 🔍 复现步骤
|
## 🖥️ 环境信息
|
||||||
<!-- 详细描述复现问题的步骤 -->
|
- 部署方式: 全模块部署 还是 单Server部署
|
||||||
|
- 版本号: 例如 0.3.x
|
||||||
|
|
||||||
|
## 🔍 告诉我们,应该怎么复现这个问题
|
||||||
|
<!-- 这个很重要,方便我们快速定位 -->
|
||||||
1. 打开 '...'
|
1. 打开 '...'
|
||||||
2. 点击 '...'
|
2. 点击 '...'
|
||||||
3. 滚动到 '...'
|
3. 滚动到 '...'
|
||||||
4. 看到错误
|
4. 看到错误
|
||||||
|
|
||||||
## 🤔 预期行为
|
## 🤔 你原本希望是怎么样的
|
||||||
<!-- 简要描述预期的正确行为 -->
|
<!-- 简要描述预期的正确行为 -->
|
||||||
|
|
||||||
## 😯 截图
|
## 😯 提供一些截图
|
||||||
<!-- 如果适用,添加问题的截图 -->
|
<!-- 如果适用,添加问题的截图 -->
|
||||||
|
1. 比如日志截图,越多越好
|
||||||
## 🖥️ 环境信息
|
2. 比如界面反应
|
||||||
- 操作系统: [例如 Windows 10]
|
|
||||||
- 浏览器: [例如 Chrome 89]
|
|
||||||
- 项目版本: [例如 1.0.0]
|
|
||||||
- Python版本: [例如 3.9.13]
|
|
||||||
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
|
|
||||||
- Nodejs版本:[例如 v20.14.0]
|
|
||||||
|
|
||||||
## 📋 其他信息
|
## 📋 其他信息
|
||||||
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
|
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -41,7 +42,13 @@ jobs:
|
|||||||
- name: Extract version from tag
|
- name: Extract version from tag
|
||||||
id: get_version
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
if [[ "$GITHUB_REF" =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||||
|
echo "VERSION=${BASH_REMATCH[1]}" >> $GITHUB_ENV
|
||||||
|
echo "IS_VERSION=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "VERSION=latest" >> $GITHUB_ENV
|
||||||
|
echo "IS_VERSION=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
# 构建 xiaozhi-server 镜像
|
# 构建 xiaozhi-server 镜像
|
||||||
- name: Build and push xiaozhi-server
|
- name: Build and push xiaozhi-server
|
||||||
@@ -51,8 +58,7 @@ jobs:
|
|||||||
file: Dockerfile-server
|
file: Dockerfile-server
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
|
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
||||||
ghcr.io/${{ github.repository }}:server_latest
|
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
# 构建 manager-api 镜像
|
# 构建 manager-api 镜像
|
||||||
@@ -63,6 +69,5 @@ jobs:
|
|||||||
file: Dockerfile-web
|
file: Dockerfile-web
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:web_${{ env.VERSION }}
|
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
||||||
ghcr.io/${{ github.repository }}:web_latest
|
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -155,7 +155,14 @@ main/manager-web/node_modules
|
|||||||
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||||
main/xiaozhi-server/models/sherpa-onnx*
|
main/xiaozhi-server/models/sherpa-onnx*
|
||||||
my_wakeup_words.mp3
|
my_wakeup_words.mp3
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_not_found.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code/*.wav
|
||||||
|
!main/xiaozhi-server/config/assets/max_output_size.wav
|
||||||
main/manager-api/.vscode
|
main/manager-api/.vscode
|
||||||
|
|
||||||
# Ignore webpack cache directory
|
# Ignore webpack cache directory
|
||||||
main/manager-web/.webpack_cache/
|
main/manager-web/.webpack_cache/
|
||||||
|
main/xiaozhi-server/mysql
|
||||||
|
uploadfile
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ RUN apt-get update && \
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 配置Nginx
|
# 配置Nginx
|
||||||
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
# 复制前端构建产物
|
# 复制前端构建产物
|
||||||
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
|
||||||
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
本项目为开源智能硬件项目
|
本项目为开源智能硬件项目
|
||||||
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
||||||
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python实现<br/>
|
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
|
||||||
帮助您快速搭建小智服务器
|
帮助您快速搭建小智服务器
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="./README_en.md">English</a>
|
<a href="./README.md">English</a>
|
||||||
· 简体中文
|
· <a href="./docs/FAQ.md">常见问题</a>
|
||||||
|
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">反馈问题</a>
|
||||||
|
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a>
|
||||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
|
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
|
||||||
· <a href="./docs/Deployment.md">部署文档</a>
|
|
||||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues ">反馈问题</a>
|
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||||
@@ -126,109 +125,71 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 系统要求与部署前提 🖥️
|
|
||||||
|
|
||||||
建议 4 核 CPU、8G 内存的电脑。如果开启ASR也使用API,可运行在2核CPU、2G内存的服务器中。[请参考部署架构图](./docs/images/deploy.png)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 警告 ⚠️
|
## 警告 ⚠️
|
||||||
|
|
||||||
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
||||||
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
||||||
|
|
||||||
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
|
2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。
|
||||||
`config.yaml` 中开启防护:
|
|
||||||
|
|
||||||
```yaml
|
---
|
||||||
server:
|
|
||||||
auth:
|
## 部署文档
|
||||||
# 开启防护
|
|
||||||
enabled: true
|

|
||||||
|
|
||||||
|
本项目提供两种部署方式,请根据您的具体需求选择:
|
||||||
|
|
||||||
|
#### 🚀 部署方式选择
|
||||||
|
|
||||||
|
| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 |
|
||||||
|
|---------|------|---------|---------|---------|
|
||||||
|
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
|
||||||
|
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
|
||||||
|
|
||||||
|
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
|
||||||
|
|
||||||
|
```
|
||||||
|
智控台地址: https://2662r3426b.vicp.fun
|
||||||
|
|
||||||
|
服务测试工具: https://2662r3426b.vicp.fun/test/
|
||||||
|
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
|
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
```
|
```
|
||||||
|
|
||||||
开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 功能清单 ✨
|
||||||
|
|
||||||
## 部署方式 🚀
|
### 已实现 ✅
|
||||||
|
|
||||||
[](./docs/Deployment.md)
|
| 功能模块 | 描述 |
|
||||||
|
|---------|------|
|
||||||
|
| 通信协议 | 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互 |
|
||||||
|
| 对话交互 | 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 |
|
||||||
|
| 意图识别 | 支持使用LLM意图识别、function call函数调用,减少硬编码意图判断 |
|
||||||
|
| 多语言识别 | 支持国语、粤语、英语、日语、韩语(默认使用 FunASR) |
|
||||||
|
| LLM 模块 | 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口 |
|
||||||
|
| TTS 模块 | 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求 |
|
||||||
|
| 记忆功能 | 支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求 |
|
||||||
|
| IOT功能 | 支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制 |
|
||||||
|
| 智控台 | 提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理 |
|
||||||
|
|
||||||
### 一、[部署文档](./docs/Deployment.md)
|
### 正在开发 🚧
|
||||||
|
|
||||||
本项目支持以下三种部署方式,您可根据实际需求选择。
|
想了解具体开发计划进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
||||||
|
|
||||||
1. [Docker 快速部署](./docs/Deployment.md)
|
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.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)
|
|
||||||
|
|
||||||
适用于已安装 Docker 且希望对代码进行自定义修改的软件工程师。
|
|
||||||
|
|
||||||
3. [本地源码运行](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)
|
|
||||||
|
|
||||||
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
|
|
||||||
|
|
||||||
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。视频教程可参考[帅哥大佬教你源码部署](https://www.bilibili.com/video/BV1GvQWYZEd2)
|
|
||||||
|
|
||||||
### 二、[固件编译](./docs/firmware-build.md)
|
|
||||||
|
|
||||||
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
|
|
||||||
|
|
||||||
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常见问题 ❓
|
|
||||||
|
|
||||||
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 产品生态 👬
|
## 产品生态 👬
|
||||||
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
||||||
|
|
||||||
- [小智安卓客户端](https://github.com/TOM88812/xiaozhi-android-client)
|
| 项目名称 | 项目地址 | 项目描述 |
|
||||||
|
|:---------------------|:--------|:--------|
|
||||||
一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。现在是flutter版本,打通IOS、Android端。
|
| 小智安卓客户端 | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | 一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。<br/>现在是flutter版本,打通IOS、Android端。 |
|
||||||
- [小智电脑客户端](https://github.com/Huang-junsen/py-xiaozhi)
|
| 小智电脑客户端 | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | 该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,<br/>依然能够体过代码体验小智 AI 的功能。 |
|
||||||
|
| 小智Java服务端 | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | 小智开源后端服务 Java 版本是一个基于 Java 的开源项目。<br/>它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
|
||||||
该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,依然能够体过代码体验小智 AI 的功能。主要功能包括 AI 语音交互、视觉多模态识别、IoT 设备集成、联网音乐播放、语音唤醒、自动对话模式、图形化界面、命令行模式、跨平台支持、音量控制、会话管理、加密音频传输、自动验证码处理等。
|
|
||||||
- [小智Java服务端](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
|
|
||||||
|
|
||||||
小智开源后端服务 Java 版本是一个基于 Java 的开源项目,它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。
|
|
||||||
---
|
|
||||||
## 功能清单 ✨
|
|
||||||
|
|
||||||
### 已实现 ✅
|
|
||||||
|
|
||||||
- **通信协议**
|
|
||||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
|
||||||
- **对话交互**
|
|
||||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
|
||||||
- **意图识别**
|
|
||||||
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
|
|
||||||
- **多语言识别**
|
|
||||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
|
||||||
- **LLM 模块**
|
|
||||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
|
||||||
- **TTS 模块**
|
|
||||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
|
||||||
- **记忆功能**
|
|
||||||
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
|
|
||||||
- **IOT功能**
|
|
||||||
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
|
|
||||||
|
|
||||||
### 正在开发 🚧
|
|
||||||
|
|
||||||
- 多种心情模式
|
|
||||||
- 智控台webui
|
|
||||||
|
|
||||||
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
|
||||||
|
|
||||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -270,7 +231,7 @@ server:
|
|||||||
| 使用方式 | 支持平台 | 免费平台 |
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||||
| 接口调用 | DoubaoASR | - |
|
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -294,10 +255,17 @@ server:
|
|||||||
|
|
||||||
## 鸣谢 🙏
|
## 鸣谢 🙏
|
||||||
|
|
||||||
- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。
|
| Logo | 项目/公司 | 说明 |
|
||||||
- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。
|
|:---:|:---:|:---|
|
||||||
|
| <img src="./docs/images/logo_bailing.png" width="160"> | [百聆语音对话机器人](https://github.com/wwbin2017/bailing) | 本项目受[百聆语音对话机器人](https://github.com/wwbin2017/bailing)启发,并在其基础上实现 |
|
||||||
|
| <img src="./docs/images/logo_tenclass.png" width="160"> | [十方融海](https://www.tenclass.com/) | 感谢[十方融海](https://www.tenclass.com/)为小智生态制定了标准的通讯协议、多设备兼容性方案及高并发场景实践示范;为本项目提供了全链路技术文档支持 |
|
||||||
|
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [玄凤科技](https://github.com/Eric0308) | 感谢[玄凤科技](https://github.com/Eric0308)贡献函数调用框架、MCP通信协议及插件化调用机制的实现代码,通过标准化的指令调度体系与动态扩展能力,显著提升了前端设备(IoT)的交互效率和功能延展性 |
|
||||||
|
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [汇远设计](http://ui.kwd988.net/) | 感谢[汇远设计](http://ui.kwd988.net/)为本项目提供专业视觉解决方案,用其服务超千家企业的设计实战经验,赋能本项目产品用户体验 |
|
||||||
|
| <img src="./docs/images/logo_qinren.png" width="160"> | [西安勤人信息科技](https://www.029app.com/) | 感谢[西安勤人信息科技](https://www.029app.com/)深化本项目视觉体系,确保整体设计风格在多场景应用中的一致性和扩展性 |
|
||||||
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||||
|
|||||||
@@ -1,302 +0,0 @@
|
|||||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
|
||||||
|
|
||||||
<center>
|
|
||||||
<h1>Xiaozhi Backend Server xiaozhi-esp32-server</h1>
|
|
||||||
</center>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
This project provides backend services for the open-source smart hardware project
|
|
||||||
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a><br/>
|
|
||||||
Implemented in Python according to the <a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">Xiaozhi Communication Protocol</a><br/>
|
|
||||||
Helping you quickly set up your Xiaozhi server
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="./README.md">简体中文</a>
|
|
||||||
· English
|
|
||||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Changelog</a>
|
|
||||||
· <a href="./docs/Deployment.md">Deployment Guide</a>
|
|
||||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Report Issues</a>
|
|
||||||
</p>
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
|
||||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors">
|
|
||||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server?logo=github" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
|
|
||||||
<img alt="Issues" src="https://img.shields.io/github/issues/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
|
|
||||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
|
|
||||||
<img alt="GitHub pull requests" src="https://img.shields.io/badge/license-MIT-white?labelColor=black" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server">
|
|
||||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/stars/xinnan-tech/xiaozhi-esp32-server?color=ffcb47&labelColor=black" />
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Target Users 👥
|
|
||||||
|
|
||||||
This project requires ESP32 hardware devices. If you have purchased ESP32-related hardware, successfully connected to Brother Xia's backend service, and wish to set up your own `xiaozhi-esp32` backend service, then this project is perfect for you.
|
|
||||||
|
|
||||||
Want to see it in action? Check out these videos 🎥
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="Xiaozhi esp32 connecting to your own backend model" src="docs/images/demo1.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="Custom voice" src="docs/images/demo2.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="Communicating in Cantonese" src="docs/images/demo3.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="Control home appliances" src="docs/images/demo5.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="Lowest cost configuration" src="docs/images/demo4.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1Vy96YCE3R" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="Custom voice" src="docs/images/demo6.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="Play music" src="docs/images/demo7.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="Weather plugin" src="docs/images/demo8.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="IOT command control device" src="docs/images/demo9.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
|
||||||
<picture>
|
|
||||||
<img alt="News broadcast" src="docs/images/demo0.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## System Requirements and Prerequisites 🖥️
|
|
||||||
|
|
||||||
- **Computer or Server**: Recommended 4-core CPU, 8GB RAM computer. If using ASR with API, can run on a 2-core CPU, 2GB RAM server.
|
|
||||||
- **Update Client Interface**: Please update the backend service interface address in the client.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Warning ⚠️
|
|
||||||
|
|
||||||
1. This is open-source software. This software and any third-party API service providers it interfaces with (including but not limited to speech recognition, large language models, speech synthesis, and other platforms) have no commercial partnership. We do not provide any form of guarantee for their service quality or financial security.
|
|
||||||
We recommend users prioritize service providers with relevant business licenses and carefully read their service agreements and privacy policies. This software does not host any account keys, does not participate in fund transfers, and does not bear the risk of recharge fund losses.
|
|
||||||
|
|
||||||
2. This project is relatively new and has not yet passed network security testing. Please do not use it in production environments. If you deploy this project for learning purposes in a public network environment, please make sure to enable protection in the `config.yaml` configuration file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
server:
|
|
||||||
auth:
|
|
||||||
# Enable protection
|
|
||||||
enabled: true
|
|
||||||
```
|
|
||||||
|
|
||||||
After enabling protection, you need to verify the machine's token or MAC address according to actual circumstances. Please refer to the configuration documentation for details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deployment Methods 🚀
|
|
||||||
|
|
||||||
### I. [Deployment Guide](./docs/Deployment.md)
|
|
||||||
|
|
||||||
This project supports three deployment methods. You can choose based on your actual needs.
|
|
||||||
|
|
||||||
1. [Quick Docker Deployment](./docs/Deployment.md)
|
|
||||||
|
|
||||||
Suitable for regular users who want to quickly experience without much environment configuration. The downside is that pulling the image can be slow. Video tutorial available: [Beautiful expert teaches Docker deployment](https://www.bilibili.com/video/BV1RNQnYDE5t)
|
|
||||||
|
|
||||||
2. [Deploy Using Docker Environment](./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)
|
|
||||||
|
|
||||||
For software engineers who have Docker installed and want to make custom code modifications.
|
|
||||||
|
|
||||||
3. [Local Source Code Run](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)
|
|
||||||
|
|
||||||
Suitable for users familiar with `Conda` environment or who want to build the running environment from scratch.
|
|
||||||
|
|
||||||
For scenarios requiring higher response speed, we recommend using the local source code run method to reduce additional overhead. Video tutorial available: [Handsome expert teaches source code deployment](https://www.bilibili.com/video/BV1GvQWYZEd2)
|
|
||||||
|
|
||||||
### II. [Firmware Compilation](./docs/firmware-build.md)
|
|
||||||
|
|
||||||
Click here to view the detailed process of [firmware compilation](./docs/firmware-build.md).
|
|
||||||
|
|
||||||
After successful flashing and network connection, wake up Xiaozhi using the wake word and pay attention to the console output on the server side.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Questions ❓
|
|
||||||
|
|
||||||
For issues or product suggestions, please [click here](docs/FAQ.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Product Ecosystem 👬
|
|
||||||
Xiaozhi is an ecosystem. When using this product, you might want to check out other excellent projects in this ecosystem:
|
|
||||||
|
|
||||||
- [Xiaozhi Android Client](https://github.com/TOM88812/xiaozhi-android-client)
|
|
||||||
A voice dialogue application based on xiaozhi-server for Android and iOS, supporting real-time voice interaction and text dialogue. Now in Flutter version, supporting both iOS and Android.
|
|
||||||
- [Xiaozhi PC Client](https://github.com/Huang-junsen/py-xiaozhi)
|
|
||||||
This project provides a Python-based Xiaobai AI client, allowing you to experience Xiaozhi AI features through code even without physical hardware. Main features include AI voice interaction, visual multimodal recognition, IoT device integration, online music playback, voice wake-up, automatic dialogue mode, graphical interface, command-line mode, cross-platform support, volume control, session management, encrypted audio transmission, automatic verification code processing, etc.
|
|
||||||
- [Xiaozhi Java Server](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
|
|
||||||
|
|
||||||
The Xiaozhi open-source backend service Java version is a Java-based open-source project that includes both frontend and backend services, aiming to provide users with a complete backend service solution.
|
|
||||||
---
|
|
||||||
## Feature List ✨
|
|
||||||
|
|
||||||
### Implemented ✅
|
|
||||||
|
|
||||||
- **Communication Protocol**
|
|
||||||
Based on `xiaozhi-esp32` protocol, implementing data interaction through WebSocket.
|
|
||||||
- **Dialogue Interaction**
|
|
||||||
Supports wake-up dialogue, manual dialogue, and real-time interruption. Automatically sleeps after long periods without dialogue
|
|
||||||
- **Intent Recognition**
|
|
||||||
Supports LLM intent recognition and function call, reducing hard-coded intent judgment
|
|
||||||
- **Multi-language Recognition**
|
|
||||||
Supports Mandarin, Cantonese, English, Japanese, Korean (default using FunASR).
|
|
||||||
- **LLM Module**
|
|
||||||
Supports flexible switching of LLM modules, default using ChatGLMLLM, can also use Alibaba Bailian, DeepSeek, Ollama, and other interfaces.
|
|
||||||
- **TTS Module**
|
|
||||||
Supports EdgeTTS (default), Volcano Engine Doubao TTS, and other TTS interfaces to meet speech synthesis needs.
|
|
||||||
- **Memory Function**
|
|
||||||
Supports ultra-long memory, local summary memory, and no memory modes to meet different scenario needs.
|
|
||||||
- **IOT Function**
|
|
||||||
Supports managing registered device IOT functions, supporting intelligent IoT control based on dialogue context.
|
|
||||||
|
|
||||||
### Under Development 🚧
|
|
||||||
|
|
||||||
- Multiple mood modes
|
|
||||||
- Smart control panel webui
|
|
||||||
|
|
||||||
To learn about specific development progress, [click here](https://github.com/users/xinnan-tech/projects/3)
|
|
||||||
|
|
||||||
If you are a software developer, here's an [Open Letter to Developers](docs/contributor_open_letter.md), welcome to join!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Supported Platforms/Components List 📋
|
|
||||||
|
|
||||||
### LLM Language Models
|
|
||||||
|
|
||||||
| Usage Method | Supported Platforms | Free Platforms |
|
|
||||||
|:---:|:---:|:---:|
|
|
||||||
| openai interface call | Alibaba Bailian, Volcano Engine Doubao, DeepSeek, Zhipu ChatGLM, Gemini | Zhipu ChatGLM, Gemini |
|
|
||||||
| ollama interface call | Ollama | - |
|
|
||||||
| dify interface call | Dify | - |
|
|
||||||
| fastgpt interface call | Fastgpt | - |
|
|
||||||
| coze interface call | Coze | - |
|
|
||||||
|
|
||||||
In fact, any LLM supporting openai interface calls can be integrated.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TTS Speech Synthesis
|
|
||||||
|
|
||||||
| Usage Method | Supported Platforms | Free Platforms |
|
|
||||||
|:---:|:---:|:---:|
|
|
||||||
| Interface Call | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Alibaba Cloud TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS | EdgeTTS, CosyVoiceSiliconflow(partial) |
|
|
||||||
| Local Service | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VAD Voice Activity Detection
|
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
|
||||||
|:---:|:---------:|:----:|:----:|:--:|
|
|
||||||
| VAD | SileroVAD | Local Use | Free | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ASR Speech Recognition
|
|
||||||
|
|
||||||
| Usage Method | Supported Platforms | Free Platforms |
|
|
||||||
|:---:|:---:|:---:|
|
|
||||||
| Local Use | FunASR, SherpaASR | FunASR, SherpaASR |
|
|
||||||
| Interface Call | DoubaoASR | - |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Memory Storage
|
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
|
||||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
|
||||||
| Memory | mem0ai | Interface Call | 1000 times/month quota | |
|
|
||||||
| Memory | mem_local_short | Local Summary | Free | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Intent Recognition
|
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
|
||||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
|
||||||
| Intent | intent_llm | Interface Call | Based on LLM pricing | Intent recognition through large models, highly generalizable |
|
|
||||||
| Intent | function_call | Interface Call | Based on LLM pricing | Intent completion through large model function calls, fast and effective |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Acknowledgments 🙏
|
|
||||||
|
|
||||||
- This project was inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it.
|
|
||||||
- Thanks to [Tenclass](https://www.tenclass.com/) for providing detailed documentation support for the Xiaozhi communication protocol.
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
|
||||||
<source media="(prefers-color-scheme: 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>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 部署方案参考
|
# 部署架构图
|
||||||

|

|
||||||
# 方式一:docker快速部署
|
# 方式一:Docker只运行Server
|
||||||
|
|
||||||
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ chmod +x docker-setup.sh
|
|||||||
>
|
>
|
||||||
> 执行完成后,请按照提示配置 API 密钥。
|
> 执行完成后,请按照提示配置 API 密钥。
|
||||||
|
|
||||||
当你一切顺利完成以上操作后,继续操作[配置项目文件](#3-配置项目文件)
|
当你一切顺利完成以上操作后,继续操作[配置项目文件](#2-配置项目文件)
|
||||||
|
|
||||||
### 1.2 手动部署
|
### 1.2 手动部署
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ xiaozhi-server
|
|||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
##### 1.2.3.2 下载 config.yaml
|
##### 1.2.3.2 创建 config.yaml
|
||||||
|
|
||||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||||
|
|
||||||
@@ -102,14 +102,14 @@ xiaozhi-server
|
|||||||
|
|
||||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
## 3. 配置项目文件
|
## 2. 配置项目文件
|
||||||
|
|
||||||
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
||||||
[跳转到配置项目文件](#配置项目)
|
[跳转到配置项目文件](#配置项目)
|
||||||
|
|
||||||
配置完项目文件后,回到本教程继续往下。
|
配置完项目文件后,回到本教程继续往下。
|
||||||
|
|
||||||
## 4. 执行docker命令
|
## 3. 执行docker命令
|
||||||
|
|
||||||
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
@@ -137,50 +137,15 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
docker stop xiaozhi-esp32-server
|
||||||
docker rm xiaozhi-esp32-server
|
docker rm xiaozhi-esp32-server
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
5.3、重新按docker方式部署
|
5.3、重新按docker方式部署
|
||||||
|
|
||||||
# 方式二:借助Docker环境运行部署
|
# 方式二:本地源码只运行Server
|
||||||
|
|
||||||
开发人员如果不想安装`conda`环境,可以使用这种方法管理好依赖。
|
|
||||||
|
|
||||||
## 1.克隆项目
|
|
||||||
|
|
||||||
## 2.[跳转到下载语音识别模型文件](#模型文件)
|
|
||||||
|
|
||||||
## 3.[跳转到配置项目文件](#配置项目)
|
|
||||||
|
|
||||||
## 4.运行docker
|
|
||||||
|
|
||||||
修改完配置后,打开命令行工具,`cd`进入到你的`main/xiaozhi-server`下,执行以下命令
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconfined \
|
|
||||||
-p 8000:8000 \
|
|
||||||
-p 8002:8002 \
|
|
||||||
-v ./:/app \
|
|
||||||
kalicyh/python:xiaozhi
|
|
||||||
```
|
|
||||||
|
|
||||||
然后就和正常开发一样了
|
|
||||||
|
|
||||||
## 5.安装依赖
|
|
||||||
|
|
||||||
在刚刚的打开的终端运行
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6.运行项目
|
|
||||||
|
|
||||||
```sh
|
|
||||||
python app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
# 方式三:本地源码运行
|
|
||||||
|
|
||||||
## 1.安装基础环境
|
## 1.安装基础环境
|
||||||
|
|
||||||
@@ -259,43 +224,36 @@ python app.py
|
|||||||
## 配置项目
|
## 配置项目
|
||||||
|
|
||||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
如果你的`data`下面没有`.config.yaml`文件,有两个方式,任选一种:
|
||||||
|
|
||||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
第一个方式:你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`。在此文件上修改
|
||||||
|
|
||||||
|
第二个方式:你也可以创建在`data`目录下手动创建`.config.yaml`空文件,然后在这个文件中增加必要的配置信息,系统会优先读取`.config.yaml`文件的配置,如果`.config.yaml`没有配置的,系统会自动去加载`xiaozhi-server`目录下的`config.yaml`的配置。推荐使用这种方式,这种方式是最简洁的方式。
|
||||||
|
|
||||||
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||||
|
|
||||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例
|
||||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
server:
|
||||||
|
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
|
||||||
|
prompt: |
|
||||||
|
我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||||
|
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
||||||
|
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
|
||||||
|
请你像一个人一样说话,请勿返回配置xml及其他特殊字符。
|
||||||
|
|
||||||
selected_module:
|
selected_module:
|
||||||
VAD: SileroVAD
|
LLM: DoubaoLLM
|
||||||
ASR: FunASR
|
|
||||||
LLM: ChatGLMLLM
|
|
||||||
TTS: EdgeTTS
|
|
||||||
# 默认不开启记忆,如需开启请看配置文件里的描述
|
|
||||||
Memory: nomem
|
|
||||||
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
|
||||||
Intent: nointent
|
|
||||||
```
|
|
||||||
|
|
||||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
|
||||||
使用时,在`selected_module`修改成对应的如下LLM配置的名称:
|
|
||||||
|
|
||||||
```
|
|
||||||
LLM:
|
LLM:
|
||||||
DeepSeekLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
ChatGLMLLM:
|
ChatGLMLLM:
|
||||||
type: openai
|
api_key: xxxxxxxxxxxxxxx.xxxxxx
|
||||||
...
|
|
||||||
DifyLLM:
|
|
||||||
type: dify
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。
|
||||||
|
比如你要换更换模型,修改`selected_module`下的配置就行。
|
||||||
|
|
||||||
## 模型文件
|
## 模型文件
|
||||||
|
|
||||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
@@ -311,33 +269,40 @@ LLM:
|
|||||||
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
||||||
|
|
||||||
```
|
```
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8002/xiaozhi/ota/
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||||
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
||||||
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
||||||
|
|
||||||
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
||||||
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`。
|
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8002/xiaozhi/ota/`。
|
||||||
|
|
||||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||||
|
|
||||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
|
|
||||||
以下是一些常见问题,供参考:
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.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)
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.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 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
[3、TTS 经常失败,经常超时](./FAQ.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 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
[4、如何提高小智对话响应速度?](./FAQ.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、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
[5、我说话很慢,停顿时小智老是抢话](./FAQ.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、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.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、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
# 部署架构图
|
||||||
|

|
||||||
|
# 方式一:Docker运行全模块
|
||||||
|
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||||
|
|
||||||
|
## 1. 安装docker
|
||||||
|
|
||||||
|
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
||||||
|
|
||||||
|
#### 1.1 创建目录
|
||||||
|
|
||||||
|
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
||||||
|
|
||||||
|
创建好目录后,你需要在`xiaozhi-server`下面创建`data`文件夹和`models`文件夹,`models`下面还要再创建`SenseVoiceSmall`文件夹。
|
||||||
|
|
||||||
|
最终目录结构如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
xiaozhi-server
|
||||||
|
├─ data
|
||||||
|
├─ models
|
||||||
|
├─ SenseVoiceSmall
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 下载语音识别模型文件
|
||||||
|
|
||||||
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
|
文件放在`models/SenseVoiceSmall`
|
||||||
|
目录下。下面两个下载路线任选一个。
|
||||||
|
|
||||||
|
- 线路一:阿里魔塔下载[SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
|
||||||
|
- 线路二:百度网盘下载[SenseVoiceSmall](https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg&pwd=qvna) 提取码:
|
||||||
|
`qvna`
|
||||||
|
|
||||||
|
|
||||||
|
#### 1.3 下载配置文件
|
||||||
|
|
||||||
|
你需要下载两个配置文件:`docker-compose_all.yaml` 和 `config_from_api.yaml`。需要从项目仓库下载这两个文件。
|
||||||
|
|
||||||
|
##### 1.3.1 下载 docker-compose_all.yaml
|
||||||
|
|
||||||
|
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose_all.yml)。
|
||||||
|
|
||||||
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
|
||||||
|
`xiaozhi-server`中。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/docker-compose_all.yml` 下载。
|
||||||
|
|
||||||
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
|
##### 1.3.2 下载 config_from_api.yaml
|
||||||
|
|
||||||
|
用浏览器打开[这个链接](../main/xiaozhi-server/config_from_api.yaml)。
|
||||||
|
|
||||||
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
|
||||||
|
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/config_from_api.yaml` 下载保存。
|
||||||
|
|
||||||
|
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
||||||
|
|
||||||
|
```
|
||||||
|
xiaozhi-server
|
||||||
|
├─ docker-compose_all.yml
|
||||||
|
├─ data
|
||||||
|
├─ .config.yaml
|
||||||
|
├─ models
|
||||||
|
├─ SenseVoiceSmall
|
||||||
|
├─ model.pt
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
|
## 2. 备份数据
|
||||||
|
|
||||||
|
如果你之前已经成功运行智控台,如果上面保存有你的密钥信息,请先从智控台上拷贝重要数据下来。因为升级过程中,有可能会覆盖原来的数据。
|
||||||
|
|
||||||
|
## 3. 清除历史版本镜像和容器
|
||||||
|
接下来打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose -f docker-compose_all.yml down
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server
|
||||||
|
docker rm xiaozhi-esp32-server
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-db
|
||||||
|
docker rm xiaozhi-esp32-server-db
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-redis
|
||||||
|
docker rm xiaozhi-esp32-server-redis
|
||||||
|
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 运行程序
|
||||||
|
执行以下命令启动新版本容器
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose -f docker-compose_all.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
执行完后,再执行以下命令,查看日志信息。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker logs -f xiaozhi-esp32-server-web
|
||||||
|
```
|
||||||
|
|
||||||
|
当你看到输出日志时,说明你的`智控台`启动成功了。
|
||||||
|
|
||||||
|
```
|
||||||
|
2025-xx-xx 22:11:12.445 [main] INFO c.a.d.s.b.a.DruidDataSourceAutoConfigure - Init DruidDataSource
|
||||||
|
2025-xx-xx 21:28:53.873 [main] INFO xiaozhi.AdminApplication - Started AdminApplication in 16.057 seconds (process running for 17.941)
|
||||||
|
http://localhost:8002/xiaozhi/doc.html
|
||||||
|
```
|
||||||
|
|
||||||
|
请注意此刻仅是`智控台`能运行,如果8000端口`xiaozhi-esp32-server`报错,先不要理会。
|
||||||
|
|
||||||
|
这时,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8002 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
||||||
|
|
||||||
|
接下来要做三件重要的事情:
|
||||||
|
|
||||||
|
### 第一件重要的事情
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到列表中第一条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||||
|
|
||||||
|
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||||
|
|
||||||
|
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
||||||
|
|
||||||
|
```
|
||||||
|
manager-api:
|
||||||
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
|
secret: 你的server.secret值
|
||||||
|
```
|
||||||
|
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||||
|
|
||||||
|
2、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
|
3、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
|
4、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
|
类似这样的效果
|
||||||
|
```
|
||||||
|
manager-api:
|
||||||
|
url: http://xiaozhi-esp32-server-web:8002/xiaozhi
|
||||||
|
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||||
|
```
|
||||||
|
|
||||||
|
保存好后,继续往下做第二件重要的事情
|
||||||
|
|
||||||
|
### 第二件重要的事情
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
|
## 5.重启xiaozhi-esp32-server
|
||||||
|
|
||||||
|
接下来打开命令行工具,使用`终端`或`命令行`工具 输入
|
||||||
|
```
|
||||||
|
docker restart xiaozhi-esp32-server
|
||||||
|
docker logs -f xiaozhi-esp32-server
|
||||||
|
```
|
||||||
|
如果你能看到,类似以下日志,则是Server启动成功的标志。
|
||||||
|
|
||||||
|
```
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - Websocket地址是 ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
由于你是全模块部署,因此你有两个重要的接口需要写入到esp32中。
|
||||||
|
|
||||||
|
OTA接口:
|
||||||
|
```
|
||||||
|
http://你电脑局域网的ip:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
Websocket接口:
|
||||||
|
```
|
||||||
|
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第三件重要的事情
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`。
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`。
|
||||||
|
|
||||||
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
|
|
||||||
|
# 方式二:本地源码运行全模块
|
||||||
|
|
||||||
|
## 1.安装MySQL数据库
|
||||||
|
|
||||||
|
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
如果还没有MySQL,你可以通过docker安装mysql
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.安装redis
|
||||||
|
|
||||||
|
如果还没有Redis,你可以通过docker安装redis
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.运行manager-api程序
|
||||||
|
|
||||||
|
3.1 安装JDK21,设置JDK环境变量
|
||||||
|
|
||||||
|
3.2 安装Maven,设置Maven环境变量
|
||||||
|
|
||||||
|
3.3 使用Vscode编程工具,安装好Java环境相关插件
|
||||||
|
|
||||||
|
3.4 使用Vscode编程工具加载manager-api模块
|
||||||
|
|
||||||
|
在`src/main/resources/application-dev.yml`中配置数据库连接信息
|
||||||
|
|
||||||
|
```
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
username: root
|
||||||
|
password: 123456
|
||||||
|
```
|
||||||
|
在`src/main/resources/application-dev.yml`中配置Redis连接信息
|
||||||
|
```
|
||||||
|
spring:
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
password:
|
||||||
|
database: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
3.5 运行主程序
|
||||||
|
|
||||||
|
本项目为SpringBoot项目,启动方式为:
|
||||||
|
打开`Application.java`运行`Main`方法启动
|
||||||
|
|
||||||
|
```
|
||||||
|
路径地址:
|
||||||
|
src/main/java/xiaozhi/AdminApplication.java
|
||||||
|
```
|
||||||
|
|
||||||
|
当你看到输出日志时,说明你的`manager-api`启动成功了。
|
||||||
|
|
||||||
|
```
|
||||||
|
2025-xx-xx 22:11:12.445 [main] INFO c.a.d.s.b.a.DruidDataSourceAutoConfigure - Init DruidDataSource
|
||||||
|
2025-xx-xx 21:28:53.873 [main] INFO xiaozhi.AdminApplication - Started AdminApplication in 16.057 seconds (process running for 17.941)
|
||||||
|
http://localhost:8002/xiaozhi/doc.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4.运行manager-web程序
|
||||||
|
|
||||||
|
4.1 安装nodejs
|
||||||
|
|
||||||
|
4.2 使用Vscode编程工具加载manager-web模块
|
||||||
|
|
||||||
|
终端命令进入manager-web目录下
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
然后启动
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
请注意,如果你的manager-api的接口不在`http://localhost:8002`,请在开发时,修改
|
||||||
|
`main/manager-web/.env.development`中的路径
|
||||||
|
|
||||||
|
运行成功后,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8001 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
||||||
|
|
||||||
|
|
||||||
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
|
## 5.安装Python环境
|
||||||
|
|
||||||
|
本项目使用`conda`管理依赖环境。如果不方便安装`conda`,需要根据实际的操作系统安装好`libopus`和`ffmpeg`。
|
||||||
|
如果确定使用`conda`,则安装好后,开始执行以下命令。
|
||||||
|
|
||||||
|
重要提示!windows 用户,可以通过安装`Anaconda`来管理环境。安装好`Anaconda`后,在`开始`那里搜索`anaconda`相关的关键词,
|
||||||
|
找到`Anaconda Prpmpt`,使用管理员身份运行它。如下图。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
运行之后,如果你能看到命令行窗口前面有一个(base)字样,说明你成功进入了`conda`环境。那么你就可以执行以下命令了。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
conda remove -n xiaozhi-esp32-server --all -y
|
||||||
|
conda create -n xiaozhi-esp32-server python=3.10 -y
|
||||||
|
conda activate xiaozhi-esp32-server
|
||||||
|
|
||||||
|
# 添加清华源通道
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free
|
||||||
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge
|
||||||
|
|
||||||
|
conda install libopus -y
|
||||||
|
conda install ffmpeg -y
|
||||||
|
```
|
||||||
|
|
||||||
|
请注意,以上命令,不是一股脑执行就成功的,你需要一步步执行,每一步执行完后,都检查一下输出的日志,查看是否成功。
|
||||||
|
|
||||||
|
## 6.安装本项目依赖
|
||||||
|
|
||||||
|
你先要下载本项目源码,源码可以通过`git clone`命令下载,如果你不熟悉`git clone`命令。
|
||||||
|
|
||||||
|
你可以用浏览器打开这个地址`https://github.com/xinnan-tech/xiaozhi-esp32-server.git`
|
||||||
|
|
||||||
|
打开完,找到页面中一个绿色的按钮,写着`Code`的按钮,点开它,然后你就看到`Download ZIP`的按钮。
|
||||||
|
|
||||||
|
点击它,下载本项目源码压缩包。下载到你电脑后,解压它,此时它的名字可能叫`xiaozhi-esp32-server-main`
|
||||||
|
你需要把它重命名成`xiaozhi-esp32-server`,在这个文件里,进入到`main`文件夹,再进入到`xiaozhi-server`,好了请记住这个目录`xiaozhi-server`。
|
||||||
|
|
||||||
|
```
|
||||||
|
# 继续使用conda环境
|
||||||
|
conda activate xiaozhi-esp32-server
|
||||||
|
# 进入到你的项目根目录,再进入main/xiaozhi-server
|
||||||
|
cd main/xiaozhi-server
|
||||||
|
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.下载语音识别模型文件
|
||||||
|
|
||||||
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
|
文件放在`models/SenseVoiceSmall`
|
||||||
|
目录下。下面两个下载路线任选一个。
|
||||||
|
|
||||||
|
- 线路一:阿里魔塔下载[SenseVoiceSmall](https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt)
|
||||||
|
- 线路二:百度网盘下载[SenseVoiceSmall](https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg&pwd=qvna) 提取码:
|
||||||
|
`qvna`
|
||||||
|
|
||||||
|
## 8.配置项目文件
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台 ,在顶部菜单找到`参数管理`,找到列表中第一条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||||
|
|
||||||
|
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||||
|
|
||||||
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
|
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config_from_api.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||||
|
|
||||||
|
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
||||||
|
|
||||||
|
```
|
||||||
|
manager-api:
|
||||||
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
|
secret: 你的server.secret值
|
||||||
|
```
|
||||||
|
|
||||||
|
把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||||
|
|
||||||
|
类似这样的效果
|
||||||
|
```
|
||||||
|
manager-api:
|
||||||
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
|
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5.运行项目
|
||||||
|
|
||||||
|
```
|
||||||
|
# 确保在xiaozhi-server目录下执行
|
||||||
|
conda activate xiaozhi-esp32-server
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
||||||
|
|
||||||
|
```
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||||
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
由于你是全模块部署,因此你有两个重要的接口。
|
||||||
|
|
||||||
|
OTA接口:
|
||||||
|
```
|
||||||
|
http://你电脑局域网的ip:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
Websocket接口:
|
||||||
|
```
|
||||||
|
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
请你务必把以上两个接口地址写入到智控台中:他们将会影响websocket地址发放和自动升级功能。
|
||||||
|
|
||||||
|
1、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`。
|
||||||
|
|
||||||
|
2、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`。
|
||||||
|
|
||||||
|
|
||||||
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
|
# 常见问题
|
||||||
|
|
||||||
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
|
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
|
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
|
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
|
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
|
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
@@ -102,48 +102,10 @@ VAD:
|
|||||||
|
|
||||||
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||||
|
|
||||||
本项目,支持以工具调用的方式控制HomeAssistant设备
|
参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md)
|
||||||
|
|
||||||
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`。
|
|
||||||
|
|
||||||
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`。
|
|
||||||
|
|
||||||
3、登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
|
||||||
|
|
||||||
在配置文件中,配置好你的home assistant的`devices`(被控制的设备)和`api_key`和`base_url`等信息。例如:
|
|
||||||
|
|
||||||
``` yaml
|
|
||||||
plugins
|
|
||||||
home_assistant:
|
|
||||||
devices:
|
|
||||||
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
|
|
||||||
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
|
|
||||||
base_url: http://你的homeassistant地址:8123
|
|
||||||
api_key: 你的home assistant api访问令牌
|
|
||||||
```
|
|
||||||
|
|
||||||
最后,允许function_call 插件在配置文件中启用`hass_get_state`(必须)、`hass_set_state`(必须)、`hass_play_music`(不想用ha听音乐可以不启动),例如:
|
|
||||||
|
|
||||||
``` yaml
|
|
||||||
Intent:
|
|
||||||
...
|
|
||||||
function_call:
|
|
||||||
type: nointent
|
|
||||||
functions:
|
|
||||||
- change_role
|
|
||||||
- get_weather
|
|
||||||
- get_news
|
|
||||||
- hass_get_state
|
|
||||||
- hass_set_state
|
|
||||||
- hass_play_music
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8、更多问题,可联系我们反馈 💬
|
### 8、更多问题,可联系我们反馈 💬
|
||||||
|
|
||||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
|
||||||
|
|
||||||
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
|
也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
|
||||||
|
|
||||||
- 硬件烧录QQ群:适用于硬件烧录问题
|
|
||||||
- 开源服务端交流群:适用于服务端问题
|
|
||||||
- 产品建议联系人:适用于产品功能、产品设计等建议
|
|
||||||
@@ -1,26 +1,53 @@
|
|||||||
server {
|
user root;
|
||||||
listen 8002;
|
worker_processes 4;
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
# 静态资源服务(Vue项目)
|
events {
|
||||||
location / {
|
worker_connections 1024;
|
||||||
root /usr/share/nginx/html;
|
}
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API反向代理(Java项目)
|
http {
|
||||||
location /xiaozhi/ {
|
include mime.types;
|
||||||
proxy_pass http://127.0.0.1:8003;
|
default_type application/octet-stream;
|
||||||
proxy_set_header Host $host;
|
sendfile on;
|
||||||
proxy_cookie_path /api/ /;
|
keepalive_timeout 300;
|
||||||
proxy_set_header Referer $http_referer;
|
client_header_timeout 180s;
|
||||||
proxy_set_header Cookie $http_cookie;
|
client_body_timeout 180s;
|
||||||
|
client_max_body_size 1024M;
|
||||||
|
|
||||||
proxy_connect_timeout 10;
|
gzip on;
|
||||||
proxy_send_timeout 10;
|
gzip_buffers 32 4K;
|
||||||
proxy_read_timeout 10;
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 100;
|
||||||
|
gzip_types application/javascript text/css text/xml image/jpeg image/gif image/png;
|
||||||
|
gzip_disable "MSIE [1-6]\.";
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
server {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
# 无域名访问,就用localhost
|
||||||
|
server_name localhost;
|
||||||
|
# 80端口
|
||||||
|
listen 8002;
|
||||||
|
|
||||||
|
# 转发到编译后到web目录
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 转发到manager-api
|
||||||
|
location /xiaozhi/ {
|
||||||
|
proxy_pass http://127.0.0.1:8003;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cookie_path /manager/ /;
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,81 @@
|
|||||||
# 编译esp32固件
|
# esp32固件编译
|
||||||
|
|
||||||
1. 下载`xiaozhi-esp32`
|
## 第1步 准备你的ota地址
|
||||||
项目,按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
|
||||||
|
|
||||||
2. 打开`xiaozhi-esp32/main/Kconfig.projbuild`文件,找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net`
|
如果你,使用的是本项目0.3.12版本,不管是简单Server部署还是全模块部署,都会有ota地址。
|
||||||
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
|
|
||||||
|
由于简单Server部署和全模块部署的OTA地址设置方式不一样,请你选择下面的具体方式:
|
||||||
|
|
||||||
|
### 如果你用的是简单Server部署
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
http://192.168.1.25:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
|
||||||
|
|
||||||
|
你可以使用项目自带的`test_page.html`测试一下,是否能连上ota页面输出的websocket地址。
|
||||||
|
|
||||||
|
如果访问不到,你需要到配置文件`.config.yaml`里修改`server.websocket`的地址,重启后再重新测试,直到`test_page.html`能正常访问。
|
||||||
|
|
||||||
|
成功后,请往下进行第2步
|
||||||
|
|
||||||
|
### 如果你用的是全模块部署
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
http://192.168.1.25:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下进行2步。
|
||||||
|
|
||||||
|
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||||
|
|
||||||
|
- 1、使用超级管理员登录智控台
|
||||||
|
|
||||||
|
- 2、顶部菜单点击`参数管理`
|
||||||
|
|
||||||
|
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://192.168.1.25:8000/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
|
||||||
|
|
||||||
|
## 第2步 配置环境
|
||||||
|
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||||
|
|
||||||
|
## 第3步 打开配置文件
|
||||||
|
配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
|
||||||
|
|
||||||
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32)。
|
||||||
|
|
||||||
|
下载后,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
||||||
|
|
||||||
|
## 第4步 修改OTA地址
|
||||||
|
|
||||||
|
找到`OTA_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
||||||
|
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
|
||||||
|
|
||||||
修改前:
|
修改前:
|
||||||
|
|
||||||
```
|
```
|
||||||
config WEBSOCKET_URL
|
config OTA_URL
|
||||||
depends on CONNECTION_TYPE_WEBSOCKET
|
string "Default OTA URL"
|
||||||
string "Websocket URL"
|
default "https://api.tenclass.net/xiaozhi/ota/"
|
||||||
default "wss://api.tenclass.net/xiaozhi/v1/"
|
|
||||||
help
|
help
|
||||||
Communication with the server through websocket after wake up.
|
The application will access this URL to check for new firmwares and server address.
|
||||||
```
|
```
|
||||||
|
修改后:
|
||||||
修改后(示例):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
config WEBSOCKET_URL
|
config OTA_URL
|
||||||
depends on CONNECTION_TYPE_WEBSOCKET
|
string "Default OTA URL"
|
||||||
string "Websocket URL"
|
default "http://192.168.1.25:8002/xiaozhi/ota/"
|
||||||
default "ws://192.168.1.25:8000/xiaozhi/v1/"
|
|
||||||
help
|
help
|
||||||
Communication with the server through websocket after wake up.
|
The application will access this URL to check for new firmwares and server address.
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
## 第4步 设置编译参数
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
设置编译参数
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
|
||||||
|
|
||||||
3. 设置编译参数
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# 终端命令行进入xiaozhi-esp32的根目录
|
# 终端命令行进入xiaozhi-esp32的根目录
|
||||||
@@ -45,40 +86,29 @@ idf.py set-target esp32s3
|
|||||||
idf.py menuconfig
|
idf.py menuconfig
|
||||||
```
|
```
|
||||||
|
|
||||||

|
进入菜单配置后,再进入`Xiaozhi Assistant`,将`BOARD_TYPE`设置你板子的具体型号
|
||||||
|
|
||||||
进入菜单配置后,再进入`Xiaozhi Assistant`,将`CONNECTION_TYPE`设置为`Websocket`
|
|
||||||
回退到主菜单,再进入`Xiaozhi Assistant`,将`BOARD_TYPE`设置你板子的具体型号
|
|
||||||
保存退出,回到终端命令行。
|
保存退出,回到终端命令行。
|
||||||
|
|
||||||

|
## 第5步 编译固件
|
||||||
|
|
||||||
4. 编译固件
|
|
||||||
|
|
||||||
```
|
```
|
||||||
idf.py build
|
idf.py build
|
||||||
```
|
```
|
||||||
|
|
||||||
如果是vscode安装的idf可以使用`F1`或者`ctrl+shift+p`,输入idf然后直接选择进行编译
|
## 第6步 打包bin固件
|
||||||
|
|
||||||
还可以直接进行烧录不用接下来的操作
|
|
||||||
|
|
||||||
<img src="./images/vscode_idf.png" width="500px"/>
|
|
||||||
|
|
||||||
5. 打包bin固件
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd scripts
|
cd scripts
|
||||||
python release.py
|
python release.py
|
||||||
```
|
```
|
||||||
|
|
||||||
编译成功后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
上面的打包命令执行完成后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
||||||
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
|
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
|
||||||
|
|
||||||
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
|
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
|
||||||
,对你没有太大影响,请继续。
|
,对你没有太大影响,请继续。
|
||||||
|
|
||||||
6. 烧录固件
|
## 第7步 烧录固件
|
||||||
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
|
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -90,16 +120,19 @@ https://espressif.github.io/esp-launchpad/
|
|||||||
|
|
||||||
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
以下是一些常见问题,供参考:
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.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、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.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 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
[3、TTS 经常失败,经常超时](./FAQ.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 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
[4、如何提高小智对话响应速度?](./FAQ.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、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
[5、我说话很慢,停顿时小智老是抢话](./FAQ.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、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.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、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 基于虾哥编译好的固件配置自定义服务器
|
||||||
|
|
||||||
|
## 第1步 确认版本
|
||||||
|
烧录虾哥已经编译好的1.6.1版本固件
|
||||||
|
|
||||||
|
## 第2步 准备你的ota地址
|
||||||
|
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
||||||
|
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下。
|
||||||
|
|
||||||
|
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||||
|
|
||||||
|
- 1、使用超级管理员登录智控台
|
||||||
|
|
||||||
|
- 2、顶部菜单点击`参数管理`
|
||||||
|
|
||||||
|
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
|
||||||
|
|
||||||
|
```
|
||||||
|
wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
|
||||||
|
|
||||||
|
## 第3步 进入配网模式
|
||||||
|
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
|
||||||
|
|
||||||
|
## 第4步 唤醒小智,查看日志输出
|
||||||
|
|
||||||
|
唤醒小智,看看日志是不是正常输出。
|
||||||
|
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
|
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
|
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
|
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
|
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
|
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# ESP32设备与HomeAssistant集成指南
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
本文档将指导您如何将ESP32设备与HomeAssistant进行集成。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
- 已安装并配置好`HomeAssistant`
|
||||||
|
- 已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`
|
||||||
|
- `xiaozhi-esp32-server智控台`能正常使用
|
||||||
|
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
|
||||||
|
|
||||||
|
## 网络环境
|
||||||
|
|
||||||
|
1、我的`HomeAssistant`部署在下面,版本是`2025.3.4`
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8123
|
||||||
|
```
|
||||||
|
|
||||||
|
2、我的`xiaozhi-esp32-server智控台`和`HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8002
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 配置步骤
|
||||||
|
|
||||||
|
### 1. 登录`HomeAssistant`整理需要控制的设备清单
|
||||||
|
|
||||||
|
登录`HomeAssistant`,点击`左下角的设置`,然后进入`设备与服务`,再点击顶部的`实体`。
|
||||||
|
|
||||||
|
然后在实体中搜索你相关控制的开关,结果出来后,在列表中,点击其中一个结果,这是会出现一个开关的界面。
|
||||||
|
|
||||||
|
在开关的界面,我们尝试点击开关,看看是开发会随着我们的点击开/关。如果能操作,说明是正常联网的。
|
||||||
|
|
||||||
|
接着在开关面板找到设置按钮,点击后,可以查看这个开关的`实体标识符`。
|
||||||
|
|
||||||
|
我们打开一个记事本,按照这样格式整理一条数据:
|
||||||
|
|
||||||
|
位置+英文逗号+设备名称+英文逗号+`实体标识符`+英文分号
|
||||||
|
|
||||||
|
例如,我在公司,我有一个玩具灯,他的标识符是switch.cuco_cn_460494544_cp1_on_p_2_1,那么就这个写这一条数据
|
||||||
|
|
||||||
|
```
|
||||||
|
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
|
||||||
|
```
|
||||||
|
|
||||||
|
当然最后我可能要操作两个灯,我的最终的结果是:
|
||||||
|
|
||||||
|
```
|
||||||
|
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
|
||||||
|
公司,台灯,switch.iot_cn_831898993_socn1_on_p_2_1;
|
||||||
|
```
|
||||||
|
|
||||||
|
这段字符,我们成为“设备清单字符”需要保存好,等一下有用。
|
||||||
|
|
||||||
|
|
||||||
|
### 2. 登录`HomeAssistant`拿到开发密钥
|
||||||
|
|
||||||
|
登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
||||||
|
|
||||||
|
|
||||||
|
### 3. 登录`智控台`
|
||||||
|
|
||||||
|
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.devices`,把刚才整理的设备清单字符粘贴进去。
|
||||||
|
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.base_url`,把你部署的`HomeAssistant`接口地址粘贴进去,我粘贴进去的地址是这样的
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8123
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去
|
||||||
|
|
||||||
|
|
||||||
|
### 4. 设置`意图识别`函数
|
||||||
|
|
||||||
|
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
|
||||||
|
|
||||||
|
然后在弹框中,在原来的基础上追加两个函数:“hass_get_state”和“hass_set_state”
|
||||||
|
|
||||||
|
修改前
|
||||||
|
```
|
||||||
|
change_role;get_weather;get_news;play_music
|
||||||
|
```
|
||||||
|
|
||||||
|
修改后
|
||||||
|
|
||||||
|
```
|
||||||
|
change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 手动重启xiaozhi-server
|
||||||
|
|
||||||
|
重启xiaozhi-server程序
|
||||||
|
|
||||||
|
|
||||||
|
### 5. 确认角色配置是否设置了函数意图识别
|
||||||
|
|
||||||
|
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
|
||||||
|
|
||||||
|
确认意图识别(Intent),是否选择“函数调用意图识别”
|
||||||
|
|
||||||
|
|
||||||
|
### 6. 唤醒设别进行控制
|
||||||
|
|
||||||
|
尝试和esp32说,“打开XXX灯”
|
||||||
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 576 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 644 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 149 KiB |
@@ -1,8 +1,6 @@
|
|||||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](./FAQ.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3)
|
||||||
|
|
||||||
# 项目目录介绍
|
# 项目目录介绍
|
||||||
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
|
|
||||||
|
|
||||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](../docs/contributor_open_letter.md),欢迎归队!
|
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](../docs/contributor_open_letter.md),欢迎归队!
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -12,9 +10,9 @@ xiaozhi-esp32-server
|
|||||||
├─ manager-api 8002 端口 Java语言开发 负责提供控制台的api
|
├─ manager-api 8002 端口 Java语言开发 负责提供控制台的api
|
||||||
```
|
```
|
||||||
|
|
||||||
# xiaozhi-server 接口协议
|
# xiaozhi-server 和ESP32通讯协议
|
||||||
|
|
||||||
[虾哥团队通信协议:Websocket 连接](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh)
|
https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh
|
||||||
|
|
||||||
# manager-web 、manager-api接口协议
|
# manager-web 、manager-api接口协议
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
|
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3)
|
||||||
|
|
||||||
# 项目介绍
|
# 项目介绍
|
||||||
|
|
||||||
manager-api 该项目基于SpringBoot框架开发。
|
manager-api 该项目基于SpringBoot框架开发。
|
||||||
@@ -12,85 +13,6 @@ MySQL 8.0+
|
|||||||
Redis 5.0+
|
Redis 5.0+
|
||||||
Vue 3.x
|
Vue 3.x
|
||||||
|
|
||||||
# 创建数据库
|
|
||||||
|
|
||||||
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
```
|
|
||||||
|
|
||||||
如果还没有MySQL,你可以通过docker安装mysql
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --name xiaozhi-esp32-server-db \
|
|
||||||
-e MYSQL_ROOT_PASSWORD=123456 \
|
|
||||||
-p 3306:3306 \
|
|
||||||
-e MYSQL_DATABASE=xiaozhi_esp32_server \
|
|
||||||
-e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" \
|
|
||||||
-d mysql:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
# 确认项目数据库连接信息
|
|
||||||
|
|
||||||
在`src/main/resources/application-dev.yml`中配置数据库连接信息
|
|
||||||
|
|
||||||
```
|
|
||||||
spring:
|
|
||||||
datasource:
|
|
||||||
username: root
|
|
||||||
password: 123456
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# 连接Redis
|
|
||||||
|
|
||||||
如果还没有Redis,你可以通过docker安装redis
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
|
|
||||||
```
|
|
||||||
|
|
||||||
# 确认项目Redis连接信息
|
|
||||||
|
|
||||||
在`src/main/resources/application-dev.yml`中配置Redis连接信息
|
|
||||||
|
|
||||||
```
|
|
||||||
spring:
|
|
||||||
data:
|
|
||||||
redis:
|
|
||||||
host: localhost
|
|
||||||
port: 6379
|
|
||||||
password:
|
|
||||||
database: 0
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# 测试启动
|
|
||||||
|
|
||||||
本项目为SpringBoot项目,启动方式为:
|
|
||||||
打开`Application.java`运行`Main`方法启动
|
|
||||||
|
|
||||||
```
|
|
||||||
路径地址:
|
|
||||||
src/main/java/xiaozhi/AdminApplication.java
|
|
||||||
```
|
|
||||||
|
|
||||||
# 打包编译
|
|
||||||
|
|
||||||
执行以下命令生产jar包
|
|
||||||
|
|
||||||
```
|
|
||||||
mvn clean install
|
|
||||||
```
|
|
||||||
|
|
||||||
把jar包放在服务器上,执行
|
|
||||||
|
|
||||||
```
|
|
||||||
nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.activate=dev
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# 接口文档
|
# 接口文档
|
||||||
启动后打开:http://localhost:8002/xiaozhi/doc.html
|
启动后打开:http://localhost:8002/xiaozhi/doc.html
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package xiaozhi.common.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RestTemplate配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ public class SwaggerConfig {
|
|||||||
public GroupedOpenApi timbreApi() {
|
public GroupedOpenApi timbreApi() {
|
||||||
return GroupedOpenApi.builder()
|
return GroupedOpenApi.builder()
|
||||||
.group("timbre")
|
.group("timbre")
|
||||||
.pathsToMatch("/timbre/**")
|
.pathsToMatch("/ttsVoice/**")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +71,14 @@ public class SwaggerConfig {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi configApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("config")
|
||||||
|
.pathsToMatch("/config/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OpenAPI customOpenAPI() {
|
public OpenAPI customOpenAPI() {
|
||||||
return new OpenAPI().info(new Info()
|
return new OpenAPI().info(new Info()
|
||||||
|
|||||||
@@ -74,8 +74,36 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String ORDER = "order";
|
String ORDER = "order";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求头授权标识
|
||||||
|
*/
|
||||||
String AUTHORIZATION = "Authorization";
|
String AUTHORIZATION = "Authorization";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器密钥
|
||||||
|
*/
|
||||||
|
String SERVER_SECRET = "server.secret";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket地址
|
||||||
|
*/
|
||||||
|
String SERVER_WEBSOCKET = "server.websocket";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ota地址
|
||||||
|
*/
|
||||||
|
String SERVER_OTA = "server.ota";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许用户注册
|
||||||
|
*/
|
||||||
|
String SERVER_ALLOW_USER_REGISTER = "server.allow_user_register";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发六位验证码时显示的控制面板地址
|
||||||
|
*/
|
||||||
|
String SERVER_FRONTED_URL = "server.fronted_url";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径分割符
|
* 路径分割符
|
||||||
*/
|
*/
|
||||||
@@ -145,4 +173,9 @@ public interface Constant {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号
|
||||||
|
*/
|
||||||
|
public static final String VERSION = "0.3.13";
|
||||||
}
|
}
|
||||||
@@ -43,4 +43,16 @@ public interface ErrorCode {
|
|||||||
int PASSWORD_WEAK_ERROR = 10031;
|
int PASSWORD_WEAK_ERROR = 10031;
|
||||||
int DEL_MYSELF_ERROR = 10032;
|
int DEL_MYSELF_ERROR = 10032;
|
||||||
int DEVICE_CAPTCHA_ERROR = 10033;
|
int DEVICE_CAPTCHA_ERROR = 10033;
|
||||||
|
|
||||||
|
// 参数校验相关错误码
|
||||||
|
int PARAM_VALUE_NULL = 10034;
|
||||||
|
int PARAM_TYPE_NULL = 10035;
|
||||||
|
int PARAM_TYPE_INVALID = 10036;
|
||||||
|
int PARAM_NUMBER_INVALID = 10037;
|
||||||
|
int PARAM_BOOLEAN_INVALID = 10038;
|
||||||
|
int PARAM_ARRAY_INVALID = 10039;
|
||||||
|
int PARAM_JSON_INVALID = 10040;
|
||||||
|
|
||||||
|
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||||
|
int OTA_DEVICE_NEED_BIND = 10042;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.apache.shiro.authz.UnauthorizedException;
|
|||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -53,4 +54,10 @@ public class RenExceptionHandler {
|
|||||||
return new Result<Void>().error();
|
return new Result<Void>().error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(NoResourceFoundException.class)
|
||||||
|
public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) {
|
||||||
|
log.warn("Resource not found: {}", ex.getMessage());
|
||||||
|
return new Result<Void>().error(404, "资源不存在");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -34,5 +34,73 @@ public class RedisKeys {
|
|||||||
return "sys:username:id:" + userid;
|
return "sys:username:id:" + userid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型名称的Key
|
||||||
|
*/
|
||||||
|
public static String getModelNameById(String id) {
|
||||||
|
return "model:name:" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型配置的Key
|
||||||
|
*/
|
||||||
|
public static String getModelConfigById(String id) {
|
||||||
|
return "model:data:" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音色名称缓存key
|
||||||
|
*/
|
||||||
|
public static String getTimbreNameById(String id) {
|
||||||
|
return "timbre:name:" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备数量缓存key
|
||||||
|
*/
|
||||||
|
public static String getAgentDeviceCountById(String id) {
|
||||||
|
return "agent:device:count:" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统配置缓存key
|
||||||
|
*/
|
||||||
|
public static String getServerConfigKey() {
|
||||||
|
return "server:config";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音色详情缓存key
|
||||||
|
*/
|
||||||
|
public static String getTimbreDetailsKey(String id) {
|
||||||
|
return "timbre:details:" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取版本号Key
|
||||||
|
*/
|
||||||
|
public static String getVersionKey() {
|
||||||
|
return "sys:version";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件ID的Key
|
||||||
|
*/
|
||||||
|
public static String getOtaIdKey(String uuid) {
|
||||||
|
return "ota:id:" + uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件下载次数的Key
|
||||||
|
*/
|
||||||
|
public static String getOtaDownloadCountKey(String uuid) {
|
||||||
|
return "ota:download:count:" + uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典数据的缓存key
|
||||||
|
*/
|
||||||
|
public static String getDictDataByTypeKey(String dictType) {
|
||||||
|
return "sys:dict:data:" + dictType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package xiaozhi.common.redis;
|
package xiaozhi.common.redis;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.springframework.data.redis.core.HashOperations;
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -27,7 +30,7 @@ public class RedisUtils {
|
|||||||
/**
|
/**
|
||||||
* 过期时长为1小时,单位:秒
|
* 过期时长为1小时,单位:秒
|
||||||
*/
|
*/
|
||||||
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
|
public final static long HOUR_ONE_EXPIRE = (long) 60 * 60;
|
||||||
/**
|
/**
|
||||||
* 过期时长为6小时,单位:秒
|
* 过期时长为6小时,单位:秒
|
||||||
*/
|
*/
|
||||||
@@ -124,4 +127,24 @@ public class RedisUtils {
|
|||||||
public Object rightPop(String key) {
|
public Object rightPop(String key) {
|
||||||
return redisTemplate.opsForList().rightPop(key);
|
return redisTemplate.opsForList().rightPop(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有 Redis 数据库中的所有键
|
||||||
|
*/
|
||||||
|
public void emptyAll() {
|
||||||
|
// Lua 脚本 FLUSHALL是redis清空所有库的命令
|
||||||
|
String luaScript ="redis.call('FLUSHALL')";
|
||||||
|
|
||||||
|
// 创建 DefaultRedisScript 对象
|
||||||
|
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
|
||||||
|
redisScript.setScriptText(luaScript); // 设置 Lua 脚本内容
|
||||||
|
redisScript.setResultType(Void.class); // 设置返回值类型
|
||||||
|
|
||||||
|
// 执行 Lua 脚本
|
||||||
|
List<String> keys = Collections.emptyList(); // 如果脚本不依赖 key,可以传入空列表
|
||||||
|
redisTemplate.execute(redisScript, keys);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -128,12 +128,10 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
|||||||
return SqlHelper.retBool(result);
|
return SqlHelper.retBool(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected Class<M> currentMapperClass() {
|
protected Class<M> currentMapperClass() {
|
||||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
@Override
|
||||||
public Class<T> currentModelClass() {
|
public Class<T> currentModelClass() {
|
||||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils;
|
|||||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
||||||
implements CrudService<T, D> {
|
implements CrudService<T, D> {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected Class<D> currentDtoClass() {
|
protected Class<D> currentDtoClass() {
|
||||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package xiaozhi.modules.agent.controller;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||||
|
|
||||||
|
@Tag(name = "智能体聊天历史管理")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/agent/chat-history")
|
||||||
|
public class AgentChatHistoryController {
|
||||||
|
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小智服务聊天上报请求
|
||||||
|
* <p>
|
||||||
|
* 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。
|
||||||
|
*
|
||||||
|
* @param request 包含上传文件及相关信息的请求对象
|
||||||
|
*/
|
||||||
|
@Operation(summary = "小智服务聊天上报请求")
|
||||||
|
@PostMapping("/report")
|
||||||
|
public Result<Boolean> uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) {
|
||||||
|
Boolean result = agentChatHistoryBizService.report(request);
|
||||||
|
return new Result<Boolean>().ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,11 +29,13 @@ import xiaozhi.common.user.UserDetail;
|
|||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
@Tag(name = "智能体管理")
|
@Tag(name = "智能体管理")
|
||||||
@@ -43,14 +45,15 @@ import xiaozhi.modules.security.user.SecurityUser;
|
|||||||
public class AgentController {
|
public class AgentController {
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final AgentTemplateService agentTemplateService;
|
private final AgentTemplateService agentTemplateService;
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@Operation(summary = "获取用户智能体列表")
|
@Operation(summary = "获取用户智能体列表")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<List<AgentEntity>> getUserAgents() {
|
public Result<List<AgentDTO>> getUserAgents() {
|
||||||
UserDetail user = SecurityUser.getUser();
|
UserDetail user = SecurityUser.getUser();
|
||||||
List<AgentEntity> agents = agentService.getUserAgents(user.getId());
|
List<AgentDTO> agents = agentService.getUserAgents(user.getId());
|
||||||
return new Result<List<AgentEntity>>().ok(agents);
|
return new Result<List<AgentDTO>>().ok(agents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/all")
|
@GetMapping("/all")
|
||||||
@@ -80,6 +83,22 @@ public class AgentController {
|
|||||||
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||||
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
||||||
|
|
||||||
|
// 获取默认模板
|
||||||
|
AgentTemplateEntity template = agentTemplateService.getDefaultTemplate();
|
||||||
|
if (template != null) {
|
||||||
|
// 设置模板中的默认值
|
||||||
|
entity.setAsrModelId(template.getAsrModelId());
|
||||||
|
entity.setVadModelId(template.getVadModelId());
|
||||||
|
entity.setLlmModelId(template.getLlmModelId());
|
||||||
|
entity.setTtsModelId(template.getTtsModelId());
|
||||||
|
entity.setTtsVoiceId(template.getTtsVoiceId());
|
||||||
|
entity.setMemModelId(template.getMemModelId());
|
||||||
|
entity.setIntentModelId(template.getIntentModelId());
|
||||||
|
entity.setSystemPrompt(template.getSystemPrompt());
|
||||||
|
entity.setLangCode(template.getLangCode());
|
||||||
|
entity.setLanguage(template.getLanguage());
|
||||||
|
}
|
||||||
|
|
||||||
// 设置用户ID和创建者信息
|
// 设置用户ID和创建者信息
|
||||||
UserDetail user = SecurityUser.getUser();
|
UserDetail user = SecurityUser.getUser();
|
||||||
entity.setUserId(user.getId());
|
entity.setUserId(user.getId());
|
||||||
@@ -157,6 +176,9 @@ public class AgentController {
|
|||||||
@Operation(summary = "删除智能体")
|
@Operation(summary = "删除智能体")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> delete(@PathVariable String id) {
|
public Result<Void> delete(@PathVariable String id) {
|
||||||
|
// 先删除关联的设备
|
||||||
|
deviceService.deleteByAgentId(id);
|
||||||
|
// 再删除智能体
|
||||||
agentService.deleteById(id);
|
agentService.deleteById(id);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
@@ -169,4 +191,5 @@ public class AgentController {
|
|||||||
.list(new QueryWrapper<AgentTemplateEntity>().orderByAsc("sort"));
|
.list(new QueryWrapper<AgentTemplateEntity>().orderByAsc("sort"));
|
||||||
return new Result<List<AgentTemplateEntity>>().ok(list);
|
return new Result<List<AgentTemplateEntity>>().ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,31 @@
|
|||||||
package xiaozhi.modules.agent.dao;
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface AgentDao extends BaseDao<AgentEntity> {
|
public interface AgentDao extends BaseDao<AgentEntity> {
|
||||||
|
/**
|
||||||
|
* 获取智能体的设备数量
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @return 设备数量
|
||||||
|
*/
|
||||||
|
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||||
|
*
|
||||||
|
* @param macAddress 设备MAC地址
|
||||||
|
* @return 默认智能体信息
|
||||||
|
*/
|
||||||
|
@Select(" SELECT a.* FROM ai_device d " +
|
||||||
|
" LEFT JOIN ai_agent a ON d.agent_id = a.id " +
|
||||||
|
" WHERE d.mac_address = #{macAddress} " +
|
||||||
|
" ORDER BY d.id DESC LIMIT 1")
|
||||||
|
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AiAgentChatAudioDao extends BaseMapper<AgentChatAudioEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小智设备聊天上报请求
|
||||||
|
*
|
||||||
|
* @author Haotian
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "小智设备聊天上报请求")
|
||||||
|
public class AgentChatHistoryReportDTO {
|
||||||
|
@Schema(description = "MAC地址", example = "00:11:22:33:44:55")
|
||||||
|
@NotBlank
|
||||||
|
private String macAddress;
|
||||||
|
@Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a")
|
||||||
|
@NotBlank
|
||||||
|
private String sessionId;
|
||||||
|
@Schema(description = "消息类型: 1-用户, 2-智能体", example = "1")
|
||||||
|
@NotNull
|
||||||
|
private Byte chatType;
|
||||||
|
@Schema(description = "聊天内容", example = "你好呀")
|
||||||
|
@NotBlank
|
||||||
|
private String content;
|
||||||
|
@Schema(description = "文件数据(opus编码)", example = "")
|
||||||
|
private String opusDataBase64;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体数据传输对象
|
||||||
|
* 用于在服务层和控制器层之间传递智能体相关的数据
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体对象")
|
||||||
|
public class AgentDTO {
|
||||||
|
@Schema(description = "智能体编码", example = "AGT_1234567890")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "智能体名称", example = "客服助手")
|
||||||
|
private String agentName;
|
||||||
|
|
||||||
|
@Schema(description = "语音合成模型名称", example = "tts_model_01")
|
||||||
|
private String ttsModelName;
|
||||||
|
|
||||||
|
@Schema(description = "音色名称", example = "voice_01")
|
||||||
|
private String ttsVoiceName;
|
||||||
|
|
||||||
|
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
||||||
|
private String llmModelName;
|
||||||
|
|
||||||
|
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
||||||
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
||||||
|
private Date lastConnectedAt;
|
||||||
|
|
||||||
|
@Schema(description = "设备数量", example = "10")
|
||||||
|
private Integer deviceCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("ai_agent_chat_audio")
|
||||||
|
public class AgentChatAudioEntity {
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频opus数据
|
||||||
|
*/
|
||||||
|
private byte[] audio;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@TableName(value = "ai_agent_chat_history")
|
||||||
|
public class AgentChatHistoryEntity {
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAC地址
|
||||||
|
*/
|
||||||
|
@TableField(value = "mac_address")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体id
|
||||||
|
*/
|
||||||
|
@TableField(value = "agent_id")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话ID
|
||||||
|
*/
|
||||||
|
@TableField(value = "session_id")
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型: 1-用户, 2-智能体
|
||||||
|
*/
|
||||||
|
@TableField(value = "chat_type")
|
||||||
|
private Byte chatType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天内容
|
||||||
|
*/
|
||||||
|
@TableField(value = "content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频base64数据
|
||||||
|
*/
|
||||||
|
@TableField(value = "audio_id")
|
||||||
|
private String audioId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频URL
|
||||||
|
*/
|
||||||
|
@TableField(value = "audio_url")
|
||||||
|
private String audioUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_at")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_at")
|
||||||
|
private Date updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表处理service
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
|
||||||
|
/**
|
||||||
|
* 保存音频数据
|
||||||
|
*
|
||||||
|
* @param audioData 音频数据
|
||||||
|
* @return 音频ID
|
||||||
|
*/
|
||||||
|
String saveAudio(byte[] audioData);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表处理service
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
|
||||||
|
}
|
||||||
@@ -5,13 +5,10 @@ import java.util.Map;
|
|||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
public interface AgentService extends BaseService<AgentEntity> {
|
public interface AgentService extends BaseService<AgentEntity> {
|
||||||
/**
|
|
||||||
* 根据用户ID获取智能体列表
|
|
||||||
*/
|
|
||||||
List<AgentEntity> getUserAgents(Long userId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员获取所有智能体列表(分页)
|
* 管理员获取所有智能体列表(分页)
|
||||||
@@ -29,4 +26,28 @@ public interface AgentService extends BaseService<AgentEntity> {
|
|||||||
* @param userId
|
* @param userId
|
||||||
*/
|
*/
|
||||||
void deleteAgentByUserId(Long userId);
|
void deleteAgentByUserId(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户智能体列表
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<AgentDTO> getUserAgents(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体的设备数量
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @return 设备数量
|
||||||
|
*/
|
||||||
|
Integer getDeviceCountByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||||
|
*
|
||||||
|
* @param macAddress 设备MAC地址
|
||||||
|
* @return 默认智能体信息,不存在时返回null
|
||||||
|
*/
|
||||||
|
AgentEntity getDefaultAgentByMacAddress(String macAddress);
|
||||||
}
|
}
|
||||||
@@ -11,4 +11,18 @@ import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
|||||||
*/
|
*/
|
||||||
public interface AgentTemplateService extends IService<AgentTemplateEntity> {
|
public interface AgentTemplateService extends IService<AgentTemplateEntity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认模板
|
||||||
|
*
|
||||||
|
* @return 默认模板实体
|
||||||
|
*/
|
||||||
|
AgentTemplateEntity getDefaultTemplate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新默认模板中的模型ID
|
||||||
|
*
|
||||||
|
* @param modelType 模型类型
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
void updateDefaultTemplateModelId(String modelType, String modelId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.agent.service.biz;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天历史业务逻辑层
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatHistoryBizService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天上报方法
|
||||||
|
*
|
||||||
|
* @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象
|
||||||
|
* 例如:设备MAC地址、文件类型、内容等
|
||||||
|
* @return 上传结果,true表示成功,false表示失败
|
||||||
|
*/
|
||||||
|
Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package xiaozhi.modules.agent.service.biz.impl;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatHistoryBizService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService {
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理聊天记录上报,包括文件上传和相关信息记录
|
||||||
|
*
|
||||||
|
* @param report 包含聊天上报所需信息的输入对象
|
||||||
|
* @return 上传结果,true表示成功,false表示失败
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean report(AgentChatHistoryReportDTO report) {
|
||||||
|
String macAddress = report.getMacAddress();
|
||||||
|
Byte chatType = report.getChatType();
|
||||||
|
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
||||||
|
|
||||||
|
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||||
|
String audioId = null;
|
||||||
|
if (report.getOpusDataBase64() != null && !report.getOpusDataBase64().isEmpty()) {
|
||||||
|
try {
|
||||||
|
// TODO: 需要考虑保留什么格式的音频数据,比如是opus还是wave
|
||||||
|
// byte[] audioData = Base64.getDecoder().decode(report.getOpusDataBase64());
|
||||||
|
// audioId = agentChatAudioService.saveAudio(audioData);
|
||||||
|
// log.info("音频数据保存成功,audioId={}", audioId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("音频数据保存失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 组装上报数据
|
||||||
|
// 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||||
|
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||||
|
if (agentEntity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String agentId = agentEntity.getId();
|
||||||
|
log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
|
||||||
|
|
||||||
|
// 2.2 构建聊天记录实体
|
||||||
|
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||||
|
.macAddress(macAddress)
|
||||||
|
.agentId(agentId)
|
||||||
|
.sessionId(report.getSessionId())
|
||||||
|
.chatType(report.getChatType())
|
||||||
|
.content(report.getContent())
|
||||||
|
.audioId(audioId)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 3. 保存数据
|
||||||
|
agentChatHistoryService.save(entity);
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表处理service {@link AgentChatAudioService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
|
||||||
|
implements AgentChatAudioService {
|
||||||
|
@Override
|
||||||
|
public String saveAudio(byte[] audioData) {
|
||||||
|
AgentChatAudioEntity entity = new AgentChatAudioEntity();
|
||||||
|
entity.setAudio(audioData);
|
||||||
|
save(entity);
|
||||||
|
return entity.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表处理service {@link AgentChatHistoryService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity> implements AgentChatHistoryService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,7 +3,10 @@ package xiaozhi.modules.agent.service.impl;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
@@ -11,30 +14,37 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.modules.agent.dao.AgentDao;
|
import xiaozhi.modules.agent.dao.AgentDao;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||||
private final AgentDao agentDao;
|
private final AgentDao agentDao;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TimbreService timbreModelService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ModelConfigService modelConfigService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisUtils redisUtils;
|
||||||
|
|
||||||
public AgentServiceImpl(AgentDao agentDao) {
|
public AgentServiceImpl(AgentDao agentDao) {
|
||||||
this.agentDao = agentDao;
|
this.agentDao = agentDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<AgentEntity> getUserAgents(Long userId) {
|
|
||||||
QueryWrapper<AgentEntity> wrapper = new QueryWrapper<>();
|
|
||||||
wrapper.eq("user_id", userId);
|
|
||||||
return agentDao.selectList(wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
IPage<AgentEntity> page = agentDao.selectPage(
|
IPage<AgentEntity> page = agentDao.selectPage(
|
||||||
getPage(params, "sort", true),
|
getPage(params, "agent_name", true),
|
||||||
new QueryWrapper<>());
|
new QueryWrapper<>());
|
||||||
return new PageData<>(page.getRecords(), page.getTotal());
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
}
|
}
|
||||||
@@ -70,4 +80,62 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
wrapper.eq("user_id", userId);
|
wrapper.eq("user_id", userId);
|
||||||
baseDao.delete(wrapper);
|
baseDao.delete(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AgentDTO> getUserAgents(Long userId) {
|
||||||
|
QueryWrapper<AgentEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
List<AgentEntity> agents = agentDao.selectList(wrapper);
|
||||||
|
return agents.stream().map(agent -> {
|
||||||
|
AgentDTO dto = new AgentDTO();
|
||||||
|
dto.setId(agent.getId());
|
||||||
|
dto.setAgentName(agent.getAgentName());
|
||||||
|
dto.setSystemPrompt(agent.getSystemPrompt());
|
||||||
|
|
||||||
|
// 获取 TTS 模型名称
|
||||||
|
dto.setTtsModelName(modelConfigService.getModelNameById(agent.getTtsModelId()));
|
||||||
|
|
||||||
|
// 获取 LLM 模型名称
|
||||||
|
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||||
|
|
||||||
|
// 获取 TTS 音色名称
|
||||||
|
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||||
|
|
||||||
|
// 获取设备数量
|
||||||
|
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getDeviceCountByAgentId(String agentId) {
|
||||||
|
if (StringUtils.isBlank(agentId)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先从Redis中获取
|
||||||
|
Integer cachedCount = (Integer) redisUtils.get(RedisKeys.getAgentDeviceCountById(agentId));
|
||||||
|
if (cachedCount != null) {
|
||||||
|
return cachedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果Redis中没有,则从数据库查询
|
||||||
|
Integer deviceCount = agentDao.getDeviceCountByAgentId(agentId);
|
||||||
|
|
||||||
|
// 将结果存入Redis
|
||||||
|
if (deviceCount != null) {
|
||||||
|
redisUtils.set(RedisKeys.getAgentDeviceCountById(agentId), deviceCount, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceCount != null ? deviceCount : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AgentEntity getDefaultAgentByMacAddress(String macAddress) {
|
||||||
|
if (StringUtils.isEmpty(macAddress)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return agentDao.getDefaultAgentByMacAddress(macAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ package xiaozhi.modules.agent.service.impl;
|
|||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||||
@@ -15,6 +17,53 @@ import xiaozhi.modules.agent.service.AgentTemplateService;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity>
|
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity>
|
||||||
implements AgentTemplateService {
|
implements AgentTemplateService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认模板
|
||||||
|
*
|
||||||
|
* @return 默认模板实体
|
||||||
|
*/
|
||||||
|
public AgentTemplateEntity getDefaultTemplate() {
|
||||||
|
LambdaQueryWrapper<AgentTemplateEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.orderByAsc(AgentTemplateEntity::getSort)
|
||||||
|
.last("LIMIT 1");
|
||||||
|
return this.getOne(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新默认模板中的模型ID
|
||||||
|
*
|
||||||
|
* @param modelType 模型类型
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
||||||
|
modelType = modelType.toUpperCase();
|
||||||
|
|
||||||
|
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
|
||||||
|
switch (modelType) {
|
||||||
|
case "ASR":
|
||||||
|
wrapper.set("asr_model_id", modelId);
|
||||||
|
break;
|
||||||
|
case "VAD":
|
||||||
|
wrapper.set("vad_model_id", modelId);
|
||||||
|
break;
|
||||||
|
case "LLM":
|
||||||
|
wrapper.set("llm_model_id", modelId);
|
||||||
|
break;
|
||||||
|
case "TTS":
|
||||||
|
wrapper.set("tts_model_id", modelId);
|
||||||
|
wrapper.set("tts_voice_id", null);
|
||||||
|
break;
|
||||||
|
case "MEMORY":
|
||||||
|
wrapper.set("mem_model_id", modelId);
|
||||||
|
break;
|
||||||
|
case "INTENT":
|
||||||
|
wrapper.set("intent_model_id", modelId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
wrapper.ge("sort", 0);
|
||||||
|
update(wrapper);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package xiaozhi.modules.config.controller;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* xiaozhi-server 配置获取
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("config")
|
||||||
|
@Tag(name = "参数管理")
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ConfigController {
|
||||||
|
private final ConfigService configService;
|
||||||
|
|
||||||
|
@PostMapping("server-base")
|
||||||
|
@Operation(summary = "获取配置")
|
||||||
|
public Result<Object> getConfig() {
|
||||||
|
Object config = configService.getConfig(true);
|
||||||
|
return new Result<Object>().ok(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("agent-models")
|
||||||
|
@Operation(summary = "获取智能体模型")
|
||||||
|
public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
|
||||||
|
// 效验数据
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
||||||
|
return new Result<Object>().ok(models);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package xiaozhi.modules.config.dto;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "获取智能体模型配置DTO")
|
||||||
|
public class AgentModelsDTO {
|
||||||
|
|
||||||
|
@NotBlank(message = "设备MAC地址不能为空")
|
||||||
|
@Schema(description = "设备MAC地址")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
@NotBlank(message = "客户端ID不能为空")
|
||||||
|
@Schema(description = "客户端ID")
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
@NotNull(message = "客户端已实例化的模型不能为空")
|
||||||
|
@Schema(description = "客户端已实例化的模型")
|
||||||
|
private Map<String, String> selectedModule;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package xiaozhi.modules.config.init;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@DependsOn("liquibase")
|
||||||
|
public class SystemInitConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysParamsService sysParamsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConfigService configService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisUtils redisUtils;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
// 检查版本号
|
||||||
|
String redisVersion = (String) redisUtils.get(RedisKeys.getVersionKey());
|
||||||
|
if (!Constant.VERSION.equals(redisVersion)) {
|
||||||
|
// 如果版本不一致,清空Redis
|
||||||
|
redisUtils.emptyAll();
|
||||||
|
// 存储新版本号
|
||||||
|
redisUtils.set(RedisKeys.getVersionKey(), Constant.VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
sysParamsService.initServerSecret();
|
||||||
|
configService.getConfig(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.config.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface ConfigService {
|
||||||
|
/**
|
||||||
|
* 获取服务器配置
|
||||||
|
*
|
||||||
|
* @param isCache 是否缓存
|
||||||
|
* @return 配置信息
|
||||||
|
*/
|
||||||
|
Object getConfig(Boolean isCache);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体模型配置
|
||||||
|
*
|
||||||
|
* @param macAddress MAC地址
|
||||||
|
* @param selectedModule 客户端已实例化的模型
|
||||||
|
* @return 模型配置信息
|
||||||
|
*/
|
||||||
|
Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule);
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
package xiaozhi.modules.config.service.impl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ConfigServiceImpl implements ConfigService {
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
private final ModelConfigService modelConfigService;
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final AgentTemplateService agentTemplateService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
private final TimbreService timbreService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getConfig(Boolean isCache) {
|
||||||
|
if (isCache) {
|
||||||
|
// 先从Redis获取配置
|
||||||
|
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
|
||||||
|
if (cachedConfig != null) {
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建配置信息
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
buildConfig(result);
|
||||||
|
|
||||||
|
// 查询默认智能体
|
||||||
|
AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate();
|
||||||
|
if (agent == null) {
|
||||||
|
throw new RenException("默认智能体未找到");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建模块配置
|
||||||
|
buildModuleConfig(
|
||||||
|
agent.getAgentName(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
agent.getVadModelId(),
|
||||||
|
agent.getAsrModelId(),
|
||||||
|
agent.getLlmModelId(),
|
||||||
|
agent.getTtsModelId(),
|
||||||
|
agent.getMemModelId(),
|
||||||
|
agent.getIntentModelId(),
|
||||||
|
result,
|
||||||
|
isCache);
|
||||||
|
|
||||||
|
// 将配置存入Redis
|
||||||
|
redisUtils.set(RedisKeys.getServerConfigKey(), result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule) {
|
||||||
|
// 根据MAC地址查找设备
|
||||||
|
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||||
|
if (device == null) {
|
||||||
|
// 如果设备,去redis里看看有没有需要连接的设备
|
||||||
|
String cachedCode = deviceService.geCodeByDeviceId(macAddress);
|
||||||
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
|
throw new RenException(ErrorCode.OTA_DEVICE_NEED_BIND, cachedCode);
|
||||||
|
}
|
||||||
|
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取智能体信息
|
||||||
|
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||||
|
if (agent == null) {
|
||||||
|
throw new RenException("智能体未找到");
|
||||||
|
}
|
||||||
|
// 获取音色信息
|
||||||
|
String voice = null;
|
||||||
|
TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId());
|
||||||
|
if (timbre != null) {
|
||||||
|
voice = timbre.getTtsVoice();
|
||||||
|
}
|
||||||
|
// 构建返回数据
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
// 获取单台设备每天最多输出字数
|
||||||
|
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||||
|
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||||
|
// 如果客户端已实例化模型,则不返回
|
||||||
|
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||||
|
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||||
|
agent.setVadModelId(null);
|
||||||
|
}
|
||||||
|
String alreadySelectedAsrModelId = (String) selectedModule.get("ASR");
|
||||||
|
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
||||||
|
agent.setAsrModelId(null);
|
||||||
|
}
|
||||||
|
String alreadySelectedLlmModelId = (String) selectedModule.get("LLM");
|
||||||
|
if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) {
|
||||||
|
agent.setLlmModelId(null);
|
||||||
|
}
|
||||||
|
String alreadySelectedMemModelId = (String) selectedModule.get("Memory");
|
||||||
|
if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) {
|
||||||
|
agent.setMemModelId(null);
|
||||||
|
}
|
||||||
|
String alreadySelectedIntentModelId = (String) selectedModule.get("Intent");
|
||||||
|
if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) {
|
||||||
|
agent.setIntentModelId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建模块配置
|
||||||
|
buildModuleConfig(
|
||||||
|
agent.getAgentName(),
|
||||||
|
agent.getSystemPrompt(),
|
||||||
|
voice,
|
||||||
|
agent.getVadModelId(),
|
||||||
|
agent.getAsrModelId(),
|
||||||
|
agent.getLlmModelId(),
|
||||||
|
agent.getTtsModelId(),
|
||||||
|
agent.getMemModelId(),
|
||||||
|
agent.getIntentModelId(),
|
||||||
|
result,
|
||||||
|
true);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建配置信息
|
||||||
|
*
|
||||||
|
* @param paramsList 系统参数列表
|
||||||
|
* @return 配置信息
|
||||||
|
*/
|
||||||
|
private Object buildConfig(Map<String, Object> config) {
|
||||||
|
|
||||||
|
// 查询所有系统参数
|
||||||
|
List<SysParamsDTO> paramsList = sysParamsService.list(new HashMap<>());
|
||||||
|
|
||||||
|
for (SysParamsDTO param : paramsList) {
|
||||||
|
String[] keys = param.getParamCode().split("\\.");
|
||||||
|
Map<String, Object> current = config;
|
||||||
|
|
||||||
|
// 遍历除最后一个key之外的所有key
|
||||||
|
for (int i = 0; i < keys.length - 1; i++) {
|
||||||
|
String key = keys[i];
|
||||||
|
if (!current.containsKey(key)) {
|
||||||
|
current.put(key, new HashMap<String, Object>());
|
||||||
|
}
|
||||||
|
current = (Map<String, Object>) current.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理最后一个key
|
||||||
|
String lastKey = keys[keys.length - 1];
|
||||||
|
String value = param.getParamValue();
|
||||||
|
|
||||||
|
// 根据valueType转换值
|
||||||
|
switch (param.getValueType().toLowerCase()) {
|
||||||
|
case "number":
|
||||||
|
try {
|
||||||
|
double doubleValue = Double.parseDouble(value);
|
||||||
|
// 如果数值是整数形式,则转换为Integer
|
||||||
|
if (doubleValue == (int) doubleValue) {
|
||||||
|
current.put(lastKey, (int) doubleValue);
|
||||||
|
} else {
|
||||||
|
current.put(lastKey, doubleValue);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
current.put(lastKey, value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "boolean":
|
||||||
|
current.put(lastKey, Boolean.parseBoolean(value));
|
||||||
|
break;
|
||||||
|
case "array":
|
||||||
|
// 将分号分隔的字符串转换为数字数组
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
for (String num : value.split(";")) {
|
||||||
|
if (StringUtils.isNotBlank(num)) {
|
||||||
|
list.add(num.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.put(lastKey, list);
|
||||||
|
break;
|
||||||
|
case "json":
|
||||||
|
try {
|
||||||
|
current.put(lastKey, JsonUtils.parseObject(value, Object.class));
|
||||||
|
} catch (Exception e) {
|
||||||
|
current.put(lastKey, value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
current.put(lastKey, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建模块配置
|
||||||
|
*
|
||||||
|
* @param prompt 提示词
|
||||||
|
* @param voice 音色
|
||||||
|
* @param vadModelId VAD模型ID
|
||||||
|
* @param asrModelId ASR模型ID
|
||||||
|
* @param llmModelId LLM模型ID
|
||||||
|
* @param ttsModelId TTS模型ID
|
||||||
|
* @param memModelId 记忆模型ID
|
||||||
|
* @param intentModelId 意图模型ID
|
||||||
|
* @param result 结果Map
|
||||||
|
*/
|
||||||
|
private void buildModuleConfig(
|
||||||
|
String assistantName,
|
||||||
|
String prompt,
|
||||||
|
String voice,
|
||||||
|
String vadModelId,
|
||||||
|
String asrModelId,
|
||||||
|
String llmModelId,
|
||||||
|
String ttsModelId,
|
||||||
|
String memModelId,
|
||||||
|
String intentModelId,
|
||||||
|
Map<String, Object> result,
|
||||||
|
boolean isCache) {
|
||||||
|
Map<String, String> selectedModule = new HashMap<>();
|
||||||
|
|
||||||
|
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM" };
|
||||||
|
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId };
|
||||||
|
String intentLLMModelId = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < modelIds.length; i++) {
|
||||||
|
if (modelIds[i] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache);
|
||||||
|
Map<String, Object> typeConfig = new HashMap<>();
|
||||||
|
if (model.getConfigJson() != null) {
|
||||||
|
typeConfig.put(model.getId(), model.getConfigJson());
|
||||||
|
// 如果是TTS类型,添加private_voice属性
|
||||||
|
if ("TTS".equals(modelTypes[i]) && voice != null) {
|
||||||
|
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
|
||||||
|
}
|
||||||
|
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型
|
||||||
|
if ("Intent".equals(modelTypes[i])) {
|
||||||
|
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
||||||
|
if ("intent_llm".equals(map.get("type"))) {
|
||||||
|
intentLLMModelId = (String) map.get("llm");
|
||||||
|
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||||
|
intentLLMModelId = null;
|
||||||
|
}
|
||||||
|
} else if ("function_call".equals(map.get("type"))) {
|
||||||
|
String functionStr = (String) map.get("functions");
|
||||||
|
if (StringUtils.isNotBlank(functionStr)) {
|
||||||
|
String[] functions = functionStr.split("\\;");
|
||||||
|
map.put("functions", functions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
||||||
|
if ("LLM".equals(modelTypes[i]) && intentLLMModelId != null) {
|
||||||
|
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache);
|
||||||
|
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.put(modelTypes[i], typeConfig);
|
||||||
|
|
||||||
|
selectedModule.put(modelTypes[i], model.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("selected_module", selectedModule);
|
||||||
|
if (StringUtils.isNotBlank(prompt)) {
|
||||||
|
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||||
|
}
|
||||||
|
result.put("prompt", prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -79,4 +80,16 @@ public class DeviceController {
|
|||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/enableOta/{id}/{status}")
|
||||||
|
@Operation(summary = "启用/关闭OTA自动升级")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> enableOtaUpgrade(@PathVariable String id, @PathVariable Integer status) {
|
||||||
|
DeviceEntity entity = deviceService.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<Void>().error("设备不存在");
|
||||||
|
}
|
||||||
|
entity.setAutoUpdate(status);
|
||||||
|
deviceService.updateById(entity);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
@@ -14,42 +15,78 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.utils.NetworkUtil;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/ota/")
|
@RequestMapping("/ota/")
|
||||||
public class OTAController {
|
public class OTAController {
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@Operation(summary = "检查 OTA 版本和设备激活状态")
|
@Operation(summary = "OTA版本和设备激活状态检查")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<String> checkOTAVersion(
|
public ResponseEntity<String> checkOTAVersion(
|
||||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||||
|
|
||||||
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
|
||||||
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isBlank(clientId)) {
|
||||||
|
clientId = deviceId;
|
||||||
|
}
|
||||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
boolean macAddressValid = isMacAddressValid(macAddress);
|
||||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||||
}
|
}
|
||||||
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "设备快速检查激活状态")
|
||||||
|
@PostMapping("activate")
|
||||||
|
public ResponseEntity<String> activateDevice(
|
||||||
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
|
return ResponseEntity.status(202).build();
|
||||||
|
}
|
||||||
|
DeviceEntity device = deviceService.getDeviceByMacAddress(deviceId);
|
||||||
|
if (device == null) {
|
||||||
|
return ResponseEntity.status(202).build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok("success");
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Hidden
|
||||||
|
public ResponseEntity<String> getOTA() {
|
||||||
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
|
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
|
}
|
||||||
|
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||||
|
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||||
|
return ResponseEntity.ok("OTA接口不正常,缺少ota地址,请登录智控台,在参数管理找到【server.ota】配置");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@@ -64,4 +101,19 @@ public class OTAController {
|
|||||||
.contentLength(jsonBytes.length)
|
.contentLength(jsonBytes.length)
|
||||||
.body(json);
|
.body(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单判断mac地址是否有效(非严格)
|
||||||
|
*
|
||||||
|
* @param macAddress
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isMacAddressValid(String macAddress) {
|
||||||
|
if (StringUtils.isBlank(macAddress)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// MAC地址通常为12位十六进制数字,可以包含冒号或连字符分隔符
|
||||||
|
String macPattern = "^([0-9A-Za-z]{2}[:-]){5}([0-9A-Za-z]{2})$";
|
||||||
|
return macAddress.matches(macPattern);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
|
||||||
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/otaMag")
|
||||||
|
public class OTAMagController {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
||||||
|
private final OtaService otaService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "分页查询 OTA 固件信息")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||||
|
})
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<PageData<OtaEntity>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
PageData<OtaEntity> page = otaService.page(params);
|
||||||
|
return new Result<PageData<OtaEntity>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
@Operation(summary = "信息 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<OtaEntity> get(@PathVariable("id") String id) {
|
||||||
|
OtaEntity data = otaService.selectById(id);
|
||||||
|
return new Result<OtaEntity>().ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "保存 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody OtaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<Void>().error("固件信息不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getFirmwareName())) {
|
||||||
|
return new Result<Void>().error("固件名称不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getType())) {
|
||||||
|
return new Result<Void>().error("固件类型不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getVersion())) {
|
||||||
|
return new Result<Void>().error("版本号不能为空");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
otaService.save(entity);
|
||||||
|
return new Result<Void>();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new Result<Void>().error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "OTA 删除")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> delete(@PathVariable("id") String[] ids) {
|
||||||
|
if (ids == null || ids.length == 0) {
|
||||||
|
return new Result<Void>().error("删除的固件ID不能为空");
|
||||||
|
}
|
||||||
|
otaService.delete(ids);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "修改 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<?> update(@PathVariable("id") String id, @RequestBody OtaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<>().error("固件信息不能为空");
|
||||||
|
}
|
||||||
|
entity.setId(id);
|
||||||
|
try {
|
||||||
|
otaService.update(entity);
|
||||||
|
return new Result<>();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new Result<>().error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/getDownloadUrl/{id}")
|
||||||
|
@Operation(summary = "获取 OTA 固件下载链接")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> getDownloadUrl(@PathVariable("id") String id) {
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getOtaIdKey(uuid), id);
|
||||||
|
return new Result<String>().ok(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/download/{uuid}")
|
||||||
|
@Operation(summary = "下载固件文件")
|
||||||
|
public ResponseEntity<byte[]> downloadFirmware(@PathVariable("uuid") String uuid) {
|
||||||
|
String id = (String) redisUtils.get(RedisKeys.getOtaIdKey(uuid));
|
||||||
|
if (StringUtils.isBlank(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查下载次数
|
||||||
|
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
||||||
|
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
|
||||||
|
if (downloadCount == null) {
|
||||||
|
downloadCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果下载次数超过3次,返回404
|
||||||
|
if (downloadCount >= 3) {
|
||||||
|
redisUtils.delete(downloadCountKey);
|
||||||
|
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
|
||||||
|
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
redisUtils.set(downloadCountKey, downloadCount + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取固件信息
|
||||||
|
OtaEntity otaEntity = otaService.selectById(id);
|
||||||
|
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
||||||
|
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
||||||
|
String firmwarePath = otaEntity.getFirmwarePath();
|
||||||
|
Path path;
|
||||||
|
|
||||||
|
// 检查是否是绝对路径
|
||||||
|
if (Paths.get(firmwarePath).isAbsolute()) {
|
||||||
|
path = Paths.get(firmwarePath);
|
||||||
|
} else {
|
||||||
|
// 如果是相对路径,则从当前工作目录解析
|
||||||
|
path = Paths.get(System.getProperty("user.dir"), firmwarePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Attempting to download firmware for ID: {}, DB path: {}, resolved path: {}",
|
||||||
|
id, firmwarePath, path.toAbsolutePath());
|
||||||
|
|
||||||
|
if (!Files.exists(path) || !Files.isRegularFile(path)) {
|
||||||
|
// 尝试直接从firmware目录下查找文件名
|
||||||
|
String fileName = new File(firmwarePath).getName();
|
||||||
|
Path altPath = Paths.get(System.getProperty("user.dir"), "firmware", fileName);
|
||||||
|
|
||||||
|
logger.info("File not found at primary path, trying alternative path: {}", altPath.toAbsolutePath());
|
||||||
|
|
||||||
|
if (Files.exists(altPath) && Files.isRegularFile(altPath)) {
|
||||||
|
path = altPath;
|
||||||
|
} else {
|
||||||
|
logger.error("Firmware file not found at either path: {} or {}",
|
||||||
|
path.toAbsolutePath(), altPath.toAbsolutePath());
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
byte[] fileContent = Files.readAllBytes(path);
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||||
|
if (firmwarePath.contains(".")) {
|
||||||
|
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
||||||
|
originalFilename += extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理文件名,移除不安全字符
|
||||||
|
String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||||
|
|
||||||
|
logger.info("Providing download for firmware ID: {}, filename: {}, size: {} bytes",
|
||||||
|
id, safeFilename, fileContent.length);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + safeFilename + "\"")
|
||||||
|
.body(fileContent);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error reading firmware file for ID: {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error during firmware download for ID: {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@Operation(summary = "上传固件文件")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> uploadFirmware(@RequestParam("file") MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return new Result<String>().error("上传文件不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件扩展名
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
if (originalFilename == null) {
|
||||||
|
return new Result<String>().error("文件名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
|
||||||
|
if (!extension.equals(".bin") && !extension.equals(".apk")) {
|
||||||
|
return new Result<String>().error("只允许上传.bin和.apk格式的文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算文件的MD5值
|
||||||
|
String md5 = calculateMD5(file);
|
||||||
|
|
||||||
|
// 设置存储路径
|
||||||
|
String uploadDir = "uploadfile";
|
||||||
|
Path uploadPath = Paths.get(uploadDir);
|
||||||
|
|
||||||
|
// 如果目录不存在,创建目录
|
||||||
|
if (!Files.exists(uploadPath)) {
|
||||||
|
Files.createDirectories(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用MD5作为文件名,固定使用.bin扩展名
|
||||||
|
String uniqueFileName = md5 + extension;
|
||||||
|
Path filePath = uploadPath.resolve(uniqueFileName);
|
||||||
|
|
||||||
|
// 检查文件是否已存在
|
||||||
|
if (Files.exists(filePath)) {
|
||||||
|
return new Result<String>().ok(filePath.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
Files.copy(file.getInputStream(), filePath);
|
||||||
|
|
||||||
|
// 返回文件路径
|
||||||
|
return new Result<String>().ok(filePath.toString());
|
||||||
|
} catch (IOException | NoSuchAlgorithmException e) {
|
||||||
|
return new Result<String>().error("文件上传失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] digest = md.digest(file.getBytes());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : digest) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package xiaozhi.modules.device.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件管理
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface OtaDao extends BaseMapper<OtaEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "固件版本信息")
|
@Schema(description = "固件版本信息")
|
||||||
private Firmware firmware;
|
private Firmware firmware;
|
||||||
|
|
||||||
|
@Schema(description = "WebSocket配置")
|
||||||
|
private Websocket websocket;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public static class Firmware {
|
public static class Firmware {
|
||||||
@@ -44,6 +47,8 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "激活码信息: 激活地址")
|
@Schema(description = "激活码信息: 激活地址")
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
|
@Schema(description = "挑战码")
|
||||||
|
private String challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -58,4 +63,11 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "时区偏移量,单位为分钟")
|
@Schema(description = "时区偏移量,单位为分钟")
|
||||||
private Integer timezone_offset;
|
private Integer timezone_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Websocket {
|
||||||
|
@Schema(description = "WebSocket服务器地址")
|
||||||
|
private String url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package xiaozhi.modules.device.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("ai_ota")
|
||||||
|
@Schema(description = "固件信息")
|
||||||
|
public class OtaEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "ID")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "固件名称")
|
||||||
|
private String firmwareName;
|
||||||
|
|
||||||
|
@Schema(description = "固件类型")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "版本号")
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小(字节)")
|
||||||
|
private Long size;
|
||||||
|
|
||||||
|
@Schema(description = "备注/说明")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "固件路径")
|
||||||
|
private String firmwarePath;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Date updateDate;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Date createDate;
|
||||||
|
}
|
||||||
@@ -3,23 +3,19 @@ package xiaozhi.modules.device.service;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
|
|
||||||
public interface DeviceService {
|
public interface DeviceService extends BaseService<DeviceEntity> {
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据Mac地址获取设备信息
|
|
||||||
*/
|
|
||||||
DeviceEntity getDeviceById(String macAddress);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查设备是否激活
|
* 检查设备是否激活
|
||||||
*/
|
*/
|
||||||
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||||
DeviceReportReqDTO deviceReport);
|
DeviceReportReqDTO deviceReport);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +40,13 @@ public interface DeviceService {
|
|||||||
*/
|
*/
|
||||||
void deleteByUserId(Long userId);
|
void deleteByUserId(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定智能体关联的所有设备
|
||||||
|
*
|
||||||
|
* @param agentId 智能体id
|
||||||
|
*/
|
||||||
|
void deleteByAgentId(String agentId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定用户的设备数量
|
* 获取指定用户的设备数量
|
||||||
*
|
*
|
||||||
@@ -59,4 +62,20 @@ public interface DeviceService {
|
|||||||
* @return 用户列表分页数据
|
* @return 用户列表分页数据
|
||||||
*/
|
*/
|
||||||
PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto);
|
PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据MAC地址获取设备信息
|
||||||
|
*
|
||||||
|
* @param macAddress MAC地址
|
||||||
|
* @return 设备信息
|
||||||
|
*/
|
||||||
|
DeviceEntity getDeviceByMacAddress(String macAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备ID获取激活码
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @return 激活码
|
||||||
|
*/
|
||||||
|
String geCodeByDeviceId(String deviceId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.device.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件管理
|
||||||
|
*/
|
||||||
|
public interface OtaService extends BaseService<OtaEntity> {
|
||||||
|
PageData<OtaEntity> page(Map<String, Object> params);
|
||||||
|
|
||||||
|
boolean save(OtaEntity entity);
|
||||||
|
|
||||||
|
void update(OtaEntity entity);
|
||||||
|
|
||||||
|
void delete(String[] ids);
|
||||||
|
|
||||||
|
OtaEntity getLatestOta(String type);
|
||||||
|
}
|
||||||
@@ -6,22 +6,26 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
@@ -31,38 +35,24 @@ import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
|||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||||
|
|
||||||
private final DeviceDao deviceDao;
|
private final DeviceDao deviceDao;
|
||||||
|
|
||||||
private final SysUserUtilService sysUserUtilService;
|
private final SysUserUtilService sysUserUtilService;
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
private final String frontedUrl;
|
private final RedisUtils redisUtils;
|
||||||
|
private final OtaService otaService;
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
|
||||||
|
|
||||||
// 添加构造函数来初始化 deviceMapper
|
|
||||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
|
||||||
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
|
||||||
RedisTemplate<String, Object> redisTemplate) {
|
|
||||||
this.deviceDao = deviceDao;
|
|
||||||
this.sysUserUtilService = sysUserUtilService;
|
|
||||||
this.frontedUrl = frontedUrl;
|
|
||||||
this.redisTemplate = redisTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DeviceEntity getDeviceById(String deviceId) {
|
|
||||||
LambdaQueryWrapper<DeviceEntity> queryWrapper = new LambdaQueryWrapper<>();
|
|
||||||
queryWrapper.eq(DeviceEntity::getId, deviceId);
|
|
||||||
return deviceDao.selectOne(queryWrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||||
@@ -70,14 +60,14 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
throw new RenException("激活码不能为空");
|
throw new RenException("激活码不能为空");
|
||||||
}
|
}
|
||||||
String deviceKey = "ota:activation:code:" + activationCode;
|
String deviceKey = "ota:activation:code:" + activationCode;
|
||||||
Object cacheDeviceId = redisTemplate.opsForValue().get(deviceKey);
|
Object cacheDeviceId = redisUtils.get(deviceKey);
|
||||||
if (cacheDeviceId == null) {
|
if (cacheDeviceId == null) {
|
||||||
throw new RenException("激活码错误");
|
throw new RenException("激活码错误");
|
||||||
}
|
}
|
||||||
String deviceId = (String) cacheDeviceId;
|
String deviceId = (String) cacheDeviceId;
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(cacheDeviceKey);
|
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||||
if (cacheMap == null) {
|
if (cacheMap == null) {
|
||||||
throw new RenException("激活码错误");
|
throw new RenException("激活码错误");
|
||||||
}
|
}
|
||||||
@@ -107,6 +97,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
deviceEntity.setMacAddress(macAddress);
|
deviceEntity.setMacAddress(macAddress);
|
||||||
deviceEntity.setUserId(user.getId());
|
deviceEntity.setUserId(user.getId());
|
||||||
deviceEntity.setCreator(user.getId());
|
deviceEntity.setCreator(user.getId());
|
||||||
|
deviceEntity.setAutoUpdate(1);
|
||||||
deviceEntity.setCreateDate(currentTime);
|
deviceEntity.setCreateDate(currentTime);
|
||||||
deviceEntity.setUpdater(user.getId());
|
deviceEntity.setUpdater(user.getId());
|
||||||
deviceEntity.setUpdateDate(currentTime);
|
deviceEntity.setUpdateDate(currentTime);
|
||||||
@@ -114,64 +105,60 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
deviceDao.insert(deviceEntity);
|
deviceDao.insert(deviceEntity);
|
||||||
|
|
||||||
// 清理redis缓存
|
// 清理redis缓存
|
||||||
redisTemplate.delete(cacheDeviceKey);
|
redisUtils.delete(cacheDeviceKey);
|
||||||
redisTemplate.delete(deviceKey);
|
redisUtils.delete(deviceKey);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||||
DeviceReportReqDTO deviceReport) {
|
DeviceReportReqDTO deviceReport) {
|
||||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||||
response.setServer_time(buildServerTime());
|
response.setServer_time(buildServerTime());
|
||||||
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
|
|
||||||
// 在未来开发了固件更新功能,需要更换此处代码,
|
|
||||||
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
|
|
||||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
|
||||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
|
||||||
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
|
||||||
response.setFirmware(firmware);
|
|
||||||
|
|
||||||
DeviceEntity deviceById = getDeviceById(deviceId);
|
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
||||||
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
|
||||||
deviceById.setLastConnectedAt(new Date());
|
|
||||||
deviceDao.updateById(deviceById);
|
|
||||||
} else { // 如果设备不存在,则生成激活码
|
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
|
||||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
|
||||||
|
|
||||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
if (deviceById == null || deviceById.getAutoUpdate() != 0) {
|
||||||
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||||
|
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||||
|
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||||
|
response.setFirmware(firmware);
|
||||||
|
} else {
|
||||||
|
response.setFirmware(null);
|
||||||
|
}
|
||||||
|
|
||||||
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
// 添加WebSocket配置
|
||||||
String cachedCode = (String) cacheMap.get("activation_code");
|
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||||
code.setCode(cachedCode);
|
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||||
code.setMessage(frontedUrl + "\n" + cachedCode);
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
|
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
|
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||||
|
websocket.setUrl(wsUrl);
|
||||||
|
} else {
|
||||||
|
String[] wsUrls = wsUrl.split("\\;");
|
||||||
|
if (wsUrls.length > 0) {
|
||||||
|
// 随机选择一个WebSocket URL
|
||||||
|
websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]);
|
||||||
} else {
|
} else {
|
||||||
String newCode = RandomUtil.randomNumbers(6);
|
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
code.setCode(newCode);
|
websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/");
|
||||||
code.setMessage(frontedUrl + "\n" + newCode);
|
|
||||||
|
|
||||||
Map<String, Object> dataMap = new HashMap<>();
|
|
||||||
dataMap.put("id", deviceId);
|
|
||||||
dataMap.put("mac_address", macAddress);
|
|
||||||
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
|
|
||||||
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
|
|
||||||
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
|
||||||
? deviceReport.getApplication().getVersion()
|
|
||||||
: null);
|
|
||||||
dataMap.put("deviceId", deviceId);
|
|
||||||
dataMap.put("activation_code", newCode);
|
|
||||||
|
|
||||||
// 写入主数据 key
|
|
||||||
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
|
||||||
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
|
||||||
|
|
||||||
// 写入反查激活码 key
|
|
||||||
String codeKey = "ota:activation:code:" + newCode;
|
|
||||||
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setWebsocket(websocket);
|
||||||
|
|
||||||
|
if (deviceById != null) {
|
||||||
|
// 如果设备存在,则更新上次连接时间
|
||||||
|
deviceById.setLastConnectedAt(new Date());
|
||||||
|
if (deviceReport.getApplication() != null
|
||||||
|
&& StringUtils.isNotBlank(deviceReport.getApplication().getVersion())) {
|
||||||
|
deviceById.setAppVersion(deviceReport.getApplication().getVersion());
|
||||||
|
}
|
||||||
|
deviceDao.updateById(deviceById);
|
||||||
|
} else {
|
||||||
|
// 如果设备不存在,则生成激活码
|
||||||
|
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||||
response.setActivation(code);
|
response.setActivation(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,13 +195,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
return baseDao.selectCount(wrapper);
|
return baseDao.selectCount(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByAgentId(String agentId) {
|
||||||
|
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||||
|
wrapper.eq("agent_id", agentId);
|
||||||
|
baseDao.delete(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto) {
|
public PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto) {
|
||||||
Map<String, Object> params = new HashMap<String, Object>();
|
Map<String, Object> params = new HashMap<String, Object>();
|
||||||
params.put(Constant.PAGE, dto.getPage());
|
params.put(Constant.PAGE, dto.getPage());
|
||||||
params.put(Constant.LIMIT, dto.getLimit());
|
params.put(Constant.LIMIT, dto.getLimit());
|
||||||
IPage<DeviceEntity> page = baseDao.selectPage(
|
IPage<DeviceEntity> page = baseDao.selectPage(
|
||||||
getPage(params, "sort", true),
|
getPage(params, "mac_address", true),
|
||||||
// 定义查询条件
|
// 定义查询条件
|
||||||
new QueryWrapper<DeviceEntity>()
|
new QueryWrapper<DeviceEntity>()
|
||||||
// 必须设备关键词查找
|
// 必须设备关键词查找
|
||||||
@@ -233,6 +227,16 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
return new PageData<>(list, page.getTotal());
|
return new PageData<>(list, page.getTotal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceEntity getDeviceByMacAddress(String macAddress) {
|
||||||
|
if (StringUtils.isBlank(macAddress)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("mac_address", macAddress);
|
||||||
|
return baseDao.selectOne(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
private DeviceReportRespDTO.ServerTime buildServerTime() {
|
private DeviceReportRespDTO.ServerTime buildServerTime() {
|
||||||
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
|
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
|
||||||
TimeZone tz = TimeZone.getDefault();
|
TimeZone tz = TimeZone.getDefault();
|
||||||
@@ -241,4 +245,129 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||||
return serverTime;
|
return serverTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String geCodeByDeviceId(String deviceId) {
|
||||||
|
String dataKey = getDeviceCacheKey(deviceId);
|
||||||
|
|
||||||
|
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
|
||||||
|
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||||
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
|
return cachedCode;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDeviceCacheKey(String deviceId) {
|
||||||
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
|
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
return dataKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
|
||||||
|
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
||||||
|
|
||||||
|
String cachedCode = geCodeByDeviceId(deviceId);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
|
code.setCode(cachedCode);
|
||||||
|
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
|
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||||
|
code.setChallenge(deviceId);
|
||||||
|
} else {
|
||||||
|
String newCode = RandomUtil.randomNumbers(6);
|
||||||
|
code.setCode(newCode);
|
||||||
|
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
|
code.setMessage(frontedUrl + "\n" + newCode);
|
||||||
|
code.setChallenge(deviceId);
|
||||||
|
|
||||||
|
Map<String, Object> dataMap = new HashMap<>();
|
||||||
|
dataMap.put("id", deviceId);
|
||||||
|
dataMap.put("mac_address", deviceId);
|
||||||
|
|
||||||
|
dataMap.put("board", (deviceReport.getBoard() != null && deviceReport.getBoard().getType() != null)
|
||||||
|
? deviceReport.getBoard().getType()
|
||||||
|
: (deviceReport.getChipModelName() != null ? deviceReport.getChipModelName() : "unknown"));
|
||||||
|
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
||||||
|
? deviceReport.getApplication().getVersion()
|
||||||
|
: null);
|
||||||
|
|
||||||
|
dataMap.put("deviceId", deviceId);
|
||||||
|
dataMap.put("activation_code", newCode);
|
||||||
|
|
||||||
|
// 写入主数据 key
|
||||||
|
String dataKey = getDeviceCacheKey(deviceId);
|
||||||
|
redisUtils.set(dataKey, dataMap);
|
||||||
|
|
||||||
|
// 写入反查激活码 key
|
||||||
|
String codeKey = "ota:activation:code:" + newCode;
|
||||||
|
redisUtils.set(codeKey, deviceId);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceReportRespDTO.Firmware buildFirmwareInfo(String type, String currentVersion) {
|
||||||
|
if (StringUtils.isBlank(type)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(currentVersion)) {
|
||||||
|
currentVersion = "0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
OtaEntity ota = otaService.getLatestOta(type);
|
||||||
|
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||||
|
String downloadUrl = null;
|
||||||
|
|
||||||
|
if (ota != null) {
|
||||||
|
// 如果设备没有版本信息,或者OTA版本比设备版本新,则返回下载地址
|
||||||
|
if (compareVersions(ota.getVersion(), currentVersion) > 0) {
|
||||||
|
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||||
|
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||||
|
log.error("OTA地址未配置,请登录智控台,在参数管理找到【server.ota】配置");
|
||||||
|
// 尝试从请求中获取
|
||||||
|
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
|
||||||
|
.getRequestAttributes())
|
||||||
|
.getRequest();
|
||||||
|
otaUrl = request.getRequestURL().toString();
|
||||||
|
}
|
||||||
|
// 将URL中的/ota/替换为/otaMag/download/
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getOtaIdKey(uuid), ota.getId());
|
||||||
|
downloadUrl = otaUrl.replace("/ota/", "/otaMag/download/") + uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
|
||||||
|
firmware.setUrl(downloadUrl == null ? "" : downloadUrl);
|
||||||
|
return firmware;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个版本号
|
||||||
|
*
|
||||||
|
* @param version1 版本1
|
||||||
|
* @param version2 版本2
|
||||||
|
* @return 如果version1 > version2返回1,version1 < version2返回-1,相等返回0
|
||||||
|
*/
|
||||||
|
private static int compareVersions(String version1, String version2) {
|
||||||
|
if (version1 == null || version2 == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] v1Parts = version1.split("\\.");
|
||||||
|
String[] v2Parts = version2.split("\\.");
|
||||||
|
|
||||||
|
int length = Math.max(v1Parts.length, v2Parts.length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int v1 = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;
|
||||||
|
int v2 = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;
|
||||||
|
|
||||||
|
if (v1 > v2) {
|
||||||
|
return 1;
|
||||||
|
} else if (v1 < v2) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.modules.device.dao.OtaDao;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implements OtaService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<OtaEntity> page(Map<String, Object> params) {
|
||||||
|
IPage<OtaEntity> page = baseDao.selectPage(
|
||||||
|
getPage(params, "update_date", true),
|
||||||
|
getWrapper(params));
|
||||||
|
|
||||||
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryWrapper<OtaEntity> getWrapper(Map<String, Object> params) {
|
||||||
|
String firmwareName = (String) params.get("firmwareName");
|
||||||
|
|
||||||
|
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.like(StringUtils.isNotBlank(firmwareName), "firmware_name", firmwareName);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(OtaEntity entity) {
|
||||||
|
// 检查是否存在相同类型和版本的固件(排除当前记录)
|
||||||
|
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||||
|
.eq("type", entity.getType())
|
||||||
|
.eq("version", entity.getVersion())
|
||||||
|
.ne("id", entity.getId()); // 排除当前记录
|
||||||
|
|
||||||
|
if (baseDao.selectCount(queryWrapper) > 0) {
|
||||||
|
throw new RuntimeException("已存在相同类型和版本的固件,请修改后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.setUpdateDate(new Date());
|
||||||
|
baseDao.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String[] ids) {
|
||||||
|
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean save(OtaEntity entity) {
|
||||||
|
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||||
|
.eq("type", entity.getType());
|
||||||
|
// 同类固件只保留最新的一条
|
||||||
|
List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
|
||||||
|
if (otaList != null && otaList.size() > 0) {
|
||||||
|
OtaEntity otaBefore = otaList.getFirst();
|
||||||
|
entity.setId(otaBefore.getId());
|
||||||
|
baseDao.updateById(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return baseDao.insert(entity) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OtaEntity getLatestOta(String type) {
|
||||||
|
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("type", type)
|
||||||
|
.orderByDesc("update_date")
|
||||||
|
.last("LIMIT 1");
|
||||||
|
return baseDao.selectOne(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package xiaozhi.modules.device.utils;
|
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 网络工具类
|
|
||||||
*/
|
|
||||||
public class NetworkUtil {
|
|
||||||
/**
|
|
||||||
* MAC地址正则表达式
|
|
||||||
*/
|
|
||||||
private static final Pattern macPattern = Pattern.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断MAC地址是否合法
|
|
||||||
*/
|
|
||||||
public static boolean isMacAddressValid(String mac) {
|
|
||||||
if (StringUtils.isBlank(mac)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 正则校验格式
|
|
||||||
if (!macPattern.matcher(mac).matches()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 校验MAC地址是否为单播地址
|
|
||||||
String normalized = mac.toLowerCase();
|
|
||||||
String[] parts = normalized.split("[:-]");
|
|
||||||
int firstByte = Integer.parseInt(parts[0], 16);
|
|
||||||
return (firstByte & 1) == 0; // 最低位为0表示单播地址,合法
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -19,12 +19,17 @@ import lombok.AllArgsConstructor;
|
|||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
|
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
|
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
import xiaozhi.modules.model.service.ModelConfigService;
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
import xiaozhi.modules.model.service.ModelProviderService;
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@RestController
|
@RestController
|
||||||
@@ -33,16 +38,18 @@ import xiaozhi.modules.model.service.ModelProviderService;
|
|||||||
public class ModelController {
|
public class ModelController {
|
||||||
|
|
||||||
private final ModelProviderService modelProviderService;
|
private final ModelProviderService modelProviderService;
|
||||||
|
private final TimbreService timbreService;
|
||||||
private final ModelConfigService modelConfigService;
|
private final ModelConfigService modelConfigService;
|
||||||
|
private final ConfigService configService;
|
||||||
|
private final AgentTemplateService agentTemplateService;
|
||||||
|
|
||||||
@GetMapping("/models/names")
|
@GetMapping("/names")
|
||||||
@Operation(summary = "获取所有模型名称")
|
@Operation(summary = "获取所有模型名称")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<List<String>> getModelNames(@RequestParam String modelType,
|
public Result<List<ModelBasicInfoDTO>> getModelNames(@RequestParam String modelType,
|
||||||
@RequestParam(required = false) String modelName) {
|
@RequestParam(required = false) String modelName) {
|
||||||
List<String> modelNameList = modelConfigService.getModelCodeList(modelType, modelName);
|
List<ModelBasicInfoDTO> modelList = modelConfigService.getModelCodeList(modelType, modelName);
|
||||||
return new Result<List<String>>().ok(modelNameList);
|
return new Result<List<ModelBasicInfoDTO>>().ok(modelList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{modelType}/provideTypes")
|
@GetMapping("/{modelType}/provideTypes")
|
||||||
@@ -53,7 +60,7 @@ public class ModelController {
|
|||||||
return new Result<List<ModelProviderDTO>>().ok(modelProviderDTOS);
|
return new Result<List<ModelProviderDTO>>().ok(modelProviderDTOS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/models/list")
|
@GetMapping("/list")
|
||||||
@Operation(summary = "获取模型配置列表")
|
@Operation(summary = "获取模型配置列表")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<PageData<ModelConfigDTO>> getModelConfigList(
|
public Result<PageData<ModelConfigDTO>> getModelConfigList(
|
||||||
@@ -65,17 +72,18 @@ public class ModelController {
|
|||||||
return new Result<PageData<ModelConfigDTO>>().ok(pageList);
|
return new Result<PageData<ModelConfigDTO>>().ok(pageList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/models/{modelType}/{provideCode}")
|
@PostMapping("/{modelType}/{provideCode}")
|
||||||
@Operation(summary = "新增模型配置")
|
@Operation(summary = "新增模型配置")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<ModelConfigDTO> addModelConfig(@PathVariable String modelType,
|
public Result<ModelConfigDTO> addModelConfig(@PathVariable String modelType,
|
||||||
@PathVariable String provideCode,
|
@PathVariable String provideCode,
|
||||||
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||||
ModelConfigDTO modelConfigDTO = modelConfigService.add(modelType, provideCode, modelConfigBodyDTO);
|
ModelConfigDTO modelConfigDTO = modelConfigService.add(modelType, provideCode, modelConfigBodyDTO);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/models/{modelType}/{provideCode}/{id}")
|
@PutMapping("/{modelType}/{provideCode}/{id}")
|
||||||
@Operation(summary = "编辑模型配置")
|
@Operation(summary = "编辑模型配置")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<ModelConfigDTO> editModelConfig(@PathVariable String modelType,
|
public Result<ModelConfigDTO> editModelConfig(@PathVariable String modelType,
|
||||||
@@ -83,10 +91,11 @@ public class ModelController {
|
|||||||
@PathVariable String id,
|
@PathVariable String id,
|
||||||
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||||
ModelConfigDTO modelConfigDTO = modelConfigService.edit(modelType, provideCode, id, modelConfigBodyDTO);
|
ModelConfigDTO modelConfigDTO = modelConfigService.edit(modelType, provideCode, id, modelConfigBodyDTO);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/models/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@Operation(summary = "删除模型配置")
|
@Operation(summary = "删除模型配置")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<Void> deleteModelConfig(@PathVariable String id) {
|
public Result<Void> deleteModelConfig(@PathVariable String id) {
|
||||||
@@ -94,7 +103,7 @@ public class ModelController {
|
|||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/models/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "获取模型配置")
|
@Operation(summary = "获取模型配置")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<ModelConfigDTO> getModelConfig(@PathVariable String id) {
|
public Result<ModelConfigDTO> getModelConfig(@PathVariable String id) {
|
||||||
@@ -103,13 +112,46 @@ public class ModelController {
|
|||||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/models/{modelId}/voices")
|
@PutMapping("/enable/{id}/{status}")
|
||||||
|
@Operation(summary = "启用/关闭模型配置")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> enableModelConfig(@PathVariable String id, @PathVariable Integer status) {
|
||||||
|
ModelConfigEntity entity = modelConfigService.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<Void>().error("模型配置不存在");
|
||||||
|
}
|
||||||
|
entity.setIsEnabled(status);
|
||||||
|
modelConfigService.updateById(entity);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/default/{id}")
|
||||||
|
@Operation(summary = "设置默认模型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> setDefaultModel(@PathVariable String id) {
|
||||||
|
ModelConfigEntity entity = modelConfigService.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<Void>().error("模型配置不存在");
|
||||||
|
}
|
||||||
|
// 将其他模型设置为非默认
|
||||||
|
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||||
|
entity.setIsEnabled(1);
|
||||||
|
entity.setIsDefault(1);
|
||||||
|
modelConfigService.updateById(entity);
|
||||||
|
|
||||||
|
// 更新模板表中对应的模型ID
|
||||||
|
agentTemplateService.updateDefaultTemplateModelId(entity.getModelType(), entity.getId());
|
||||||
|
|
||||||
|
configService.getConfig(false);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{modelId}/voices")
|
||||||
@Operation(summary = "获取模型音色")
|
@Operation(summary = "获取模型音色")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<List<String>> getVoiceList(@PathVariable String modelId,
|
public Result<List<VoiceDTO>> getVoiceList(@PathVariable String modelId,
|
||||||
@RequestParam(required = false) String voiceName) {
|
@RequestParam(required = false) String voiceName) {
|
||||||
|
List<VoiceDTO> voiceList = timbreService.getVoiceNames(modelId, voiceName);
|
||||||
List<String> voiceList = modelConfigService.getVoiceList(modelId, voiceName);
|
return new Result<List<VoiceDTO>>().ok(voiceList);
|
||||||
return new Result<List<String>>().ok(voiceList);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package xiaozhi.modules.model.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ModelBasicInfoDTO {
|
||||||
|
private String id;
|
||||||
|
private String modelName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.model.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "音色信息")
|
||||||
|
public class VoiceDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "音色ID")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "音色名称")
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -4,13 +4,14 @@ import java.util.List;
|
|||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
|
|
||||||
public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
||||||
|
|
||||||
List<String> getModelCodeList(String modelType, String modelName);
|
List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName);
|
||||||
|
|
||||||
PageData<ModelConfigDTO> getPageList(String modelType, String modelName, String page, String limit);
|
PageData<ModelConfigDTO> getPageList(String modelType, String modelName, String page, String limit);
|
||||||
|
|
||||||
@@ -20,5 +21,28 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
|||||||
|
|
||||||
void delete(String id);
|
void delete(String id);
|
||||||
|
|
||||||
List<String> getVoiceList(String modelName, String voiceName);
|
/**
|
||||||
|
* 根据ID获取模型名称
|
||||||
|
*
|
||||||
|
* @param id 模型ID
|
||||||
|
* @return 模型名称
|
||||||
|
*/
|
||||||
|
String getModelNameById(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取模型配置
|
||||||
|
*
|
||||||
|
* @param id 模型ID
|
||||||
|
* @param isCache 是否缓存
|
||||||
|
* @return 模型配置实体
|
||||||
|
*/
|
||||||
|
ModelConfigEntity getModelById(String id, boolean isCache);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置默认模型
|
||||||
|
*
|
||||||
|
* @param modelType 模型类型
|
||||||
|
* @param isDefault 是否默认
|
||||||
|
*/
|
||||||
|
void setDefaultModel(String modelType, int isDefault);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.service.impl;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -15,16 +16,20 @@ import lombok.AllArgsConstructor;
|
|||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.modules.agent.dao.AgentDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||||
|
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
import xiaozhi.modules.model.service.ModelConfigService;
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
import xiaozhi.modules.model.service.ModelProviderService;
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
import xiaozhi.modules.timbre.service.TimbreService;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -33,11 +38,18 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
|
|
||||||
private final ModelConfigDao modelConfigDao;
|
private final ModelConfigDao modelConfigDao;
|
||||||
private final ModelProviderService modelProviderService;
|
private final ModelProviderService modelProviderService;
|
||||||
private final TimbreService timbreService;
|
private final RedisUtils redisUtils;
|
||||||
|
private final AgentDao agentDao;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getModelCodeList(String modelType, String modelName) {
|
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
|
||||||
return modelConfigDao.getModelCodeList(modelType, modelName);
|
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||||
|
new QueryWrapper<ModelConfigEntity>()
|
||||||
|
.eq("model_type", modelType)
|
||||||
|
.eq("is_enabled", 1)
|
||||||
|
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||||
|
.select("id", "model_name"));
|
||||||
|
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -67,6 +79,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
// 再保存供应器提供的模型
|
// 再保存供应器提供的模型
|
||||||
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||||
modelConfigEntity.setModelType(modelType);
|
modelConfigEntity.setModelType(modelType);
|
||||||
|
modelConfigEntity.setIsDefault(0);
|
||||||
modelConfigDao.insert(modelConfigEntity);
|
modelConfigDao.insert(modelConfigEntity);
|
||||||
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||||
}
|
}
|
||||||
@@ -87,16 +100,118 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
modelConfigEntity.setId(id);
|
modelConfigEntity.setId(id);
|
||||||
modelConfigEntity.setModelType(modelType);
|
modelConfigEntity.setModelType(modelType);
|
||||||
modelConfigDao.updateById(modelConfigEntity);
|
modelConfigDao.updateById(modelConfigEntity);
|
||||||
|
// 清除缓存
|
||||||
|
redisUtils.delete(RedisKeys.getModelConfigById(modelConfigEntity.getId()));
|
||||||
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String id) {
|
public void delete(String id) {
|
||||||
|
// 查看是否是默认
|
||||||
|
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
|
||||||
|
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
|
||||||
|
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
|
||||||
|
}
|
||||||
|
// 验证是否有引用
|
||||||
|
checkAgentReference(id);
|
||||||
|
checkIntentConfigReference(id);
|
||||||
|
|
||||||
modelConfigDao.deleteById(id);
|
modelConfigDao.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查智能体配置是否有引用
|
||||||
|
*
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
private void checkAgentReference(String modelId) {
|
||||||
|
List<AgentEntity> agents = agentDao.selectList(
|
||||||
|
new QueryWrapper<AgentEntity>()
|
||||||
|
.eq("vad_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("asr_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("llm_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("tts_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("mem_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("intent_model_id", modelId));
|
||||||
|
if (!agents.isEmpty()) {
|
||||||
|
String agentNames = agents.stream()
|
||||||
|
.map(AgentEntity::getAgentName)
|
||||||
|
.collect(Collectors.joining("、"));
|
||||||
|
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查意图识别配置是否有引用
|
||||||
|
*
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
private void checkIntentConfigReference(String modelId) {
|
||||||
|
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
|
||||||
|
if (modelConfig != null
|
||||||
|
&& "LLM".equals(modelConfig.getModelType() == null ? null : modelConfig.getModelType().toUpperCase())) {
|
||||||
|
List<ModelConfigEntity> intentConfigs = modelConfigDao.selectList(
|
||||||
|
new QueryWrapper<ModelConfigEntity>()
|
||||||
|
.eq("model_type", "Intent")
|
||||||
|
.like("config_json", "%" + modelId + "%"));
|
||||||
|
if (!intentConfigs.isEmpty()) {
|
||||||
|
throw new RenException("该LLM模型已被意图识别配置引用,无法删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getVoiceList(String modelId, String voiceName) {
|
public String getModelNameById(String id) {
|
||||||
return timbreService.getVoiceNames(modelId, voiceName);
|
if (StringUtils.isBlank(id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cachedName = (String) redisUtils.get(RedisKeys.getModelNameById(id));
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(cachedName)) {
|
||||||
|
return cachedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelConfigEntity entity = modelConfigDao.selectById(id);
|
||||||
|
if (entity != null) {
|
||||||
|
String modelName = entity.getModelName();
|
||||||
|
if (StringUtils.isNotBlank(modelName)) {
|
||||||
|
redisUtils.set(RedisKeys.getModelNameById(id), modelName);
|
||||||
|
}
|
||||||
|
return modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModelConfigEntity getModelById(String id, boolean isCache) {
|
||||||
|
if (StringUtils.isBlank(id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (isCache) {
|
||||||
|
ModelConfigEntity cachedConfig = (ModelConfigEntity) redisUtils.get(RedisKeys.getModelConfigById(id));
|
||||||
|
if (cachedConfig != null) {
|
||||||
|
return ConvertUtils.sourceToTarget(cachedConfig, ModelConfigEntity.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ModelConfigEntity entity = modelConfigDao.selectById(id);
|
||||||
|
if (entity != null) {
|
||||||
|
redisUtils.set(RedisKeys.getModelConfigById(id), entity);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDefaultModel(String modelType, int isDefault) {
|
||||||
|
ModelConfigEntity entity = new ModelConfigEntity();
|
||||||
|
entity.setIsDefault(isDefault);
|
||||||
|
modelConfigDao.update(entity, new QueryWrapper<ModelConfigEntity>()
|
||||||
|
.eq("model_type", modelType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package xiaozhi.modules.security.config;
|
package xiaozhi.modules.security.config;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import jakarta.servlet.Filter;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.shiro.mgt.SecurityManager;
|
import org.apache.shiro.mgt.SecurityManager;
|
||||||
import org.apache.shiro.session.mgt.SessionManager;
|
import org.apache.shiro.session.mgt.SessionManager;
|
||||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||||
@@ -14,10 +11,14 @@ import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
|||||||
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
|
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||||
|
import xiaozhi.modules.security.secret.ServerSecretFilter;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shiro的配置文件
|
* Shiro的配置文件
|
||||||
@@ -46,7 +47,7 @@ public class ShiroConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean("shiroFilter")
|
@Bean("shiroFilter")
|
||||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
|
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
|
||||||
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
||||||
config.setFilterOncePerRequest(true);
|
config.setFilterOncePerRequest(true);
|
||||||
|
|
||||||
@@ -54,9 +55,11 @@ public class ShiroConfig {
|
|||||||
shiroFilter.setSecurityManager(securityManager);
|
shiroFilter.setSecurityManager(securityManager);
|
||||||
shiroFilter.setShiroFilterConfiguration(config);
|
shiroFilter.setShiroFilterConfiguration(config);
|
||||||
|
|
||||||
// oauth过滤
|
|
||||||
Map<String, Filter> filters = new HashMap<>();
|
Map<String, Filter> filters = new HashMap<>();
|
||||||
|
// oauth过滤
|
||||||
filters.put("oauth2", new Oauth2Filter());
|
filters.put("oauth2", new Oauth2Filter());
|
||||||
|
// 服务密钥过滤
|
||||||
|
filters.put("server", new ServerSecretFilter(sysParamsService));
|
||||||
shiroFilter.setFilters(filters);
|
shiroFilter.setFilters(filters);
|
||||||
|
|
||||||
// 添加Shiro的内置过滤器
|
// 添加Shiro的内置过滤器
|
||||||
@@ -69,6 +72,7 @@ public class ShiroConfig {
|
|||||||
*/
|
*/
|
||||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||||
filterMap.put("/ota/**", "anon");
|
filterMap.put("/ota/**", "anon");
|
||||||
|
filterMap.put("/otaMag/download/**", "anon");
|
||||||
filterMap.put("/webjars/**", "anon");
|
filterMap.put("/webjars/**", "anon");
|
||||||
filterMap.put("/druid/**", "anon");
|
filterMap.put("/druid/**", "anon");
|
||||||
filterMap.put("/v3/api-docs/**", "anon");
|
filterMap.put("/v3/api-docs/**", "anon");
|
||||||
@@ -76,8 +80,11 @@ public class ShiroConfig {
|
|||||||
filterMap.put("/favicon.ico", "anon");
|
filterMap.put("/favicon.ico", "anon");
|
||||||
filterMap.put("/user/captcha", "anon");
|
filterMap.put("/user/captcha", "anon");
|
||||||
filterMap.put("/user/login", "anon");
|
filterMap.put("/user/login", "anon");
|
||||||
|
filterMap.put("/user/pub-config", "anon");
|
||||||
filterMap.put("/user/register", "anon");
|
filterMap.put("/user/register", "anon");
|
||||||
filterMap.put("/device/register", "anon");
|
// 将config路径使用server服务过滤器
|
||||||
|
filterMap.put("/config/**", "server");
|
||||||
|
filterMap.put("/agent/chat-history/report", "server");
|
||||||
filterMap.put("/**", "oauth2");
|
filterMap.put("/**", "oauth2");
|
||||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,15 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||||
|
// 特殊用途的转换器
|
||||||
converters.add(new ByteArrayHttpMessageConverter());
|
converters.add(new ByteArrayHttpMessageConverter());
|
||||||
converters.add(new StringHttpMessageConverter());
|
|
||||||
converters.add(new ResourceHttpMessageConverter());
|
converters.add(new ResourceHttpMessageConverter());
|
||||||
converters.add(new AllEncompassingFormHttpMessageConverter());
|
|
||||||
|
// 通用转换器
|
||||||
converters.add(new StringHttpMessageConverter());
|
converters.add(new StringHttpMessageConverter());
|
||||||
|
converters.add(new AllEncompassingFormHttpMessageConverter());
|
||||||
|
|
||||||
|
// JSON 转换器
|
||||||
converters.add(jackson2HttpMessageConverter());
|
converters.add(jackson2HttpMessageConverter());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package xiaozhi.modules.security.controller;
|
package xiaozhi.modules.security.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -13,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.TokenDTO;
|
import xiaozhi.common.page.TokenDTO;
|
||||||
@@ -75,6 +78,9 @@ public class LoginController {
|
|||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@Operation(summary = "注册")
|
@Operation(summary = "注册")
|
||||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||||
|
if (!sysUserService.getAllowUserRegister()) {
|
||||||
|
throw new RenException("当前不允许普通用户注册");
|
||||||
|
}
|
||||||
// 验证是否正确输入验证码
|
// 验证是否正确输入验证码
|
||||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
@@ -111,4 +117,13 @@ public class LoginController {
|
|||||||
sysUserTokenService.changePassword(userId, passwordDTO);
|
sysUserTokenService.changePassword(userId, passwordDTO);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pub-config")
|
||||||
|
@Operation(summary = "公共配置")
|
||||||
|
public Result<Map<String, Object>> pubConfig() {
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.put("version", Constant.VERSION);
|
||||||
|
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||||
|
return new Result<Map<String, Object>>().ok(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package xiaozhi.modules.security.secret;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.utils.HttpContextUtils;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config API 过滤器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ServerSecretFilter extends AuthenticatingFilter {
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) {
|
||||||
|
// 获取请求token
|
||||||
|
String token = getRequestToken((HttpServletRequest) request);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(token)) {
|
||||||
|
log.warn("createToken:token is empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServerSecretToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
||||||
|
// 对OPTIONS请求放行
|
||||||
|
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
|
||||||
|
// 获取token并校验
|
||||||
|
String token = getRequestToken((HttpServletRequest) servletRequest);
|
||||||
|
if (StringUtils.isBlank(token)) {
|
||||||
|
// token为空,返回401
|
||||||
|
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "服务器密钥不能为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token是否匹配
|
||||||
|
String serverSecret = getServerSecret();
|
||||||
|
if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) {
|
||||||
|
// token无效,返回401
|
||||||
|
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的服务器密钥");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送未授权响应
|
||||||
|
*/
|
||||||
|
private void sendUnauthorizedResponse(HttpServletResponse response, String message) {
|
||||||
|
response.setContentType("application/json;charset=utf-8");
|
||||||
|
response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
|
response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = JsonUtils.toJsonString(new Result<Void>().error(ErrorCode.UNAUTHORIZED, message));
|
||||||
|
response.getWriter().print(json);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("响应输出失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求的token
|
||||||
|
*/
|
||||||
|
private String getRequestToken(HttpServletRequest httpRequest) {
|
||||||
|
String token = null;
|
||||||
|
// 从header中获取token
|
||||||
|
String authorization = httpRequest.getHeader("Authorization");
|
||||||
|
if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
|
||||||
|
token = authorization.replace("Bearer ", "");
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getServerSecret() {
|
||||||
|
return sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package xiaozhi.modules.security.secret;
|
||||||
|
|
||||||
|
import org.apache.shiro.authc.AuthenticationToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config API Token
|
||||||
|
*/
|
||||||
|
public class ServerSecretToken implements AuthenticationToken {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final String token;
|
||||||
|
|
||||||
|
public ServerSecretToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,6 @@ public class AdminController {
|
|||||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||||
dto.setPage((String) params.get(Constant.PAGE));
|
dto.setPage((String) params.get(Constant.PAGE));
|
||||||
ValidatorUtils.validateEntity(dto);
|
ValidatorUtils.validateEntity(dto);
|
||||||
ValidatorUtils.validateEntity(dto);
|
|
||||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||||
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||||
}
|
}
|
||||||
@@ -84,7 +83,7 @@ public class AdminController {
|
|||||||
@Operation(summary = "批量修改用户状态")
|
@Operation(summary = "批量修改用户状态")
|
||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
@Parameter(name = "status", description = "用户状态", required = true)
|
@Parameter(name = "status", description = "用户状态", required = true)
|
||||||
public Result<Void> changeStatus(@PathVariable Integer status, @RequestBody Long[] userIds) {
|
public Result<Void> changeStatus(@PathVariable Integer status, @RequestBody String[] userIds) {
|
||||||
sysUserService.changeStatus(status, userIds);
|
sysUserService.changeStatus(status, userIds);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典数据管理
|
||||||
|
*
|
||||||
|
* @author czc
|
||||||
|
* @since 2025-04-30
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/dict/data")
|
||||||
|
@Tag(name = "字典数据管理")
|
||||||
|
public class SysDictDataController {
|
||||||
|
private final SysDictDataService sysDictDataService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "分页查询字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({ @Parameter(name = "dictTypeId", description = "字典类型ID", required = true),
|
||||||
|
@Parameter(name = "dictLabel", description = "数据标签"), @Parameter(name = "dictValue", description = "数据值"),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
|
||||||
|
public Result<PageData<SysDictDataVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
// 强制校验dictTypeId是否存在
|
||||||
|
if (!params.containsKey("dictTypeId") || StringUtils.isEmpty(String.valueOf(params.get("dictTypeId")))) {
|
||||||
|
return new Result<PageData<SysDictDataVO>>().error("dictTypeId不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
PageData<SysDictDataVO> page = sysDictDataService.page(params);
|
||||||
|
return new Result<PageData<SysDictDataVO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取字典数据详情")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<SysDictDataVO> get(@PathVariable("id") Long id) {
|
||||||
|
SysDictDataVO vo = sysDictDataService.get(id);
|
||||||
|
return new Result<SysDictDataVO>().ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@Operation(summary = "新增字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody SysDictDataDTO dto) {
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
sysDictDataService.save(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "修改字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> update(@RequestBody SysDictDataDTO dto) {
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
sysDictDataService.update(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@Operation(summary = "删除字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||||
|
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||||
|
sysDictDataService.delete(ids);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/type/{dictType}")
|
||||||
|
@Operation(summary = "获取字典数据列表")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<List<SysDictDataItem>> getDictDataByType(@PathVariable("dictType") String dictType) {
|
||||||
|
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(dictType);
|
||||||
|
return new Result<List<SysDictDataItem>>().ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典类型管理
|
||||||
|
*
|
||||||
|
* @author czc
|
||||||
|
* @since 2025-04-30
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/dict/type")
|
||||||
|
@Tag(name = "字典类型管理")
|
||||||
|
public class SysDictTypeController {
|
||||||
|
private final SysDictTypeService sysDictTypeService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "分页查询字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({ @Parameter(name = "dictType", description = "字典类型编码"),
|
||||||
|
@Parameter(name = "dictName", description = "字典类型名称"),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
|
||||||
|
public Result<PageData<SysDictTypeVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
PageData<SysDictTypeVO> page = sysDictTypeService.page(params);
|
||||||
|
return new Result<PageData<SysDictTypeVO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取字典类型详情")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<SysDictTypeVO> get(@PathVariable("id") Long id) {
|
||||||
|
SysDictTypeVO vo = sysDictTypeService.get(id);
|
||||||
|
return new Result<SysDictTypeVO>().ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@Operation(summary = "保存字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody SysDictTypeDTO dto) {
|
||||||
|
// 参数校验
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
|
||||||
|
sysDictTypeService.save(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "修改字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> update(@RequestBody SysDictTypeDTO dto) {
|
||||||
|
// 参数校验
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
|
||||||
|
sysDictTypeService.update(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@Operation(summary = "删除字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||||
|
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||||
|
sysDictTypeService.delete(ids);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.annotation.LogOperation;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.AssertUtils;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.common.validator.group.AddGroup;
|
||||||
|
import xiaozhi.common.validator.group.DefaultGroup;
|
||||||
|
import xiaozhi.common.validator.group.UpdateGroup;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
|
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
import xiaozhi.modules.sys.utils.WebSocketValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数管理
|
||||||
|
*
|
||||||
|
* @author Mark sunlightcs@gmail.com
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("admin/params")
|
||||||
|
@Tag(name = "参数管理")
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SysParamsController {
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
private final ConfigService configService;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
@GetMapping("page")
|
||||||
|
@Operation(summary = "分页")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", in = ParameterIn.QUERY, required = true, ref = "int"),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", in = ParameterIn.QUERY, required = true, ref = "int"),
|
||||||
|
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段", in = ParameterIn.QUERY, ref = "String"),
|
||||||
|
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)", in = ParameterIn.QUERY, ref = "String"),
|
||||||
|
@Parameter(name = "paramCode", description = "参数编码", in = ParameterIn.QUERY, ref = "String")
|
||||||
|
})
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<PageData<SysParamsDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
PageData<SysParamsDTO> page = sysParamsService.page(params);
|
||||||
|
|
||||||
|
return new Result<PageData<SysParamsDTO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
@Operation(summary = "信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<SysParamsDTO> get(@PathVariable("id") Long id) {
|
||||||
|
SysParamsDTO data = sysParamsService.get(id);
|
||||||
|
|
||||||
|
return new Result<SysParamsDTO>().ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "保存")
|
||||||
|
@LogOperation("保存")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody SysParamsDTO dto) {
|
||||||
|
// 效验数据
|
||||||
|
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
|
sysParamsService.save(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
@Operation(summary = "修改")
|
||||||
|
@LogOperation("修改")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> update(@RequestBody SysParamsDTO dto) {
|
||||||
|
// 效验数据
|
||||||
|
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
|
// 验证WebSocket地址列表
|
||||||
|
validateWebSocketUrls(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
|
// 验证OTA地址
|
||||||
|
validateOtaUrl(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
|
sysParamsService.update(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证WebSocket地址列表
|
||||||
|
*
|
||||||
|
* @param urls WebSocket地址列表,以分号分隔
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
private void validateWebSocketUrls(String paramCode, String urls) {
|
||||||
|
if (!paramCode.equals(Constant.SERVER_WEBSOCKET)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] wsUrls = urls.split("\\;");
|
||||||
|
if (wsUrls.length == 0) {
|
||||||
|
throw new RenException("WebSocket地址列表不能为空");
|
||||||
|
}
|
||||||
|
for (String url : wsUrls) {
|
||||||
|
if (StringUtils.isNotBlank(url)) {
|
||||||
|
// 检查是否包含localhost或127.0.0.1
|
||||||
|
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||||
|
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证WebSocket地址格式
|
||||||
|
if (!WebSocketValidator.validateUrlFormat(url)) {
|
||||||
|
throw new RenException("WebSocket地址格式不正确: " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试WebSocket连接
|
||||||
|
if (!WebSocketValidator.testConnection(url)) {
|
||||||
|
throw new RenException("WebSocket连接测试失败: " + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证OTA地址
|
||||||
|
*/
|
||||||
|
private void validateOtaUrl(String paramCode, String url) {
|
||||||
|
if (!paramCode.equals(Constant.SERVER_OTA)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||||
|
throw new RenException("OTA地址不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含localhost或127.0.0.1
|
||||||
|
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||||
|
throw new RenException("OTA地址不能使用localhost或127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证URL格式
|
||||||
|
if (!url.toLowerCase().startsWith("http")) {
|
||||||
|
throw new RenException("OTA地址必须以http或https开头");
|
||||||
|
}
|
||||||
|
if (!url.endsWith("/ota/")) {
|
||||||
|
throw new RenException("OTA地址必须以/ota/结尾");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 发送GET请求
|
||||||
|
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||||
|
if (response.getStatusCode() != HttpStatus.OK) {
|
||||||
|
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode());
|
||||||
|
}
|
||||||
|
// 检查响应内容是否包含OTA相关信息
|
||||||
|
String body = response.getBody();
|
||||||
|
if (body == null || !body.contains("OTA")) {
|
||||||
|
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RenException("OTA接口验证失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@Operation(summary = "删除")
|
||||||
|
@LogOperation("删除")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> delete(@RequestBody String[] ids) {
|
||||||
|
// 效验数据
|
||||||
|
AssertUtils.isArrayEmpty(ids, "id");
|
||||||
|
|
||||||
|
sysParamsService.delete(ids);
|
||||||
|
configService.getConfig(false);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,8 @@ import java.util.List;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
|
||||||
import xiaozhi.modules.sys.entity.DictData;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典数据
|
* 字典数据
|
||||||
@@ -15,10 +14,13 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
||||||
|
|
||||||
/**
|
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||||
* 字典数据列表
|
|
||||||
*/
|
|
||||||
List<DictData> getDictDataList();
|
|
||||||
|
|
||||||
List<SysDictDataDTO> getDataByTypeCode(String dictType);
|
/**
|
||||||
|
* 根据字典类型ID获取字典类型编码
|
||||||
|
*
|
||||||
|
* @param dictTypeId 字典类型ID
|
||||||
|
* @return 字典类型编码
|
||||||
|
*/
|
||||||
|
String getTypeByTypeId(Long dictTypeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package xiaozhi.modules.sys.dao;
|
package xiaozhi.modules.sys.dao;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.sys.entity.DictType;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,9 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> {
|
public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> {
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典类型列表
|
|
||||||
*/
|
|
||||||
List<DictType> getDictTypeList();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public interface SysParamsDao extends BaseDao<SysParamsEntity> {
|
|||||||
* @param ids ids
|
* @param ids ids
|
||||||
* @return 返回参数编码列表
|
* @return 返回参数编码列表
|
||||||
*/
|
*/
|
||||||
List<String> getParamCodeList(Long[] ids);
|
List<String> getParamCodeList(String[] ids);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据参数编码,更新value
|
* 根据参数编码,更新value
|
||||||
|
|||||||
@@ -51,5 +51,6 @@ public class SysDictTypeDTO implements Serializable {
|
|||||||
|
|
||||||
@Schema(description = "更新时间")
|
@Schema(description = "更新时间")
|
||||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||||
private Date updateDate;
|
private Date updateDate;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Null;
|
import jakarta.validation.constraints.Null;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import xiaozhi.common.utils.DateUtils;
|
import xiaozhi.common.utils.DateUtils;
|
||||||
import xiaozhi.common.validator.group.AddGroup;
|
import xiaozhi.common.validator.group.AddGroup;
|
||||||
@@ -36,6 +37,11 @@ public class SysParamsDTO implements Serializable {
|
|||||||
@NotBlank(message = "{sysparams.paramvalue.require}", groups = DefaultGroup.class)
|
@NotBlank(message = "{sysparams.paramvalue.require}", groups = DefaultGroup.class)
|
||||||
private String paramValue;
|
private String paramValue;
|
||||||
|
|
||||||
|
@Schema(description = "值类型")
|
||||||
|
@NotBlank(message = "{sysparams.valuetype.require}", groups = DefaultGroup.class)
|
||||||
|
@Pattern(regexp = "^(string|number|boolean|array)$", message = "{sysparams.valuetype.pattern}", groups = DefaultGroup.class)
|
||||||
|
private String valueType;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package xiaozhi.modules.sys.entity;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典数据
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class DictData {
|
|
||||||
@JsonIgnore
|
|
||||||
private Long dictTypeId;
|
|
||||||
private String dictLabel;
|
|
||||||
private String dictValue;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package xiaozhi.modules.sys.entity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典类型
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class DictType {
|
|
||||||
@JsonIgnore
|
|
||||||
private Long id;
|
|
||||||
private String dictType;
|
|
||||||
private List<DictData> dataList = new ArrayList<>();
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ import xiaozhi.common.entity.BaseEntity;
|
|||||||
@TableName("sys_dict_type")
|
@TableName("sys_dict_type")
|
||||||
public class SysDictTypeEntity extends BaseEntity {
|
public class SysDictTypeEntity extends BaseEntity {
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型编码
|
||||||
*/
|
*/
|
||||||
private String dictType;
|
private String dictType;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ public class SysParamsEntity extends BaseEntity {
|
|||||||
* 参数值
|
* 参数值
|
||||||
*/
|
*/
|
||||||
private String paramValue;
|
private String paramValue;
|
||||||
|
/**
|
||||||
|
* 值类型:string-字符串,number-数字,boolean-布尔,array-数组
|
||||||
|
*/
|
||||||
|
private String valueType;
|
||||||
/**
|
/**
|
||||||
* 类型 0:系统参数 1:非系统参数
|
* 类型 0:系统参数 1:非系统参数
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,25 +1,70 @@
|
|||||||
package xiaozhi.modules.sys.service;
|
package xiaozhi.modules.sys.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据字典
|
* 数据字典
|
||||||
*/
|
*/
|
||||||
public interface SysDictDataService extends BaseService<SysDictDataEntity> {
|
public interface SysDictDataService extends BaseService<SysDictDataEntity> {
|
||||||
|
|
||||||
PageData<SysDictDataDTO> page(Map<String, Object> params);
|
/**
|
||||||
|
* 分页查询数据字典信息
|
||||||
|
*
|
||||||
|
* @param params 查询参数,包含分页信息和查询条件
|
||||||
|
* @return 返回数据字典的分页查询结果
|
||||||
|
*/
|
||||||
|
PageData<SysDictDataVO> page(Map<String, Object> params);
|
||||||
|
|
||||||
SysDictDataDTO get(Long id);
|
/**
|
||||||
|
* 根据ID获取数据字典实体
|
||||||
|
*
|
||||||
|
* @param id 数据字典实体的唯一标识
|
||||||
|
* @return 返回数据字典实体的详细信息
|
||||||
|
*/
|
||||||
|
SysDictDataVO get(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存新的数据字典项
|
||||||
|
*
|
||||||
|
* @param dto 数据字典项的保存数据传输对象
|
||||||
|
*/
|
||||||
void save(SysDictDataDTO dto);
|
void save(SysDictDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据字典项
|
||||||
|
*
|
||||||
|
* @param dto 数据字典项的更新数据传输对象
|
||||||
|
*/
|
||||||
void update(SysDictDataDTO dto);
|
void update(SysDictDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据字典项
|
||||||
|
*
|
||||||
|
* @param ids 要删除的数据字典项的ID数组
|
||||||
|
*/
|
||||||
void delete(Long[] ids);
|
void delete(Long[] ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典类型ID删除对应的字典数据
|
||||||
|
*
|
||||||
|
* @param dictTypeId 字典类型ID
|
||||||
|
*/
|
||||||
|
void deleteByTypeId(Long dictTypeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典类型获取字典数据列表
|
||||||
|
*
|
||||||
|
* @param dictType 字典类型
|
||||||
|
* @return 返回字典数据列表
|
||||||
|
*/
|
||||||
|
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,25 +6,55 @@ import java.util.Map;
|
|||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||||
import xiaozhi.modules.sys.entity.DictType;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据字典
|
* 数据字典
|
||||||
*/
|
*/
|
||||||
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
||||||
|
|
||||||
PageData<SysDictTypeDTO> page(Map<String, Object> params);
|
/**
|
||||||
|
* 分页查询字典类型信息
|
||||||
|
*
|
||||||
|
* @param params 查询参数,包含分页信息和查询条件
|
||||||
|
* @return 返回分页的字典类型数据
|
||||||
|
*/
|
||||||
|
PageData<SysDictTypeVO> page(Map<String, Object> params);
|
||||||
|
|
||||||
SysDictTypeDTO get(Long id);
|
/**
|
||||||
|
* 根据ID获取字典类型信息
|
||||||
|
*
|
||||||
|
* @param id 字典类型ID
|
||||||
|
* @return 返回字典类型对象
|
||||||
|
*/
|
||||||
|
SysDictTypeVO get(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存字典类型信息
|
||||||
|
*
|
||||||
|
* @param dto 字典类型数据传输对象
|
||||||
|
*/
|
||||||
void save(SysDictTypeDTO dto);
|
void save(SysDictTypeDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新字典类型信息
|
||||||
|
*
|
||||||
|
* @param dto 字典类型数据传输对象
|
||||||
|
*/
|
||||||
void update(SysDictTypeDTO dto);
|
void update(SysDictTypeDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除字典类型信息
|
||||||
|
*
|
||||||
|
* @param ids 要删除的字典类型ID数组
|
||||||
|
*/
|
||||||
void delete(Long[] ids);
|
void delete(Long[] ids);
|
||||||
|
|
||||||
List<DictType> getAllList();
|
/**
|
||||||
|
* 列出所有字典类型信息
|
||||||
List<DictType> getDictTypeList();
|
*
|
||||||
|
* @return 返回字典类型列表
|
||||||
|
*/
|
||||||
|
List<SysDictTypeVO> list(Map<String, Object> params);
|
||||||
}
|
}
|
||||||
@@ -23,14 +23,15 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
|
|||||||
|
|
||||||
void update(SysParamsDTO dto);
|
void update(SysParamsDTO dto);
|
||||||
|
|
||||||
void delete(Long[] ids);
|
void delete(String[] ids);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据参数编码,获取参数的value值
|
* 根据参数编码,获取参数的value值
|
||||||
*
|
*
|
||||||
* @param paramCode 参数编码
|
* @param paramCode 参数编码
|
||||||
|
* @param fromCache 是否从缓存中获取
|
||||||
*/
|
*/
|
||||||
String getValue(String paramCode);
|
String getValue(String paramCode, Boolean fromCache);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据参数编码,获取value的Object对象
|
* 根据参数编码,获取value的Object对象
|
||||||
@@ -47,4 +48,9 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
|
|||||||
* @param paramValue 参数值
|
* @param paramValue 参数值
|
||||||
*/
|
*/
|
||||||
int updateValueByCode(String paramCode, String paramValue);
|
int updateValueByCode(String paramCode, String paramValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化服务器密钥
|
||||||
|
*/
|
||||||
|
void initServerSecret();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,5 +64,12 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
|||||||
* @param status 用户状态
|
* @param status 用户状态
|
||||||
* @param userIds 用户ID数组
|
* @param userIds 用户ID数组
|
||||||
*/
|
*/
|
||||||
void changeStatus(Integer status, Long[] userIds);
|
void changeStatus(Integer status, String[] userIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否允许用户注册
|
||||||
|
*
|
||||||
|
* @return 是否允许用户注册
|
||||||
|
*/
|
||||||
|
boolean getAllowUserRegister();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,55 @@
|
|||||||
package xiaozhi.modules.sys.service.impl;
|
package xiaozhi.modules.sys.service.impl;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||||
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
|
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
|
||||||
implements SysDictDataService {
|
implements SysDictDataService {
|
||||||
|
private final SysUserDao sysUserDao;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<SysDictDataDTO> page(Map<String, Object> params) {
|
public PageData<SysDictDataVO> page(Map<String, Object> params) {
|
||||||
IPage<SysDictDataEntity> page = baseDao.selectPage(
|
IPage<SysDictDataEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
|
||||||
getPage(params, "sort", true),
|
|
||||||
getWrapper(params));
|
|
||||||
|
|
||||||
return getPageData(page, SysDictDataDTO.class);
|
PageData<SysDictDataVO> pageData = getPageData(page, SysDictDataVO.class);
|
||||||
|
|
||||||
|
setUserName(pageData.getList());
|
||||||
|
|
||||||
|
return pageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) {
|
private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) {
|
||||||
@@ -48,32 +66,118 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysDictDataDTO get(Long id) {
|
public SysDictDataVO get(Long id) {
|
||||||
SysDictDataEntity entity = baseDao.selectById(id);
|
SysDictDataEntity entity = baseDao.selectById(id);
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entity, SysDictDataDTO.class);
|
return ConvertUtils.sourceToTarget(entity, SysDictDataVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void save(SysDictDataDTO dto) {
|
public void save(SysDictDataDTO dto) {
|
||||||
|
// 相同字典类型的标签不能相同
|
||||||
|
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), null);
|
||||||
|
|
||||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||||
|
|
||||||
insert(entity);
|
insert(entity);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void update(SysDictDataDTO dto) {
|
public void update(SysDictDataDTO dto) {
|
||||||
|
// 相同字典类型的标签不能相同
|
||||||
|
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), String.valueOf(dto.getId()));
|
||||||
|
|
||||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||||
|
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void delete(Long[] ids) {
|
||||||
// 删除
|
for (Long id : ids) {
|
||||||
deleteBatchIds(Arrays.asList(ids));
|
SysDictDataEntity entity = baseDao.selectById(id);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(entity.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
|
// 删除
|
||||||
|
deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteByTypeId(Long dictTypeId) {
|
||||||
|
LambdaQueryWrapper<SysDictDataEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId);
|
||||||
|
baseDao.delete(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户名
|
||||||
|
*
|
||||||
|
* @param sysDictDataList 字典类型集合
|
||||||
|
*/
|
||||||
|
private void setUserName(List<SysDictDataVO> sysDictDataList) {
|
||||||
|
// 收集所有用户 ID
|
||||||
|
Set<Long> userIds = sysDictDataList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 设置更新者和创建者名称
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||||
|
// 把List转成Map,Map<Long, String>
|
||||||
|
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||||
|
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||||
|
|
||||||
|
sysDictDataList.forEach(vo -> {
|
||||||
|
vo.setCreatorName(userNameMap.get(vo.getCreator()));
|
||||||
|
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDictValueUnique(Long dictTypeId, String dictValue, String excludeId) {
|
||||||
|
LambdaQueryWrapper<SysDictDataEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId).eq(SysDictDataEntity::getDictLabel, dictValue);
|
||||||
|
if (StringUtils.isNotBlank(excludeId)) {
|
||||||
|
queryWrapper.ne(SysDictDataEntity::getId, excludeId);
|
||||||
|
}
|
||||||
|
boolean exists = baseDao.exists(queryWrapper);
|
||||||
|
if (exists) {
|
||||||
|
throw new RenException("字典标签重复");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SysDictDataItem> getDictDataByType(String dictType) {
|
||||||
|
if (StringUtils.isBlank(dictType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先从Redis获取缓存
|
||||||
|
String key = RedisKeys.getDictDataByTypeKey(dictType);
|
||||||
|
List<SysDictDataItem> cachedData = (List<SysDictDataItem>) redisUtils.get(key);
|
||||||
|
if (cachedData != null) {
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果缓存中没有,则从数据库获取
|
||||||
|
List<SysDictDataItem> data = baseDao.getDictDataByType(dictType);
|
||||||
|
|
||||||
|
// 存入Redis缓存
|
||||||
|
if (data != null) {
|
||||||
|
redisUtils.set(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,42 +3,52 @@ package xiaozhi.modules.sys.service.impl;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
|
||||||
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
||||||
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
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.entity.SysDictTypeEntity;
|
||||||
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
import xiaozhi.modules.sys.service.SysDictTypeService;
|
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型
|
||||||
*/
|
*/
|
||||||
@AllArgsConstructor
|
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
|
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
|
||||||
implements SysDictTypeService {
|
implements SysDictTypeService {
|
||||||
private final SysDictDataDao sysDictDataDao;
|
private final SysUserDao sysUserDao;
|
||||||
|
private final SysDictDataService sysDictDataService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<SysDictTypeDTO> page(Map<String, Object> params) {
|
public PageData<SysDictTypeVO> page(Map<String, Object> params) {
|
||||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(
|
IPage<SysDictTypeEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
|
||||||
getPage(params, "sort", true),
|
|
||||||
getWrapper(params));
|
|
||||||
|
|
||||||
return getPageData(page, SysDictTypeDTO.class);
|
PageData<SysDictTypeVO> pageData = getPageData(page, SysDictTypeVO.class);
|
||||||
|
|
||||||
|
setUserName(pageData.getList());
|
||||||
|
|
||||||
|
return pageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
||||||
@@ -53,15 +63,21 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysDictTypeDTO get(Long id) {
|
public SysDictTypeVO get(Long id) {
|
||||||
SysDictTypeEntity entity = baseDao.selectById(id);
|
SysDictTypeEntity entity = baseDao.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new RenException("字典类型不存在");
|
||||||
|
}
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class);
|
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void save(SysDictTypeDTO dto) {
|
public void save(SysDictTypeDTO dto) {
|
||||||
|
// 字典类型编码不能重复
|
||||||
|
checkDictTypeUnique(dto.getDictType(), null);
|
||||||
|
|
||||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||||
|
|
||||||
insert(entity);
|
insert(entity);
|
||||||
@@ -70,6 +86,9 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void update(SysDictTypeDTO dto) {
|
public void update(SysDictTypeDTO dto) {
|
||||||
|
// 字典类型编码不能重复
|
||||||
|
checkDictTypeUnique(dto.getDictType(), String.valueOf(dto.getId()));
|
||||||
|
|
||||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||||
|
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
@@ -78,27 +97,57 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void delete(Long[] ids) {
|
||||||
// 删除
|
// 先删除对应的字典数据
|
||||||
|
for (Long id : ids) {
|
||||||
|
sysDictDataService.deleteByTypeId(id);
|
||||||
|
}
|
||||||
|
// 再删除字典类型
|
||||||
deleteBatchIds(Arrays.asList(ids));
|
deleteBatchIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<DictType> getAllList() {
|
public List<SysDictTypeVO> list(Map<String, Object> params) {
|
||||||
List<DictType> typeList = baseDao.getDictTypeList();
|
List<SysDictTypeEntity> entityList = baseDao.selectList(getWrapper(params));
|
||||||
List<DictData> dataList = sysDictDataDao.getDictDataList();
|
List<SysDictTypeVO> sysDictTypeVOList = ConvertUtils.sourceToTarget(entityList, SysDictTypeVO.class);
|
||||||
for (DictType type : typeList) {
|
// 设置用户名
|
||||||
for (DictData data : dataList) {
|
setUserName(sysDictTypeVOList);
|
||||||
if (type.getId().equals(data.getDictTypeId())) {
|
|
||||||
type.getDataList().add(data);
|
return sysDictTypeVOList;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 设置用户名
|
||||||
|
*
|
||||||
|
* @param sysDictTypeList 字典类型集合
|
||||||
|
*/
|
||||||
|
private void setUserName(List<SysDictTypeVO> sysDictTypeList) {
|
||||||
|
// 收集所有用户 ID
|
||||||
|
Set<Long> userIds = sysDictTypeList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 设置更新者和创建者名称
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||||
|
// 把List转成Map,Map<Long, String>
|
||||||
|
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||||
|
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||||
|
|
||||||
|
sysDictTypeList.forEach(vo -> {
|
||||||
|
vo.setCreatorName(userNameMap.get(vo.getCreator()));
|
||||||
|
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return typeList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void checkDictTypeUnique(String dictType, String excludeId) {
|
||||||
public List<DictType> getDictTypeList() {
|
LambdaQueryWrapper<SysDictTypeEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
return baseDao.getDictTypeList();
|
queryWrapper.eq(SysDictTypeEntity::getDictType, dictType);
|
||||||
|
if (StringUtils.isNotBlank(excludeId)) {
|
||||||
|
queryWrapper.ne(SysDictTypeEntity::getId, excludeId);
|
||||||
|
}
|
||||||
|
boolean exists = baseDao.exists(queryWrapper);
|
||||||
|
if (exists) {
|
||||||
|
throw new RenException("字典类型编码重复");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.sys.service.impl;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -36,7 +37,7 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
@Override
|
@Override
|
||||||
public PageData<SysParamsDTO> page(Map<String, Object> params) {
|
public PageData<SysParamsDTO> page(Map<String, Object> params) {
|
||||||
IPage<SysParamsEntity> page = baseDao.selectPage(
|
IPage<SysParamsEntity> page = baseDao.selectPage(
|
||||||
getPage(params, Constant.CREATE_DATE, false),
|
getPage(params, null, false),
|
||||||
getWrapper(params));
|
getWrapper(params));
|
||||||
|
|
||||||
return getPageData(page, SysParamsDTO.class);
|
return getPageData(page, SysParamsDTO.class);
|
||||||
@@ -69,6 +70,8 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void save(SysParamsDTO dto) {
|
public void save(SysParamsDTO dto) {
|
||||||
|
validateParamValue(dto);
|
||||||
|
|
||||||
SysParamsEntity entity = ConvertUtils.sourceToTarget(dto, SysParamsEntity.class);
|
SysParamsEntity entity = ConvertUtils.sourceToTarget(dto, SysParamsEntity.class);
|
||||||
insert(entity);
|
insert(entity);
|
||||||
|
|
||||||
@@ -78,38 +81,95 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void update(SysParamsDTO dto) {
|
public void update(SysParamsDTO dto) {
|
||||||
|
validateParamValue(dto);
|
||||||
|
|
||||||
SysParamsEntity entity = ConvertUtils.sourceToTarget(dto, SysParamsEntity.class);
|
SysParamsEntity entity = ConvertUtils.sourceToTarget(dto, SysParamsEntity.class);
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
|
|
||||||
sysParamsRedis.set(entity.getParamCode(), entity.getParamValue());
|
sysParamsRedis.set(entity.getParamCode(), entity.getParamValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验参数值类型
|
||||||
|
*/
|
||||||
|
private void validateParamValue(SysParamsDTO dto) {
|
||||||
|
if (dto == null) {
|
||||||
|
throw new RenException(ErrorCode.PARAM_VALUE_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(dto.getParamValue())) {
|
||||||
|
throw new RenException(ErrorCode.PARAM_VALUE_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(dto.getValueType())) {
|
||||||
|
throw new RenException(ErrorCode.PARAM_TYPE_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
String valueType = dto.getValueType().toLowerCase();
|
||||||
|
String paramValue = dto.getParamValue();
|
||||||
|
|
||||||
|
switch (valueType) {
|
||||||
|
case "string":
|
||||||
|
break;
|
||||||
|
case "array":
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
try {
|
||||||
|
Double.parseDouble(paramValue);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new RenException(ErrorCode.PARAM_NUMBER_INVALID);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "boolean":
|
||||||
|
if (!"true".equalsIgnoreCase(paramValue) && !"false".equalsIgnoreCase(paramValue)) {
|
||||||
|
throw new RenException(ErrorCode.PARAM_BOOLEAN_INVALID);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "json":
|
||||||
|
try {
|
||||||
|
JsonUtils.parseObject(paramValue, Object.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new RenException(ErrorCode.PARAM_TYPE_INVALID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void delete(String[] ids) {
|
||||||
// 删除Redis数据
|
// 删除Redis数据
|
||||||
List<String> paramCodeList = baseDao.getParamCodeList(ids);
|
List<String> paramCodeList = baseDao.getParamCodeList(ids);
|
||||||
String[] paramCodes = paramCodeList.toArray(new String[paramCodeList.size()]);
|
String[] paramCodes = paramCodeList.toArray(new String[paramCodeList.size()]);
|
||||||
sysParamsRedis.delete(paramCodes);
|
if (paramCodes.length > 0) {
|
||||||
|
sysParamsRedis.delete(paramCodes);
|
||||||
|
}
|
||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
deleteBatchIds(Arrays.asList(ids));
|
deleteBatchIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getValue(String paramCode) {
|
public String getValue(String paramCode, Boolean fromCache) {
|
||||||
String paramValue = sysParamsRedis.get(paramCode);
|
String paramValue = null;
|
||||||
if (paramValue == null) {
|
if (fromCache) {
|
||||||
paramValue = baseDao.getValueByCode(paramCode);
|
paramValue = sysParamsRedis.get(paramCode);
|
||||||
|
if (paramValue == null) {
|
||||||
|
paramValue = baseDao.getValueByCode(paramCode);
|
||||||
|
|
||||||
sysParamsRedis.set(paramCode, paramValue);
|
sysParamsRedis.set(paramCode, paramValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paramValue = baseDao.getValueByCode(paramCode);
|
||||||
}
|
}
|
||||||
return paramValue;
|
return paramValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <T> T getValueObject(String paramCode, Class<T> clazz) {
|
public <T> T getValueObject(String paramCode, Class<T> clazz) {
|
||||||
String paramValue = getValue(paramCode);
|
String paramValue = getValue(paramCode, true);
|
||||||
if (StringUtils.isNotBlank(paramValue)) {
|
if (StringUtils.isNotBlank(paramValue)) {
|
||||||
return JsonUtils.parseObject(paramValue, clazz);
|
return JsonUtils.parseObject(paramValue, clazz);
|
||||||
}
|
}
|
||||||
@@ -129,4 +189,13 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initServerSecret() {
|
||||||
|
// 获取服务器密钥
|
||||||
|
String secretParam = getValue(Constant.SERVER_SECRET, false);
|
||||||
|
if (StringUtils.isBlank(secretParam) || "null".equals(secretParam)) {
|
||||||
|
String newSecret = UUID.randomUUID().toString();
|
||||||
|
updateValueByCode(Constant.SERVER_SECRET, newSecret);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||