Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8225a5a12f | ||
|
|
5655362a00 | ||
|
|
2f2884e991 | ||
|
|
6061a1d2bf | ||
|
|
a28d316ca4 | ||
|
|
d53230245f | ||
|
|
d2e8d1c261 | ||
|
|
fe86e1fb0f | ||
|
|
03441c8582 | ||
|
|
d255edfd5f | ||
|
|
52187daafa | ||
|
|
6f46776484 | ||
|
|
50b2b37bc1 | ||
|
|
35eed6dca0 | ||
|
|
e337dd920d | ||
|
|
671c992f3a | ||
|
|
b5ff4d329b | ||
|
|
941eb063fb | ||
|
|
148e71985f | ||
|
|
efd22b36ef | ||
|
|
70ea24ac83 | ||
|
|
3c9b7ad60b | ||
|
|
18cc1273f6 | ||
|
|
a6f9f0d88c | ||
|
|
d36210ad6b | ||
|
|
b2262d062c | ||
|
|
27be7fd217 | ||
|
|
97f210c9de | ||
|
|
a230fc6aaa | ||
|
|
b042a34726 | ||
|
|
1673ad7e70 | ||
|
|
f7eab31b57 | ||
|
|
4553188739 | ||
|
|
7550ac327e | ||
|
|
5b28cf6c66 | ||
|
|
4ea27cbdc2 | ||
|
|
e474a2114c | ||
|
|
06e4834e27 | ||
|
|
083e7071f0 | ||
|
|
14be24a515 | ||
|
|
fbdfd8df69 | ||
|
|
17b5308515 | ||
|
|
8f4c0901f0 | ||
|
|
e18065a1bf | ||
|
|
ad0493f8a0 | ||
|
|
b85368f8d4 | ||
|
|
18ea1e7acf | ||
|
|
50d7627b81 | ||
|
|
5934cf718d | ||
|
|
c57ab0e703 | ||
|
|
2fb7281d07 | ||
|
|
4c0d55d2e6 | ||
|
|
a1904d4a80 | ||
|
|
8856f7efa6 | ||
|
|
bdedc48aca | ||
|
|
8a6b4e5499 | ||
|
|
f7a6de912c | ||
|
|
48fa48c159 | ||
|
|
c811beadb4 | ||
|
|
4c76ad2ba7 | ||
|
|
0a63b00199 | ||
|
|
0761d5a8c2 | ||
|
|
f6ad17729f | ||
|
|
1f9f564def | ||
|
|
198755c3f5 | ||
|
|
d2b6d5f083 | ||
|
|
444e3192a2 | ||
|
|
c12d3df073 | ||
|
|
82b1548ae0 | ||
|
|
afd1d2ae6a | ||
|
|
90aafe1b5f | ||
|
|
143d33e285 | ||
|
|
09938e0aa6 | ||
|
|
fd59aff4d0 | ||
|
|
59133b9d8d | ||
|
|
cca42309c4 | ||
|
|
510ee1b282 | ||
|
|
c4d3020a1d | ||
|
|
132174568f | ||
|
|
f3af30ae53 | ||
|
|
6d3f270a8f | ||
|
|
86621661df | ||
|
|
d668917694 | ||
|
|
8bc1d09c4a | ||
|
|
76b90c179b | ||
|
|
ee17d9416a | ||
|
|
7c389a0641 | ||
|
|
4241120853 | ||
|
|
5998fc3edd | ||
|
|
392f8a1ce0 | ||
|
|
04203c4103 | ||
|
|
188cd7f023 | ||
|
|
8d17f5bd3c | ||
|
|
9d39880e9d | ||
|
|
f27e213d13 | ||
|
|
69ecb2c503 | ||
|
|
0585a0732f | ||
|
|
fab4adb07e | ||
|
|
5c3ae278be | ||
|
|
19a067f4e8 | ||
|
|
9430094072 | ||
|
|
a7b5e0df6c | ||
|
|
83e9725d02 | ||
|
|
086dea8594 | ||
|
|
50490b70b7 | ||
|
|
fac3a0acd7 | ||
|
|
73c4d6a604 | ||
|
|
6ba63db47f | ||
|
|
d3c8e4badb | ||
|
|
fdd16ad3c7 | ||
|
|
9178faef6d | ||
|
|
665318164e | ||
|
|
44f8254b43 | ||
|
|
e085fb1b10 | ||
|
|
48ed5d5c9b | ||
|
|
e2da300e09 | ||
|
|
e11a37ffec | ||
|
|
20c1f0576b | ||
|
|
fef8cece90 | ||
|
|
f549f26541 | ||
|
|
9ba836b814 | ||
|
|
26e471616c | ||
|
|
f20c17f27d | ||
|
|
fd188f6f65 | ||
|
|
d10ba456d7 | ||
|
|
ee3808dd2f | ||
|
|
a345d41e9e | ||
|
|
d36ea8a906 | ||
|
|
28f8533d0a | ||
|
|
587b9e8800 | ||
|
|
d2d97149fc | ||
|
|
c3a1b0f609 | ||
|
|
1b25ed5903 | ||
|
|
e87c96300b | ||
|
|
ba5bf33b5a | ||
|
|
94e1c09647 | ||
|
|
6bd7ad9413 | ||
|
|
d75032e853 | ||
|
|
261ecfea4d | ||
|
|
6f9cd94e66 | ||
|
|
ee2e1ed5fc | ||
|
|
92c1317d4c | ||
|
|
cfb97a087b | ||
|
|
76ba99aa6c | ||
|
|
330d7ed95c | ||
|
|
878e73524b | ||
|
|
13b0aa1a7b | ||
|
|
326342b834 | ||
|
|
8bbd610f12 | ||
|
|
549c2af988 | ||
|
|
d02c56a0e0 | ||
|
|
1f3654cc01 | ||
|
|
b6b6b0310f | ||
|
|
4c91bffcc7 | ||
|
|
d730a6cd06 | ||
|
|
67050f972e | ||
|
|
ffafd32693 | ||
|
|
ab4ec5e468 | ||
|
|
a07277feb8 | ||
|
|
a3cd4b6e66 | ||
|
|
25b7e76407 | ||
|
|
630cb13066 | ||
|
|
81188a0129 | ||
|
|
cf1abd90a0 | ||
|
|
6865a8be45 | ||
|
|
b63fcc80a1 | ||
|
|
a19ba6d48b | ||
|
|
70b6efe922 | ||
|
|
0b07269346 | ||
|
|
336954eeeb | ||
|
|
2988e5eef2 | ||
|
|
fe87889ccd | ||
|
|
0598ab4400 | ||
|
|
7d090f8ecc | ||
|
|
088ee47b3c | ||
|
|
a320b4c001 | ||
|
|
3adce95340 | ||
|
|
2a6bfd792a | ||
|
|
0816f1f589 | ||
|
|
497def8055 | ||
|
|
62be26ff47 | ||
|
|
6a81d80778 | ||
|
|
8914beae26 | ||
|
|
60c38a8dea | ||
|
|
4e0934debf | ||
|
|
17126303fa | ||
|
|
98fa95190c | ||
|
|
3a2261107c | ||
|
|
b1b64abed9 | ||
|
|
05356b6652 | ||
|
|
cbd0c6615d | ||
|
|
90e389efa2 | ||
|
|
00a0b26797 | ||
|
|
69b5ffe3bf | ||
|
|
31539294eb | ||
|
|
d1a2307134 | ||
|
|
97b777f1b2 | ||
|
|
c565bfc6bc | ||
|
|
a63137abc9 | ||
|
|
3ca06f8d50 | ||
|
|
e5b0e202be | ||
|
|
41db6aafd9 | ||
|
|
c66385d1f7 | ||
|
|
b0304fd875 | ||
|
|
5bfc3efc9c | ||
|
|
62845cfcf1 | ||
|
|
5c536e72df | ||
|
|
7cfece1592 | ||
|
|
f393b2c9ba | ||
|
|
164f2e5472 | ||
|
|
1434ecbc2e | ||
|
|
c2e443d4ac | ||
|
|
c079125458 | ||
|
|
4c1d7b34b9 | ||
|
|
53772b3fe3 | ||
|
|
28d3c40835 | ||
|
|
a01faecd0c | ||
|
|
a1b071e38e | ||
|
|
571622a0f4 | ||
|
|
4ab84d7209 | ||
|
|
4b90e70aad | ||
|
|
30a938e348 | ||
|
|
f6f4384e42 | ||
|
|
911b11211e | ||
|
|
ee0da9fc3a | ||
|
|
8bb596c74f | ||
|
|
d0fdeb33e0 | ||
|
|
ae7f41032e | ||
|
|
f30cb6badb | ||
|
|
97a9c1e08a | ||
|
|
1e699c027d | ||
|
|
9bd731729d | ||
|
|
dc17eccf76 | ||
|
|
3649a33afb | ||
|
|
d76bcc1366 | ||
|
|
2987f1c680 | ||
|
|
ab93ee0406 | ||
|
|
69dd933a7d | ||
|
|
5440de0453 | ||
|
|
f2e130c70e | ||
|
|
4c845c26f7 | ||
|
|
a43acca375 | ||
|
|
635520aa85 | ||
|
|
fce9759e93 | ||
|
|
18243cd4b0 | ||
|
|
75a6e51e68 | ||
|
|
312edd4b15 | ||
|
|
be3e67b2c1 | ||
|
|
a5765f81fe | ||
|
|
4bee92abb0 | ||
|
|
d9dcb90745 | ||
|
|
fbd25353d5 | ||
|
|
44c3e73577 | ||
|
|
9cbad1a448 | ||
|
|
3a3ddbc2bc | ||
|
|
63a3c33df9 | ||
|
|
32d7400f7d | ||
|
|
38191f976b | ||
|
|
c380e0e110 | ||
|
|
2199b8eca7 | ||
|
|
88455604a2 | ||
|
|
63eb69091f | ||
|
|
fc668810bd | ||
|
|
2773e3a48a | ||
|
|
18a03f6ccc | ||
|
|
e9348f5512 | ||
|
|
ce65226ea4 | ||
|
|
a3b3079b63 | ||
|
|
22de4bb13b | ||
|
|
eef7bb0cad | ||
|
|
8f1fbbdeef | ||
|
|
f3562e091f | ||
|
|
2001258ef4 | ||
|
|
52a66ea304 | ||
|
|
21ddb1541f | ||
|
|
a3ec6d79b0 | ||
|
|
bb8c315ffc | ||
|
|
f2ae398d3f | ||
|
|
02e33f68b4 | ||
|
|
8c4c9d88da | ||
|
|
49cb98a606 | ||
|
|
bdd18b384b | ||
|
|
32ab716f0d | ||
|
|
fe24529fce | ||
|
|
b59c097bca | ||
|
|
6d6414eb62 | ||
|
|
5b1c20d625 | ||
|
|
af6c8ab935 | ||
|
|
4acb5f924a | ||
|
|
b1b99f1066 | ||
|
|
004ee6f143 | ||
|
|
19455f89d3 | ||
|
|
18d98e3bd0 | ||
|
|
af038b7fc8 | ||
|
|
4a9297dc65 | ||
|
|
49fefb41a6 | ||
|
|
2fa64dfdd0 | ||
|
|
e0f777b9b4 | ||
|
|
dfe3de14d8 | ||
|
|
abe234c7ce | ||
|
|
d7bf67fd0e | ||
|
|
8218996f04 | ||
|
|
ed91e5d607 | ||
|
|
0bf0b0d381 | ||
|
|
d88559f5de | ||
|
|
f398115df3 | ||
|
|
82413fecab | ||
|
|
66352de1eb | ||
|
|
a7730f6e93 | ||
|
|
d5a5b24223 | ||
|
|
472dba2665 | ||
|
|
a5444b11b3 | ||
|
|
5a05a68524 | ||
|
|
36be61bbe1 | ||
|
|
ec6ab983f7 | ||
|
|
69630c2be0 | ||
|
|
8e605dd1d4 | ||
|
|
8bb0cf02ad | ||
|
|
60c054f457 | ||
|
|
5648ee974a | ||
|
|
dc4968f961 | ||
|
|
36d1370720 | ||
|
|
7ead5537f6 | ||
|
|
7fe797d44d | ||
|
|
a1e959420d | ||
|
|
4c99b40257 | ||
|
|
ead0958d18 | ||
|
|
220af8068e | ||
|
|
a1730b2d52 | ||
|
|
1a1d6c0835 | ||
|
|
06cbffdea4 | ||
|
|
4409f37000 | ||
|
|
c45e1faf42 | ||
|
|
732d42f174 | ||
|
|
524fde99f8 | ||
|
|
649246dc6a | ||
|
|
6c7ac25388 | ||
|
|
cb1d6ddeec | ||
|
|
58313889ee | ||
|
|
5ebc62a084 | ||
|
|
fcba384a15 | ||
|
|
3ea95ffa48 | ||
|
|
898fd36eca | ||
|
|
6cd88fb98f | ||
|
|
396de72c5d | ||
|
|
9932d60dee | ||
|
|
1ad93ce1fd | ||
|
|
2041513676 | ||
|
|
3ea1a2bf2f | ||
|
|
2b81ebca8e | ||
|
|
27df97d710 | ||
|
|
a25b7b9b3f | ||
|
|
de9809ca5a | ||
|
|
8504c181c0 | ||
|
|
8a005ffb00 | ||
|
|
80ffa7a577 | ||
|
|
ca8f9a7e4a | ||
|
|
8d19d78297 | ||
|
|
483e5eb85a | ||
|
|
a4b4f491d2 | ||
|
|
08093f509a | ||
|
|
9a96223d12 | ||
|
|
0c2b194444 | ||
|
|
a06055168f | ||
|
|
e04546a9ae | ||
|
|
f6da1a045d | ||
|
|
acd386bb09 | ||
|
|
45225c0ce1 | ||
|
|
0bd884d309 | ||
|
|
aea2ae8d79 | ||
|
|
99ec5ab161 | ||
|
|
4431e7470f | ||
|
|
ed775405c6 | ||
|
|
4aefbe5aa8 | ||
|
|
786c467464 | ||
|
|
50182ef940 | ||
|
|
bf3aa2df09 | ||
|
|
0b744a2599 | ||
|
|
a75b8b40e2 | ||
|
|
2e23c08608 | ||
|
|
178f45ee26 | ||
|
|
7b16edc99c | ||
|
|
f3738a1630 | ||
|
|
34fa65132d | ||
|
|
19fe2d2f03 | ||
|
|
c796af7d07 | ||
|
|
b7251e2be2 | ||
|
|
6efede79c7 | ||
|
|
e58da80f8e | ||
|
|
bd556c33a0 | ||
|
|
878809ecb4 | ||
|
|
ea4966f494 | ||
|
|
f5d9b478d5 | ||
|
|
3d97fab16d | ||
|
|
af2ae11eb1 | ||
|
|
2c1fc30bfb | ||
|
|
5aa4199dfb | ||
|
|
773790c8ec | ||
|
|
c7dec8045e | ||
|
|
5d7d12415c | ||
|
|
8a066163fe | ||
|
|
08e57936fc | ||
|
|
7d474e17fa | ||
|
|
6eac25425c | ||
|
|
c0f1981d6f | ||
|
|
d3571942d7 | ||
|
|
8376b6f0f2 | ||
|
|
490a66d3fe | ||
|
|
2e182626b9 | ||
|
|
66f5f8ab12 | ||
|
|
50ecee99cc | ||
|
|
fc3f982309 | ||
|
|
b8e9aded6b | ||
|
|
74ce9f7a86 | ||
|
|
5b2957e7eb | ||
|
|
55a464f1f6 | ||
|
|
72ec6beb99 | ||
|
|
40eb4ea4ec | ||
|
|
895f3d9cb3 | ||
|
|
ae758da449 | ||
|
|
a8f06f5718 | ||
|
|
c458a1b59a | ||
|
|
29d59b3974 | ||
|
|
27f267cb3d | ||
|
|
0f41039b99 | ||
|
|
8f76db9e41 | ||
|
|
fc922e996c | ||
|
|
0a26ad8925 | ||
|
|
a8c6c7cb36 | ||
|
|
81fee53085 | ||
|
|
7c5f2d95a4 | ||
|
|
3d6bd0e23e | ||
|
|
8064e84276 | ||
|
|
f8ece2d984 | ||
|
|
442eef36ba | ||
|
|
7d737ad312 | ||
|
|
2b662d3a14 | ||
|
|
4f3fae81c1 | ||
|
|
7b266ad7c0 | ||
|
|
16601a67e3 | ||
|
|
224d59d50d | ||
|
|
6142d325c8 | ||
|
|
f9472627f4 | ||
|
|
69735207e6 | ||
|
|
3b47f18a12 | ||
|
|
63f34e5a82 | ||
|
|
aa21aa686d | ||
|
|
9c2b2a2dcc | ||
|
|
baff979ab4 | ||
|
|
ec2719a572 | ||
|
|
07fe330e4f | ||
|
|
f98c3efef7 | ||
|
|
8b74dec910 | ||
|
|
dc4b8a5002 | ||
|
+2 |
dd5fecdf5a | ||
|
|
a466126d63 | ||
|
|
6c52754c8f | ||
|
|
e1ebcfa865 | ||
|
|
6464c81e72 | ||
|
|
0b7abfae3a | ||
|
|
76c16ad16e | ||
|
|
568f322a25 | ||
|
|
1b64959e2c | ||
|
|
1f722477ce |
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
Dockerfile
|
||||||
|
tmp/
|
||||||
|
data/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: Bug 报告(Bug Report)
|
||||||
|
about: 反馈项目中的缺陷或问题
|
||||||
|
title: "[Bug] 简短描述问题"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 问题描述
|
||||||
|
<!-- 清晰简洁地描述问题是什么 -->
|
||||||
|
|
||||||
|
## 🔍 复现步骤
|
||||||
|
<!-- 详细描述复现问题的步骤 -->
|
||||||
|
1. 打开 '...'
|
||||||
|
2. 点击 '...'
|
||||||
|
3. 滚动到 '...'
|
||||||
|
4. 看到错误
|
||||||
|
|
||||||
|
## 🤔 预期行为
|
||||||
|
<!-- 简要描述预期的正确行为 -->
|
||||||
|
|
||||||
|
## 😯 截图
|
||||||
|
<!-- 如果适用,添加问题的截图 -->
|
||||||
|
|
||||||
|
## 🖥️ 环境信息
|
||||||
|
- 操作系统: [例如 Windows 10]
|
||||||
|
- 浏览器: [例如 Chrome 89]
|
||||||
|
- 项目版本: [例如 1.0.0]
|
||||||
|
- Python版本: [例如 3.9.13]
|
||||||
|
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
|
||||||
|
- Nodejs版本:[例如 v20.14.0]
|
||||||
|
|
||||||
|
## 📋 其他信息
|
||||||
|
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: 代码优化建议(Code Improvement)
|
||||||
|
about: 提出对现有代码的优化或改进建议
|
||||||
|
title: "[Improvement] 简短描述改进内容"
|
||||||
|
labels: refactor
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 改进描述
|
||||||
|
<!-- 描述需要改进的代码部分以及存在的问题 -->
|
||||||
|
|
||||||
|
## 🌟 改进建议
|
||||||
|
<!-- 提出具体的改进方案或思路 -->
|
||||||
|
|
||||||
|
## 🛠️ 相关代码
|
||||||
|
<!-- 如果可能,提供相关代码片段或链接 -->
|
||||||
|
|
||||||
|
## 📋 其他信息
|
||||||
|
<!-- 在此添加任何其他相关信息 -->
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: 文档改进建议(Documentation Improvement)
|
||||||
|
about: 提出对项目文档的改进或补充建议
|
||||||
|
title: "[Docs] 简短描述改进内容"
|
||||||
|
labels: documentation
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 改进描述
|
||||||
|
<!-- 描述需要改进的文档部分以及存在的问题 -->
|
||||||
|
|
||||||
|
## ✨ 改进建议
|
||||||
|
<!-- 提出具体的改进方案或内容 -->
|
||||||
|
|
||||||
|
## 📋 其他信息
|
||||||
|
<!-- 在此添加任何其他相关信息 -->
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: 功能请求(Feature Request)
|
||||||
|
about: 提出新的功能或改进建议
|
||||||
|
title: "[Feature] 简短描述功能"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 需求描述
|
||||||
|
<!-- 清晰简洁地描述你希望发生什么 -->
|
||||||
|
|
||||||
|
## 🎯 解决方案
|
||||||
|
<!-- 描述你认为可行的解决方案或实现方式 -->
|
||||||
|
|
||||||
|
## 📝 备选方案
|
||||||
|
<!-- 描述你考虑过的替代方案或功能 -->
|
||||||
|
|
||||||
|
## 📋 其他信息
|
||||||
|
<!-- 在此添加关于需求的任何其他上下文信息 -->
|
||||||
@@ -7,30 +7,31 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release Docker image
|
name: Release Docker images
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: write
|
contents: write
|
||||||
id-token: write
|
id-token: write
|
||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check Disk Space
|
- name: Check Disk Space
|
||||||
run: |
|
run: |
|
||||||
df -h
|
df -h
|
||||||
docker system df
|
docker system df
|
||||||
|
|
||||||
- name: Clean up Docker resources
|
- name: Clean up Docker resources
|
||||||
run: |
|
run: |
|
||||||
docker system prune -af
|
docker system prune -af
|
||||||
docker builder prune -af
|
docker builder prune -af
|
||||||
- name: Check out the repo
|
|
||||||
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to the GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -42,13 +43,26 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build and push Docker image
|
# 构建 xiaozhi-server 镜像
|
||||||
id: build_push
|
- name: Build and push xiaozhi-server
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: Dockerfile-server
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:${{ env.VERSION }}
|
ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
|
||||||
ghcr.io/${{ github.repository }}:latest
|
ghcr.io/${{ github.repository }}:server_latest
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
# 构建 manager-api 镜像
|
||||||
|
- name: Build and push manager-web
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile-web
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository }}:web_${{ env.VERSION }}
|
||||||
|
ghcr.io/${{ github.repository }}:web_latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -75,6 +75,7 @@ docs/_build/
|
|||||||
# PyBuilder
|
# PyBuilder
|
||||||
.pybuilder/
|
.pybuilder/
|
||||||
target/
|
target/
|
||||||
|
*.pid
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
@@ -106,7 +107,6 @@ celerybeat.pid
|
|||||||
*.sage.py
|
*.sage.py
|
||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
@@ -141,13 +141,21 @@ music/
|
|||||||
# Cython debug symbols
|
# Cython debug symbols
|
||||||
cython_debug/
|
cython_debug/
|
||||||
*.iml
|
*.iml
|
||||||
model.pt
|
|
||||||
tmp
|
tmp
|
||||||
|
.history
|
||||||
.DS_Store
|
.DS_Store
|
||||||
main/xiaozhi-server/data
|
main/xiaozhi-server/data
|
||||||
|
main/xiaozhi-server/config/assets/wakeup_words.*
|
||||||
main/manager-web/node_modules
|
main/manager-web/node_modules
|
||||||
.config.yaml
|
.config.yaml
|
||||||
.secrets.yaml
|
.secrets.yaml
|
||||||
.private_config.yaml
|
.private_config.yaml
|
||||||
.env.development
|
|
||||||
|
|
||||||
|
# model files
|
||||||
|
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||||
|
main/xiaozhi-server/models/sherpa-onnx*
|
||||||
|
my_wakeup_words.mp3
|
||||||
|
main/manager-api/.vscode
|
||||||
|
|
||||||
|
# Ignore webpack cache directory
|
||||||
|
main/manager-web/.webpack_cache/
|
||||||
@@ -5,15 +5,15 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY main/xiaozhi-server/requirements.txt .
|
COPY main/xiaozhi-server/requirements.txt .
|
||||||
|
|
||||||
# 优化apt安装
|
# 安装Python依赖
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# 第三阶段:生产镜像
|
# 第二阶段:生产镜像
|
||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
|
|
||||||
WORKDIR /opt/xiaozhi-esp32-server
|
WORKDIR /opt/xiaozhi-esp32-server
|
||||||
|
|
||||||
# 优化apt安装
|
# 安装系统依赖
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
apt-get install -y --no-install-recommends libopus0 ffmpeg && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
@@ -23,7 +23,7 @@ RUN apt-get update && \
|
|||||||
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
|
||||||
|
|
||||||
# 复制应用代码
|
# 复制应用代码
|
||||||
COPY main/xiaozhi-server/ .
|
COPY main/xiaozhi-server .
|
||||||
|
|
||||||
# 启动应用
|
# 启动应用
|
||||||
CMD ["python", "app.py"]
|
CMD ["python", "app.py"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# 第一阶段:构建Vue前端
|
||||||
|
FROM node:18 as web-builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY main/manager-web/package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY main/manager-web .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 第二阶段:构建Java后端
|
||||||
|
FROM maven:3-eclipse-temurin-21-alpine as api-builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY main/manager-api/pom.xml .
|
||||||
|
COPY main/manager-api/src ./src
|
||||||
|
RUN mvn clean package -Dmaven.test.skip=true
|
||||||
|
|
||||||
|
# 第三阶段:构建最终镜像
|
||||||
|
FROM eclipse-temurin:21-jdk-jammy
|
||||||
|
|
||||||
|
# 安装Nginx并清理缓存
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y nginx && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 配置Nginx
|
||||||
|
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 复制前端构建产物
|
||||||
|
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制Java后端JAR包
|
||||||
|
COPY --from=api-builder /app/target/xiaozhi-esp32-api.jar /app/xiaozhi-esp32-api.jar
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8002
|
||||||
|
|
||||||
|
# 启动脚本
|
||||||
|
COPY docs/docker/start.sh /start.sh
|
||||||
|
RUN chmod +x /start.sh
|
||||||
|
CMD ["/start.sh"]
|
||||||
@@ -1,7 +1,28 @@
|
|||||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
|
||||||
|
<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://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python实现<br/>
|
||||||
|
帮助您快速搭建小智服务器
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="./README_en.md">English</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 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">
|
<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" />
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server?logo=github" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
|
<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" />
|
<img alt="Issues" src="https://img.shields.io/github/issues/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||||
@@ -9,15 +30,14 @@
|
|||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
|
<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" />
|
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/LICENSE">
|
||||||
|
<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="stars" src="https://img.shields.io/github/stars/xinnan-tech/xiaozhi-esp32-server?color=ffcb47&labelColor=black" />
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# 小智 ESP-32 后端服务(xiaozhi-esp32-server)
|
|
||||||
|
|
||||||
(中文 | [English](README_en.md))
|
|
||||||
|
|
||||||
本项目为开源智能硬件项目 [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)
|
|
||||||
提供后端服务。根据 [小智通信协议](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh) 使用 `Python` 实现。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 适用人群 👥
|
## 适用人群 👥
|
||||||
@@ -51,12 +71,19 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/av114036381327149" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="成本最低配置" src="docs/images/demo4.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -74,13 +101,25 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="成本最低配置" src="docs/images/demo4.png" />
|
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="IOT指令控制设备" src="docs/images/demo9.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -89,16 +128,7 @@
|
|||||||
|
|
||||||
## 系统要求与部署前提 🖥️
|
## 系统要求与部署前提 🖥️
|
||||||
|
|
||||||
- **硬件**:一套兼容 `xiaozhi-esp32`
|
建议 4 核 CPU、8G 内存的电脑。如果开启ASR也使用API,可运行在2核CPU、2G内存的服务器中。[请参考部署架构图](./docs/images/deploy.png)
|
||||||
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
|
|
||||||
|
|
||||||
- **电脑或服务器**:至少 4 核 CPU、8G 内存的电脑。
|
|
||||||
- **固件编译**:请将本后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译`xiaozhi-esp32`固件并烧录到设备上。
|
|
||||||
|
|
||||||
如果你没有esp32相关的硬件设备,但是非常想体验该项目,可以使用以下的项目让你的电脑、手机模拟成esp32设备。
|
|
||||||
|
|
||||||
- [小智安卓端](https://github.com/TOM88812/xiaozhi-android-client)
|
|
||||||
- [小智电脑端](https://github.com/Huang-junsen/py-xiaozhi)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -121,101 +151,17 @@ server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 功能清单 ✨
|
## 部署方式 🚀
|
||||||
|
|
||||||
### 已实现 ✅
|
[](./docs/Deployment.md)
|
||||||
|
|
||||||
- **通信协议**
|
|
||||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
|
||||||
- **对话交互**
|
|
||||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
|
||||||
- **多语言识别**
|
|
||||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
|
||||||
- **LLM 模块**
|
|
||||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
|
||||||
- **TTS 模块**
|
|
||||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
|
||||||
|
|
||||||
### 正在开发 🚧
|
|
||||||
|
|
||||||
- 对话记忆功能
|
|
||||||
- 多种心情模式
|
|
||||||
- 智控台webui
|
|
||||||
|
|
||||||

|
|
||||||
---
|
|
||||||
|
|
||||||
## 本项目支持的平台/组件列表 📋
|
|
||||||
|
|
||||||
### LLM
|
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
|
||||||
|:---:|:------------------:|:---------------------:|:--------:|:-----------------------------------------------------------------:|
|
|
||||||
| LLM | 阿里百炼 (AliLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
|
||||||
| LLM | 深度求索 (DeepSeekLLM) | openai 接口调用 | 消耗 token | [点击申请密钥](https://platform.deepseek.com/) |
|
|
||||||
| LLM | 智谱(ChatGLMLLM) | openai 接口调用 | 免费 | 虽然免费,仍需[点击申请密钥](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
|
||||||
| LLM | OllamaLLM | ollama 接口调用 | 免费/自定义 | 需预先下载模型(`ollama pull`),服务地址:`http://localhost:11434` |
|
|
||||||
| LLM | DifyLLM | dify 接口调用 | 消耗 token | 本地化部署,注意配置提示词需在 Dify 控制台设置 |
|
|
||||||
| LLM | GeminiLLM | gemini 接口调用 | 免费 | [点击申请密钥](https://aistudio.google.com/apikey) |
|
|
||||||
| LLM | CozeLLM | coze 接口调用 | 消耗 token | 需提供 bot_id、user_id 及个人令牌 |
|
|
||||||
| LLM | Home Assistant | homeassistant语音助手接口调用 | 免费 | 需提供home assistant令牌 |
|
|
||||||
|
|
||||||
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TTS
|
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
|
||||||
|:---:|:----------------------:|:----:|:--------:|:-------------------------------------------------------------------------:|
|
|
||||||
| TTS | EdgeTTS | 接口调用 | 免费 | 默认 TTS,基于微软语音合成技术 |
|
|
||||||
| TTS | 火山引擎豆包 TTS (DoubaoTTS) | 接口调用 | 消耗 token | [点击创建密钥](https://console.volcengine.com/speech/service/8);建议使用付费版本以获得更高并发 |
|
|
||||||
| TTS | CosyVoiceSiliconflow | 接口调用 | 消耗 token | 需申请硅基流动 API 密钥;输出格式为 wav |
|
|
||||||
| TTS | CozeCnTTS | 接口调用 | 消耗 token | 需提供 Coze API key;输出格式为 wav |
|
|
||||||
| TTS | FishSpeech | 接口调用 | 免费/自定义 | 本地启动 TTS 服务;启动方法见配置文件内说明 |
|
|
||||||
| TTS | GPT_SOVITS_V2 | 接口调用 | 免费/自定义 | 本地启动 TTS 服务,适用于个性化语音合成场景 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VAD
|
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
|
||||||
|:---:|:---------:|:----:|:----:|:--:|
|
|
||||||
| VAD | SileroVAD | 本地使用 | 免费 | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ASR
|
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
|
||||||
|:---:|:---------:|:----:|:----:|:--:|
|
|
||||||
| ASR | FunASR | 本地使用 | 免费 | |
|
|
||||||
| ASR | DoubaoASR | 接口调用 | 收费 | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Memory
|
|
||||||
|
|
||||||
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
|
||||||
|:------:|:------:|:----:|:----:|:--:|
|
|
||||||
| Memory | mem0ai | 接口调用 | 免费 | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 使用方式 🚀
|
|
||||||
|
|
||||||
### 一、[部署文档](./docs/Deployment.md)
|
### 一、[部署文档](./docs/Deployment.md)
|
||||||
|
|
||||||
本项目支持以下三种部署方式,您可根据实际需求选择。
|
本项目支持以下三种部署方式,您可根据实际需求选择。
|
||||||
|
|
||||||
本项目的文档主要是`文字版本`的教程,如果你想要`视频版本`
|
|
||||||
的教程,您可以学习一下[这个大佬的手把手教程](https://www.bilibili.com/video/BV1gePuejEvT)。
|
|
||||||
|
|
||||||
如果你能把`文字版本的教程`和`视频版本的教程`结合起来一起看,可以让你更快上手。
|
|
||||||
|
|
||||||
1. [Docker 快速部署](./docs/Deployment.md)
|
1. [Docker 快速部署](./docs/Deployment.md)
|
||||||
|
|
||||||
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。
|
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。视频教程可参考[美女大佬教你Docker部署](https://www.bilibili.com/video/BV1RNQnYDE5t)
|
||||||
|
|
||||||
2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
|
2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
|
||||||
|
|
||||||
@@ -225,122 +171,124 @@ server:
|
|||||||
|
|
||||||
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
|
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
|
||||||
|
|
||||||
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。
|
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。视频教程可参考[帅哥大佬教你源码部署](https://www.bilibili.com/video/BV1GvQWYZEd2)
|
||||||
|
|
||||||
### 二、[固件编译](./docs/firmware-build.md)
|
### 二、[固件编译](./docs/firmware-build.md)
|
||||||
|
|
||||||
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
|
点这里查看[固件编译](./docs/firmware-build.md)的详细过程。
|
||||||
|
|
||||||
编译成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题 ❓
|
## 常见问题 ❓
|
||||||
|
|
||||||
### 1、TTS 经常失败,经常超时 ⏰
|
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
||||||
|
|
||||||
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
---
|
||||||
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
|
||||||
|
|
||||||
### 2、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
## 产品生态 👬
|
||||||
|
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
||||||
|
|
||||||
建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。
|
- [小智安卓客户端](https://github.com/TOM88812/xiaozhi-android-client)
|
||||||
|
|
||||||
### 3、我说话很慢,停顿时小智老是抢话 🗣️
|
一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。现在是flutter版本,打通IOS、Android端。
|
||||||
|
- [小智电脑客户端](https://github.com/Huang-junsen/py-xiaozhi)
|
||||||
|
|
||||||
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,依然能够体过代码体验小智 AI 的功能。主要功能包括 AI 语音交互、视觉多模态识别、IoT 设备集成、联网音乐播放、语音唤醒、自动对话模式、图形化界面、命令行模式、跨平台支持、音量控制、会话管理、加密音频传输、自动验证码处理等。
|
||||||
|
- [小智Java服务端](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
|
||||||
|
|
||||||
```yaml
|
小智开源后端服务 Java 版本是一个基于 Java 的开源项目,它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。
|
||||||
VAD:
|
---
|
||||||
SileroVAD:
|
## 功能清单 ✨
|
||||||
threshold: 0.5
|
|
||||||
model_dir: models/snakers4_silero-vad
|
|
||||||
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
### 已实现 ✅
|
||||||
|
|
||||||
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
|
- **通信协议**
|
||||||
文件,如果没有就要下载,查看这里[下载语音识别模型文件](docs/Deployment.md#模型文件)
|
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
||||||
|
- **对话交互**
|
||||||
|
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
||||||
|
- **意图识别**
|
||||||
|
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
|
||||||
|
- **多语言识别**
|
||||||
|
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
||||||
|
- **LLM 模块**
|
||||||
|
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
||||||
|
- **TTS 模块**
|
||||||
|
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
||||||
|
- **记忆功能**
|
||||||
|
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
|
||||||
|
- **IOT功能**
|
||||||
|
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
|
||||||
|
|
||||||
### 5、为什么会出现“TTS 任务出错 文件不存在”?📁
|
### 正在开发 🚧
|
||||||
|
|
||||||
建议:检查一下是否正确使用`conda` 安装了`libopus`和`ffmpeg`库。
|
- 多种心情模式
|
||||||
|
- 智控台webui
|
||||||
|
|
||||||
如果没有安装,就安装
|
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
||||||
|
|
||||||
```
|
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
||||||
conda install conda-forge::libopus
|
|
||||||
conda install conda-forge::ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6、如何提高小智对话响应速度? ⚡
|
---
|
||||||
|
|
||||||
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决“跑得动”的问题,再优化“跑得快”。
|
## 本项目支持的平台/组件列表 📋
|
||||||
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
|
||||||
|
|
||||||
| 影响因素 | 因素值 |
|
### LLM 语言模型
|
||||||
|:-----:|:----------------:|
|
|
||||||
| 测试地点 | 广东省广州市海珠区 |
|
|
||||||
| 测试时间 | 2025年2月19日 12:52 |
|
|
||||||
| 宽带运营商 | 中国联通 |
|
|
||||||
|
|
||||||
测试方法:
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|
|:---:|:---:|:---:|
|
||||||
|
| openai 接口调用 | 阿里百炼、火山引擎豆包、深度求索、智谱ChatGLM、Gemini | 智谱ChatGLM、Gemini |
|
||||||
|
| ollama 接口调用 | Ollama | - |
|
||||||
|
| dify 接口调用 | Dify | - |
|
||||||
|
| fastgpt 接口调用 | Fastgpt | - |
|
||||||
|
| coze 接口调用 | Coze | - |
|
||||||
|
|
||||||
1、把各组件的密钥配置上去,只有配置了密钥的组件才参与测试。
|
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||||
|
|
||||||
2、配置完密钥后,执行以下方法
|
---
|
||||||
|
|
||||||
```
|
### TTS 语音合成
|
||||||
# 进入项目根目录,执行以下命令:
|
|
||||||
conda activate xiaozhi-esp32-server
|
|
||||||
python performance_tester.py
|
|
||||||
```
|
|
||||||
|
|
||||||
生成报告如下
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|
|:---:|:---:|:---:|
|
||||||
|
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS | EdgeTTS、CosyVoiceSiliconflow(部分) |
|
||||||
|
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS |
|
||||||
|
|
||||||
LLM 性能排行:
|
---
|
||||||
|
|
||||||
| 模块名称 | 平均首Token时间 | 平均总响应时间 |
|
### VAD 语音活动检测
|
||||||
|:-----------|:-----------|:--------|
|
|
||||||
| AliLLM | 0.547s | 1.485s |
|
|
||||||
| ChatGLMLLM | 0.677s | 3.057s |
|
|
||||||
| OllamaLLM | 0.003s | 0.003s |
|
|
||||||
|
|
||||||
TTS 性能排行:
|
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||||
|
|:---:|:---------:|:----:|:----:|:--:|
|
||||||
|
| VAD | SileroVAD | 本地使用 | 免费 | |
|
||||||
|
|
||||||
| 模块名称 | 平均合成时间 |
|
---
|
||||||
|----------------------|--------|
|
|
||||||
| EdgeTTS | 1.019s |
|
|
||||||
| DoubaoTTS | 0.503s |
|
|
||||||
| CosyVoiceSiliconflow | 3.732s |
|
|
||||||
|
|
||||||
推荐配置组合 (综合响应速度):
|
### ASR 语音识别
|
||||||
|
|
||||||
| 组合方案 | 综合得分 | LLM首Token | TTS合成 |
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|-------------------------------|-------|-----------|--------|
|
|:---:|:---:|:---:|
|
||||||
| AliLLM + DoubaoTTS | 0.539 | 0.547s | 0.503s |
|
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||||
| AliLLM + EdgeTTS | 0.642 | 0.547s | 1.019s |
|
| 接口调用 | DoubaoASR | - |
|
||||||
| ChatGLMLLM + DoubaoTTS | 0.642 | 0.677s | 0.503s |
|
|
||||||
| ChatGLMLLM + EdgeTTS | 0.745 | 0.677s | 1.019s |
|
|
||||||
| AliLLM + CosyVoiceSiliconflow | 1.184 | 0.547s | 3.732s |
|
|
||||||
|
|
||||||
### 结论 🔍
|
---
|
||||||
|
|
||||||
`2025年2月19日`,如果我的电脑在`广东省广州市海珠区`,且使用的是`中国联通`网络,我会优先使用:
|
### Memory 记忆存储
|
||||||
|
|
||||||
- LLM:`AliLLM`
|
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||||
- TTS:`DoubaoTTS`
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
|
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
||||||
|
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||||
|
|
||||||
### 7、更多问题,可联系我们反馈 💬
|
---
|
||||||
|
|
||||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
### Intent 意图识别
|
||||||
|
|
||||||
网盘里有“硬件烧录QQ群”、“开源服务端交流群”、“产品建议联系人” 三张图片,请根据需要选择加入。
|
| 类型 | 平台名称 | 使用方式 | 收费模式 | 备注 |
|
||||||
|
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||||
- 硬件烧录QQ群:适用于硬件烧录问题
|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
||||||
- 开源服务端交流群:适用于服务端问题
|
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
||||||
- 产品建议联系人:适用于产品功能、产品设计等建议
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
|
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<h1>Xiaozhi Backend Server xiaozhi-esp32-server</h1>
|
||||||
|
</center>
|
||||||
|
|
||||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
|
||||||
<p align="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">
|
<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" />
|
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server?logo=github" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
|
<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" />
|
<img alt="Issues" src="https://img.shields.io/github/issues/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||||
@@ -10,79 +31,114 @@
|
|||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
|
<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" />
|
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||||
</a>
|
</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>
|
</p>
|
||||||
|
|
||||||
# XiaoZhi ESP-32 Backend Service (xiaozhi-esp32-server)
|
|
||||||
|
|
||||||
([中文](README.md) | English)
|
|
||||||
|
|
||||||
This project provides the backend service for the open source smart hardware project [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32). It is implemented in `Python` based on the [XiaoZhi Communication Protocol](https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Target Audience 👥
|
## Target Users 👥
|
||||||
|
|
||||||
This project is designed to be used in conjunction with ESP32 hardware devices. If you have already purchased an ESP32 device, successfully connected to the backend service deployed by XieGe, and now wish to set up your own `xiaozhi-esp32` backend service, then this project is perfect for you.
|
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 the videos 🎥
|
Want to see it in action? Check out these videos 🎥
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="XiaoZhi ESP32 connecting to a custom backend model" src="docs/images/demo1.png" />
|
<img alt="Xiaozhi esp32 connecting to your own backend model" src="docs/images/demo1.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Custom Voice" src="docs/images/demo2.png" />
|
<img alt="Custom voice" src="docs/images/demo2.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Conversing in Cantonese" src="docs/images/demo3.png" />
|
<img alt="Communicating in Cantonese" src="docs/images/demo3.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/av114036381327149" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Control Home Appliances" src="docs/images/demo5.png" />
|
<img alt="Control home appliances" src="docs/images/demo5.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
||||||
<picture>
|
<picture>
|
||||||
<img alt="Lowest Cost Configuration" src="docs/images/demo4.png" />
|
<img alt="Lowest cost configuration" src="docs/images/demo4.png" />
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</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>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## System Requirements and Deployment Prerequisites 🖥️
|
## System Requirements and Prerequisites 🖥️
|
||||||
|
|
||||||
- **Hardware**: A set of devices compatible with `xiaozhi-esp32` (for specific models, please refer to [this link](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf)).
|
- **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.
|
||||||
- **Server**: A computer with at least a 4-core CPU and 8GB of memory.
|
- **Update Client Interface**: Please update the backend service interface address in the client.
|
||||||
- **Firmware Compilation**: Please update the backend service API endpoint in the `xiaozhi-esp32` project, then recompile the firmware and flash it to your device.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Warning ⚠️
|
## Warning ⚠️
|
||||||
|
|
||||||
This project is relatively new and has not yet undergone network security evaluations. **Do not use it in a production environment.**
|
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.
|
||||||
|
|
||||||
If you deploy this project on a public network for learning purposes, be sure to enable protection in the configuration file `config.yaml`:
|
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
|
```yaml
|
||||||
server:
|
server:
|
||||||
@@ -91,201 +147,151 @@ server:
|
|||||||
enabled: true
|
enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
Once protection is enabled, you will need to validate the machine's token or MAC address based on your actual situation. Please refer to the configuration documentation for details.
|
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 ✨
|
## Feature List ✨
|
||||||
|
|
||||||
### Implemented ✅
|
### Implemented ✅
|
||||||
|
|
||||||
- **Communication Protocol**
|
- **Communication Protocol**
|
||||||
Based on the `xiaozhi-esp32` protocol, data exchange is implemented via WebSocket.
|
Based on `xiaozhi-esp32` protocol, implementing data interaction through WebSocket.
|
||||||
- **Dialogue Interaction**
|
- **Dialogue Interaction**
|
||||||
Supports wake-up dialogues, manual conversations, and real-time interruptions. Automatically enters sleep mode after long periods of inactivity.
|
Supports wake-up dialogue, manual dialogue, and real-time interruption. Automatically sleeps after long periods without dialogue
|
||||||
- **Multilingual Recognition**
|
- **Intent Recognition**
|
||||||
Supports Mandarin, Cantonese, English, Japanese, and Korean (default using FunASR).
|
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**
|
- **LLM Module**
|
||||||
Allows flexible switching of LLM modules. The default is ChatGLMLLM, with options to use AliLLM, DeepSeek, Ollama, and others.
|
Supports flexible switching of LLM modules, default using ChatGLMLLM, can also use Alibaba Bailian, DeepSeek, Ollama, and other interfaces.
|
||||||
- **TTS Module**
|
- **TTS Module**
|
||||||
Supports multiple TTS interfaces including EdgeTTS (default) and Volcano Engine Doubao TTS to meet speech synthesis requirements.
|
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.
|
||||||
|
|
||||||
### In Development 🚧
|
### Under Development 🚧
|
||||||
|
|
||||||
- Conversation Memory Feature
|
- Multiple mood modes
|
||||||
- Multiple Mood Modes
|
- Smart control panel webui
|
||||||
- Smart Control Panel Web UI
|
|
||||||
|
|
||||||

|
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 📋
|
## Supported Platforms/Components List 📋
|
||||||
|
|
||||||
### LLM
|
### LLM Language Models
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|
| Usage Method | Supported Platforms | Free Platforms |
|
||||||
|:----:|:-----------------------------:|:-----------------------------:|:-----------------:|:-------------------------------------------------------------------------:|
|
|:---:|:---:|:---:|
|
||||||
| LLM | AliLLM (阿里百炼) | OpenAI API call | Token consumption | [Click to apply for API key](https://bailian.console.aliyun.com/?apiKey=1#/api-key) |
|
| openai interface call | Alibaba Bailian, Volcano Engine Doubao, DeepSeek, Zhipu ChatGLM, Gemini | Zhipu ChatGLM, Gemini |
|
||||||
| LLM | DeepSeekLLM (深度求索) | OpenAI API call | Token consumption | [Click to apply for API key](https://platform.deepseek.com/) |
|
| ollama interface call | Ollama | - |
|
||||||
| LLM | ChatGLMLLM (智谱) | OpenAI API call | Free | Although free, you still need to [click to apply for an API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) |
|
| dify interface call | Dify | - |
|
||||||
| LLM | OllamaLLM | Ollama API call | Free/Custom | Requires pre-downloading the model (`ollama pull`); service URL: `http://localhost:11434` |
|
| fastgpt interface call | Fastgpt | - |
|
||||||
| LLM | DifyLLM | Dify API call | Token consumption | For local deployment. Note that prompt configuration must be set in the Dify console. |
|
| coze interface call | Coze | - |
|
||||||
| LLM | GeminiLLM | Gemini API call | Free | [Click to apply for API key](https://aistudio.google.com/apikey) |
|
|
||||||
| LLM | CozeLLM | Coze API call | Token consumption | Requires providing bot_id, user_id, and personal token. |
|
|
||||||
| LLM | Home Assistant | Home Assistant voice assistant API call | Free | Requires providing a Home Assistant token. |
|
|
||||||
|
|
||||||
In fact, any LLM that supports OpenAI API calls can be integrated.
|
In fact, any LLM supporting openai interface calls can be integrated.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TTS
|
### TTS Speech Synthesis
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|
| Usage Method | Supported Platforms | Free Platforms |
|
||||||
|:----:|:--------------------------------------:|:------------:|:-----------------:|:--------------------------------------------------------------------------------------:|
|
|:---:|:---:|:---:|
|
||||||
| TTS | EdgeTTS | API call | Free | Default TTS based on Microsoft's speech synthesis technology. |
|
| Interface Call | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Alibaba Cloud TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS | EdgeTTS, CosyVoiceSiliconflow(partial) |
|
||||||
| TTS | DoubaoTTS (火山引擎豆包 TTS) | API call | Token consumption | [Click to create an API key](https://console.volcengine.com/speech/service/8); it is recommended to use the paid version for higher concurrency. |
|
| Local Service | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
|
||||||
| TTS | CosyVoiceSiliconflow | API call | Token consumption | Requires application for the Siliconflow API key; output format is WAV. |
|
|
||||||
| TTS | CozeCnTTS | API call | Token consumption | Requires providing a Coze API key; output format is WAV. |
|
|
||||||
| TTS | FishSpeech | API call | Free/Custom | Starts a local TTS service; see the configuration file for startup instructions. |
|
|
||||||
| TTS | GPT_SOVITS_V2 | API call | Free/Custom | Starts a local TTS service, suitable for personalized speech synthesis scenarios. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### VAD
|
### VAD Voice Activity Detection
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||||
|:----:|:-------------------:|:------------:|:-------------:|:-------:|
|
|:---:|:---------:|:----:|:----:|:--:|
|
||||||
| VAD | SileroVAD | Local | Free | |
|
| VAD | SileroVAD | Local Use | Free | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ASR
|
### ASR Speech Recognition
|
||||||
|
|
||||||
| Type | Platform Name | Usage Method | Pricing Model | Remarks |
|
| Usage Method | Supported Platforms | Free Platforms |
|
||||||
|:----:|:-------------------:|:------------:|:-------------:|:-------:|
|
|:---:|:---:|:---:|
|
||||||
| ASR | FunASR | Local | Free | |
|
| Local Use | FunASR, SherpaASR | FunASR, SherpaASR |
|
||||||
| ASR | DoubaoASR | API call | Paid | |
|
| Interface Call | DoubaoASR | - |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage 🚀
|
### Memory Storage
|
||||||
|
|
||||||
### 1. [Deployment Documentation](./docs/Deployment.md)
|
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||||
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
This project supports three deployment methods. Choose the one that best fits your needs.
|
| Memory | mem0ai | Interface Call | 1000 times/month quota | |
|
||||||
|
| Memory | mem_local_short | Local Summary | Free | |
|
||||||
The documentation provided here is a **written tutorial**. If you prefer a **video tutorial**, you can refer to [this expert's hands-on guide](https://www.bilibili.com/video/BV1gePuejEvT).
|
|
||||||
|
|
||||||
Combining both the written and video tutorials can help you get started more quickly.
|
|
||||||
|
|
||||||
1. [Docker Quick Deployment](./docs/Deployment.md)
|
|
||||||
Suitable for general users who want a quick experience without extensive environment configuration. The only downside is that pulling the image can be a bit slow.
|
|
||||||
|
|
||||||
2. [Deployment 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)
|
|
||||||
Ideal for software engineers who already have Docker installed and wish to customize the code.
|
|
||||||
|
|
||||||
3. [Running from Local Source Code](./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 the `Conda` environment or those who wish to build the runtime environment from scratch.
|
|
||||||
|
|
||||||
For scenarios requiring higher response speeds, running from the local source code is recommended to reduce additional overhead.
|
|
||||||
|
|
||||||
### 2. [Firmware Compilation](./docs/firmware-build.md)
|
|
||||||
|
|
||||||
Click [here](./docs/firmware-build.md) for a detailed guide on firmware compilation.
|
|
||||||
|
|
||||||
After successful compilation and network connection, wake up XiaoZhi using the wake-up word and monitor the server console for output.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Frequently Asked Questions ❓
|
### Intent Recognition
|
||||||
|
|
||||||
### 1. TTS often fails and times out ⏰
|
| Type | Platform Name | Usage Method | Pricing Model | Notes |
|
||||||
|
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||||
**Suggestion:**
|
| Intent | intent_llm | Interface Call | Based on LLM pricing | Intent recognition through large models, highly generalizable |
|
||||||
If `EdgeTTS` frequently fails, please first check whether you are using a proxy (VPN). If so, try disabling the proxy and try again. If you are using Volcano Engine Doubao TTS and it often fails, it is recommended to use the paid version since the trial only supports 2 concurrent requests.
|
| Intent | function_call | Interface Call | Based on LLM pricing | Intent completion through large model function calls, fast and effective |
|
||||||
|
|
||||||
### 2. I want to control lights, air conditioners, remote power on/off, etc. with XiaoZhi 💡
|
|
||||||
|
|
||||||
**Suggestion:**
|
|
||||||
Set the `LLM` to `HomeAssistant` in the configuration file and use the `HomeAssistant` API to perform the relevant controls.
|
|
||||||
|
|
||||||
### 3. I speak slowly, and XiaoZhi always interrupts during pauses 🗣️
|
|
||||||
|
|
||||||
**Suggestion:**
|
|
||||||
Locate the following section in the configuration file and increase the value of `min_silence_duration_ms` (for example, change it to `1000`):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
VAD:
|
|
||||||
SileroVAD:
|
|
||||||
threshold: 0.5
|
|
||||||
model_dir: models/snakers4_silero-vad
|
|
||||||
min_silence_duration_ms: 700 # If your pauses are longer, increase this value
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Why does XiaoZhi recognize a lot of Korean, Japanese, and English in what I say? 🇰🇷
|
|
||||||
|
|
||||||
**Suggestion:**
|
|
||||||
Check whether the `model.pt` file exists in the `models/SenseVoiceSmall` directory. If it does not, please download it. See [Download ASR Model Files](docs/Deployment.md#模型文件) for details.
|
|
||||||
|
|
||||||
### 5. Why does the error “TTS task error: file does not exist” occur? 📁
|
|
||||||
|
|
||||||
**Suggestion:**
|
|
||||||
Verify that you have correctly installed the `libopus` and `ffmpeg` libraries using `conda`. If not, install them using:
|
|
||||||
|
|
||||||
```
|
|
||||||
conda install conda-forge::libopus
|
|
||||||
conda install conda-forge::ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. How can I improve XiaoZhi's dialogue response speed? ⚡
|
|
||||||
|
|
||||||
The default configuration of this project is designed to be cost-effective. It is recommended that beginners first use the default free models to ensure that the system runs smoothly, then optimize for faster response times.
|
|
||||||
To improve response speed, you can try replacing individual components. Below are the response time test results for each component (for reference only, not a guarantee):
|
|
||||||
|
|
||||||
**LLM Performance Ranking:**
|
|
||||||
|
|
||||||
| Module Name | Average First Token Time | Average Total Response Time |
|
|
||||||
|--------------|--------------------------|-----------------------------|
|
|
||||||
| AliLLM | 0.547s | 1.485s |
|
|
||||||
| ChatGLMLLM | 0.677s | 3.057s |
|
|
||||||
| OllamaLLM | 0.003s | 0.003s |
|
|
||||||
|
|
||||||
**TTS Performance Ranking:**
|
|
||||||
|
|
||||||
| Module Name | Average Synthesis Time |
|
|
||||||
|----------------------------|------------------------|
|
|
||||||
| EdgeTTS | 1.019s |
|
|
||||||
| DoubaoTTS | 0.503s |
|
|
||||||
| CosyVoiceSiliconflow | 3.732s |
|
|
||||||
|
|
||||||
**Recommended Configuration Combination (Overall Response Speed):**
|
|
||||||
|
|
||||||
| Combination Scheme | Overall Score | LLM First Token | TTS Synthesis |
|
|
||||||
|-----------------------------------|---------------|-----------------|---------------|
|
|
||||||
| AliLLM + DoubaoTTS | 0.539 | 0.547s | 0.503s |
|
|
||||||
| AliLLM + EdgeTTS | 0.642 | 0.547s | 1.019s |
|
|
||||||
| ChatGLMLLM + DoubaoTTS | 0.642 | 0.677s | 0.503s |
|
|
||||||
| ChatGLMLLM + EdgeTTS | 0.745 | 0.677s | 1.019s |
|
|
||||||
| AliLLM + CosyVoiceSiliconflow | 1.184 | 0.547s | 3.732s |
|
|
||||||
|
|
||||||
**Conclusion 🔍**
|
|
||||||
|
|
||||||
_As of February 19, 2025, if my computer were located in Haizhu District, Guangzhou, Guangdong Province, and connected via China Unicom, I would prioritize using:_
|
|
||||||
|
|
||||||
- **LLM:** `AliLLM`
|
|
||||||
- **TTS:** `DoubaoTTS`
|
|
||||||
|
|
||||||
### 7. For more questions, feel free to contact us for feedback 💬
|
|
||||||
|
|
||||||
Our contact information is in [Baidu Netdisk](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),The extraction code is`223y`。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Acknowledgements 🙏
|
## Acknowledgments 🙏
|
||||||
|
|
||||||
- This project was inspired by the [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it.
|
- This project was inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it.
|
||||||
- Many thanks to [Tenclass](https://www.tenclass.com/) for providing detailed documentation support for the XiaoZhi communication protocol.
|
- 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">
|
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||||
<picture>
|
<picture>
|
||||||
@@ -293,4 +299,4 @@ Our contact information is in [Baidu Netdisk](https://pan.baidu.com/s/1x6USjvP1n
|
|||||||
<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" />
|
||||||
<img alt="Star History Chart" src="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>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# 本文件是用于一键自动下载本项目所需文件,自动创建好目录
|
||||||
|
# 所需条件(否则无法使用):
|
||||||
|
# 1、请确保你的环境可以正常访问 GitHub 否则无法下载脚本
|
||||||
|
#
|
||||||
|
# 检测操作系统类型
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*) OS=Linux;;
|
||||||
|
Darwin*) OS=Mac;;
|
||||||
|
CYGWIN*) OS=Windows;;
|
||||||
|
MINGW*) OS=Windows;;
|
||||||
|
MSYS*) OS=Windows;;
|
||||||
|
*) OS=UNKNOWN;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 设置颜色(Windows CMD 不支持,但不影响使用)
|
||||||
|
if [ "$OS" = "Windows" ]; then
|
||||||
|
GREEN=""
|
||||||
|
RED=""
|
||||||
|
NC=""
|
||||||
|
else
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${GREEN}开始安装小智服务端...${NC}"
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
echo "创建目录结构..."
|
||||||
|
mkdir -p xiaozhi-server/data xiaozhi-server/models/SenseVoiceSmall
|
||||||
|
cd xiaozhi-server || exit
|
||||||
|
|
||||||
|
# 根据操作系统选择下载命令
|
||||||
|
if [ "$OS" = "Windows" ]; then
|
||||||
|
DOWNLOAD_CMD="curl -L -o"
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="powershell -Command Invoke-WebRequest -Uri"
|
||||||
|
DOWNLOAD_CMD_SUFFIX="-OutFile"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="curl -L -o"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
DOWNLOAD_CMD="wget -O"
|
||||||
|
else
|
||||||
|
echo "${RED}错误: 需要安装 curl 或 wget${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 下载语音识别模型
|
||||||
|
echo "下载语音识别模型..."
|
||||||
|
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
|
||||||
|
$DOWNLOAD_CMD "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" $DOWNLOAD_CMD_SUFFIX "models/SenseVoiceSmall/model.pt"
|
||||||
|
else
|
||||||
|
$DOWNLOAD_CMD "models/SenseVoiceSmall/model.pt" "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "${RED}模型下载失败。请手动从以下地址下载:${NC}"
|
||||||
|
echo "1. https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt"
|
||||||
|
echo "2. 百度网盘: https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg (提取码: qvna)"
|
||||||
|
echo "下载后请将文件放置在 models/SenseVoiceSmall/model.pt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 下载配置文件
|
||||||
|
echo "下载配置文件..."
|
||||||
|
if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then
|
||||||
|
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml" $DOWNLOAD_CMD_SUFFIX "docker-compose.yml"
|
||||||
|
$DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml" $DOWNLOAD_CMD_SUFFIX "data/.config.yaml"
|
||||||
|
else
|
||||||
|
$DOWNLOAD_CMD "docker-compose.yml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml"
|
||||||
|
$DOWNLOAD_CMD "data/.config.yaml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
echo "检查文件完整性..."
|
||||||
|
FILES_TO_CHECK="docker-compose.yml data/.config.yaml models/SenseVoiceSmall/model.pt"
|
||||||
|
ALL_FILES_EXIST=true
|
||||||
|
|
||||||
|
for FILE in $FILES_TO_CHECK; do
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "${RED}错误: $FILE 不存在${NC}"
|
||||||
|
ALL_FILES_EXIST=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ALL_FILES_EXIST" = false ]; then
|
||||||
|
echo "${RED}某些文件下载失败,请检查上述错误信息并手动下载缺失的文件。${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${GREEN}文件下载完成!${NC}"
|
||||||
|
echo "请编辑 data/.config.yaml 文件配置你的API密钥。"
|
||||||
|
echo "配置完成后,运行以下命令启动服务:"
|
||||||
|
echo "${GREEN}docker-compose up -d${NC}"
|
||||||
|
echo "查看日志请运行:"
|
||||||
|
echo "${GREEN}docker logs -f xiaozhi-esp32-server${NC}"
|
||||||
|
|
||||||
|
# 提示用户编辑配置文件
|
||||||
|
echo "\n${RED}重要提示:${NC}"
|
||||||
|
echo "1. 请确保编辑 data/.config.yaml 文件,配置必要的API密钥"
|
||||||
|
echo "2. 特别是 ChatGLM 和 mem0ai 的密钥必须配置"
|
||||||
|
echo "3. 配置完成后再启动 docker 服务"
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# 部署方案参考
|
||||||
|

|
||||||
# 方式一:docker快速部署
|
# 方式一:docker快速部署
|
||||||
|
|
||||||
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统上运行。
|
||||||
@@ -6,7 +8,45 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
|||||||
|
|
||||||
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
||||||
|
|
||||||
## 2. 创建目录
|
如果你已经安装好docker,你可以[1.1使用懒人脚本](#11-懒人脚本)自动帮你下载所需的文件和配置文件,你可以使用docker[1.2手动部署](#12-手动部署)。
|
||||||
|
|
||||||
|
### 1.1 懒人脚本
|
||||||
|
|
||||||
|
你可以使用以下命令一键下载并执行部署脚本:
|
||||||
|
请确保你的环境可以正常访问 GitHub 否则无法下载脚本。
|
||||||
|
```bash
|
||||||
|
curl -L -o docker-setup.sh https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您的电脑是windows系统,请使用使用 Git Bash、WSL、PowerShell 或 CMD 运行以下命令:
|
||||||
|
```bash
|
||||||
|
# Git Bash 或 WSL
|
||||||
|
sh docker-setup.sh
|
||||||
|
# PowerShell 或 CMD
|
||||||
|
.\docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
如果您的电脑是linux 或者 macos 系统,请使用终端运行以下命令:
|
||||||
|
```bash
|
||||||
|
chmod +x docker-setup.sh
|
||||||
|
./docker-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会自动完成以下操作:
|
||||||
|
> 1. 创建必要的目录结构
|
||||||
|
> 2. 下载语音识别模型
|
||||||
|
> 3. 下载配置文件
|
||||||
|
> 4. 检查文件完整性
|
||||||
|
>
|
||||||
|
> 执行完成后,请按照提示配置 API 密钥。
|
||||||
|
|
||||||
|
当你一切顺利完成以上操作后,继续操作[配置项目文件](#3-配置项目文件)
|
||||||
|
|
||||||
|
### 1.2 手动部署
|
||||||
|
|
||||||
|
如果懒人脚本无法正常运行,请按本章节1.2进行手动部署。
|
||||||
|
|
||||||
|
#### 1.2.1 创建目录
|
||||||
|
|
||||||
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
||||||
|
|
||||||
@@ -21,14 +61,18 @@ xiaozhi-server
|
|||||||
├─ SenseVoiceSmall
|
├─ SenseVoiceSmall
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. 下载语音识别模型文件
|
#### 1.2.2 下载语音识别模型文件
|
||||||
|
|
||||||
你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
|
你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载
|
||||||
[跳转到下载语音识别模型文件](#模型文件)
|
[跳转到下载语音识别模型文件](#模型文件)
|
||||||
|
|
||||||
下载完后,回到本教程。
|
下载完后,回到本教程。
|
||||||
|
|
||||||
## 3. 下载docker-compose.yaml
|
#### 1.2.3 下载配置文件
|
||||||
|
|
||||||
|
你需要下载两个配置文件:`docker-compose.yaml` 和 `config.yaml`。需要从项目仓库下载这两个文件。
|
||||||
|
|
||||||
|
##### 1.2.3.1 下载 docker-compose.yaml
|
||||||
|
|
||||||
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
|
||||||
|
|
||||||
@@ -37,7 +81,7 @@ xiaozhi-server
|
|||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
## 3. 下载配置文件
|
##### 1.2.3.2 下载 config.yaml
|
||||||
|
|
||||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||||
|
|
||||||
@@ -58,14 +102,14 @@ xiaozhi-server
|
|||||||
|
|
||||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
## 4. 配置项目文件
|
## 3. 配置项目文件
|
||||||
|
|
||||||
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
||||||
[跳转到配置项目文件](#配置项目)
|
[跳转到配置项目文件](#配置项目)
|
||||||
|
|
||||||
配置完项目文件后,回到本教程继续往下。
|
配置完项目文件后,回到本教程继续往下。
|
||||||
|
|
||||||
## 5. 执行docker命令
|
## 4. 执行docker命令
|
||||||
|
|
||||||
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
@@ -81,22 +125,22 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
|
|
||||||
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认)
|
||||||
|
|
||||||
## 6.版本升级操作
|
## 5. 版本升级操作
|
||||||
|
|
||||||
如果后期想升级版本,可以这么操作
|
如果后期想升级版本,可以这么操作
|
||||||
|
|
||||||
1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
|
5.1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。
|
||||||
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
|
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
|
||||||
|
|
||||||
2、执行以下命令
|
5.2、执行以下命令
|
||||||
|
|
||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
docker stop xiaozhi-esp32-server
|
||||||
docker rm xiaozhi-esp32-server
|
docker rm xiaozhi-esp32-server
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
3、重新按docker方式部署
|
5.3、重新按docker方式部署
|
||||||
|
|
||||||
# 方式二:借助Docker环境运行部署
|
# 方式二:借助Docker环境运行部署
|
||||||
|
|
||||||
@@ -117,7 +161,7 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
|
|||||||
-p 8000:8000 \
|
-p 8000:8000 \
|
||||||
-p 8002:8002 \
|
-p 8002:8002 \
|
||||||
-v ./:/app \
|
-v ./:/app \
|
||||||
kalicyh/poetry:v3.10_xiaozhi
|
kalicyh/python:xiaozhi
|
||||||
```
|
```
|
||||||
|
|
||||||
然后就和正常开发一样了
|
然后就和正常开发一样了
|
||||||
@@ -127,29 +171,13 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
|
|||||||
在刚刚的打开的终端运行
|
在刚刚的打开的终端运行
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
poetry install --no-root
|
pip install -r requirements.txt
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --no-install-recommends libopus0 ffmpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
速度慢可以尝试使用清华镜像
|
|
||||||
|
|
||||||
```sh
|
|
||||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list
|
|
||||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
|
||||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
|
||||||
echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --no-install-recommends libopus0 ffmpeg
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6.运行项目
|
## 6.运行项目
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
poetry run python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
# 方式三:本地源码运行
|
# 方式三:本地源码运行
|
||||||
@@ -213,7 +241,7 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
## 4.配置项目文件
|
## 4.配置项目文件
|
||||||
|
|
||||||
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
接下来,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
||||||
[跳转到配置项目文件](#配置项目)
|
[跳转到配置项目文件](#配置项目)
|
||||||
|
|
||||||
## 5.运行项目
|
## 5.运行项目
|
||||||
@@ -233,20 +261,23 @@ python app.py
|
|||||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
||||||
|
|
||||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的两个配置。
|
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
||||||
|
|
||||||
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||||
- 默认的记忆层`mem0ai`,你需要配置密钥,因为他们的API,虽然有免费额度,但是仍要去[官网](https://app.mem0.ai/dashboard/api-keys)注册密钥,才能启动。
|
|
||||||
|
|
||||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
||||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
||||||
|
|
||||||
```
|
```
|
||||||
selected_module:
|
selected_module:
|
||||||
ASR: FunASR
|
|
||||||
VAD: SileroVAD
|
VAD: SileroVAD
|
||||||
|
ASR: FunASR
|
||||||
LLM: ChatGLMLLM
|
LLM: ChatGLMLLM
|
||||||
TTS: EdgeTTS
|
TTS: EdgeTTS
|
||||||
|
# 默认不开启记忆,如需开启请看配置文件里的描述
|
||||||
|
Memory: nomem
|
||||||
|
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
||||||
|
Intent: nointent
|
||||||
```
|
```
|
||||||
|
|
||||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
||||||
@@ -265,8 +296,6 @@ LLM:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
有些服务,比如如果你使用`Dify`、`豆包的TTS`,是需要密钥的,记得在配置文件加上哦!
|
|
||||||
|
|
||||||
## 模型文件
|
## 模型文件
|
||||||
|
|
||||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
@@ -282,16 +311,33 @@ LLM:
|
|||||||
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
||||||
|
|
||||||
```
|
```
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000
|
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协议地址,请勿用浏览器访问=======
|
||||||
|
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 - =======================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
||||||
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
||||||
|
|
||||||
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
||||||
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000`。
|
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`。
|
||||||
|
|
||||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||||
|
|
||||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
接下来,你就可以开始 [编译esp32固件](firmware-build.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)
|
||||||
|
|
||||||
|
[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)
|
||||||
|
|
||||||
|
[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)
|
||||||
|
|
||||||
|
[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)
|
||||||
|
|
||||||
|
[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)
|
||||||
|
|
||||||
|
[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)
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# 常见问题 ❓
|
||||||
|
|
||||||
|
### 1、为什么我说的话,小智识别出来很多韩文、日文、英文?🇰🇷
|
||||||
|
|
||||||
|
建议:检查一下`models/SenseVoiceSmall`是否已经有`model.pt`
|
||||||
|
文件,如果没有就要下载,查看这里[下载语音识别模型文件](Deployment.md#模型文件)
|
||||||
|
|
||||||
|
### 2、为什么会出现"TTS 任务出错 文件不存在"?📁
|
||||||
|
|
||||||
|
建议:检查一下是否正确使用`conda` 安装了`libopus`和`ffmpeg`库。
|
||||||
|
|
||||||
|
如果没有安装,就安装
|
||||||
|
|
||||||
|
```
|
||||||
|
conda install conda-forge::libopus
|
||||||
|
conda install conda-forge::ffmpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3、TTS 经常失败,经常超时 ⏰
|
||||||
|
|
||||||
|
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
||||||
|
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
||||||
|
|
||||||
|
### 4、使用Wifi能连接自建服务器,但是4G模式却接不上 🔐
|
||||||
|
|
||||||
|
原因:虾哥的固件,4G模式需要使用安全连接。
|
||||||
|
|
||||||
|
解决方法:目前有两种方法可以解决。任选一种:
|
||||||
|
|
||||||
|
1、改代码。参考这个视频解决 https://www.bilibili.com/video/BV18MfTYoE85
|
||||||
|
|
||||||
|
2、使用nginx配置ssl证书。参考教程 https://icnt94i5ctj4.feishu.cn/docx/GnYOdMNJOoRCljx1ctecsj9cnRe
|
||||||
|
|
||||||
|
### 5、如何提高小智对话响应速度? ⚡
|
||||||
|
|
||||||
|
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决"跑得动"的问题,再优化"跑得快"。
|
||||||
|
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
||||||
|
|
||||||
|
| 影响因素 | 因素值 |
|
||||||
|
|:-----:|:----------------:|
|
||||||
|
| 测试地点 | 广东省广州市海珠区 |
|
||||||
|
| 测试时间 | 2025年2月19日 12:52 |
|
||||||
|
| 宽带运营商 | 中国联通 |
|
||||||
|
|
||||||
|
测试方法:
|
||||||
|
|
||||||
|
1、把各组件的密钥配置上去,只有配置了密钥的组件才参与测试。
|
||||||
|
|
||||||
|
2、配置完密钥后,执行以下方法
|
||||||
|
|
||||||
|
```
|
||||||
|
# 进入项目根目录,执行以下命令:
|
||||||
|
conda activate xiaozhi-esp32-server
|
||||||
|
python performance_tester.py
|
||||||
|
```
|
||||||
|
|
||||||
|
生成报告如下
|
||||||
|
|
||||||
|
LLM 性能排行:
|
||||||
|
|
||||||
|
| 模块名称 | 平均首Token时间 | 平均总响应时间 |
|
||||||
|
|:-----------|:-----------|:--------|
|
||||||
|
| AliLLM | 0.547s | 1.485s |
|
||||||
|
| ChatGLMLLM | 0.677s | 3.057s |
|
||||||
|
|
||||||
|
TTS 性能排行:
|
||||||
|
|
||||||
|
| 模块名称 | 平均合成时间 |
|
||||||
|
|----------------------|--------|
|
||||||
|
| EdgeTTS | 1.019s |
|
||||||
|
| DoubaoTTS | 0.503s |
|
||||||
|
| CosyVoiceSiliconflow | 3.732s |
|
||||||
|
|
||||||
|
推荐配置组合 (综合响应速度):
|
||||||
|
|
||||||
|
| 组合方案 | 综合得分 | LLM首Token | TTS合成 |
|
||||||
|
|-------------------------------|-------|-----------|--------|
|
||||||
|
| AliLLM + DoubaoTTS | 0.539 | 0.547s | 0.503s |
|
||||||
|
| AliLLM + EdgeTTS | 0.642 | 0.547s | 1.019s |
|
||||||
|
| ChatGLMLLM + DoubaoTTS | 0.642 | 0.677s | 0.503s |
|
||||||
|
| ChatGLMLLM + EdgeTTS | 0.745 | 0.677s | 1.019s |
|
||||||
|
| AliLLM + CosyVoiceSiliconflow | 1.184 | 0.547s | 3.732s |
|
||||||
|
|
||||||
|
### 结论 🔍
|
||||||
|
|
||||||
|
`2025年2月19日`,如果我的电脑在`广东省广州市海珠区`,且使用的是`中国联通`网络,我会优先使用:
|
||||||
|
|
||||||
|
- LLM:`AliLLM`
|
||||||
|
- TTS:`DoubaoTTS`
|
||||||
|
|
||||||
|
### 6、我说话很慢,停顿时小智老是抢话 🗣️
|
||||||
|
|
||||||
|
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
VAD:
|
||||||
|
SileroVAD:
|
||||||
|
threshold: 0.5
|
||||||
|
model_dir: models/snakers4_silero-vad
|
||||||
|
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||||
|
|
||||||
|
本项目,支持以工具调用的方式控制HomeAssistant设备
|
||||||
|
|
||||||
|
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、更多问题,可联系我们反馈 💬
|
||||||
|
|
||||||
|
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||||
|
|
||||||
|
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
|
||||||
|
|
||||||
|
- 硬件烧录QQ群:适用于硬件烧录问题
|
||||||
|
- 开源服务端交流群:适用于服务端问题
|
||||||
|
- 产品建议联系人:适用于产品功能、产品设计等建议
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 致开发者的公开信
|
||||||
|
|
||||||
|
"春江水暖鸭先知,正是河豚欲上时!"
|
||||||
|
|
||||||
|
亲爱的朋友,我是John,是一名普通公司里的Java程序员,今天,我怀着无比真挚的心情,向热爱AI技术与创新的你发出这封公开信。
|
||||||
|
|
||||||
|
半年前我看到很多优秀的项目,比如`Dify`、`Chat2DB`等人工智能相关的项目,我在想,我要是能参与这些项目多好,可惜“报国无门,空打十年代码”。
|
||||||
|
|
||||||
|
我是2025年初刷到虾哥团队的视频,我非常好奇他是怎么实现的,我想复刻他们的后端服务,打造一个低成本民用贾维斯。很可惜现在做的作品依然只是一个人工智障,它并发低、没有灵魂,响应很慢,bug很多。
|
||||||
|
|
||||||
|
虾哥团队是我们学习的对象,我很想拥有像虾哥团队一样智能的小智后端服务。但是我也能理解虾哥不开源的决定。“一花独放不是春,百花齐放春满园”,人工智能遍地开花的时代,也许就在我们这代实现,我们可以用自己的双手,实现低成本民用贾维斯。我个人认为,他能实现的,我们也能实现,只是时间问题而已,我称之为“我们的取经之路”。
|
||||||
|
|
||||||
|
那么这条取经之路,我们会遇到什么困难?我想应该不少于八十一难。这一路必然会出现各种妖怪,当然也有神仙暗中帮助我们,也有人加入取经队伍。
|
||||||
|
|
||||||
|
以上内容,如果你觉得好笑。那我也觉得非常的幸运。我能够在你人生3万多天里博你笑五秒,也算是为你做了一次贡献。
|
||||||
|
|
||||||
|
民用低成本贾维斯这个想法,会失败吗,我不知道,但是我们普通人的一生,这种失败不是很常见吗?
|
||||||
|
|
||||||
|
未来,有一点是可以确定的,就一定会有人完全复刻虾哥团队的功能,实现民用低成本贾维斯。这个项目会是我们吗?
|
||||||
|
|
||||||
|
期待与你携手前行,共创未来。
|
||||||
|
|
||||||
|
John,2025.3.11,广州
|
||||||
|
|
||||||
|
# 附 开发贡献指南
|
||||||
|
## 项目目标
|
||||||
|
|
||||||
|
1. **民用低成本贾维斯解决方案**
|
||||||
|
|
||||||
|
2. **智能联动周边硬件的解决方案**
|
||||||
|
|
||||||
|
## 加入我们
|
||||||
|
|
||||||
|
我们热忱欢迎志同道合的朋友加入,共同为项目贡献力量。您可在[这个链接](https://github.com/users/xinnan-tech/projects/3)查看我们近期要实现的功能,功能列表中还没指派相关人员处理的,正是急需您的参与。参与方式如下:
|
||||||
|
|
||||||
|
### 1、成为普通贡献者
|
||||||
|
|
||||||
|
Fork 项目,提交 PR,由开发者审核后合入主分支。
|
||||||
|
|
||||||
|
### 2、成为开发者
|
||||||
|
|
||||||
|
当你累计提交 3 次有效 PR 后,可以联系群主申请成为开发者,群主将邀请你加入独立的开发者群,共同探讨项目未来。
|
||||||
|
|
||||||
|
## 开发者开发流程
|
||||||
|
|
||||||
|
1. **创建新分支**
|
||||||
|
每个功能点请以新分支方式开发,分支名称应简洁明了,让人一眼看出所实现的功能,避免功能撞车。
|
||||||
|
|
||||||
|
2. **提交 PR 审核**
|
||||||
|
功能开发完成后,请在 GitHub 上提交 PR,由其他开发者审核,审核通过后合并入主分支。
|
||||||
@@ -8,41 +8,14 @@ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin
|
|||||||
```
|
```
|
||||||
2、编译docker镜像
|
2、编译docker镜像
|
||||||
```
|
```
|
||||||
#进入xiaozhi-server目录
|
#进入项目根目录
|
||||||
cd main/xiaozhi-server/
|
# 编译server
|
||||||
# 普通编译
|
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
|
||||||
docker build -t xiaozhi-esp32-server:local -f ./Dockerfile-pip .
|
# 编译web
|
||||||
```
|
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
|
||||||
3、测试本地镜像
|
|
||||||
```
|
|
||||||
docker stop xiaozhi-esp32-server
|
|
||||||
docker rm xiaozhi-esp32-server
|
|
||||||
|
|
||||||
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/data/.config.yaml:/opt/xiaozhi-esp32-server/config.yaml xiaozhi-esp32-server:local
|
|
||||||
|
|
||||||
docker logs -f xiaozhi-esp32-server
|
|
||||||
|
|
||||||
|
# 编译完成后,可以使用docker-compose启动项目
|
||||||
|
# docker-compose.yml你需要修改成自己编译的镜像版本
|
||||||
|
cd main/xiaozhi-server
|
||||||
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
5、发布腾讯云镜像
|
|
||||||
```
|
|
||||||
# amd64
|
|
||||||
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
|
|
||||||
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64
|
|
||||||
|
|
||||||
# arm64
|
|
||||||
docker tag xiaozhi-esp32-server:local ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
|
|
||||||
docker push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64
|
|
||||||
|
|
||||||
# 推送最新版本
|
|
||||||
docker manifest rm ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
docker manifest create ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-amd64 ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest-arm64 --amend
|
|
||||||
docker manifest inspect ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
docker manifest push ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
|
|
||||||
```
|
|
||||||
6、运行线上镜像
|
|
||||||
```
|
|
||||||
cd /Users/hrz/myworkspace/docker-java-env/thirddata/
|
|
||||||
docker run -d --name xiaozhi-esp32-server --restart always -p 8000:8000 -v $(pwd)/config.yaml:/opt/xiaozhi-esp32-server/config.yaml ccr.ccs.tencentyun.com/xinnan/xiaozhi-esp32-server:latest
|
|
||||||
docker logs -f xiaozhi-esp32-server
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
server {
|
||||||
|
listen 8002;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# 静态资源服务(Vue项目)
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API反向代理(Java项目)
|
||||||
|
location /xiaozhi/ {
|
||||||
|
proxy_pass http://127.0.0.1:8003;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cookie_path /api/ /;
|
||||||
|
proxy_set_header Referer $http_referer;
|
||||||
|
proxy_set_header Cookie $http_cookie;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10;
|
||||||
|
proxy_send_timeout 10;
|
||||||
|
proxy_read_timeout 10;
|
||||||
|
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 启动Java后端(docker内监听8003端口)
|
||||||
|
java -jar /app/xiaozhi-esp32-api.jar \
|
||||||
|
--server.port=8003 \
|
||||||
|
--spring.datasource.druid.url=${SPRING_DATASOURCE_DRUID_URL} \
|
||||||
|
--spring.datasource.druid.username=${SPRING_DATASOURCE_DRUID_USERNAME} \
|
||||||
|
--spring.datasource.druid.password=${SPRING_DATASOURCE_DRUID_PASSWORD} \
|
||||||
|
--spring.data.redis.host=${SPRING_DATA_REDIS_HOST} \
|
||||||
|
--spring.data.redis.port=${SPRING_DATA_REDIS_PORT} &
|
||||||
|
|
||||||
|
# 启动Nginx(前台运行保持容器存活)
|
||||||
|
nginx -g 'daemon off;'
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
项目,按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
项目,按照这个教程配置项目环境[《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`
|
2. 打开`xiaozhi-esp32/main/Kconfig.projbuild`文件,找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net`
|
||||||
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000`,就把内容改成这个。
|
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
|
||||||
|
|
||||||
修改前:
|
修改前:
|
||||||
|
|
||||||
@@ -87,3 +87,19 @@ https://espressif.github.io/esp-launchpad/
|
|||||||
|
|
||||||
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
|
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
|
||||||
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
|
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
|
||||||
|
|
||||||
|
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||||
|
|
||||||
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./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#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#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、如何提高小智对话响应速度?](./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#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#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-)
|
||||||
|
|||||||
|
After Width: | Height: | Size: 396 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 576 KiB |
|
Before Width: | Height: | Size: 484 KiB |
@@ -1,9 +1,9 @@
|
|||||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](./FAQ.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
||||||
|
|
||||||
# 项目目录介绍
|
# 项目目录介绍
|
||||||
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
|
当你看到这份文件的时候,这个这个项目还没完善好。我们还有很多东西要做。
|
||||||
|
|
||||||
如果你会开发,我们非常欢迎您的加入。
|
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](../docs/contributor_open_letter.md),欢迎归队!
|
||||||
|
|
||||||
```
|
```
|
||||||
xiaozhi-esp32-server
|
xiaozhi-esp32-server
|
||||||
@@ -18,6 +18,4 @@ xiaozhi-esp32-server
|
|||||||
|
|
||||||
# manager-web 、manager-api接口协议
|
# manager-web 、manager-api接口协议
|
||||||
|
|
||||||
[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)
|
https://2662r3426b.vicp.fun/xiaozhi/v1/doc.html
|
||||||
|
|
||||||
[前端页面设计图](https://codesign.qq.com/app/s/526108506410828)
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../README.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
|
||||||
# 项目介绍
|
# 项目介绍
|
||||||
|
|
||||||
manager-api 该项目基于SpringBoot框架开发。
|
manager-api 该项目基于SpringBoot框架开发。
|
||||||
|
|
||||||
开发使用代码编辑器,导入项目时,选择`manager-api`文件夹作为项目目录
|
开发使用代码编辑器,导入项目时,选择`manager-api`文件夹作为项目目录
|
||||||
|
|
||||||
参照[manager前后端接口协议](https://app.apifox.com/invite/project?token=H_8qhgfjUeaAL0wybghgU)开发
|
|
||||||
|
|
||||||
# 开发环境
|
# 开发环境
|
||||||
JDK 21
|
JDK 21
|
||||||
Maven 3.8+
|
Maven 3.8+
|
||||||
MySQL 8.0+
|
MySQL 8.0+
|
||||||
|
Redis 5.0+
|
||||||
Vue 3.x
|
Vue 3.x
|
||||||
|
|
||||||
# 创建数据库
|
# 创建数据库
|
||||||
@@ -43,6 +42,30 @@ spring:
|
|||||||
password: 123456
|
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项目,启动方式为:
|
本项目为SpringBoot项目,启动方式为:
|
||||||
@@ -58,15 +81,16 @@ src/main/java/xiaozhi/AdminApplication.java
|
|||||||
执行以下命令生产jar包
|
执行以下命令生产jar包
|
||||||
|
|
||||||
```
|
```
|
||||||
mvn install
|
mvn clean install
|
||||||
```
|
```
|
||||||
|
|
||||||
把jar包放在服务器上,执行
|
把jar包放在服务器上,执行
|
||||||
|
|
||||||
```
|
```
|
||||||
nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.active=dev >/dev/null &
|
nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.activate=dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# 接口文档
|
# 接口文档
|
||||||
启动后打开:http://localhost:8002/xiaozhi-esp32-api/doc.html
|
启动后打开:http://localhost:8002/xiaozhi/doc.html
|
||||||
|
|
||||||
|
|||||||
@@ -18,37 +18,19 @@
|
|||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<junit.version>5.12.0</junit.version>
|
<junit.version>5.10.1</junit.version>
|
||||||
<druid.version>1.2.20</druid.version>
|
<druid.version>1.2.20</druid.version>
|
||||||
<mybatisplus.version>3.5.5</mybatisplus.version>
|
<mybatisplus.version>3.5.5</mybatisplus.version>
|
||||||
<hutool.version>5.8.24</hutool.version>
|
<hutool.version>5.8.24</hutool.version>
|
||||||
<jsoup.version>1.19.1</jsoup.version>
|
<jsoup.version>1.19.1</jsoup.version>
|
||||||
<jasypt.version>3.0.5</jasypt.version>
|
|
||||||
<knife4j.version>4.6.0</knife4j.version>
|
<knife4j.version>4.6.0</knife4j.version>
|
||||||
<quartz.version>2.3.2</quartz.version>
|
|
||||||
<shiro.version>2.0.2</shiro.version>
|
<shiro.version>2.0.2</shiro.version>
|
||||||
<captcha.version>1.6.2</captcha.version>
|
<captcha.version>1.6.2</captcha.version>
|
||||||
<guava.version>33.0.0-jre</guava.version>
|
<guava.version>33.0.0-jre</guava.version>
|
||||||
<easyexcel.version>3.3.2</easyexcel.version>
|
|
||||||
<liquibase-core.version>4.20.0</liquibase-core.version>
|
<liquibase-core.version>4.20.0</liquibase-core.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>org.quartz-scheduler</groupId>
|
|
||||||
<artifactId>quartz</artifactId>
|
|
||||||
<version>${quartz.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>com.mchange</groupId>
|
|
||||||
<artifactId>c3p0</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>com.zaxxer</groupId>
|
|
||||||
<artifactId>HikariCP-java6</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.shiro</groupId>
|
<groupId>org.apache.shiro</groupId>
|
||||||
<artifactId>shiro-core</artifactId>
|
<artifactId>shiro-core</artifactId>
|
||||||
@@ -83,6 +65,7 @@
|
|||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- 验证码工具包 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.whvcse</groupId>
|
<groupId>com.github.whvcse</groupId>
|
||||||
<artifactId>easy-captcha</artifactId>
|
<artifactId>easy-captcha</artifactId>
|
||||||
@@ -104,6 +87,36 @@
|
|||||||
<version>${junit.version}</version>
|
<version>${junit.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.platform</groupId>
|
||||||
|
<artifactId>junit-platform-launcher</artifactId>
|
||||||
|
<version>1.10.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.platform</groupId>
|
||||||
|
<artifactId>junit-platform-commons</artifactId>
|
||||||
|
<version>1.10.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.platform</groupId>
|
||||||
|
<artifactId>junit-platform-engine</artifactId>
|
||||||
|
<version>1.10.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ public class AdminApplication {
|
|||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(AdminApplication.class, args);
|
SpringApplication.run(AdminApplication.class, args);
|
||||||
System.out.println("http://localhost:8002/xiaozhi-esp32-api/doc.html");
|
System.out.println("http://localhost:8002/xiaozhi/doc.html");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
package xiaozhi.common.annotation;
|
package xiaozhi.common.annotation;
|
||||||
|
|
||||||
import java.lang.annotation.*;
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据过滤注解
|
* 数据过滤注解
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package xiaozhi.common.annotation;
|
package xiaozhi.common.annotation;
|
||||||
|
|
||||||
import java.lang.annotation.*;
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 操作日志注解
|
* 操作日志注解
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
package xiaozhi.common.aspect;
|
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
|
||||||
import xiaozhi.common.annotation.DataFilter;
|
|
||||||
import xiaozhi.common.constant.Constant;
|
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
|
||||||
import xiaozhi.common.exception.RenException;
|
|
||||||
import xiaozhi.common.interceptor.DataScope;
|
|
||||||
import xiaozhi.common.user.UserDetail;
|
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
|
||||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.aspectj.lang.JoinPoint;
|
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
|
||||||
import org.aspectj.lang.annotation.Before;
|
|
||||||
import org.aspectj.lang.annotation.Pointcut;
|
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据过滤,切面处理类
|
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
|
||||||
* Website: https://www.renren.io
|
|
||||||
*/
|
|
||||||
@Aspect
|
|
||||||
@Component
|
|
||||||
public class DataFilterAspect {
|
|
||||||
|
|
||||||
@Pointcut("@annotation(xiaozhi.common.annotation.DataFilter)")
|
|
||||||
public void dataFilterCut() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before("dataFilterCut()")
|
|
||||||
public void dataFilter(JoinPoint point) {
|
|
||||||
Object params = point.getArgs()[0];
|
|
||||||
if (params != null && params instanceof Map) {
|
|
||||||
UserDetail user = SecurityUser.getUser();
|
|
||||||
|
|
||||||
//如果是超级管理员,则不进行数据过滤
|
|
||||||
if (user.getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
//否则进行数据过滤
|
|
||||||
Map map = (Map) params;
|
|
||||||
String sqlFilter = getSqlFilter(user, point);
|
|
||||||
map.put(Constant.SQL_FILTER, new DataScope(sqlFilter));
|
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new RenException(ErrorCode.DATA_SCOPE_PARAMS_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取数据过滤的SQL
|
|
||||||
*/
|
|
||||||
private String getSqlFilter(UserDetail user, JoinPoint point) throws Exception {
|
|
||||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
|
||||||
Method method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getParameterTypes());
|
|
||||||
DataFilter dataFilter = method.getAnnotation(DataFilter.class);
|
|
||||||
|
|
||||||
//获取表的别名
|
|
||||||
String tableAlias = dataFilter.tableAlias();
|
|
||||||
if (StringUtils.isNotBlank(tableAlias)) {
|
|
||||||
tableAlias += ".";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sqlFilter = new StringBuilder();
|
|
||||||
sqlFilter.append(" (");
|
|
||||||
|
|
||||||
//部门ID列表
|
|
||||||
List<Long> deptIdList = user.getDeptIdList();
|
|
||||||
if (CollUtil.isNotEmpty(deptIdList)) {
|
|
||||||
sqlFilter.append(tableAlias).append(dataFilter.deptId());
|
|
||||||
|
|
||||||
sqlFilter.append(" in(").append(StringUtils.join(deptIdList, ",")).append(")");
|
|
||||||
}
|
|
||||||
|
|
||||||
//查询本人数据
|
|
||||||
if (CollUtil.isNotEmpty(deptIdList)) {
|
|
||||||
sqlFilter.append(" or ");
|
|
||||||
}
|
|
||||||
sqlFilter.append(tableAlias).append(dataFilter.userId()).append("=").append(user.getId());
|
|
||||||
|
|
||||||
sqlFilter.append(")");
|
|
||||||
|
|
||||||
return sqlFilter.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,41 @@
|
|||||||
package xiaozhi.common.aspect;
|
package xiaozhi.common.aspect;
|
||||||
|
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import xiaozhi.common.exception.RenException;
|
import org.aspectj.lang.annotation.Around;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.aspectj.lang.annotation.Around;
|
import org.springframework.stereotype.Component;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
/**
|
|
||||||
* Redis切面处理类
|
/**
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Redis切面处理类
|
||||||
* Website: https://www.renren.io
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
*/
|
* Website: https://www.renren.io
|
||||||
@Slf4j
|
*/
|
||||||
@Aspect
|
@Slf4j
|
||||||
@Component
|
@Aspect
|
||||||
public class RedisAspect {
|
@Component
|
||||||
/**
|
public class RedisAspect {
|
||||||
* 是否开启redis缓存 true开启 false关闭
|
/**
|
||||||
*/
|
* 是否开启redis缓存 true开启 false关闭
|
||||||
@Value("${renren.redis.open}")
|
*/
|
||||||
private boolean open;
|
@Value("${renren.redis.open}")
|
||||||
|
private boolean open;
|
||||||
@Around("execution(* xiaozhi.common.redis.RedisUtils.*(..))")
|
|
||||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
@Around("execution(* xiaozhi.common.redis.RedisUtils.*(..))")
|
||||||
Object result = null;
|
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||||
if (open) {
|
Object result = null;
|
||||||
try {
|
if (open) {
|
||||||
result = point.proceed();
|
try {
|
||||||
} catch (Exception e) {
|
result = point.proceed();
|
||||||
log.error("redis error", e);
|
} catch (Exception e) {
|
||||||
throw new RenException(ErrorCode.REDIS_ERROR);
|
log.error("redis error", e);
|
||||||
}
|
throw new RenException(ErrorCode.REDIS_ERROR);
|
||||||
}
|
}
|
||||||
return result;
|
}
|
||||||
}
|
return result;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package xiaozhi.common.config;
|
package xiaozhi.common.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
|
|
||||||
import xiaozhi.common.interceptor.DataFilterInterceptor;
|
import xiaozhi.common.interceptor.DataFilterInterceptor;
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mybatis-plus配置
|
* mybatis-plus配置
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package xiaozhi.common.config;
|
package xiaozhi.common.config;
|
||||||
|
|
||||||
import io.swagger.v3.oas.models.OpenAPI;
|
|
||||||
import io.swagger.v3.oas.models.info.Info;
|
|
||||||
import org.springdoc.core.models.GroupedOpenApi;
|
import org.springdoc.core.models.GroupedOpenApi;
|
||||||
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 io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swagger配置
|
* Swagger配置
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
@@ -14,11 +15,60 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class SwaggerConfig {
|
public class SwaggerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi deviceApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("device")
|
||||||
|
.pathsToMatch("/device/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi agentApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("agent")
|
||||||
|
.pathsToMatch("/agent/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi modelApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("models")
|
||||||
|
.pathsToMatch("/models/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi oatApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("ota")
|
||||||
|
.pathsToMatch("/ota/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi timbreApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("timbre")
|
||||||
|
.pathsToMatch("/timbre/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi sysApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("admin")
|
||||||
|
.pathsToMatch("/admin/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public GroupedOpenApi userApi() {
|
public GroupedOpenApi userApi() {
|
||||||
String[] paths = {"/**"};
|
return GroupedOpenApi.builder()
|
||||||
return GroupedOpenApi.builder().group("xiaozhi")
|
.group("user")
|
||||||
.pathsToMatch(paths).build();
|
.pathsToMatch("/user/**")
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -73,10 +73,8 @@ public interface Constant {
|
|||||||
* 排序方式
|
* 排序方式
|
||||||
*/
|
*/
|
||||||
String ORDER = "order";
|
String ORDER = "order";
|
||||||
/**
|
|
||||||
* token header
|
String AUTHORIZATION = "Authorization";
|
||||||
*/
|
|
||||||
String TOKEN_HEADER = "token";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径分割符
|
* 路径分割符
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package xiaozhi.common.convert;
|
package xiaozhi.common.convert;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日期转换
|
* 日期转换
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package xiaozhi.common.entity;
|
package xiaozhi.common.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import lombok.Data;
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础实体类,所有实体都需要继承
|
* 基础实体类,所有实体都需要继承
|
||||||
|
|||||||
@@ -22,17 +22,25 @@ public interface ErrorCode {
|
|||||||
int CAPTCHA_ERROR = 10007;
|
int CAPTCHA_ERROR = 10007;
|
||||||
int SUB_MENU_EXIST = 10008;
|
int SUB_MENU_EXIST = 10008;
|
||||||
int PASSWORD_ERROR = 10009;
|
int PASSWORD_ERROR = 10009;
|
||||||
|
|
||||||
int SUPERIOR_DEPT_ERROR = 10011;
|
int SUPERIOR_DEPT_ERROR = 10011;
|
||||||
int SUPERIOR_MENU_ERROR = 10012;
|
int SUPERIOR_MENU_ERROR = 10012;
|
||||||
int DATA_SCOPE_PARAMS_ERROR = 10013;
|
int DATA_SCOPE_PARAMS_ERROR = 10013;
|
||||||
int DEPT_SUB_DELETE_ERROR = 10014;
|
int DEPT_SUB_DELETE_ERROR = 10014;
|
||||||
int DEPT_USER_DELETE_ERROR = 10015;
|
int DEPT_USER_DELETE_ERROR = 10015;
|
||||||
|
|
||||||
int UPLOAD_FILE_EMPTY = 10019;
|
int UPLOAD_FILE_EMPTY = 10019;
|
||||||
int TOKEN_NOT_EMPTY = 10020;
|
int TOKEN_NOT_EMPTY = 10020;
|
||||||
int TOKEN_INVALID = 10021;
|
int TOKEN_INVALID = 10021;
|
||||||
int ACCOUNT_LOCK = 10022;
|
int ACCOUNT_LOCK = 10022;
|
||||||
|
|
||||||
int OSS_UPLOAD_FILE_ERROR = 10024;
|
int OSS_UPLOAD_FILE_ERROR = 10024;
|
||||||
|
|
||||||
int REDIS_ERROR = 10027;
|
int REDIS_ERROR = 10027;
|
||||||
int JOB_ERROR = 10028;
|
int JOB_ERROR = 10028;
|
||||||
int INVALID_SYMBOL = 10029;
|
int INVALID_SYMBOL = 10029;
|
||||||
|
int PASSWORD_LENGTH_ERROR = 10030;
|
||||||
|
int PASSWORD_WEAK_ERROR = 10031;
|
||||||
|
int DEL_MYSELF_ERROR = 10032;
|
||||||
|
int DEVICE_CAPTCHA_ERROR = 10033;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package xiaozhi.common.exception;
|
package xiaozhi.common.exception;
|
||||||
|
|
||||||
|
|
||||||
import xiaozhi.common.utils.MessageUtils;
|
import xiaozhi.common.utils.MessageUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,7 +9,6 @@ import xiaozhi.common.utils.MessageUtils;
|
|||||||
*/
|
*/
|
||||||
public class RenException extends RuntimeException {
|
public class RenException extends RuntimeException {
|
||||||
|
|
||||||
|
|
||||||
private int code;
|
private int code;
|
||||||
private String msg;
|
private String msg;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package xiaozhi.common.exception;
|
package xiaozhi.common.exception;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.shiro.authz.UnauthorizedException;
|
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 xiaozhi.common.utils.Result;
|
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异常处理器
|
* 异常处理器
|
||||||
@@ -23,35 +23,34 @@ public class RenExceptionHandler {
|
|||||||
* 处理自定义异常
|
* 处理自定义异常
|
||||||
*/
|
*/
|
||||||
@ExceptionHandler(RenException.class)
|
@ExceptionHandler(RenException.class)
|
||||||
public Result handleRenException(RenException ex) {
|
public Result<Void> handleRenException(RenException ex) {
|
||||||
Result result = new Result();
|
Result<Void> result = new Result<>();
|
||||||
result.error(ex.getCode(), ex.getMsg());
|
result.error(ex.getCode(), ex.getMsg());
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(DuplicateKeyException.class)
|
@ExceptionHandler(DuplicateKeyException.class)
|
||||||
public Result handleDuplicateKeyException(DuplicateKeyException ex) {
|
public Result<Void> handleDuplicateKeyException(DuplicateKeyException ex) {
|
||||||
Result result = new Result();
|
Result<Void> result = new Result<>();
|
||||||
result.error(ErrorCode.DB_RECORD_EXISTS);
|
result.error(ErrorCode.DB_RECORD_EXISTS);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(UnauthorizedException.class)
|
@ExceptionHandler(UnauthorizedException.class)
|
||||||
public Result handleUnauthorizedException(UnauthorizedException ex) {
|
public Result<Void> handleUnauthorizedException(UnauthorizedException ex) {
|
||||||
Result result = new Result();
|
Result<Void> result = new Result<>();
|
||||||
result.error(ErrorCode.FORBIDDEN);
|
result.error(ErrorCode.FORBIDDEN);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public Result handleException(Exception ex) {
|
public Result<Void> handleException(Exception ex) {
|
||||||
log.error(ex.getMessage(), ex);
|
log.error(ex.getMessage(), ex);
|
||||||
|
|
||||||
return new Result().error();
|
return new Result<Void>().error();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
package xiaozhi.common.handler;
|
package xiaozhi.common.handler;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
import java.util.Date;
|
||||||
import xiaozhi.common.constant.Constant;
|
|
||||||
import xiaozhi.common.user.UserDetail;
|
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
|
||||||
import org.apache.ibatis.reflection.MetaObject;
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.Date;
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
|
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.user.UserDetail;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公共字段,自动填充值
|
* 公共字段,自动填充值
|
||||||
@@ -22,38 +24,34 @@ public class FieldMetaObjectHandler implements MetaObjectHandler {
|
|||||||
private final static String UPDATER = "updater";
|
private final static String UPDATER = "updater";
|
||||||
|
|
||||||
private final static String DATA_OPERATION = "dataOperation";
|
private final static String DATA_OPERATION = "dataOperation";
|
||||||
private final static String DEPT_ID = "deptId";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void insertFill(MetaObject metaObject) {
|
public void insertFill(MetaObject metaObject) {
|
||||||
UserDetail user = SecurityUser.getUser();
|
UserDetail user = SecurityUser.getUser();
|
||||||
Date date = new Date();
|
Date date = new Date();
|
||||||
|
|
||||||
//创建者
|
// 创建者
|
||||||
strictInsertFill(metaObject, CREATOR, Long.class, user.getId());
|
strictInsertFill(metaObject, CREATOR, Long.class, user.getId());
|
||||||
//创建时间
|
// 创建时间
|
||||||
strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
|
strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
|
||||||
|
|
||||||
//创建者所属部门
|
// 更新者
|
||||||
strictInsertFill(metaObject, DEPT_ID, Long.class, user.getDeptId());
|
|
||||||
|
|
||||||
//更新者
|
|
||||||
strictInsertFill(metaObject, UPDATER, Long.class, user.getId());
|
strictInsertFill(metaObject, UPDATER, Long.class, user.getId());
|
||||||
//更新时间
|
// 更新时间
|
||||||
strictInsertFill(metaObject, UPDATE_DATE, Date.class, date);
|
strictInsertFill(metaObject, UPDATE_DATE, Date.class, date);
|
||||||
|
|
||||||
//数据标识
|
// 数据标识
|
||||||
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.INSERT.getValue());
|
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.INSERT.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateFill(MetaObject metaObject) {
|
public void updateFill(MetaObject metaObject) {
|
||||||
//更新者
|
// 更新者
|
||||||
strictUpdateFill(metaObject, UPDATER, Long.class, SecurityUser.getUserId());
|
strictUpdateFill(metaObject, UPDATER, Long.class, SecurityUser.getUserId());
|
||||||
//更新时间
|
// 更新时间
|
||||||
strictUpdateFill(metaObject, UPDATE_DATE, Date.class, new Date());
|
strictUpdateFill(metaObject, UPDATE_DATE, Date.class, new Date());
|
||||||
|
|
||||||
//数据标识
|
// 数据标识
|
||||||
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.UPDATE.getValue());
|
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.UPDATE.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
package xiaozhi.common.interceptor;
|
package xiaozhi.common.interceptor;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.ibatis.executor.Executor;
|
||||||
|
import org.apache.ibatis.mapping.BoundSql;
|
||||||
|
import org.apache.ibatis.mapping.MappedStatement;
|
||||||
|
import org.apache.ibatis.session.ResultHandler;
|
||||||
|
import org.apache.ibatis.session.RowBounds;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import net.sf.jsqlparser.JSQLParserException;
|
import net.sf.jsqlparser.JSQLParserException;
|
||||||
import net.sf.jsqlparser.expression.Expression;
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
import net.sf.jsqlparser.expression.StringValue;
|
import net.sf.jsqlparser.expression.StringValue;
|
||||||
@@ -10,13 +19,6 @@ import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
|||||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||||
import net.sf.jsqlparser.statement.select.Select;
|
import net.sf.jsqlparser.statement.select.Select;
|
||||||
import org.apache.ibatis.executor.Executor;
|
|
||||||
import org.apache.ibatis.mapping.BoundSql;
|
|
||||||
import org.apache.ibatis.mapping.MappedStatement;
|
|
||||||
import org.apache.ibatis.session.ResultHandler;
|
|
||||||
import org.apache.ibatis.session.RowBounds;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据过滤
|
* 数据过滤
|
||||||
@@ -25,8 +27,10 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class DataFilterInterceptor implements InnerInterceptor {
|
public class DataFilterInterceptor implements InnerInterceptor {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
@Override
|
@Override
|
||||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
|
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
|
||||||
|
ResultHandler resultHandler, BoundSql boundSql) {
|
||||||
DataScope scope = getDataScope(parameter);
|
DataScope scope = getDataScope(parameter);
|
||||||
// 不进行数据过滤
|
// 不进行数据过滤
|
||||||
if (scope == null || StrUtil.isBlank(scope.getSqlFilter())) {
|
if (scope == null || StrUtil.isBlank(scope.getSqlFilter())) {
|
||||||
@@ -48,7 +52,7 @@ public class DataFilterInterceptor implements InnerInterceptor {
|
|||||||
// 判断参数里是否有DataScope对象
|
// 判断参数里是否有DataScope对象
|
||||||
if (parameter instanceof Map) {
|
if (parameter instanceof Map) {
|
||||||
Map<?, ?> parameterMap = (Map<?, ?>) parameter;
|
Map<?, ?> parameterMap = (Map<?, ?>) parameter;
|
||||||
for (Map.Entry entry : parameterMap.entrySet()) {
|
for (Map.Entry<?, ?> entry : parameterMap.entrySet()) {
|
||||||
if (entry.getValue() != null && entry.getValue() instanceof DataScope) {
|
if (entry.getValue() != null && entry.getValue() instanceof DataScope) {
|
||||||
return (DataScope) entry.getValue();
|
return (DataScope) entry.getValue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package xiaozhi.common.page;
|
package xiaozhi.common.page;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页工具类
|
* 分页工具类
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package xiaozhi.common.page;
|
package xiaozhi.common.page;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 令牌信息
|
* 令牌信息
|
||||||
*
|
*
|
||||||
@@ -14,7 +14,6 @@ import java.io.Serializable;
|
|||||||
@Schema(description = "令牌信息")
|
@Schema(description = "令牌信息")
|
||||||
public class TokenDTO implements Serializable {
|
public class TokenDTO implements Serializable {
|
||||||
|
|
||||||
|
|
||||||
@Schema(description = "密码")
|
@Schema(description = "密码")
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package xiaozhi.common.redis;
|
package xiaozhi.common.redis;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
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 org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
@@ -8,6 +7,8 @@ import org.springframework.data.redis.core.RedisTemplate;
|
|||||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis配置
|
* Redis配置
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -21,60 +21,18 @@ public class RedisKeys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录用户Key
|
* 未注册设备验证码Key
|
||||||
*/
|
*/
|
||||||
public static String getSecurityUserKey(Long id) {
|
public static String getDeviceCaptchaKey(String captcha) {
|
||||||
return "sys:security:user:" + id;
|
return "sys:device:captcha:" + captcha;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统日志Key
|
* 用户id的Key
|
||||||
*/
|
*/
|
||||||
public static String getSysLogKey() {
|
public static String getUserIdKey(Long userid) {
|
||||||
return "sys:log";
|
return "sys:username:id:" + userid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 系统资源Key
|
|
||||||
*/
|
|
||||||
public static String getSysResourceKey() {
|
|
||||||
return "sys:resource";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户菜单导航Key
|
|
||||||
*/
|
|
||||||
public static String getUserMenuNavKey(Long userId) {
|
|
||||||
return "sys:user:nav:" + userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户权限标识Key
|
|
||||||
*/
|
|
||||||
public static String getUserPermissionsKey(Long userId) {
|
|
||||||
return "sys:user:permissions:" + userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户登陆错误次数
|
|
||||||
*
|
|
||||||
* @param username
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getUserLoginErrorCountKey(String username) {
|
|
||||||
return "sys:user:login:error:" + username;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getUserInfoKey(Long userId) {
|
|
||||||
return "sys:user:" + userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getDataScopeListKey(Long userId) {
|
|
||||||
return "sys:user:data:scope:" + userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getSysUserName(Long id) {
|
|
||||||
return "sys:user:name" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package xiaozhi.common.redis;
|
package xiaozhi.common.redis;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import org.springframework.data.redis.core.HashOperations;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
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.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis工具类
|
* Redis工具类
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -1,108 +1,109 @@
|
|||||||
package xiaozhi.common.service;
|
package xiaozhi.common.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
import java.io.Serializable;
|
||||||
|
import java.util.Collection;
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Collection;
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础服务接口,所有Service接口都要继承
|
* 基础服务接口,所有Service接口都要继承
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
* Website: https://www.renren.io
|
* Website: https://www.renren.io
|
||||||
*/
|
*/
|
||||||
public interface BaseService<T> {
|
public interface BaseService<T> {
|
||||||
Class<T> currentModelClass();
|
Class<T> currentModelClass();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 插入一条记录(选择字段,策略插入)
|
* 插入一条记录(选择字段,策略插入)
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param entity 实体对象
|
* @param entity 实体对象
|
||||||
*/
|
*/
|
||||||
boolean insert(T entity);
|
boolean insert(T entity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 插入(批量),该方法不支持 Oracle、SQL Server
|
* 插入(批量),该方法不支持 Oracle、SQL Server
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param entityList 实体对象集合
|
* @param entityList 实体对象集合
|
||||||
*/
|
*/
|
||||||
boolean insertBatch(Collection<T> entityList);
|
boolean insertBatch(Collection<T> entityList);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 插入(批量),该方法不支持 Oracle、SQL Server
|
* 插入(批量),该方法不支持 Oracle、SQL Server
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param entityList 实体对象集合
|
* @param entityList 实体对象集合
|
||||||
* @param batchSize 插入批次数量
|
* @param batchSize 插入批次数量
|
||||||
*/
|
*/
|
||||||
boolean insertBatch(Collection<T> entityList, int batchSize);
|
boolean insertBatch(Collection<T> entityList, int batchSize);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 根据 ID 选择修改
|
* 根据 ID 选择修改
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param entity 实体对象
|
* @param entity 实体对象
|
||||||
*/
|
*/
|
||||||
boolean updateById(T entity);
|
boolean updateById(T entity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 根据 whereEntity 条件,更新记录
|
* 根据 whereEntity 条件,更新记录
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param entity 实体对象
|
* @param entity 实体对象
|
||||||
* @param updateWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
|
* @param updateWrapper 实体对象封装操作类
|
||||||
*/
|
* {@link com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper}
|
||||||
boolean update(T entity, Wrapper<T> updateWrapper);
|
*/
|
||||||
|
boolean update(T entity, Wrapper<T> updateWrapper);
|
||||||
/**
|
|
||||||
* <p>
|
/**
|
||||||
* 根据ID 批量更新
|
* <p>
|
||||||
* </p>
|
* 根据ID 批量更新
|
||||||
*
|
* </p>
|
||||||
* @param entityList 实体对象集合
|
*
|
||||||
*/
|
* @param entityList 实体对象集合
|
||||||
boolean updateBatchById(Collection<T> entityList);
|
*/
|
||||||
|
boolean updateBatchById(Collection<T> entityList);
|
||||||
/**
|
|
||||||
* <p>
|
/**
|
||||||
* 根据ID 批量更新
|
* <p>
|
||||||
* </p>
|
* 根据ID 批量更新
|
||||||
*
|
* </p>
|
||||||
* @param entityList 实体对象集合
|
*
|
||||||
* @param batchSize 更新批次数量
|
* @param entityList 实体对象集合
|
||||||
*/
|
* @param batchSize 更新批次数量
|
||||||
boolean updateBatchById(Collection<T> entityList, int batchSize);
|
*/
|
||||||
|
boolean updateBatchById(Collection<T> entityList, int batchSize);
|
||||||
/**
|
|
||||||
* <p>
|
/**
|
||||||
* 根据 ID 查询
|
* <p>
|
||||||
* </p>
|
* 根据 ID 查询
|
||||||
*
|
* </p>
|
||||||
* @param id 主键ID
|
*
|
||||||
*/
|
* @param id 主键ID
|
||||||
T selectById(Serializable id);
|
*/
|
||||||
|
T selectById(Serializable id);
|
||||||
/**
|
|
||||||
* <p>
|
/**
|
||||||
* 根据 ID 删除
|
* <p>
|
||||||
* </p>
|
* 根据 ID 删除
|
||||||
*
|
* </p>
|
||||||
* @param id 主键ID
|
*
|
||||||
*/
|
* @param id 主键ID
|
||||||
boolean deleteById(Serializable id);
|
*/
|
||||||
|
boolean deleteById(Serializable id);
|
||||||
/**
|
|
||||||
* <p>
|
/**
|
||||||
* 删除(根据ID 批量删除)
|
* <p>
|
||||||
* </p>
|
* 删除(根据ID 批量删除)
|
||||||
*
|
* </p>
|
||||||
* @param idList 主键ID列表
|
*
|
||||||
*/
|
* @param idList 主键ID列表
|
||||||
boolean deleteBatchIds(Collection<? extends Serializable> idList);
|
*/
|
||||||
|
boolean deleteBatchIds(Collection<? extends Serializable> idList);
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package xiaozhi.common.service;
|
package xiaozhi.common.service;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CRUD基础服务接口
|
* CRUD基础服务接口
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -1,214 +1,218 @@
|
|||||||
package xiaozhi.common.service.impl;
|
package xiaozhi.common.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
import java.io.Serializable;
|
||||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
import java.util.Collection;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import java.util.List;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import java.util.Map;
|
||||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
import java.util.function.BiConsumer;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
import org.apache.ibatis.binding.MapperMethod;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
import org.apache.ibatis.logging.Log;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import org.apache.ibatis.logging.LogFactory;
|
||||||
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
import org.apache.ibatis.session.SqlSession;
|
||||||
import xiaozhi.common.constant.Constant;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import xiaozhi.common.page.PageData;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import xiaozhi.common.service.BaseService;
|
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||||
import org.apache.ibatis.binding.MapperMethod;
|
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||||
import org.apache.ibatis.logging.Log;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import org.apache.ibatis.logging.LogFactory;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import org.apache.ibatis.session.SqlSession;
|
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||||
import java.io.Serializable;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import java.util.Collection;
|
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import xiaozhi.common.constant.Constant;
|
||||||
import java.util.function.BiConsumer;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
/**
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
* 基础服务类,所有Service都要继承
|
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
/**
|
||||||
* Website: https://www.renren.io
|
* 基础服务类,所有Service都要继承
|
||||||
*/
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> {
|
* Website: https://www.renren.io
|
||||||
@Autowired
|
*/
|
||||||
protected M baseDao;
|
public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements BaseService<T> {
|
||||||
protected Log log = LogFactory.getLog(getClass());
|
@Autowired
|
||||||
|
protected M baseDao;
|
||||||
/**
|
protected Log log = LogFactory.getLog(getClass());
|
||||||
* 获取分页对象
|
|
||||||
*
|
/**
|
||||||
* @param params 分页查询参数
|
* 获取分页对象
|
||||||
* @param defaultOrderField 默认排序字段
|
*
|
||||||
* @param isAsc 排序方式
|
* @param params 分页查询参数
|
||||||
*/
|
* @param defaultOrderField 默认排序字段
|
||||||
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
* @param isAsc 排序方式
|
||||||
//分页参数
|
*/
|
||||||
long curPage = 1;
|
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
||||||
long limit = 10;
|
// 分页参数
|
||||||
|
long curPage = 1;
|
||||||
if (params.get(Constant.PAGE) != null) {
|
long limit = 10;
|
||||||
curPage = Long.parseLong((String) params.get(Constant.PAGE));
|
|
||||||
}
|
if (params.get(Constant.PAGE) != null) {
|
||||||
if (params.get(Constant.LIMIT) != null) {
|
curPage = Long.parseLong((String) params.get(Constant.PAGE));
|
||||||
limit = Long.parseLong((String) params.get(Constant.LIMIT));
|
}
|
||||||
}
|
if (params.get(Constant.LIMIT) != null) {
|
||||||
|
limit = Long.parseLong((String) params.get(Constant.LIMIT));
|
||||||
//分页对象
|
}
|
||||||
Page<T> page = new Page<>(curPage, limit);
|
|
||||||
|
// 分页对象
|
||||||
//分页参数
|
Page<T> page = new Page<>(curPage, limit);
|
||||||
params.put(Constant.PAGE, page);
|
|
||||||
|
// 分页参数
|
||||||
//排序字段
|
params.put(Constant.PAGE, page);
|
||||||
String orderField = (String) params.get(Constant.ORDER_FIELD);
|
|
||||||
String order = (String) params.get(Constant.ORDER);
|
// 排序字段
|
||||||
|
String orderField = (String) params.get(Constant.ORDER_FIELD);
|
||||||
//前端字段排序
|
String order = (String) params.get(Constant.ORDER);
|
||||||
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
|
|
||||||
if (Constant.ASC.equalsIgnoreCase(order)) {
|
// 前端字段排序
|
||||||
return page.addOrder(OrderItem.asc(orderField));
|
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
|
||||||
} else {
|
if (Constant.ASC.equalsIgnoreCase(order)) {
|
||||||
return page.addOrder(OrderItem.desc(orderField));
|
return page.addOrder(OrderItem.asc(orderField));
|
||||||
}
|
} else {
|
||||||
}
|
return page.addOrder(OrderItem.desc(orderField));
|
||||||
|
}
|
||||||
//没有排序字段,则不排序
|
}
|
||||||
if (StringUtils.isBlank(defaultOrderField)) {
|
|
||||||
return page;
|
// 没有排序字段,则不排序
|
||||||
}
|
if (StringUtils.isBlank(defaultOrderField)) {
|
||||||
|
return page;
|
||||||
//默认排序
|
}
|
||||||
if (isAsc) {
|
|
||||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
// 默认排序
|
||||||
} else {
|
if (isAsc) {
|
||||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||||
}
|
} else {
|
||||||
|
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||||
return page;
|
}
|
||||||
}
|
|
||||||
|
return page;
|
||||||
protected <T> PageData<T> getPageData(List<?> list, long total, Class<T> target) {
|
}
|
||||||
List<T> targetList = ConvertUtils.sourceToTarget(list, target);
|
|
||||||
|
protected <D> PageData<D> getPageData(List<?> list, long total, Class<D> target) {
|
||||||
return new PageData<>(targetList, total);
|
List<D> targetList = ConvertUtils.sourceToTarget(list, target);
|
||||||
}
|
|
||||||
|
return new PageData<>(targetList, total);
|
||||||
protected <T> PageData<T> getPageData(IPage page, Class<T> target) {
|
}
|
||||||
return getPageData(page.getRecords(), page.getTotal(), target);
|
|
||||||
}
|
protected <D> PageData<D> getPageData(IPage<?> page, Class<D> target) {
|
||||||
|
return getPageData(page.getRecords(), page.getTotal(), target);
|
||||||
protected void paramsToLike(Map<String, Object> params, String... likes) {
|
}
|
||||||
for (String like : likes) {
|
|
||||||
String val = (String) params.get(like);
|
protected void paramsToLike(Map<String, Object> params, String... likes) {
|
||||||
if (StringUtils.isNotBlank(val)) {
|
for (String like : likes) {
|
||||||
params.put(like, "%" + val + "%");
|
String val = (String) params.get(like);
|
||||||
} else {
|
if (StringUtils.isNotBlank(val)) {
|
||||||
params.put(like, null);
|
params.put(like, "%" + val + "%");
|
||||||
}
|
} else {
|
||||||
}
|
params.put(like, null);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
}
|
||||||
* <p>
|
|
||||||
* 判断数据库操作是否成功
|
/**
|
||||||
* </p>
|
* <p>
|
||||||
* <p>
|
* 判断数据库操作是否成功
|
||||||
* 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
|
* </p>
|
||||||
* </p>
|
* <p>
|
||||||
*
|
* 注意!! 该方法为 Integer 判断,不可传入 int 基本类型
|
||||||
* @param result 数据库操作返回影响条数
|
* </p>
|
||||||
* @return boolean
|
*
|
||||||
*/
|
* @param result 数据库操作返回影响条数
|
||||||
protected static boolean retBool(Integer result) {
|
* @return boolean
|
||||||
return SqlHelper.retBool(result);
|
*/
|
||||||
}
|
protected static boolean retBool(Integer result) {
|
||||||
|
return SqlHelper.retBool(result);
|
||||||
protected Class<M> currentMapperClass() {
|
}
|
||||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
|
||||||
}
|
@SuppressWarnings("unchecked")
|
||||||
|
protected Class<M> currentMapperClass() {
|
||||||
@Override
|
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||||
public Class<T> currentModelClass() {
|
}
|
||||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
|
||||||
}
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
protected String getSqlStatement(SqlMethod sqlMethod) {
|
public Class<T> currentModelClass() {
|
||||||
return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
|
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected String getSqlStatement(SqlMethod sqlMethod) {
|
||||||
@Transactional(rollbackFor = Exception.class)
|
return SqlHelper.getSqlStatement(this.currentMapperClass(), sqlMethod);
|
||||||
public boolean insert(T entity) {
|
}
|
||||||
return BaseServiceImpl.retBool(baseDao.insert(entity));
|
|
||||||
}
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
public boolean insert(T entity) {
|
||||||
@Transactional(rollbackFor = Exception.class)
|
return BaseServiceImpl.retBool(baseDao.insert(entity));
|
||||||
public boolean insertBatch(Collection<T> entityList) {
|
}
|
||||||
return insertBatch(entityList, 100);
|
|
||||||
}
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
/**
|
public boolean insertBatch(Collection<T> entityList) {
|
||||||
* 批量插入
|
return insertBatch(entityList, 100);
|
||||||
*/
|
}
|
||||||
@Override
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
/**
|
||||||
public boolean insertBatch(Collection<T> entityList, int batchSize) {
|
* 批量插入
|
||||||
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
|
*/
|
||||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
|
@Override
|
||||||
}
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean insertBatch(Collection<T> entityList, int batchSize) {
|
||||||
/**
|
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
|
||||||
* 执行批量操作
|
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
|
||||||
*/
|
}
|
||||||
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
|
|
||||||
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
|
/**
|
||||||
}
|
* 执行批量操作
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@Override
|
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
|
||||||
@Transactional(rollbackFor = Exception.class)
|
return SqlHelper.executeBatch(this.currentModelClass(), this.log, list, batchSize, consumer);
|
||||||
public boolean updateById(T entity) {
|
}
|
||||||
return BaseServiceImpl.retBool(baseDao.updateById(entity));
|
|
||||||
}
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
public boolean updateById(T entity) {
|
||||||
public boolean update(T entity, Wrapper<T> updateWrapper) {
|
return BaseServiceImpl.retBool(baseDao.updateById(entity));
|
||||||
return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public boolean update(T entity, Wrapper<T> updateWrapper) {
|
||||||
@Transactional(rollbackFor = Exception.class)
|
return BaseServiceImpl.retBool(baseDao.update(entity, updateWrapper));
|
||||||
public boolean updateBatchById(Collection<T> entityList) {
|
}
|
||||||
return updateBatchById(entityList, 30);
|
|
||||||
}
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
public boolean updateBatchById(Collection<T> entityList) {
|
||||||
@Transactional(rollbackFor = Exception.class)
|
return updateBatchById(entityList, 30);
|
||||||
public boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
}
|
||||||
String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
|
|
||||||
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
|
@Override
|
||||||
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
|
@Transactional(rollbackFor = Exception.class)
|
||||||
param.put(Constants.ENTITY, entity);
|
public boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
||||||
sqlSession.update(sqlStatement, param);
|
String sqlStatement = getSqlStatement(SqlMethod.UPDATE_BY_ID);
|
||||||
});
|
return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
|
||||||
}
|
MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
|
||||||
|
param.put(Constants.ENTITY, entity);
|
||||||
@Override
|
sqlSession.update(sqlStatement, param);
|
||||||
public T selectById(Serializable id) {
|
});
|
||||||
return baseDao.selectById(id);
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public T selectById(Serializable id) {
|
||||||
public boolean deleteById(Serializable id) {
|
return baseDao.selectById(id);
|
||||||
return SqlHelper.retBool(baseDao.deleteById(id));
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
@Override
|
public boolean deleteById(Serializable id) {
|
||||||
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
|
return SqlHelper.retBool(baseDao.deleteById(id));
|
||||||
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteBatchIds(Collection<? extends Serializable> idList) {
|
||||||
|
return SqlHelper.retBool(baseDao.deleteBatchIds(idList));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,30 @@
|
|||||||
package xiaozhi.common.service.impl;
|
package xiaozhi.common.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
|
||||||
import xiaozhi.common.page.PageData;
|
|
||||||
import xiaozhi.common.service.CrudService;
|
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
|
||||||
import org.springframework.beans.BeanUtils;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
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 org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.CrudService;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CRUD基础服务类
|
* CRUD基础服务类
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
* Website: https://www.renren.io
|
* Website: https://www.renren.io
|
||||||
*/
|
*/
|
||||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T> implements CrudService<T, D> {
|
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -29,8 +33,7 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends Bas
|
|||||||
public PageData<D> page(Map<String, Object> params) {
|
public PageData<D> page(Map<String, Object> params) {
|
||||||
IPage<T> page = baseDao.selectPage(
|
IPage<T> page = baseDao.selectPage(
|
||||||
getPage(params, null, false),
|
getPage(params, null, false),
|
||||||
getWrapper(params)
|
getWrapper(params));
|
||||||
);
|
|
||||||
|
|
||||||
return getPageData(page, currentDtoClass());
|
return getPageData(page, currentDtoClass());
|
||||||
}
|
}
|
||||||
@@ -56,7 +59,7 @@ public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends Bas
|
|||||||
T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
|
T entity = ConvertUtils.sourceToTarget(dto, currentModelClass());
|
||||||
insert(entity);
|
insert(entity);
|
||||||
|
|
||||||
//copy主键值到dto
|
// copy主键值到dto
|
||||||
BeanUtils.copyProperties(entity, dto);
|
BeanUtils.copyProperties(entity, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package xiaozhi.common.user;
|
package xiaozhi.common.user;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录用户信息
|
* 登录用户信息
|
||||||
@@ -14,19 +13,7 @@ import java.util.List;
|
|||||||
public class UserDetail implements Serializable {
|
public class UserDetail implements Serializable {
|
||||||
private Long id;
|
private Long id;
|
||||||
private String username;
|
private String username;
|
||||||
private String realName;
|
|
||||||
private String headUrl;
|
|
||||||
private Integer gender;
|
|
||||||
private String email;
|
|
||||||
private String mobile;
|
|
||||||
private Long deptId;
|
|
||||||
private String password;
|
|
||||||
private Integer status;
|
|
||||||
private Integer superAdmin;
|
private Integer superAdmin;
|
||||||
private String token;
|
private String token;
|
||||||
/**
|
private Integer status;
|
||||||
* 部门数据权限
|
|
||||||
*/
|
|
||||||
private List<Long> deptIdList;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
package xiaozhi.common.utils;
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.BeanUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换工具类
|
* 转换工具类
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
@@ -20,7 +21,7 @@ public class ConvertUtils {
|
|||||||
}
|
}
|
||||||
T targetObject = null;
|
T targetObject = null;
|
||||||
try {
|
try {
|
||||||
targetObject = target.newInstance();
|
targetObject = target.getDeclaredConstructor().newInstance();
|
||||||
BeanUtils.copyProperties(source, targetObject);
|
BeanUtils.copyProperties(source, targetObject);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("convert error ", e);
|
log.error("convert error ", e);
|
||||||
@@ -34,10 +35,10 @@ public class ConvertUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List targetList = new ArrayList<>(sourceList.size());
|
List<T> targetList = new ArrayList<>(sourceList.size());
|
||||||
try {
|
try {
|
||||||
for (Object source : sourceList) {
|
for (Object source : sourceList) {
|
||||||
T targetObject = target.newInstance();
|
T targetObject = target.getDeclaredConstructor().newInstance();
|
||||||
BeanUtils.copyProperties(source, targetObject);
|
BeanUtils.copyProperties(source, targetObject);
|
||||||
targetList.add(targetObject);
|
targetList.add(targetObject);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package xiaozhi.common.utils;
|
|||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,4 +63,39 @@ public class DateUtils {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒
|
||||||
|
* @param date
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String getShortTime(Date date) {
|
||||||
|
if (date == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 将 Date 转换为 Instant
|
||||||
|
LocalDateTime localDateTime = date.toInstant()
|
||||||
|
// 获取系统默认时区
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
// 转换为 LocalDateTime
|
||||||
|
.toLocalDateTime();
|
||||||
|
// 当前时间
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
// 时间差,单位为秒
|
||||||
|
long secondsBetween = ChronoUnit.SECONDS.between(localDateTime, now);
|
||||||
|
|
||||||
|
if (secondsBetween <= 10) {
|
||||||
|
return "刚刚";
|
||||||
|
} else if (secondsBetween < 60) {
|
||||||
|
return secondsBetween + "秒前";
|
||||||
|
} else if (secondsBetween < 60 * 60) {
|
||||||
|
return secondsBetween / 60 + "分钟前";
|
||||||
|
} else if (secondsBetween < 86400) {
|
||||||
|
return secondsBetween / 3600 + "小时前";
|
||||||
|
} else if (secondsBetween < 604800) {
|
||||||
|
return secondsBetween / 86400 + "天前";
|
||||||
|
} else {
|
||||||
|
// 超过一周,显示完整日期时间
|
||||||
|
return format(date,DATE_TIME_PATTERN);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package xiaozhi.common.utils;
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
import xiaozhi.common.constant.Constant;
|
import java.util.Date;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.util.DigestUtils;
|
import org.springframework.util.DigestUtils;
|
||||||
@@ -9,10 +12,9 @@ import org.springframework.web.context.request.RequestAttributes;
|
|||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
import java.util.Date;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.Enumeration;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import java.util.HashMap;
|
import xiaozhi.common.exception.RenException;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Http
|
* Http
|
||||||
@@ -30,13 +32,14 @@ public class HttpContextUtils {
|
|||||||
return ((ServletRequestAttributes) requestAttributes).getRequest();
|
return ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getToken() {
|
public static String getToken(String authorization) {
|
||||||
HttpServletRequest httpRequest = getHttpServletRequest();
|
String token;
|
||||||
String token = httpRequest.getHeader(Constant.TOKEN_HEADER);
|
if (StringUtils.isBlank(authorization) && authorization.contains("Bearer ")) {
|
||||||
|
throw new RenException(ErrorCode.UNAUTHORIZED);
|
||||||
//如果header中不存在token,则从参数中获取token
|
}
|
||||||
|
token = authorization.replace("Bearer ", "");
|
||||||
if (StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(token)) {
|
||||||
token = httpRequest.getParameter(Constant.TOKEN_HEADER);
|
throw new RenException(ErrorCode.TOKEN_NOT_EMPTY);
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
@@ -68,15 +71,15 @@ public class HttpContextUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getLanguage() {
|
public static String getLanguage() {
|
||||||
//默认语言
|
// 默认语言
|
||||||
String defaultLanguage = "zh-CN";
|
String defaultLanguage = "zh-CN";
|
||||||
//request
|
// request
|
||||||
HttpServletRequest request = getHttpServletRequest();
|
HttpServletRequest request = getHttpServletRequest();
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
return defaultLanguage;
|
return defaultLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
//请求语言
|
// 请求语言
|
||||||
defaultLanguage = request.getHeader(HttpHeaders.ACCEPT_LANGUAGE);
|
defaultLanguage = request.getHeader(HttpHeaders.ACCEPT_LANGUAGE);
|
||||||
|
|
||||||
return defaultLanguage;
|
return defaultLanguage;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package xiaozhi.common.utils;
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IP地址
|
* IP地址
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package xiaozhi.common.utils;
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON 工具类
|
* JSON 工具类
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
@@ -59,7 +60,8 @@ public class JsonUtils {
|
|||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
return objectMapper.readValue(text,
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
package xiaozhi.common.utils;
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import java.io.Serializable;
|
||||||
import xiaozhi.common.page.PageData;
|
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 响应数据
|
* 响应数据
|
||||||
@@ -42,10 +36,6 @@ public class Result<T> implements Serializable {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean success() {
|
|
||||||
return code == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Result<T> error() {
|
public Result<T> error() {
|
||||||
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
|
this.code = ErrorCode.INTERNAL_SERVER_ERROR;
|
||||||
this.msg = MessageUtils.getMessage(this.code);
|
this.msg = MessageUtils.getMessage(this.code);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package xiaozhi.common.utils;
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 树节点,所有需要实现树节点的,都需要继承该类
|
* 树节点,所有需要实现树节点的,都需要继承该类
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package xiaozhi.common.utils;
|
package xiaozhi.common.utils;
|
||||||
|
|
||||||
import xiaozhi.common.validator.AssertUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import xiaozhi.common.validator.AssertUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 树形结构工具类,如:菜单、部门等
|
* 树形结构工具类,如:菜单、部门等
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
@@ -17,8 +17,8 @@ public class TreeUtils {
|
|||||||
/**
|
/**
|
||||||
* 根据pid,构建树节点
|
* 根据pid,构建树节点
|
||||||
*/
|
*/
|
||||||
public static <T extends TreeNode> List<T> build(List<T> treeNodes, Long pid) {
|
public static <T extends TreeNode<T>> List<T> build(List<T> treeNodes, Long pid) {
|
||||||
//pid不能为空
|
// pid不能为空
|
||||||
AssertUtils.isNull(pid, "pid");
|
AssertUtils.isNull(pid, "pid");
|
||||||
|
|
||||||
List<T> treeList = new ArrayList<>();
|
List<T> treeList = new ArrayList<>();
|
||||||
@@ -34,7 +34,7 @@ public class TreeUtils {
|
|||||||
/**
|
/**
|
||||||
* 查找子节点
|
* 查找子节点
|
||||||
*/
|
*/
|
||||||
private static <T extends TreeNode> T findChildren(List<T> treeNodes, T rootNode) {
|
private static <T extends TreeNode<T>> T findChildren(List<T> treeNodes, T rootNode) {
|
||||||
for (T treeNode : treeNodes) {
|
for (T treeNode : treeNodes) {
|
||||||
if (rootNode.getId().equals(treeNode.getPid())) {
|
if (rootNode.getId().equals(treeNode.getPid())) {
|
||||||
rootNode.getChildren().add(findChildren(treeNodes, treeNode));
|
rootNode.getChildren().add(findChildren(treeNodes, treeNode));
|
||||||
@@ -46,10 +46,10 @@ public class TreeUtils {
|
|||||||
/**
|
/**
|
||||||
* 构建树节点
|
* 构建树节点
|
||||||
*/
|
*/
|
||||||
public static <T extends TreeNode> List<T> build(List<T> treeNodes) {
|
public static <T extends TreeNode<T>> List<T> build(List<T> treeNodes) {
|
||||||
List<T> result = new ArrayList<>();
|
List<T> result = new ArrayList<>();
|
||||||
|
|
||||||
//list转map
|
// list转map
|
||||||
Map<Long, T> nodeMap = new LinkedHashMap<>(treeNodes.size());
|
Map<Long, T> nodeMap = new LinkedHashMap<>(treeNodes.size());
|
||||||
for (T treeNode : treeNodes) {
|
for (T treeNode : treeNodes) {
|
||||||
nodeMap.put(treeNode.getId(), treeNode);
|
nodeMap.put(treeNode.getId(), treeNode);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package xiaozhi.common.validator;
|
package xiaozhi.common.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.map.MapUtil;
|
import cn.hutool.core.map.MapUtil;
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验工具类
|
* 校验工具类
|
||||||
@@ -73,11 +74,11 @@ public class AssertUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void isMapEmpty(Map map, String... params) {
|
public static void isMapEmpty(Map<?, ?> map, String... params) {
|
||||||
isMapEmpty(map, ErrorCode.NOT_NULL, params);
|
isMapEmpty(map, ErrorCode.NOT_NULL, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void isMapEmpty(Map map, Integer code, String... params) {
|
public static void isMapEmpty(Map<?, ?> map, Integer code, String... params) {
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
throw new RenException(ErrorCode.NOT_NULL, "code");
|
throw new RenException(ErrorCode.NOT_NULL, "code");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package xiaozhi.common.validator;
|
package xiaozhi.common.validator;
|
||||||
|
|
||||||
import xiaozhi.common.exception.RenException;
|
import java.util.Locale;
|
||||||
import jakarta.validation.ConstraintViolation;
|
import java.util.Set;
|
||||||
import jakarta.validation.Validation;
|
|
||||||
import jakarta.validation.Validator;
|
|
||||||
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
|
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
|
||||||
import org.springframework.context.i18n.LocaleContextHolder;
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
import org.springframework.context.support.ResourceBundleMessageSource;
|
import org.springframework.context.support.ResourceBundleMessageSource;
|
||||||
import org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator;
|
import org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator;
|
||||||
|
|
||||||
import java.util.Locale;
|
import jakarta.validation.ConstraintViolation;
|
||||||
import java.util.Set;
|
import jakarta.validation.Validation;
|
||||||
|
import jakarta.validation.Validator;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* hibernate-validator校验工具类
|
* hibernate-validator校验工具类
|
||||||
@@ -35,7 +36,7 @@ public class ValidatorUtils {
|
|||||||
throws RenException {
|
throws RenException {
|
||||||
Locale.setDefault(LocaleContextHolder.getLocale());
|
Locale.setDefault(LocaleContextHolder.getLocale());
|
||||||
Validator validator = Validation.byDefaultProvider().configure().messageInterpolator(
|
Validator validator = Validation.byDefaultProvider().configure().messageInterpolator(
|
||||||
new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(getMessageSource())))
|
new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(getMessageSource())))
|
||||||
.buildValidatorFactory().getValidator();
|
.buildValidatorFactory().getValidator();
|
||||||
|
|
||||||
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package xiaozhi.common.xss;
|
package xiaozhi.common.xss;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQL过滤
|
* SQL过滤
|
||||||
@@ -20,19 +21,20 @@ public class SqlFilter {
|
|||||||
if (StringUtils.isBlank(str)) {
|
if (StringUtils.isBlank(str)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
//去掉'|"|;|\字符
|
// 去掉'|"|;|\字符
|
||||||
str = StringUtils.replace(str, "'", "");
|
str = StringUtils.replace(str, "'", "");
|
||||||
str = StringUtils.replace(str, "\"", "");
|
str = StringUtils.replace(str, "\"", "");
|
||||||
str = StringUtils.replace(str, ";", "");
|
str = StringUtils.replace(str, ";", "");
|
||||||
str = StringUtils.replace(str, "\\", "");
|
str = StringUtils.replace(str, "\\", "");
|
||||||
|
|
||||||
//转换成小写
|
// 转换成小写
|
||||||
str = str.toLowerCase();
|
str = str.toLowerCase();
|
||||||
|
|
||||||
//非法字符
|
// 非法字符
|
||||||
String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alter", "drop"};
|
String[] keywords = { "master", "truncate", "insert", "select", "delete", "update", "declare", "alter",
|
||||||
|
"drop" };
|
||||||
|
|
||||||
//判断是否包含非法字符
|
// 判断是否包含非法字符
|
||||||
for (String keyword : keywords) {
|
for (String keyword : keywords) {
|
||||||
if (str.contains(keyword)) {
|
if (str.contains(keyword)) {
|
||||||
throw new RenException(ErrorCode.INVALID_SYMBOL);
|
throw new RenException(ErrorCode.INVALID_SYMBOL);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package xiaozhi.common.xss;
|
package xiaozhi.common.xss;
|
||||||
|
|
||||||
import jakarta.servlet.DispatcherType;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
@@ -8,6 +7,8 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.util.PathMatcher;
|
import org.springframework.util.PathMatcher;
|
||||||
|
|
||||||
|
import jakarta.servlet.DispatcherType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* XSS 配置文件
|
* XSS 配置文件
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package xiaozhi.common.xss;
|
package xiaozhi.common.xss;
|
||||||
|
|
||||||
import jakarta.servlet.*;
|
import java.io.IOException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import org.springframework.util.PathMatcher;
|
import org.springframework.util.PathMatcher;
|
||||||
|
|
||||||
import java.io.IOException;
|
import jakarta.servlet.Filter;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.FilterConfig;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* XSS过滤
|
* XSS过滤
|
||||||
@@ -38,7 +44,8 @@ public class XssFilter implements Filter {
|
|||||||
|
|
||||||
private boolean shouldNotFilter(HttpServletRequest request) {
|
private boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
// 放行不过滤的URL
|
// 放行不过滤的URL
|
||||||
return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, request.getServletPath()));
|
return properties.getExcludeUrls().stream()
|
||||||
|
.anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, request.getServletPath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
package xiaozhi.common.xss;
|
package xiaozhi.common.xss;
|
||||||
|
|
||||||
import cn.hutool.core.io.IoUtil;
|
|
||||||
import jakarta.servlet.ReadListener;
|
|
||||||
import jakarta.servlet.ServletInputStream;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import jakarta.servlet.ReadListener;
|
||||||
|
import jakarta.servlet.ServletInputStream;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* XSS过滤处理
|
* XSS过滤处理
|
||||||
@@ -29,18 +29,18 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ServletInputStream getInputStream() throws IOException {
|
public ServletInputStream getInputStream() throws IOException {
|
||||||
//非json类型,直接返回
|
// 非json类型,直接返回
|
||||||
if (!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE))) {
|
if (!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(super.getHeader(HttpHeaders.CONTENT_TYPE))) {
|
||||||
return super.getInputStream();
|
return super.getInputStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
//为空,直接返回
|
// 为空,直接返回
|
||||||
String json = IoUtil.readUtf8(super.getInputStream());
|
String json = IoUtil.readUtf8(super.getInputStream());
|
||||||
if (StringUtils.isBlank(json)) {
|
if (StringUtils.isBlank(json)) {
|
||||||
return super.getInputStream();
|
return super.getInputStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
//xss过滤
|
// xss过滤
|
||||||
json = xssEncode(json);
|
json = xssEncode(json);
|
||||||
final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
|
final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
|
||||||
return new ServletInputStream() {
|
return new ServletInputStream() {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package xiaozhi.common.xss;
|
package xiaozhi.common.xss;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* XSS 配置项
|
* XSS 配置项
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
|
|||||||
@@ -22,13 +22,15 @@ public class XssUtils extends Safelist {
|
|||||||
*/
|
*/
|
||||||
private static Safelist xssWhitelist() {
|
private static Safelist xssWhitelist() {
|
||||||
return new Safelist()
|
return new Safelist()
|
||||||
//支持的标签
|
// 支持的标签
|
||||||
.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl",
|
.addTags("a", "b", "blockquote", "br", "caption", "cite", "code", "col", "colgroup", "dd", "div", "dl",
|
||||||
"dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol", "p", "pre", "q", "small",
|
"dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", "img", "li", "ol", "p", "pre", "q",
|
||||||
"strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul",
|
"small",
|
||||||
|
"strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
|
||||||
|
"ul",
|
||||||
"embed", "object", "param", "span")
|
"embed", "object", "param", "span")
|
||||||
|
|
||||||
//支持的标签属性
|
// 支持的标签属性
|
||||||
.addAttributes("a", "href", "class", "style", "target", "rel", "nofollow")
|
.addAttributes("a", "href", "class", "style", "target", "rel", "nofollow")
|
||||||
.addAttributes("blockquote", "cite")
|
.addAttributes("blockquote", "cite")
|
||||||
.addAttributes("code", "class", "style")
|
.addAttributes("code", "class", "style")
|
||||||
@@ -44,13 +46,16 @@ public class XssUtils extends Safelist {
|
|||||||
.addAttributes("ul", "type", "style")
|
.addAttributes("ul", "type", "style")
|
||||||
.addAttributes("pre", "class", "style")
|
.addAttributes("pre", "class", "style")
|
||||||
.addAttributes("div", "class", "id", "style")
|
.addAttributes("div", "class", "id", "style")
|
||||||
.addAttributes("embed", "src", "wmode", "flashvars", "pluginspage", "allowFullScreen", "allowfullscreen",
|
.addAttributes("embed", "src", "wmode", "flashvars", "pluginspage", "allowFullScreen",
|
||||||
"quality", "width", "height", "align", "allowScriptAccess", "allowscriptaccess", "allownetworking", "type")
|
"allowfullscreen",
|
||||||
.addAttributes("object", "type", "id", "name", "data", "width", "height", "style", "classid", "codebase")
|
"quality", "width", "height", "align", "allowScriptAccess", "allowscriptaccess",
|
||||||
|
"allownetworking", "type")
|
||||||
|
.addAttributes("object", "type", "id", "name", "data", "width", "height", "style", "classid",
|
||||||
|
"codebase")
|
||||||
.addAttributes("param", "name", "value")
|
.addAttributes("param", "name", "value")
|
||||||
.addAttributes("span", "class", "style")
|
.addAttributes("span", "class", "style")
|
||||||
|
|
||||||
//标签属性对应的协议
|
// 标签属性对应的协议
|
||||||
.addProtocols("a", "href", "ftp", "http", "https", "mailto")
|
.addProtocols("a", "href", "ftp", "http", "https", "mailto")
|
||||||
.addProtocols("img", "src", "http", "https")
|
.addProtocols("img", "src", "http", "https")
|
||||||
.addProtocols("blockquote", "cite", "http", "https")
|
.addProtocols("blockquote", "cite", "http", "https")
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package xiaozhi.modules.agent.controller;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
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 com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
|
||||||
|
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 jakarta.validation.Valid;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.user.UserDetail;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
|
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.security.user.SecurityUser;
|
||||||
|
|
||||||
|
@Tag(name = "智能体管理")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/agent")
|
||||||
|
public class AgentController {
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final AgentTemplateService agentTemplateService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@Operation(summary = "获取用户智能体列表")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<AgentEntity>> getUserAgents() {
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
List<AgentEntity> agents = agentService.getUserAgents(user.getId());
|
||||||
|
return new Result<List<AgentEntity>>().ok(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
@Operation(summary = "智能体列表(管理员)")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||||
|
})
|
||||||
|
public Result<PageData<AgentEntity>> adminAgentList(
|
||||||
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
PageData<AgentEntity> page = agentService.adminAgentList(params);
|
||||||
|
return new Result<PageData<AgentEntity>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取智能体详情")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<AgentEntity> getAgentById(@PathVariable("id") String id) {
|
||||||
|
AgentEntity agent = agentService.getAgentById(id);
|
||||||
|
return new Result<AgentEntity>().ok(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "创建智能体")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||||
|
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
||||||
|
|
||||||
|
// 设置用户ID和创建者信息
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
entity.setUserId(user.getId());
|
||||||
|
entity.setCreator(user.getId());
|
||||||
|
entity.setCreatedAt(new Date());
|
||||||
|
|
||||||
|
// ID、智能体编码和排序会在Service层自动生成
|
||||||
|
agentService.insert(entity);
|
||||||
|
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "更新智能体")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
||||||
|
// 先查询现有实体
|
||||||
|
AgentEntity existingEntity = agentService.getAgentById(id);
|
||||||
|
if (existingEntity == null) {
|
||||||
|
return new Result<Void>().error("智能体不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只更新提供的非空字段
|
||||||
|
if (dto.getAgentName() != null) {
|
||||||
|
existingEntity.setAgentName(dto.getAgentName());
|
||||||
|
}
|
||||||
|
if (dto.getAgentCode() != null) {
|
||||||
|
existingEntity.setAgentCode(dto.getAgentCode());
|
||||||
|
}
|
||||||
|
if (dto.getAsrModelId() != null) {
|
||||||
|
existingEntity.setAsrModelId(dto.getAsrModelId());
|
||||||
|
}
|
||||||
|
if (dto.getVadModelId() != null) {
|
||||||
|
existingEntity.setVadModelId(dto.getVadModelId());
|
||||||
|
}
|
||||||
|
if (dto.getLlmModelId() != null) {
|
||||||
|
existingEntity.setLlmModelId(dto.getLlmModelId());
|
||||||
|
}
|
||||||
|
if (dto.getTtsModelId() != null) {
|
||||||
|
existingEntity.setTtsModelId(dto.getTtsModelId());
|
||||||
|
}
|
||||||
|
if (dto.getTtsVoiceId() != null) {
|
||||||
|
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
|
||||||
|
}
|
||||||
|
if (dto.getMemModelId() != null) {
|
||||||
|
existingEntity.setMemModelId(dto.getMemModelId());
|
||||||
|
}
|
||||||
|
if (dto.getIntentModelId() != null) {
|
||||||
|
existingEntity.setIntentModelId(dto.getIntentModelId());
|
||||||
|
}
|
||||||
|
if (dto.getSystemPrompt() != null) {
|
||||||
|
existingEntity.setSystemPrompt(dto.getSystemPrompt());
|
||||||
|
}
|
||||||
|
if (dto.getLangCode() != null) {
|
||||||
|
existingEntity.setLangCode(dto.getLangCode());
|
||||||
|
}
|
||||||
|
if (dto.getLanguage() != null) {
|
||||||
|
existingEntity.setLanguage(dto.getLanguage());
|
||||||
|
}
|
||||||
|
if (dto.getSort() != null) {
|
||||||
|
existingEntity.setSort(dto.getSort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置更新者信息
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
existingEntity.setUpdater(user.getId());
|
||||||
|
existingEntity.setUpdatedAt(new Date());
|
||||||
|
|
||||||
|
agentService.updateById(existingEntity);
|
||||||
|
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "删除智能体")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> delete(@PathVariable String id) {
|
||||||
|
agentService.deleteById(id);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/template")
|
||||||
|
@Operation(summary = "智能体模板模板列表")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<AgentTemplateEntity>> templateList() {
|
||||||
|
List<AgentTemplateEntity> list = agentTemplateService
|
||||||
|
.list(new QueryWrapper<AgentTemplateEntity>().orderByAsc("sort"));
|
||||||
|
return new Result<List<AgentTemplateEntity>>().ok(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import xiaozhi.common.dao.BaseDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AgentDao extends BaseDao<AgentEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author chenerlei
|
||||||
|
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Mapper
|
||||||
|
* @createDate 2025-03-22 11:48:18
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AgentTemplateDao extends BaseMapper<AgentTemplateEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体创建DTO
|
||||||
|
* 专用于新增智能体,不包含id、agentCode和sort字段,这些字段由系统自动生成/设置默认值
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体创建对象")
|
||||||
|
public class AgentCreateDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "智能体名称", example = "客服助手")
|
||||||
|
@NotBlank(message = "智能体名称不能为空")
|
||||||
|
private String agentName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体更新DTO
|
||||||
|
* 专用于更新智能体,id字段是必需的,用于标识要更新的智能体
|
||||||
|
* 其他字段均为非必填,只更新提供的字段
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体更新对象")
|
||||||
|
public class AgentUpdateDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "智能体编码", example = "AGT_1234567890", required = false)
|
||||||
|
private String agentCode;
|
||||||
|
|
||||||
|
@Schema(description = "智能体名称", example = "客服助手", required = false)
|
||||||
|
private String agentName;
|
||||||
|
|
||||||
|
@Schema(description = "语音识别模型标识", example = "asr_model_02", required = false)
|
||||||
|
private String asrModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音活动检测标识", example = "vad_model_02", required = false)
|
||||||
|
private String vadModelId;
|
||||||
|
|
||||||
|
@Schema(description = "大语言模型标识", example = "llm_model_02", required = false)
|
||||||
|
private String llmModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色标识", example = "voice_02", required = false)
|
||||||
|
private String ttsVoiceId;
|
||||||
|
|
||||||
|
@Schema(description = "记忆模型标识", example = "mem_model_02", required = false)
|
||||||
|
private String memModelId;
|
||||||
|
|
||||||
|
@Schema(description = "意图模型标识", example = "intent_model_02", required = false)
|
||||||
|
private String intentModelId;
|
||||||
|
|
||||||
|
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
||||||
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Schema(description = "交互语种", example = "中文", required = false)
|
||||||
|
private String language;
|
||||||
|
|
||||||
|
@Schema(description = "排序", example = "1", required = false)
|
||||||
|
private Integer sort;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("ai_agent")
|
||||||
|
@Schema(description = "智能体信息")
|
||||||
|
public class AgentEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "智能体唯一标识")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "所属用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "智能体编码")
|
||||||
|
private String agentCode;
|
||||||
|
|
||||||
|
@Schema(description = "智能体名称")
|
||||||
|
private String agentName;
|
||||||
|
|
||||||
|
@Schema(description = "语音识别模型标识")
|
||||||
|
private String asrModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音活动检测标识")
|
||||||
|
private String vadModelId;
|
||||||
|
|
||||||
|
@Schema(description = "大语言模型标识")
|
||||||
|
private String llmModelId;
|
||||||
|
|
||||||
|
@Schema(description = "语音合成模型标识")
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
@Schema(description = "音色标识")
|
||||||
|
private String ttsVoiceId;
|
||||||
|
|
||||||
|
@Schema(description = "记忆模型标识")
|
||||||
|
private String memModelId;
|
||||||
|
|
||||||
|
@Schema(description = "意图模型标识")
|
||||||
|
private String intentModelId;
|
||||||
|
|
||||||
|
@Schema(description = "角色设定参数")
|
||||||
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "语言编码")
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
@Schema(description = "交互语种")
|
||||||
|
private String language;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
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.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体配置模板表
|
||||||
|
*
|
||||||
|
* @TableName ai_agent_template
|
||||||
|
*/
|
||||||
|
@TableName(value = "ai_agent_template")
|
||||||
|
@Data
|
||||||
|
public class AgentTemplateEntity implements Serializable {
|
||||||
|
/**
|
||||||
|
* 智能体唯一标识
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体编码
|
||||||
|
*/
|
||||||
|
private String agentCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体名称
|
||||||
|
*/
|
||||||
|
private String agentName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音识别模型标识
|
||||||
|
*/
|
||||||
|
private String asrModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音活动检测标识
|
||||||
|
*/
|
||||||
|
private String vadModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大语言模型标识
|
||||||
|
*/
|
||||||
|
private String llmModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音合成模型标识
|
||||||
|
*/
|
||||||
|
private String ttsModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音色标识
|
||||||
|
*/
|
||||||
|
private String ttsVoiceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆模型标识
|
||||||
|
*/
|
||||||
|
private String memModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 意图模型标识
|
||||||
|
*/
|
||||||
|
private String intentModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色设定参数
|
||||||
|
*/
|
||||||
|
private String systemPrompt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语言编码
|
||||||
|
*/
|
||||||
|
private String langCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交互语种
|
||||||
|
*/
|
||||||
|
private String language;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序权重
|
||||||
|
*/
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建者 ID
|
||||||
|
*/
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新者 ID
|
||||||
|
*/
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private Date updatedAt;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
|
public interface AgentService extends BaseService<AgentEntity> {
|
||||||
|
/**
|
||||||
|
* 根据用户ID获取智能体列表
|
||||||
|
*/
|
||||||
|
List<AgentEntity> getUserAgents(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员获取所有智能体列表(分页)
|
||||||
|
*/
|
||||||
|
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体详情
|
||||||
|
*/
|
||||||
|
AgentEntity getAgentById(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除这个用户的所有
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
void deleteAgentByUserId(Long userId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author chenerlei
|
||||||
|
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service
|
||||||
|
* @createDate 2025-03-22 11:48:18
|
||||||
|
*/
|
||||||
|
public interface AgentTemplateService extends IService<AgentTemplateEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.modules.agent.dao.AgentDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||||
|
private final AgentDao agentDao;
|
||||||
|
|
||||||
|
public AgentServiceImpl(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
|
||||||
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
|
IPage<AgentEntity> page = agentDao.selectPage(
|
||||||
|
getPage(params, "sort", true),
|
||||||
|
new QueryWrapper<>());
|
||||||
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AgentEntity getAgentById(String id) {
|
||||||
|
return agentDao.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean insert(AgentEntity entity) {
|
||||||
|
// 如果ID为空,自动生成一个UUID作为ID
|
||||||
|
if (entity.getId() == null || entity.getId().trim().isEmpty()) {
|
||||||
|
entity.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果智能体编码为空,自动生成一个带前缀的编码
|
||||||
|
if (entity.getAgentCode() == null || entity.getAgentCode().trim().isEmpty()) {
|
||||||
|
entity.setAgentCode("AGT_" + System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果排序字段为空,设置默认值0
|
||||||
|
if (entity.getSort() == null) {
|
||||||
|
entity.setSort(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.insert(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteAgentByUserId(Long userId) {
|
||||||
|
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
baseDao.delete(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author chenerlei
|
||||||
|
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service实现
|
||||||
|
* @createDate 2025-03-22 11:48:18
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity>
|
||||||
|
implements AgentTemplateService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package xiaozhi.modules.agent.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class AgentTemplateVO extends AgentTemplateEntity {
|
||||||
|
// 角色音色
|
||||||
|
private String ttsModelName;
|
||||||
|
|
||||||
|
// 角色模型
|
||||||
|
private String llmModelName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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.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 lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.common.user.UserDetail;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
|
||||||
|
@Tag(name = "设备管理")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/device")
|
||||||
|
public class DeviceController {
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
|
@PostMapping("/bind/{agentId}/{deviceCode}")
|
||||||
|
@Operation(summary = "绑定设备")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> bindDevice(@PathVariable String agentId, @PathVariable String deviceCode) {
|
||||||
|
deviceService.deviceActivation(agentId, deviceCode);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
@Operation(summary = "注册设备")
|
||||||
|
public Result<String> registerDevice(@RequestBody DeviceRegisterDTO deviceRegisterDTO) {
|
||||||
|
String macAddress = deviceRegisterDTO.getMacAddress();
|
||||||
|
if (StringUtils.isBlank(macAddress)) {
|
||||||
|
return new Result<String>().error(ErrorCode.NOT_NULL, "mac地址不能为空");
|
||||||
|
}
|
||||||
|
// 生成六位验证码
|
||||||
|
String code = String.valueOf(Math.random()).substring(2, 8);
|
||||||
|
String key = RedisKeys.getDeviceCaptchaKey(code);
|
||||||
|
String existsMac = null;
|
||||||
|
do {
|
||||||
|
existsMac = (String) redisUtils.get(key);
|
||||||
|
} while (StringUtils.isNotBlank(existsMac));
|
||||||
|
|
||||||
|
redisUtils.set(key, macAddress);
|
||||||
|
return new Result<String>().ok(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/bind/{agentId}")
|
||||||
|
@Operation(summary = "获取已绑定设备")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<DeviceEntity>> getUserDevices(@PathVariable String agentId) {
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
|
||||||
|
return new Result<List<DeviceEntity>>().ok(devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/unbind")
|
||||||
|
@Operation(summary = "解绑设备")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> unbindDevice(@RequestBody DeviceUnBindDTO unDeviveBind) {
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
deviceService.unbindDevice(user.getId(), unDeviveBind.getDeviceId());
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.device.utils.NetworkUtil;
|
||||||
|
|
||||||
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/ota/")
|
||||||
|
public class OTAController {
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
|
@Operation(summary = "检查 OTA 版本和设备激活状态")
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<String> checkOTAVersion(
|
||||||
|
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||||
|
|
||||||
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
|
||||||
|
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
||||||
|
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||||
|
}
|
||||||
|
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||||
|
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||||
|
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||||
|
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||||
|
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||||
|
}
|
||||||
|
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private ResponseEntity<String> createResponse(DeviceReportRespDTO deviceReportRespDTO) {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||||
|
String json = objectMapper.writeValueAsString(deviceReportRespDTO);
|
||||||
|
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
return ResponseEntity
|
||||||
|
.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.contentLength(jsonBytes.length)
|
||||||
|
.body(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package xiaozhi.modules.device.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备绑定的DTO
|
||||||
|
*
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-28
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "设备连接头信息")
|
||||||
|
public class DeviceBindDTO {
|
||||||
|
|
||||||
|
@Schema(description = "mac地址")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
@Schema(description = "所属用户id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "智能体id")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有设备的DTO
|
||||||
|
*
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-21
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "查询所有设备的DTO")
|
||||||
|
public class DevicePageUserDTO {
|
||||||
|
|
||||||
|
@Schema(description = "设备关键词")
|
||||||
|
private String keywords;
|
||||||
|
|
||||||
|
@Schema(description = "页数")
|
||||||
|
@Min(value = 0, message = "{page.number}")
|
||||||
|
private String page;
|
||||||
|
|
||||||
|
@Schema(description = "显示列数")
|
||||||
|
@Min(value = 0, message = "{limit.number}")
|
||||||
|
private String limit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备注册头信息
|
||||||
|
*
|
||||||
|
* @author zjy
|
||||||
|
* @since 2025-3-28
|
||||||
|
*/
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
@Schema(description = "设备注册头信息")
|
||||||
|
public class DeviceRegisterDTO implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "mac地址")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
@Schema(description = "设备固件信息上报求请求体")
|
||||||
|
public class DeviceReportReqDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
// region 实体属性
|
||||||
|
@Schema(description = "板子固件版本号")
|
||||||
|
private Integer version;
|
||||||
|
|
||||||
|
@Schema(description = "闪存大小(单位:字节)")
|
||||||
|
@JsonProperty("flash_size")
|
||||||
|
private Integer flashSize;
|
||||||
|
|
||||||
|
@Schema(description = "最小空闲堆内存(字节)")
|
||||||
|
@JsonProperty("minimum_free_heap_size")
|
||||||
|
private Integer minimumFreeHeapSize;
|
||||||
|
|
||||||
|
@Schema(description = "设备 MAC 地址")
|
||||||
|
@JsonProperty("mac_address")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
@Schema(description = "设备唯一标识 UUID")
|
||||||
|
private String uuid;
|
||||||
|
|
||||||
|
@Schema(description = "芯片型号名称")
|
||||||
|
@JsonProperty("chip_model_name")
|
||||||
|
private String chipModelName;
|
||||||
|
|
||||||
|
@Schema(description = "芯片详细信息")
|
||||||
|
@JsonProperty("chip_info")
|
||||||
|
private ChipInfo chipInfo;
|
||||||
|
|
||||||
|
@Schema(description = "应用程序信息")
|
||||||
|
private Application application;
|
||||||
|
|
||||||
|
@Schema(description = "分区表列表")
|
||||||
|
@JsonProperty("partition_table")
|
||||||
|
private List<Partition> partitionTable;
|
||||||
|
|
||||||
|
@Schema(description = "当前运行的 OTA 分区信息")
|
||||||
|
private OtaInfo ota;
|
||||||
|
|
||||||
|
@Schema(description = "板子配置信息")
|
||||||
|
private BoardInfo board;
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "芯片信息")
|
||||||
|
public static class ChipInfo {
|
||||||
|
@Schema(description = "芯片模型代码")
|
||||||
|
private Integer model;
|
||||||
|
|
||||||
|
@Schema(description = "核心数")
|
||||||
|
private Integer cores;
|
||||||
|
|
||||||
|
@Schema(description = "硬件修订版本")
|
||||||
|
private Integer revision;
|
||||||
|
|
||||||
|
@Schema(description = "芯片功能标志位")
|
||||||
|
private Integer features;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "板子编译信息")
|
||||||
|
public static class Application {
|
||||||
|
@Schema(description = "名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "应用版本号")
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
@Schema(description = "编译时间(UTC ISO格式)")
|
||||||
|
@JsonProperty("compile_time")
|
||||||
|
private String compileTime;
|
||||||
|
|
||||||
|
@Schema(description = "ESP-IDF 版本号")
|
||||||
|
@JsonProperty("idf_version")
|
||||||
|
private String idfVersion;
|
||||||
|
|
||||||
|
@Schema(description = "ELF 文件 SHA256 校验")
|
||||||
|
@JsonProperty("elf_sha256")
|
||||||
|
private String elfSha256;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "分区信息")
|
||||||
|
public static class Partition {
|
||||||
|
@Schema(description = "分区标签名")
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
@Schema(description = "分区类型")
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "子类型")
|
||||||
|
private Integer subtype;
|
||||||
|
|
||||||
|
@Schema(description = "起始地址")
|
||||||
|
private Integer address;
|
||||||
|
|
||||||
|
@Schema(description = "分区大小")
|
||||||
|
private Integer size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "OTA信息")
|
||||||
|
public static class OtaInfo {
|
||||||
|
@Schema(description = "当前OTA标签")
|
||||||
|
private String label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Schema(description = "板子连接和网络信息")
|
||||||
|
public static class BoardInfo {
|
||||||
|
@Schema(description = "板子类型")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "连接的 Wi-Fi SSID")
|
||||||
|
private String ssid;
|
||||||
|
|
||||||
|
@Schema(description = "Wi-Fi 信号强度(RSSI)")
|
||||||
|
private Integer rssi;
|
||||||
|
|
||||||
|
@Schema(description = "Wi-Fi 信道")
|
||||||
|
private Integer channel;
|
||||||
|
|
||||||
|
@Schema(description = "IP 地址")
|
||||||
|
private String ip;
|
||||||
|
|
||||||
|
@Schema(description = "MAC 地址")
|
||||||
|
private String mac;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "设备OTA检测版本返回体,包含激活码要求")
|
||||||
|
public class DeviceReportRespDTO {
|
||||||
|
@Schema(description = "服务器时间")
|
||||||
|
private ServerTime server_time;
|
||||||
|
|
||||||
|
@Schema(description = "激活码")
|
||||||
|
private Activation activation;
|
||||||
|
|
||||||
|
@Schema(description = "错误信息")
|
||||||
|
private String error;
|
||||||
|
|
||||||
|
@Schema(description = "固件版本信息")
|
||||||
|
private Firmware firmware;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Firmware {
|
||||||
|
@Schema(description = "版本号")
|
||||||
|
private String version;
|
||||||
|
@Schema(description = "下载地址")
|
||||||
|
private String url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DeviceReportRespDTO createError(String message) {
|
||||||
|
DeviceReportRespDTO resp = new DeviceReportRespDTO();
|
||||||
|
resp.setError(message);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
public static class Activation {
|
||||||
|
@Schema(description = "激活码")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "激活码信息: 激活地址")
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class ServerTime {
|
||||||
|
@Schema(description = "时间戳")
|
||||||
|
private Long timestamp;
|
||||||
|
|
||||||
|
@Schema(description = "时区")
|
||||||
|
private String timeZone;
|
||||||
|
|
||||||
|
@Schema(description = "时区偏移量,单位为分钟")
|
||||||
|
private Integer timezone_offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备解绑表单
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "设备解绑表单")
|
||||||
|
public class DeviceUnBindDTO implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "设备ID")
|
||||||
|
@NotBlank(message = "设备ID不能为空")
|
||||||
|
private String deviceId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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_device")
|
||||||
|
@Schema(description = "设备信息")
|
||||||
|
public class DeviceEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "ID")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "关联用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Schema(description = "MAC地址")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
@Schema(description = "最后连接时间")
|
||||||
|
private Date lastConnectedAt;
|
||||||
|
|
||||||
|
@Schema(description = "自动更新开关(0关闭/1开启)")
|
||||||
|
private Integer autoUpdate;
|
||||||
|
|
||||||
|
@Schema(description = "设备硬件型号")
|
||||||
|
private String board;
|
||||||
|
|
||||||
|
@Schema(description = "设备别名")
|
||||||
|
private String alias;
|
||||||
|
|
||||||
|
@Schema(description = "智能体ID")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
@Schema(description = "固件版本号")
|
||||||
|
private String appVersion;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
@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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package xiaozhi.modules.device.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
|
|
||||||
|
public interface DeviceService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据Mac地址获取设备信息
|
||||||
|
*/
|
||||||
|
DeviceEntity getDeviceById(String macAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查设备是否激活
|
||||||
|
*/
|
||||||
|
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
||||||
|
DeviceReportReqDTO deviceReport);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户指定智能体的设备列表,
|
||||||
|
*/
|
||||||
|
List<DeviceEntity> getUserDevices(Long userId, String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑设备
|
||||||
|
*/
|
||||||
|
void unbindDevice(Long userId, String deviceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备激活
|
||||||
|
*/
|
||||||
|
Boolean deviceActivation(String agentId, String activationCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除此用户的所有设备
|
||||||
|
*
|
||||||
|
* @param userId 用户id
|
||||||
|
*/
|
||||||
|
void deleteByUserId(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定用户的设备数量
|
||||||
|
*
|
||||||
|
* @param userId 用户id
|
||||||
|
* @return 设备数量
|
||||||
|
*/
|
||||||
|
Long selectCountByUserId(Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取全部设备信息
|
||||||
|
*
|
||||||
|
* @param dto 分页查找参数
|
||||||
|
* @return 用户列表分页数据
|
||||||
|
*/
|
||||||
|
PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto);
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
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 com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.common.user.UserDetail;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.common.utils.DateUtils;
|
||||||
|
import xiaozhi.modules.device.dao.DeviceDao;
|
||||||
|
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||||
|
|
||||||
|
private final DeviceDao deviceDao;
|
||||||
|
|
||||||
|
private final SysUserUtilService sysUserUtilService;
|
||||||
|
|
||||||
|
private final String frontedUrl;
|
||||||
|
|
||||||
|
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
|
||||||
|
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||||
|
if (StringUtils.isBlank(activationCode)) {
|
||||||
|
throw new RenException("激活码不能为空");
|
||||||
|
}
|
||||||
|
String deviceKey = "ota:activation:code:" + activationCode;
|
||||||
|
Object cacheDeviceId = redisTemplate.opsForValue().get(deviceKey);
|
||||||
|
if (cacheDeviceId == null) {
|
||||||
|
throw new RenException("激活码错误");
|
||||||
|
}
|
||||||
|
String deviceId = (String) cacheDeviceId;
|
||||||
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
|
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(cacheDeviceKey);
|
||||||
|
if (cacheMap == null) {
|
||||||
|
throw new RenException("激活码错误");
|
||||||
|
}
|
||||||
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
|
if (!activationCode.equals(cachedCode)) {
|
||||||
|
throw new RenException("激活码错误");
|
||||||
|
}
|
||||||
|
// 检查设备有没有被激活
|
||||||
|
if (selectById(deviceId) != null) {
|
||||||
|
throw new RenException("设备已激活");
|
||||||
|
}
|
||||||
|
|
||||||
|
String macAddress = (String) cacheMap.get("mac_address");
|
||||||
|
String board = (String) cacheMap.get("board");
|
||||||
|
String appVersion = (String) cacheMap.get("app_version");
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
if (user.getId() == null) {
|
||||||
|
throw new RenException("用户未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date currentTime = new Date();
|
||||||
|
DeviceEntity deviceEntity = new DeviceEntity();
|
||||||
|
deviceEntity.setId(deviceId);
|
||||||
|
deviceEntity.setBoard(board);
|
||||||
|
deviceEntity.setAgentId(agentId);
|
||||||
|
deviceEntity.setAppVersion(appVersion);
|
||||||
|
deviceEntity.setMacAddress(macAddress);
|
||||||
|
deviceEntity.setUserId(user.getId());
|
||||||
|
deviceEntity.setCreator(user.getId());
|
||||||
|
deviceEntity.setCreateDate(currentTime);
|
||||||
|
deviceEntity.setUpdater(user.getId());
|
||||||
|
deviceEntity.setUpdateDate(currentTime);
|
||||||
|
deviceEntity.setLastConnectedAt(currentTime);
|
||||||
|
deviceDao.insert(deviceEntity);
|
||||||
|
|
||||||
|
// 清理redis缓存
|
||||||
|
redisTemplate.delete(cacheDeviceKey);
|
||||||
|
redisTemplate.delete(deviceKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
||||||
|
DeviceReportReqDTO deviceReport) {
|
||||||
|
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
||||||
|
|
||||||
|
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||||
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
|
code.setCode(cachedCode);
|
||||||
|
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||||
|
} else {
|
||||||
|
String newCode = RandomUtil.randomNumbers(6);
|
||||||
|
code.setCode(newCode);
|
||||||
|
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.setActivation(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DeviceEntity> getUserDevices(Long userId, String agentId) {
|
||||||
|
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
wrapper.eq("agent_id", agentId);
|
||||||
|
return baseDao.selectList(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unbindDevice(Long userId, String deviceId) {
|
||||||
|
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
wrapper.eq("id", deviceId);
|
||||||
|
baseDao.delete(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteByUserId(Long userId) {
|
||||||
|
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
baseDao.delete(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long selectCountByUserId(Long userId) {
|
||||||
|
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||||
|
wrapper.eq("user_id", userId);
|
||||||
|
return baseDao.selectCount(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto) {
|
||||||
|
Map<String, Object> params = new HashMap<String, Object>();
|
||||||
|
params.put(Constant.PAGE, dto.getPage());
|
||||||
|
params.put(Constant.LIMIT, dto.getLimit());
|
||||||
|
IPage<DeviceEntity> page = baseDao.selectPage(
|
||||||
|
getPage(params, "sort", true),
|
||||||
|
// 定义查询条件
|
||||||
|
new QueryWrapper<DeviceEntity>()
|
||||||
|
// 必须设备关键词查找
|
||||||
|
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
|
||||||
|
// 循环处理page获取回来的数据,返回需要的字段
|
||||||
|
List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> {
|
||||||
|
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
||||||
|
// 把最后修改的时间,改为简短描述的时间
|
||||||
|
vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate()));
|
||||||
|
sysUserUtilService.assignUsername(device.getUserId(),
|
||||||
|
vo::setBindUserName);
|
||||||
|
vo.setDeviceType(device.getBoard());
|
||||||
|
return vo;
|
||||||
|
}).toList();
|
||||||
|
// 计算页数
|
||||||
|
return new PageData<>(list, page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceReportRespDTO.ServerTime buildServerTime() {
|
||||||
|
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
|
||||||
|
TimeZone tz = TimeZone.getDefault();
|
||||||
|
serverTime.setTimestamp(Instant.now().toEpochMilli());
|
||||||
|
serverTime.setTimeZone(tz.getID());
|
||||||
|
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||||
|
return serverTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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表示单播地址,合法
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package xiaozhi.modules.device.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class DeviceOtaVO {
|
||||||
|
private Activation activation;
|
||||||
|
private Mqtt mqtt;
|
||||||
|
private ServerTime server_time;
|
||||||
|
private Firmware firmware;
|
||||||
|
private String error;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Activation {
|
||||||
|
private String code;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public Activation() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Activation(String code, String message) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Mqtt {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ServerTime {
|
||||||
|
private Long timestamp;
|
||||||
|
private Integer timezone_offset;
|
||||||
|
|
||||||
|
public ServerTime() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerTime(Long timestamp, Integer timezone_offset) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.timezone_offset = timezone_offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Firmware {
|
||||||
|
private String version;
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
public Firmware() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Firmware(String version, String url) {
|
||||||
|
this.version = version;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package xiaozhi.modules.device.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "用户显示设备列表VO")
|
||||||
|
public class UserShowDeviceListVO {
|
||||||
|
|
||||||
|
@Schema(description = "app版本")
|
||||||
|
private String appVersion;
|
||||||
|
|
||||||
|
@Schema(description = "绑定用户名称")
|
||||||
|
private String bindUserName;
|
||||||
|
|
||||||
|
@Schema(description = "设备型号")
|
||||||
|
private String deviceType;
|
||||||
|
|
||||||
|
@Schema(description = "设备唯一标识符")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "mac地址")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
@Schema(description = "开启OTA")
|
||||||
|
private Integer otaUpgrade;
|
||||||
|
|
||||||
|
@Schema(description = "最近对话时间")
|
||||||
|
private String recentChatTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package xiaozhi.modules.model.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
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 io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
|
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||||
|
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||||
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
import xiaozhi.modules.model.service.ModelProviderService;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/models")
|
||||||
|
@Tag(name = "模型配置")
|
||||||
|
public class ModelController {
|
||||||
|
|
||||||
|
private final ModelProviderService modelProviderService;
|
||||||
|
|
||||||
|
private final ModelConfigService modelConfigService;
|
||||||
|
|
||||||
|
@GetMapping("/models/names")
|
||||||
|
@Operation(summary = "获取所有模型名称")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<List<String>> getModelNames(@RequestParam String modelType,
|
||||||
|
@RequestParam(required = false) String modelName) {
|
||||||
|
List<String> modelNameList = modelConfigService.getModelCodeList(modelType, modelName);
|
||||||
|
return new Result<List<String>>().ok(modelNameList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{modelType}/provideTypes")
|
||||||
|
@Operation(summary = "获取模型供应器列表")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<List<ModelProviderDTO>> getModelProviderList(@PathVariable String modelType) {
|
||||||
|
List<ModelProviderDTO> modelProviderDTOS = modelProviderService.getListByModelType(modelType);
|
||||||
|
return new Result<List<ModelProviderDTO>>().ok(modelProviderDTOS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/models/list")
|
||||||
|
@Operation(summary = "获取模型配置列表")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<PageData<ModelConfigDTO>> getModelConfigList(
|
||||||
|
@RequestParam(required = true) String modelType,
|
||||||
|
@RequestParam(required = false) String modelName,
|
||||||
|
@RequestParam(required = true, defaultValue = "0") String page,
|
||||||
|
@RequestParam(required = true, defaultValue = "10") String limit) {
|
||||||
|
PageData<ModelConfigDTO> pageList = modelConfigService.getPageList(modelType, modelName, page, limit);
|
||||||
|
return new Result<PageData<ModelConfigDTO>>().ok(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/models/{modelType}/{provideCode}")
|
||||||
|
@Operation(summary = "新增模型配置")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<ModelConfigDTO> addModelConfig(@PathVariable String modelType,
|
||||||
|
@PathVariable String provideCode,
|
||||||
|
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||||
|
ModelConfigDTO modelConfigDTO = modelConfigService.add(modelType, provideCode, modelConfigBodyDTO);
|
||||||
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/models/{modelType}/{provideCode}/{id}")
|
||||||
|
@Operation(summary = "编辑模型配置")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<ModelConfigDTO> editModelConfig(@PathVariable String modelType,
|
||||||
|
@PathVariable String provideCode,
|
||||||
|
@PathVariable String id,
|
||||||
|
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||||
|
ModelConfigDTO modelConfigDTO = modelConfigService.edit(modelType, provideCode, id, modelConfigBodyDTO);
|
||||||
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/models/{id}")
|
||||||
|
@Operation(summary = "删除模型配置")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> deleteModelConfig(@PathVariable String id) {
|
||||||
|
modelConfigService.delete(id);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/models/{id}")
|
||||||
|
@Operation(summary = "获取模型配置")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<ModelConfigDTO> getModelConfig(@PathVariable String id) {
|
||||||
|
ModelConfigEntity item = modelConfigService.selectById(id);
|
||||||
|
ModelConfigDTO modelConfigDTO = ConvertUtils.sourceToTarget(item, ModelConfigDTO.class);
|
||||||
|
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/models/{modelId}/voices")
|
||||||
|
@Operation(summary = "获取模型音色")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<String>> getVoiceList(@PathVariable String modelId,
|
||||||
|
@RequestParam(required = false) String voiceName) {
|
||||||
|
|
||||||
|
List<String> voiceList = modelConfigService.getVoiceList(modelId, voiceName);
|
||||||
|
return new Result<List<String>>().ok(voiceList);
|
||||||
|
}
|
||||||
|
}
|
||||||