Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d934e0a69d | ||
|
|
2ecce6485c | ||
|
|
1ad8aeeaa7 | ||
|
|
46a15082d6 | ||
|
|
153ecf0b41 | ||
|
|
f0f7304ae2 | ||
|
|
997e171118 | ||
|
|
04a8fe8af2 | ||
|
|
a0822a356c | ||
|
|
b78d337fd0 | ||
|
|
13c5cb3f1d | ||
|
|
197c0532fa | ||
|
|
76342b09ff | ||
|
|
241d96f6ac | ||
|
|
0dc267c109 | ||
|
|
bc6c85c6c6 | ||
|
|
915f4cac5e | ||
|
|
b8da0714c2 | ||
|
|
f1105e6822 | ||
|
|
20296148eb | ||
|
|
2138c0fb4f | ||
|
|
34d9857089 | ||
|
|
888b71a196 | ||
|
|
5f16c871dc | ||
|
|
595a338de7 | ||
|
|
fe47fd1910 | ||
|
|
ac9abf8ede | ||
|
|
282706dbc8 | ||
|
|
5360cbf19c | ||
|
|
84ac1b6686 | ||
|
|
ebc16207f4 | ||
|
|
7611e98156 | ||
|
|
848f73715f | ||
|
|
6f30dfb963 | ||
|
|
c8aa903e3e | ||
|
|
f0e55037de | ||
|
|
1335b368d1 | ||
|
|
a647f3c105 | ||
|
|
c936372105 | ||
|
|
f242d43f73 | ||
|
|
c216012304 | ||
|
|
87c2f0e38e | ||
|
|
d9cc75edc0 | ||
|
|
1f91ec32da | ||
|
|
92b60b7f25 | ||
|
|
8104d02a3a | ||
|
|
83c4add375 | ||
|
|
c19084b7b4 | ||
|
|
2761e97c6a | ||
|
|
0cdfd1d114 | ||
|
|
8566e048b6 | ||
|
|
31fc731af6 | ||
|
|
ff95764824 | ||
|
|
737c4a6b5d | ||
|
|
db68871646 | ||
|
|
748f17b777 | ||
|
|
bbba24c7bc | ||
|
|
5bba864faa | ||
|
|
33b7fe6072 | ||
|
|
4d0bef62ed | ||
|
|
e09ba81cfa | ||
|
|
baf15450a3 | ||
|
|
d24419cfb7 | ||
|
|
fe88db2094 | ||
|
|
91d2ab7d1d | ||
|
|
9a405a756f | ||
|
|
86f5be23fe | ||
|
|
9f4ca1aa6b | ||
|
|
624b1cce74 | ||
|
|
29216768e1 | ||
|
|
59e18dc1c1 | ||
|
|
858a85f952 | ||
|
|
1cbd68f1e3 | ||
|
|
324bf62bb8 | ||
|
|
dd1d7d5dc1 | ||
|
|
e6ad9e23cc | ||
|
|
a557322dc6 | ||
|
|
7d7324ddbc | ||
|
|
680ed4596f | ||
|
|
e66ab647e1 | ||
|
|
253f4c59a3 | ||
|
|
38a97d6b5a | ||
|
|
ae8f11d5a8 | ||
|
|
dcfd82f84e | ||
|
|
25a531341a | ||
|
|
f421f6dee8 | ||
|
|
a5332ccc7d | ||
|
|
76c3f401ea | ||
|
|
5173dded2a | ||
|
|
bdd8597f69 | ||
|
|
46bba64a94 | ||
|
|
d0694a0e70 | ||
|
|
bd12b0e81f | ||
|
|
2f4fb0fc52 | ||
|
|
cbabf01902 | ||
|
|
c51c8b4af0 | ||
|
|
2741e11078 | ||
|
|
aa251aa011 | ||
|
|
0509ddb129 | ||
|
|
5a180836b7 | ||
|
|
9c7c6b9ea7 | ||
|
|
d64eb0b087 | ||
|
|
28e4318f8a | ||
|
|
36f0821ea0 | ||
|
|
293a690ccd | ||
|
|
1c96ff7d3b | ||
|
|
db8a2bb872 | ||
|
|
dfbe4fc7ee | ||
|
|
3cccc4ec64 | ||
|
|
e29684e672 | ||
|
|
30796c6f81 | ||
|
|
86b5eb1818 | ||
|
|
696405a8bf | ||
|
|
a2c2eab6b3 | ||
|
|
eadb69bdc0 | ||
|
|
e73223ca8a | ||
|
|
6dd66dbae9 | ||
|
|
ead9ee81fb | ||
|
|
073b69f94a | ||
|
|
1a0a0777a3 | ||
|
|
a971ae2168 | ||
|
|
bd140af714 | ||
|
|
6b8d2502b7 | ||
|
|
96008fa78f | ||
|
|
da4d0f515c | ||
|
|
67e6a22da9 | ||
|
|
b791fb1494 | ||
|
|
9e329bf19f | ||
|
|
3906e8a942 | ||
|
|
f74faea065 | ||
|
|
7d3759acce | ||
|
|
172962f711 | ||
|
|
36109e3b88 | ||
|
|
7c63b7c062 | ||
|
|
1efaf048f5 | ||
|
|
ac52714498 | ||
|
|
c3de89f504 | ||
|
|
6f6a645d0a | ||
|
|
5605352144 | ||
|
|
ef433f446d | ||
|
|
fe8f200364 | ||
|
|
e4194cdf4d | ||
|
|
c8392c9b47 | ||
|
|
be3a030302 | ||
|
|
cd6d4488d1 | ||
|
|
da3a5df5c3 | ||
|
|
4864407d61 | ||
|
|
f6f7879d6e | ||
|
|
dab1f14a59 | ||
|
|
51ca914206 | ||
|
|
933ffb8ca2 | ||
|
|
2f5f20a257 | ||
|
|
92544f89f3 | ||
|
|
f1cf1acbfa | ||
|
|
f22008389c | ||
|
|
91de372cbc | ||
|
|
811bd94934 | ||
|
|
5987488b3d | ||
|
|
966c9e972d | ||
|
|
464261a1b7 | ||
|
|
363dcef81a | ||
|
|
4439617c50 | ||
|
|
5d2fa3d65d | ||
|
|
8930ab0af6 | ||
|
|
6af6fe22fc | ||
|
|
dd577a7843 | ||
|
|
f772155674 | ||
|
|
3ce204338a | ||
|
|
168bc9ef45 | ||
|
|
bfc3d4888b | ||
|
|
0148b115db | ||
|
|
0b11953f6f | ||
|
|
6e210faacf | ||
|
|
4ef755e10b | ||
|
|
62b7cc5509 | ||
|
|
0e1ec05110 | ||
|
|
dd38fc74db | ||
|
|
d619d7ac76 | ||
|
|
37f7511ba0 | ||
|
|
afd03ba1b5 | ||
|
|
ac7b02d28a | ||
|
|
4a0bdbf779 | ||
|
|
164dada46a | ||
|
|
fd1c4ec792 | ||
|
|
add000d221 | ||
|
|
def598f728 | ||
|
|
0294885fbf | ||
|
|
c0ec4338f8 | ||
|
|
fb7f6f73bd | ||
|
|
e04c38e557 | ||
|
|
bfb5420733 | ||
|
|
9e6fc32903 | ||
|
|
217ceb1c7a | ||
|
|
846f89664e | ||
|
|
0e30f69e96 | ||
|
|
af9eb538f4 | ||
|
|
66a0beea33 | ||
|
|
411e73bb35 | ||
|
|
852e222f5d | ||
|
|
054ceab4f4 | ||
|
|
d9149ff572 | ||
|
|
7f331690a0 | ||
|
|
a5698dfff0 | ||
|
|
1e5866da06 | ||
|
|
3f7b7da184 | ||
|
|
c0020c8769 | ||
|
|
1c5e678cef | ||
|
|
3ab1b8ebc9 | ||
|
|
7635c68ccd | ||
|
|
a3b562b895 | ||
|
|
94d1493195 | ||
|
|
3a5bbe32b9 | ||
|
|
7e712c5003 | ||
|
|
31328e092e | ||
|
|
29671f48a8 | ||
|
|
61c2782491 | ||
|
|
15263b5223 | ||
|
|
557d1ee5b0 | ||
|
|
777265c730 | ||
|
|
e76a8d547b | ||
|
|
ff98f84f18 | ||
|
|
d0b4fa5b28 | ||
|
|
91b80f7c49 | ||
|
|
47f409246d | ||
|
|
be1ff83297 | ||
|
|
297c9e0085 | ||
|
|
ec95917b4d | ||
|
|
98bfe863fe | ||
|
|
af3d00662e | ||
|
|
39803fb0df | ||
|
|
4dad5ea6c1 | ||
|
|
ba8cb8c6f8 | ||
|
|
64f10b28e7 | ||
|
|
7f34447ace | ||
|
|
c2e000f937 | ||
|
|
831ab6d99b | ||
|
|
b246d9e567 | ||
|
|
919c2ffd46 | ||
|
|
e6d63a811e | ||
|
|
92227098b7 | ||
|
|
571080c1d6 | ||
|
|
d07feb837d | ||
|
|
1c823c4255 | ||
|
|
63e378524b | ||
|
|
ea5f54e421 | ||
|
|
ee18fbebae | ||
|
|
fa56d06e0c | ||
|
|
72f7514114 | ||
|
|
44e1f00ffc | ||
|
|
e7054ea13f | ||
|
|
4a3ac2cfcd | ||
|
|
bbc31e01c4 | ||
|
|
ba86b34a8c | ||
|
|
770a198772 | ||
|
|
a26bee3696 | ||
|
|
abb8f4f963 | ||
|
|
43d2adff70 | ||
|
|
474eeb9e68 | ||
|
|
e2dffea423 | ||
|
|
3d5eaba46f | ||
|
|
1bb8fbd56f | ||
|
|
b699886953 | ||
|
|
12c957d48b | ||
|
|
48e890c1b1 | ||
|
|
05331f001a | ||
|
|
bde260b330 | ||
|
|
59ef51ea20 | ||
|
|
aa77bfdfc4 | ||
|
|
a2baef8911 | ||
|
|
3fcfb65d45 | ||
|
|
8f266bea0d | ||
|
|
0a765f4aac | ||
|
|
ee3f0555d1 | ||
|
|
0c0386f4ba | ||
|
|
6c2b1cb589 | ||
|
|
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 | ||
|
|
244c76793c | ||
|
|
39fe057f9f | ||
|
|
c9eb640510 | ||
|
|
766a706250 | ||
|
|
f55d6c2e60 |
@@ -9,26 +9,24 @@ assignees: ''
|
||||
## 🐛 问题描述
|
||||
<!-- 清晰简洁地描述问题是什么 -->
|
||||
|
||||
## 🔍 复现步骤
|
||||
<!-- 详细描述复现问题的步骤 -->
|
||||
## 🖥️ 环境信息
|
||||
- 部署方式: 全模块部署 还是 单Server部署
|
||||
- 版本号: 例如 0.3.x
|
||||
|
||||
## 🔍 告诉我们,应该怎么复现这个问题
|
||||
<!-- 这个很重要,方便我们快速定位 -->
|
||||
1. 打开 '...'
|
||||
2. 点击 '...'
|
||||
3. 滚动到 '...'
|
||||
4. 看到错误
|
||||
|
||||
## 🤔 预期行为
|
||||
## 🤔 你原本希望是怎么样的
|
||||
<!-- 简要描述预期的正确行为 -->
|
||||
|
||||
## 😯 截图
|
||||
## 😯 提供一些截图
|
||||
<!-- 如果适用,添加问题的截图 -->
|
||||
|
||||
## 🖥️ 环境信息
|
||||
- 操作系统: [例如 Windows 10]
|
||||
- 浏览器: [例如 Chrome 89]
|
||||
- 项目版本: [例如 1.0.0]
|
||||
- Python版本: [例如 3.9.13]
|
||||
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
|
||||
- Nodejs版本:[例如 v20.14.0]
|
||||
1. 比如日志截图,越多越好
|
||||
2. 比如界面反应
|
||||
|
||||
## 📋 其他信息
|
||||
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||
|
||||
@@ -165,3 +165,4 @@ main/manager-api/.vscode
|
||||
main/manager-web/.webpack_cache/
|
||||
main/xiaozhi-server/mysql
|
||||
uploadfile
|
||||
.vscode
|
||||
|
||||
@@ -7,20 +7,23 @@ COPY main/manager-web .
|
||||
RUN npm run build
|
||||
|
||||
# 第二阶段:构建Java后端
|
||||
FROM maven:3-eclipse-temurin-21-alpine as api-builder
|
||||
FROM maven:3.9.4-eclipse-temurin-21 as api-builder
|
||||
WORKDIR /app
|
||||
COPY main/manager-api/pom.xml .
|
||||
COPY main/manager-api/src ./src
|
||||
RUN mvn clean package -Dmaven.test.skip=true
|
||||
|
||||
# 第三阶段:构建最终镜像
|
||||
FROM eclipse-temurin:21-jdk-jammy
|
||||
FROM bellsoft/liberica-runtime-container:jre-21-glibc
|
||||
|
||||
# 安装Nginx并清理缓存
|
||||
RUN apt-get update && \
|
||||
apt-get install -y nginx && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
# 安装Nginx和字体库
|
||||
RUN apk update && \
|
||||
apk add --no-cache nginx bash && \
|
||||
apk add --no-cache fontconfig ttf-dejavu msttcorefonts-installer && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
# 更新字体缓存
|
||||
RUN printf 'YES\n' | update-ms-fonts && fc-cache -f -v
|
||||
|
||||
# 配置Nginx
|
||||
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
|
||||
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
||||
|
||||
<p align="center">
|
||||
@@ -11,12 +10,11 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">English</a>
|
||||
· 简体中文
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</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="./README_en.md">English</a>
|
||||
· <a href="./docs/FAQ.md">常见问题</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">反馈问题</a>
|
||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
@@ -132,17 +130,7 @@
|
||||
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
||||
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
||||
|
||||
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
|
||||
`config.yaml` 中开启防护:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
auth:
|
||||
# 开启防护
|
||||
enabled: true
|
||||
```
|
||||
|
||||
开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。
|
||||
2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。
|
||||
|
||||
---
|
||||
|
||||
@@ -163,14 +151,11 @@ server:
|
||||
|
||||
```
|
||||
智控台地址: https://2662r3426b.vicp.fun
|
||||
|
||||
服务测试工具: https://2662r3426b.vicp.fun/test/
|
||||
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
```
|
||||
---
|
||||
|
||||
## 常见问题 ❓
|
||||
|
||||
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
||||
|
||||
---
|
||||
## 功能清单 ✨
|
||||
@@ -246,7 +231,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
| 使用方式 | 支持平台 | 免费平台 |
|
||||
|:---:|:---:|:---:|
|
||||
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||
| 接口调用 | DoubaoASR | - |
|
||||
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
<h1 align="center">Xiaozhi Backend Service xiaozhi-esp32-server</h1>
|
||||
|
||||
<p align="center">
|
||||
This project provides backend services for the open-source smart hardware project
|
||||
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a><br/>
|
||||
Implemented using Python, Java, and Vue according to the <a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">Xiaozhi Communication Protocol</a><br/>
|
||||
Helping you quickly set up your Xiaozhi server
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">中文</a>
|
||||
· <a href="./docs/FAQ.md">FAQ</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">Report Issues</a>
|
||||
· <a href="./README_ed.md#deployment-documentation">Deployment Guide</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">Release Notes</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/v/release/xinnan-tech/xiaozhi-esp32-server?logo=docker" />
|
||||
</a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/graphs/contributors">
|
||||
<img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/xinnan-tech/xiaozhi-esp32-server?logo=github" />
|
||||
</a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">
|
||||
<img alt="Issues" src="https://img.shields.io/github/issues/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/pulls">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/xinnan-tech/xiaozhi-esp32-server?color=0088ff" />
|
||||
</a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/LICENSE">
|
||||
<img alt="GitHub pull requests" src="https://img.shields.io/badge/license-MIT-white?labelColor=black" />
|
||||
</a>
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server">
|
||||
<img alt="stars" src="https://img.shields.io/github/stars/xinnan-tech/xiaozhi-esp32-server?color=ffcb47&labelColor=black" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Target Users 👥
|
||||
|
||||
This project requires ESP32 hardware devices. If you have purchased ESP32-related hardware, successfully connected to Brother Xia's backend service, and want to set up your own `xiaozhi-esp32` backend service, then this project is perfect for you.
|
||||
|
||||
Want to see it in action? Check out these videos 🎥
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="Xiaozhi esp32 connecting to custom backend model" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Custom voice" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="Cantonese communication" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="Home appliance control" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1kgA2eYEQ9" target="_blank">
|
||||
<picture>
|
||||
<img alt="Lowest cost configuration" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Vy96YCE3R" target="_blank">
|
||||
<picture>
|
||||
<img alt="Custom voice" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Music playback" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="Weather plugin" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV178XuYfEpi" target="_blank">
|
||||
<picture>
|
||||
<img alt="IOT device control" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="News broadcast" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## Warning ⚠️
|
||||
|
||||
1. This project is open-source software. This software has no commercial relationship with any third-party API service providers (including but not limited to speech recognition, large models, speech synthesis, and other platforms) and does not provide any form of guarantee for their service quality or financial security.
|
||||
It is recommended that users prioritize service providers with relevant business licenses and carefully read their service agreements and privacy policies. This software does not host any account keys, does not participate in fund transfers, and does not bear the risk of recharge fund losses.
|
||||
|
||||
2. This project's functionality is not complete and has not passed network security testing. Please do not use it in production environments. If you deploy this project for learning in a public network environment, please ensure necessary protection measures are in place.
|
||||
|
||||
---
|
||||
|
||||
## Deployment Documentation
|
||||
|
||||

|
||||
|
||||
This project offers two deployment methods. Please choose based on your specific needs:
|
||||
|
||||
#### 🚀 Deployment Method Selection
|
||||
|
||||
| Deployment Method | Features | Use Case | Docker Deployment Guide | Source Code Deployment Guide |
|
||||
|---------|------|---------|---------|---------|
|
||||
| **Simplified Installation** | Smart dialogue, IOT functionality, data stored in configuration files | Low-configuration environment, no database required | [Docker Server Only](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [Local Source Code Server Only](./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)|
|
||||
| **Full Module Installation** | Smart dialogue, IOT, OTA, Control Panel, data stored in database | Complete functionality experience |[Docker Full Module](./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) | [Local Source Code Full Module](./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) |
|
||||
|
||||
> 💡 Note: Below is the test platform deployed with the latest code. You can flash and test if needed. Concurrent users: 6, data cleared daily
|
||||
|
||||
```
|
||||
Control Panel: https://2662r3426b.vicp.fun
|
||||
|
||||
Service Test Tool: https://2662r3426b.vicp.fun/test/
|
||||
OTA Interface: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||
Websocket Interface: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
```
|
||||
|
||||
---
|
||||
## Feature List ✨
|
||||
|
||||
### Implemented ✅
|
||||
|
||||
| Feature Module | Description |
|
||||
|---------|------|
|
||||
| Communication Protocol | Based on `xiaozhi-esp32` protocol, implements data interaction through WebSocket |
|
||||
| Dialogue Interaction | Supports wake-up dialogue, manual dialogue, and real-time interruption. Auto-sleep after long periods of inactivity |
|
||||
| Intent Recognition | Supports LLM intent recognition, function call, reducing hard-coded intent judgment |
|
||||
| Multi-language Recognition | Supports Mandarin, Cantonese, English, Japanese, Korean (default using FunASR) |
|
||||
| LLM Module | Supports flexible LLM module switching, default using ChatGLMLLM, also supports Ali Bailing, DeepSeek, Ollama, etc. |
|
||||
| TTS Module | Supports EdgeTTS (default), Volcano Engine Doubao TTS, and other TTS interfaces for speech synthesis |
|
||||
| Memory Function | Supports ultra-long memory, local summary memory, and no memory modes for different scenarios |
|
||||
| IOT Function | Supports managing registered device IOT functionality, intelligent IoT control based on dialogue context |
|
||||
| Control Panel | Provides web management interface, supports agent management, user management, system configuration, etc. |
|
||||
|
||||
### In Development 🚧
|
||||
|
||||
To learn about specific development progress, [click here](https://github.com/users/xinnan-tech/projects/3)
|
||||
|
||||
If you're a software developer, here's an [Open Letter to Developers](docs/contributor_open_letter.md). Welcome to join!
|
||||
|
||||
---
|
||||
|
||||
## Product Ecosystem 👬
|
||||
Xiaozhi is an ecosystem. When using this product, you might want to check out other excellent projects in this ecosystem:
|
||||
|
||||
| Project Name | Project Link | Description |
|
||||
|:---------------------|:--------|:--------|
|
||||
| Xiaozhi Android Client | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | A Flutter-based Android and iOS voice dialogue application supporting real-time voice interaction and text dialogue |
|
||||
| Xiaozhi PC Client | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | A Python-based AI client that allows you to experience Xiaozhi AI functionality through code without physical hardware |
|
||||
| Xiaozhi Java Server | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | A Java-based open-source project providing complete backend service solutions |
|
||||
|
||||
---
|
||||
|
||||
## Supported Platforms/Components 📋
|
||||
|
||||
### LLM Language Models
|
||||
|
||||
| Usage Method | Supported Platforms | Free Platforms |
|
||||
|:---:|:---:|:---:|
|
||||
| openai API | Ali Bailing, Volcano Engine Doubao, DeepSeek, ChatGLM, Gemini | ChatGLM, Gemini |
|
||||
| ollama API | Ollama | - |
|
||||
| dify API | Dify | - |
|
||||
| fastgpt API | Fastgpt | - |
|
||||
| coze API | Coze | - |
|
||||
|
||||
Actually, any LLM supporting openai API calls can be integrated.
|
||||
|
||||
---
|
||||
|
||||
### TTS Speech Synthesis
|
||||
|
||||
| Usage Method | Supported Platforms | Free Platforms |
|
||||
|:---:|:---:|:---:|
|
||||
| API Calls | EdgeTTS, Volcano Engine Doubao TTS, Tencent Cloud, Aliyun TTS, CosyVoiceSiliconflow, TTS302AI, CozeCnTTS, GizwitsTTS, ACGNTTS, OpenAITTS | EdgeTTS, CosyVoiceSiliconflow(partial) |
|
||||
| Local Service | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS | FishSpeech, GPT_SOVITS_V2, GPT_SOVITS_V3, MinimaxTTS |
|
||||
|
||||
---
|
||||
|
||||
### VAD Voice Activity Detection
|
||||
|
||||
| Type | Platform Name | Usage Method | Pricing | Notes |
|
||||
|:---:|:---------:|:----:|:----:|:--:|
|
||||
| VAD | SileroVAD | Local Use | Free | |
|
||||
|
||||
---
|
||||
|
||||
### ASR Speech Recognition
|
||||
|
||||
| Usage Method | Supported Platforms | Free Platforms |
|
||||
|:---:|:---:|:---:|
|
||||
| Local Use | FunASR, SherpaASR | FunASR, SherpaASR |
|
||||
| API Calls | DoubaoASR, FunASRServer, TencentASR, AliyunASR | FunASRServer |
|
||||
|
||||
---
|
||||
|
||||
### Memory Storage
|
||||
|
||||
| Type | Platform Name | Usage Method | Pricing | Notes |
|
||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||
| Memory | mem0ai | API Calls | 1000 calls/month quota | |
|
||||
| Memory | mem_local_short | Local Summary | Free | |
|
||||
|
||||
---
|
||||
|
||||
### Intent Recognition
|
||||
|
||||
| Type | Platform Name | Usage Method | Pricing | Notes |
|
||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||
| Intent | intent_llm | API Calls | Based on LLM pricing | Uses large model for intent recognition, highly versatile |
|
||||
| Intent | function_call | API Calls | Based on LLM pricing | Uses large model function calls for intent, fast and effective |
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments 🙏
|
||||
|
||||
| Logo | Project/Company | Description |
|
||||
|:---:|:---:|:---|
|
||||
| <img src="./docs/images/logo_bailing.png" width="160"> | [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) | This project was inspired by [Bailing Voice Dialogue Robot](https://github.com/wwbin2017/bailing) and implemented based on it |
|
||||
| <img src="./docs/images/logo_tenclass.png" width="160"> | [Tenclass](https://www.tenclass.com/) | Thanks to [Tenclass](https://www.tenclass.com/) for developing standard communication protocols, multi-device compatibility solutions, and high-concurrency scenario practices for the Xiaozhi ecosystem; providing comprehensive technical documentation support for this project |
|
||||
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [Xuanfeng Technology](https://github.com/Eric0308) | Thanks to [Xuanfeng Technology](https://github.com/Eric0308) for contributing function call framework, MCP communication protocol, and plugin call mechanism implementation code, significantly improving front-end device (IoT) interaction efficiency and functional extensibility through standardized instruction scheduling system and dynamic expansion capabilities |
|
||||
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [Huiyuan Design](http://ui.kwd988.net/) | Thanks to [Huiyuan Design](http://ui.kwd988.net/) for providing professional visual solutions for this project, empowering product user experience with their design experience serving over a thousand enterprises |
|
||||
| <img src="./docs/images/logo_qinren.png" width="160"> | [Xi'an Qinren Information Technology](https://www.029app.com/) | Thanks to [Xi'an Qinren Information Technology](https://www.029app.com/) for deepening this project's visual system, ensuring consistency and extensibility of overall design style in multi-scenario applications |
|
||||
|
||||
|
||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
@@ -81,7 +81,7 @@ xiaozhi-server
|
||||
|
||||
下载完后,回到本教程继续往下。
|
||||
|
||||
##### 1.2.3.2 下载 config.yaml
|
||||
##### 1.2.3.2 创建 config.yaml
|
||||
|
||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||
|
||||
@@ -224,43 +224,36 @@ python app.py
|
||||
## 配置项目
|
||||
|
||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.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`模型。如果需要切换模型,就是改对应的名称。
|
||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
||||
以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例
|
||||
|
||||
```
|
||||
server:
|
||||
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
|
||||
prompt: |
|
||||
我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
||||
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
|
||||
请你像一个人一样说话,请勿返回配置xml及其他特殊字符。
|
||||
|
||||
selected_module:
|
||||
VAD: SileroVAD
|
||||
ASR: FunASR
|
||||
LLM: ChatGLMLLM
|
||||
TTS: EdgeTTS
|
||||
# 默认不开启记忆,如需开启请看配置文件里的描述
|
||||
Memory: nomem
|
||||
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
||||
Intent: nointent
|
||||
```
|
||||
LLM: DoubaoLLM
|
||||
|
||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
||||
使用时,在`selected_module`修改成对应的如下LLM配置的名称:
|
||||
|
||||
```
|
||||
LLM:
|
||||
DeepSeekLLM:
|
||||
type: openai
|
||||
...
|
||||
ChatGLMLLM:
|
||||
type: openai
|
||||
...
|
||||
DifyLLM:
|
||||
type: dify
|
||||
...
|
||||
api_key: xxxxxxxxxxxxxxx.xxxxxx
|
||||
```
|
||||
|
||||
建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。
|
||||
比如你要换更换模型,修改`selected_module`下的配置就行。
|
||||
|
||||
## 模型文件
|
||||
|
||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||
|
||||
@@ -213,7 +213,7 @@ CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unico
|
||||
如果还没有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
|
||||
|
||||
@@ -104,12 +104,12 @@ VAD:
|
||||
|
||||
参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md)
|
||||
|
||||
### 8、更多问题,可联系我们反馈 💬
|
||||
### 8、如何开启手机注册智控台 📱
|
||||
|
||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||
参考教程[阿里云短信集成指南](./ali-sms-integration.md)
|
||||
|
||||
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
|
||||
### 9、更多问题,可联系我们反馈 💬
|
||||
|
||||
- 硬件烧录QQ群:适用于硬件烧录问题
|
||||
- 开源服务端交流群:适用于服务端问题
|
||||
- 产品建议联系人:适用于产品功能、产品设计等建议
|
||||
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
|
||||
|
||||
也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
|
||||
@@ -0,0 +1,44 @@
|
||||
# 阿里云短信集成指南
|
||||
|
||||
登录阿里云控制台,进入“短信服务”页面:https://dysms.console.aliyun.com/overview
|
||||
|
||||
## 第一步 添加签名
|
||||

