Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
919c2ffd46 | ||
|
|
e6d63a811e | ||
|
|
92227098b7 | ||
|
|
571080c1d6 | ||
|
|
d07feb837d | ||
|
|
63e378524b | ||
|
|
ea5f54e421 | ||
|
|
72f7514114 | ||
|
|
44e1f00ffc | ||
|
|
4a3ac2cfcd | ||
|
|
bbc31e01c4 | ||
|
|
770a198772 | ||
|
|
a26bee3696 | ||
|
|
abb8f4f963 | ||
|
|
43d2adff70 | ||
|
|
e2dffea423 | ||
|
|
3d5eaba46f | ||
|
|
1bb8fbd56f | ||
|
|
b699886953 | ||
|
|
12c957d48b | ||
|
|
48e890c1b1 | ||
|
|
05331f001a | ||
|
|
aa77bfdfc4 | ||
|
|
a2baef8911 | ||
|
|
3fcfb65d45 | ||
|
|
8f266bea0d | ||
|
|
0a765f4aac | ||
|
|
ee3f0555d1 | ||
|
|
9fc1285c09 | ||
|
|
e6e8ccee50 | ||
|
|
f9632af016 | ||
|
|
81359bf419 | ||
|
|
a144570ed7 | ||
|
|
0732b72e8f | ||
|
|
8332445a52 | ||
|
|
c9111d1bfc | ||
|
|
e7d999278d | ||
|
|
4043d20ea6 | ||
|
|
afe88ccfd6 | ||
|
|
56c3a04809 | ||
|
|
4530345a1f | ||
|
|
4a89ec8515 | ||
|
|
cb5f1c6485 | ||
|
|
15c0677a4b | ||
|
|
dc68b8148a | ||
|
|
1976034f12 | ||
|
|
727621fdac | ||
|
|
0f3806d358 | ||
|
|
dfb2bf3923 | ||
|
|
bf08a91191 | ||
|
|
f0e353cc1a | ||
|
|
456b1eddcc | ||
|
|
5e08f05437 | ||
|
|
9f9227bae4 | ||
|
|
76f47bce37 | ||
|
|
e1f54b30dc | ||
|
|
fc4e7e6c05 | ||
|
|
7aaa79b644 | ||
|
|
9b6f894be2 | ||
|
|
48f7ef2023 | ||
|
|
54692859fd | ||
|
|
1719244015 | ||
|
|
48f8c6c5b7 | ||
|
|
f0815e595b | ||
|
|
4f829017ed | ||
|
|
d2f8f05acb | ||
|
|
e8b1156bfd | ||
|
|
bf748773c8 | ||
|
|
a05b25ae90 | ||
|
|
3ceb3feccf | ||
|
|
b4944f23ef | ||
|
|
09c17b2dc5 | ||
|
|
6f7ff9f858 | ||
|
|
9e9af2a031 | ||
|
|
7d74e853ae | ||
|
|
1d627d570d | ||
|
|
9b0088f4f0 | ||
|
|
b50c7dbed4 | ||
|
|
f92c0313a6 | ||
|
|
233e1ee0a6 | ||
|
|
805b40ec76 | ||
|
|
86ca3e5be0 | ||
|
|
bb2143d9a0 | ||
|
|
ef662e0ccf | ||
|
|
30708a59f1 | ||
|
|
b0262cfe72 | ||
|
|
259886c1a8 | ||
|
|
f1b4b33727 | ||
|
|
38e8b690bd | ||
|
|
9a52b43e09 | ||
|
|
5e35cfc44a | ||
|
|
ac0ec6dfe6 | ||
|
|
361defc348 | ||
|
|
b9fc0da215 | ||
|
|
6ac8947168 | ||
|
|
5655930d12 | ||
|
|
18acc37324 | ||
|
|
e9155d719e | ||
|
|
16e77b02c2 | ||
|
|
adc1902649 | ||
|
|
1cc470bc5a | ||
|
|
b3f6efc7d0 | ||
|
|
d91fbf3ff4 | ||
|
|
e552593474 | ||
|
|
1f5a78f458 | ||
|
|
f490c15282 | ||
|
|
90a31debd1 | ||
|
|
416145a90e | ||
|
|
6285786282 | ||
|
|
f9f6a69c89 | ||
|
|
61da60bdf1 | ||
|
|
2d5ef3011a | ||
|
|
63dab6d224 | ||
|
|
38dd76dfb3 | ||
|
|
fe642de526 | ||
|
|
f4b7eefc38 | ||
|
|
5bbbad02b4 | ||
|
|
bd85dd5faf | ||
|
|
d91ba3d219 | ||
|
|
7992c40815 | ||
|
|
574b01db97 | ||
|
|
2d6049b7bd | ||
|
|
96d0f2e6f9 | ||
|
|
b46bd4d0dd | ||
|
|
244c76793c | ||
|
|
411fc46189 | ||
|
|
48427e330f | ||
|
|
5ddc2451c1 | ||
|
|
e357cad2bb | ||
|
|
80be8b2eaa | ||
|
|
9a5aaea8ca | ||
|
|
7d585615a9 | ||
|
|
3f3375dcac | ||
|
|
f555eae653 | ||
|
|
e805a6e037 | ||
|
|
3ed82ed7bb | ||
|
|
d638e512c9 | ||
|
|
204b966935 | ||
|
|
8318ba5e55 | ||
|
|
d39c595828 | ||
|
|
04154162c6 | ||
|
|
47f8e0f04c | ||
|
|
7e4ada5f4b | ||
|
|
b0b4f87848 | ||
|
|
d9085b82bf | ||
|
|
8e5770ff59 | ||
|
|
beb420c69d | ||
|
|
1d8b898924 | ||
|
|
b5b4c5ced7 | ||
|
|
b4e9d77895 | ||
|
|
21a08b3c7a | ||
|
|
6977d6bf73 | ||
|
|
39f81dd0c0 | ||
|
|
b3191f5e0b | ||
|
|
78f2f21142 | ||
|
|
497fc1d1ae | ||
|
|
d83e1c6fb2 | ||
|
|
c5218d485a | ||
|
|
19cbd3b8da | ||
|
|
c0c5630bfb | ||
|
|
969b5fcfbb | ||
|
|
0739fa2195 | ||
|
|
3c62adab3c | ||
|
|
edc601c35e | ||
|
|
951370eee7 | ||
|
|
fa81f4a150 | ||
|
|
e6a90c40fb | ||
|
|
2e0a1c00fa | ||
|
|
109754fac9 | ||
|
|
c3fbff3d7a | ||
|
|
6dd967fe24 | ||
|
|
f0bfdba944 | ||
|
|
7f76b5f4ee | ||
|
|
35f4d8f15a | ||
|
|
783f17e9e8 | ||
|
|
9e305c2840 | ||
|
|
78fa5d3110 | ||
|
|
d57482d966 | ||
|
|
3d7c2aff4b | ||
|
|
3757c5dde7 | ||
|
|
13c0a966ec | ||
|
|
d987619946 | ||
|
|
8fb9498fe6 | ||
|
|
b8f64a6693 | ||
|
|
e31bb047bf | ||
|
|
6897b813a6 | ||
|
|
83f86406b5 | ||
|
|
dde237ff0d | ||
|
|
6a4887e252 | ||
|
|
d945b94910 | ||
|
|
bc7dcf791d | ||
|
|
66c71fe3b7 | ||
|
|
16a7aeec3b | ||
|
|
94605345e5 | ||
|
|
ec9fa32c81 | ||
|
|
264aa15281 | ||
|
|
180a6ff824 | ||
|
|
5904ee1571 | ||
|
|
bc99341f85 | ||
|
|
8b9174b67e | ||
|
|
6c04a6cf69 | ||
|
|
14a63c4b97 | ||
|
|
098d13a34b | ||
|
|
488f247744 | ||
|
|
c74dbf03cd | ||
|
|
1f836c3235 | ||
|
|
bd2e2e77d5 | ||
|
|
6282ef14e8 | ||
|
|
558f23688f | ||
|
|
39fe057f9f | ||
|
|
541f2de599 | ||
|
|
68116254cd | ||
|
|
77ff4599ea | ||
|
|
652f5a3247 | ||
|
|
9b6e57b143 | ||
|
|
bdc19256bf | ||
|
|
de5eaf4467 | ||
|
|
ca884833d1 | ||
|
|
9fc164a69d | ||
|
|
0da2da83a5 | ||
|
|
bfdfa44edd | ||
|
|
ed1cfe5eb6 | ||
|
|
b696f3f5db | ||
|
|
7a0cf5ef9a | ||
|
|
d1b7cb6a36 | ||
|
|
b21d699472 | ||
|
|
029fadd778 | ||
|
|
215d50235b | ||
|
|
000e676397 | ||
|
|
208c045d3d | ||
|
|
ee20f59a8e | ||
|
|
925cf3dabb | ||
|
|
0049d0e926 | ||
|
|
56959808a9 | ||
|
|
8667a9680e | ||
|
|
8c7d129089 | ||
|
|
a3a9b98a1d | ||
|
|
f2f66579cb | ||
|
|
1354cf2a87 | ||
|
|
15f5592429 | ||
|
|
dc69094f00 | ||
|
|
8ad3783a36 | ||
|
|
15d5dd8e92 | ||
|
|
b07edb977b | ||
|
|
1509fc2039 | ||
|
|
2877bb8faa | ||
|
|
a83410cb49 | ||
|
|
4ff9f2235b | ||
|
|
1bc611f097 | ||
|
|
4e0a3941ff | ||
|
|
4e885dac3e | ||
|
|
eafd131a22 | ||
|
|
289ce3716b | ||
|
|
2242070bdc | ||
|
|
ee83cea11c | ||
|
|
6e571a43d8 | ||
|
|
466a0245a9 | ||
|
|
d9ce5e4ce7 | ||
|
|
6b0dd291c2 | ||
|
|
4ff77d0d7e | ||
|
|
e00da5d8d2 | ||
|
|
1a525a59ab | ||
|
|
d5581c4236 | ||
|
|
4bf950574c | ||
|
|
0b9e434c4b | ||
|
|
c4b1466643 | ||
|
|
488842029b | ||
|
|
1b0ce59371 | ||
|
|
97bea89204 | ||
|
|
0892dfc601 | ||
|
|
20d66ff401 | ||
|
|
e2fda532d1 | ||
|
|
68035af7b3 | ||
|
|
ee25555f0e | ||
|
|
e528510f5c | ||
|
|
afa0527b4c | ||
|
|
dd7d23498f | ||
|
|
eaaea2b24c | ||
|
|
58d0aefae1 | ||
|
|
11c9915a70 | ||
|
|
6cf6b0d645 | ||
|
|
1980a39b73 | ||
|
|
8d3fbeeb22 | ||
|
|
2678644c2c | ||
|
|
25da9fe119 | ||
|
|
03d89d22ec | ||
|
|
100cd25ffc | ||
|
|
6284a74a6e | ||
|
|
978cd8ba8e | ||
|
|
c76f28824c | ||
|
|
703ecfae40 | ||
|
|
b15c59aa48 | ||
|
|
98a4ce701c | ||
|
|
2fc0de8f4e | ||
|
|
66fd551d68 | ||
|
|
6e4e9a002b | ||
|
|
91278b0b3d | ||
|
|
ff9b3ed1a2 | ||
|
|
0c855d4ba8 | ||
|
|
9176e9d255 | ||
|
|
2bab917b3e | ||
|
|
b80086a349 | ||
|
|
8a4fca9bd5 | ||
|
|
13288a6b3a | ||
|
|
57ed552f43 | ||
|
|
5a4d65986d | ||
|
|
427b3bb774 | ||
|
|
dcb6d0feab | ||
|
|
c9eb640510 | ||
|
|
766a706250 | ||
|
|
f55d6c2e60 |
@@ -9,26 +9,24 @@ assignees: ''
|
|||||||
## 🐛 问题描述
|
## 🐛 问题描述
|
||||||
<!-- 清晰简洁地描述问题是什么 -->
|
<!-- 清晰简洁地描述问题是什么 -->
|
||||||
|
|
||||||
## 🔍 复现步骤
|
## 🖥️ 环境信息
|
||||||
<!-- 详细描述复现问题的步骤 -->
|
- 部署方式: 全模块部署 还是 单Server部署
|
||||||
|
- 版本号: 例如 0.3.x
|
||||||
|
|
||||||
|
## 🔍 告诉我们,应该怎么复现这个问题
|
||||||
|
<!-- 这个很重要,方便我们快速定位 -->
|
||||||
1. 打开 '...'
|
1. 打开 '...'
|
||||||
2. 点击 '...'
|
2. 点击 '...'
|
||||||
3. 滚动到 '...'
|
3. 滚动到 '...'
|
||||||
4. 看到错误
|
4. 看到错误
|
||||||
|
|
||||||
## 🤔 预期行为
|
## 🤔 你原本希望是怎么样的
|
||||||
<!-- 简要描述预期的正确行为 -->
|
<!-- 简要描述预期的正确行为 -->
|
||||||
|
|
||||||
## 😯 截图
|
## 😯 提供一些截图
|
||||||
<!-- 如果适用,添加问题的截图 -->
|
<!-- 如果适用,添加问题的截图 -->
|
||||||
|
1. 比如日志截图,越多越好
|
||||||
## 🖥️ 环境信息
|
2. 比如界面反应
|
||||||
- 操作系统: [例如 Windows 10]
|
|
||||||
- 浏览器: [例如 Chrome 89]
|
|
||||||
- 项目版本: [例如 1.0.0]
|
|
||||||
- Python版本: [例如 3.9.13]
|
|
||||||
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
|
|
||||||
- Nodejs版本:[例如 v20.14.0]
|
|
||||||
|
|
||||||
## 📋 其他信息
|
## 📋 其他信息
|
||||||
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||||
|
|||||||
@@ -155,7 +155,14 @@ main/manager-web/node_modules
|
|||||||
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||||
main/xiaozhi-server/models/sherpa-onnx*
|
main/xiaozhi-server/models/sherpa-onnx*
|
||||||
my_wakeup_words.mp3
|
my_wakeup_words.mp3
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_not_found.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code/*.wav
|
||||||
|
!main/xiaozhi-server/config/assets/max_output_size.wav
|
||||||
main/manager-api/.vscode
|
main/manager-api/.vscode
|
||||||
|
|
||||||
# Ignore webpack cache directory
|
# Ignore webpack cache directory
|
||||||
main/manager-web/.webpack_cache/
|
main/manager-web/.webpack_cache/
|
||||||
|
main/xiaozhi-server/mysql
|
||||||
|
uploadfile
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ RUN apt-get update && \
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 配置Nginx
|
# 配置Nginx
|
||||||
COPY docs/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
# 复制前端构建产物
|
# 复制前端构建产物
|
||||||
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
COPY --from=web-builder /app/dist /usr/share/nginx/html
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
|
||||||
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
本项目为开源智能硬件项目
|
本项目为开源智能硬件项目
|
||||||
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
||||||
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python实现<br/>
|
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
|
||||||
帮助您快速搭建小智服务器
|
帮助您快速搭建小智服务器
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="./README.md">English</a>
|
<a href="./README.md">English</a>
|
||||||
· 简体中文
|
· <a href="./docs/FAQ.md">常见问题</a>
|
||||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
|
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">反馈问题</a>
|
||||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a>
|
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a>
|
||||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues ">反馈问题</a>
|
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||||
@@ -131,108 +130,66 @@
|
|||||||
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
||||||
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
||||||
|
|
||||||
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
|
2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。
|
||||||
`config.yaml` 中开启防护:
|
|
||||||
|
|
||||||
```yaml
|
---
|
||||||
server:
|
|
||||||
auth:
|
## 部署文档
|
||||||
# 开启防护
|
|
||||||
enabled: true
|

|
||||||
|
|
||||||
|
本项目提供两种部署方式,请根据您的具体需求选择:
|
||||||
|
|
||||||
|
#### 🚀 部署方式选择
|
||||||
|
|
||||||
|
| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 |
|
||||||
|
|---------|------|---------|---------|---------|
|
||||||
|
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
|
||||||
|
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
|
||||||
|
|
||||||
|
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
|
||||||
|
|
||||||
|
```
|
||||||
|
智控台地址: https://2662r3426b.vicp.fun
|
||||||
|
|
||||||
|
服务测试工具: https://2662r3426b.vicp.fun/test/
|
||||||
|
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
|
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
```
|
```
|
||||||
|
|
||||||
开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 功能清单 ✨
|
||||||
|
|
||||||
## 部署方式 🚀
|
### 已实现 ✅
|
||||||
|
|
||||||
[](./docs/Deployment.md)
|
| 功能模块 | 描述 |
|
||||||
|
|---------|------|
|
||||||
|
| 通信协议 | 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互 |
|
||||||
|
| 对话交互 | 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 |
|
||||||
|
| 意图识别 | 支持使用LLM意图识别、function call函数调用,减少硬编码意图判断 |
|
||||||
|
| 多语言识别 | 支持国语、粤语、英语、日语、韩语(默认使用 FunASR) |
|
||||||
|
| LLM 模块 | 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口 |
|
||||||
|
| TTS 模块 | 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求 |
|
||||||
|
| 记忆功能 | 支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求 |
|
||||||
|
| IOT功能 | 支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制 |
|
||||||
|
| 智控台 | 提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理 |
|
||||||
|
|
||||||
### 部署文档
|
### 正在开发 🚧
|
||||||
|
|
||||||
本项目支持`最简化`安装和`全模块`安装。请根据具体需求选择安装方式。
|
想了解具体开发计划进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
||||||
|
|
||||||
`最简化`适合在较低资源配置中运行,无需数据库。[请参考-最简化安装架构图](./docs/images/deploy1.png)
|
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
||||||
|
|
||||||
`全模块`适合想体验完整功能,过程复杂。[请参考-全模块安装架构图](./docs/images/deploy2.png)
|
|
||||||
|
|
||||||
#### 1、 最简化安装
|
|
||||||
|
|
||||||
`最简化`安装包含:智能对话、IOT等功能,数据存储在`配置文件`。我们提供了两种安装教程,如果你决定使用最简化安装,可选择`Docker只运行Server`或`本地源码只运行Server`任意一种方式。
|
|
||||||
|
|
||||||
##### 1.1. Docker只运行Server
|
|
||||||
|
|
||||||
你可以使用编译好的docker镜像安装,[点击Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)。优点:减少环境配置的烦恼;缺点:版本可能有点旧,镜像文件较大。
|
|
||||||
|
|
||||||
##### 2.2. 本地源码只运行Server
|
|
||||||
|
|
||||||
你可以以本地源码的方式运行,[点击本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)。优点:可以随时更新最新代码;缺点:安装Conda环境较为复杂。
|
|
||||||
|
|
||||||
#### 2、 全模块安装
|
|
||||||
|
|
||||||
`全模块`安装包含:智能对话、IOT、OTA、智控台,数据存储在`数据库`中。我们提供了两种安装教程,如果你决定使用全模块安装,可选择`Docker运行全模块`或`本地源码运行全模块`任意一种方式。
|
|
||||||
|
|
||||||
##### 2.1. Docker运行全模块
|
|
||||||
|
|
||||||
你可以使用编译好的docker镜像安装,[点击Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97)。优点:减少环境配置的烦恼;缺点:版本可能有点旧,镜像文件较大。
|
|
||||||
|
|
||||||
##### 2.2. 本地源码运行全模块
|
|
||||||
|
|
||||||
你可以以本地源码的方式运行,[点击本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97)。优点:可以随时更新最新代码;缺点:安装Java、Node.js、Conda环境较为复杂。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常见问题 ❓
|
|
||||||
|
|
||||||
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 产品生态 👬
|
## 产品生态 👬
|
||||||
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
||||||
|
|
||||||
- [小智安卓客户端](https://github.com/TOM88812/xiaozhi-android-client)
|
| 项目名称 | 项目地址 | 项目描述 |
|
||||||
|
|:---------------------|:--------|:--------|
|
||||||
一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。现在是flutter版本,打通IOS、Android端。
|
| 小智安卓客户端 | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | 一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。<br/>现在是flutter版本,打通IOS、Android端。 |
|
||||||
- [小智电脑客户端](https://github.com/Huang-junsen/py-xiaozhi)
|
| 小智电脑客户端 | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | 该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,<br/>依然能够体过代码体验小智 AI 的功能。 |
|
||||||
|
| 小智Java服务端 | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | 小智开源后端服务 Java 版本是一个基于 Java 的开源项目。<br/>它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
|
||||||
该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,依然能够体过代码体验小智 AI 的功能。主要功能包括 AI 语音交互、视觉多模态识别、IoT 设备集成、联网音乐播放、语音唤醒、自动对话模式、图形化界面、命令行模式、跨平台支持、音量控制、会话管理、加密音频传输、自动验证码处理等。
|
|
||||||
- [小智Java服务端](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
|
|
||||||
|
|
||||||
小智开源后端服务 Java 版本是一个基于 Java 的开源项目,它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。
|
|
||||||
---
|
|
||||||
## 功能清单 ✨
|
|
||||||
|
|
||||||
### 已实现 ✅
|
|
||||||
|
|
||||||
- **通信协议**
|
|
||||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
|
||||||
- **对话交互**
|
|
||||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
|
||||||
- **意图识别**
|
|
||||||
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
|
|
||||||
- **多语言识别**
|
|
||||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
|
||||||
- **LLM 模块**
|
|
||||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
|
||||||
- **TTS 模块**
|
|
||||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
|
||||||
- **记忆功能**
|
|
||||||
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
|
|
||||||
- **IOT功能**
|
|
||||||
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
|
|
||||||
- **智控台**
|
|
||||||
提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理。
|
|
||||||
|
|
||||||
|
|
||||||
### 正在开发 🚧
|
|
||||||
|
|
||||||
- 多种心情模式
|
|
||||||
|
|
||||||
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
|
||||||
|
|
||||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -274,7 +231,7 @@ server:
|
|||||||
| 使用方式 | 支持平台 | 免费平台 |
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||||
| 接口调用 | DoubaoASR | - |
|
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -298,10 +255,17 @@ server:
|
|||||||
|
|
||||||
## 鸣谢 🙏
|
## 鸣谢 🙏
|
||||||
|
|
||||||
- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。
|
| Logo | 项目/公司 | 说明 |
|
||||||
- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。
|
|:---:|:---:|:---|
|
||||||
|
| <img src="./docs/images/logo_bailing.png" width="160"> | [百聆语音对话机器人](https://github.com/wwbin2017/bailing) | 本项目受[百聆语音对话机器人](https://github.com/wwbin2017/bailing)启发,并在其基础上实现 |
|
||||||
|
| <img src="./docs/images/logo_tenclass.png" width="160"> | [十方融海](https://www.tenclass.com/) | 感谢[十方融海](https://www.tenclass.com/)为小智生态制定了标准的通讯协议、多设备兼容性方案及高并发场景实践示范;为本项目提供了全链路技术文档支持 |
|
||||||
|
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [玄凤科技](https://github.com/Eric0308) | 感谢[玄凤科技](https://github.com/Eric0308)贡献函数调用框架、MCP通信协议及插件化调用机制的实现代码,通过标准化的指令调度体系与动态扩展能力,显著提升了前端设备(IoT)的交互效率和功能延展性 |
|
||||||
|
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [汇远设计](http://ui.kwd988.net/) | 感谢[汇远设计](http://ui.kwd988.net/)为本项目提供专业视觉解决方案,用其服务超千家企业的设计实战经验,赋能本项目产品用户体验 |
|
||||||
|
| <img src="./docs/images/logo_qinren.png" width="160"> | [西安勤人信息科技](https://www.029app.com/) | 感谢[西安勤人信息科技](https://www.029app.com/)深化本项目视觉体系,确保整体设计风格在多场景应用中的一致性和扩展性 |
|
||||||
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ xiaozhi-server
|
|||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
##### 1.2.3.2 下载 config.yaml
|
##### 1.2.3.2 创建 config.yaml
|
||||||
|
|
||||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||||
|
|
||||||
@@ -137,7 +137,10 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
docker stop xiaozhi-esp32-server
|
||||||
docker rm xiaozhi-esp32-server
|
docker rm xiaozhi-esp32-server
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
5.3、重新按docker方式部署
|
5.3、重新按docker方式部署
|
||||||
@@ -221,43 +224,36 @@ python app.py
|
|||||||
## 配置项目
|
## 配置项目
|
||||||
|
|
||||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
如果你的`data`下面没有`.config.yaml`文件,有两个方式,任选一种:
|
||||||
|
|
||||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
第一个方式:你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`。在此文件上修改
|
||||||
|
|
||||||
|
第二个方式:你也可以创建在`data`目录下手动创建`.config.yaml`空文件,然后在这个文件中增加必要的配置信息,系统会优先读取`.config.yaml`文件的配置,如果`.config.yaml`没有配置的,系统会自动去加载`xiaozhi-server`目录下的`config.yaml`的配置。推荐使用这种方式,这种方式是最简洁的方式。
|
||||||
|
|
||||||
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||||
|
|
||||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例
|
||||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
server:
|
||||||
|
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
|
||||||
|
prompt: |
|
||||||
|
我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||||
|
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
||||||
|
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
|
||||||
|
请你像一个人一样说话,请勿返回配置xml及其他特殊字符。
|
||||||
|
|
||||||
selected_module:
|
selected_module:
|
||||||
VAD: SileroVAD
|
LLM: DoubaoLLM
|
||||||
ASR: FunASR
|
|
||||||
LLM: ChatGLMLLM
|
|
||||||
TTS: EdgeTTS
|
|
||||||
# 默认不开启记忆,如需开启请看配置文件里的描述
|
|
||||||
Memory: nomem
|
|
||||||
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
|
||||||
Intent: nointent
|
|
||||||
```
|
|
||||||
|
|
||||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
|
||||||
使用时,在`selected_module`修改成对应的如下LLM配置的名称:
|
|
||||||
|
|
||||||
```
|
|
||||||
LLM:
|
LLM:
|
||||||
DeepSeekLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
ChatGLMLLM:
|
ChatGLMLLM:
|
||||||
type: openai
|
api_key: xxxxxxxxxxxxxxx.xxxxxx
|
||||||
...
|
|
||||||
DifyLLM:
|
|
||||||
type: dify
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。
|
||||||
|
比如你要换更换模型,修改`selected_module`下的配置就行。
|
||||||
|
|
||||||
## 模型文件
|
## 模型文件
|
||||||
|
|
||||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
@@ -273,21 +269,26 @@ LLM:
|
|||||||
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
||||||
|
|
||||||
```
|
```
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8002/xiaozhi/ota/
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||||
|
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
||||||
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
||||||
|
|
||||||
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
||||||
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`。
|
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8002/xiaozhi/ota/`。
|
||||||
|
|
||||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||||
|
|
||||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
|
|
||||||
以下是一些常见问题,供参考:
|
以下是一些常见问题,供参考:
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ xiaozhi-server
|
|||||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
|
||||||
`xiaozhi-server`中。
|
`xiaozhi-server`中。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/docker-compose_all.yml` 下载。
|
||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
##### 1.3.2 下载 config_from_api.yaml
|
##### 1.3.2 下载 config_from_api.yaml
|
||||||
@@ -53,6 +55,8 @@ xiaozhi-server
|
|||||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
|
||||||
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`。
|
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/config_from_api.yaml` 下载保存。
|
||||||
|
|
||||||
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -67,57 +71,37 @@ xiaozhi-server
|
|||||||
|
|
||||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
## 2. 安装Mysql和Redis
|
## 2. 备份数据
|
||||||
|
|
||||||
### 2.1 安装Mysql数据库
|
如果你之前已经成功运行智控台,如果上面保存有你的密钥信息,请先从智控台上拷贝重要数据下来。因为升级过程中,有可能会覆盖原来的数据。
|
||||||
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
```
|
|
||||||
|
|
||||||
如果还没有MySQL,你可以通过docker安装mysql
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 安装Redis
|
|
||||||
|
|
||||||
如果还没有Redis,你可以通过docker安装redis
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 配置docker-compose_all.yml文件文件
|
|
||||||
|
|
||||||
在`xiaozhi-server`目录,打开docker-compose_all.yml,
|
|
||||||
|
|
||||||
1、修改`SPRING_DATASOURCE_DRUID_URL`里,把`192.168.1.25`修改成部署mysql的电脑局域网ip,如果是你本地,你要查看一下你本机的局域网ip是什么
|
|
||||||
|
|
||||||
2、修改`SPRING_DATA_REDIS_HOST`里,把`192.168.1.25`修改成部署redis的电脑局域网ip,如果是你本地,你要查看一下你本机的局域网ip是什么
|
|
||||||
|
|
||||||
3、确认一下mysql的用户名`SPRING_DATASOURCE_DRUID_USERNAME`和密码`SPRING_DATASOURCE_DRUID_PASSWORD`
|
|
||||||
|
|
||||||
## 4. 运行程序
|
|
||||||
|
|
||||||
确认mysql和redis是否运行正常,输入
|
|
||||||
```
|
|
||||||
docker ps
|
|
||||||
```
|
|
||||||
如果你能看到`xiaozhi-esp32-server-redis`和`xiaozhi-esp32-server-db`信息,就可以继续往下,如果看不到,说明你的`mysql`和`redis`没有安装或没有启动。需要继续回到上面的教程,看看哪一步漏了。
|
|
||||||
|
|
||||||
```
|
|
||||||
CONTAINER ID IMAGE COMMAND CREATED TATUS PORTS NAMES
|
|
||||||
xxx redis "xx" xxx xxx 0.0.0.0:6379->6379/tcp xiaozhi-esp32-server-redis
|
|
||||||
xxx mysql:latest "xx" xxx xxx 0.0.0.0:3306->3306/tcp, 33060/tcp xiaozhi-esp32-server-db
|
|
||||||
```
|
|
||||||
|
|
||||||
|
## 3. 清除历史版本镜像和容器
|
||||||
接下来打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
接下来打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose_all.yml up -d
|
docker compose -f docker-compose_all.yml down
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server
|
||||||
|
docker rm xiaozhi-esp32-server
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-db
|
||||||
|
docker rm xiaozhi-esp32-server-db
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-redis
|
||||||
|
docker rm xiaozhi-esp32-server-redis
|
||||||
|
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 运行程序
|
||||||
|
执行以下命令启动新版本容器
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose -f docker-compose_all.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
执行完后,再执行以下命令,查看日志信息。
|
执行完后,再执行以下命令,查看日志信息。
|
||||||
@@ -138,11 +122,11 @@ http://localhost:8002/xiaozhi/doc.html
|
|||||||
|
|
||||||
这时,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8002 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
这时,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8002 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
||||||
|
|
||||||
接下来要做两件重要的事情:
|
接下来要做三件重要的事情:
|
||||||
|
|
||||||
### 第一件重要的事情
|
### 第一件重要的事情
|
||||||
|
|
||||||
使用超级管理员账号,登录系统,在顶部菜单找到`参数管理`,找到列表中第三条数据,参数编码是`server.secret`,复制它到`参数值`。
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到列表中第一条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||||
|
|
||||||
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||||
|
|
||||||
@@ -150,17 +134,21 @@ http://localhost:8002/xiaozhi/doc.html
|
|||||||
|
|
||||||
```
|
```
|
||||||
manager-api:
|
manager-api:
|
||||||
url: http://127.0.0.1:8002/xiaozhi
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
secret: 你的server.secret值
|
secret: 你的server.secret值
|
||||||
```
|
```
|
||||||
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||||
|
|
||||||
2、把`url`里的`127.0.0.1`改成你电脑局域网内的ip
|
2、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
|
3、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
|
4、因为你是docker部署,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
类似这样的效果
|
类似这样的效果
|
||||||
```
|
```
|
||||||
manager-api:
|
manager-api:
|
||||||
url: http://192.168.1.25:8002/xiaozhi
|
url: http://xiaozhi-esp32-server-web:8002/xiaozhi
|
||||||
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -168,7 +156,7 @@ manager-api:
|
|||||||
|
|
||||||
### 第二件重要的事情
|
### 第二件重要的事情
|
||||||
|
|
||||||
使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
## 5.重启xiaozhi-esp32-server
|
## 5.重启xiaozhi-esp32-server
|
||||||
@@ -181,7 +169,7 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
如果你能看到,类似以下日志,则是Server启动成功的标志。
|
如果你能看到,类似以下日志,则是Server启动成功的标志。
|
||||||
|
|
||||||
```
|
```
|
||||||
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地址是 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 - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||||
@@ -199,26 +187,18 @@ Websocket接口:
|
|||||||
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||||
```
|
```
|
||||||
|
|
||||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
### 第三件重要的事情
|
||||||
|
|
||||||
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`。
|
||||||
|
|
||||||
## 6. 版本升级操作
|
使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`。
|
||||||
|
|
||||||
如果后期想升级版本,先备份你的模型密钥。
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
进入`xiaozhi-server`目录
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
5.1、执行以下命令
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose down
|
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
|
||||||
```
|
|
||||||
|
|
||||||
5.2、删掉`docker-compose_all.yaml`文件
|
|
||||||
|
|
||||||
5.3、重新按1.1开始部署
|
|
||||||
|
|
||||||
# 方式二:本地源码运行全模块
|
# 方式二:本地源码运行全模块
|
||||||
|
|
||||||
@@ -233,7 +213,7 @@ CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unico
|
|||||||
如果还没有MySQL,你可以通过docker安装mysql
|
如果还没有MySQL,你可以通过docker安装mysql
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -e TZ=Asia/Shanghai -d mysql:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2.安装redis
|
## 2.安装redis
|
||||||
@@ -313,13 +293,13 @@ npm run serve
|
|||||||
运行成功后,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8001 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
运行成功后,你需要使用浏览器,打开`智控台`,链接:http://127.0.0.1:8001 ,注册第一个用户。第一个用户即是超级管理员,以后的用户都是普通用户。普通用户只能绑定设备和配置智能体;超级管理员可以进行模型管理、用户管理、参数配置等功能。
|
||||||
|
|
||||||
|
|
||||||
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
重要:注册成功后,使用超级管理员账号,登录系统,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
重要:注册成功后,使用超级管理员账号,登录智控台,在顶部菜单找到`模型配置`,然后在左侧栏点击`大语言模型`,找到第一条数据`智谱AI`,点击`修改`按钮,
|
||||||
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
弹出修改框后,将你注册到的`智谱AI`的密钥填写到`API密钥`中。然后点击保存。
|
||||||
|
|
||||||
## 5.安装Python环境
|
## 5.安装Python环境
|
||||||
@@ -384,10 +364,13 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
## 8.配置项目文件
|
## 8.配置项目文件
|
||||||
|
|
||||||
使用超级管理员账号,登录智控台 http://127.0.0.1:8001 ,在顶部菜单找到`参数管理`,找到列表中第三条数据,参数编码是`server.secret`,复制它到`参数值`。
|
使用超级管理员账号,登录智控台 ,在顶部菜单找到`参数管理`,找到列表中第一条数据,参数编码是`server.secret`,复制它到`参数值`。
|
||||||
|
|
||||||
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||||
|
|
||||||
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
|
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config_from_api.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||||
|
|
||||||
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -422,7 +405,7 @@ python app.py
|
|||||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
由于你是全模块部署,因此你有两个重要的接口需要写入到esp32中。
|
由于你是全模块部署,因此你有两个重要的接口。
|
||||||
|
|
||||||
OTA接口:
|
OTA接口:
|
||||||
```
|
```
|
||||||
@@ -434,7 +417,18 @@ Websocket接口:
|
|||||||
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
||||||
```
|
```
|
||||||
|
|
||||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
请你务必把以上两个接口地址写入到智控台中:他们将会影响websocket地址发放和自动升级功能。
|
||||||
|
|
||||||
|
1、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到参数编码是`server.websocket`,输入你的`Websocket接口`。
|
||||||
|
|
||||||
|
2、使用超级管理员账号,登录智控台,在顶部菜单找到`参数管理`,找到数编码是`server.ota`,输入你的`OTA接口`。
|
||||||
|
|
||||||
|
|
||||||
|
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||||
|
|
||||||
|
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||||
|
|
||||||
# 常见问题
|
# 常见问题
|
||||||
|
|
||||||
|
|||||||
@@ -102,48 +102,10 @@ VAD:
|
|||||||
|
|
||||||
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||||
|
|
||||||
本项目,支持以工具调用的方式控制HomeAssistant设备
|
参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md)
|
||||||
|
|
||||||
1、首先选择一款支持function call支持的LLM,例如`ChatGLMLLM`。
|
|
||||||
|
|
||||||
2、在配置文件中,将 `selected_module.Intent` 设置为 `function_call`。
|
|
||||||
|
|
||||||
3、登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
|
||||||
|
|
||||||
在配置文件中,配置好你的home assistant的`devices`(被控制的设备)和`api_key`和`base_url`等信息。例如:
|
|
||||||
|
|
||||||
``` yaml
|
|
||||||
plugins
|
|
||||||
home_assistant:
|
|
||||||
devices:
|
|
||||||
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
|
|
||||||
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
|
|
||||||
base_url: http://你的homeassistant地址:8123
|
|
||||||
api_key: 你的home assistant api访问令牌
|
|
||||||
```
|
|
||||||
|
|
||||||
最后,允许function_call 插件在配置文件中启用`hass_get_state`(必须)、`hass_set_state`(必须)、`hass_play_music`(不想用ha听音乐可以不启动),例如:
|
|
||||||
|
|
||||||
``` yaml
|
|
||||||
Intent:
|
|
||||||
...
|
|
||||||
function_call:
|
|
||||||
type: nointent
|
|
||||||
functions:
|
|
||||||
- change_role
|
|
||||||
- get_weather
|
|
||||||
- get_news
|
|
||||||
- hass_get_state
|
|
||||||
- hass_set_state
|
|
||||||
- hass_play_music
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8、更多问题,可联系我们反馈 💬
|
### 8、更多问题,可联系我们反馈 💬
|
||||||
|
|
||||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
|
||||||
|
|
||||||
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
|
也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
|
||||||
|
|
||||||
- 硬件烧录QQ群:适用于硬件烧录问题
|
|
||||||
- 开源服务端交流群:适用于服务端问题
|
|
||||||
- 产品建议联系人:适用于产品功能、产品设计等建议
|
|
||||||
@@ -1,26 +1,53 @@
|
|||||||
server {
|
user root;
|
||||||
listen 8002;
|
worker_processes 4;
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
# 静态资源服务(Vue项目)
|
events {
|
||||||
location / {
|
worker_connections 1024;
|
||||||
root /usr/share/nginx/html;
|
}
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API反向代理(Java项目)
|
http {
|
||||||
location /xiaozhi/ {
|
include mime.types;
|
||||||
proxy_pass http://127.0.0.1:8003;
|
default_type application/octet-stream;
|
||||||
proxy_set_header Host $host;
|
sendfile on;
|
||||||
proxy_cookie_path /api/ /;
|
keepalive_timeout 300;
|
||||||
proxy_set_header Referer $http_referer;
|
client_header_timeout 180s;
|
||||||
proxy_set_header Cookie $http_cookie;
|
client_body_timeout 180s;
|
||||||
|
client_max_body_size 1024M;
|
||||||
|
|
||||||
proxy_connect_timeout 10;
|
gzip on;
|
||||||
proxy_send_timeout 10;
|
gzip_buffers 32 4K;
|
||||||
proxy_read_timeout 10;
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 100;
|
||||||
|
gzip_types application/javascript text/css text/xml image/jpeg image/gif image/png;
|
||||||
|
gzip_disable "MSIE [1-6]\.";
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
server {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
# 无域名访问,就用localhost
|
||||||
|
server_name localhost;
|
||||||
|
# 80端口
|
||||||
|
listen 8002;
|
||||||
|
|
||||||
|
# 转发到编译后到web目录
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 转发到manager-api
|
||||||
|
location /xiaozhi/ {
|
||||||
|
proxy_pass http://127.0.0.1:8003;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cookie_path /manager/ /;
|
||||||
|
proxy_set_header Referer $http_referer;
|
||||||
|
proxy_set_header Cookie $http_cookie;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10;
|
||||||
|
proxy_send_timeout 10;
|
||||||
|
proxy_read_timeout 10;
|
||||||
|
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,67 +1,79 @@
|
|||||||
# 编译esp32固件
|
# esp32固件编译
|
||||||
|
|
||||||
## 第1步 配置环境
|
## 第1步 准备你的ota地址
|
||||||
|
|
||||||
|
如果你,使用的是本项目0.3.12版本,不管是简单Server部署还是全模块部署,都会有ota地址。
|
||||||
|
|
||||||
|
由于简单Server部署和全模块部署的OTA地址设置方式不一样,请你选择下面的具体方式:
|
||||||
|
|
||||||
|
### 如果你用的是简单Server部署
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
http://192.168.1.25:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
|
||||||
|
|
||||||
|
你可以使用项目自带的`test_page.html`测试一下,是否能连上ota页面输出的websocket地址。
|
||||||
|
|
||||||
|
如果访问不到,你需要到配置文件`.config.yaml`里修改`server.websocket`的地址,重启后再重新测试,直到`test_page.html`能正常访问。
|
||||||
|
|
||||||
|
成功后,请往下进行第2步
|
||||||
|
|
||||||
|
### 如果你用的是全模块部署
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
http://192.168.1.25:8002/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下进行2步。
|
||||||
|
|
||||||
|
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||||
|
|
||||||
|
- 1、使用超级管理员登录智控台
|
||||||
|
|
||||||
|
- 2、顶部菜单点击`参数管理`
|
||||||
|
|
||||||
|
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://192.168.1.25:8000/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
|
||||||
|
|
||||||
|
## 第2步 配置环境
|
||||||
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||||
|
|
||||||
## 第2步 打开配置文件
|
## 第3步 打开配置文件
|
||||||
配置好编译环境后,下载虾哥[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)项目源码,进入虾哥项目,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
|
||||||
|
|
||||||
## 第3步 修改WEBSOCKET地址
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32)。
|
||||||
找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
|
|
||||||
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
|
|
||||||
|
|
||||||
修改前:
|
下载后,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
||||||
|
|
||||||
```
|
|
||||||
config WEBSOCKET_URL
|
|
||||||
depends on CONNECTION_TYPE_WEBSOCKET
|
|
||||||
string "Websocket URL"
|
|
||||||
default "wss://api.tenclass.net/xiaozhi/v1/"
|
|
||||||
help
|
|
||||||
Communication with the server through websocket after wake up.
|
|
||||||
```
|
|
||||||
|
|
||||||
修改后(示例):
|
|
||||||
|
|
||||||
```
|
|
||||||
config WEBSOCKET_URL
|
|
||||||
depends on CONNECTION_TYPE_WEBSOCKET
|
|
||||||
string "Websocket URL"
|
|
||||||
default "ws://192.168.1.25:8000/xiaozhi/v1/"
|
|
||||||
help
|
|
||||||
Communication with the server through websocket after wake up.
|
|
||||||
```
|
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
|
||||||
|
|
||||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
|
||||||
|
|
||||||
## 第4步 修改OTA地址
|
## 第4步 修改OTA地址
|
||||||
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
|
|
||||||
|
|
||||||
找到`OTA_VERSION_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
找到`OTA_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
||||||
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8000/xiaozhi/ota/`,就把内容改成这个。
|
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
|
||||||
|
|
||||||
修改前:
|
修改前:
|
||||||
```
|
```
|
||||||
config OTA_VERSION_URL
|
config OTA_URL
|
||||||
string "OTA Version URL"
|
string "Default OTA URL"
|
||||||
default "https://api.tenclass.net/xiaozhi/ota/"
|
default "https://api.tenclass.net/xiaozhi/ota/"
|
||||||
help
|
help
|
||||||
The application will access this URL to check for updates.
|
The application will access this URL to check for new firmwares and server address.
|
||||||
```
|
```
|
||||||
修改后:
|
修改后:
|
||||||
```
|
```
|
||||||
config OTA_VERSION_URL
|
config OTA_URL
|
||||||
string "OTA Version URL"
|
string "Default OTA URL"
|
||||||
default "http://192.168.1.25:8000/xiaozhi/ota/"
|
default "http://192.168.1.25:8002/xiaozhi/ota/"
|
||||||
help
|
help
|
||||||
The application will access this URL to check for updates.
|
The application will access this URL to check for new firmwares and server address.
|
||||||
```
|
```
|
||||||
|
|
||||||
## 第5步 设置编译参数
|
## 第4步 设置编译参数
|
||||||
|
|
||||||
设置编译参数
|
设置编译参数
|
||||||
|
|
||||||
@@ -74,40 +86,29 @@ idf.py set-target esp32s3
|
|||||||
idf.py menuconfig
|
idf.py menuconfig
|
||||||
```
|
```
|
||||||
|
|
||||||

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

|
## 第5步 编译固件
|
||||||
|
|
||||||
## 第6步 编译固件
|
|
||||||
|
|
||||||
```
|
```
|
||||||
idf.py build
|
idf.py build
|
||||||
```
|
```
|
||||||
|
|
||||||
如果是vscode安装的idf可以使用`F1`或者`ctrl+shift+p`,输入idf然后直接选择进行编译
|
## 第6步 打包bin固件
|
||||||
|
|
||||||
还可以直接进行烧录不用接下来的操作
|
|
||||||
|
|
||||||
<img src="./images/vscode_idf.png" width="500px"/>
|
|
||||||
|
|
||||||
## 第7步 打包bin固件
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd scripts
|
cd scripts
|
||||||
python release.py
|
python release.py
|
||||||
```
|
```
|
||||||
|
|
||||||
编译成功后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
上面的打包命令执行完成后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
||||||
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
|
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
|
||||||
|
|
||||||
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
|
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
|
||||||
,对你没有太大影响,请继续。
|
,对你没有太大影响,请继续。
|
||||||
|
|
||||||
## 第8步 烧录固件
|
## 第7步 烧录固件
|
||||||
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
|
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 基于虾哥编译好的固件配置自定义服务器
|
||||||
|
|
||||||
|
## 第1步 确认版本
|
||||||
|
烧录虾哥已经编译好的1.6.1版本固件
|
||||||
|
|
||||||
|
## 第2步 准备你的ota地址
|
||||||
|
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
||||||
|
|
||||||
|
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||||
|
```
|
||||||
|
https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
|
```
|
||||||
|
|
||||||
|
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下。
|
||||||
|
|
||||||
|
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||||
|
|
||||||
|
- 1、使用超级管理员登录智控台
|
||||||
|
|
||||||
|
- 2、顶部菜单点击`参数管理`
|
||||||
|
|
||||||
|
- 3、在列表中找到`server.websocket`项目,输入你的`Websocket`地址。例如我的就是
|
||||||
|
|
||||||
|
```
|
||||||
|
wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
配置完后,再使用浏览器刷新你的ota接口地址,看看是不是正常了。如果还不正常就,就再次确认一下Websocket是否正常启动,是否配置了Websocket地址。
|
||||||
|
|
||||||
|
## 第3步 进入配网模式
|
||||||
|
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
|
||||||
|
|
||||||
|
## 第4步 唤醒小智,查看日志输出
|
||||||
|
|
||||||
|
唤醒小智,看看日志是不是正常输出。
|
||||||
|
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||||
|
|
||||||
|
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||||
|
|
||||||
|
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||||
|
|
||||||
|
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||||
|
|
||||||
|
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||||
|
|
||||||
|
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||||
|
|
||||||
|
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# ESP32设备与HomeAssistant集成指南
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
本文档将指导您如何将ESP32设备与HomeAssistant进行集成。
|
||||||
|
|
||||||
|
## 前提条件
|
||||||
|
|
||||||
|
- 已安装并配置好`HomeAssistant`
|
||||||
|
- 已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`
|
||||||
|
- `xiaozhi-esp32-server智控台`能正常使用
|
||||||
|
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
|
||||||
|
|
||||||
|
## 网络环境
|
||||||
|
|
||||||
|
1、我的`HomeAssistant`部署在下面,版本是`2025.3.4`
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8123
|
||||||
|
```
|
||||||
|
|
||||||
|
2、我的`xiaozhi-esp32-server智控台`和`HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8002
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 配置步骤
|
||||||
|
|
||||||
|
### 1. 登录`HomeAssistant`整理需要控制的设备清单
|
||||||
|
|
||||||
|
登录`HomeAssistant`,点击`左下角的设置`,然后进入`设备与服务`,再点击顶部的`实体`。
|
||||||
|
|
||||||
|
然后在实体中搜索你相关控制的开关,结果出来后,在列表中,点击其中一个结果,这是会出现一个开关的界面。
|
||||||
|
|
||||||
|
在开关的界面,我们尝试点击开关,看看是开发会随着我们的点击开/关。如果能操作,说明是正常联网的。
|
||||||
|
|
||||||
|
接着在开关面板找到设置按钮,点击后,可以查看这个开关的`实体标识符`。
|
||||||
|
|
||||||
|
我们打开一个记事本,按照这样格式整理一条数据:
|
||||||
|
|
||||||
|
位置+英文逗号+设备名称+英文逗号+`实体标识符`+英文分号
|
||||||
|
|
||||||
|
例如,我在公司,我有一个玩具灯,他的标识符是switch.cuco_cn_460494544_cp1_on_p_2_1,那么就这个写这一条数据
|
||||||
|
|
||||||
|
```
|
||||||
|
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
|
||||||
|
```
|
||||||
|
|
||||||
|
当然最后我可能要操作两个灯,我的最终的结果是:
|
||||||
|
|
||||||
|
```
|
||||||
|
公司,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;
|
||||||
|
公司,台灯,switch.iot_cn_831898993_socn1_on_p_2_1;
|
||||||
|
```
|
||||||
|
|
||||||
|
这段字符,我们成为“设备清单字符”需要保存好,等一下有用。
|
||||||
|
|
||||||
|
|
||||||
|
### 2. 登录`HomeAssistant`拿到开发密钥
|
||||||
|
|
||||||
|
登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
||||||
|
|
||||||
|
|
||||||
|
### 3. 登录`智控台`
|
||||||
|
|
||||||
|
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.devices`,把刚才整理的设备清单字符粘贴进去。
|
||||||
|
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.base_url`,把你部署的`HomeAssistant`接口地址粘贴进去,我粘贴进去的地址是这样的
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.4.7:8123
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去
|
||||||
|
|
||||||
|
|
||||||
|
### 4. 设置`意图识别`函数
|
||||||
|
|
||||||
|
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
|
||||||
|
|
||||||
|
然后在弹框中,在原来的基础上追加两个函数:“hass_get_state”和“hass_set_state”
|
||||||
|
|
||||||
|
修改前
|
||||||
|
```
|
||||||
|
change_role;get_weather;get_news;play_music
|
||||||
|
```
|
||||||
|
|
||||||
|
修改后
|
||||||
|
|
||||||
|
```
|
||||||
|
change_role;get_weather;get_news;play_music;hass_get_state;hass_set_state
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 手动重启xiaozhi-server
|
||||||
|
|
||||||
|
重启xiaozhi-server程序
|
||||||
|
|
||||||
|
|
||||||
|
### 5. 确认角色配置是否设置了函数意图识别
|
||||||
|
|
||||||
|
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
|
||||||
|
|
||||||
|
确认意图识别(Intent),是否选择“函数调用意图识别”
|
||||||
|
|
||||||
|
|
||||||
|
### 6. 唤醒设别进行控制
|
||||||
|
|
||||||
|
尝试和esp32说,“打开XXX灯”
|
||||||
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 149 KiB |
@@ -197,6 +197,10 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<!-- 阿里云maven仓库 -->
|
<!-- 阿里云maven仓库 -->
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package xiaozhi.common.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RestTemplate配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate() {
|
||||||
|
return new RestTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,26 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_SECRET = "server.secret";
|
String SERVER_SECRET = "server.secret";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket地址
|
||||||
|
*/
|
||||||
|
String SERVER_WEBSOCKET = "server.websocket";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ota地址
|
||||||
|
*/
|
||||||
|
String SERVER_OTA = "server.ota";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许用户注册
|
||||||
|
*/
|
||||||
|
String SERVER_ALLOW_USER_REGISTER = "server.allow_user_register";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发六位验证码时显示的控制面板地址
|
||||||
|
*/
|
||||||
|
String SERVER_FRONTED_URL = "server.fronted_url";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径分割符
|
* 路径分割符
|
||||||
*/
|
*/
|
||||||
@@ -153,4 +173,9 @@ public interface Constant {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号
|
||||||
|
*/
|
||||||
|
public static final String VERSION = "0.4.1";
|
||||||
}
|
}
|
||||||
@@ -52,4 +52,7 @@ public interface ErrorCode {
|
|||||||
int PARAM_BOOLEAN_INVALID = 10038;
|
int PARAM_BOOLEAN_INVALID = 10038;
|
||||||
int PARAM_ARRAY_INVALID = 10039;
|
int PARAM_ARRAY_INVALID = 10039;
|
||||||
int PARAM_JSON_INVALID = 10040;
|
int PARAM_JSON_INVALID = 10040;
|
||||||
|
|
||||||
|
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||||
|
int OTA_DEVICE_NEED_BIND = 10042;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ public class RedisKeys {
|
|||||||
return "agent:device:count:" + id;
|
return "agent:device:count:" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体最后连接时间缓存key
|
||||||
|
*/
|
||||||
|
public static String getAgentDeviceLastConnectedAtById(String id) {
|
||||||
|
return "agent:device:lastConnected:" + id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统配置缓存key
|
* 获取系统配置缓存key
|
||||||
*/
|
*/
|
||||||
@@ -75,4 +82,39 @@ public class RedisKeys {
|
|||||||
public static String getTimbreDetailsKey(String id) {
|
public static String getTimbreDetailsKey(String id) {
|
||||||
return "timbre:details:" + id;
|
return "timbre:details:" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取版本号Key
|
||||||
|
*/
|
||||||
|
public static String getVersionKey() {
|
||||||
|
return "sys:version";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件ID的Key
|
||||||
|
*/
|
||||||
|
public static String getOtaIdKey(String uuid) {
|
||||||
|
return "ota:id:" + uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件下载次数的Key
|
||||||
|
*/
|
||||||
|
public static String getOtaDownloadCountKey(String uuid) {
|
||||||
|
return "ota:download:count:" + uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典数据的缓存key
|
||||||
|
*/
|
||||||
|
public static String getDictDataByTypeKey(String dictType) {
|
||||||
|
return "sys:dict:data:" + dictType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体音频ID的缓存key
|
||||||
|
*/
|
||||||
|
public static String getAgentAudioIdKey(String uuid) {
|
||||||
|
return "agent:audio:id:" + uuid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package xiaozhi.common.redis;
|
package xiaozhi.common.redis;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.springframework.data.redis.core.HashOperations;
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -27,7 +30,7 @@ public class RedisUtils {
|
|||||||
/**
|
/**
|
||||||
* 过期时长为1小时,单位:秒
|
* 过期时长为1小时,单位:秒
|
||||||
*/
|
*/
|
||||||
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
|
public final static long HOUR_ONE_EXPIRE = (long) 60 * 60;
|
||||||
/**
|
/**
|
||||||
* 过期时长为6小时,单位:秒
|
* 过期时长为6小时,单位:秒
|
||||||
*/
|
*/
|
||||||
@@ -124,4 +127,24 @@ public class RedisUtils {
|
|||||||
public Object rightPop(String key) {
|
public Object rightPop(String key) {
|
||||||
return redisTemplate.opsForList().rightPop(key);
|
return redisTemplate.opsForList().rightPop(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有 Redis 数据库中的所有键
|
||||||
|
*/
|
||||||
|
public void emptyAll() {
|
||||||
|
// Lua 脚本 FLUSHALL是redis清空所有库的命令
|
||||||
|
String luaScript ="redis.call('FLUSHALL')";
|
||||||
|
|
||||||
|
// 创建 DefaultRedisScript 对象
|
||||||
|
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
|
||||||
|
redisScript.setScriptText(luaScript); // 设置 Lua 脚本内容
|
||||||
|
redisScript.setResultType(Void.class); // 设置返回值类型
|
||||||
|
|
||||||
|
// 执行 Lua 脚本
|
||||||
|
List<String> keys = Collections.emptyList(); // 如果脚本不依赖 key,可以传入空列表
|
||||||
|
redisTemplate.execute(redisScript, keys);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -128,12 +128,10 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
|||||||
return SqlHelper.retBool(result);
|
return SqlHelper.retBool(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected Class<M> currentMapperClass() {
|
protected Class<M> currentMapperClass() {
|
||||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
@Override
|
||||||
public Class<T> currentModelClass() {
|
public Class<T> currentModelClass() {
|
||||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils;
|
|||||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
||||||
implements CrudService<T, D> {
|
implements CrudService<T, D> {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected Class<D> currentDtoClass() {
|
protected Class<D> currentDtoClass() {
|
||||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package xiaozhi.modules.agent.controller;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||||
|
|
||||||
|
@Tag(name = "智能体聊天历史管理")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/agent/chat-history")
|
||||||
|
public class AgentChatHistoryController {
|
||||||
|
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小智服务聊天上报请求
|
||||||
|
* <p>
|
||||||
|
* 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。
|
||||||
|
*
|
||||||
|
* @param request 包含上传文件及相关信息的请求对象
|
||||||
|
*/
|
||||||
|
@Operation(summary = "小智服务聊天上报请求")
|
||||||
|
@PostMapping("/report")
|
||||||
|
public Result<Boolean> uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) {
|
||||||
|
Boolean result = agentChatHistoryBizService.report(request);
|
||||||
|
return new Result<Boolean>().ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,13 @@ package xiaozhi.modules.agent.controller;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@@ -25,14 +30,20 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
@@ -46,6 +57,9 @@ public class AgentController {
|
|||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final AgentTemplateService agentTemplateService;
|
private final AgentTemplateService agentTemplateService;
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@Operation(summary = "获取用户智能体列表")
|
@Operation(summary = "获取用户智能体列表")
|
||||||
@@ -80,7 +94,7 @@ public class AgentController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "创建智能体")
|
@Operation(summary = "创建智能体")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||||
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
||||||
|
|
||||||
// 获取默认模板
|
// 获取默认模板
|
||||||
@@ -108,7 +122,7 @@ public class AgentController {
|
|||||||
// ID、智能体编码和排序会在Service层自动生成
|
// ID、智能体编码和排序会在Service层自动生成
|
||||||
agentService.insert(entity);
|
agentService.insert(entity);
|
||||||
|
|
||||||
return new Result<>();
|
return new Result<String>().ok(entity.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@@ -178,6 +192,8 @@ public class AgentController {
|
|||||||
public Result<Void> delete(@PathVariable String id) {
|
public Result<Void> delete(@PathVariable String id) {
|
||||||
// 先删除关联的设备
|
// 先删除关联的设备
|
||||||
deviceService.deleteByAgentId(id);
|
deviceService.deleteByAgentId(id);
|
||||||
|
// 删除关联的聊天记录
|
||||||
|
agentChatHistoryService.deleteByAgentId(id);
|
||||||
// 再删除智能体
|
// 再删除智能体
|
||||||
agentService.deleteById(id);
|
agentService.deleteById(id);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
@@ -192,4 +208,71 @@ public class AgentController {
|
|||||||
return new Result<List<AgentTemplateEntity>>().ok(list);
|
return new Result<List<AgentTemplateEntity>>().ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/sessions")
|
||||||
|
@Operation(summary = "获取智能体会话列表")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||||
|
})
|
||||||
|
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
|
||||||
|
@PathVariable("id") String id,
|
||||||
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
params.put("agentId", id);
|
||||||
|
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
|
||||||
|
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/chat-history/{sessionId}")
|
||||||
|
@Operation(summary = "获取智能体聊天记录")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<AgentChatHistoryDTO>> getAgentChatHistory(
|
||||||
|
@PathVariable("id") String id,
|
||||||
|
@PathVariable("sessionId") String sessionId) {
|
||||||
|
// 获取当前用户
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!agentService.checkAgentPermission(id, user.getId())) {
|
||||||
|
return new Result<List<AgentChatHistoryDTO>>().error("没有权限查看该智能体的聊天记录");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询聊天记录
|
||||||
|
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||||
|
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/audio/{audioId}")
|
||||||
|
@Operation(summary = "获取音频下载ID")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
|
||||||
|
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||||
|
if (audioData == null) {
|
||||||
|
return new Result<String>().error("音频不存在");
|
||||||
|
}
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId);
|
||||||
|
return new Result<String>().ok(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/play/{uuid}")
|
||||||
|
@Operation(summary = "播放音频")
|
||||||
|
public ResponseEntity<byte[]> playAudio(@PathVariable("uuid") String uuid) {
|
||||||
|
|
||||||
|
String audioId = (String) redisUtils.get(RedisKeys.getAgentAudioIdKey(uuid));
|
||||||
|
if (StringUtils.isBlank(audioId)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||||
|
if (audioData == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
redisUtils.delete(RedisKeys.getAgentAudioIdKey(uuid));
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"play.wav\"")
|
||||||
|
.body(audioData);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
@@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao<AgentEntity> {
|
|||||||
* @return 设备数量
|
* @return 设备数量
|
||||||
*/
|
*/
|
||||||
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||||
|
*
|
||||||
|
* @param macAddress 设备MAC地址
|
||||||
|
* @return 默认智能体信息
|
||||||
|
*/
|
||||||
|
@Select(" SELECT a.* FROM ai_device d " +
|
||||||
|
" LEFT JOIN ai_agent a ON d.agent_id = a.id " +
|
||||||
|
" WHERE d.mac_address = #{macAddress} " +
|
||||||
|
" ORDER BY d.id DESC LIMIT 1")
|
||||||
|
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AiAgentChatAudioDao extends BaseMapper<AgentChatAudioEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除音频
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteAudioByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除聊天历史记录
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteHistoryByAgentId(String agentId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体聊天记录")
|
||||||
|
public class AgentChatHistoryDTO {
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "消息类型: 1-用户, 2-智能体")
|
||||||
|
private Byte chatType;
|
||||||
|
|
||||||
|
@Schema(description = "聊天内容")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "音频ID")
|
||||||
|
private String audioId;
|
||||||
|
|
||||||
|
@Schema(description = "MAC地址")
|
||||||
|
private String macAddress;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小智设备聊天上报请求
|
||||||
|
*
|
||||||
|
* @author Haotian
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "小智设备聊天上报请求")
|
||||||
|
public class AgentChatHistoryReportDTO {
|
||||||
|
@Schema(description = "MAC地址", example = "00:11:22:33:44:55")
|
||||||
|
@NotBlank
|
||||||
|
private String macAddress;
|
||||||
|
@Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a")
|
||||||
|
@NotBlank
|
||||||
|
private String sessionId;
|
||||||
|
@Schema(description = "消息类型: 1-用户, 2-智能体", example = "1")
|
||||||
|
@NotNull
|
||||||
|
private Byte chatType;
|
||||||
|
@Schema(description = "聊天内容", example = "你好呀")
|
||||||
|
@NotBlank
|
||||||
|
private String content;
|
||||||
|
@Schema(description = "base64编码的opus音频数据", example = "")
|
||||||
|
private String audioBase64;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体会话列表DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AgentChatSessionDTO {
|
||||||
|
/**
|
||||||
|
* 会话ID
|
||||||
|
*/
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天条数
|
||||||
|
*/
|
||||||
|
private Integer chatCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("ai_agent_chat_audio")
|
||||||
|
public class AgentChatAudioEntity {
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频opus数据
|
||||||
|
*/
|
||||||
|
private byte[] audio;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@TableName(value = "ai_agent_chat_history")
|
||||||
|
public class AgentChatHistoryEntity {
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAC地址
|
||||||
|
*/
|
||||||
|
@TableField(value = "mac_address")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体id
|
||||||
|
*/
|
||||||
|
@TableField(value = "agent_id")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话ID
|
||||||
|
*/
|
||||||
|
@TableField(value = "session_id")
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型: 1-用户, 2-智能体
|
||||||
|
*/
|
||||||
|
@TableField(value = "chat_type")
|
||||||
|
private Byte chatType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天内容
|
||||||
|
*/
|
||||||
|
@TableField(value = "content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频base64数据
|
||||||
|
*/
|
||||||
|
@TableField(value = "audio_id")
|
||||||
|
private String audioId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_at")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_at")
|
||||||
|
private Date updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表处理service
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
|
||||||
|
/**
|
||||||
|
* 保存音频数据
|
||||||
|
*
|
||||||
|
* @param audioData 音频数据
|
||||||
|
* @return 音频ID
|
||||||
|
*/
|
||||||
|
String saveAudio(byte[] audioData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音频数据
|
||||||
|
*
|
||||||
|
* @param audioId 音频ID
|
||||||
|
* @return 音频数据
|
||||||
|
*/
|
||||||
|
byte[] getAudio(String audioId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表处理service
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID获取会话列表
|
||||||
|
*
|
||||||
|
* @param params 查询参数,包含agentId、page、limit
|
||||||
|
* @return 分页的会话列表
|
||||||
|
*/
|
||||||
|
PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据会话ID获取聊天记录列表
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
* @return 聊天记录列表
|
||||||
|
*/
|
||||||
|
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除聊天记录
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteByAgentId(String agentId);
|
||||||
|
}
|
||||||
@@ -8,38 +8,75 @@ import xiaozhi.common.service.BaseService;
|
|||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体表处理service
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
public interface AgentService extends BaseService<AgentEntity> {
|
public interface AgentService extends BaseService<AgentEntity> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员获取所有智能体列表(分页)
|
* 获取管理员智能体列表
|
||||||
|
*
|
||||||
|
* @param params 查询参数
|
||||||
|
* @return 分页数据
|
||||||
*/
|
*/
|
||||||
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取智能体详情
|
* 根据ID获取智能体
|
||||||
|
*
|
||||||
|
* @param id 智能体ID
|
||||||
|
* @return 智能体实体
|
||||||
*/
|
*/
|
||||||
AgentEntity getAgentById(String id);
|
AgentEntity getAgentById(String id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除这个用户的所有
|
* 插入智能体
|
||||||
*
|
*
|
||||||
* @param userId
|
* @param entity 智能体实体
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean insert(AgentEntity entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID删除智能体
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
*/
|
*/
|
||||||
void deleteAgentByUserId(Long userId);
|
void deleteAgentByUserId(Long userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户智能体列表
|
* 获取用户智能体列表
|
||||||
*
|
*
|
||||||
* @param userId
|
* @param userId 用户ID
|
||||||
* @return
|
* @return 智能体列表
|
||||||
*/
|
*/
|
||||||
List<AgentDTO> getUserAgents(Long userId);
|
List<AgentDTO> getUserAgents(Long userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取智能体的设备数量
|
* 根据智能体ID获取设备数量
|
||||||
*
|
*
|
||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
* @return 设备数量
|
* @return 设备数量
|
||||||
*/
|
*/
|
||||||
Integer getDeviceCountByAgentId(String agentId);
|
Integer getDeviceCountByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||||
|
*
|
||||||
|
* @param macAddress 设备MAC地址
|
||||||
|
* @return 默认智能体信息,不存在时返回null
|
||||||
|
*/
|
||||||
|
AgentEntity getDefaultAgentByMacAddress(String macAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否有权限访问智能体
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 是否有权限
|
||||||
|
*/
|
||||||
|
boolean checkAgentPermission(String agentId, Long userId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.agent.service.biz;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天历史业务逻辑层
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatHistoryBizService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天上报方法
|
||||||
|
*
|
||||||
|
* @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象
|
||||||
|
* 例如:设备MAC地址、文件类型、内容等
|
||||||
|
* @return 上传结果,true表示成功,false表示失败
|
||||||
|
*/
|
||||||
|
Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package xiaozhi.modules.agent.service.biz.impl;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatHistoryBizService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService {
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理聊天记录上报,包括文件上传和相关信息记录
|
||||||
|
*
|
||||||
|
* @param report 包含聊天上报所需信息的输入对象
|
||||||
|
* @return 上传结果,true表示成功,false表示失败
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean report(AgentChatHistoryReportDTO report) {
|
||||||
|
String macAddress = report.getMacAddress();
|
||||||
|
Byte chatType = report.getChatType();
|
||||||
|
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
||||||
|
|
||||||
|
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||||
|
String audioId = null;
|
||||||
|
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
||||||
|
try {
|
||||||
|
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
||||||
|
audioId = agentChatAudioService.saveAudio(audioData);
|
||||||
|
log.info("音频数据保存成功,audioId={}", audioId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("音频数据保存失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 组装上报数据
|
||||||
|
// 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||||
|
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||||
|
if (agentEntity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String agentId = agentEntity.getId();
|
||||||
|
log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
|
||||||
|
|
||||||
|
// 2.2 构建聊天记录实体
|
||||||
|
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||||
|
.macAddress(macAddress)
|
||||||
|
.agentId(agentId)
|
||||||
|
.sessionId(report.getSessionId())
|
||||||
|
.chatType(report.getChatType())
|
||||||
|
.content(report.getContent())
|
||||||
|
.audioId(audioId)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 3. 保存数据
|
||||||
|
agentChatHistoryService.save(entity);
|
||||||
|
// 4. 更新设备最后对话时间
|
||||||
|
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表处理service {@link AgentChatAudioService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
|
||||||
|
implements AgentChatAudioService {
|
||||||
|
@Override
|
||||||
|
public String saveAudio(byte[] audioData) {
|
||||||
|
AgentChatAudioEntity entity = new AgentChatAudioEntity();
|
||||||
|
entity.setAudio(audioData);
|
||||||
|
save(entity);
|
||||||
|
return entity.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getAudio(String audioId) {
|
||||||
|
AgentChatAudioEntity entity = getById(audioId);
|
||||||
|
return entity != null ? entity.getAudio() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表处理service {@link AgentChatHistoryService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||||
|
implements AgentChatHistoryService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
|
||||||
|
String agentId = (String) params.get("agentId");
|
||||||
|
int page = Integer.parseInt(params.get(Constant.PAGE).toString());
|
||||||
|
int limit = Integer.parseInt(params.get(Constant.LIMIT).toString());
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.select("session_id", "MAX(created_at) as created_at", "COUNT(*) as chat_count")
|
||||||
|
.eq("agent_id", agentId)
|
||||||
|
.groupBy("session_id")
|
||||||
|
.orderByDesc("created_at");
|
||||||
|
|
||||||
|
// 执行分页查询
|
||||||
|
Page<Map<String, Object>> pageParam = new Page<>(page, limit);
|
||||||
|
IPage<Map<String, Object>> result = this.baseMapper.selectMapsPage(pageParam, wrapper);
|
||||||
|
|
||||||
|
List<AgentChatSessionDTO> records = result.getRecords().stream().map(map -> {
|
||||||
|
AgentChatSessionDTO dto = new AgentChatSessionDTO();
|
||||||
|
dto.setSessionId((String) map.get("session_id"));
|
||||||
|
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
|
||||||
|
dto.setChatCount(((Number) map.get("chat_count")).intValue());
|
||||||
|
return dto;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new PageData<>(records, result.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId) {
|
||||||
|
// 构建查询条件
|
||||||
|
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("agent_id", agentId)
|
||||||
|
.eq("session_id", sessionId)
|
||||||
|
.orderByAsc("created_at");
|
||||||
|
|
||||||
|
// 查询聊天记录
|
||||||
|
List<AgentChatHistoryEntity> historyList = list(wrapper);
|
||||||
|
|
||||||
|
// 转换为DTO
|
||||||
|
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteByAgentId(String agentId) {
|
||||||
|
baseMapper.deleteAudioByAgentId(agentId);
|
||||||
|
baseMapper.deleteHistoryByAgentId(agentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ import java.util.UUID;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -21,25 +21,20 @@ import xiaozhi.modules.agent.dao.AgentDao;
|
|||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.model.service.ModelConfigService;
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
import xiaozhi.modules.timbre.service.TimbreService;
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||||
private final AgentDao agentDao;
|
private final AgentDao agentDao;
|
||||||
|
private final TimbreService timbreModelService;
|
||||||
@Autowired
|
private final ModelConfigService modelConfigService;
|
||||||
private TimbreService timbreModelService;
|
private final RedisUtils redisUtils;
|
||||||
|
private final DeviceService deviceService;
|
||||||
@Autowired
|
|
||||||
private ModelConfigService modelConfigService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RedisUtils redisUtils;
|
|
||||||
|
|
||||||
public AgentServiceImpl(AgentDao agentDao) {
|
|
||||||
this.agentDao = agentDao;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
@@ -101,9 +96,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
// 获取 TTS 音色名称
|
// 获取 TTS 音色名称
|
||||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||||
|
|
||||||
|
// 获取智能体最近的最后连接时长
|
||||||
|
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
||||||
|
|
||||||
// 获取设备数量
|
// 获取设备数量
|
||||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@@ -130,4 +127,29 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
|
|
||||||
return deviceCount != null ? deviceCount : 0;
|
return deviceCount != null ? deviceCount : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AgentEntity getDefaultAgentByMacAddress(String macAddress) {
|
||||||
|
if (StringUtils.isEmpty(macAddress)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return agentDao.getDefaultAgentByMacAddress(macAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkAgentPermission(String agentId, Long userId) {
|
||||||
|
// 获取智能体信息
|
||||||
|
AgentEntity agent = getAgentById(agentId);
|
||||||
|
if (agent == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是超级管理员,直接返回true
|
||||||
|
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是智能体的所有者
|
||||||
|
return userId.equals(agent.getUserId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.service.impl;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||||
@@ -39,29 +40,30 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
|||||||
@Override
|
@Override
|
||||||
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
||||||
modelType = modelType.toUpperCase();
|
modelType = modelType.toUpperCase();
|
||||||
AgentTemplateEntity defaultTemplate = getDefaultTemplate();
|
|
||||||
if (defaultTemplate != null) {
|
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
|
||||||
switch (modelType) {
|
switch (modelType) {
|
||||||
case "ASR":
|
case "ASR":
|
||||||
defaultTemplate.setAsrModelId(modelId);
|
wrapper.set("asr_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "VAD":
|
case "VAD":
|
||||||
defaultTemplate.setVadModelId(modelId);
|
wrapper.set("vad_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "LLM":
|
case "LLM":
|
||||||
defaultTemplate.setLlmModelId(modelId);
|
wrapper.set("llm_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "TTS":
|
case "TTS":
|
||||||
defaultTemplate.setTtsModelId(modelId);
|
wrapper.set("tts_model_id", modelId);
|
||||||
break;
|
wrapper.set("tts_voice_id", null);
|
||||||
case "Memory":
|
break;
|
||||||
defaultTemplate.setMemModelId(modelId);
|
case "MEMORY":
|
||||||
break;
|
wrapper.set("mem_model_id", modelId);
|
||||||
case "Intent":
|
break;
|
||||||
defaultTemplate.setIntentModelId(modelId);
|
case "INTENT":
|
||||||
break;
|
wrapper.set("intent_model_id", modelId);
|
||||||
}
|
break;
|
||||||
this.updateById(defaultTemplate);
|
|
||||||
}
|
}
|
||||||
|
wrapper.ge("sort", 0);
|
||||||
|
update(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package xiaozhi.modules.config.controller;
|
package xiaozhi.modules.config.controller;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -8,15 +7,12 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
|
||||||
import xiaozhi.common.exception.RenException;
|
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.ValidatorUtils;
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
||||||
import xiaozhi.modules.config.service.ConfigService;
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.dto.ConfigSecretDTO;
|
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* xiaozhi-server 配置获取
|
* xiaozhi-server 配置获取
|
||||||
@@ -29,33 +25,20 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ConfigController {
|
public class ConfigController {
|
||||||
private final ConfigService configService;
|
private final ConfigService configService;
|
||||||
private final SysParamsService sysParamsService;
|
|
||||||
|
|
||||||
@PostMapping("server-base")
|
@PostMapping("server-base")
|
||||||
@Operation(summary = "获取配置")
|
@Operation(summary = "获取配置")
|
||||||
public Result<Object> getConfig(@RequestBody ConfigSecretDTO dto) {
|
public Result<Object> getConfig() {
|
||||||
// 效验数据
|
|
||||||
ValidatorUtils.validateEntity(dto);
|
|
||||||
checkSecret(dto.getSecret());
|
|
||||||
Object config = configService.getConfig(true);
|
Object config = configService.getConfig(true);
|
||||||
return new Result<Object>().ok(config);
|
return new Result<Object>().ok(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("agent-models")
|
@PostMapping("agent-models")
|
||||||
@Operation(summary = "获取智能体模型")
|
@Operation(summary = "获取智能体模型")
|
||||||
public Result<Object> getAgentModels(@RequestBody AgentModelsDTO dto) {
|
public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
|
||||||
// 效验数据
|
// 效验数据
|
||||||
ValidatorUtils.validateEntity(dto);
|
ValidatorUtils.validateEntity(dto);
|
||||||
checkSecret(dto.getSecret());
|
|
||||||
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
||||||
return new Result<Object>().ok(models);
|
return new Result<Object>().ok(models);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSecret(String secret) {
|
|
||||||
String secretParam = sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
|
||||||
// 验证密钥
|
|
||||||
if (StringUtils.isBlank(secret) || !secret.equals(secretParam)) {
|
|
||||||
throw new RenException("密钥错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
@Schema(description = "获取智能体模型配置DTO")
|
@Schema(description = "获取智能体模型配置DTO")
|
||||||
public class AgentModelsDTO {
|
public class AgentModelsDTO {
|
||||||
@NotBlank(message = "密钥不能为空")
|
|
||||||
@Schema(description = "密钥")
|
|
||||||
private String secret;
|
|
||||||
|
|
||||||
@NotBlank(message = "设备MAC地址不能为空")
|
@NotBlank(message = "设备MAC地址不能为空")
|
||||||
@Schema(description = "设备MAC地址")
|
@Schema(description = "设备MAC地址")
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.config.service.ConfigService;
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@@ -18,8 +21,20 @@ public class SystemInitConfig {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ConfigService configService;
|
private ConfigService configService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisUtils redisUtils;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
|
// 检查版本号
|
||||||
|
String redisVersion = (String) redisUtils.get(RedisKeys.getVersionKey());
|
||||||
|
if (!Constant.VERSION.equals(redisVersion)) {
|
||||||
|
// 如果版本不一致,清空Redis
|
||||||
|
redisUtils.emptyAll();
|
||||||
|
// 存储新版本号
|
||||||
|
redisUtils.set(RedisKeys.getVersionKey(), Constant.VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
sysParamsService.initServerSecret();
|
sysParamsService.initServerSecret();
|
||||||
configService.getConfig(false);
|
configService.getConfig(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -60,15 +61,15 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
|
|
||||||
// 构建模块配置
|
// 构建模块配置
|
||||||
buildModuleConfig(
|
buildModuleConfig(
|
||||||
agent.getAgentName(),
|
null,
|
||||||
agent.getSystemPrompt(),
|
null,
|
||||||
null,
|
null,
|
||||||
agent.getVadModelId(),
|
agent.getVadModelId(),
|
||||||
agent.getAsrModelId(),
|
agent.getAsrModelId(),
|
||||||
agent.getLlmModelId(),
|
null,
|
||||||
agent.getTtsModelId(),
|
null,
|
||||||
agent.getMemModelId(),
|
null,
|
||||||
agent.getIntentModelId(),
|
null,
|
||||||
result,
|
result,
|
||||||
isCache);
|
isCache);
|
||||||
|
|
||||||
@@ -83,8 +84,14 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
// 根据MAC地址查找设备
|
// 根据MAC地址查找设备
|
||||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
throw new RenException("设备未找到");
|
// 如果设备,去redis里看看有没有需要连接的设备
|
||||||
|
String cachedCode = deviceService.geCodeByDeviceId(macAddress);
|
||||||
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
|
throw new RenException(ErrorCode.OTA_DEVICE_NEED_BIND, cachedCode);
|
||||||
|
}
|
||||||
|
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取智能体信息
|
// 获取智能体信息
|
||||||
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
@@ -98,7 +105,9 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
}
|
}
|
||||||
// 构建返回数据
|
// 构建返回数据
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
// 获取单台设备每天最多输出字数
|
||||||
|
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||||
|
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||||
// 如果客户端已实例化模型,则不返回
|
// 如果客户端已实例化模型,则不返回
|
||||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||||
@@ -108,18 +117,6 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
||||||
agent.setAsrModelId(null);
|
agent.setAsrModelId(null);
|
||||||
}
|
}
|
||||||
String alreadySelectedLlmModelId = (String) selectedModule.get("LLM");
|
|
||||||
if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) {
|
|
||||||
agent.setLlmModelId(null);
|
|
||||||
}
|
|
||||||
String alreadySelectedMemModelId = (String) selectedModule.get("Memory");
|
|
||||||
if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) {
|
|
||||||
agent.setMemModelId(null);
|
|
||||||
}
|
|
||||||
String alreadySelectedIntentModelId = (String) selectedModule.get("Intent");
|
|
||||||
if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) {
|
|
||||||
agent.setIntentModelId(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建模块配置
|
// 构建模块配置
|
||||||
buildModuleConfig(
|
buildModuleConfig(
|
||||||
@@ -144,7 +141,6 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
* @param paramsList 系统参数列表
|
* @param paramsList 系统参数列表
|
||||||
* @return 配置信息
|
* @return 配置信息
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Object buildConfig(Map<String, Object> config) {
|
private Object buildConfig(Map<String, Object> config) {
|
||||||
|
|
||||||
// 查询所有系统参数
|
// 查询所有系统参数
|
||||||
@@ -171,7 +167,13 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
switch (param.getValueType().toLowerCase()) {
|
switch (param.getValueType().toLowerCase()) {
|
||||||
case "number":
|
case "number":
|
||||||
try {
|
try {
|
||||||
current.put(lastKey, Double.parseDouble(value));
|
double doubleValue = Double.parseDouble(value);
|
||||||
|
// 如果数值是整数形式,则转换为Integer
|
||||||
|
if (doubleValue == (int) doubleValue) {
|
||||||
|
current.put(lastKey, (int) doubleValue);
|
||||||
|
} else {
|
||||||
|
current.put(lastKey, doubleValue);
|
||||||
|
}
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
current.put(lastKey, value);
|
current.put(lastKey, value);
|
||||||
}
|
}
|
||||||
@@ -255,6 +257,12 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||||
intentLLMModelId = null;
|
intentLLMModelId = null;
|
||||||
}
|
}
|
||||||
|
} else if ("function_call".equals(map.get("type"))) {
|
||||||
|
String functionStr = (String) map.get("functions");
|
||||||
|
if (StringUtils.isNotBlank(functionStr)) {
|
||||||
|
String[] functions = functionStr.split("\\;");
|
||||||
|
map.put("functions", functions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
||||||
@@ -270,7 +278,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
|
|
||||||
result.put("selected_module", selectedModule);
|
result.put("selected_module", selectedModule);
|
||||||
if (StringUtils.isNotBlank(prompt)) {
|
if (StringUtils.isNotBlank(prompt)) {
|
||||||
prompt = prompt.replace("{{assistant_name}}", "小智");
|
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||||
}
|
}
|
||||||
result.put("prompt", prompt);
|
result.put("prompt", prompt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -79,4 +80,16 @@ public class DeviceController {
|
|||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/enableOta/{id}/{status}")
|
||||||
|
@Operation(summary = "启用/关闭OTA自动升级")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<Void> enableOtaUpgrade(@PathVariable String id, @PathVariable Integer status) {
|
||||||
|
DeviceEntity entity = deviceService.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<Void>().error("设备不存在");
|
||||||
|
}
|
||||||
|
entity.setAutoUpdate(status);
|
||||||
|
deviceService.updateById(entity);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,48 +15,78 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.utils.NetworkUtil;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/ota/")
|
@RequestMapping("/ota/")
|
||||||
public class OTAController {
|
public class OTAController {
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@Operation(summary = "检查 OTA 版本和设备激活状态")
|
@Operation(summary = "OTA版本和设备激活状态检查")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<String> checkOTAVersion(
|
public ResponseEntity<String> checkOTAVersion(
|
||||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||||
|
|
||||||
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
|
||||||
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isBlank(clientId)) {
|
||||||
|
clientId = deviceId;
|
||||||
|
}
|
||||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
boolean macAddressValid = isMacAddressValid(macAddress);
|
||||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||||
}
|
}
|
||||||
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "设备快速检查激活状态")
|
||||||
|
@PostMapping("activate")
|
||||||
|
public ResponseEntity<String> activateDevice(
|
||||||
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
|
return ResponseEntity.status(202).build();
|
||||||
|
}
|
||||||
|
DeviceEntity device = deviceService.getDeviceByMacAddress(deviceId);
|
||||||
|
if (device == null) {
|
||||||
|
return ResponseEntity.status(202).build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok("success");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取 OTA 提示信息")
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<String> getOTAPrompt() {
|
@Hidden
|
||||||
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
|
public ResponseEntity<String> getOTA() {
|
||||||
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
|
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
|
}
|
||||||
|
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||||
|
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||||
|
return ResponseEntity.ok("OTA接口不正常,缺少ota地址,请登录智控台,在参数管理找到【server.ota】配置");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@@ -71,4 +101,19 @@ public class OTAController {
|
|||||||
.contentLength(jsonBytes.length)
|
.contentLength(jsonBytes.length)
|
||||||
.body(json);
|
.body(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单判断mac地址是否有效(非严格)
|
||||||
|
*
|
||||||
|
* @param macAddress
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isMacAddressValid(String macAddress) {
|
||||||
|
if (StringUtils.isBlank(macAddress)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// MAC地址通常为12位十六进制数字,可以包含冒号或连字符分隔符
|
||||||
|
String macPattern = "^([0-9A-Za-z]{2}[:-]){5}([0-9A-Za-z]{2})$";
|
||||||
|
return macAddress.matches(macPattern);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
|
||||||
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/otaMag")
|
||||||
|
public class OTAMagController {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
||||||
|
private final OtaService otaService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "分页查询 OTA 固件信息")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||||
|
})
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<PageData<OtaEntity>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
PageData<OtaEntity> page = otaService.page(params);
|
||||||
|
return new Result<PageData<OtaEntity>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
@Operation(summary = "信息 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<OtaEntity> get(@PathVariable("id") String id) {
|
||||||
|
OtaEntity data = otaService.selectById(id);
|
||||||
|
return new Result<OtaEntity>().ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "保存 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody OtaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<Void>().error("固件信息不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getFirmwareName())) {
|
||||||
|
return new Result<Void>().error("固件名称不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getType())) {
|
||||||
|
return new Result<Void>().error("固件类型不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getVersion())) {
|
||||||
|
return new Result<Void>().error("版本号不能为空");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
otaService.save(entity);
|
||||||
|
return new Result<Void>();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new Result<Void>().error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "OTA 删除")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> delete(@PathVariable("id") String[] ids) {
|
||||||
|
if (ids == null || ids.length == 0) {
|
||||||
|
return new Result<Void>().error("删除的固件ID不能为空");
|
||||||
|
}
|
||||||
|
otaService.delete(ids);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "修改 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<?> update(@PathVariable("id") String id, @RequestBody OtaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<>().error("固件信息不能为空");
|
||||||
|
}
|
||||||
|
entity.setId(id);
|
||||||
|
try {
|
||||||
|
otaService.update(entity);
|
||||||
|
return new Result<>();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new Result<>().error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/getDownloadUrl/{id}")
|
||||||
|
@Operation(summary = "获取 OTA 固件下载链接")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> getDownloadUrl(@PathVariable("id") String id) {
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getOtaIdKey(uuid), id);
|
||||||
|
return new Result<String>().ok(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/download/{uuid}")
|
||||||
|
@Operation(summary = "下载固件文件")
|
||||||
|
public ResponseEntity<byte[]> downloadFirmware(@PathVariable("uuid") String uuid) {
|
||||||
|
String id = (String) redisUtils.get(RedisKeys.getOtaIdKey(uuid));
|
||||||
|
if (StringUtils.isBlank(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查下载次数
|
||||||
|
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
||||||
|
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
|
||||||
|
if (downloadCount == null) {
|
||||||
|
downloadCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果下载次数超过3次,返回404
|
||||||
|
if (downloadCount >= 3) {
|
||||||
|
redisUtils.delete(downloadCountKey);
|
||||||
|
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
|
||||||
|
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
redisUtils.set(downloadCountKey, downloadCount + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取固件信息
|
||||||
|
OtaEntity otaEntity = otaService.selectById(id);
|
||||||
|
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
||||||
|
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
||||||
|
String firmwarePath = otaEntity.getFirmwarePath();
|
||||||
|
Path path;
|
||||||
|
|
||||||
|
// 检查是否是绝对路径
|
||||||
|
if (Paths.get(firmwarePath).isAbsolute()) {
|
||||||
|
path = Paths.get(firmwarePath);
|
||||||
|
} else {
|
||||||
|
// 如果是相对路径,则从当前工作目录解析
|
||||||
|
path = Paths.get(System.getProperty("user.dir"), firmwarePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Attempting to download firmware for ID: {}, DB path: {}, resolved path: {}",
|
||||||
|
id, firmwarePath, path.toAbsolutePath());
|
||||||
|
|
||||||
|
if (!Files.exists(path) || !Files.isRegularFile(path)) {
|
||||||
|
// 尝试直接从firmware目录下查找文件名
|
||||||
|
String fileName = new File(firmwarePath).getName();
|
||||||
|
Path altPath = Paths.get(System.getProperty("user.dir"), "firmware", fileName);
|
||||||
|
|
||||||
|
logger.info("File not found at primary path, trying alternative path: {}", altPath.toAbsolutePath());
|
||||||
|
|
||||||
|
if (Files.exists(altPath) && Files.isRegularFile(altPath)) {
|
||||||
|
path = altPath;
|
||||||
|
} else {
|
||||||
|
logger.error("Firmware file not found at either path: {} or {}",
|
||||||
|
path.toAbsolutePath(), altPath.toAbsolutePath());
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
byte[] fileContent = Files.readAllBytes(path);
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||||
|
if (firmwarePath.contains(".")) {
|
||||||
|
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
||||||
|
originalFilename += extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理文件名,移除不安全字符
|
||||||
|
String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||||
|
|
||||||
|
logger.info("Providing download for firmware ID: {}, filename: {}, size: {} bytes",
|
||||||
|
id, safeFilename, fileContent.length);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + safeFilename + "\"")
|
||||||
|
.body(fileContent);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error reading firmware file for ID: {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error during firmware download for ID: {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@Operation(summary = "上传固件文件")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> uploadFirmware(@RequestParam("file") MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return new Result<String>().error("上传文件不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件扩展名
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
if (originalFilename == null) {
|
||||||
|
return new Result<String>().error("文件名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
|
||||||
|
if (!extension.equals(".bin") && !extension.equals(".apk")) {
|
||||||
|
return new Result<String>().error("只允许上传.bin和.apk格式的文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算文件的MD5值
|
||||||
|
String md5 = calculateMD5(file);
|
||||||
|
|
||||||
|
// 设置存储路径
|
||||||
|
String uploadDir = "uploadfile";
|
||||||
|
Path uploadPath = Paths.get(uploadDir);
|
||||||
|
|
||||||
|
// 如果目录不存在,创建目录
|
||||||
|
if (!Files.exists(uploadPath)) {
|
||||||
|
Files.createDirectories(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用MD5作为文件名,固定使用.bin扩展名
|
||||||
|
String uniqueFileName = md5 + extension;
|
||||||
|
Path filePath = uploadPath.resolve(uniqueFileName);
|
||||||
|
|
||||||
|
// 检查文件是否已存在
|
||||||
|
if (Files.exists(filePath)) {
|
||||||
|
return new Result<String>().ok(filePath.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
Files.copy(file.getInputStream(), filePath);
|
||||||
|
|
||||||
|
// 返回文件路径
|
||||||
|
return new Result<String>().ok(filePath.toString());
|
||||||
|
} catch (IOException | NoSuchAlgorithmException e) {
|
||||||
|
return new Result<String>().error("文件上传失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] digest = md.digest(file.getBytes());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : digest) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,16 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|||||||
|
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||||
|
/**
|
||||||
|
* 获取此智能体全部设备的最后连接时间
|
||||||
|
* @param agentId 智能体id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Date getAllLastConnectedAtByAgentId(String agentId);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package xiaozhi.modules.device.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件管理
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface OtaDao extends BaseMapper<OtaEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "固件版本信息")
|
@Schema(description = "固件版本信息")
|
||||||
private Firmware firmware;
|
private Firmware firmware;
|
||||||
|
|
||||||
|
@Schema(description = "WebSocket配置")
|
||||||
|
private Websocket websocket;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public static class Firmware {
|
public static class Firmware {
|
||||||
@@ -44,6 +47,8 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "激活码信息: 激活地址")
|
@Schema(description = "激活码信息: 激活地址")
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
|
@Schema(description = "挑战码")
|
||||||
|
private String challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -58,4 +63,11 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "时区偏移量,单位为分钟")
|
@Schema(description = "时区偏移量,单位为分钟")
|
||||||
private Integer timezone_offset;
|
private Integer timezone_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Websocket {
|
||||||
|
@Schema(description = "WebSocket服务器地址")
|
||||||
|
private String url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package xiaozhi.modules.device.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("ai_ota")
|
||||||
|
@Schema(description = "固件信息")
|
||||||
|
public class OtaEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "ID")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "固件名称")
|
||||||
|
private String firmwareName;
|
||||||
|
|
||||||
|
@Schema(description = "固件类型")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "版本号")
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小(字节)")
|
||||||
|
private Long size;
|
||||||
|
|
||||||
|
@Schema(description = "备注/说明")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "固件路径")
|
||||||
|
private String firmwarePath;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Date updateDate;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Date createDate;
|
||||||
|
}
|
||||||
@@ -1,25 +1,22 @@
|
|||||||
package xiaozhi.modules.device.service;
|
package xiaozhi.modules.device.service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
|
|
||||||
public interface DeviceService {
|
public interface DeviceService extends BaseService<DeviceEntity> {
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据Mac地址获取设备信息
|
|
||||||
*/
|
|
||||||
DeviceEntity getDeviceById(String macAddress);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查设备是否激活
|
* 检查设备是否激活
|
||||||
*/
|
*/
|
||||||
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||||
DeviceReportReqDTO deviceReport);
|
DeviceReportReqDTO deviceReport);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,4 +71,21 @@ public interface DeviceService {
|
|||||||
* @return 设备信息
|
* @return 设备信息
|
||||||
*/
|
*/
|
||||||
DeviceEntity getDeviceByMacAddress(String macAddress);
|
DeviceEntity getDeviceByMacAddress(String macAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备ID获取激活码
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @return 激活码
|
||||||
|
*/
|
||||||
|
String geCodeByDeviceId(String deviceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取这个智能体设备理的最近的最后连接时间
|
||||||
|
* @param agentId 智能体id
|
||||||
|
* @return 返回设备最近的最后连接时间
|
||||||
|
*/
|
||||||
|
Date getLatestLastConnectionTime(String agentId);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.device.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件管理
|
||||||
|
*/
|
||||||
|
public interface OtaService extends BaseService<OtaEntity> {
|
||||||
|
PageData<OtaEntity> page(Map<String, Object> params);
|
||||||
|
|
||||||
|
boolean save(OtaEntity entity);
|
||||||
|
|
||||||
|
void update(OtaEntity entity);
|
||||||
|
|
||||||
|
void delete(String[] ids);
|
||||||
|
|
||||||
|
OtaEntity getLatestOta(String type);
|
||||||
|
}
|
||||||
@@ -6,22 +6,26 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
@@ -31,38 +35,24 @@ import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
|||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||||
|
|
||||||
private final DeviceDao deviceDao;
|
private final DeviceDao deviceDao;
|
||||||
|
|
||||||
private final SysUserUtilService sysUserUtilService;
|
private final SysUserUtilService sysUserUtilService;
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
private final String frontedUrl;
|
private final RedisUtils redisUtils;
|
||||||
|
private final OtaService otaService;
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
|
||||||
|
|
||||||
// 添加构造函数来初始化 deviceMapper
|
|
||||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
|
||||||
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
|
||||||
RedisTemplate<String, Object> redisTemplate) {
|
|
||||||
this.deviceDao = deviceDao;
|
|
||||||
this.sysUserUtilService = sysUserUtilService;
|
|
||||||
this.frontedUrl = frontedUrl;
|
|
||||||
this.redisTemplate = redisTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DeviceEntity getDeviceById(String deviceId) {
|
|
||||||
LambdaQueryWrapper<DeviceEntity> queryWrapper = new LambdaQueryWrapper<>();
|
|
||||||
queryWrapper.eq(DeviceEntity::getId, deviceId);
|
|
||||||
return deviceDao.selectOne(queryWrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||||
@@ -70,14 +60,14 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
throw new RenException("激活码不能为空");
|
throw new RenException("激活码不能为空");
|
||||||
}
|
}
|
||||||
String deviceKey = "ota:activation:code:" + activationCode;
|
String deviceKey = "ota:activation:code:" + activationCode;
|
||||||
Object cacheDeviceId = redisTemplate.opsForValue().get(deviceKey);
|
Object cacheDeviceId = redisUtils.get(deviceKey);
|
||||||
if (cacheDeviceId == null) {
|
if (cacheDeviceId == null) {
|
||||||
throw new RenException("激活码错误");
|
throw new RenException("激活码错误");
|
||||||
}
|
}
|
||||||
String deviceId = (String) cacheDeviceId;
|
String deviceId = (String) cacheDeviceId;
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(cacheDeviceKey);
|
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||||
if (cacheMap == null) {
|
if (cacheMap == null) {
|
||||||
throw new RenException("激活码错误");
|
throw new RenException("激活码错误");
|
||||||
}
|
}
|
||||||
@@ -107,6 +97,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
deviceEntity.setMacAddress(macAddress);
|
deviceEntity.setMacAddress(macAddress);
|
||||||
deviceEntity.setUserId(user.getId());
|
deviceEntity.setUserId(user.getId());
|
||||||
deviceEntity.setCreator(user.getId());
|
deviceEntity.setCreator(user.getId());
|
||||||
|
deviceEntity.setAutoUpdate(1);
|
||||||
deviceEntity.setCreateDate(currentTime);
|
deviceEntity.setCreateDate(currentTime);
|
||||||
deviceEntity.setUpdater(user.getId());
|
deviceEntity.setUpdater(user.getId());
|
||||||
deviceEntity.setUpdateDate(currentTime);
|
deviceEntity.setUpdateDate(currentTime);
|
||||||
@@ -114,64 +105,60 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
deviceDao.insert(deviceEntity);
|
deviceDao.insert(deviceEntity);
|
||||||
|
|
||||||
// 清理redis缓存
|
// 清理redis缓存
|
||||||
redisTemplate.delete(cacheDeviceKey);
|
redisUtils.delete(cacheDeviceKey);
|
||||||
redisTemplate.delete(deviceKey);
|
redisUtils.delete(deviceKey);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||||
DeviceReportReqDTO deviceReport) {
|
DeviceReportReqDTO deviceReport) {
|
||||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||||
response.setServer_time(buildServerTime());
|
response.setServer_time(buildServerTime());
|
||||||
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
|
|
||||||
// 在未来开发了固件更新功能,需要更换此处代码,
|
|
||||||
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
|
|
||||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
|
||||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
|
||||||
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
|
||||||
response.setFirmware(firmware);
|
|
||||||
|
|
||||||
DeviceEntity deviceById = getDeviceById(deviceId);
|
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
||||||
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
|
||||||
deviceById.setLastConnectedAt(new Date());
|
|
||||||
deviceDao.updateById(deviceById);
|
|
||||||
} else { // 如果设备不存在,则生成激活码
|
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
|
||||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
|
||||||
|
|
||||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
if (deviceById == null || deviceById.getAutoUpdate() != 0) {
|
||||||
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||||
|
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||||
|
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||||
|
response.setFirmware(firmware);
|
||||||
|
} else {
|
||||||
|
response.setFirmware(null);
|
||||||
|
}
|
||||||
|
|
||||||
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
// 添加WebSocket配置
|
||||||
String cachedCode = (String) cacheMap.get("activation_code");
|
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||||
code.setCode(cachedCode);
|
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||||
code.setMessage(frontedUrl + "\n" + cachedCode);
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
|
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
|
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||||
|
websocket.setUrl(wsUrl);
|
||||||
|
} else {
|
||||||
|
String[] wsUrls = wsUrl.split("\\;");
|
||||||
|
if (wsUrls.length > 0) {
|
||||||
|
// 随机选择一个WebSocket URL
|
||||||
|
websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]);
|
||||||
} else {
|
} else {
|
||||||
String newCode = RandomUtil.randomNumbers(6);
|
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
code.setCode(newCode);
|
websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/");
|
||||||
code.setMessage(frontedUrl + "\n" + newCode);
|
|
||||||
|
|
||||||
Map<String, Object> dataMap = new HashMap<>();
|
|
||||||
dataMap.put("id", deviceId);
|
|
||||||
dataMap.put("mac_address", macAddress);
|
|
||||||
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
|
|
||||||
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
|
|
||||||
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
|
||||||
? deviceReport.getApplication().getVersion()
|
|
||||||
: null);
|
|
||||||
dataMap.put("deviceId", deviceId);
|
|
||||||
dataMap.put("activation_code", newCode);
|
|
||||||
|
|
||||||
// 写入主数据 key
|
|
||||||
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
|
||||||
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
|
||||||
|
|
||||||
// 写入反查激活码 key
|
|
||||||
String codeKey = "ota:activation:code:" + newCode;
|
|
||||||
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setWebsocket(websocket);
|
||||||
|
|
||||||
|
if (deviceById != null) {
|
||||||
|
// 如果设备存在,则更新上次连接时间
|
||||||
|
deviceById.setLastConnectedAt(new Date());
|
||||||
|
if (deviceReport.getApplication() != null
|
||||||
|
&& StringUtils.isNotBlank(deviceReport.getApplication().getVersion())) {
|
||||||
|
deviceById.setAppVersion(deviceReport.getApplication().getVersion());
|
||||||
|
}
|
||||||
|
deviceDao.updateById(deviceById);
|
||||||
|
} else {
|
||||||
|
// 如果设备不存在,则生成激活码
|
||||||
|
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||||
response.setActivation(code);
|
response.setActivation(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,4 +245,143 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||||
return serverTime;
|
return serverTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String geCodeByDeviceId(String deviceId) {
|
||||||
|
String dataKey = getDeviceCacheKey(deviceId);
|
||||||
|
|
||||||
|
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
|
||||||
|
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||||
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
|
return cachedCode;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getLatestLastConnectionTime(String agentId) {
|
||||||
|
// 查询是否有缓存时间,有则返回
|
||||||
|
Date cachedDate = (Date) redisUtils.get(RedisKeys.getAgentDeviceLastConnectedAtById(agentId));
|
||||||
|
if (cachedDate != null) {
|
||||||
|
return cachedDate;
|
||||||
|
}
|
||||||
|
Date maxDate = deviceDao.getAllLastConnectedAtByAgentId(agentId);
|
||||||
|
if (maxDate != null) {
|
||||||
|
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), maxDate);
|
||||||
|
}
|
||||||
|
return maxDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDeviceCacheKey(String deviceId) {
|
||||||
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
|
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
return dataKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
|
||||||
|
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
||||||
|
|
||||||
|
String cachedCode = geCodeByDeviceId(deviceId);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
|
code.setCode(cachedCode);
|
||||||
|
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
|
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||||
|
code.setChallenge(deviceId);
|
||||||
|
} else {
|
||||||
|
String newCode = RandomUtil.randomNumbers(6);
|
||||||
|
code.setCode(newCode);
|
||||||
|
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
|
code.setMessage(frontedUrl + "\n" + newCode);
|
||||||
|
code.setChallenge(deviceId);
|
||||||
|
|
||||||
|
Map<String, Object> dataMap = new HashMap<>();
|
||||||
|
dataMap.put("id", deviceId);
|
||||||
|
dataMap.put("mac_address", deviceId);
|
||||||
|
|
||||||
|
dataMap.put("board", (deviceReport.getBoard() != null && deviceReport.getBoard().getType() != null)
|
||||||
|
? deviceReport.getBoard().getType()
|
||||||
|
: (deviceReport.getChipModelName() != null ? deviceReport.getChipModelName() : "unknown"));
|
||||||
|
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
||||||
|
? deviceReport.getApplication().getVersion()
|
||||||
|
: null);
|
||||||
|
|
||||||
|
dataMap.put("deviceId", deviceId);
|
||||||
|
dataMap.put("activation_code", newCode);
|
||||||
|
|
||||||
|
// 写入主数据 key
|
||||||
|
String dataKey = getDeviceCacheKey(deviceId);
|
||||||
|
redisUtils.set(dataKey, dataMap);
|
||||||
|
|
||||||
|
// 写入反查激活码 key
|
||||||
|
String codeKey = "ota:activation:code:" + newCode;
|
||||||
|
redisUtils.set(codeKey, deviceId);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceReportRespDTO.Firmware buildFirmwareInfo(String type, String currentVersion) {
|
||||||
|
if (StringUtils.isBlank(type)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(currentVersion)) {
|
||||||
|
currentVersion = "0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
OtaEntity ota = otaService.getLatestOta(type);
|
||||||
|
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||||
|
String downloadUrl = null;
|
||||||
|
|
||||||
|
if (ota != null) {
|
||||||
|
// 如果设备没有版本信息,或者OTA版本比设备版本新,则返回下载地址
|
||||||
|
if (compareVersions(ota.getVersion(), currentVersion) > 0) {
|
||||||
|
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||||
|
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||||
|
log.error("OTA地址未配置,请登录智控台,在参数管理找到【server.ota】配置");
|
||||||
|
// 尝试从请求中获取
|
||||||
|
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
|
||||||
|
.getRequestAttributes())
|
||||||
|
.getRequest();
|
||||||
|
otaUrl = request.getRequestURL().toString();
|
||||||
|
}
|
||||||
|
// 将URL中的/ota/替换为/otaMag/download/
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getOtaIdKey(uuid), ota.getId());
|
||||||
|
downloadUrl = otaUrl.replace("/ota/", "/otaMag/download/") + uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
|
||||||
|
firmware.setUrl(downloadUrl == null ? "" : downloadUrl);
|
||||||
|
return firmware;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个版本号
|
||||||
|
*
|
||||||
|
* @param version1 版本1
|
||||||
|
* @param version2 版本2
|
||||||
|
* @return 如果version1 > version2返回1,version1 < version2返回-1,相等返回0
|
||||||
|
*/
|
||||||
|
private static int compareVersions(String version1, String version2) {
|
||||||
|
if (version1 == null || version2 == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] v1Parts = version1.split("\\.");
|
||||||
|
String[] v2Parts = version2.split("\\.");
|
||||||
|
|
||||||
|
int length = Math.max(v1Parts.length, v2Parts.length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int v1 = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;
|
||||||
|
int v2 = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;
|
||||||
|
|
||||||
|
if (v1 > v2) {
|
||||||
|
return 1;
|
||||||
|
} else if (v1 < v2) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.modules.device.dao.OtaDao;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implements OtaService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<OtaEntity> page(Map<String, Object> params) {
|
||||||
|
IPage<OtaEntity> page = baseDao.selectPage(
|
||||||
|
getPage(params, "update_date", true),
|
||||||
|
getWrapper(params));
|
||||||
|
|
||||||
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryWrapper<OtaEntity> getWrapper(Map<String, Object> params) {
|
||||||
|
String firmwareName = (String) params.get("firmwareName");
|
||||||
|
|
||||||
|
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.like(StringUtils.isNotBlank(firmwareName), "firmware_name", firmwareName);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(OtaEntity entity) {
|
||||||
|
// 检查是否存在相同类型和版本的固件(排除当前记录)
|
||||||
|
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||||
|
.eq("type", entity.getType())
|
||||||
|
.eq("version", entity.getVersion())
|
||||||
|
.ne("id", entity.getId()); // 排除当前记录
|
||||||
|
|
||||||
|
if (baseDao.selectCount(queryWrapper) > 0) {
|
||||||
|
throw new RuntimeException("已存在相同类型和版本的固件,请修改后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.setUpdateDate(new Date());
|
||||||
|
baseDao.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String[] ids) {
|
||||||
|
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean save(OtaEntity entity) {
|
||||||
|
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||||
|
.eq("type", entity.getType());
|
||||||
|
// 同类固件只保留最新的一条
|
||||||
|
List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
|
||||||
|
if (otaList != null && otaList.size() > 0) {
|
||||||
|
OtaEntity otaBefore = otaList.getFirst();
|
||||||
|
entity.setId(otaBefore.getId());
|
||||||
|
baseDao.updateById(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return baseDao.insert(entity) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OtaEntity getLatestOta(String type) {
|
||||||
|
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("type", type)
|
||||||
|
.orderByDesc("update_date")
|
||||||
|
.last("LIMIT 1");
|
||||||
|
return baseDao.selectOne(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package xiaozhi.modules.device.utils;
|
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 网络工具类
|
|
||||||
*/
|
|
||||||
public class NetworkUtil {
|
|
||||||
/**
|
|
||||||
* MAC地址正则表达式
|
|
||||||
*/
|
|
||||||
private static final Pattern macPattern = Pattern.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断MAC地址是否合法
|
|
||||||
*/
|
|
||||||
public static boolean isMacAddressValid(String mac) {
|
|
||||||
if (StringUtils.isBlank(mac)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 正则校验格式
|
|
||||||
if (!macPattern.matcher(mac).matches()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 校验MAC地址是否为单播地址
|
|
||||||
String normalized = mac.toLowerCase();
|
|
||||||
String[] parts = normalized.split("[:-]");
|
|
||||||
int firstByte = Integer.parseInt(parts[0], 16);
|
|
||||||
return (firstByte & 1) == 0; // 最低位为0表示单播地址,合法
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -135,6 +135,7 @@ public class ModelController {
|
|||||||
}
|
}
|
||||||
// 将其他模型设置为非默认
|
// 将其他模型设置为非默认
|
||||||
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||||
|
entity.setIsEnabled(1);
|
||||||
entity.setIsDefault(1);
|
entity.setIsDefault(1);
|
||||||
modelConfigService.updateById(entity);
|
modelConfigService.updateById(entity);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.service.impl;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -19,6 +20,8 @@ import xiaozhi.common.redis.RedisKeys;
|
|||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.modules.agent.dao.AgentDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
@@ -36,12 +39,14 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
private final ModelConfigDao modelConfigDao;
|
private final ModelConfigDao modelConfigDao;
|
||||||
private final ModelProviderService modelProviderService;
|
private final ModelProviderService modelProviderService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
|
private final AgentDao agentDao;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
|
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
|
||||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||||
new QueryWrapper<ModelConfigEntity>()
|
new QueryWrapper<ModelConfigEntity>()
|
||||||
.eq("model_type", modelType)
|
.eq("model_type", modelType)
|
||||||
|
.eq("is_enabled", 1)
|
||||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||||
.select("id", "model_name"));
|
.select("id", "model_name"));
|
||||||
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||||
@@ -74,6 +79,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
// 再保存供应器提供的模型
|
// 再保存供应器提供的模型
|
||||||
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||||
modelConfigEntity.setModelType(modelType);
|
modelConfigEntity.setModelType(modelType);
|
||||||
|
modelConfigEntity.setIsDefault(0);
|
||||||
modelConfigDao.insert(modelConfigEntity);
|
modelConfigDao.insert(modelConfigEntity);
|
||||||
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||||
}
|
}
|
||||||
@@ -101,9 +107,64 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String id) {
|
public void delete(String id) {
|
||||||
|
// 查看是否是默认
|
||||||
|
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
|
||||||
|
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
|
||||||
|
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
|
||||||
|
}
|
||||||
|
// 验证是否有引用
|
||||||
|
checkAgentReference(id);
|
||||||
|
checkIntentConfigReference(id);
|
||||||
|
|
||||||
modelConfigDao.deleteById(id);
|
modelConfigDao.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查智能体配置是否有引用
|
||||||
|
*
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
private void checkAgentReference(String modelId) {
|
||||||
|
List<AgentEntity> agents = agentDao.selectList(
|
||||||
|
new QueryWrapper<AgentEntity>()
|
||||||
|
.eq("vad_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("asr_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("llm_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("tts_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("mem_model_id", modelId)
|
||||||
|
.or()
|
||||||
|
.eq("intent_model_id", modelId));
|
||||||
|
if (!agents.isEmpty()) {
|
||||||
|
String agentNames = agents.stream()
|
||||||
|
.map(AgentEntity::getAgentName)
|
||||||
|
.collect(Collectors.joining("、"));
|
||||||
|
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查意图识别配置是否有引用
|
||||||
|
*
|
||||||
|
* @param modelId 模型ID
|
||||||
|
*/
|
||||||
|
private void checkIntentConfigReference(String modelId) {
|
||||||
|
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
|
||||||
|
if (modelConfig != null
|
||||||
|
&& "LLM".equals(modelConfig.getModelType() == null ? null : modelConfig.getModelType().toUpperCase())) {
|
||||||
|
List<ModelConfigEntity> intentConfigs = modelConfigDao.selectList(
|
||||||
|
new QueryWrapper<ModelConfigEntity>()
|
||||||
|
.eq("model_type", "Intent")
|
||||||
|
.like("config_json", "%" + modelId + "%"));
|
||||||
|
if (!intentConfigs.isEmpty()) {
|
||||||
|
throw new RenException("该LLM模型已被意图识别配置引用,无法删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getModelNameById(String id) {
|
public String getModelNameById(String id) {
|
||||||
if (StringUtils.isBlank(id)) {
|
if (StringUtils.isBlank(id)) {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import jakarta.servlet.Filter;
|
import jakarta.servlet.Filter;
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||||
|
import xiaozhi.modules.security.secret.ServerSecretFilter;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shiro的配置文件
|
* Shiro的配置文件
|
||||||
@@ -46,7 +48,7 @@ public class ShiroConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean("shiroFilter")
|
@Bean("shiroFilter")
|
||||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
|
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
|
||||||
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
||||||
config.setFilterOncePerRequest(true);
|
config.setFilterOncePerRequest(true);
|
||||||
|
|
||||||
@@ -54,9 +56,11 @@ public class ShiroConfig {
|
|||||||
shiroFilter.setSecurityManager(securityManager);
|
shiroFilter.setSecurityManager(securityManager);
|
||||||
shiroFilter.setShiroFilterConfiguration(config);
|
shiroFilter.setShiroFilterConfiguration(config);
|
||||||
|
|
||||||
// oauth过滤
|
|
||||||
Map<String, Filter> filters = new HashMap<>();
|
Map<String, Filter> filters = new HashMap<>();
|
||||||
|
// oauth过滤
|
||||||
filters.put("oauth2", new Oauth2Filter());
|
filters.put("oauth2", new Oauth2Filter());
|
||||||
|
// 服务密钥过滤
|
||||||
|
filters.put("server", new ServerSecretFilter(sysParamsService));
|
||||||
shiroFilter.setFilters(filters);
|
shiroFilter.setFilters(filters);
|
||||||
|
|
||||||
// 添加Shiro的内置过滤器
|
// 添加Shiro的内置过滤器
|
||||||
@@ -69,6 +73,7 @@ public class ShiroConfig {
|
|||||||
*/
|
*/
|
||||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||||
filterMap.put("/ota/**", "anon");
|
filterMap.put("/ota/**", "anon");
|
||||||
|
filterMap.put("/otaMag/download/**", "anon");
|
||||||
filterMap.put("/webjars/**", "anon");
|
filterMap.put("/webjars/**", "anon");
|
||||||
filterMap.put("/druid/**", "anon");
|
filterMap.put("/druid/**", "anon");
|
||||||
filterMap.put("/v3/api-docs/**", "anon");
|
filterMap.put("/v3/api-docs/**", "anon");
|
||||||
@@ -76,9 +81,12 @@ public class ShiroConfig {
|
|||||||
filterMap.put("/favicon.ico", "anon");
|
filterMap.put("/favicon.ico", "anon");
|
||||||
filterMap.put("/user/captcha", "anon");
|
filterMap.put("/user/captcha", "anon");
|
||||||
filterMap.put("/user/login", "anon");
|
filterMap.put("/user/login", "anon");
|
||||||
|
filterMap.put("/user/pub-config", "anon");
|
||||||
filterMap.put("/user/register", "anon");
|
filterMap.put("/user/register", "anon");
|
||||||
filterMap.put("/config/server-base", "anon");
|
// 将config路径使用server服务过滤器
|
||||||
filterMap.put("/config/agent-models", "anon");
|
filterMap.put("/config/**", "server");
|
||||||
|
filterMap.put("/agent/chat-history/report", "server");
|
||||||
|
filterMap.put("/agent/play/**", "anon");
|
||||||
filterMap.put("/**", "oauth2");
|
filterMap.put("/**", "oauth2");
|
||||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package xiaozhi.modules.security.config;
|
package xiaozhi.modules.security.config;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
@@ -18,6 +19,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||||
|
|
||||||
|
import xiaozhi.common.utils.DateUtils;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
@@ -33,11 +43,15 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||||
|
// 特殊用途的转换器
|
||||||
converters.add(new ByteArrayHttpMessageConverter());
|
converters.add(new ByteArrayHttpMessageConverter());
|
||||||
converters.add(new StringHttpMessageConverter());
|
|
||||||
converters.add(new ResourceHttpMessageConverter());
|
converters.add(new ResourceHttpMessageConverter());
|
||||||
converters.add(new AllEncompassingFormHttpMessageConverter());
|
|
||||||
|
// 通用转换器
|
||||||
converters.add(new StringHttpMessageConverter());
|
converters.add(new StringHttpMessageConverter());
|
||||||
|
converters.add(new AllEncompassingFormHttpMessageConverter());
|
||||||
|
|
||||||
|
// JSON 转换器
|
||||||
converters.add(jackson2HttpMessageConverter());
|
converters.add(jackson2HttpMessageConverter());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +63,29 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
// 忽略未知属性
|
// 忽略未知属性
|
||||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
// 日期格式转换
|
// 设置时区
|
||||||
// mapper.setDateFormat(new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN));
|
|
||||||
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
|
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
|
||||||
|
|
||||||
|
// 配置Java8日期时间序列化
|
||||||
|
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||||
|
javaTimeModule.addSerializer(java.time.LocalDateTime.class, new LocalDateTimeSerializer(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
|
||||||
|
javaTimeModule.addSerializer(java.time.LocalDate.class, new LocalDateSerializer(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
|
||||||
|
javaTimeModule.addSerializer(java.time.LocalTime.class,
|
||||||
|
new LocalTimeSerializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||||
|
javaTimeModule.addDeserializer(java.time.LocalDateTime.class, new LocalDateTimeDeserializer(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
|
||||||
|
javaTimeModule.addDeserializer(java.time.LocalDate.class, new LocalDateDeserializer(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
|
||||||
|
javaTimeModule.addDeserializer(java.time.LocalTime.class,
|
||||||
|
new LocalTimeDeserializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||||
|
mapper.registerModule(javaTimeModule);
|
||||||
|
|
||||||
|
// 配置java.util.Date的序列化和反序列化
|
||||||
|
SimpleDateFormat dateFormat = new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN);
|
||||||
|
mapper.setDateFormat(dateFormat);
|
||||||
|
|
||||||
// Long类型转String类型
|
// Long类型转String类型
|
||||||
SimpleModule simpleModule = new SimpleModule();
|
SimpleModule simpleModule = new SimpleModule();
|
||||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package xiaozhi.modules.security.controller;
|
package xiaozhi.modules.security.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -13,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.TokenDTO;
|
import xiaozhi.common.page.TokenDTO;
|
||||||
@@ -75,6 +78,9 @@ public class LoginController {
|
|||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@Operation(summary = "注册")
|
@Operation(summary = "注册")
|
||||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||||
|
if (!sysUserService.getAllowUserRegister()) {
|
||||||
|
throw new RenException("当前不允许普通用户注册");
|
||||||
|
}
|
||||||
// 验证是否正确输入验证码
|
// 验证是否正确输入验证码
|
||||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
@@ -111,4 +117,13 @@ public class LoginController {
|
|||||||
sysUserTokenService.changePassword(userId, passwordDTO);
|
sysUserTokenService.changePassword(userId, passwordDTO);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pub-config")
|
||||||
|
@Operation(summary = "公共配置")
|
||||||
|
public Result<Map<String, Object>> pubConfig() {
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.put("version", Constant.VERSION);
|
||||||
|
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||||
|
return new Result<Map<String, Object>>().ok(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package xiaozhi.modules.security.secret;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.utils.HttpContextUtils;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config API 过滤器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ServerSecretFilter extends AuthenticatingFilter {
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) {
|
||||||
|
// 获取请求token
|
||||||
|
String token = getRequestToken((HttpServletRequest) request);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(token)) {
|
||||||
|
log.warn("createToken:token is empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServerSecretToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
||||||
|
// 对OPTIONS请求放行
|
||||||
|
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
|
||||||
|
// 获取token并校验
|
||||||
|
String token = getRequestToken((HttpServletRequest) servletRequest);
|
||||||
|
if (StringUtils.isBlank(token)) {
|
||||||
|
// token为空,返回401
|
||||||
|
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "服务器密钥不能为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token是否匹配
|
||||||
|
String serverSecret = getServerSecret();
|
||||||
|
if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) {
|
||||||
|
// token无效,返回401
|
||||||
|
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的服务器密钥");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送未授权响应
|
||||||
|
*/
|
||||||
|
private void sendUnauthorizedResponse(HttpServletResponse response, String message) {
|
||||||
|
response.setContentType("application/json;charset=utf-8");
|
||||||
|
response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
|
response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = JsonUtils.toJsonString(new Result<Void>().error(ErrorCode.UNAUTHORIZED, message));
|
||||||
|
response.getWriter().print(json);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("响应输出失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求的token
|
||||||
|
*/
|
||||||
|
private String getRequestToken(HttpServletRequest httpRequest) {
|
||||||
|
String token = null;
|
||||||
|
// 从header中获取token
|
||||||
|
String authorization = httpRequest.getHeader("Authorization");
|
||||||
|
if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
|
||||||
|
token = authorization.replace("Bearer ", "");
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getServerSecret() {
|
||||||
|
return sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package xiaozhi.modules.security.secret;
|
||||||
|
|
||||||
|
import org.apache.shiro.authc.AuthenticationToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config API Token
|
||||||
|
*/
|
||||||
|
public class ServerSecretToken implements AuthenticationToken {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final String token;
|
||||||
|
|
||||||
|
public ServerSecretToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,6 @@ public class AdminController {
|
|||||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||||
dto.setPage((String) params.get(Constant.PAGE));
|
dto.setPage((String) params.get(Constant.PAGE));
|
||||||
ValidatorUtils.validateEntity(dto);
|
ValidatorUtils.validateEntity(dto);
|
||||||
ValidatorUtils.validateEntity(dto);
|
|
||||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||||
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典数据管理
|
||||||
|
*
|
||||||
|
* @author czc
|
||||||
|
* @since 2025-04-30
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/dict/data")
|
||||||
|
@Tag(name = "字典数据管理")
|
||||||
|
public class SysDictDataController {
|
||||||
|
private final SysDictDataService sysDictDataService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "分页查询字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({ @Parameter(name = "dictTypeId", description = "字典类型ID", required = true),
|
||||||
|
@Parameter(name = "dictLabel", description = "数据标签"), @Parameter(name = "dictValue", description = "数据值"),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
|
||||||
|
public Result<PageData<SysDictDataVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
// 强制校验dictTypeId是否存在
|
||||||
|
if (!params.containsKey("dictTypeId") || StringUtils.isEmpty(String.valueOf(params.get("dictTypeId")))) {
|
||||||
|
return new Result<PageData<SysDictDataVO>>().error("dictTypeId不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
PageData<SysDictDataVO> page = sysDictDataService.page(params);
|
||||||
|
return new Result<PageData<SysDictDataVO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取字典数据详情")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<SysDictDataVO> get(@PathVariable("id") Long id) {
|
||||||
|
SysDictDataVO vo = sysDictDataService.get(id);
|
||||||
|
return new Result<SysDictDataVO>().ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@Operation(summary = "新增字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody SysDictDataDTO dto) {
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
sysDictDataService.save(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "修改字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> update(@RequestBody SysDictDataDTO dto) {
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
sysDictDataService.update(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@Operation(summary = "删除字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||||
|
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||||
|
sysDictDataService.delete(ids);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/type/{dictType}")
|
||||||
|
@Operation(summary = "获取字典数据列表")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<SysDictDataItem>> getDictDataByType(@PathVariable("dictType") String dictType) {
|
||||||
|
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(dictType);
|
||||||
|
return new Result<List<SysDictDataItem>>().ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典类型管理
|
||||||
|
*
|
||||||
|
* @author czc
|
||||||
|
* @since 2025-04-30
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/dict/type")
|
||||||
|
@Tag(name = "字典类型管理")
|
||||||
|
public class SysDictTypeController {
|
||||||
|
private final SysDictTypeService sysDictTypeService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "分页查询字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({ @Parameter(name = "dictType", description = "字典类型编码"),
|
||||||
|
@Parameter(name = "dictName", description = "字典类型名称"),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
|
||||||
|
public Result<PageData<SysDictTypeVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
PageData<SysDictTypeVO> page = sysDictTypeService.page(params);
|
||||||
|
return new Result<PageData<SysDictTypeVO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取字典类型详情")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<SysDictTypeVO> get(@PathVariable("id") Long id) {
|
||||||
|
SysDictTypeVO vo = sysDictTypeService.get(id);
|
||||||
|
return new Result<SysDictTypeVO>().ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@Operation(summary = "保存字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody SysDictTypeDTO dto) {
|
||||||
|
// 参数校验
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
|
||||||
|
sysDictTypeService.save(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "修改字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> update(@RequestBody SysDictTypeDTO dto) {
|
||||||
|
// 参数校验
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
|
||||||
|
sysDictTypeService.update(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@Operation(summary = "删除字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||||
|
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||||
|
sysDictTypeService.delete(ids);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ package xiaozhi.modules.sys.controller;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -11,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@@ -20,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.annotation.LogOperation;
|
import xiaozhi.common.annotation.LogOperation;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.AssertUtils;
|
import xiaozhi.common.validator.AssertUtils;
|
||||||
@@ -27,8 +32,10 @@ import xiaozhi.common.validator.ValidatorUtils;
|
|||||||
import xiaozhi.common.validator.group.AddGroup;
|
import xiaozhi.common.validator.group.AddGroup;
|
||||||
import xiaozhi.common.validator.group.DefaultGroup;
|
import xiaozhi.common.validator.group.DefaultGroup;
|
||||||
import xiaozhi.common.validator.group.UpdateGroup;
|
import xiaozhi.common.validator.group.UpdateGroup;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
import xiaozhi.modules.sys.utils.WebSocketValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 参数管理
|
* 参数管理
|
||||||
@@ -42,6 +49,8 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class SysParamsController {
|
public class SysParamsController {
|
||||||
private final SysParamsService sysParamsService;
|
private final SysParamsService sysParamsService;
|
||||||
|
private final ConfigService configService;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
@GetMapping("page")
|
@GetMapping("page")
|
||||||
@Operation(summary = "分页")
|
@Operation(summary = "分页")
|
||||||
@@ -77,7 +86,7 @@ public class SysParamsController {
|
|||||||
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
sysParamsService.save(dto);
|
sysParamsService.save(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +98,91 @@ public class SysParamsController {
|
|||||||
// 效验数据
|
// 效验数据
|
||||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
sysParamsService.update(dto);
|
// 验证WebSocket地址列表
|
||||||
|
validateWebSocketUrls(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
|
// 验证OTA地址
|
||||||
|
validateOtaUrl(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
|
sysParamsService.update(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证WebSocket地址列表
|
||||||
|
*
|
||||||
|
* @param urls WebSocket地址列表,以分号分隔
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
private void validateWebSocketUrls(String paramCode, String urls) {
|
||||||
|
if (!paramCode.equals(Constant.SERVER_WEBSOCKET)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] wsUrls = urls.split("\\;");
|
||||||
|
if (wsUrls.length == 0) {
|
||||||
|
throw new RenException("WebSocket地址列表不能为空");
|
||||||
|
}
|
||||||
|
for (String url : wsUrls) {
|
||||||
|
if (StringUtils.isNotBlank(url)) {
|
||||||
|
// 检查是否包含localhost或127.0.0.1
|
||||||
|
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||||
|
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证WebSocket地址格式
|
||||||
|
if (!WebSocketValidator.validateUrlFormat(url)) {
|
||||||
|
throw new RenException("WebSocket地址格式不正确: " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试WebSocket连接
|
||||||
|
if (!WebSocketValidator.testConnection(url)) {
|
||||||
|
throw new RenException("WebSocket连接测试失败: " + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证OTA地址
|
||||||
|
*/
|
||||||
|
private void validateOtaUrl(String paramCode, String url) {
|
||||||
|
if (!paramCode.equals(Constant.SERVER_OTA)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||||
|
throw new RenException("OTA地址不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含localhost或127.0.0.1
|
||||||
|
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||||
|
throw new RenException("OTA地址不能使用localhost或127.0.0.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证URL格式
|
||||||
|
if (!url.toLowerCase().startsWith("http")) {
|
||||||
|
throw new RenException("OTA地址必须以http或https开头");
|
||||||
|
}
|
||||||
|
if (!url.endsWith("/ota/")) {
|
||||||
|
throw new RenException("OTA地址必须以/ota/结尾");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 发送GET请求
|
||||||
|
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||||
|
if (response.getStatusCode() != HttpStatus.OK) {
|
||||||
|
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode());
|
||||||
|
}
|
||||||
|
// 检查响应内容是否包含OTA相关信息
|
||||||
|
String body = response.getBody();
|
||||||
|
if (body == null || !body.contains("OTA")) {
|
||||||
|
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RenException("OTA接口验证失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/delete")
|
@PostMapping("/delete")
|
||||||
@Operation(summary = "删除")
|
@Operation(summary = "删除")
|
||||||
@LogOperation("删除")
|
@LogOperation("删除")
|
||||||
@@ -103,7 +192,7 @@ public class SysParamsController {
|
|||||||
AssertUtils.isArrayEmpty(ids, "id");
|
AssertUtils.isArrayEmpty(ids, "id");
|
||||||
|
|
||||||
sysParamsService.delete(ids);
|
sysParamsService.delete(ids);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import java.util.List;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
|
||||||
import xiaozhi.modules.sys.entity.DictData;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典数据
|
* 字典数据
|
||||||
@@ -15,10 +14,13 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
||||||
|
|
||||||
/**
|
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||||
* 字典数据列表
|
|
||||||
*/
|
|
||||||
List<DictData> getDictDataList();
|
|
||||||
|
|
||||||
List<SysDictDataDTO> getDataByTypeCode(String dictType);
|
/**
|
||||||
|
* 根据字典类型ID获取字典类型编码
|
||||||
|
*
|
||||||
|
* @param dictTypeId 字典类型ID
|
||||||
|
* @return 字典类型编码
|
||||||
|
*/
|
||||||
|
String getTypeByTypeId(Long dictTypeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package xiaozhi.modules.sys.dao;
|
package xiaozhi.modules.sys.dao;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.sys.entity.DictType;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,9 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> {
|
public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> {
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典类型列表
|
|
||||||
*/
|
|
||||||
List<DictType> getDictTypeList();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package xiaozhi.modules.sys.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Schema(description = "配置密钥DTO")
|
|
||||||
public class ConfigSecretDTO {
|
|
||||||
@Schema(description = "密钥")
|
|
||||||
@NotBlank(message = "密钥不能为空")
|
|
||||||
private String secret;
|
|
||||||
}
|
|
||||||
@@ -51,5 +51,6 @@ public class SysDictTypeDTO implements Serializable {
|
|||||||
|
|
||||||
@Schema(description = "更新时间")
|
@Schema(description = "更新时间")
|
||||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||||
private Date updateDate;
|
private Date updateDate;
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package xiaozhi.modules.sys.entity;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典数据
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class DictData {
|
|
||||||
@JsonIgnore
|
|
||||||
private Long dictTypeId;
|
|
||||||
private String dictLabel;
|
|
||||||
private String dictValue;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package xiaozhi.modules.sys.entity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典类型
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class DictType {
|
|
||||||
@JsonIgnore
|
|
||||||
private Long id;
|
|
||||||
private String dictType;
|
|
||||||
private List<DictData> dataList = new ArrayList<>();
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ import xiaozhi.common.entity.BaseEntity;
|
|||||||
@TableName("sys_dict_type")
|
@TableName("sys_dict_type")
|
||||||
public class SysDictTypeEntity extends BaseEntity {
|
public class SysDictTypeEntity extends BaseEntity {
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型编码
|
||||||
*/
|
*/
|
||||||
private String dictType;
|
private String dictType;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,25 +1,70 @@
|
|||||||
package xiaozhi.modules.sys.service;
|
package xiaozhi.modules.sys.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据字典
|
* 数据字典
|
||||||
*/
|
*/
|
||||||
public interface SysDictDataService extends BaseService<SysDictDataEntity> {
|
public interface SysDictDataService extends BaseService<SysDictDataEntity> {
|
||||||
|
|
||||||
PageData<SysDictDataDTO> page(Map<String, Object> params);
|
/**
|
||||||
|
* 分页查询数据字典信息
|
||||||
|
*
|
||||||
|
* @param params 查询参数,包含分页信息和查询条件
|
||||||
|
* @return 返回数据字典的分页查询结果
|
||||||
|
*/
|
||||||
|
PageData<SysDictDataVO> page(Map<String, Object> params);
|
||||||
|
|
||||||
SysDictDataDTO get(Long id);
|
/**
|
||||||
|
* 根据ID获取数据字典实体
|
||||||
|
*
|
||||||
|
* @param id 数据字典实体的唯一标识
|
||||||
|
* @return 返回数据字典实体的详细信息
|
||||||
|
*/
|
||||||
|
SysDictDataVO get(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存新的数据字典项
|
||||||
|
*
|
||||||
|
* @param dto 数据字典项的保存数据传输对象
|
||||||
|
*/
|
||||||
void save(SysDictDataDTO dto);
|
void save(SysDictDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据字典项
|
||||||
|
*
|
||||||
|
* @param dto 数据字典项的更新数据传输对象
|
||||||
|
*/
|
||||||
void update(SysDictDataDTO dto);
|
void update(SysDictDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据字典项
|
||||||
|
*
|
||||||
|
* @param ids 要删除的数据字典项的ID数组
|
||||||
|
*/
|
||||||
void delete(Long[] ids);
|
void delete(Long[] ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典类型ID删除对应的字典数据
|
||||||
|
*
|
||||||
|
* @param dictTypeId 字典类型ID
|
||||||
|
*/
|
||||||
|
void deleteByTypeId(Long dictTypeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典类型获取字典数据列表
|
||||||
|
*
|
||||||
|
* @param dictType 字典类型
|
||||||
|
* @return 返回字典数据列表
|
||||||
|
*/
|
||||||
|
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,25 +6,55 @@ import java.util.Map;
|
|||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||||
import xiaozhi.modules.sys.entity.DictType;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据字典
|
* 数据字典
|
||||||
*/
|
*/
|
||||||
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
||||||
|
|
||||||
PageData<SysDictTypeDTO> page(Map<String, Object> params);
|
/**
|
||||||
|
* 分页查询字典类型信息
|
||||||
|
*
|
||||||
|
* @param params 查询参数,包含分页信息和查询条件
|
||||||
|
* @return 返回分页的字典类型数据
|
||||||
|
*/
|
||||||
|
PageData<SysDictTypeVO> page(Map<String, Object> params);
|
||||||
|
|
||||||
SysDictTypeDTO get(Long id);
|
/**
|
||||||
|
* 根据ID获取字典类型信息
|
||||||
|
*
|
||||||
|
* @param id 字典类型ID
|
||||||
|
* @return 返回字典类型对象
|
||||||
|
*/
|
||||||
|
SysDictTypeVO get(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存字典类型信息
|
||||||
|
*
|
||||||
|
* @param dto 字典类型数据传输对象
|
||||||
|
*/
|
||||||
void save(SysDictTypeDTO dto);
|
void save(SysDictTypeDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新字典类型信息
|
||||||
|
*
|
||||||
|
* @param dto 字典类型数据传输对象
|
||||||
|
*/
|
||||||
void update(SysDictTypeDTO dto);
|
void update(SysDictTypeDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除字典类型信息
|
||||||
|
*
|
||||||
|
* @param ids 要删除的字典类型ID数组
|
||||||
|
*/
|
||||||
void delete(Long[] ids);
|
void delete(Long[] ids);
|
||||||
|
|
||||||
List<DictType> getAllList();
|
/**
|
||||||
|
* 列出所有字典类型信息
|
||||||
List<DictType> getDictTypeList();
|
*
|
||||||
|
* @return 返回字典类型列表
|
||||||
|
*/
|
||||||
|
List<SysDictTypeVO> list(Map<String, Object> params);
|
||||||
}
|
}
|
||||||
@@ -65,4 +65,11 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
|||||||
* @param userIds 用户ID数组
|
* @param userIds 用户ID数组
|
||||||
*/
|
*/
|
||||||
void changeStatus(Integer status, String[] userIds);
|
void changeStatus(Integer status, String[] userIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否允许用户注册
|
||||||
|
*
|
||||||
|
* @return 是否允许用户注册
|
||||||
|
*/
|
||||||
|
boolean getAllowUserRegister();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,55 @@
|
|||||||
package xiaozhi.modules.sys.service.impl;
|
package xiaozhi.modules.sys.service.impl;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||||
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
|
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
|
||||||
implements SysDictDataService {
|
implements SysDictDataService {
|
||||||
|
private final SysUserDao sysUserDao;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<SysDictDataDTO> page(Map<String, Object> params) {
|
public PageData<SysDictDataVO> page(Map<String, Object> params) {
|
||||||
IPage<SysDictDataEntity> page = baseDao.selectPage(
|
IPage<SysDictDataEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
|
||||||
getPage(params, "sort", true),
|
|
||||||
getWrapper(params));
|
|
||||||
|
|
||||||
return getPageData(page, SysDictDataDTO.class);
|
PageData<SysDictDataVO> pageData = getPageData(page, SysDictDataVO.class);
|
||||||
|
|
||||||
|
setUserName(pageData.getList());
|
||||||
|
|
||||||
|
return pageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) {
|
private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) {
|
||||||
@@ -48,32 +66,118 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysDictDataDTO get(Long id) {
|
public SysDictDataVO get(Long id) {
|
||||||
SysDictDataEntity entity = baseDao.selectById(id);
|
SysDictDataEntity entity = baseDao.selectById(id);
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entity, SysDictDataDTO.class);
|
return ConvertUtils.sourceToTarget(entity, SysDictDataVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void save(SysDictDataDTO dto) {
|
public void save(SysDictDataDTO dto) {
|
||||||
|
// 相同字典类型的标签不能相同
|
||||||
|
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), null);
|
||||||
|
|
||||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||||
|
|
||||||
insert(entity);
|
insert(entity);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void update(SysDictDataDTO dto) {
|
public void update(SysDictDataDTO dto) {
|
||||||
|
// 相同字典类型的标签不能相同
|
||||||
|
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), String.valueOf(dto.getId()));
|
||||||
|
|
||||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||||
|
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void delete(Long[] ids) {
|
||||||
// 删除
|
for (Long id : ids) {
|
||||||
deleteBatchIds(Arrays.asList(ids));
|
SysDictDataEntity entity = baseDao.selectById(id);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(entity.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
|
// 删除
|
||||||
|
deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteByTypeId(Long dictTypeId) {
|
||||||
|
LambdaQueryWrapper<SysDictDataEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId);
|
||||||
|
baseDao.delete(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户名
|
||||||
|
*
|
||||||
|
* @param sysDictDataList 字典类型集合
|
||||||
|
*/
|
||||||
|
private void setUserName(List<SysDictDataVO> sysDictDataList) {
|
||||||
|
// 收集所有用户 ID
|
||||||
|
Set<Long> userIds = sysDictDataList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 设置更新者和创建者名称
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||||
|
// 把List转成Map,Map<Long, String>
|
||||||
|
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||||
|
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||||
|
|
||||||
|
sysDictDataList.forEach(vo -> {
|
||||||
|
vo.setCreatorName(userNameMap.get(vo.getCreator()));
|
||||||
|
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDictValueUnique(Long dictTypeId, String dictValue, String excludeId) {
|
||||||
|
LambdaQueryWrapper<SysDictDataEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId).eq(SysDictDataEntity::getDictLabel, dictValue);
|
||||||
|
if (StringUtils.isNotBlank(excludeId)) {
|
||||||
|
queryWrapper.ne(SysDictDataEntity::getId, excludeId);
|
||||||
|
}
|
||||||
|
boolean exists = baseDao.exists(queryWrapper);
|
||||||
|
if (exists) {
|
||||||
|
throw new RenException("字典标签重复");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SysDictDataItem> getDictDataByType(String dictType) {
|
||||||
|
if (StringUtils.isBlank(dictType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先从Redis获取缓存
|
||||||
|
String key = RedisKeys.getDictDataByTypeKey(dictType);
|
||||||
|
List<SysDictDataItem> cachedData = (List<SysDictDataItem>) redisUtils.get(key);
|
||||||
|
if (cachedData != null) {
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果缓存中没有,则从数据库获取
|
||||||
|
List<SysDictDataItem> data = baseDao.getDictDataByType(dictType);
|
||||||
|
|
||||||
|
// 存入Redis缓存
|
||||||
|
if (data != null) {
|
||||||
|
redisUtils.set(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,42 +3,52 @@ package xiaozhi.modules.sys.service.impl;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
|
||||||
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
||||||
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||||
import xiaozhi.modules.sys.entity.DictData;
|
|
||||||
import xiaozhi.modules.sys.entity.DictType;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||||
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
import xiaozhi.modules.sys.service.SysDictTypeService;
|
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型
|
||||||
*/
|
*/
|
||||||
@AllArgsConstructor
|
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
|
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
|
||||||
implements SysDictTypeService {
|
implements SysDictTypeService {
|
||||||
private final SysDictDataDao sysDictDataDao;
|
private final SysUserDao sysUserDao;
|
||||||
|
private final SysDictDataService sysDictDataService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<SysDictTypeDTO> page(Map<String, Object> params) {
|
public PageData<SysDictTypeVO> page(Map<String, Object> params) {
|
||||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(
|
IPage<SysDictTypeEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
|
||||||
getPage(params, "sort", true),
|
|
||||||
getWrapper(params));
|
|
||||||
|
|
||||||
return getPageData(page, SysDictTypeDTO.class);
|
PageData<SysDictTypeVO> pageData = getPageData(page, SysDictTypeVO.class);
|
||||||
|
|
||||||
|
setUserName(pageData.getList());
|
||||||
|
|
||||||
|
return pageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
||||||
@@ -53,15 +63,21 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysDictTypeDTO get(Long id) {
|
public SysDictTypeVO get(Long id) {
|
||||||
SysDictTypeEntity entity = baseDao.selectById(id);
|
SysDictTypeEntity entity = baseDao.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new RenException("字典类型不存在");
|
||||||
|
}
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class);
|
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void save(SysDictTypeDTO dto) {
|
public void save(SysDictTypeDTO dto) {
|
||||||
|
// 字典类型编码不能重复
|
||||||
|
checkDictTypeUnique(dto.getDictType(), null);
|
||||||
|
|
||||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||||
|
|
||||||
insert(entity);
|
insert(entity);
|
||||||
@@ -70,6 +86,9 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void update(SysDictTypeDTO dto) {
|
public void update(SysDictTypeDTO dto) {
|
||||||
|
// 字典类型编码不能重复
|
||||||
|
checkDictTypeUnique(dto.getDictType(), String.valueOf(dto.getId()));
|
||||||
|
|
||||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||||
|
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
@@ -78,27 +97,57 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void delete(Long[] ids) {
|
||||||
// 删除
|
// 先删除对应的字典数据
|
||||||
|
for (Long id : ids) {
|
||||||
|
sysDictDataService.deleteByTypeId(id);
|
||||||
|
}
|
||||||
|
// 再删除字典类型
|
||||||
deleteBatchIds(Arrays.asList(ids));
|
deleteBatchIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<DictType> getAllList() {
|
public List<SysDictTypeVO> list(Map<String, Object> params) {
|
||||||
List<DictType> typeList = baseDao.getDictTypeList();
|
List<SysDictTypeEntity> entityList = baseDao.selectList(getWrapper(params));
|
||||||
List<DictData> dataList = sysDictDataDao.getDictDataList();
|
List<SysDictTypeVO> sysDictTypeVOList = ConvertUtils.sourceToTarget(entityList, SysDictTypeVO.class);
|
||||||
for (DictType type : typeList) {
|
// 设置用户名
|
||||||
for (DictData data : dataList) {
|
setUserName(sysDictTypeVOList);
|
||||||
if (type.getId().equals(data.getDictTypeId())) {
|
|
||||||
type.getDataList().add(data);
|
return sysDictTypeVOList;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 设置用户名
|
||||||
|
*
|
||||||
|
* @param sysDictTypeList 字典类型集合
|
||||||
|
*/
|
||||||
|
private void setUserName(List<SysDictTypeVO> sysDictTypeList) {
|
||||||
|
// 收集所有用户 ID
|
||||||
|
Set<Long> userIds = sysDictTypeList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 设置更新者和创建者名称
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||||
|
// 把List转成Map,Map<Long, String>
|
||||||
|
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||||
|
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||||
|
|
||||||
|
sysDictTypeList.forEach(vo -> {
|
||||||
|
vo.setCreatorName(userNameMap.get(vo.getCreator()));
|
||||||
|
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return typeList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void checkDictTypeUnique(String dictType, String excludeId) {
|
||||||
public List<DictType> getDictTypeList() {
|
LambdaQueryWrapper<SysDictTypeEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
return baseDao.getDictTypeList();
|
queryWrapper.eq(SysDictTypeEntity::getDictType, dictType);
|
||||||
|
if (StringUtils.isNotBlank(excludeId)) {
|
||||||
|
queryWrapper.ne(SysDictTypeEntity::getId, excludeId);
|
||||||
|
}
|
||||||
|
boolean exists = baseDao.exists(queryWrapper);
|
||||||
|
if (exists) {
|
||||||
|
throw new RenException("字典类型编码重复");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ import xiaozhi.modules.sys.dto.PasswordDTO;
|
|||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserService;
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||||
|
|
||||||
@@ -45,6 +46,8 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
|
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
|
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysUserDTO getByUsername(String username) {
|
public SysUserDTO getByUsername(String username) {
|
||||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||||
@@ -166,6 +169,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
|
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
|
||||||
adminPageUserVO.setDeviceCount(deviceCount);
|
adminPageUserVO.setDeviceCount(deviceCount);
|
||||||
adminPageUserVO.setStatus(user.getStatus());
|
adminPageUserVO.setStatus(user.getStatus());
|
||||||
|
adminPageUserVO.setCreateDate(user.getCreateDate());
|
||||||
return adminPageUserVO;
|
return adminPageUserVO;
|
||||||
}).toList();
|
}).toList();
|
||||||
return new PageData<>(list, page.getTotal());
|
return new PageData<>(list, page.getTotal());
|
||||||
@@ -205,4 +209,17 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
updateById(entity);
|
updateById(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getAllowUserRegister() {
|
||||||
|
String allowUserRegister = sysParamsService.getValue(Constant.SERVER_ALLOW_USER_REGISTER, true);
|
||||||
|
if (allowUserRegister.equals("true")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Long userCount = baseDao.selectCount(new QueryWrapper<SysUserEntity>());
|
||||||
|
if (userCount == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ public class SysUserUtilServiceImpl extends BaseServiceImpl<SysUserDao, SysUserE
|
|||||||
@Override
|
@Override
|
||||||
public void assignUsername(Long userId, Consumer<String> setter) {
|
public void assignUsername(Long userId, Consumer<String> setter) {
|
||||||
String userIdKey = RedisKeys.getUserIdKey(userId);
|
String userIdKey = RedisKeys.getUserIdKey(userId);
|
||||||
String username = redisUtils.get(userIdKey).toString();
|
|
||||||
|
Object value = redisUtils.get(userIdKey);
|
||||||
|
String username = (value != null) ? value.toString() : null;
|
||||||
if(username != null){
|
if(username != null){
|
||||||
setter.accept(username);
|
setter.accept(username);
|
||||||
}else {
|
}else {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package xiaozhi.modules.sys.utils;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.WebSocketMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
public class WebSocketTestHandler implements WebSocketHandler {
|
||||||
|
private final CompletableFuture<Boolean> future;
|
||||||
|
|
||||||
|
public WebSocketTestHandler(CompletableFuture<Boolean> future) {
|
||||||
|
this.future = future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(WebSocketSession session) {
|
||||||
|
future.complete(true);
|
||||||
|
try {
|
||||||
|
session.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略关闭异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||||
|
// 不需要处理消息
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||||
|
future.complete(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
|
||||||
|
// 连接关闭时不做任何处理
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsPartialMessages() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package xiaozhi.modules.sys.utils;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||||
|
import org.springframework.web.socket.client.WebSocketClient;
|
||||||
|
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||||
|
|
||||||
|
public class WebSocketValidator {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(WebSocketValidator.class);
|
||||||
|
|
||||||
|
// WebSocket URL正则表达式
|
||||||
|
private static final Pattern WS_URL_PATTERN = Pattern
|
||||||
|
.compile("^wss?://[\\w.-]+(?:\\.[\\w.-]+)*(?::\\d+)?(?:/[\\w.-]*)*$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证WebSocket地址格式
|
||||||
|
*
|
||||||
|
* @param url WebSocket地址
|
||||||
|
* @return 是否有效
|
||||||
|
*/
|
||||||
|
public static boolean validateUrlFormat(String url) {
|
||||||
|
if (StringUtils.isBlank(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return WS_URL_PATTERN.matcher(url.trim()).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试WebSocket连接
|
||||||
|
*
|
||||||
|
* @param url WebSocket地址
|
||||||
|
* @return 是否可连接
|
||||||
|
*/
|
||||||
|
public static boolean testConnection(String url) {
|
||||||
|
if (!validateUrlFormat(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WebSocketClient client = new StandardWebSocketClient();
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||||
|
|
||||||
|
client.execute(new WebSocketTestHandler(future), headers, URI.create(url));
|
||||||
|
|
||||||
|
// 等待最多5秒获取连接结果
|
||||||
|
return future.get(5, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("WebSocket连接测试失败: {}", url, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package xiaozhi.modules.sys.vo;
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -23,4 +25,7 @@ public class AdminPageUserVO {
|
|||||||
|
|
||||||
@Schema(description = "用户id")
|
@Schema(description = "用户id")
|
||||||
private String userid;
|
private String userid;
|
||||||
|
|
||||||
|
@Schema(description = "注册时间")
|
||||||
|
private Date createDate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典数据VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "字典数据项")
|
||||||
|
public class SysDictDataItem implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "字典标签")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "字典值")
|
||||||
|
private String key;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典数据VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "字典数据VO")
|
||||||
|
public class SysDictDataVO implements Serializable {
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "字典类型ID")
|
||||||
|
private Long dictTypeId;
|
||||||
|
|
||||||
|
@Schema(description = "字典标签")
|
||||||
|
private String dictLabel;
|
||||||
|
|
||||||
|
@Schema(description = "字典值")
|
||||||
|
private String dictValue;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建者名称")
|
||||||
|
private String creatorName;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createDate;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新者名称")
|
||||||
|
private String updaterName;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典类型VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "字典类型VO")
|
||||||
|
public class SysDictTypeVO implements Serializable {
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "字典类型")
|
||||||
|
private String dictType;
|
||||||
|
|
||||||
|
@Schema(description = "字典名称")
|
||||||
|
private String dictName;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建者名称")
|
||||||
|
private String creatorName;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createDate;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新者名称")
|
||||||
|
private String updaterName;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ public class TimbreDataDTO {
|
|||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
@NotBlank(message = "{timbre.remark.require}")
|
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
@Schema(description = "排序")
|
@Schema(description = "排序")
|
||||||
@@ -40,6 +39,5 @@ public class TimbreDataDTO {
|
|||||||
private String ttsVoice;
|
private String ttsVoice;
|
||||||
|
|
||||||
@Schema(description = "音频播放地址")
|
@Schema(description = "音频播放地址")
|
||||||
@NotBlank(message = "{timbre.voiceDemo.require}")
|
|
||||||
private String voiceDemo;
|
private String voiceDemo;
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,3 @@ mybatis-plus:
|
|||||||
prefix:
|
prefix:
|
||||||
blobType: BLOB
|
blobType: BLOB
|
||||||
boolValue: TRUE
|
boolValue: TRUE
|
||||||
|
|
||||||
app:
|
|
||||||
fronted-url: http://localhost:8001
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- 对0.3.0版本之前的参数进行修改
|
|
||||||
update `sys_params` set param_value = '.mp3;.wav;.p3' where param_code = 'plugins.play_music.music_ext';
|
|
||||||
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm';
|
|
||||||
@@ -16,3 +16,13 @@ INSERT INTO `ai_tts_voice` VALUES
|
|||||||
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 增加是否允许用户注册参数
|
||||||
|
delete from `sys_params` where id in (103,104);
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (103, 'server.allow_user_register', 'false', 'boolean', 1, '是否运行管理员以外的人注册');
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (104, 'server.fronted_url', 'http://xiaozhi.server.com', 'string', 1, '下发六位验证码时显示的控制面板地址');
|
||||||
|
|
||||||
|
-- 修正CosyVoiceSiliconflow音色
|
||||||
|
delete from `ai_tts_voice` where tts_model_id = 'TTS_CosyVoiceSiliconflow';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'FunAudioLLM/CosyVoice2-0.5B:alex', '中文', 'https://example.com/cosyvoice/alex.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0002', 'TTS_CosyVoiceSiliconflow', 'CosyVoice女声', 'FunAudioLLM/CosyVoice2-0.5B:bella', '中文', 'https://example.com/cosyvoice/bella.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 调整意图识别配置
|
||||||
|
delete from `ai_model_config` where id = 'Intent_function_call';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": \"change_role;get_weather;get_news;play_music\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 增加单台设备每天最多聊天句数
|
||||||
|
delete from `sys_params` where id = 105;
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (105, 'device_max_output_size', '0', 'number', 1, '单台设备每天最多输出字数,0表示不限制');
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- 删除无用模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_LLM_doubao';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_LLM_chatglm';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_302ai';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_gizwits';
|
||||||
|
|
||||||
|
-- 添加模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_ASR_TencentASR';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_ASR_TencentASR', 'ASR', 'tencent', '腾讯语音识别', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 4, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
-- 添加腾讯语音合成模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_TencentTTS';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_TTS_TencentTTS', 'TTS', 'tencent', '腾讯语音合成', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"region","label":"区域","type":"string"},{"key":"voice","label":"音色ID","type":"string"}]', 5, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
|
||||||
|
-- 添加edge音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_EdgeTTS0001', 'TTS_EdgeTTS0002', 'TTS_EdgeTTS0003', 'TTS_EdgeTTS0004', 'TTS_EdgeTTS0005', 'TTS_EdgeTTS0006', 'TTS_EdgeTTS0007', 'TTS_EdgeTTS0008', 'TTS_EdgeTTS0009', 'TTS_EdgeTTS0010', 'TTS_EdgeTTS0011');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES
|
||||||
|
('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声-晓晓', 'zh-CN-XiaoxiaoNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0002', 'TTS_EdgeTTS', 'EdgeTTS男声-云扬', 'zh-CN-YunyangNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0003', 'TTS_EdgeTTS', 'EdgeTTS女声-晓伊', 'zh-CN-XiaoyiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0004', 'TTS_EdgeTTS', 'EdgeTTS男声-云健', 'zh-CN-YunjianNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0005', 'TTS_EdgeTTS', 'EdgeTTS男声-云希', 'zh-CN-YunxiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0006', 'TTS_EdgeTTS', 'EdgeTTS男声-云夏', 'zh-CN-YunxiaNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0007', 'TTS_EdgeTTS', 'EdgeTTS女声-辽宁小贝', 'zh-CN-liaoning-XiaobeiNeural', '辽宁', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0008', 'TTS_EdgeTTS', 'EdgeTTS女声-陕西小妮', 'zh-CN-shaanxi-XiaoniNeural', '陕西', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- DoubaoTTS音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS0002', 'TTS_DoubaoTTS0003', 'TTS_DoubaoTTS0004', 'TTS_DoubaoTTS0005');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS', '通用女声', 'BV001_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV001.mp3', NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0002', 'TTS_DoubaoTTS', '通用男声', 'BV002_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV002.mp3', NULL, 2, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0003', 'TTS_DoubaoTTS', '阳光男生', 'BV056_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV056.mp3', NULL, 4, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0004', 'TTS_DoubaoTTS', '奶气萌娃', 'BV051_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV051.mp3', NULL, 5, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0005', 'TTS_DoubaoTTS', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '普通话', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 修正CosyVoiceSiliconflow音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow0002');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'FunAudioLLM/CosyVoice2-0.5B:alex', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0002', 'TTS_CosyVoiceSiliconflow', 'CosyVoice女声', 'FunAudioLLM/CosyVoice2-0.5B:bella', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- CozeCnTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_CozeCnTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CozeCnTTS0001', 'TTS_CozeCnTTS', 'CozeCn音色', '7426720361733046281', '中文', NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- MinimaxTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_MinimaxTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxTTS0001', 'TTS_MinimaxTTS', 'Minimax少女音', 'female-shaonv', '中文', NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- AliyunTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_AliyunTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunTTS0001', 'TTS_AliyunTTS', '阿里云小云', 'xiaoyun', '中文', NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- TTS302AI音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_TTS302AI0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_TTS302AI0001', 'TTS_TTS302AI', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 10, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- GizwitsTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_GizwitsTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GizwitsTTS0001', 'TTS_GizwitsTTS', '机智云湾湾', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 11, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- ACGNTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_ACGNTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_ACGNTTS0001', 'TTS_ACGNTTS', 'ACG音色', '1695', '中文', NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- OpenAITTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_OpenAITTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_OpenAITTS0001', 'TTS_OpenAITTS', 'OpenAI男声', 'onyx', '中文', NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 添加腾讯语音合成音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_TencentTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_TencentTTS0001', 'TTS_TencentTTS', '智瑜', '101001', '中文', NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 其他音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_FishSpeech0000';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_FishSpeech0000', 'TTS_FishSpeech', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_GPT_SOVITS_V20000';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V20000', 'TTS_GPT_SOVITS_V2', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_GPT_SOVITS_V30000', 'TTS_CustomTTS0000');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V30000', 'TTS_GPT_SOVITS_V3', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CustomTTS0000', 'TTS_CustomTTS', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- 添加火山引擎边缘大模型网关(LLM + TTS)
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('LLM_VolcesAiGatewayLLM', 'LLM', 'VolcesAiGatewayTTS', '火山引擎边缘大模型网关', 0, 1, '{\"type\": \"openai\", \"base_url\": \"https://ai-gateway.vei.volces.com/v1\", \"model_name\": \"doubao-pro-32k-functioncall\", \"api_key\": \"你的网关访问密钥\"}', NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('TTS_VolcesAiGatewayTTS', 'TTS', 'VolcesAiGatewayTTS', '火山引擎边缘大模型网关', 0, 1, '{\"type\": \"openai\", \"api_key\": \"你的网关访问密钥\", \"api_url\": \"https://ai-gateway.vei.volces.com/v1/audio/speech\", \"model\": \"doubao-tts\", \"voice\": \"zh_male_shaonianzixin_moon_bigtts\", \"speed\": 1, \"output_dir\": \"tmp/\"}', NULL, NULL, 15, NULL, NULL, NULL, NULL);
|
||||||
|
-- 添加火山引擎边缘大模型网关语音合成音色
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0001', 'TTS_VolcesAiGatewayTTS', '灿灿/Shiny', 'zh_female_cancan_mars_bigtts', '中文、美式英语', NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0002', 'TTS_VolcesAiGatewayTTS', '清新女声', 'zh_female_qingxinnvsheng_mars_bigtts', '中文', NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0003', 'TTS_VolcesAiGatewayTTS', '爽快思思/Skye', 'zh_female_shuangkuaisisi_moon_bigtts', '中文、美式英语', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0004', 'TTS_VolcesAiGatewayTTS', '温暖阿虎/Alvin', 'zh_male_wennuanahu_moon_bigtts', '中文、美式英语', NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0005', 'TTS_VolcesAiGatewayTTS', '少年梓辛/Brayan', 'zh_male_shaonianzixin_moon_bigtts', '中文、美式英语', NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0006', 'TTS_VolcesAiGatewayTTS', '知性女声', 'zh_female_zhixingnvsheng_mars_bigtts', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0007', 'TTS_VolcesAiGatewayTTS', '清爽男大', 'zh_male_qingshuangnanda_mars_bigtts', '中文', NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0008', 'TTS_VolcesAiGatewayTTS', '邻家女孩', 'zh_female_linjianvhai_moon_bigtts', '中文', NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0009', 'TTS_VolcesAiGatewayTTS', '渊博小叔', 'zh_male_yuanboxiaoshu_moon_bigtts', '中文', NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0010', 'TTS_VolcesAiGatewayTTS', '阳光青年', 'zh_male_yangguangqingnian_moon_bigtts', '中文', NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0011', 'TTS_VolcesAiGatewayTTS', '甜美小源', 'zh_female_tianmeixiaoyuan_moon_bigtts', '中文', NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0012', 'TTS_VolcesAiGatewayTTS', '清澈梓梓', 'zh_female_qingchezizi_moon_bigtts', '中文', NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0013', 'TTS_VolcesAiGatewayTTS', '解说小明', 'zh_male_jieshuoxiaoming_moon_bigtts', '中文', NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0014', 'TTS_VolcesAiGatewayTTS', '开朗姐姐', 'zh_female_kailangjiejie_moon_bigtts', '中文', NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0015', 'TTS_VolcesAiGatewayTTS', '邻家男孩', 'zh_male_linjiananhai_moon_bigtts', '中文', NULL, NULL, 15, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0016', 'TTS_VolcesAiGatewayTTS', '甜美悦悦', 'zh_female_tianmeiyueyue_moon_bigtts', '中文', NULL, NULL, 16, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0017', 'TTS_VolcesAiGatewayTTS', '心灵鸡汤', 'zh_female_xinlingjitang_moon_bigtts', '中文', NULL, NULL, 17, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0018', 'TTS_VolcesAiGatewayTTS', '知性温婉', 'ICL_zh_female_zhixingwenwan_tob', '中文', NULL, NULL, 18, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0019', 'TTS_VolcesAiGatewayTTS', '暖心体贴', 'ICL_zh_male_nuanxintitie_tob', '中文', NULL, NULL, 19, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0020', 'TTS_VolcesAiGatewayTTS', '温柔文雅', 'ICL_zh_female_wenrouwenya_tob', '中文', NULL, NULL, 20, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0021', 'TTS_VolcesAiGatewayTTS', '开朗轻快', 'ICL_zh_male_kailangqingkuai_tob', '中文', NULL, NULL, 21, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0022', 'TTS_VolcesAiGatewayTTS', '活泼爽朗', 'ICL_zh_male_huoposhuanglang_tob', '中文', NULL, NULL, 22, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0023', 'TTS_VolcesAiGatewayTTS', '率真小伙', 'ICL_zh_male_shuaizhenxiaohuo_tob', '中文', NULL, NULL, 23, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0024', 'TTS_VolcesAiGatewayTTS', '温柔小哥', 'zh_male_wenrouxiaoge_mars_bigtts', '中文', NULL, NULL, 24, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0025', 'TTS_VolcesAiGatewayTTS', 'Smith', 'en_male_smith_mars_bigtts', '英式英语', NULL, NULL, 25, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0026', 'TTS_VolcesAiGatewayTTS', 'Anna', 'en_female_anna_mars_bigtts', '英式英语', NULL, NULL, 26, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0027', 'TTS_VolcesAiGatewayTTS', 'Adam', 'en_male_adam_mars_bigtts', '美式英语', NULL, NULL, 27, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0028', 'TTS_VolcesAiGatewayTTS', 'Sarah', 'en_female_sarah_mars_bigtts', '澳洲英语', NULL, NULL, 28, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0029', 'TTS_VolcesAiGatewayTTS', 'Dryw', 'en_male_dryw_mars_bigtts', '澳洲英语', NULL, NULL, 29, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0030', 'TTS_VolcesAiGatewayTTS', 'かずね(和音)', 'multi_male_jingqiangkanye_moon_bigtts', '日语、西语', NULL, NULL, 30, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0031', 'TTS_VolcesAiGatewayTTS', 'はるこ(晴子)', 'multi_female_shuangkuaisisi_moon_bigtts', '日语、西语', NULL, NULL, 31, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0032', 'TTS_VolcesAiGatewayTTS', 'ひろし(広志)', 'multi_male_wanqudashu_moon_bigtts', '日语、西语', NULL, NULL, 32, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0033', 'TTS_VolcesAiGatewayTTS', 'あけみ(朱美)', 'multi_female_gaolengyujie_moon_bigtts', '日语', NULL, NULL, 33, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0034', 'TTS_VolcesAiGatewayTTS', 'Amanda', 'en_female_amanda_mars_bigtts', '美式英语', NULL, NULL, 34, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0035', 'TTS_VolcesAiGatewayTTS', 'Jackson', 'en_male_jackson_mars_bigtts', '美式英语', NULL, NULL, 35, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0036', 'TTS_VolcesAiGatewayTTS', '京腔侃爷/Harmony', 'zh_male_jingqiangkanye_moon_bigtts', '中文-北京口音、英文', NULL, NULL, 36, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0037', 'TTS_VolcesAiGatewayTTS', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文-台湾口音', NULL, NULL, 37, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0038', 'TTS_VolcesAiGatewayTTS', '湾区大叔', 'zh_female_wanqudashu_moon_bigtts', '中文-广东口音', NULL, NULL, 38, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0039', 'TTS_VolcesAiGatewayTTS', '呆萌川妹', 'zh_female_daimengchuanmei_moon_bigtts', '中文-四川口音', NULL, NULL, 39, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0040', 'TTS_VolcesAiGatewayTTS', '广州德哥', 'zh_male_guozhoudege_moon_bigtts', '中文-广东口音', NULL, NULL, 40, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0041', 'TTS_VolcesAiGatewayTTS', '北京小爷', 'zh_male_beijingxiaoye_moon_bigtts', '中文-北京口音', NULL, NULL, 41, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0042', 'TTS_VolcesAiGatewayTTS', '浩宇小哥', 'zh_male_haoyuxiaoge_moon_bigtts', '中文-青岛口音', NULL, NULL, 42, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0043', 'TTS_VolcesAiGatewayTTS', '广西远舟', 'zh_male_guangxiyuanzhou_moon_bigtts', '中文-广西口音', NULL, NULL, 43, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0044', 'TTS_VolcesAiGatewayTTS', '妹坨洁儿', 'zh_female_meituojieer_moon_bigtts', '中文-长沙口音', NULL, NULL, 44, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_VolcesAiGatewayTTS_0045', 'TTS_VolcesAiGatewayTTS', '豫州子轩', 'zh_male_yuzhouzixuan_moon_bigtts', '中文-河南口音', NULL, NULL, 45, NULL, NULL, NULL, NULL);
|
||||||