|
||||

|
||||
|
||||
以上步骤,会得到签名,请把它写入到智控台参数,`aliyun.sms.sign_name`
|
||||
|
||||
## 第二步 添加模版
|
||||

|
||||
|
||||
以上步骤,会得到模版code,请把它写入到智控台参数,`aliyun.sms.sms_code_template_code`
|
||||
|
||||
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
|
||||
|
||||
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
|
||||
|
||||
注意,签名要等7个工作日,等运营商报备成功后才能发送成功。
|
||||
|
||||
可以等报备成功后,再继续往下操作。
|
||||
|
||||
## 第三步 创建短信账户和开通权限
|
||||
|
||||
登录阿里云控制台,进入“访问控制”页面:https://ram.console.aliyun.com/overview?activeTab=overview
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
以上步骤,会得到access_key_id和access_key_secret,请把它写入到智控台参数,`aliyun.sms.access_key_id`、`aliyun.sms.access_key_secret`
|
||||
## 第四步 启动手机注册功能
|
||||
|
||||
1、正常来说,以上信息都填完后,会有这个效果,如果没有,可能缺少了某个步骤
|
||||
|
||||

|
||||
|
||||
2、开启允许非管理员用户可注册,将参数`server.allow_user_register`设置成`true`
|
||||
|
||||
3、开启手机注册功能,将参数`server.enable_mobile_register`设置成`true`
|
||||

|
||||
@@ -86,14 +86,9 @@ idf.py set-target esp32s3
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||

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

|
||||
|
||||
## 第5步 编译固件
|
||||
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 基于虾哥编译好的固件配置自定义服务器
|
||||
|
||||
## 第1步 确认版本
|
||||
烧录虾哥已经编译好的1.6.1版本固件
|
||||
烧录虾哥已经编译好的[1.6.1版本以上固件](https://github.com/78/xiaozhi-esp32/releases)
|
||||
|
||||
## 第2步 准备你的ota地址
|
||||
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
||||
@@ -29,6 +29,7 @@ wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|
||||
## 第3步 进入配网模式
|
||||
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
|
||||

|
||||
|
||||
## 第4步 唤醒小智,查看日志输出
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
登录AutoDL,租赁镜像
|
||||
选择镜像:
|
||||
```
|
||||
PyTorch / 2.1.0 / 3.10(ubuntu22.04) / cuda 12.1
|
||||
```
|
||||
|
||||
机器开机后,设置学术加速
|
||||
```
|
||||
source /etc/network_turbo
|
||||
```
|
||||
|
||||
进入工作目录
|
||||
```
|
||||
cd autodl-tmp/
|
||||
```
|
||||
|
||||
拉取项目
|
||||
```
|
||||
git clone https://gitclone.com/github.com/fishaudio/fish-speech.git ; cd fish-speech
|
||||
```
|
||||
|
||||
安装依赖
|
||||
```
|
||||
pip install -e.
|
||||
```
|
||||
|
||||
如果报错,安装portaudio
|
||||
```
|
||||
apt-get install portaudio19-dev -y
|
||||
```
|
||||
|
||||
安装后执行
|
||||
```
|
||||
pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121
|
||||
```
|
||||
|
||||
下载模型
|
||||
```
|
||||
cd tools
|
||||
python download_models.py
|
||||
```
|
||||
|
||||
下载完模型后运行接口
|
||||
```
|
||||
python -m tools.api_server --listen 0.0.0.0:6006
|
||||
```
|
||||
|
||||
然后用浏览器去到aotodl实例页面
|
||||
```
|
||||
https://autodl.com/console/instance/list
|
||||
```
|
||||
|
||||
如下图点击你刚才机器的`自定义服务`按钮,开启端口转发服务
|
||||

|
||||
|
||||
端口转发服务设置完成后,你本地电脑打开网址`http://localhost:6006/`,就可以访问fish-speech的接口了
|
||||

|
||||
|
||||
|
||||
如果你是单模块部署,核心配置如下
|
||||
```
|
||||
selected_module:
|
||||
TTS: FishSpeech
|
||||
TTS:
|
||||
FishSpeech:
|
||||
reference_audio: ["config/assets/wakeup_words.wav",]
|
||||
reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",]
|
||||
api_key: "123"
|
||||
api_url: "http://127.0.0.1:6006/v1/tts"
|
||||
```
|
||||
|
||||
然后重启服务
|
||||
@@ -1,4 +1,8 @@
|
||||
# ESP32设备与HomeAssistant集成指南
|
||||
# 小智ESP32-开源服务端与HomeAssistant集成指南
|
||||
|
||||
[TOC]
|
||||
|
||||
-----
|
||||
|
||||
## 简介
|
||||
|
||||
@@ -7,26 +11,58 @@
|
||||
## 前提条件
|
||||
|
||||
- 已安装并配置好`HomeAssistant`
|
||||
- 已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`
|
||||
- `xiaozhi-esp32-server智控台`能正常使用
|
||||
- 本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用
|
||||
|
||||
## 网络环境
|
||||
## 开始前的操作(必要)
|
||||
|
||||
### 1. 获取HA的网络网络地址信息
|
||||
|
||||
请访问你Home Assistant的网络地址,例如,我的HA的地址是192.168.4.7,端口则是默认的8123,则在浏览器打开
|
||||
|
||||
1、我的`HomeAssistant`部署在下面,版本是`2025.3.4`
|
||||
```
|
||||
http://192.168.4.7:8123
|
||||
```
|
||||
|
||||
2、我的`xiaozhi-esp32-server智控台`和`HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
|
||||
> 手动查询 HA 的 IP 地址方法**(仅限小智esp32-server和HA部署在同一个网络设备[例如同一个wifi]下)**:
|
||||
>
|
||||
> 1. 进入 Home Assistant(前端)。
|
||||
>
|
||||
> 2. 点击左下角 **设置(Settings)** → **系统(System)** → **网络(Network)**。
|
||||
>
|
||||
> 3. 滑到最底部`Home Assistant 网址(Home Assistant website)`区域,在`本地网络(local network)`中,点击`眼睛`按钮,可以看到当前使用的 IP 地址(如 `192.168.1.10`)和网络接口。点击`复制连接(copy link)`可以直接复制。
|
||||
>
|
||||
> 
|
||||
|
||||
或,您已经设置了直接可以访问的Home Assistant的OAuth地址,您也可以在浏览器内直接访问
|
||||
|
||||
```
|
||||
http://192.168.4.7:8002
|
||||
http://homeassistant.local:8123
|
||||
```
|
||||
|
||||
### 2. 登录`Home Assistant`拿到开发密钥
|
||||
|
||||
## 配置步骤
|
||||
登录`HomeAssistant`,点击`左下角头像 -> 个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key,并复制保存,后续的方法都需要使用这个api key且仅出现一次(小tips: 您可以保存生成的二维码图像,后续可以扫描二维码再此提取api key)。
|
||||
|
||||
### 1. 登录`HomeAssistant`整理需要控制的设备清单
|
||||
## 方法1:小智社区共建的HA调用功能
|
||||
|
||||
### 功能描述
|
||||
|
||||
- 如您后续需要增加新的设备,该方法需要手动重启`xiaozhi-esp32-server服务端`以此更新设备信息**(重要**)。
|
||||
|
||||
- 需要您确保已经在HomeAssistant中集成`Xiaomi Home`,并将米家的设备导入进`HomeAssistant`。
|
||||
|
||||
- 需要您确保`xiaozhi-esp32-server智控台`能正常使用。
|
||||
|
||||
- 我的`xiaozhi-esp32-server智控台`和`HomeAssistant`部署在同一台机器的另一个端口,版本是`0.3.10`
|
||||
|
||||
```
|
||||
http://192.168.4.7:8002
|
||||
```
|
||||
|
||||
|
||||
### 配置步骤
|
||||
|
||||
#### 1. 登录`HomeAssistant`整理需要控制的设备清单
|
||||
|
||||
登录`HomeAssistant`,点击`左下角的设置`,然后进入`设备与服务`,再点击顶部的`实体`。
|
||||
|
||||
@@ -56,12 +92,7 @@ http://192.168.4.7:8002
|
||||
这段字符,我们成为“设备清单字符”需要保存好,等一下有用。
|
||||
|
||||
|
||||
### 2. 登录`HomeAssistant`拿到开发密钥
|
||||
|
||||
登录`HomeAssistant`,点击`左下角个人`,切换`安全`导航栏,划到底部`长期访问令牌`生成api_key。
|
||||
|
||||
|
||||
### 3. 登录`智控台`
|
||||
#### 2. 登录`智控台`
|
||||
|
||||
使用管理员账号,登录`智控台`。点击顶部菜单`参数管理`,搜索`plugins.home_assistant.`,会有三条结果出来
|
||||
|
||||
@@ -77,7 +108,7 @@ http://192.168.4.7:8123
|
||||
编辑`plugins.home_assistant.api_key`,把你从`HomeAssistant`复制过来的密钥,粘贴进去
|
||||
|
||||
|
||||
### 4. 设置`意图识别`函数
|
||||
#### 3. 设置`意图识别`函数
|
||||
|
||||
在智控台,点击顶部菜单“模型配置”,在左侧栏,找到“意图识别”,找到id为`Intent_function_call`的意图,点击编辑
|
||||
|
||||
@@ -94,18 +125,124 @@ 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
|
||||
#### 4. 手动重启xiaozhi-server
|
||||
|
||||
重启xiaozhi-server程序
|
||||
|
||||
|
||||
### 5. 确认角色配置是否设置了函数意图识别
|
||||
#### 5. 确认角色配置是否设置了函数意图识别
|
||||
|
||||
在智控台,点击顶部菜单“智能体管理”,找到设备所在的智能体,点击“配置角色”
|
||||
|
||||
确认意图识别(Intent),是否选择“函数调用意图识别”
|
||||
|
||||
|
||||
### 6. 唤醒设别进行控制
|
||||
#### 6. 唤醒设别进行控制
|
||||
|
||||
尝试和esp32说,“打开XXX灯”
|
||||
尝试和esp32说,“打开XXX灯”
|
||||
|
||||
## 方法2:小智将Home Assistant的语音助手作为LLM工具
|
||||
|
||||
### 功能描述
|
||||
|
||||
- 该方法有一个比较严重的缺点——**该方法无法使用小智开源生态的function_call插件功能的能力**,因为使用Home Assistant作为小智的LLM工具会将意图识别能力转让给Home Assistant。但是**这个方法是能体验到原生的Home Assistant操作功能,且小智的聊天能力不变**。如实在介意可以使用同样是Home Assistant支持的[方法3](##方法3:使用Home Assistant的MCP服务(推荐)),能够最大程度体验到Home Assistant的功能。
|
||||
|
||||
### 配置步骤:
|
||||
|
||||
#### 1. 配置Home Assistant的大模型语音助手。
|
||||
|
||||
**需要您提前配置好Home Assistant的语音助手或大模型工具。**
|
||||
|
||||
#### 2. 获取Home Assistant的语言助手的Agent ID.
|
||||
|
||||
1. 进入Home Assistant页面内。左侧点击`开发者助手`。
|
||||
2. 在打开的`开发者助手`内,点击`动作`选项卡(如图示操作1),在页面内的选项栏`动作`中,找到或输入`conversation.process(对话-处理)`并选择`对话(conversation): 处理`(如图示操作2)。
|
||||
|
||||

|
||||
|
||||
3. 在页面内勾选`代理(agent)`选项,在变成常亮的`对话代理(conversation agent)`内选择您步骤一配置好的语音助手名称,如图示,我这边配置好的是`ZhipuAi`并选择。
|
||||
|
||||

|
||||
|
||||
4. 选中后,点击表单左下方的`进入YAML模式`。
|
||||
|
||||

|
||||
|
||||
5. 复制其中的agent-id的值,例如图示中我的是`01JP2DYMBDF7F4ZA2DMCF2AGX2`(仅供参考)。
|
||||
|
||||

|
||||
|
||||
6. 切换到小智开源服务端`xiaozhi-esp32-server`的`config.yaml`文件内,在LLM配置中,找到Home Assistant,设置您的Home Assistant的网络地址,Api key和刚刚查询到的agent_id。
|
||||
7. 修改`config.yaml`文件内的`selected_module`属性的`LLM`为`HomeAssistant`,`Intent`为`nointent`。
|
||||
8. 重启小智开源服务端`xiaozhi-esp32-server`即可正常使用。
|
||||
|
||||
## 方法3:使用Home Assistant的MCP服务(推荐)
|
||||
|
||||
### 功能描述
|
||||
|
||||
- 需要您提前在Home Assistant内集成并安装好HA集成——[Model Context Protocol Server](https://www.home-assistant.io/integrations/mcp_server/)。
|
||||
|
||||
- 这个方法与方法2都是HA官方提供的解决方法,与方法2不同的是,您可以正常使用小智开源服务端`xiaozhi-esp32-server`的开源共建的插件,同时允许您随意使用任何一个支持function_call功能的LLM大模型。
|
||||
|
||||
### 配置步骤
|
||||
|
||||
#### 1. 安装Home Assistant的MCP服务集成。
|
||||
|
||||
集成官方网址——[Model Context Protocol Server](https://www.home-assistant.io/integrations/mcp_server/)。。
|
||||
|
||||
或跟随以下手动操作。
|
||||
|
||||
> - 前往Home Assistant页面的**[设置 > 设备和服务(Settings > Devices & Services.)](https://my.home-assistant.io/redirect/integrations)**。
|
||||
>
|
||||
> - 在右下角,选择 **[添加集成(Add Integration)](https://my.home-assistant.io/redirect/config_flow_start?domain=mcp_server)**按钮。
|
||||
>
|
||||
> - 从列表中选择**模型上下文协议服务器(Model Context Protocol Server)**。
|
||||
>
|
||||
> - 按照屏幕上的说明完成设置。
|
||||
|
||||
#### 2. 配置小智开源服务端MCP配置信息
|
||||
|
||||
切换到小智开源服务端`xiaozhi-esp32-server`的`mcp_server_settings.json`文件内,在`"mcpServers"`的括号内添加以下内容:
|
||||
|
||||
```json
|
||||
"Home Assistant": {
|
||||
"command": "mcp-proxy",
|
||||
"args": [
|
||||
"http://YOUR_HA_HOST/mcp_server/sse"
|
||||
],
|
||||
"env": {
|
||||
"API_ACCESS_TOKEN": "YOUR_API_ACCESS_TOKEN"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
1. **替换配置:**
|
||||
- 替换`args`内的`YOUR_HA_HOST`为您的HA服务地址,如果你的服务地址已经包含了https/http字样(例如`http://192.168.1.101:8123`),则只需要填入`192.168.1.101:8123`即可。
|
||||
- 将`env`内`API_ACCESS_TOKEN`的`YOUR_API_ACCESS_TOKEN`替换成您之前获取到的开发密钥api key。
|
||||
2. **如果你添加配置是在`"mcpServers"`的括号内后续没有新的`mcpServers`的配置时,需要把最后的逗号`,`移除**,否则可能会解析失败。
|
||||
|
||||
**最后效果参考以下(参考如下)**:
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"Home Assistant": {
|
||||
"command": "mcp-proxy",
|
||||
"args": [
|
||||
"http://192.168.1.101:8123/mcp_server/sse"
|
||||
],
|
||||
"env": {
|
||||
"API_ACCESS_TOKEN": "abcd.efghi.jkl"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 配置小智开源服务端的系统配置
|
||||
|
||||
1. **选择任意一款支持function_call的LLM大模型作为小智的LLM聊天助手(但不要选择Home Assistant作为LLM工具)**,本次我选择的模型是:免费的ChatGLM,它支持functioncall函数调用,但部分时候调用不太稳定,如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-1-5-pro-32k-250115。
|
||||
|
||||
2. 切换到小智开源服务端`xiaozhi-esp32-server`的`config.yaml`文件内,设置您的LLM大模型配置,并且将`selected_module`配置的`Intent`调整为`function_call`。
|
||||
|
||||
3. 重启小智开源服务端`xiaozhi-esp32-server`即可正常使用。
|
||||
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 149 KiB |
@@ -16,4 +16,4 @@ https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh
|
||||
|
||||
# manager-web 、manager-api接口协议
|
||||
|
||||
https://2662r3426b.vicp.fun/xiaozhi/v1/doc.html
|
||||
https://2662r3426b.vicp.fun/xiaozhi/doc.html
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<captcha.version>1.6.2</captcha.version>
|
||||
<guava.version>33.0.0-jre</guava.version>
|
||||
<liquibase-core.version>4.20.0</liquibase-core.version>
|
||||
<aliyun-sms-version>4.1.0</aliyun-sms-version>
|
||||
<okio-version>3.4.0</okio-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -37,6 +39,7 @@
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-spring</artifactId>
|
||||
@@ -197,6 +200,22 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<!-- 阿里云短信sdk -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
<version>${aliyun-sms-version}</version>
|
||||
</dependency>
|
||||
<!-- 阿里云短信sdk使用的okio:1.15.0版本有安全漏洞:GHSA-w33c-445m-f8w7 现在升级成3.4.0安全版本-->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio</artifactId>
|
||||
<version>${okio-version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<!-- 阿里云maven仓库 -->
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package xiaozhi.common.config;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableAspectJAutoProxy(exposeProxy = true)
|
||||
public class AsyncConfig {
|
||||
|
||||
@Bean(name = "taskExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setMaxPoolSize(4);
|
||||
executor.setQueueCapacity(1000);
|
||||
executor.setThreadNamePrefix("AsyncThread-");
|
||||
// 设置拒绝策略:由调用线程执行
|
||||
executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
|
||||
@Override
|
||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||
try {
|
||||
// 如果线程池已满,则由调用线程执行
|
||||
r.run();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("执行异步任务失败", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package xiaozhi.common.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 常量
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
@@ -109,31 +111,24 @@ public interface Constant {
|
||||
*/
|
||||
String FILE_EXTENSION_SEG = ".";
|
||||
|
||||
/**
|
||||
* 无记忆
|
||||
*/
|
||||
String MEMORY_NO_MEM = "Memory_nomem";
|
||||
|
||||
enum SysBaseParam {
|
||||
/**
|
||||
* 系统全称
|
||||
* ICP备案号
|
||||
*/
|
||||
SYS_NAME("SYS_NAME"),
|
||||
BEIAN_ICP_NUM("server.beian_icp_num"),
|
||||
/**
|
||||
* 系统简称
|
||||
* GA备案号
|
||||
*/
|
||||
SYS_SHORT_NAME("SYS_SHORT_NAME"),
|
||||
BEIAN_GA_NUM("server.beian_ga_num"),
|
||||
/**
|
||||
* 系统描述
|
||||
* 系统名称
|
||||
*/
|
||||
SYS_DES("SYS_DES"),
|
||||
/**
|
||||
* 登录失败几次锁定
|
||||
*/
|
||||
LOGIN_LOCK_COUNT("LOGIN_LOCK_COUNT"),
|
||||
/**
|
||||
* 账号失败锁定分钟数
|
||||
*/
|
||||
LOGIN_LOCK_TIME("LOGIN_LOCK_TIME"),
|
||||
/**
|
||||
* TOKEN强验证
|
||||
*/
|
||||
SYS_TOKEN_SECURITY("SYS_TOKEN_SECURITY");
|
||||
SERVER_NAME("server.name");
|
||||
|
||||
private String value;
|
||||
|
||||
@@ -146,6 +141,46 @@ public interface Constant {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统短信
|
||||
*/
|
||||
enum SysMSMParam {
|
||||
/**
|
||||
* 阿里云授权keyID
|
||||
*/
|
||||
ALIYUN_SMS_ACCESS_KEY_ID("aliyun.sms.access_key_id"),
|
||||
/**
|
||||
* 阿里云授权密钥
|
||||
*/
|
||||
ALIYUN_SMS_ACCESS_KEY_SECRET("aliyun.sms.access_key_secret"),
|
||||
/**
|
||||
* 阿里云短信签名
|
||||
*/
|
||||
ALIYUN_SMS_SIGN_NAME("aliyun.sms.sign_name"),
|
||||
/**
|
||||
* 阿里云短信模板
|
||||
*/
|
||||
ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE("aliyun.sms.sms_code_template_code"),
|
||||
/**
|
||||
* 单号码最大短信发送条数
|
||||
*/
|
||||
SERVER_SMS_MAX_SEND_COUNT("server.sms_max_send_count"),
|
||||
/**
|
||||
* 是否开启手机注册
|
||||
*/
|
||||
SERVER_ENABLE_MOBILE_REGISTER("server.enable_mobile_register");
|
||||
|
||||
private String value;
|
||||
|
||||
SysMSMParam(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据状态
|
||||
*/
|
||||
@@ -174,8 +209,48 @@ public interface Constant {
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
enum ChatHistoryConfEnum {
|
||||
IGNORE(0, "不记录"),
|
||||
RECORD_TEXT(1, "记录文本"),
|
||||
RECORD_TEXT_AUDIO(2, "文本音频都记录");
|
||||
|
||||
private final int code;
|
||||
private final String name;
|
||||
|
||||
ChatHistoryConfEnum(int code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.3.12";
|
||||
public static final String VERSION = "0.4.4";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
*/
|
||||
String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL";
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
enum DictType {
|
||||
/**
|
||||
* 手机区号
|
||||
*/
|
||||
MOBILE_AREA("MOBILE_AREA");
|
||||
|
||||
private String value;
|
||||
|
||||
DictType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public interface ErrorCode {
|
||||
int ACCOUNT_DISABLE = 10005;
|
||||
int IDENTIFIER_NOT_NULL = 10006;
|
||||
int CAPTCHA_ERROR = 10007;
|
||||
int SUB_MENU_EXIST = 10008;
|
||||
int PHONE_NOT_NULL = 10008;
|
||||
int PASSWORD_ERROR = 10009;
|
||||
|
||||
int SUPERIOR_DEPT_ERROR = 10011;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package xiaozhi.common.exception;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
@@ -60,4 +66,20 @@ public class RenExceptionHandler {
|
||||
return new Result<Void>().error(404, "资源不存在");
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
||||
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
|
||||
String errorMsg = allErrors.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(err -> {
|
||||
String msg = err.getDefaultMessage();
|
||||
return (msg != null && !msg.trim().isEmpty()) ? msg : null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse("请求参数错误!");
|
||||
|
||||
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -62,6 +62,13 @@ public class RedisKeys {
|
||||
return "agent:device:count:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体最后连接时间缓存key
|
||||
*/
|
||||
public static String getAgentDeviceLastConnectedAtById(String id) {
|
||||
return "agent:device:lastConnected:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置缓存key
|
||||
*/
|
||||
@@ -96,4 +103,40 @@ public class RedisKeys {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码的缓存key
|
||||
*/
|
||||
public static String getSMSValidateCodeKey(String phone) {
|
||||
return "sms:Validate:Code:" + phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码最后发送时间的缓存key
|
||||
*/
|
||||
public static String getSMSLastSendTimeKey(String phone) {
|
||||
return "sms:Validate:Code:" + phone + ":last_send_time";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码今日发送次数的缓存key
|
||||
*/
|
||||
public static String getSMSTodayCountKey(String phone) {
|
||||
return "sms:Validate:Code:" + phone + ":today_count";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import xiaozhi.common.utils.ResourcesUtils;
|
||||
|
||||
/**
|
||||
* Redis工具类
|
||||
@@ -23,6 +25,9 @@ public class RedisUtils {
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private ResourcesUtils resourceUtils;
|
||||
|
||||
/**
|
||||
* 默认过期时长为24小时,单位:秒
|
||||
*/
|
||||
@@ -40,6 +45,24 @@ public class RedisUtils {
|
||||
*/
|
||||
public final static long NOT_EXPIRE = -1L;
|
||||
|
||||
public Long increment(String key, long expire) {
|
||||
Long increment = redisTemplate.opsForValue().increment(key, 1L);
|
||||
if (expire != NOT_EXPIRE) {
|
||||
expire(key, expire);
|
||||
}
|
||||
return increment;
|
||||
}
|
||||
|
||||
public Long increment(String key) {
|
||||
return redisTemplate.opsForValue().increment(key, 1L);
|
||||
}
|
||||
|
||||
public Long decrement(String key) {
|
||||
return redisTemplate.opsForValue().decrement(key, 1L);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void set(String key, Object value, long expire) {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
if (expire != NOT_EXPIRE) {
|
||||
@@ -134,7 +157,7 @@ public class RedisUtils {
|
||||
*/
|
||||
public void emptyAll() {
|
||||
// Lua 脚本 FLUSHALL是redis清空所有库的命令
|
||||
String luaScript ="redis.call('FLUSHALL')";
|
||||
String luaScript =resourceUtils.loadString("lua/emptyAll.lua");
|
||||
|
||||
// 创建 DefaultRedisScript 对象
|
||||
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
|
||||
@@ -147,4 +170,26 @@ public class RedisUtils {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在redis指定key的值,如果值为空,着设置key的默认值
|
||||
* @param key redis的key
|
||||
* @param defaultValue 默认值
|
||||
* @param expiresInSecond 过期时间
|
||||
* @return 返回key的值
|
||||
*/
|
||||
public String getKeyOrCreate(String key, String defaultValue,Long expiresInSecond) {
|
||||
// Lua 脚本
|
||||
String luaScript = resourceUtils.loadString("lua/getKeyOrCreate.lua");
|
||||
|
||||
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
|
||||
redisScript.setScriptText(luaScript);
|
||||
redisScript.setResultType(String.class);
|
||||
|
||||
// 执行 Lua 脚本
|
||||
List<String> keys = Collections.singletonList(key);
|
||||
return redisTemplate.execute(redisScript, keys, defaultValue,expiresInSecond);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.common.service.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -18,6 +19,7 @@ import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
@@ -45,6 +47,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
* @param params 分页查询参数
|
||||
* @param defaultOrderField 默认排序字段
|
||||
* @param isAsc 排序方式
|
||||
* @see xiaozhi.common.constant.Constant
|
||||
* params.put(Constant.PAGE, "1");
|
||||
* params.put(Constant.LIMIT, "10");
|
||||
* params.put(Constant.ORDER_FIELD, "field"); // 单个字段
|
||||
* params.put(Constant.ORDER_FIELD, List.of("field1", "field2")); // 多个字段
|
||||
* params.put(Constant.ORDER, "asc");
|
||||
*/
|
||||
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
||||
// 分页参数
|
||||
@@ -65,28 +73,34 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
params.put(Constant.PAGE, page);
|
||||
|
||||
// 排序字段
|
||||
String orderField = (String) params.get(Constant.ORDER_FIELD);
|
||||
Object orderField = params.get(Constant.ORDER_FIELD);
|
||||
String order = (String) params.get(Constant.ORDER);
|
||||
|
||||
// 前端字段排序
|
||||
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
|
||||
if (Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.asc(orderField));
|
||||
List<String> orderFields = new ArrayList<>();
|
||||
|
||||
// 处理排序字段
|
||||
if (orderField instanceof String) {
|
||||
orderFields.add((String) orderField);
|
||||
} else if (orderField instanceof List) {
|
||||
orderFields.addAll((List<String>) orderField);
|
||||
}
|
||||
|
||||
// 有排序字段则排序
|
||||
if (CollectionUtils.isNotEmpty(orderFields)) {
|
||||
if (StringUtils.isNotBlank(order) && Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.ascs(orderFields.toArray(new String[0])));
|
||||
} else {
|
||||
return page.addOrder(OrderItem.desc(orderField));
|
||||
return page.addOrder(OrderItem.descs(orderFields.toArray(new String[0])));
|
||||
}
|
||||
}
|
||||
|
||||
// 没有排序字段,则不排序
|
||||
if (StringUtils.isBlank(defaultOrderField)) {
|
||||
return page;
|
||||
}
|
||||
|
||||
// 默认排序
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
// 没有排序字段,使用默认排序
|
||||
if (StringUtils.isNotBlank(defaultOrderField)) {
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
}
|
||||
}
|
||||
|
||||
return page;
|
||||
@@ -128,12 +142,10 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
return SqlHelper.retBool(result);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<M> currentMapperClass() {
|
||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Class<T> currentModelClass() {
|
||||
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>
|
||||
implements CrudService<T, D> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<D> currentDtoClass() {
|
||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ public class DateUtils {
|
||||
* 时间格式(yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
public final static String DATE_TIME_MILLIS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
|
||||
|
||||
/**
|
||||
* 日期格式化 日期格式为:yyyy-MM-dd
|
||||
@@ -63,6 +65,19 @@ public class DateUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static String getDateTimeNow() {
|
||||
return getDateTimeNow(DATE_TIME_PATTERN);
|
||||
}
|
||||
|
||||
public static String getDateTimeNow(String pattern) {
|
||||
return format(new Date(), pattern);
|
||||
}
|
||||
|
||||
public static String millsToSecond(long mills) {
|
||||
return String.format("%.3f", mills / 1000.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒
|
||||
* @param date
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* 资源处理工具
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ResourcesUtils {
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
/**
|
||||
* 读取资源,返回字符串
|
||||
* @param fileName 资源路径:resources下开始
|
||||
* @return 字符串
|
||||
*/
|
||||
public String loadString(String fileName) {
|
||||
Resource resource = resourceLoader.getResource("classpath:" + fileName);
|
||||
StringBuilder luaScriptBuilder = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(resource.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
luaScriptBuilder.append(line).append("\n");
|
||||
}
|
||||
} catch (IOException e){
|
||||
log.error("方法:loadString()读取资源失败--{}",e.getMessage());
|
||||
throw new RenException("读取资源失败");
|
||||
}
|
||||
return luaScriptBuilder.toString();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package xiaozhi.common.validator;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
@@ -45,4 +46,32 @@ public class ValidatorUtils {
|
||||
throw new RenException(constraint.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际手机号正则表达式
|
||||
* 要求必须带国际区号,格式:+[国家代码][手机号]
|
||||
* 例如:
|
||||
* - +8613800138000
|
||||
* - +12345678900
|
||||
* - +447123456789
|
||||
*/
|
||||
private static final String INTERNATIONAL_PHONE_REGEX = "^\\+[1-9]\\d{0,3}[1-9]\\d{4,14}$";
|
||||
|
||||
/**
|
||||
* 校验手机号是否有效
|
||||
* 要求必须带国际区号,格式:+[国家代码][手机号]
|
||||
* 例如:+8613800138000
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isValidPhone(String phone) {
|
||||
if (phone == null || phone.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证必须带国际区号的手机号格式
|
||||
Pattern pattern = Pattern.compile(INTERNATIONAL_PHONE_REGEX);
|
||||
return pattern.matcher(phone).matches();
|
||||
}
|
||||
}
|
||||
@@ -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.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -25,16 +30,24 @@ import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
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.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
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.AgentTemplateService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@@ -46,6 +59,9 @@ public class AgentController {
|
||||
private final AgentService agentService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final DeviceService deviceService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取用户智能体列表")
|
||||
@@ -80,7 +96,7 @@ public class AgentController {
|
||||
@PostMapping
|
||||
@Operation(summary = "创建智能体")
|
||||
@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);
|
||||
|
||||
// 获取默认模板
|
||||
@@ -95,6 +111,8 @@ public class AgentController {
|
||||
entity.setMemModelId(template.getMemModelId());
|
||||
entity.setIntentModelId(template.getIntentModelId());
|
||||
entity.setSystemPrompt(template.getSystemPrompt());
|
||||
entity.setSummaryMemory(template.getSummaryMemory());
|
||||
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||
entity.setLangCode(template.getLangCode());
|
||||
entity.setLanguage(template.getLanguage());
|
||||
}
|
||||
@@ -108,13 +126,29 @@ public class AgentController {
|
||||
// ID、智能体编码和排序会在Service层自动生成
|
||||
agentService.insert(entity);
|
||||
|
||||
return new Result<>();
|
||||
return new Result<String>().ok(entity.getId());
|
||||
}
|
||||
|
||||
@PutMapping("/saveMemory/{macAddress}")
|
||||
@Operation(summary = "根据设备id更新智能体")
|
||||
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null) {
|
||||
return new Result<>();
|
||||
}
|
||||
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
||||
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
||||
return updateAgentById(device.getAgentId(), agentUpdateDTO);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
||||
return updateAgentById(id, dto);
|
||||
}
|
||||
|
||||
private Result<Void> updateAgentById(String id, AgentUpdateDTO dto) {
|
||||
// 先查询现有实体
|
||||
AgentEntity existingEntity = agentService.getAgentById(id);
|
||||
if (existingEntity == null) {
|
||||
@@ -152,6 +186,12 @@ public class AgentController {
|
||||
if (dto.getSystemPrompt() != null) {
|
||||
existingEntity.setSystemPrompt(dto.getSystemPrompt());
|
||||
}
|
||||
if (dto.getSummaryMemory() != null) {
|
||||
existingEntity.setSummaryMemory(dto.getSummaryMemory());
|
||||
}
|
||||
if (dto.getChatHistoryConf() != null) {
|
||||
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
|
||||
}
|
||||
if (dto.getLangCode() != null) {
|
||||
existingEntity.setLangCode(dto.getLangCode());
|
||||
}
|
||||
@@ -167,8 +207,16 @@ public class AgentController {
|
||||
existingEntity.setUpdater(user.getId());
|
||||
existingEntity.setUpdatedAt(new Date());
|
||||
|
||||
// 更新记忆策略
|
||||
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
// 删除所有记录
|
||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
|
||||
existingEntity.setSummaryMemory("");
|
||||
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
|
||||
// 删除音频数据
|
||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
||||
}
|
||||
agentService.updateById(existingEntity);
|
||||
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -178,6 +226,8 @@ public class AgentController {
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
// 先删除关联的设备
|
||||
deviceService.deleteByAgentId(id);
|
||||
// 删除关联的聊天记录
|
||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
return new Result<>();
|
||||
@@ -192,4 +242,71 @@ public class AgentController {
|
||||
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.Param;
|
||||
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
|
||||
@@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao<AgentEntity> {
|
||||
* @return 设备数量
|
||||
*/
|
||||
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||
*
|
||||
* @param macAddress 设备MAC地址
|
||||
* @return 默认智能体信息
|
||||
*/
|
||||
@Select(" SELECT a.* FROM ai_device d " +
|
||||
" LEFT JOIN ai_agent a ON d.agent_id = a.id " +
|
||||
" WHERE d.mac_address = #{macAddress} " +
|
||||
" ORDER BY d.id DESC LIMIT 1")
|
||||
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||
|
||||
/**
|
||||
* {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/5/8
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiAgentChatAudioDao extends BaseMapper<AgentChatAudioEntity> {
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除音频ID
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAudioIdByAgentId(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;
|
||||
}
|
||||
@@ -27,9 +27,16 @@ public class AgentDTO {
|
||||
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
||||
private String llmModelName;
|
||||
|
||||
@Schema(description = "记忆模型ID", example = "mem_model_01")
|
||||
private String memModelId;
|
||||
|
||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
||||
private Date lastConnectedAt;
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体记忆更新DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能体记忆更新对象")
|
||||
public class AgentMemoryDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
}
|
||||
@@ -45,6 +45,13 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false)
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
||||
private String langCode;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -48,9 +48,16 @@ public class AgentEntity {
|
||||
@Schema(description = "意图模型标识")
|
||||
private String intentModelId;
|
||||
|
||||
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)")
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
@Schema(description = "角色设定参数")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "语言编码")
|
||||
private String langCode;
|
||||
|
||||
|
||||
@@ -69,11 +69,20 @@ public class AgentTemplateEntity implements Serializable {
|
||||
*/
|
||||
private String intentModelId;
|
||||
|
||||
/**
|
||||
* 聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)
|
||||
*/
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
/**
|
||||
* 角色设定参数
|
||||
*/
|
||||
private String systemPrompt;
|
||||
|
||||
/**
|
||||
* 总结记忆
|
||||
*/
|
||||
private String summaryMemory;
|
||||
/**
|
||||
* 语言编码
|
||||
*/
|
||||
|
||||
@@ -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,47 @@
|
||||
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
|
||||
* @param deleteAudio 是否删除音频
|
||||
* @param deleteText 是否删除文本
|
||||
*/
|
||||
void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText);
|
||||
}
|
||||
@@ -8,38 +8,75 @@ import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
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> {
|
||||
|
||||
/**
|
||||
* 管理员获取所有智能体列表(分页)
|
||||
* 获取管理员智能体列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 分页数据
|
||||
*/
|
||||
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 获取智能体详情
|
||||
* 根据ID获取智能体
|
||||
*
|
||||
* @param id 智能体ID
|
||||
* @return 智能体实体
|
||||
*/
|
||||
AgentEntity getAgentById(String id);
|
||||
|
||||
/**
|
||||
* 删除这个用户的所有
|
||||
*
|
||||
* @param userId
|
||||
* 插入智能体
|
||||
*
|
||||
* @param entity 智能体实体
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean insert(AgentEntity entity);
|
||||
|
||||
/**
|
||||
* 根据用户ID删除智能体
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
void deleteAgentByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 获取用户智能体列表
|
||||
*
|
||||
* @param userId
|
||||
* @return
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 智能体列表
|
||||
*/
|
||||
List<AgentDTO> getUserAgents(Long userId);
|
||||
|
||||
/**
|
||||
* 获取智能体的设备数量
|
||||
*
|
||||
* 根据智能体ID获取设备数量
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 设备数量
|
||||
*/
|
||||
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,112 @@
|
||||
package xiaozhi.modules.agent.service.biz.impl;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
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);
|
||||
|
||||
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||
if (agentEntity == null) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
|
||||
Integer chatHistoryConf = agentEntity.getChatHistoryConf();
|
||||
String agentId = agentEntity.getId();
|
||||
|
||||
if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT.getCode())) {
|
||||
saveChatText(report, agentId, macAddress, null);
|
||||
} else if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode())) {
|
||||
String audioId = saveChatAudio(report);
|
||||
saveChatText(report, agentId, macAddress, audioId);
|
||||
}
|
||||
|
||||
// 更新设备最后对话时间
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||
*/
|
||||
private String saveChatAudio(AgentChatHistoryReportDTO report) {
|
||||
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 null;
|
||||
}
|
||||
}
|
||||
return audioId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装上报数据
|
||||
*/
|
||||
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId) {
|
||||
|
||||
// 构建聊天记录实体
|
||||
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||
.macAddress(macAddress)
|
||||
.agentId(agentId)
|
||||
.sessionId(report.getSessionId())
|
||||
.chatType(report.getChatType())
|
||||
.content(report.getContent())
|
||||
.audioId(audioId)
|
||||
.build();
|
||||
|
||||
// 保存数据
|
||||
agentChatHistoryService.save(entity);
|
||||
|
||||
log.info("设备 {} 对应智能体 {} 上报成功", macAddress, agentId);
|
||||
}
|
||||
}
|
||||
@@ -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,93 @@
|
||||
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, Boolean deleteAudio, Boolean deleteText) {
|
||||
if (deleteAudio) {
|
||||
baseMapper.deleteAudioByAgentId(agentId);
|
||||
}
|
||||
if (deleteAudio && !deleteText) {
|
||||
baseMapper.deleteAudioIdByAgentId(agentId);
|
||||
}
|
||||
if (deleteText) {
|
||||
baseMapper.deleteHistoryByAgentId(agentId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,14 @@ import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
@@ -21,25 +22,20 @@ import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||
private final AgentDao agentDao;
|
||||
|
||||
@Autowired
|
||||
private TimbreService timbreModelService;
|
||||
|
||||
@Autowired
|
||||
private ModelConfigService modelConfigService;
|
||||
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
public AgentServiceImpl(AgentDao agentDao) {
|
||||
this.agentDao = agentDao;
|
||||
}
|
||||
private final TimbreService timbreModelService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -51,7 +47,15 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
@Override
|
||||
public AgentEntity getAgentById(String id) {
|
||||
return agentDao.selectById(id);
|
||||
AgentEntity agent = agentDao.selectById(id);
|
||||
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||
} else if (agent != null && agent.getMemModelId() != null
|
||||
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||
&& agent.getChatHistoryConf() == null) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,12 +102,17 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
// 获取 LLM 模型名称
|
||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||
|
||||
// 获取记忆模型名称
|
||||
dto.setMemModelId(agent.getMemModelId());
|
||||
|
||||
// 获取 TTS 音色名称
|
||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||
|
||||
// 获取智能体最近的最后连接时长
|
||||
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
||||
|
||||
// 获取设备数量
|
||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
@@ -130,4 +139,29 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package xiaozhi.modules.config.controller;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
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.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.sys.dto.ConfigSecretDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* xiaozhi-server 配置获取
|
||||
@@ -29,33 +25,20 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
||||
@AllArgsConstructor
|
||||
public class ConfigController {
|
||||
private final ConfigService configService;
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
@PostMapping("server-base")
|
||||
@Operation(summary = "获取配置")
|
||||
public Result<Object> getConfig(@RequestBody ConfigSecretDTO dto) {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
checkSecret(dto.getSecret());
|
||||
public Result<Object> getConfig() {
|
||||
Object config = configService.getConfig(true);
|
||||
return new Result<Object>().ok(config);
|
||||
}
|
||||
|
||||
@PostMapping("agent-models")
|
||||
@Operation(summary = "获取智能体模型")
|
||||
public Result<Object> getAgentModels(@RequestBody AgentModelsDTO dto) {
|
||||
public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
checkSecret(dto.getSecret());
|
||||
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
||||
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
|
||||
@Schema(description = "获取智能体模型配置DTO")
|
||||
public class AgentModelsDTO {
|
||||
@NotBlank(message = "密钥不能为空")
|
||||
@Schema(description = "密钥")
|
||||
private String secret;
|
||||
|
||||
@NotBlank(message = "设备MAC地址不能为空")
|
||||
@Schema(description = "设备MAC地址")
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
@@ -61,15 +62,16 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
|
||||
// 构建模块配置
|
||||
buildModuleConfig(
|
||||
agent.getAgentName(),
|
||||
agent.getSystemPrompt(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
agent.getLlmModelId(),
|
||||
agent.getTtsModelId(),
|
||||
agent.getMemModelId(),
|
||||
agent.getIntentModelId(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
result,
|
||||
isCache);
|
||||
|
||||
@@ -108,6 +110,17 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
// 获取单台设备每天最多输出字数
|
||||
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||
|
||||
// 获取聊天记录配置
|
||||
Integer chatHistoryConf = agent.getChatHistoryConf();
|
||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
chatHistoryConf = Constant.ChatHistoryConfEnum.IGNORE.getCode();
|
||||
} else if (agent.getMemModelId() != null
|
||||
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||
&& agent.getChatHistoryConf() == null) {
|
||||
chatHistoryConf = Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode();
|
||||
}
|
||||
result.put("chat_history_conf", chatHistoryConf);
|
||||
// 如果客户端已实例化模型,则不返回
|
||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||
@@ -117,23 +130,12 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
||||
agent.setAsrModelId(null);
|
||||
}
|
||||
String alreadySelectedLlmModelId = (String) selectedModule.get("LLM");
|
||||
if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) {
|
||||
agent.setLlmModelId(null);
|
||||
}
|
||||
String alreadySelectedMemModelId = (String) selectedModule.get("Memory");
|
||||
if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) {
|
||||
agent.setMemModelId(null);
|
||||
}
|
||||
String alreadySelectedIntentModelId = (String) selectedModule.get("Intent");
|
||||
if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) {
|
||||
agent.setIntentModelId(null);
|
||||
}
|
||||
|
||||
// 构建模块配置
|
||||
buildModuleConfig(
|
||||
agent.getAgentName(),
|
||||
agent.getSystemPrompt(),
|
||||
agent.getSummaryMemory(),
|
||||
voice,
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
@@ -153,7 +155,6 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
* @param paramsList 系统参数列表
|
||||
* @return 配置信息
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object buildConfig(Map<String, Object> config) {
|
||||
|
||||
// 查询所有系统参数
|
||||
@@ -235,6 +236,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private void buildModuleConfig(
|
||||
String assistantName,
|
||||
String prompt,
|
||||
String summaryMemory,
|
||||
String voice,
|
||||
String vadModelId,
|
||||
String asrModelId,
|
||||
@@ -270,7 +272,8 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||
intentLLMModelId = null;
|
||||
}
|
||||
} else if ("function_call".equals(map.get("type"))) {
|
||||
}
|
||||
if (map.get("functions") != null) {
|
||||
String functionStr = (String) map.get("functions");
|
||||
if (StringUtils.isNotBlank(functionStr)) {
|
||||
String[] functions = functionStr.split("\\;");
|
||||
@@ -294,5 +297,6 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||
}
|
||||
result.put("prompt", prompt);
|
||||
result.put("summaryMemory", summaryMemory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.device.utils.NetworkUtil;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||
@@ -53,7 +52,7 @@ public class OTAController {
|
||||
clientId = deviceId;
|
||||
}
|
||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||
boolean macAddressValid = isMacAddressValid(macAddress);
|
||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||
@@ -102,4 +101,19 @@ public class OTAController {
|
||||
.contentLength(jsonBytes.length)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package xiaozhi.modules.device.dao;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
@@ -8,4 +10,12 @@ import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
|
||||
@Mapper
|
||||
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||
/**
|
||||
* 获取此智能体全部设备的最后连接时间
|
||||
*
|
||||
* @param agentId 智能体id
|
||||
* @return
|
||||
*/
|
||||
Date getAllLastConnectedAtByAgentId(String agentId);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package xiaozhi.modules.device.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
@@ -78,4 +79,13 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
* @return 激活码
|
||||
*/
|
||||
String geCodeByDeviceId(String deviceId);
|
||||
|
||||
/**
|
||||
* 获取这个智能体设备理的最近的最后连接时间
|
||||
* @param agentId 智能体id
|
||||
* @return 返回设备最近的最后连接时间
|
||||
*/
|
||||
Date getLatestLastConnectionTime(String agentId);
|
||||
|
||||
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.aop.framework.AopContext;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
@@ -54,6 +56,24 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
private final RedisUtils redisUtils;
|
||||
private final OtaService otaService;
|
||||
|
||||
@Async
|
||||
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
||||
try {
|
||||
DeviceEntity device = new DeviceEntity();
|
||||
device.setId(deviceId);
|
||||
device.setLastConnectedAt(new Date());
|
||||
if (StringUtils.isNotBlank(appVersion)) {
|
||||
device.setAppVersion(appVersion);
|
||||
}
|
||||
deviceDao.updateById(device);
|
||||
if (StringUtils.isNotBlank(agentId)) {
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("异步更新设备连接信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||
if (StringUtils.isBlank(activationCode)) {
|
||||
@@ -118,13 +138,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
||||
|
||||
if (deviceById == null || deviceById.getAutoUpdate() != 0) {
|
||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||
// 设备未绑定,则返回当前上传的固件信息(不更新)以此兼容旧固件版本
|
||||
if (deviceById == null) {
|
||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
|
||||
response.setFirmware(firmware);
|
||||
} else {
|
||||
response.setFirmware(null);
|
||||
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
||||
if (deviceById.getAutoUpdate() != 0) {
|
||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||
response.setFirmware(firmware);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加WebSocket配置
|
||||
@@ -149,13 +176,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
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);
|
||||
// 如果设备存在,则异步更新上次连接时间和版本信息
|
||||
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
|
||||
: null;
|
||||
// 通过Spring代理调用异步方法
|
||||
((DeviceServiceImpl) AopContext.currentProxy()).updateDeviceConnectionInfo(deviceById.getAgentId(),
|
||||
deviceById.getId(), appVersion);
|
||||
} else {
|
||||
// 如果设备不存在,则生成激活码
|
||||
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||
@@ -258,6 +284,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
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);
|
||||
@@ -338,7 +378,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
|
||||
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
|
||||
firmware.setUrl(downloadUrl == null ? "" : downloadUrl);
|
||||
firmware.setUrl(downloadUrl == null ? Constant.INVALID_FIRMWARE_URL : downloadUrl);
|
||||
return firmware;
|
||||
}
|
||||
|
||||
@@ -370,4 +410,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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表示单播地址,合法
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package xiaozhi.modules.model.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/models/provider")
|
||||
@Tag(name = "模型供应器")
|
||||
public class ModelProviderController {
|
||||
|
||||
private final ModelProviderService modelProviderService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取模型供应器列表")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<ModelProviderDTO>> getListPage(ModelProviderDTO modelProviderDTO,
|
||||
@RequestParam(required = true, defaultValue = "0") String page,
|
||||
@RequestParam(required = true, defaultValue = "10") String limit) {
|
||||
return new Result<PageData<ModelProviderDTO>>()
|
||||
.ok(modelProviderService.getListPage(modelProviderDTO, page, limit));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "新增模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelProviderDTO> add(@RequestBody @Validated ModelProviderDTO modelProviderDTO) {
|
||||
ModelProviderDTO resp = modelProviderService.add(modelProviderDTO);
|
||||
return new Result<ModelProviderDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "修改模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelProviderDTO> edit(@RequestBody @Validated(UpdateGroup.class) ModelProviderDTO modelProviderDTO) {
|
||||
ModelProviderDTO resp = modelProviderService.edit(modelProviderDTO);
|
||||
return new Result<ModelProviderDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||
public Result<Void> delete(@RequestBody List<String> ids) {
|
||||
modelProviderService.delete(ids);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,29 +8,37 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
|
||||
@Data
|
||||
@Schema(description = "模型供应器/商")
|
||||
public class ModelProviderDTO implements Serializable {
|
||||
//
|
||||
// @Schema(description = "主键")
|
||||
// private Long id;
|
||||
@Schema(description = "主键")
|
||||
@NotBlank(message = "id不能为空", groups = UpdateGroup.class)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||
@NotBlank(message = "modelType不能为空")
|
||||
private String modelType;
|
||||
|
||||
@Schema(description = "供应器类型")
|
||||
@NotBlank(message = "providerCode不能为空")
|
||||
private String providerCode;
|
||||
|
||||
@Schema(description = "供应器名称")
|
||||
@NotBlank(message = "name不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "供应器字段列表(JSON格式)")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@NotBlank(message = "fields(JSON格式)不能为空")
|
||||
private String fields;
|
||||
|
||||
@Schema(description = "排序")
|
||||
@NotNull(message = "sort不能为空")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
|
||||
@@ -3,10 +3,8 @@ package xiaozhi.modules.model.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 com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -30,7 +28,6 @@ public class ModelProviderEntity {
|
||||
private String name;
|
||||
|
||||
@Schema(description = "供应器字段列表(JSON格式)")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private String fields;
|
||||
|
||||
@Schema(description = "排序")
|
||||
|
||||
@@ -2,8 +2,8 @@ package xiaozhi.modules.model.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
|
||||
public interface ModelProviderService {
|
||||
|
||||
@@ -11,11 +11,15 @@ public interface ModelProviderService {
|
||||
|
||||
List<ModelProviderDTO> getListByModelType(String modelType);
|
||||
|
||||
ModelProviderDTO add(ModelProviderEntity modelProviderEntity);
|
||||
ModelProviderDTO add(ModelProviderDTO modelProviderDTO);
|
||||
|
||||
ModelProviderDTO edit(ModelProviderEntity modelProviderEntity);
|
||||
ModelProviderDTO edit(ModelProviderDTO modelProviderDTO);
|
||||
|
||||
void delete();
|
||||
void delete(String id);
|
||||
|
||||
void delete(List<String> id);
|
||||
|
||||
PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit);
|
||||
|
||||
List<ModelProviderDTO> getList(String modelType, String provideCode);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
@@ -41,8 +39,6 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
private final ModelConfigDao modelConfigDao;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final AgentTemplateDao agentTemplateDao;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final AgentDao agentDao;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
package xiaozhi.modules.model.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import cn.hutool.json.JSONArray;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.model.dao.ModelProviderDao;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@@ -32,18 +42,78 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProviderDTO add(ModelProviderEntity modelProviderEntity) {
|
||||
return null;
|
||||
public PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit) {
|
||||
|
||||
Map<String, Object> params = new HashMap<String, Object>();
|
||||
params.put(Constant.PAGE, page);
|
||||
params.put(Constant.LIMIT, limit);
|
||||
params.put(Constant.ORDER_FIELD, List.of("model_type", "sort"));
|
||||
params.put(Constant.ORDER, "asc");
|
||||
|
||||
IPage<ModelProviderEntity> pageParam = getPage(params, null, true);
|
||||
|
||||
QueryWrapper<ModelProviderEntity> wrapper = new QueryWrapper<ModelProviderEntity>();
|
||||
|
||||
if (StringUtils.isNotBlank(modelProviderDTO.getModelType())) {
|
||||
wrapper.eq("model_type", modelProviderDTO.getModelType());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(modelProviderDTO.getName())) {
|
||||
wrapper.and(w -> w.like("name", modelProviderDTO.getName())
|
||||
.or()
|
||||
.like("provider_code", modelProviderDTO.getName()));
|
||||
}
|
||||
return getPageData(modelProviderDao.selectPage(pageParam, wrapper), ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String jsonString = "\"[]\"";
|
||||
JSONArray jsonArray = new JSONArray(jsonString);
|
||||
System.out.println("字符串转 JSONArray: " + jsonArray.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProviderDTO edit(ModelProviderEntity modelProviderEntity) {
|
||||
return null;
|
||||
public ModelProviderDTO add(ModelProviderDTO modelProviderDTO) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
modelProviderDTO.setCreator(user.getId());
|
||||
modelProviderDTO.setUpdater(user.getId());
|
||||
modelProviderDTO.setCreateDate(new Date());
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
// 去除Fields左右的双引号
|
||||
|
||||
modelProviderDTO.setFields(modelProviderDTO.getFields());
|
||||
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
|
||||
if (modelProviderDao.insert(entity) == 0) {
|
||||
throw new RenException("新增数据失败");
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
public ModelProviderDTO edit(ModelProviderDTO modelProviderDTO) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
modelProviderDTO.setUpdater(user.getId());
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
if (modelProviderDao
|
||||
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||
throw new RenException("修改数据失败");
|
||||
}
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
if (modelProviderDao.deleteById(id) == 0) {
|
||||
throw new RenException("删除数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(List<String> ids) {
|
||||
if (modelProviderDao.deleteBatchIds(ids) == 0) {
|
||||
throw new RenException("删除数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import jakarta.servlet.Filter;
|
||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||
import xiaozhi.modules.security.secret.ServerSecretFilter;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* Shiro的配置文件
|
||||
@@ -46,7 +48,7 @@ public class ShiroConfig {
|
||||
}
|
||||
|
||||
@Bean("shiroFilter")
|
||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
|
||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
|
||||
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
||||
config.setFilterOncePerRequest(true);
|
||||
|
||||
@@ -54,9 +56,11 @@ public class ShiroConfig {
|
||||
shiroFilter.setSecurityManager(securityManager);
|
||||
shiroFilter.setShiroFilterConfiguration(config);
|
||||
|
||||
// oauth过滤
|
||||
Map<String, Filter> filters = new HashMap<>();
|
||||
// oauth过滤
|
||||
filters.put("oauth2", new Oauth2Filter());
|
||||
// 服务密钥过滤
|
||||
filters.put("server", new ServerSecretFilter(sysParamsService));
|
||||
shiroFilter.setFilters(filters);
|
||||
|
||||
// 添加Shiro的内置过滤器
|
||||
@@ -76,11 +80,16 @@ public class ShiroConfig {
|
||||
filterMap.put("/doc.html", "anon");
|
||||
filterMap.put("/favicon.ico", "anon");
|
||||
filterMap.put("/user/captcha", "anon");
|
||||
filterMap.put("/user/smsVerification", "anon");
|
||||
filterMap.put("/user/login", "anon");
|
||||
filterMap.put("/user/pub-config", "anon");
|
||||
filterMap.put("/user/register", "anon");
|
||||
filterMap.put("/config/server-base", "anon");
|
||||
filterMap.put("/config/agent-models", "anon");
|
||||
filterMap.put("/user/retrieve-password", "anon");
|
||||
// 将config路径使用server服务过滤器
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/saveMemory/**", "server");
|
||||
filterMap.put("/agent/play/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
@@ -98,4 +107,4 @@ public class ShiroConfig {
|
||||
advisor.setSecurityManager(securityManager);
|
||||
return advisor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package xiaozhi.modules.security.config;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
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.module.SimpleModule;
|
||||
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
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
@@ -53,10 +63,29 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
// 忽略未知属性
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
// 日期格式转换
|
||||
// mapper.setDateFormat(new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN));
|
||||
// 设置时区
|
||||
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类型
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package xiaozhi.modules.security.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -24,13 +26,18 @@ import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.security.dto.LoginDTO;
|
||||
import xiaozhi.modules.security.dto.SmsVerificationDTO;
|
||||
import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.security.service.CaptchaService;
|
||||
import xiaozhi.modules.security.service.SysUserTokenService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
|
||||
/**
|
||||
* 登录控制层
|
||||
@@ -43,24 +50,43 @@ public class LoginController {
|
||||
private final SysUserService sysUserService;
|
||||
private final SysUserTokenService sysUserTokenService;
|
||||
private final CaptchaService captchaService;
|
||||
private final SysParamsService sysParamsService;
|
||||
private final SysDictDataService sysDictDataService;
|
||||
|
||||
@GetMapping("/captcha")
|
||||
@Operation(summary = "验证码")
|
||||
public void captcha(HttpServletResponse response, String uuid) throws IOException {
|
||||
// uuid不能为空
|
||||
AssertUtils.isBlank(uuid, ErrorCode.IDENTIFIER_NOT_NULL);
|
||||
|
||||
// 生成验证码
|
||||
captchaService.create(response, uuid);
|
||||
}
|
||||
|
||||
@PostMapping("/smsVerification")
|
||||
@Operation(summary = "短信验证码")
|
||||
public Result<Void> smsVerification(@RequestBody SmsVerificationDTO dto) {
|
||||
// 验证图形验证码
|
||||
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
|
||||
if (!validate) {
|
||||
throw new RenException("图形验证码错误");
|
||||
}
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
if (!isMobileRegister) {
|
||||
throw new RenException("没有开启手机注册,没法使用短信验证码功能");
|
||||
}
|
||||
// 发送短信验证码
|
||||
captchaService.sendSMSValidateCode(dto.getPhone());
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "登录")
|
||||
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
||||
if (!validate) {
|
||||
throw new RenException("验证码错误,请重新获取");
|
||||
throw new RenException("图形验证码错误,请重新获取");
|
||||
}
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
@@ -81,11 +107,29 @@ public class LoginController {
|
||||
if (!sysUserService.getAllowUserRegister()) {
|
||||
throw new RenException("当前不允许普通用户注册");
|
||||
}
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||
if (!validate) {
|
||||
throw new RenException("验证码错误,请重新获取");
|
||||
// 是否开启手机注册
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
boolean validate;
|
||||
if (isMobileRegister) {
|
||||
// 验证用户是否是手机号码
|
||||
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
|
||||
if (!validPhone) {
|
||||
throw new RenException("用户名不是手机号码,请重新输入");
|
||||
}
|
||||
// 验证短信验证码是否正常
|
||||
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
|
||||
if (!validate) {
|
||||
throw new RenException("手机验证码错误,请重新获取");
|
||||
}
|
||||
} else {
|
||||
// 验证是否正确输入验证码
|
||||
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
||||
if (!validate) {
|
||||
throw new RenException("图形验证码错误,请重新获取");
|
||||
}
|
||||
}
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
if (userDTO != null) {
|
||||
@@ -96,7 +140,6 @@ public class LoginController {
|
||||
userDTO.setPassword(login.getPassword());
|
||||
sysUserService.save(userDTO);
|
||||
return new Result<>();
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/info")
|
||||
@@ -118,12 +161,54 @@ public class LoginController {
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PutMapping("/retrieve-password")
|
||||
@Operation(summary = "找回密码")
|
||||
public Result<?> retrievePassword(@RequestBody RetrievePasswordDTO dto) {
|
||||
// 是否开启手机注册
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
if (!isMobileRegister) {
|
||||
throw new RenException("没有开启手机注册,没法使用找回密码功能");
|
||||
}
|
||||
// 判断非空
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
// 验证用户是否是手机号码
|
||||
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
|
||||
if (!validPhone) {
|
||||
throw new RenException("输入的手机号码格式不正确");
|
||||
}
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
|
||||
if (userDTO == null) {
|
||||
throw new RenException("输入的手机号码未注册");
|
||||
}
|
||||
// 验证短信验证码是否正常
|
||||
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
|
||||
// 判断是否通过验证
|
||||
if (!validate) {
|
||||
throw new RenException("输入的手机验证码错误");
|
||||
}
|
||||
|
||||
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@GetMapping("/pub-config")
|
||||
@Operation(summary = "公共配置")
|
||||
public Result<Map<String, Object>> pubConfig() {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("enableMobileRegister", sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class));
|
||||
config.put("version", Constant.VERSION);
|
||||
config.put("year", "©" + Calendar.getInstance().get(Calendar.YEAR));
|
||||
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(Constant.DictType.MOBILE_AREA.getValue());
|
||||
config.put("mobileAreaList", list);
|
||||
config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true));
|
||||
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
|
||||
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
|
||||
|
||||
return new Result<Map<String, Object>>().ok(config);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ public class LoginDTO implements Serializable {
|
||||
@NotBlank(message = "{sysuser.captcha.require}")
|
||||
private String captcha;
|
||||
|
||||
@Schema(description = "手机验证码")
|
||||
private String mobileCaptcha;
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
@NotBlank(message = "{sysuser.uuid.require}")
|
||||
private String captchaId;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package xiaozhi.modules.security.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 短信验证码请求DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "短信验证码请求")
|
||||
public class SmsVerificationDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "手机号码")
|
||||
@NotBlank(message = "{sysuser.username.require}")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "验证码")
|
||||
@NotBlank(message = "{sysuser.captcha.require}")
|
||||
private String captcha;
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
@NotBlank(message = "{sysuser.uuid.require}")
|
||||
private String captchaId;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,28 @@ public interface CaptchaService {
|
||||
|
||||
/**
|
||||
* 验证码效验
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @param code 验证码
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @param code 验证码
|
||||
* @param delete 是否删除验证码
|
||||
* @return true:成功 false:失败
|
||||
*/
|
||||
boolean validate(String uuid, String code);
|
||||
boolean validate(String uuid, String code, Boolean delete);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param phone 手机
|
||||
*/
|
||||
void sendSMSValidateCode(String phone);
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*
|
||||
* @param phone 手机
|
||||
* @param code 验证码
|
||||
* @param delete 是否删除验证码
|
||||
* @return true:成功 false:失败
|
||||
*/
|
||||
boolean validateSMSValidateCode(String phone, String code, Boolean delete);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.security.service.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -14,9 +15,13 @@ import com.wf.captcha.base.Captcha;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.security.service.CaptchaService;
|
||||
import xiaozhi.modules.sms.service.SmsService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
@@ -25,6 +30,10 @@ import xiaozhi.modules.security.service.CaptchaService;
|
||||
public class CaptchaServiceImpl implements CaptchaService {
|
||||
@Resource
|
||||
private RedisUtils redisUtils;
|
||||
@Resource
|
||||
private SmsService smsService;
|
||||
@Resource
|
||||
private SysParamsService sysParamsService;
|
||||
@Value("${renren.redis.open}")
|
||||
private boolean open;
|
||||
/**
|
||||
@@ -51,12 +60,12 @@ public class CaptchaServiceImpl implements CaptchaService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(String uuid, String code) {
|
||||
public boolean validate(String uuid, String code, Boolean delete) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return false;
|
||||
}
|
||||
// 获取验证码
|
||||
String captcha = getCache(uuid);
|
||||
String captcha = getCache(uuid, delete);
|
||||
|
||||
// 效验成功
|
||||
if (code.equalsIgnoreCase(captcha)) {
|
||||
@@ -66,21 +75,97 @@ public class CaptchaServiceImpl implements CaptchaService {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSMSValidateCode(String phone) {
|
||||
// 检查发送间隔
|
||||
String lastSendTimeKey = RedisKeys.getSMSLastSendTimeKey(phone);
|
||||
// 获取是否发送过,如果没有设置最后发送时间(60秒)
|
||||
String lastSendTime = redisUtils
|
||||
.getKeyOrCreate(lastSendTimeKey,
|
||||
String.valueOf(System.currentTimeMillis()), 60L);
|
||||
if (lastSendTime != null) {
|
||||
long lastSendTimeLong = Long.parseLong(lastSendTime);
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeDiff = currentTime - lastSendTimeLong;
|
||||
if (timeDiff < 60000) {
|
||||
throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查今日发送次数
|
||||
String todayCountKey = RedisKeys.getSMSTodayCountKey(phone);
|
||||
Integer todayCount = (Integer) redisUtils.get(todayCountKey);
|
||||
if (todayCount == null) {
|
||||
todayCount = 0;
|
||||
}
|
||||
|
||||
// 获取最大发送次数限制
|
||||
Integer maxSendCount = sysParamsService.getValueObject(
|
||||
Constant.SysMSMParam.SERVER_SMS_MAX_SEND_COUNT.getValue(),
|
||||
Integer.class);
|
||||
if (maxSendCount == null) {
|
||||
maxSendCount = 5; // 默认值
|
||||
}
|
||||
|
||||
if (todayCount >= maxSendCount) {
|
||||
throw new RenException("今日发送次数已达上限");
|
||||
}
|
||||
|
||||
String key = RedisKeys.getSMSValidateCodeKey(phone);
|
||||
String validateCodes = generateValidateCode(6);
|
||||
|
||||
// 设置验证码
|
||||
setCache(key, validateCodes);
|
||||
|
||||
// 更新今日发送次数
|
||||
if (todayCount == 0) {
|
||||
redisUtils.increment(todayCountKey, RedisUtils.DEFAULT_EXPIRE);
|
||||
} else {
|
||||
redisUtils.increment(todayCountKey);
|
||||
}
|
||||
|
||||
// 发送验证码短信
|
||||
smsService.sendVerificationCodeSms(phone, validateCodes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateSMSValidateCode(String phone, String code, Boolean delete) {
|
||||
String key = RedisKeys.getSMSValidateCodeKey(phone);
|
||||
return validate(key, code, delete);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定数量的随机数验证码
|
||||
*
|
||||
* @param length 数量
|
||||
* @return 随机码
|
||||
*/
|
||||
private String generateValidateCode(Integer length) {
|
||||
String chars = "0123456789"; // 字符范围可以自定义:数字
|
||||
Random random = new Random();
|
||||
StringBuilder code = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
code.append(chars.charAt(random.nextInt(chars.length())));
|
||||
}
|
||||
return code.toString();
|
||||
}
|
||||
|
||||
private void setCache(String key, String value) {
|
||||
if (open) {
|
||||
key = RedisKeys.getCaptchaKey(key);
|
||||
// 设置5分钟过期
|
||||
redisUtils.set(key, value, 300);
|
||||
} else {
|
||||
localCache.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private String getCache(String key) {
|
||||
private String getCache(String key, Boolean delete) {
|
||||
if (open) {
|
||||
key = RedisKeys.getCaptchaKey(key);
|
||||
String captcha = (String) redisUtils.get(key);
|
||||
// 删除验证码
|
||||
if (captcha != null) {
|
||||
if (captcha != null && delete) {
|
||||
redisUtils.delete(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package xiaozhi.modules.sms.service;
|
||||
|
||||
/**
|
||||
* 短信服务的方法定义接口
|
||||
*
|
||||
* @author zjy
|
||||
* @since 2025-05-12
|
||||
*/
|
||||
public interface SmsService {
|
||||
|
||||
/**
|
||||
* 发送验证码短信
|
||||
* @param phone 手机号码
|
||||
* @param VerificationCode 验证码
|
||||
*/
|
||||
void sendVerificationCodeSms(String phone, String VerificationCode) ;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package xiaozhi.modules.sms.service.imp;
|
||||
|
||||
import com.aliyun.dysmsapi20170525.Client;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
|
||||
import com.aliyun.teaopenapi.models.Config;
|
||||
import com.aliyun.teautil.models.RuntimeOptions;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.sms.service.SmsService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class ALiYunSmsService implements SmsService {
|
||||
private final SysParamsService sysParamsService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public void sendVerificationCodeSms(String phone, String VerificationCode) {
|
||||
Client client = createClient();
|
||||
String SignName = sysParamsService.getValue(Constant.SysMSMParam
|
||||
.ALIYUN_SMS_SIGN_NAME.getValue(),true);
|
||||
String TemplateCode = sysParamsService.getValue(Constant.SysMSMParam
|
||||
.ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE.getValue(),true);
|
||||
try {
|
||||
SendSmsRequest sendSmsRequest = new SendSmsRequest()
|
||||
.setSignName(SignName)
|
||||
.setTemplateCode(TemplateCode)
|
||||
.setPhoneNumbers(phone)
|
||||
.setTemplateParam(String.format("{\"code\":\"%s\"}", VerificationCode));
|
||||
RuntimeOptions runtime = new RuntimeOptions();
|
||||
// 复制代码运行请自行打印 API 的返回值
|
||||
SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
|
||||
log.info("发送短信响应的requestID: {}", sendSmsResponse.getBody().getRequestId());
|
||||
} catch (Exception e) {
|
||||
// 如果发送失败了退还这次发送数
|
||||
String todayCountKey = RedisKeys.getSMSTodayCountKey(phone);
|
||||
redisUtils.delete(todayCountKey);
|
||||
// 错误 message
|
||||
log.error(e.getMessage());
|
||||
throw new RenException("短信发送失败");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建阿里云连接
|
||||
* @return 返回连接对象
|
||||
*/
|
||||
private Client createClient(){
|
||||
String ACCESS_KEY_ID = sysParamsService.getValue(Constant.SysMSMParam
|
||||
.ALIYUN_SMS_ACCESS_KEY_ID.getValue(),true);
|
||||
String ACCESS_KEY_SECRET = sysParamsService.getValue(Constant.SysMSMParam
|
||||
.ALIYUN_SMS_ACCESS_KEY_SECRET.getValue(),true);
|
||||
try {
|
||||
Config config = new Config()
|
||||
.setAccessKeyId(ACCESS_KEY_ID)
|
||||
.setAccessKeySecret(ACCESS_KEY_SECRET);
|
||||
// 配置 Endpoint。中国站请使用dysmsapi.aliyuncs.com
|
||||
config.endpoint = "dysmsapi.aliyuncs.com";
|
||||
return new Client(config);
|
||||
}catch (Exception e){
|
||||
// 错误 message
|
||||
log.error(e.getMessage());
|
||||
throw new RenException("短信连接建立失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,6 @@ public class AdminController {
|
||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||
dto.setPage((String) params.get(Constant.PAGE));
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package xiaozhi.modules.sys.controller;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
|
||||
import xiaozhi.modules.sys.dto.ServerActionPayloadDTO;
|
||||
import xiaozhi.modules.sys.dto.ServerActionResponseDTO;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.utils.WebSocketClientManager;
|
||||
|
||||
/**
|
||||
* 服务端管理控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/server")
|
||||
@Tag(name = "服务端管理")
|
||||
@AllArgsConstructor
|
||||
public class ServerSideManageController {
|
||||
private final SysParamsService sysParamsService;
|
||||
private static final ObjectMapper objectMapper;
|
||||
static {
|
||||
objectMapper = new ObjectMapper();
|
||||
// 忽略json字符串中存在,但pojo中不存在对应字段的情况
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取Ws服务端列表")
|
||||
@GetMapping("/server-list")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<List<String>> getWsServerList() {
|
||||
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsText)) {
|
||||
return new Result<List<String>>().ok(Collections.emptyList());
|
||||
}
|
||||
return new Result<List<String>>().ok(Arrays.asList(wsText.split(";")));
|
||||
}
|
||||
|
||||
@Operation(summary = "通知python服务端更新配置")
|
||||
@PostMapping("/emit-action")
|
||||
@LogOperation("通知python服务端更新配置")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
|
||||
if (emitSeverActionDTO.getAction() == null) {
|
||||
throw new RenException("无效服务端操作");
|
||||
}
|
||||
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsText)) {
|
||||
throw new RenException("未配置服务端WebSocket地址");
|
||||
}
|
||||
String targetWs = emitSeverActionDTO.getTargetWs();
|
||||
String[] wsList = wsText.split(";");
|
||||
// 找到需要发起的
|
||||
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
|
||||
throw new RenException("目标WebSocket地址不存在");
|
||||
}
|
||||
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
|
||||
}
|
||||
|
||||
private Boolean emitServerActionByWs(String targetWsUri, ServerActionEnum actionEnum) {
|
||||
if (StringUtils.isBlank(targetWsUri) || actionEnum == null) {
|
||||
return false;
|
||||
}
|
||||
String serverSK = sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.add("device-id", UUID.randomUUID().toString());
|
||||
headers.add("client-id", UUID.randomUUID().toString());
|
||||
|
||||
try (WebSocketClientManager client = new WebSocketClientManager.Builder()
|
||||
.connectTimeout(3, TimeUnit.SECONDS)
|
||||
.maxSessionDuration(120, TimeUnit.SECONDS)
|
||||
.uri(targetWsUri)
|
||||
.headers(headers)
|
||||
.build()) {
|
||||
// 如果连接成功则发送一个json数据包并等待服务端响应
|
||||
client.sendJson(
|
||||
ServerActionPayloadDTO.build(
|
||||
actionEnum,
|
||||
Map.of("secret", serverSK)));
|
||||
// 等待服务端响应并持续监听信息
|
||||
client.listener((jsonText) -> {
|
||||
if (StringUtils.isBlank(jsonText)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ServerActionResponseDTO response = objectMapper.readValue(jsonText, ServerActionResponseDTO.class);
|
||||
Boolean isSuccess = ServerActionResponseDTO.isSuccess(response);
|
||||
return isSuccess;
|
||||
} catch (JsonProcessingException e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// 捕获全部错误,由全局异常处理器返回
|
||||
throw new RenException("WebSocket连接失败或连接超时");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public class SysParamsController {
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", in = ParameterIn.QUERY, required = true, ref = "int"),
|
||||
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段", in = ParameterIn.QUERY, ref = "String"),
|
||||
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)", in = ParameterIn.QUERY, ref = "String"),
|
||||
@Parameter(name = "paramCode", description = "参数编码", in = ParameterIn.QUERY, ref = "String")
|
||||
@Parameter(name = "paramCode", description = "参数编码或参数备注", in = ParameterIn.QUERY, ref = "String")
|
||||
})
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<SysParamsDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
@@ -111,7 +111,7 @@ public class SysParamsController {
|
||||
|
||||
/**
|
||||
* 验证WebSocket地址列表
|
||||
*
|
||||
*
|
||||
* @param urls WebSocket地址列表,以分号分隔
|
||||
* @return 验证结果
|
||||
*/
|
||||
@@ -143,6 +143,19 @@ public class SysParamsController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@RequestBody String[] ids) {
|
||||
// 效验数据
|
||||
AssertUtils.isArrayEmpty(ids, "id");
|
||||
|
||||
sysParamsService.delete(ids);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证OTA地址
|
||||
*/
|
||||
@@ -182,17 +195,4 @@ public class SysParamsController {
|
||||
throw new RenException("OTA接口验证失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@RequestBody String[] ids) {
|
||||
// 效验数据
|
||||
AssertUtils.isArrayEmpty(ids, "id");
|
||||
|
||||
sysParamsService.delete(ids);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
}
|
||||
|
||||