Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f7b7da184 | ||
|
|
c0020c8769 | ||
|
|
1c5e678cef | ||
|
|
3ab1b8ebc9 | ||
|
|
7635c68ccd | ||
|
|
a3b562b895 | ||
|
|
94d1493195 | ||
|
|
3a5bbe32b9 | ||
|
|
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 | ||
|
|
e2dffea423 | ||
|
|
3d5eaba46f | ||
|
|
1bb8fbd56f | ||
|
|
b699886953 | ||
|
|
12c957d48b | ||
|
|
48e890c1b1 | ||
|
|
05331f001a | ||
|
|
bde260b330 | ||
|
|
59ef51ea20 | ||
|
|
aa77bfdfc4 | ||
|
|
a2baef8911 | ||
|
|
3fcfb65d45 | ||
|
|
8f266bea0d | ||
|
|
0a765f4aac | ||
|
|
ee3f0555d1 | ||
|
|
9fc1285c09 | ||
|
|
e6e8ccee50 | ||
|
|
f9632af016 | ||
|
|
81359bf419 | ||
|
|
a144570ed7 | ||
|
|
0732b72e8f | ||
|
|
8332445a52 | ||
|
|
c9111d1bfc | ||
|
|
e7d999278d | ||
|
|
4043d20ea6 | ||
|
|
afe88ccfd6 | ||
|
|
56c3a04809 | ||
|
|
4530345a1f | ||
|
|
4a89ec8515 | ||
|
|
cb5f1c6485 | ||
|
|
15c0677a4b | ||
|
|
dc68b8148a | ||
|
|
1976034f12 | ||
|
|
727621fdac | ||
|
|
0f3806d358 | ||
|
|
dfb2bf3923 | ||
|
|
bf08a91191 | ||
|
|
f0e353cc1a | ||
|
|
456b1eddcc | ||
|
|
5e08f05437 | ||
|
|
9f9227bae4 | ||
|
|
76f47bce37 | ||
|
|
e1f54b30dc | ||
|
|
fc4e7e6c05 | ||
|
|
7aaa79b644 | ||
|
|
9b6f894be2 | ||
|
|
48f7ef2023 | ||
|
|
54692859fd | ||
|
|
1719244015 | ||
|
|
48f8c6c5b7 | ||
|
|
f0815e595b | ||
|
|
4f829017ed | ||
|
|
d2f8f05acb | ||
|
|
e8b1156bfd | ||
|
|
bf748773c8 | ||
|
|
a05b25ae90 | ||
|
|
3ceb3feccf | ||
|
|
b4944f23ef | ||
|
|
09c17b2dc5 | ||
|
|
6f7ff9f858 | ||
|
|
9e9af2a031 | ||
|
|
7d74e853ae | ||
|
|
1d627d570d | ||
|
|
9b0088f4f0 | ||
|
|
b50c7dbed4 | ||
|
|
f92c0313a6 | ||
|
|
233e1ee0a6 | ||
|
|
805b40ec76 | ||
|
|
86ca3e5be0 | ||
|
|
bb2143d9a0 | ||
|
|
ef662e0ccf | ||
|
|
30708a59f1 | ||
|
|
b0262cfe72 | ||
|
|
259886c1a8 | ||
|
|
f1b4b33727 | ||
|
|
38e8b690bd | ||
|
|
9a52b43e09 | ||
|
|
5e35cfc44a | ||
|
|
ac0ec6dfe6 | ||
|
|
361defc348 | ||
|
|
b9fc0da215 | ||
|
|
6ac8947168 | ||
|
|
5655930d12 | ||
|
|
18acc37324 | ||
|
|
e9155d719e | ||
|
|
16e77b02c2 | ||
|
|
adc1902649 | ||
|
|
1cc470bc5a | ||
|
|
b3f6efc7d0 | ||
|
|
d91fbf3ff4 | ||
|
|
e552593474 | ||
|
|
1f5a78f458 | ||
|
|
f490c15282 | ||
|
|
90a31debd1 | ||
|
|
416145a90e | ||
|
|
6285786282 | ||
|
|
f9f6a69c89 | ||
|
|
61da60bdf1 | ||
|
|
244c76793c | ||
|
|
39fe057f9f | ||
|
|
c9eb640510 | ||
|
|
766a706250 | ||
|
|
f55d6c2e60 |
@@ -9,26 +9,24 @@ assignees: ''
|
|||||||
## 🐛 问题描述
|
## 🐛 问题描述
|
||||||
<!-- 清晰简洁地描述问题是什么 -->
|
<!-- 清晰简洁地描述问题是什么 -->
|
||||||
|
|
||||||
## 🔍 复现步骤
|
## 🖥️ 环境信息
|
||||||
<!-- 详细描述复现问题的步骤 -->
|
- 部署方式: 全模块部署 还是 单Server部署
|
||||||
|
- 版本号: 例如 0.3.x
|
||||||
|
|
||||||
|
## 🔍 告诉我们,应该怎么复现这个问题
|
||||||
|
<!-- 这个很重要,方便我们快速定位 -->
|
||||||
1. 打开 '...'
|
1. 打开 '...'
|
||||||
2. 点击 '...'
|
2. 点击 '...'
|
||||||
3. 滚动到 '...'
|
3. 滚动到 '...'
|
||||||
4. 看到错误
|
4. 看到错误
|
||||||
|
|
||||||
## 🤔 预期行为
|
## 🤔 你原本希望是怎么样的
|
||||||
<!-- 简要描述预期的正确行为 -->
|
<!-- 简要描述预期的正确行为 -->
|
||||||
|
|
||||||
## 😯 截图
|
## 😯 提供一些截图
|
||||||
<!-- 如果适用,添加问题的截图 -->
|
<!-- 如果适用,添加问题的截图 -->
|
||||||
|
1. 比如日志截图,越多越好
|
||||||
## 🖥️ 环境信息
|
2. 比如界面反应
|
||||||
- 操作系统: [例如 Windows 10]
|
|
||||||
- 浏览器: [例如 Chrome 89]
|
|
||||||
- 项目版本: [例如 1.0.0]
|
|
||||||
- Python版本: [例如 3.9.13]
|
|
||||||
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
|
|
||||||
- Nodejs版本:[例如 v20.14.0]
|
|
||||||
|
|
||||||
## 📋 其他信息
|
## 📋 其他信息
|
||||||
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||||
|
|||||||
@@ -165,3 +165,4 @@ main/manager-api/.vscode
|
|||||||
main/manager-web/.webpack_cache/
|
main/manager-web/.webpack_cache/
|
||||||
main/xiaozhi-server/mysql
|
main/xiaozhi-server/mysql
|
||||||
uploadfile
|
uploadfile
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||||
|
|
||||||
|
|
||||||
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -11,12 +10,11 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="./README.md">English</a>
|
<a href="./README_en.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="./docs/FAQ.md">常见问题</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>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||||
@@ -132,17 +130,7 @@
|
|||||||
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
||||||
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
||||||
|
|
||||||
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
|
2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。
|
||||||
`config.yaml` 中开启防护:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
server:
|
|
||||||
auth:
|
|
||||||
# 开启防护
|
|
||||||
enabled: true
|
|
||||||
```
|
|
||||||
|
|
||||||
开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -163,14 +151,11 @@ server:
|
|||||||
|
|
||||||
```
|
```
|
||||||
智控台地址: https://2662r3426b.vicp.fun
|
智控台地址: https://2662r3426b.vicp.fun
|
||||||
|
|
||||||
|
服务测试工具: https://2662r3426b.vicp.fun/test/
|
||||||
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
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 |
|
| 本地使用 | 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)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||||
|
|
||||||
@@ -224,43 +224,36 @@ python app.py
|
|||||||
## 配置项目
|
## 配置项目
|
||||||
|
|
||||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
如果你的`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`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||||
|
|
||||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例
|
||||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
server:
|
||||||
|
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
|
||||||
|
prompt: |
|
||||||
|
我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||||
|
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
||||||
|
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
|
||||||
|
请你像一个人一样说话,请勿返回配置xml及其他特殊字符。
|
||||||
|
|
||||||
selected_module:
|
selected_module:
|
||||||
VAD: SileroVAD
|
LLM: DoubaoLLM
|
||||||
ASR: FunASR
|
|
||||||
LLM: ChatGLMLLM
|
|
||||||
TTS: EdgeTTS
|
|
||||||
# 默认不开启记忆,如需开启请看配置文件里的描述
|
|
||||||
Memory: nomem
|
|
||||||
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
|
||||||
Intent: nointent
|
|
||||||
```
|
|
||||||
|
|
||||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
|
||||||
使用时,在`selected_module`修改成对应的如下LLM配置的名称:
|
|
||||||
|
|
||||||
```
|
|
||||||
LLM:
|
LLM:
|
||||||
DeepSeekLLM:
|
|
||||||
type: openai
|
|
||||||
...
|
|
||||||
ChatGLMLLM:
|
ChatGLMLLM:
|
||||||
type: openai
|
api_key: xxxxxxxxxxxxxxx.xxxxxx
|
||||||
...
|
|
||||||
DifyLLM:
|
|
||||||
type: dify
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。
|
||||||
|
比如你要换更换模型,修改`selected_module`下的配置就行。
|
||||||
|
|
||||||
## 模型文件
|
## 模型文件
|
||||||
|
|
||||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unico
|
|||||||
如果还没有MySQL,你可以通过docker安装mysql
|
如果还没有MySQL,你可以通过docker安装mysql
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -e TZ=Asia/Shanghai -d mysql:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2.安装redis
|
## 2.安装redis
|
||||||
|
|||||||
@@ -106,10 +106,6 @@ VAD:
|
|||||||
|
|
||||||
### 8、更多问题,可联系我们反馈 💬
|
### 8、更多问题,可联系我们反馈 💬
|
||||||
|
|
||||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
|
||||||
|
|
||||||
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
|
也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
|
||||||
|
|
||||||
- 硬件烧录QQ群:适用于硬件烧录问题
|
|
||||||
- 开源服务端交流群:适用于服务端问题
|
|
||||||
- 产品建议联系人:适用于产品功能、产品设计等建议
|
|
||||||
@@ -86,14 +86,9 @@ idf.py set-target esp32s3
|
|||||||
idf.py menuconfig
|
idf.py menuconfig
|
||||||
```
|
```
|
||||||
|
|
||||||

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

|
|
||||||
|
|
||||||
## 第5步 编译固件
|
## 第5步 编译固件
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 149 KiB |
@@ -16,4 +16,4 @@ https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh
|
|||||||
|
|
||||||
# manager-web 、manager-api接口协议
|
# manager-web 、manager-api接口协议
|
||||||
|
|
||||||
https://2662r3426b.vicp.fun/xiaozhi/v1/doc.html
|
https://2662r3426b.vicp.fun/xiaozhi/doc.html
|
||||||
|
|||||||
@@ -197,6 +197,10 @@
|
|||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<!-- 阿里云maven仓库 -->
|
<!-- 阿里云maven仓库 -->
|
||||||
|
|||||||
@@ -177,5 +177,5 @@ public interface Constant {
|
|||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.3.12";
|
public static final String VERSION = "0.4.2";
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,13 @@ public class RedisKeys {
|
|||||||
return "agent:device:count:" + id;
|
return "agent:device:count:" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体最后连接时间缓存key
|
||||||
|
*/
|
||||||
|
public static String getAgentDeviceLastConnectedAtById(String id) {
|
||||||
|
return "agent:device:lastConnected:" + id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统配置缓存key
|
* 获取系统配置缓存key
|
||||||
*/
|
*/
|
||||||
@@ -96,4 +103,18 @@ public class RedisKeys {
|
|||||||
public static String getOtaDownloadCountKey(String uuid) {
|
public static String getOtaDownloadCountKey(String uuid) {
|
||||||
return "ota:download:count:" + 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,10 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
|||||||
return SqlHelper.retBool(result);
|
return SqlHelper.retBool(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected Class<M> currentMapperClass() {
|
protected Class<M> currentMapperClass() {
|
||||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override
|
@Override
|
||||||
public Class<T> currentModelClass() {
|
public Class<T> currentModelClass() {
|
||||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils;
|
|||||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
||||||
implements CrudService<T, D> {
|
implements CrudService<T, D> {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected Class<D> currentDtoClass() {
|
protected Class<D> currentDtoClass() {
|
||||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package xiaozhi.modules.agent.controller;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||||
|
|
||||||
|
@Tag(name = "智能体聊天历史管理")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/agent/chat-history")
|
||||||
|
public class AgentChatHistoryController {
|
||||||
|
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小智服务聊天上报请求
|
||||||
|
* <p>
|
||||||
|
* 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。
|
||||||
|
*
|
||||||
|
* @param request 包含上传文件及相关信息的请求对象
|
||||||
|
*/
|
||||||
|
@Operation(summary = "小智服务聊天上报请求")
|
||||||
|
@PostMapping("/report")
|
||||||
|
public Result<Boolean> uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) {
|
||||||
|
Boolean result = agentChatHistoryBizService.report(request);
|
||||||
|
return new Result<Boolean>().ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,13 @@ package xiaozhi.modules.agent.controller;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@@ -25,14 +30,20 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
@@ -46,6 +57,9 @@ public class AgentController {
|
|||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
private final AgentTemplateService agentTemplateService;
|
private final AgentTemplateService agentTemplateService;
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@Operation(summary = "获取用户智能体列表")
|
@Operation(summary = "获取用户智能体列表")
|
||||||
@@ -80,7 +94,7 @@ public class AgentController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "创建智能体")
|
@Operation(summary = "创建智能体")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||||
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
||||||
|
|
||||||
// 获取默认模板
|
// 获取默认模板
|
||||||
@@ -108,7 +122,7 @@ public class AgentController {
|
|||||||
// ID、智能体编码和排序会在Service层自动生成
|
// ID、智能体编码和排序会在Service层自动生成
|
||||||
agentService.insert(entity);
|
agentService.insert(entity);
|
||||||
|
|
||||||
return new Result<>();
|
return new Result<String>().ok(entity.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@@ -178,6 +192,8 @@ public class AgentController {
|
|||||||
public Result<Void> delete(@PathVariable String id) {
|
public Result<Void> delete(@PathVariable String id) {
|
||||||
// 先删除关联的设备
|
// 先删除关联的设备
|
||||||
deviceService.deleteByAgentId(id);
|
deviceService.deleteByAgentId(id);
|
||||||
|
// 删除关联的聊天记录
|
||||||
|
agentChatHistoryService.deleteByAgentId(id);
|
||||||
// 再删除智能体
|
// 再删除智能体
|
||||||
agentService.deleteById(id);
|
agentService.deleteById(id);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
@@ -192,4 +208,71 @@ public class AgentController {
|
|||||||
return new Result<List<AgentTemplateEntity>>().ok(list);
|
return new Result<List<AgentTemplateEntity>>().ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/sessions")
|
||||||
|
@Operation(summary = "获取智能体会话列表")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||||
|
})
|
||||||
|
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
|
||||||
|
@PathVariable("id") String id,
|
||||||
|
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
params.put("agentId", id);
|
||||||
|
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
|
||||||
|
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/chat-history/{sessionId}")
|
||||||
|
@Operation(summary = "获取智能体聊天记录")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<AgentChatHistoryDTO>> getAgentChatHistory(
|
||||||
|
@PathVariable("id") String id,
|
||||||
|
@PathVariable("sessionId") String sessionId) {
|
||||||
|
// 获取当前用户
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
if (!agentService.checkAgentPermission(id, user.getId())) {
|
||||||
|
return new Result<List<AgentChatHistoryDTO>>().error("没有权限查看该智能体的聊天记录");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询聊天记录
|
||||||
|
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||||
|
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/audio/{audioId}")
|
||||||
|
@Operation(summary = "获取音频下载ID")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
|
||||||
|
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||||
|
if (audioData == null) {
|
||||||
|
return new Result<String>().error("音频不存在");
|
||||||
|
}
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId);
|
||||||
|
return new Result<String>().ok(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/play/{uuid}")
|
||||||
|
@Operation(summary = "播放音频")
|
||||||
|
public ResponseEntity<byte[]> playAudio(@PathVariable("uuid") String uuid) {
|
||||||
|
|
||||||
|
String audioId = (String) redisUtils.get(RedisKeys.getAgentAudioIdKey(uuid));
|
||||||
|
if (StringUtils.isBlank(audioId)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||||
|
if (audioData == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
redisUtils.delete(RedisKeys.getAgentAudioIdKey(uuid));
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"play.wav\"")
|
||||||
|
.body(audioData);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
@@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao<AgentEntity> {
|
|||||||
* @return 设备数量
|
* @return 设备数量
|
||||||
*/
|
*/
|
||||||
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||||
|
*
|
||||||
|
* @param macAddress 设备MAC地址
|
||||||
|
* @return 默认智能体信息
|
||||||
|
*/
|
||||||
|
@Select(" SELECT a.* FROM ai_device d " +
|
||||||
|
" LEFT JOIN ai_agent a ON d.agent_id = a.id " +
|
||||||
|
" WHERE d.mac_address = #{macAddress} " +
|
||||||
|
" ORDER BY d.id DESC LIMIT 1")
|
||||||
|
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AiAgentChatAudioDao extends BaseMapper<AgentChatAudioEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package xiaozhi.modules.agent.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除音频
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteAudioByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除聊天历史记录
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteHistoryByAgentId(String agentId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "智能体聊天记录")
|
||||||
|
public class AgentChatHistoryDTO {
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "消息类型: 1-用户, 2-智能体")
|
||||||
|
private Byte chatType;
|
||||||
|
|
||||||
|
@Schema(description = "聊天内容")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@Schema(description = "音频ID")
|
||||||
|
private String audioId;
|
||||||
|
|
||||||
|
@Schema(description = "MAC地址")
|
||||||
|
private String macAddress;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小智设备聊天上报请求
|
||||||
|
*
|
||||||
|
* @author Haotian
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "小智设备聊天上报请求")
|
||||||
|
public class AgentChatHistoryReportDTO {
|
||||||
|
@Schema(description = "MAC地址", example = "00:11:22:33:44:55")
|
||||||
|
@NotBlank
|
||||||
|
private String macAddress;
|
||||||
|
@Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a")
|
||||||
|
@NotBlank
|
||||||
|
private String sessionId;
|
||||||
|
@Schema(description = "消息类型: 1-用户, 2-智能体", example = "1")
|
||||||
|
@NotNull
|
||||||
|
private Byte chatType;
|
||||||
|
@Schema(description = "聊天内容", example = "你好呀")
|
||||||
|
@NotBlank
|
||||||
|
private String content;
|
||||||
|
@Schema(description = "base64编码的opus音频数据", example = "")
|
||||||
|
private String audioBase64;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package xiaozhi.modules.agent.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体会话列表DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AgentChatSessionDTO {
|
||||||
|
/**
|
||||||
|
* 会话ID
|
||||||
|
*/
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天条数
|
||||||
|
*/
|
||||||
|
private Integer chatCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("ai_agent_chat_audio")
|
||||||
|
public class AgentChatAudioEntity {
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频opus数据
|
||||||
|
*/
|
||||||
|
private byte[] audio;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package xiaozhi.modules.agent.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@TableName(value = "ai_agent_chat_history")
|
||||||
|
public class AgentChatHistoryEntity {
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAC地址
|
||||||
|
*/
|
||||||
|
@TableField(value = "mac_address")
|
||||||
|
private String macAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体id
|
||||||
|
*/
|
||||||
|
@TableField(value = "agent_id")
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话ID
|
||||||
|
*/
|
||||||
|
@TableField(value = "session_id")
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型: 1-用户, 2-智能体
|
||||||
|
*/
|
||||||
|
@TableField(value = "chat_type")
|
||||||
|
private Byte chatType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天内容
|
||||||
|
*/
|
||||||
|
@TableField(value = "content")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频base64数据
|
||||||
|
*/
|
||||||
|
@TableField(value = "audio_id")
|
||||||
|
private String audioId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "created_at")
|
||||||
|
private Date createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
@TableField(value = "updated_at")
|
||||||
|
private Date updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表处理service
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
|
||||||
|
/**
|
||||||
|
* 保存音频数据
|
||||||
|
*
|
||||||
|
* @param audioData 音频数据
|
||||||
|
* @return 音频ID
|
||||||
|
*/
|
||||||
|
String saveAudio(byte[] audioData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取音频数据
|
||||||
|
*
|
||||||
|
* @param audioId 音频ID
|
||||||
|
* @return 音频数据
|
||||||
|
*/
|
||||||
|
byte[] getAudio(String audioId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package xiaozhi.modules.agent.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表处理service
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID获取会话列表
|
||||||
|
*
|
||||||
|
* @param params 查询参数,包含agentId、page、limit
|
||||||
|
* @return 分页的会话列表
|
||||||
|
*/
|
||||||
|
PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据会话ID获取聊天记录列表
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
* @return 聊天记录列表
|
||||||
|
*/
|
||||||
|
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除聊天记录
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteByAgentId(String agentId);
|
||||||
|
}
|
||||||
@@ -8,38 +8,75 @@ import xiaozhi.common.service.BaseService;
|
|||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体表处理service
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
public interface AgentService extends BaseService<AgentEntity> {
|
public interface AgentService extends BaseService<AgentEntity> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员获取所有智能体列表(分页)
|
* 获取管理员智能体列表
|
||||||
|
*
|
||||||
|
* @param params 查询参数
|
||||||
|
* @return 分页数据
|
||||||
*/
|
*/
|
||||||
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取智能体详情
|
* 根据ID获取智能体
|
||||||
|
*
|
||||||
|
* @param id 智能体ID
|
||||||
|
* @return 智能体实体
|
||||||
*/
|
*/
|
||||||
AgentEntity getAgentById(String id);
|
AgentEntity getAgentById(String id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除这个用户的所有
|
* 插入智能体
|
||||||
*
|
*
|
||||||
* @param userId
|
* @param entity 智能体实体
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
boolean insert(AgentEntity entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户ID删除智能体
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
*/
|
*/
|
||||||
void deleteAgentByUserId(Long userId);
|
void deleteAgentByUserId(Long userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户智能体列表
|
* 获取用户智能体列表
|
||||||
*
|
*
|
||||||
* @param userId
|
* @param userId 用户ID
|
||||||
* @return
|
* @return 智能体列表
|
||||||
*/
|
*/
|
||||||
List<AgentDTO> getUserAgents(Long userId);
|
List<AgentDTO> getUserAgents(Long userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取智能体的设备数量
|
* 根据智能体ID获取设备数量
|
||||||
*
|
*
|
||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
* @return 设备数量
|
* @return 设备数量
|
||||||
*/
|
*/
|
||||||
Integer getDeviceCountByAgentId(String agentId);
|
Integer getDeviceCountByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||||
|
*
|
||||||
|
* @param macAddress 设备MAC地址
|
||||||
|
* @return 默认智能体信息,不存在时返回null
|
||||||
|
*/
|
||||||
|
AgentEntity getDefaultAgentByMacAddress(String macAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否有权限访问智能体
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 是否有权限
|
||||||
|
*/
|
||||||
|
boolean checkAgentPermission(String agentId, Long userId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package xiaozhi.modules.agent.service.biz;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天历史业务逻辑层
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
public interface AgentChatHistoryBizService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天上报方法
|
||||||
|
*
|
||||||
|
* @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象
|
||||||
|
* 例如:设备MAC地址、文件类型、内容等
|
||||||
|
* @return 上传结果,true表示成功,false表示失败
|
||||||
|
*/
|
||||||
|
Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package xiaozhi.modules.agent.service.biz.impl;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AgentChatHistoryBizService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService {
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理聊天记录上报,包括文件上传和相关信息记录
|
||||||
|
*
|
||||||
|
* @param report 包含聊天上报所需信息的输入对象
|
||||||
|
* @return 上传结果,true表示成功,false表示失败
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean report(AgentChatHistoryReportDTO report) {
|
||||||
|
String macAddress = report.getMacAddress();
|
||||||
|
Byte chatType = report.getChatType();
|
||||||
|
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
||||||
|
|
||||||
|
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||||
|
String audioId = null;
|
||||||
|
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
||||||
|
try {
|
||||||
|
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
||||||
|
audioId = agentChatAudioService.saveAudio(audioData);
|
||||||
|
log.info("音频数据保存成功,audioId={}", audioId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("音频数据保存失败", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 组装上报数据
|
||||||
|
// 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||||
|
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||||
|
if (agentEntity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String agentId = agentEntity.getId();
|
||||||
|
log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
|
||||||
|
|
||||||
|
// 2.2 构建聊天记录实体
|
||||||
|
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||||
|
.macAddress(macAddress)
|
||||||
|
.agentId(agentId)
|
||||||
|
.sessionId(report.getSessionId())
|
||||||
|
.chatType(report.getChatType())
|
||||||
|
.content(report.getContent())
|
||||||
|
.audioId(audioId)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 3. 保存数据
|
||||||
|
agentChatHistoryService.save(entity);
|
||||||
|
// 4. 更新设备最后对话时间
|
||||||
|
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
|
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天音频数据表处理service {@link AgentChatAudioService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/5/8
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
|
||||||
|
implements AgentChatAudioService {
|
||||||
|
@Override
|
||||||
|
public String saveAudio(byte[] audioData) {
|
||||||
|
AgentChatAudioEntity entity = new AgentChatAudioEntity();
|
||||||
|
entity.setAudio(audioData);
|
||||||
|
save(entity);
|
||||||
|
return entity.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getAudio(String audioId) {
|
||||||
|
AgentChatAudioEntity entity = getById(audioId);
|
||||||
|
return entity != null ? entity.getAudio() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package xiaozhi.modules.agent.service.impl;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
|
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||||
|
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天记录表处理service {@link AgentChatHistoryService} impl
|
||||||
|
*
|
||||||
|
* @author Goody
|
||||||
|
* @version 1.0, 2025/4/30
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||||
|
implements AgentChatHistoryService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
|
||||||
|
String agentId = (String) params.get("agentId");
|
||||||
|
int page = Integer.parseInt(params.get(Constant.PAGE).toString());
|
||||||
|
int limit = Integer.parseInt(params.get(Constant.LIMIT).toString());
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.select("session_id", "MAX(created_at) as created_at", "COUNT(*) as chat_count")
|
||||||
|
.eq("agent_id", agentId)
|
||||||
|
.groupBy("session_id")
|
||||||
|
.orderByDesc("created_at");
|
||||||
|
|
||||||
|
// 执行分页查询
|
||||||
|
Page<Map<String, Object>> pageParam = new Page<>(page, limit);
|
||||||
|
IPage<Map<String, Object>> result = this.baseMapper.selectMapsPage(pageParam, wrapper);
|
||||||
|
|
||||||
|
List<AgentChatSessionDTO> records = result.getRecords().stream().map(map -> {
|
||||||
|
AgentChatSessionDTO dto = new AgentChatSessionDTO();
|
||||||
|
dto.setSessionId((String) map.get("session_id"));
|
||||||
|
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
|
||||||
|
dto.setChatCount(((Number) map.get("chat_count")).intValue());
|
||||||
|
return dto;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new PageData<>(records, result.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId) {
|
||||||
|
// 构建查询条件
|
||||||
|
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("agent_id", agentId)
|
||||||
|
.eq("session_id", sessionId)
|
||||||
|
.orderByAsc("created_at");
|
||||||
|
|
||||||
|
// 查询聊天记录
|
||||||
|
List<AgentChatHistoryEntity> historyList = list(wrapper);
|
||||||
|
|
||||||
|
// 转换为DTO
|
||||||
|
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteByAgentId(String agentId) {
|
||||||
|
baseMapper.deleteAudioByAgentId(agentId);
|
||||||
|
baseMapper.deleteHistoryByAgentId(agentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ import java.util.UUID;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -21,25 +21,20 @@ import xiaozhi.modules.agent.dao.AgentDao;
|
|||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.model.service.ModelConfigService;
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
import xiaozhi.modules.timbre.service.TimbreService;
|
import xiaozhi.modules.timbre.service.TimbreService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||||
private final AgentDao agentDao;
|
private final AgentDao agentDao;
|
||||||
|
private final TimbreService timbreModelService;
|
||||||
@Autowired
|
private final ModelConfigService modelConfigService;
|
||||||
private TimbreService timbreModelService;
|
private final RedisUtils redisUtils;
|
||||||
|
private final DeviceService deviceService;
|
||||||
@Autowired
|
|
||||||
private ModelConfigService modelConfigService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RedisUtils redisUtils;
|
|
||||||
|
|
||||||
public AgentServiceImpl(AgentDao agentDao) {
|
|
||||||
this.agentDao = agentDao;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
@@ -101,9 +96,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
// 获取 TTS 音色名称
|
// 获取 TTS 音色名称
|
||||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||||
|
|
||||||
|
// 获取智能体最近的最后连接时长
|
||||||
|
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
||||||
|
|
||||||
// 获取设备数量
|
// 获取设备数量
|
||||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@@ -130,4 +127,29 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
|
|
||||||
return deviceCount != null ? deviceCount : 0;
|
return deviceCount != null ? deviceCount : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AgentEntity getDefaultAgentByMacAddress(String macAddress) {
|
||||||
|
if (StringUtils.isEmpty(macAddress)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return agentDao.getDefaultAgentByMacAddress(macAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkAgentPermission(String agentId, Long userId) {
|
||||||
|
// 获取智能体信息
|
||||||
|
AgentEntity agent = getAgentById(agentId);
|
||||||
|
if (agent == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是超级管理员,直接返回true
|
||||||
|
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是智能体的所有者
|
||||||
|
return userId.equals(agent.getUserId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package xiaozhi.modules.config.controller;
|
package xiaozhi.modules.config.controller;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -8,15 +7,12 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
|
||||||
import xiaozhi.common.exception.RenException;
|
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.ValidatorUtils;
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
||||||
import xiaozhi.modules.config.service.ConfigService;
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.dto.ConfigSecretDTO;
|
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* xiaozhi-server 配置获取
|
* xiaozhi-server 配置获取
|
||||||
@@ -29,33 +25,20 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class ConfigController {
|
public class ConfigController {
|
||||||
private final ConfigService configService;
|
private final ConfigService configService;
|
||||||
private final SysParamsService sysParamsService;
|
|
||||||
|
|
||||||
@PostMapping("server-base")
|
@PostMapping("server-base")
|
||||||
@Operation(summary = "获取配置")
|
@Operation(summary = "获取配置")
|
||||||
public Result<Object> getConfig(@RequestBody ConfigSecretDTO dto) {
|
public Result<Object> getConfig() {
|
||||||
// 效验数据
|
|
||||||
ValidatorUtils.validateEntity(dto);
|
|
||||||
checkSecret(dto.getSecret());
|
|
||||||
Object config = configService.getConfig(true);
|
Object config = configService.getConfig(true);
|
||||||
return new Result<Object>().ok(config);
|
return new Result<Object>().ok(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("agent-models")
|
@PostMapping("agent-models")
|
||||||
@Operation(summary = "获取智能体模型")
|
@Operation(summary = "获取智能体模型")
|
||||||
public Result<Object> getAgentModels(@RequestBody AgentModelsDTO dto) {
|
public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
|
||||||
// 效验数据
|
// 效验数据
|
||||||
ValidatorUtils.validateEntity(dto);
|
ValidatorUtils.validateEntity(dto);
|
||||||
checkSecret(dto.getSecret());
|
|
||||||
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
||||||
return new Result<Object>().ok(models);
|
return new Result<Object>().ok(models);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSecret(String secret) {
|
|
||||||
String secretParam = sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
|
||||||
// 验证密钥
|
|
||||||
if (StringUtils.isBlank(secret) || !secret.equals(secretParam)) {
|
|
||||||
throw new RenException("密钥错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
@Schema(description = "获取智能体模型配置DTO")
|
@Schema(description = "获取智能体模型配置DTO")
|
||||||
public class AgentModelsDTO {
|
public class AgentModelsDTO {
|
||||||
@NotBlank(message = "密钥不能为空")
|
|
||||||
@Schema(description = "密钥")
|
|
||||||
private String secret;
|
|
||||||
|
|
||||||
@NotBlank(message = "设备MAC地址不能为空")
|
@NotBlank(message = "设备MAC地址不能为空")
|
||||||
@Schema(description = "设备MAC地址")
|
@Schema(description = "设备MAC地址")
|
||||||
|
|||||||
@@ -61,15 +61,15 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
|
|
||||||
// 构建模块配置
|
// 构建模块配置
|
||||||
buildModuleConfig(
|
buildModuleConfig(
|
||||||
agent.getAgentName(),
|
null,
|
||||||
agent.getSystemPrompt(),
|
null,
|
||||||
null,
|
null,
|
||||||
agent.getVadModelId(),
|
agent.getVadModelId(),
|
||||||
agent.getAsrModelId(),
|
agent.getAsrModelId(),
|
||||||
agent.getLlmModelId(),
|
null,
|
||||||
agent.getTtsModelId(),
|
null,
|
||||||
agent.getMemModelId(),
|
null,
|
||||||
agent.getIntentModelId(),
|
null,
|
||||||
result,
|
result,
|
||||||
isCache);
|
isCache);
|
||||||
|
|
||||||
@@ -117,18 +117,6 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
||||||
agent.setAsrModelId(null);
|
agent.setAsrModelId(null);
|
||||||
}
|
}
|
||||||
String alreadySelectedLlmModelId = (String) selectedModule.get("LLM");
|
|
||||||
if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) {
|
|
||||||
agent.setLlmModelId(null);
|
|
||||||
}
|
|
||||||
String alreadySelectedMemModelId = (String) selectedModule.get("Memory");
|
|
||||||
if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) {
|
|
||||||
agent.setMemModelId(null);
|
|
||||||
}
|
|
||||||
String alreadySelectedIntentModelId = (String) selectedModule.get("Intent");
|
|
||||||
if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) {
|
|
||||||
agent.setIntentModelId(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建模块配置
|
// 构建模块配置
|
||||||
buildModuleConfig(
|
buildModuleConfig(
|
||||||
@@ -153,7 +141,6 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
* @param paramsList 系统参数列表
|
* @param paramsList 系统参数列表
|
||||||
* @return 配置信息
|
* @return 配置信息
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Object buildConfig(Map<String, Object> config) {
|
private Object buildConfig(Map<String, Object> config) {
|
||||||
|
|
||||||
// 查询所有系统参数
|
// 查询所有系统参数
|
||||||
@@ -270,7 +257,8 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||||
intentLLMModelId = null;
|
intentLLMModelId = null;
|
||||||
}
|
}
|
||||||
} else if ("function_call".equals(map.get("type"))) {
|
}
|
||||||
|
if (map.get("functions") != null) {
|
||||||
String functionStr = (String) map.get("functions");
|
String functionStr = (String) map.get("functions");
|
||||||
if (StringUtils.isNotBlank(functionStr)) {
|
if (StringUtils.isNotBlank(functionStr)) {
|
||||||
String[] functions = functionStr.split("\\;");
|
String[] functions = functionStr.split("\\;");
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
|||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.utils.NetworkUtil;
|
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
@@ -53,7 +52,7 @@ public class OTAController {
|
|||||||
clientId = deviceId;
|
clientId = deviceId;
|
||||||
}
|
}
|
||||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
boolean macAddressValid = isMacAddressValid(macAddress);
|
||||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||||
@@ -102,4 +101,19 @@ public class OTAController {
|
|||||||
.contentLength(jsonBytes.length)
|
.contentLength(jsonBytes.length)
|
||||||
.body(json);
|
.body(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单判断mac地址是否有效(非严格)
|
||||||
|
*
|
||||||
|
* @param macAddress
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private boolean isMacAddressValid(String macAddress) {
|
||||||
|
if (StringUtils.isBlank(macAddress)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// MAC地址通常为12位十六进制数字,可以包含冒号或连字符分隔符
|
||||||
|
String macPattern = "^([0-9A-Za-z]{2}[:-]){5}([0-9A-Za-z]{2})$";
|
||||||
|
return macAddress.matches(macPattern);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package xiaozhi.modules.device.dao;
|
package xiaozhi.modules.device.dao;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
@@ -8,4 +10,12 @@ import xiaozhi.modules.device.entity.DeviceEntity;
|
|||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||||
|
/**
|
||||||
|
* 获取此智能体全部设备的最后连接时间
|
||||||
|
*
|
||||||
|
* @param agentId 智能体id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Date getAllLastConnectedAtByAgentId(String agentId);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package xiaozhi.modules.device.service;
|
package xiaozhi.modules.device.service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
@@ -78,4 +79,13 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
|||||||
* @return 激活码
|
* @return 激活码
|
||||||
*/
|
*/
|
||||||
String geCodeByDeviceId(String deviceId);
|
String geCodeByDeviceId(String deviceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取这个智能体设备理的最近的最后连接时间
|
||||||
|
* @param agentId 智能体id
|
||||||
|
* @return 返回设备最近的最后连接时间
|
||||||
|
*/
|
||||||
|
Date getLatestLastConnectionTime(String agentId);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,8 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
||||||
|
|
||||||
if (deviceById == null || deviceById.getAutoUpdate() != 0) {
|
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
||||||
|
if (deviceById != null && deviceById.getAutoUpdate() != 0) {
|
||||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||||
@@ -258,6 +259,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
return null;
|
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) {
|
private String getDeviceCacheKey(String deviceId) {
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
|||||||
@@ -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表示单播地址,合法
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -21,9 +21,7 @@ import xiaozhi.common.redis.RedisUtils;
|
|||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.agent.dao.AgentDao;
|
import xiaozhi.modules.agent.dao.AgentDao;
|
||||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
|
||||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||||
@@ -41,8 +39,6 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
private final ModelConfigDao modelConfigDao;
|
private final ModelConfigDao modelConfigDao;
|
||||||
private final ModelProviderService modelProviderService;
|
private final ModelProviderService modelProviderService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
private final AgentTemplateDao agentTemplateDao;
|
|
||||||
private final AgentTemplateService agentTemplateService;
|
|
||||||
private final AgentDao agentDao;
|
private final AgentDao agentDao;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import jakarta.servlet.Filter;
|
import jakarta.servlet.Filter;
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||||
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||||
|
import xiaozhi.modules.security.secret.ServerSecretFilter;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shiro的配置文件
|
* Shiro的配置文件
|
||||||
@@ -46,7 +48,7 @@ public class ShiroConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean("shiroFilter")
|
@Bean("shiroFilter")
|
||||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
|
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
|
||||||
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
||||||
config.setFilterOncePerRequest(true);
|
config.setFilterOncePerRequest(true);
|
||||||
|
|
||||||
@@ -54,9 +56,11 @@ public class ShiroConfig {
|
|||||||
shiroFilter.setSecurityManager(securityManager);
|
shiroFilter.setSecurityManager(securityManager);
|
||||||
shiroFilter.setShiroFilterConfiguration(config);
|
shiroFilter.setShiroFilterConfiguration(config);
|
||||||
|
|
||||||
// oauth过滤
|
|
||||||
Map<String, Filter> filters = new HashMap<>();
|
Map<String, Filter> filters = new HashMap<>();
|
||||||
|
// oauth过滤
|
||||||
filters.put("oauth2", new Oauth2Filter());
|
filters.put("oauth2", new Oauth2Filter());
|
||||||
|
// 服务密钥过滤
|
||||||
|
filters.put("server", new ServerSecretFilter(sysParamsService));
|
||||||
shiroFilter.setFilters(filters);
|
shiroFilter.setFilters(filters);
|
||||||
|
|
||||||
// 添加Shiro的内置过滤器
|
// 添加Shiro的内置过滤器
|
||||||
@@ -79,8 +83,10 @@ public class ShiroConfig {
|
|||||||
filterMap.put("/user/login", "anon");
|
filterMap.put("/user/login", "anon");
|
||||||
filterMap.put("/user/pub-config", "anon");
|
filterMap.put("/user/pub-config", "anon");
|
||||||
filterMap.put("/user/register", "anon");
|
filterMap.put("/user/register", "anon");
|
||||||
filterMap.put("/config/server-base", "anon");
|
// 将config路径使用server服务过滤器
|
||||||
filterMap.put("/config/agent-models", "anon");
|
filterMap.put("/config/**", "server");
|
||||||
|
filterMap.put("/agent/chat-history/report", "server");
|
||||||
|
filterMap.put("/agent/play/**", "anon");
|
||||||
filterMap.put("/**", "oauth2");
|
filterMap.put("/**", "oauth2");
|
||||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package xiaozhi.modules.security.config;
|
package xiaozhi.modules.security.config;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
@@ -18,6 +19,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||||
|
|
||||||
|
import xiaozhi.common.utils.DateUtils;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
@@ -53,10 +63,29 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
// 忽略未知属性
|
// 忽略未知属性
|
||||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
|
||||||
// 日期格式转换
|
// 设置时区
|
||||||
// mapper.setDateFormat(new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN));
|
|
||||||
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
|
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
|
||||||
|
|
||||||
|
// 配置Java8日期时间序列化
|
||||||
|
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||||
|
javaTimeModule.addSerializer(java.time.LocalDateTime.class, new LocalDateTimeSerializer(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
|
||||||
|
javaTimeModule.addSerializer(java.time.LocalDate.class, new LocalDateSerializer(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
|
||||||
|
javaTimeModule.addSerializer(java.time.LocalTime.class,
|
||||||
|
new LocalTimeSerializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||||
|
javaTimeModule.addDeserializer(java.time.LocalDateTime.class, new LocalDateTimeDeserializer(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
|
||||||
|
javaTimeModule.addDeserializer(java.time.LocalDate.class, new LocalDateDeserializer(
|
||||||
|
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
|
||||||
|
javaTimeModule.addDeserializer(java.time.LocalTime.class,
|
||||||
|
new LocalTimeDeserializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||||
|
mapper.registerModule(javaTimeModule);
|
||||||
|
|
||||||
|
// 配置java.util.Date的序列化和反序列化
|
||||||
|
SimpleDateFormat dateFormat = new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN);
|
||||||
|
mapper.setDateFormat(dateFormat);
|
||||||
|
|
||||||
// Long类型转String类型
|
// Long类型转String类型
|
||||||
SimpleModule simpleModule = new SimpleModule();
|
SimpleModule simpleModule = new SimpleModule();
|
||||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package xiaozhi.modules.security.secret;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.utils.HttpContextUtils;
|
||||||
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config API 过滤器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ServerSecretFilter extends AuthenticatingFilter {
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) {
|
||||||
|
// 获取请求token
|
||||||
|
String token = getRequestToken((HttpServletRequest) request);
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(token)) {
|
||||||
|
log.warn("createToken:token is empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServerSecretToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
||||||
|
// 对OPTIONS请求放行
|
||||||
|
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
|
||||||
|
// 获取token并校验
|
||||||
|
String token = getRequestToken((HttpServletRequest) servletRequest);
|
||||||
|
if (StringUtils.isBlank(token)) {
|
||||||
|
// token为空,返回401
|
||||||
|
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "服务器密钥不能为空");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token是否匹配
|
||||||
|
String serverSecret = getServerSecret();
|
||||||
|
if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) {
|
||||||
|
// token无效,返回401
|
||||||
|
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的服务器密钥");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送未授权响应
|
||||||
|
*/
|
||||||
|
private void sendUnauthorizedResponse(HttpServletResponse response, String message) {
|
||||||
|
response.setContentType("application/json;charset=utf-8");
|
||||||
|
response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
|
response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = JsonUtils.toJsonString(new Result<Void>().error(ErrorCode.UNAUTHORIZED, message));
|
||||||
|
response.getWriter().print(json);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("响应输出失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求的token
|
||||||
|
*/
|
||||||
|
private String getRequestToken(HttpServletRequest httpRequest) {
|
||||||
|
String token = null;
|
||||||
|
// 从header中获取token
|
||||||
|
String authorization = httpRequest.getHeader("Authorization");
|
||||||
|
if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
|
||||||
|
token = authorization.replace("Bearer ", "");
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getServerSecret() {
|
||||||
|
return sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package xiaozhi.modules.security.secret;
|
||||||
|
|
||||||
|
import org.apache.shiro.authc.AuthenticationToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config API Token
|
||||||
|
*/
|
||||||
|
public class ServerSecretToken implements AuthenticationToken {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final String token;
|
||||||
|
|
||||||
|
public ServerSecretToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,6 @@ public class AdminController {
|
|||||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||||
dto.setPage((String) params.get(Constant.PAGE));
|
dto.setPage((String) params.get(Constant.PAGE));
|
||||||
ValidatorUtils.validateEntity(dto);
|
ValidatorUtils.validateEntity(dto);
|
||||||
ValidatorUtils.validateEntity(dto);
|
|
||||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||||
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典数据管理
|
||||||
|
*
|
||||||
|
* @author czc
|
||||||
|
* @since 2025-04-30
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/dict/data")
|
||||||
|
@Tag(name = "字典数据管理")
|
||||||
|
public class SysDictDataController {
|
||||||
|
private final SysDictDataService sysDictDataService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "分页查询字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({ @Parameter(name = "dictTypeId", description = "字典类型ID", required = true),
|
||||||
|
@Parameter(name = "dictLabel", description = "数据标签"), @Parameter(name = "dictValue", description = "数据值"),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
|
||||||
|
public Result<PageData<SysDictDataVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
// 强制校验dictTypeId是否存在
|
||||||
|
if (!params.containsKey("dictTypeId") || StringUtils.isEmpty(String.valueOf(params.get("dictTypeId")))) {
|
||||||
|
return new Result<PageData<SysDictDataVO>>().error("dictTypeId不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
PageData<SysDictDataVO> page = sysDictDataService.page(params);
|
||||||
|
return new Result<PageData<SysDictDataVO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取字典数据详情")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<SysDictDataVO> get(@PathVariable("id") Long id) {
|
||||||
|
SysDictDataVO vo = sysDictDataService.get(id);
|
||||||
|
return new Result<SysDictDataVO>().ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@Operation(summary = "新增字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody SysDictDataDTO dto) {
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
sysDictDataService.save(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "修改字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> update(@RequestBody SysDictDataDTO dto) {
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
sysDictDataService.update(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@Operation(summary = "删除字典数据")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||||
|
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||||
|
sysDictDataService.delete(ids);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/type/{dictType}")
|
||||||
|
@Operation(summary = "获取字典数据列表")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<List<SysDictDataItem>> getDictDataByType(@PathVariable("dictType") String dictType) {
|
||||||
|
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(dictType);
|
||||||
|
return new Result<List<SysDictDataItem>>().ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package xiaozhi.modules.sys.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典类型管理
|
||||||
|
*
|
||||||
|
* @author czc
|
||||||
|
* @since 2025-04-30
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin/dict/type")
|
||||||
|
@Tag(name = "字典类型管理")
|
||||||
|
public class SysDictTypeController {
|
||||||
|
private final SysDictTypeService sysDictTypeService;
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "分页查询字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameters({ @Parameter(name = "dictType", description = "字典类型编码"),
|
||||||
|
@Parameter(name = "dictName", description = "字典类型名称"),
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
|
||||||
|
public Result<PageData<SysDictTypeVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
PageData<SysDictTypeVO> page = sysDictTypeService.page(params);
|
||||||
|
return new Result<PageData<SysDictTypeVO>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@Operation(summary = "获取字典类型详情")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<SysDictTypeVO> get(@PathVariable("id") Long id) {
|
||||||
|
SysDictTypeVO vo = sysDictTypeService.get(id);
|
||||||
|
return new Result<SysDictTypeVO>().ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/save")
|
||||||
|
@Operation(summary = "保存字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody SysDictTypeDTO dto) {
|
||||||
|
// 参数校验
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
|
||||||
|
sysDictTypeService.save(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "修改字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> update(@RequestBody SysDictTypeDTO dto) {
|
||||||
|
// 参数校验
|
||||||
|
ValidatorUtils.validateEntity(dto);
|
||||||
|
|
||||||
|
sysDictTypeService.update(dto);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/delete")
|
||||||
|
@Operation(summary = "删除字典类型")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||||
|
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||||
|
sysDictTypeService.delete(ids);
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,8 @@ import java.util.List;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
|
||||||
import xiaozhi.modules.sys.entity.DictData;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典数据
|
* 字典数据
|
||||||
@@ -15,10 +14,13 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
||||||
|
|
||||||
/**
|
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||||
* 字典数据列表
|
|
||||||
*/
|
|
||||||
List<DictData> getDictDataList();
|
|
||||||
|
|
||||||
List<SysDictDataDTO> getDataByTypeCode(String dictType);
|
/**
|
||||||
|
* 根据字典类型ID获取字典类型编码
|
||||||
|
*
|
||||||
|
* @param dictTypeId 字典类型ID
|
||||||
|
* @return 字典类型编码
|
||||||
|
*/
|
||||||
|
String getTypeByTypeId(Long dictTypeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package xiaozhi.modules.sys.dao;
|
package xiaozhi.modules.sys.dao;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
import xiaozhi.common.dao.BaseDao;
|
import xiaozhi.common.dao.BaseDao;
|
||||||
import xiaozhi.modules.sys.entity.DictType;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,9 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> {
|
public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> {
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典类型列表
|
|
||||||
*/
|
|
||||||
List<DictType> getDictTypeList();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package xiaozhi.modules.sys.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Schema(description = "配置密钥DTO")
|
|
||||||
public class ConfigSecretDTO {
|
|
||||||
@Schema(description = "密钥")
|
|
||||||
@NotBlank(message = "密钥不能为空")
|
|
||||||
private String secret;
|
|
||||||
}
|
|
||||||
@@ -51,5 +51,6 @@ public class SysDictTypeDTO implements Serializable {
|
|||||||
|
|
||||||
@Schema(description = "更新时间")
|
@Schema(description = "更新时间")
|
||||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||||
private Date updateDate;
|
private Date updateDate;
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ public class SysParamsDTO implements Serializable {
|
|||||||
|
|
||||||
@Schema(description = "值类型")
|
@Schema(description = "值类型")
|
||||||
@NotBlank(message = "{sysparams.valuetype.require}", groups = DefaultGroup.class)
|
@NotBlank(message = "{sysparams.valuetype.require}", groups = DefaultGroup.class)
|
||||||
@Pattern(regexp = "^(string|number|boolean|array)$", message = "{sysparams.valuetype.pattern}", groups = DefaultGroup.class)
|
@Pattern(regexp = "^(string|number|boolean|array|json)$", message = "{sysparams.valuetype.pattern}", groups = DefaultGroup.class)
|
||||||
private String valueType;
|
private String valueType;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package xiaozhi.modules.sys.entity;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典数据
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class DictData {
|
|
||||||
@JsonIgnore
|
|
||||||
private Long dictTypeId;
|
|
||||||
private String dictLabel;
|
|
||||||
private String dictValue;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package xiaozhi.modules.sys.entity;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字典类型
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class DictType {
|
|
||||||
@JsonIgnore
|
|
||||||
private Long id;
|
|
||||||
private String dictType;
|
|
||||||
private List<DictData> dataList = new ArrayList<>();
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ import xiaozhi.common.entity.BaseEntity;
|
|||||||
@TableName("sys_dict_type")
|
@TableName("sys_dict_type")
|
||||||
public class SysDictTypeEntity extends BaseEntity {
|
public class SysDictTypeEntity extends BaseEntity {
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型编码
|
||||||
*/
|
*/
|
||||||
private String dictType;
|
private String dictType;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,25 +1,70 @@
|
|||||||
package xiaozhi.modules.sys.service;
|
package xiaozhi.modules.sys.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据字典
|
* 数据字典
|
||||||
*/
|
*/
|
||||||
public interface SysDictDataService extends BaseService<SysDictDataEntity> {
|
public interface SysDictDataService extends BaseService<SysDictDataEntity> {
|
||||||
|
|
||||||
PageData<SysDictDataDTO> page(Map<String, Object> params);
|
/**
|
||||||
|
* 分页查询数据字典信息
|
||||||
|
*
|
||||||
|
* @param params 查询参数,包含分页信息和查询条件
|
||||||
|
* @return 返回数据字典的分页查询结果
|
||||||
|
*/
|
||||||
|
PageData<SysDictDataVO> page(Map<String, Object> params);
|
||||||
|
|
||||||
SysDictDataDTO get(Long id);
|
/**
|
||||||
|
* 根据ID获取数据字典实体
|
||||||
|
*
|
||||||
|
* @param id 数据字典实体的唯一标识
|
||||||
|
* @return 返回数据字典实体的详细信息
|
||||||
|
*/
|
||||||
|
SysDictDataVO get(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存新的数据字典项
|
||||||
|
*
|
||||||
|
* @param dto 数据字典项的保存数据传输对象
|
||||||
|
*/
|
||||||
void save(SysDictDataDTO dto);
|
void save(SysDictDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新数据字典项
|
||||||
|
*
|
||||||
|
* @param dto 数据字典项的更新数据传输对象
|
||||||
|
*/
|
||||||
void update(SysDictDataDTO dto);
|
void update(SysDictDataDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据字典项
|
||||||
|
*
|
||||||
|
* @param ids 要删除的数据字典项的ID数组
|
||||||
|
*/
|
||||||
void delete(Long[] ids);
|
void delete(Long[] ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典类型ID删除对应的字典数据
|
||||||
|
*
|
||||||
|
* @param dictTypeId 字典类型ID
|
||||||
|
*/
|
||||||
|
void deleteByTypeId(Long dictTypeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字典类型获取字典数据列表
|
||||||
|
*
|
||||||
|
* @param dictType 字典类型
|
||||||
|
* @return 返回字典数据列表
|
||||||
|
*/
|
||||||
|
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,25 +6,55 @@ import java.util.Map;
|
|||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.BaseService;
|
import xiaozhi.common.service.BaseService;
|
||||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||||
import xiaozhi.modules.sys.entity.DictType;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据字典
|
* 数据字典
|
||||||
*/
|
*/
|
||||||
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
||||||
|
|
||||||
PageData<SysDictTypeDTO> page(Map<String, Object> params);
|
/**
|
||||||
|
* 分页查询字典类型信息
|
||||||
|
*
|
||||||
|
* @param params 查询参数,包含分页信息和查询条件
|
||||||
|
* @return 返回分页的字典类型数据
|
||||||
|
*/
|
||||||
|
PageData<SysDictTypeVO> page(Map<String, Object> params);
|
||||||
|
|
||||||
SysDictTypeDTO get(Long id);
|
/**
|
||||||
|
* 根据ID获取字典类型信息
|
||||||
|
*
|
||||||
|
* @param id 字典类型ID
|
||||||
|
* @return 返回字典类型对象
|
||||||
|
*/
|
||||||
|
SysDictTypeVO get(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存字典类型信息
|
||||||
|
*
|
||||||
|
* @param dto 字典类型数据传输对象
|
||||||
|
*/
|
||||||
void save(SysDictTypeDTO dto);
|
void save(SysDictTypeDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新字典类型信息
|
||||||
|
*
|
||||||
|
* @param dto 字典类型数据传输对象
|
||||||
|
*/
|
||||||
void update(SysDictTypeDTO dto);
|
void update(SysDictTypeDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除字典类型信息
|
||||||
|
*
|
||||||
|
* @param ids 要删除的字典类型ID数组
|
||||||
|
*/
|
||||||
void delete(Long[] ids);
|
void delete(Long[] ids);
|
||||||
|
|
||||||
List<DictType> getAllList();
|
/**
|
||||||
|
* 列出所有字典类型信息
|
||||||
List<DictType> getDictTypeList();
|
*
|
||||||
|
* @return 返回字典类型列表
|
||||||
|
*/
|
||||||
|
List<SysDictTypeVO> list(Map<String, Object> params);
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,55 @@
|
|||||||
package xiaozhi.modules.sys.service.impl;
|
package xiaozhi.modules.sys.service.impl;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||||
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||||
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
|
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
|
||||||
implements SysDictDataService {
|
implements SysDictDataService {
|
||||||
|
private final SysUserDao sysUserDao;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<SysDictDataDTO> page(Map<String, Object> params) {
|
public PageData<SysDictDataVO> page(Map<String, Object> params) {
|
||||||
IPage<SysDictDataEntity> page = baseDao.selectPage(
|
IPage<SysDictDataEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
|
||||||
getPage(params, "sort", true),
|
|
||||||
getWrapper(params));
|
|
||||||
|
|
||||||
return getPageData(page, SysDictDataDTO.class);
|
PageData<SysDictDataVO> pageData = getPageData(page, SysDictDataVO.class);
|
||||||
|
|
||||||
|
setUserName(pageData.getList());
|
||||||
|
|
||||||
|
return pageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) {
|
private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) {
|
||||||
@@ -48,32 +66,118 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysDictDataDTO get(Long id) {
|
public SysDictDataVO get(Long id) {
|
||||||
SysDictDataEntity entity = baseDao.selectById(id);
|
SysDictDataEntity entity = baseDao.selectById(id);
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entity, SysDictDataDTO.class);
|
return ConvertUtils.sourceToTarget(entity, SysDictDataVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void save(SysDictDataDTO dto) {
|
public void save(SysDictDataDTO dto) {
|
||||||
|
// 相同字典类型的标签不能相同
|
||||||
|
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), null);
|
||||||
|
|
||||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||||
|
|
||||||
insert(entity);
|
insert(entity);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void update(SysDictDataDTO dto) {
|
public void update(SysDictDataDTO dto) {
|
||||||
|
// 相同字典类型的标签不能相同
|
||||||
|
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), String.valueOf(dto.getId()));
|
||||||
|
|
||||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||||
|
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void delete(Long[] ids) {
|
||||||
// 删除
|
for (Long id : ids) {
|
||||||
deleteBatchIds(Arrays.asList(ids));
|
SysDictDataEntity entity = baseDao.selectById(id);
|
||||||
|
// 删除Redis缓存
|
||||||
|
String dictType = baseDao.getTypeByTypeId(entity.getDictTypeId());
|
||||||
|
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||||
|
// 删除
|
||||||
|
deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteByTypeId(Long dictTypeId) {
|
||||||
|
LambdaQueryWrapper<SysDictDataEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId);
|
||||||
|
baseDao.delete(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户名
|
||||||
|
*
|
||||||
|
* @param sysDictDataList 字典类型集合
|
||||||
|
*/
|
||||||
|
private void setUserName(List<SysDictDataVO> sysDictDataList) {
|
||||||
|
// 收集所有用户 ID
|
||||||
|
Set<Long> userIds = sysDictDataList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 设置更新者和创建者名称
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||||
|
// 把List转成Map,Map<Long, String>
|
||||||
|
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||||
|
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||||
|
|
||||||
|
sysDictDataList.forEach(vo -> {
|
||||||
|
vo.setCreatorName(userNameMap.get(vo.getCreator()));
|
||||||
|
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDictValueUnique(Long dictTypeId, String dictValue, String excludeId) {
|
||||||
|
LambdaQueryWrapper<SysDictDataEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId).eq(SysDictDataEntity::getDictLabel, dictValue);
|
||||||
|
if (StringUtils.isNotBlank(excludeId)) {
|
||||||
|
queryWrapper.ne(SysDictDataEntity::getId, excludeId);
|
||||||
|
}
|
||||||
|
boolean exists = baseDao.exists(queryWrapper);
|
||||||
|
if (exists) {
|
||||||
|
throw new RenException("字典标签重复");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SysDictDataItem> getDictDataByType(String dictType) {
|
||||||
|
if (StringUtils.isBlank(dictType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先从Redis获取缓存
|
||||||
|
String key = RedisKeys.getDictDataByTypeKey(dictType);
|
||||||
|
List<SysDictDataItem> cachedData = (List<SysDictDataItem>) redisUtils.get(key);
|
||||||
|
if (cachedData != null) {
|
||||||
|
return cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果缓存中没有,则从数据库获取
|
||||||
|
List<SysDictDataItem> data = baseDao.getDictDataByType(dictType);
|
||||||
|
|
||||||
|
// 存入Redis缓存
|
||||||
|
if (data != null) {
|
||||||
|
redisUtils.set(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,42 +3,52 @@ package xiaozhi.modules.sys.service.impl;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
|
||||||
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
||||||
|
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||||
import xiaozhi.modules.sys.entity.DictData;
|
|
||||||
import xiaozhi.modules.sys.entity.DictType;
|
|
||||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||||
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
|
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||||
import xiaozhi.modules.sys.service.SysDictTypeService;
|
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||||
|
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典类型
|
* 字典类型
|
||||||
*/
|
*/
|
||||||
@AllArgsConstructor
|
|
||||||
@Service
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
|
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
|
||||||
implements SysDictTypeService {
|
implements SysDictTypeService {
|
||||||
private final SysDictDataDao sysDictDataDao;
|
private final SysUserDao sysUserDao;
|
||||||
|
private final SysDictDataService sysDictDataService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<SysDictTypeDTO> page(Map<String, Object> params) {
|
public PageData<SysDictTypeVO> page(Map<String, Object> params) {
|
||||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(
|
IPage<SysDictTypeEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
|
||||||
getPage(params, "sort", true),
|
|
||||||
getWrapper(params));
|
|
||||||
|
|
||||||
return getPageData(page, SysDictTypeDTO.class);
|
PageData<SysDictTypeVO> pageData = getPageData(page, SysDictTypeVO.class);
|
||||||
|
|
||||||
|
setUserName(pageData.getList());
|
||||||
|
|
||||||
|
return pageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
||||||
@@ -53,15 +63,21 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysDictTypeDTO get(Long id) {
|
public SysDictTypeVO get(Long id) {
|
||||||
SysDictTypeEntity entity = baseDao.selectById(id);
|
SysDictTypeEntity entity = baseDao.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new RenException("字典类型不存在");
|
||||||
|
}
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class);
|
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void save(SysDictTypeDTO dto) {
|
public void save(SysDictTypeDTO dto) {
|
||||||
|
// 字典类型编码不能重复
|
||||||
|
checkDictTypeUnique(dto.getDictType(), null);
|
||||||
|
|
||||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||||
|
|
||||||
insert(entity);
|
insert(entity);
|
||||||
@@ -70,6 +86,9 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void update(SysDictTypeDTO dto) {
|
public void update(SysDictTypeDTO dto) {
|
||||||
|
// 字典类型编码不能重复
|
||||||
|
checkDictTypeUnique(dto.getDictType(), String.valueOf(dto.getId()));
|
||||||
|
|
||||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||||
|
|
||||||
updateById(entity);
|
updateById(entity);
|
||||||
@@ -78,27 +97,57 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long[] ids) {
|
public void delete(Long[] ids) {
|
||||||
// 删除
|
// 先删除对应的字典数据
|
||||||
|
for (Long id : ids) {
|
||||||
|
sysDictDataService.deleteByTypeId(id);
|
||||||
|
}
|
||||||
|
// 再删除字典类型
|
||||||
deleteBatchIds(Arrays.asList(ids));
|
deleteBatchIds(Arrays.asList(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<DictType> getAllList() {
|
public List<SysDictTypeVO> list(Map<String, Object> params) {
|
||||||
List<DictType> typeList = baseDao.getDictTypeList();
|
List<SysDictTypeEntity> entityList = baseDao.selectList(getWrapper(params));
|
||||||
List<DictData> dataList = sysDictDataDao.getDictDataList();
|
List<SysDictTypeVO> sysDictTypeVOList = ConvertUtils.sourceToTarget(entityList, SysDictTypeVO.class);
|
||||||
for (DictType type : typeList) {
|
// 设置用户名
|
||||||
for (DictData data : dataList) {
|
setUserName(sysDictTypeVOList);
|
||||||
if (type.getId().equals(data.getDictTypeId())) {
|
|
||||||
type.getDataList().add(data);
|
return sysDictTypeVOList;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 设置用户名
|
||||||
|
*
|
||||||
|
* @param sysDictTypeList 字典类型集合
|
||||||
|
*/
|
||||||
|
private void setUserName(List<SysDictTypeVO> sysDictTypeList) {
|
||||||
|
// 收集所有用户 ID
|
||||||
|
Set<Long> userIds = sysDictTypeList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
|
||||||
|
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// 设置更新者和创建者名称
|
||||||
|
if (!userIds.isEmpty()) {
|
||||||
|
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||||
|
// 把List转成Map,Map<Long, String>
|
||||||
|
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||||
|
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||||
|
|
||||||
|
sysDictTypeList.forEach(vo -> {
|
||||||
|
vo.setCreatorName(userNameMap.get(vo.getCreator()));
|
||||||
|
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return typeList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void checkDictTypeUnique(String dictType, String excludeId) {
|
||||||
public List<DictType> getDictTypeList() {
|
LambdaQueryWrapper<SysDictTypeEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
return baseDao.getDictTypeList();
|
queryWrapper.eq(SysDictTypeEntity::getDictType, dictType);
|
||||||
|
if (StringUtils.isNotBlank(excludeId)) {
|
||||||
|
queryWrapper.ne(SysDictTypeEntity::getId, excludeId);
|
||||||
|
}
|
||||||
|
boolean exists = baseDao.exists(queryWrapper);
|
||||||
|
if (exists) {
|
||||||
|
throw new RenException("字典类型编码重复");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -127,6 +127,12 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
break;
|
break;
|
||||||
case "json":
|
case "json":
|
||||||
try {
|
try {
|
||||||
|
// 首先检查是否以 { 开头,以 } 结尾
|
||||||
|
String trimmedValue = paramValue.trim();
|
||||||
|
if (!trimmedValue.startsWith("{") || !trimmedValue.endsWith("}")) {
|
||||||
|
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
|
||||||
|
}
|
||||||
|
// 然后尝试解析JSON
|
||||||
JsonUtils.parseObject(paramValue, Object.class);
|
JsonUtils.parseObject(paramValue, Object.class);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
|
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
|
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
|
||||||
adminPageUserVO.setDeviceCount(deviceCount);
|
adminPageUserVO.setDeviceCount(deviceCount);
|
||||||
adminPageUserVO.setStatus(user.getStatus());
|
adminPageUserVO.setStatus(user.getStatus());
|
||||||
|
adminPageUserVO.setCreateDate(user.getCreateDate());
|
||||||
return adminPageUserVO;
|
return adminPageUserVO;
|
||||||
}).toList();
|
}).toList();
|
||||||
return new PageData<>(list, page.getTotal());
|
return new PageData<>(list, page.getTotal());
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ public class SysUserUtilServiceImpl extends BaseServiceImpl<SysUserDao, SysUserE
|
|||||||
@Override
|
@Override
|
||||||
public void assignUsername(Long userId, Consumer<String> setter) {
|
public void assignUsername(Long userId, Consumer<String> setter) {
|
||||||
String userIdKey = RedisKeys.getUserIdKey(userId);
|
String userIdKey = RedisKeys.getUserIdKey(userId);
|
||||||
String username = redisUtils.get(userIdKey).toString();
|
|
||||||
|
Object value = redisUtils.get(userIdKey);
|
||||||
|
String username = (value != null) ? value.toString() : null;
|
||||||
if(username != null){
|
if(username != null){
|
||||||
setter.accept(username);
|
setter.accept(username);
|
||||||
}else {
|
}else {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.util.regex.Pattern;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||||
import org.springframework.web.socket.client.WebSocketClient;
|
import org.springframework.web.socket.client.WebSocketClient;
|
||||||
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||||
|
|
||||||
@@ -45,8 +46,9 @@ public class WebSocketValidator {
|
|||||||
try {
|
try {
|
||||||
WebSocketClient client = new StandardWebSocketClient();
|
WebSocketClient client = new StandardWebSocketClient();
|
||||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||||
|
|
||||||
client.doHandshake(new WebSocketTestHandler(future), null, URI.create(url));
|
client.execute(new WebSocketTestHandler(future), headers, URI.create(url));
|
||||||
|
|
||||||
// 等待最多5秒获取连接结果
|
// 等待最多5秒获取连接结果
|
||||||
return future.get(5, TimeUnit.SECONDS);
|
return future.get(5, TimeUnit.SECONDS);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package xiaozhi.modules.sys.vo;
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -23,4 +25,7 @@ public class AdminPageUserVO {
|
|||||||
|
|
||||||
@Schema(description = "用户id")
|
@Schema(description = "用户id")
|
||||||
private String userid;
|
private String userid;
|
||||||
|
|
||||||
|
@Schema(description = "注册时间")
|
||||||
|
private Date createDate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典数据VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "字典数据项")
|
||||||
|
public class SysDictDataItem implements Serializable {
|
||||||
|
|
||||||
|
@Schema(description = "字典标签")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "字典值")
|
||||||
|
private String key;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典数据VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "字典数据VO")
|
||||||
|
public class SysDictDataVO implements Serializable {
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "字典类型ID")
|
||||||
|
private Long dictTypeId;
|
||||||
|
|
||||||
|
@Schema(description = "字典标签")
|
||||||
|
private String dictLabel;
|
||||||
|
|
||||||
|
@Schema(description = "字典值")
|
||||||
|
private String dictValue;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建者名称")
|
||||||
|
private String creatorName;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createDate;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新者名称")
|
||||||
|
private String updaterName;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package xiaozhi.modules.sys.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典类型VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "字典类型VO")
|
||||||
|
public class SysDictTypeVO implements Serializable {
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "字典类型")
|
||||||
|
private String dictType;
|
||||||
|
|
||||||
|
@Schema(description = "字典名称")
|
||||||
|
private String dictName;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建者名称")
|
||||||
|
private String creatorName;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createDate;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新者名称")
|
||||||
|
private String updaterName;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
-- 增加FunASR服务语音识别模型供应器和配置
|
||||||
|
DELETE FROM `ai_model_provider` WHERE `id` = 'SYSTEM_ASR_FunASRServer';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_ASR_FunASRServer', 'ASR', 'fun_server', 'FunASR服务语音识别', '[{"key":"host","label":"服务地址","type":"string"},{"key":"port","label":"端口号","type":"number"}]', 4, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
DELETE FROM `ai_model_config` WHERE `id` = 'ASR_FunASRServer';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('ASR_FunASRServer', 'ASR', 'FunASRServer', 'FunASR服务语音识别', 0, 1, '{\"type\": \"fun_server\", \"host\": \"127.0.0.1\", \"port\": 10096}', NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 修改ai_model_config表的remark字段类型为TEXT
|
||||||
|
ALTER TABLE `ai_model_config` MODIFY COLUMN `remark` TEXT COMMENT '备注';
|
||||||
|
|
||||||
|
-- 更新ASR模型配置的说明文档
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md',
|
||||||
|
`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话
|
||||||
|
第一句:mkdir -p ./funasr-runtime-resources/models
|
||||||
|
第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||||
|
上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
||||||
|
不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
||||||
|
上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
||||||
|
第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了
|
||||||
|
以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer';
|
||||||
|
|
||||||
|
-- 更新FunASR本地模型配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/modelscope/FunASR',
|
||||||
|
`remark` = 'FunASR本地模型配置说明:
|
||||||
|
1. 需要下载模型文件到xiaozhi-server/models/SenseVoiceSmall目录
|
||||||
|
2. 支持中日韩粤语音识别
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 待识别文件保存在tmp/目录' WHERE `id` = 'ASR_FunASR';
|
||||||
|
|
||||||
|
-- 更新SherpaASR配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/k2-fsa/sherpa-onnx',
|
||||||
|
`remark` = 'SherpaASR配置说明:
|
||||||
|
1. 运行时自动下载模型文件到models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17目录
|
||||||
|
2. 支持中文、英文、日语、韩语、粤语等多种语言
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 输出文件保存在tmp/目录' WHERE `id` = 'ASR_SherpaASR';
|
||||||
|
|
||||||
|
-- 更新豆包ASR配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.volcengine.com/speech/app',
|
||||||
|
`remark` = '豆包ASR配置说明:
|
||||||
|
1. 需要在火山引擎控制台创建应用并获取appid和access_token
|
||||||
|
2. 支持中文语音识别
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://console.volcengine.com/speech/app
|
||||||
|
2. 创建新应用
|
||||||
|
3. 获取appid和access_token
|
||||||
|
4. 填入配置文件中' WHERE `id` = 'ASR_DoubaoASR';
|
||||||
|
|
||||||
|
-- 更新腾讯ASR配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.cloud.tencent.com/cam/capi',
|
||||||
|
`remark` = '腾讯ASR配置说明:
|
||||||
|
1. 需要在腾讯云控制台创建应用并获取appid、secret_id和secret_key
|
||||||
|
2. 支持中文语音识别
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥
|
||||||
|
2. 访问 https://console.cloud.tencent.com/asr/resourcebundle 领取免费资源
|
||||||
|
3. 获取appid、secret_id和secret_key
|
||||||
|
4. 填入配置文件中' WHERE `id` = 'ASR_TencentASR';
|
||||||
|
|
||||||
|
-- 更新TTS模型配置说明
|
||||||
|
-- EdgeTTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/rany2/edge-tts',
|
||||||
|
`remark` = 'EdgeTTS配置说明:
|
||||||
|
1. 使用微软Edge TTS服务
|
||||||
|
2. 支持多种语言和音色
|
||||||
|
3. 免费使用,无需注册
|
||||||
|
4. 需要网络连接
|
||||||
|
5. 输出文件保存在tmp/目录' WHERE `id` = 'TTS_EdgeTTS';
|
||||||
|
|
||||||
|
-- 豆包TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.volcengine.com/speech/service/8',
|
||||||
|
`remark` = '豆包TTS配置说明:
|
||||||
|
1. 访问 https://console.volcengine.com/speech/service/8
|
||||||
|
2. 需要在火山引擎控制台创建应用并获取appid和access_token
|
||||||
|
3. 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误
|
||||||
|
4. 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。
|
||||||
|
5. 填入配置文件中' WHERE `id` = 'TTS_DoubaoTTS';
|
||||||
|
|
||||||
|
-- 硅基流动TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://cloud.siliconflow.cn/account/ak',
|
||||||
|
`remark` = '硅基流动TTS配置说明:
|
||||||
|
1. 访问 https://cloud.siliconflow.cn/account/ak
|
||||||
|
2. 注册并获取API密钥
|
||||||
|
3. 填入配置文件中' WHERE `id` = 'TTS_CosyVoiceSiliconflow';
|
||||||
|
|
||||||
|
-- Coze中文TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://www.coze.cn/open/oauth/pats',
|
||||||
|
`remark` = 'Coze中文TTS配置说明:
|
||||||
|
1. 访问 https://www.coze.cn/open/oauth/pats
|
||||||
|
2. 获取个人令牌
|
||||||
|
3. 填入配置文件中' WHERE `id` = 'TTS_CozeCnTTS';
|
||||||
|
|
||||||
|
-- FishSpeech配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/fishaudio/fish-speech',
|
||||||
|
`remark` = 'FishSpeech配置说明:
|
||||||
|
1. 需要本地部署FishSpeech服务
|
||||||
|
2. 支持自定义音色
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
5. 运行服务示例命令:python -m tools.api_server --listen 0.0.0.0:8080 --llama-checkpoint-path "checkpoints/fish-speech-1.5" --decoder-checkpoint-path "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth" --decoder-config-name firefly_gan_vq --compile' WHERE `id` = 'TTS_FishSpeech';
|
||||||
|
|
||||||
|
-- GPT-SoVITS V2配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/RVC-Boss/GPT-SoVITS',
|
||||||
|
`remark` = 'GPT-SoVITS V2配置说明:
|
||||||
|
1. 需要本地部署GPT-SoVITS服务
|
||||||
|
2. 支持自定义音色克隆
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
部署步骤:
|
||||||
|
1. 运行服务示例命令:python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/demo.yaml' WHERE `id` = 'TTS_GPT_SOVITS_V2';
|
||||||
|
|
||||||
|
-- GPT-SoVITS V3配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/RVC-Boss/GPT-SoVITS',
|
||||||
|
`remark` = 'GPT-SoVITS V3配置说明:
|
||||||
|
1. 需要本地部署GPT-SoVITS V3服务
|
||||||
|
2. 支持自定义音色克隆
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 输出文件保存在tmp/目录' WHERE `id` = 'TTS_GPT_SOVITS_V3';
|
||||||
|
|
||||||
|
-- MiniMax TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://platform.minimaxi.com/',
|
||||||
|
`remark` = 'MiniMax TTS配置说明:
|
||||||
|
1. 需要在MiniMax平台创建账户并充值
|
||||||
|
2. 支持多种音色,当前配置使用female-shaonv
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://platform.minimaxi.com/ 注册账号
|
||||||
|
2. 访问 https://platform.minimaxi.com/user-center/payment/balance 充值
|
||||||
|
3. 访问 https://platform.minimaxi.com/user-center/basic-information 获取group_id
|
||||||
|
4. 访问 https://platform.minimaxi.com/user-center/basic-information/interface-key 获取api_key
|
||||||
|
5. 填入配置文件中' WHERE `id` = 'TTS_MinimaxTTS';
|
||||||
|
|
||||||
|
-- 阿里云TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://nls-portal.console.aliyun.com/',
|
||||||
|
`remark` = '阿里云TTS配置说明:
|
||||||
|
1. 需要在阿里云平台开通智能语音交互服务
|
||||||
|
2. 支持多种音色,当前配置使用xiaoyun
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://nls-portal.console.aliyun.com/ 开通服务
|
||||||
|
2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey
|
||||||
|
3. 访问 https://nls-portal.console.aliyun.com/overview 获取token
|
||||||
|
4. 填入配置文件中
|
||||||
|
注意:token是临时的24小时有效,长期使用需要配置access_key_id和access_key_secret' WHERE `id` = 'TTS_AliyunTTS';
|
||||||
|
|
||||||
|
-- 腾讯TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.cloud.tencent.com/cam/capi',
|
||||||
|
`remark` = '腾讯TTS配置说明:
|
||||||
|
1. 需要在腾讯云平台开通智能语音交互服务
|
||||||
|
2. 支持多种音色,当前配置使用101001
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥
|
||||||
|
2. 访问 https://console.cloud.tencent.com/tts/resourcebundle 领取免费资源
|
||||||
|
3. 创建新应用
|
||||||
|
4. 获取appid、secret_id和secret_key
|
||||||
|
5. 填入配置文件中' WHERE `id` = 'TTS_TencentTTS';
|
||||||
|
|
||||||
|
-- 302AI TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://dash.302.ai/',
|
||||||
|
`remark` = '302AI TTS配置说明:
|
||||||
|
1. 需要在302平台创建账户并获取API密钥
|
||||||
|
2. 支持多种音色,当前配置使用湾湾小何音色
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://dash.302.ai/ 注册账号
|
||||||
|
2. 访问 https://dash.302.ai/apis/list 获取API密钥
|
||||||
|
3. 填入配置文件中
|
||||||
|
价格:$35/百万字符' WHERE `id` = 'TTS_TTS302AI';
|
||||||
|
|
||||||
|
-- 机智云TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://agentrouter.gizwitsapi.com/panel/token',
|
||||||
|
`remark` = '机智云TTS配置说明:
|
||||||
|
1. 需要在机智云平台获取API密钥
|
||||||
|
2. 支持多种音色,当前配置使用湾湾小何音色
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://agentrouter.gizwitsapi.com/panel/token 获取API密钥
|
||||||
|
2. 填入配置文件中
|
||||||
|
注意:前一万名注册的用户,将送5元体验金额' WHERE `id` = 'TTS_GizwitsTTS';
|
||||||
|
|
||||||
|
-- ACGN TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://acgn.ttson.cn/',
|
||||||
|
`remark` = 'ACGN TTS配置说明:
|
||||||
|
1. 需要在ttson平台购买token
|
||||||
|
2. 支持多种角色音色,当前配置使用角色ID:1695
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://acgn.ttson.cn/ 查看角色列表
|
||||||
|
2. 访问 www.ttson.cn 购买token
|
||||||
|
3. 填入配置文件中
|
||||||
|
开发相关疑问请提交至网站上的qq' WHERE `id` = 'TTS_ACGNTTS';
|
||||||
|
|
||||||
|
-- OpenAI TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://platform.openai.com/api-keys',
|
||||||
|
`remark` = 'OpenAI TTS配置说明:
|
||||||
|
1. 需要在OpenAI平台获取API密钥
|
||||||
|
2. 支持多种音色,当前配置使用onyx
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://platform.openai.com/api-keys 获取API密钥
|
||||||
|
2. 填入配置文件中
|
||||||
|
注意:国内需要使用代理访问' WHERE `id` = 'TTS_OpenAITTS';
|
||||||
|
|
||||||
|
-- 自定义TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = '自定义TTS配置说明:
|
||||||
|
1. 支持自定义TTS接口服务
|
||||||
|
2. 使用GET方式请求
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
配置说明:
|
||||||
|
1. 在params中配置请求参数
|
||||||
|
2. 在headers中配置请求头
|
||||||
|
3. 设置返回音频格式' WHERE `id` = 'TTS_CustomTTS';
|
||||||
|
|
||||||
|
-- 火山引擎边缘大模型网关TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.volcengine.com/vei/aigateway/',
|
||||||
|
`remark` = '火山引擎边缘大模型网关TTS配置说明:
|
||||||
|
1. 访问 https://console.volcengine.com/vei/aigateway/
|
||||||
|
2. 创建网关访问密钥,搜索并勾选 Doubao-语音合成
|
||||||
|
3. 如果需要使用LLM,一并勾选 Doubao-pro-32k-functioncall
|
||||||
|
4. 访问 https://console.volcengine.com/vei/aigateway/tokens-list 获取密钥
|
||||||
|
5. 填入配置文件中
|
||||||
|
音色列表参考:https://www.volcengine.com/docs/6561/1257544' WHERE `id` = 'TTS_VolcesAiGatewayTTS';
|
||||||
|
|
||||||
|
-- 更新LLM模型配置说明
|
||||||
|
-- ChatGLM配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys',
|
||||||
|
`remark` = 'ChatGLM配置说明:
|
||||||
|
1. 访问 https://bigmodel.cn/usercenter/proj-mgmt/apikeys
|
||||||
|
2. 注册并获取API密钥
|
||||||
|
3. 填入配置文件中' WHERE `id` = 'LLM_ChatGLMLLM';
|
||||||
|
|
||||||
|
-- Ollama配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://ollama.com/',
|
||||||
|
`remark` = 'Ollama配置说明:
|
||||||
|
1. 安装Ollama服务
|
||||||
|
2. 运行命令:ollama pull qwen2.5
|
||||||
|
3. 确保服务运行在http://localhost:11434' WHERE `id` = 'LLM_OllamaLLM';
|
||||||
|
|
||||||
|
-- 通义千问配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
|
||||||
|
`remark` = '通义千问配置说明:
|
||||||
|
1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key
|
||||||
|
2. 获取API密钥
|
||||||
|
3. 填入配置文件中,当前配置使用qwen-turbo模型
|
||||||
|
4. 支持自定义参数:temperature=0.7, max_tokens=500, top_p=1, top_k=50' WHERE `id` = 'LLM_AliLLM';
|
||||||
|
|
||||||
|
-- 通义百炼配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
|
||||||
|
`remark` = '通义百炼配置说明:
|
||||||
|
1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key
|
||||||
|
2. 获取app_id和api_key
|
||||||
|
3. 填入配置文件中' WHERE `id` = 'LLM_AliAppLLM';
|
||||||
|
|
||||||
|
-- 豆包大模型配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement',
|
||||||
|
`remark` = '豆包大模型配置说明:
|
||||||
|
1. 访问 https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement
|
||||||
|
2. 开通Doubao-1.5-pro服务
|
||||||
|
3. 访问 https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey 获取API密钥
|
||||||
|
4. 填入配置文件中
|
||||||
|
5. 当前建议使用doubao-1-5-pro-32k-250115
|
||||||
|
注意:有免费额度500000token' WHERE `id` = 'LLM_DoubaoLLM';
|
||||||
|
|
||||||
|
-- DeepSeek配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://platform.deepseek.com/',
|
||||||
|
`remark` = 'DeepSeek配置说明:
|
||||||
|
1. 访问 https://platform.deepseek.com/
|
||||||
|
2. 注册并获取API密钥
|
||||||
|
3. 填入配置文件中' WHERE `id` = 'LLM_DeepSeekLLM';
|
||||||
|
|
||||||
|
-- Dify配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://cloud.dify.ai/',
|
||||||
|
`remark` = 'Dify配置说明:
|
||||||
|
1. 访问 https://cloud.dify.ai/
|
||||||
|
2. 注册并获取API密钥
|
||||||
|
3. 填入配置文件中
|
||||||
|
4. 支持多种对话模式:workflows/run, chat-messages, completion-messages
|
||||||
|
5. 平台设置的角色定义会失效,需要在Dify控制台设置
|
||||||
|
注意:建议使用本地部署的Dify接口,国内部分区域访问公有云接口可能受限' WHERE `id` = 'LLM_DifyLLM';
|
||||||
|
|
||||||
|
-- Gemini配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://aistudio.google.com/apikey',
|
||||||
|
`remark` = 'Gemini配置说明:
|
||||||
|
1. 使用谷歌Gemini API服务
|
||||||
|
2. 当前配置使用gemini-2.0-flash模型
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 支持配置代理
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://aistudio.google.com/apikey
|
||||||
|
2. 创建API密钥
|
||||||
|
3. 填入配置文件中
|
||||||
|
注意:若在中国境内使用,请遵守《生成式人工智能服务管理暂行办法》' WHERE `id` = 'LLM_GeminiLLM';
|
||||||
|
|
||||||
|
-- Coze配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://www.coze.cn/open/oauth/pats',
|
||||||
|
`remark` = 'Coze配置说明:
|
||||||
|
1. 使用Coze平台服务
|
||||||
|
2. 需要bot_id、user_id和个人令牌
|
||||||
|
3. 需要网络连接
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://www.coze.cn/open/oauth/pats
|
||||||
|
2. 获取个人令牌
|
||||||
|
3. 手动计算bot_id和user_id
|
||||||
|
4. 填入配置文件中' WHERE `id` = 'LLM_CozeLLM';
|
||||||
|
|
||||||
|
-- LM Studio配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://lmstudio.ai/',
|
||||||
|
`remark` = 'LM Studio配置说明:
|
||||||
|
1. 使用本地部署的LM Studio服务
|
||||||
|
2. 当前配置使用deepseek-r1-distill-llama-8b@q4_k_m模型
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 需要预先下载模型
|
||||||
|
部署步骤:
|
||||||
|
1. 安装LM Studio
|
||||||
|
2. 从社区下载模型
|
||||||
|
3. 确保服务运行在http://localhost:1234/v1' WHERE `id` = 'LLM_LMStudioLLM';
|
||||||
|
|
||||||
|
-- FastGPT配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://cloud.tryfastgpt.ai/account/apikey',
|
||||||
|
`remark` = 'FastGPT配置说明:
|
||||||
|
1. 使用FastGPT平台服务
|
||||||
|
2. 需要网络连接
|
||||||
|
3. 配置文件中的prompt无效,需要在FastGPT控制台设置
|
||||||
|
4. 支持自定义变量
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://cloud.tryfastgpt.ai/account/apikey
|
||||||
|
2. 获取API密钥
|
||||||
|
3. 填入配置文件中' WHERE `id` = 'LLM_FastgptLLM';
|
||||||
|
|
||||||
|
-- Xinference配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/xorbitsai/inference',
|
||||||
|
`remark` = 'Xinference配置说明:
|
||||||
|
1. 使用本地部署的Xinference服务
|
||||||
|
2. 当前配置使用qwen2.5:72b-AWQ模型
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 需要预先启动对应模型
|
||||||
|
部署步骤:
|
||||||
|
1. 安装Xinference
|
||||||
|
2. 启动服务并加载模型
|
||||||
|
3. 确保服务运行在http://localhost:9997' WHERE `id` = 'LLM_XinferenceLLM';
|
||||||
|
|
||||||
|
-- Xinference小模型配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/xorbitsai/inference',
|
||||||
|
`remark` = 'Xinference小模型配置说明:
|
||||||
|
1. 使用本地部署的Xinference服务
|
||||||
|
2. 当前配置使用qwen2.5:3b-AWQ模型
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 用于意图识别
|
||||||
|
部署步骤:
|
||||||
|
1. 安装Xinference
|
||||||
|
2. 启动服务并加载模型
|
||||||
|
3. 确保服务运行在http://localhost:9997' WHERE `id` = 'LLM_XinferenceSmallLLM';
|
||||||
|
|
||||||
|
-- 火山引擎边缘大模型网关LLM配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.volcengine.com/vei/aigateway/',
|
||||||
|
`remark` = '火山引擎边缘大模型网关LLM配置说明:
|
||||||
|
1. 使用火山引擎边缘大模型网关服务
|
||||||
|
2. 需要网关访问密钥
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 支持function_call功能
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://console.volcengine.com/vei/aigateway/
|
||||||
|
2. 创建网关访问密钥,搜索并勾选 Doubao-pro-32k-functioncall
|
||||||
|
3. 如果需要使用语音合成,一并勾选 Doubao-语音合成
|
||||||
|
4. 访问 https://console.volcengine.com/vei/aigateway/tokens-list 获取密钥
|
||||||
|
5. 填入配置文件中' WHERE `id` = 'LLM_VolcesAiGatewayLLM';
|
||||||
|
|
||||||
|
-- 更新Memory模型配置说明
|
||||||
|
-- 无记忆配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = '无记忆配置说明:
|
||||||
|
1. 不保存对话历史
|
||||||
|
2. 每次对话都是独立的
|
||||||
|
3. 无需额外配置
|
||||||
|
4. 适合对隐私要求高的场景' WHERE `id` = 'Memory_nomem';
|
||||||
|
|
||||||
|
-- 本地短期记忆配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = '本地短期记忆配置说明:
|
||||||
|
1. 使用本地存储保存对话历史
|
||||||
|
2. 通过selected_module的llm总结对话内容
|
||||||
|
3. 数据保存在本地,不会上传到服务器
|
||||||
|
4. 适合注重隐私的场景
|
||||||
|
5. 无需额外配置' WHERE `id` = 'Memory_mem_local_short';
|
||||||
|
|
||||||
|
-- Mem0AI记忆配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://app.mem0.ai/dashboard/api-keys',
|
||||||
|
`remark` = 'Mem0AI记忆配置说明:
|
||||||
|
1. 使用Mem0AI服务保存对话历史
|
||||||
|
2. 需要API密钥
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 每月有1000次免费调用
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://app.mem0.ai/dashboard/api-keys
|
||||||
|
2. 获取API密钥
|
||||||
|
3. 填入配置文件中' WHERE `id` = 'Memory_mem0ai';
|
||||||
|
|
||||||
|
-- 更新Intent模型配置说明
|
||||||
|
-- 无意图识别配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = '无意图识别配置说明:
|
||||||
|
1. 不进行意图识别
|
||||||
|
2. 所有对话直接传递给LLM处理
|
||||||
|
3. 无需额外配置
|
||||||
|
4. 适合简单对话场景' WHERE `id` = 'Intent_nointent';
|
||||||
|
|
||||||
|
-- LLM意图识别配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = 'LLM意图识别配置说明:
|
||||||
|
1. 使用独立的LLM进行意图识别
|
||||||
|
2. 默认使用selected_module.LLM的模型
|
||||||
|
3. 可以配置使用独立的LLM(如免费的ChatGLMLLM)
|
||||||
|
4. 通用性强,但会增加处理时间
|
||||||
|
5. 不支持控制音量大小等iot操作
|
||||||
|
配置说明:
|
||||||
|
1. 在llm字段中指定使用的LLM模型
|
||||||
|
2. 如果不指定,则使用selected_module.LLM的模型' WHERE `id` = 'Intent_intent_llm';
|
||||||
|
|
||||||
|
-- 函数调用意图识别配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = '函数调用意图识别配置说明:
|
||||||
|
1. 使用LLM的function_call功能进行意图识别
|
||||||
|
2. 需要所选择的LLM支持function_call
|
||||||
|
3. 按需调用工具,处理速度快
|
||||||
|
4. 支持所有iot指令
|
||||||
|
5. 默认已加载以下功能:
|
||||||
|
- handle_exit_intent(退出识别)
|
||||||
|
- play_music(音乐播放)
|
||||||
|
- change_role(角色切换)
|
||||||
|
- get_weather(天气查询)
|
||||||
|
- get_news(新闻查询)
|
||||||
|
配置说明:
|
||||||
|
1. 在functions字段中配置需要加载的功能模块
|
||||||
|
2. 系统默认已加载基础功能,无需重复配置
|
||||||
|
3. 可以添加自定义功能模块' WHERE `id` = 'Intent_function_call';
|
||||||
|
|
||||||
|
-- 更新VAD模型配置说明
|
||||||
|
-- SileroVAD配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/snakers4/silero-vad',
|
||||||
|
`remark` = 'SileroVAD配置说明:
|
||||||
|
1. 使用SileroVAD模型进行语音活动检测
|
||||||
|
2. 本地推理,无需网络连接
|
||||||
|
3. 需要下载模型文件到models/snakers4_silero-vad目录
|
||||||
|
4. 可配置参数:
|
||||||
|
- threshold: 0.5(语音检测阈值)
|
||||||
|
- min_silence_duration_ms: 700(最小静音持续时间,单位毫秒)
|
||||||
|
5. 如果说话停顿比较长,可以适当增加min_silence_duration_ms的值' WHERE `id` = 'VAD_SileroVAD';
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
update `ai_model_provider` set `fields` =
|
||||||
|
'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]'
|
||||||
|
where `id` = 'SYSTEM_TTS_doubao';
|
||||||
|
|
||||||
|
-- 添加阿里云ASR供应器
|
||||||
|
delete from `ai_model_provider` where `id` = 'SYSTEM_ASR_AliyunASR';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_ASR_AliyunASR', 'ASR', 'aliyun', '阿里云语音识别', '[{"key":"appkey","label":"应用AppKey","type":"string"},{"key":"token","label":"临时Token","type":"string"},{"key":"access_key_id","label":"AccessKey ID","type":"string"},{"key":"access_key_secret","label":"AccessKey Secret","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 5, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
-- 添加阿里云ASR模型配置
|
||||||
|
delete from `ai_model_config` where `id` = 'ASR_AliyunASR';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('ASR_AliyunASR', 'ASR', 'AliyunASR', '阿里云语音识别', 0, 1, '{\"type\": \"aliyun\", \"appkey\": \"\", \"token\": \"\", \"access_key_id\": \"\", \"access_key_secret\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 更新阿里云ASR模型配置的说明文档
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://nls-portal.console.aliyun.com/',
|
||||||
|
`remark` = '阿里云ASR配置说明:
|
||||||
|
1. 访问 https://nls-portal.console.aliyun.com/ 开通服务
|
||||||
|
2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey
|
||||||
|
3. 访问 https://nls-portal.console.aliyun.com/overview 获取token
|
||||||
|
4. 获取access_key_id和access_key_secret
|
||||||
|
5. 填入配置文件中' WHERE `id` = 'ASR_AliyunASR';
|
||||||
|
|
||||||
|
-- 插入固件类型字典类型
|
||||||
|
delete from `sys_dict_type` where `id` = 101;
|
||||||
|
INSERT INTO `sys_dict_type` (`id`, `dict_type`, `dict_name`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
(101, 'FIRMWARE_TYPE', '固件类型', '固件类型字典', 0, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
-- 插入固件类型字典数据
|
||||||
|
delete from `sys_dict_data` where `dict_type_id` = 101;
|
||||||
|
INSERT INTO `sys_dict_data` (`id`, `dict_type_id`, `dict_label`, `dict_value`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
(101001, 101, '面包板新版接线(WiFi)', 'bread-compact-wifi', '面包板新版接线(WiFi)', 1, 1, NOW(), 1, NOW()),
|
||||||
|
(101002, 101, '面包板新版接线(WiFi)+ LCD', 'bread-compact-wifi-lcd', '面包板新版接线(WiFi)+ LCD', 2, 1, NOW(), 1, NOW()),
|
||||||
|
(101003, 101, '面包板新版接线(ML307 AT)', 'bread-compact-ml307', '面包板新版接线(ML307 AT)', 3, 1, NOW(), 1, NOW()),
|
||||||
|
(101004, 101, '面包板(WiFi) ESP32 DevKit', 'bread-compact-esp32', '面包板(WiFi) ESP32 DevKit', 4, 1, NOW(), 1, NOW()),
|
||||||
|
(101005, 101, '面包板(WiFi+ LCD) ESP32 DevKit', 'bread-compact-esp32-lcd', '面包板(WiFi+ LCD) ESP32 DevKit', 5, 1, NOW(), 1, NOW()),
|
||||||
|
(101006, 101, 'DFRobot 行空板 k10', 'df-k10', 'DFRobot 行空板 k10', 6, 1, NOW(), 1, NOW()),
|
||||||
|
(101007, 101, 'ESP32 CGC', 'esp32-cgc', 'ESP32 CGC', 7, 1, NOW(), 1, NOW()),
|
||||||
|
(101008, 101, 'ESP BOX 3', 'esp-box-3', 'ESP BOX 3', 8, 1, NOW(), 1, NOW()),
|
||||||
|
(101009, 101, 'ESP BOX', 'esp-box', 'ESP BOX', 9, 1, NOW(), 1, NOW()),
|
||||||
|
(101010, 101, 'ESP BOX Lite', 'esp-box-lite', 'ESP BOX Lite', 10, 1, NOW(), 1, NOW()),
|
||||||
|
(101011, 101, 'Kevin Box 1', 'kevin-box-1', 'Kevin Box 1', 11, 1, NOW(), 1, NOW()),
|
||||||
|
(101012, 101, 'Kevin Box 2', 'kevin-box-2', 'Kevin Box 2', 12, 1, NOW(), 1, NOW()),
|
||||||
|
(101013, 101, 'Kevin C3', 'kevin-c3', 'Kevin C3', 13, 1, NOW(), 1, NOW()),
|
||||||
|
(101014, 101, 'Kevin SP V3开发板', 'kevin-sp-v3-dev', 'Kevin SP V3开发板', 14, 1, NOW(), 1, NOW()),
|
||||||
|
(101015, 101, 'Kevin SP V4开发板', 'kevin-sp-v4-dev', 'Kevin SP V4开发板', 15, 1, NOW(), 1, NOW()),
|
||||||
|
(101016, 101, '鱼鹰科技3.13LCD开发板', 'kevin-yuying-313lcd', '鱼鹰科技3.13LCD开发板', 16, 1, NOW(), 1, NOW()),
|
||||||
|
(101017, 101, '立创·实战派ESP32-S3开发板', 'lichuang-dev', '立创·实战派ESP32-S3开发板', 17, 1, NOW(), 1, NOW()),
|
||||||
|
(101018, 101, '立创·实战派ESP32-C3开发板', 'lichuang-c3-dev', '立创·实战派ESP32-C3开发板', 18, 1, NOW(), 1, NOW()),
|
||||||
|
(101019, 101, '神奇按钮 Magiclick_2.4', 'magiclick-2p4', '神奇按钮 Magiclick_2.4', 19, 1, NOW(), 1, NOW()),
|
||||||
|
(101020, 101, '神奇按钮 Magiclick_2.5', 'magiclick-2p5', '神奇按钮 Magiclick_2.5', 20, 1, NOW(), 1, NOW()),
|
||||||
|
(101021, 101, '神奇按钮 Magiclick_C3', 'magiclick-c3', '神奇按钮 Magiclick_C3', 21, 1, NOW(), 1, NOW()),
|
||||||
|
(101022, 101, '神奇按钮 Magiclick_C3_v2', 'magiclick-c3-v2', '神奇按钮 Magiclick_C3_v2', 22, 1, NOW(), 1, NOW()),
|
||||||
|
(101023, 101, 'M5Stack CoreS3', 'm5stack-core-s3', 'M5Stack CoreS3', 23, 1, NOW(), 1, NOW()),
|
||||||
|
(101024, 101, 'AtomS3 + Echo Base', 'atoms3-echo-base', 'AtomS3 + Echo Base', 24, 1, NOW(), 1, NOW()),
|
||||||
|
(101025, 101, 'AtomS3R + Echo Base', 'atoms3r-echo-base', 'AtomS3R + Echo Base', 25, 1, NOW(), 1, NOW()),
|
||||||
|
(101026, 101, 'AtomS3R CAM/M12 + Echo Base', 'atoms3r-cam-m12-echo-base', 'AtomS3R CAM/M12 + Echo Base', 26, 1, NOW(), 1, NOW()),
|
||||||
|
(101027, 101, 'AtomMatrix + Echo Base', 'atommatrix-echo-base', 'AtomMatrix + Echo Base', 27, 1, NOW(), 1, NOW()),
|
||||||
|
(101028, 101, '虾哥 Mini C3', 'xmini-c3', '虾哥 Mini C3', 28, 1, NOW(), 1, NOW()),
|
||||||
|
(101029, 101, 'ESP32S3_KORVO2_V3开发板', 'esp32s3-korvo2-v3', 'ESP32S3_KORVO2_V3开发板', 29, 1, NOW(), 1, NOW()),
|
||||||
|
(101030, 101, 'ESP-SparkBot开发板', 'esp-sparkbot', 'ESP-SparkBot开发板', 30, 1, NOW(), 1, NOW()),
|
||||||
|
(101031, 101, 'ESP-Spot-S3', 'esp-spot-s3', 'ESP-Spot-S3', 31, 1, NOW(), 1, NOW()),
|
||||||
|
(101032, 101, 'Waveshare ESP32-S3-Touch-AMOLED-1.8', 'esp32-s3-touch-amoled-1.8', 'Waveshare ESP32-S3-Touch-AMOLED-1.8', 32, 1, NOW(), 1, NOW()),
|
||||||
|
(101033, 101, 'Waveshare ESP32-S3-Touch-LCD-1.85C', 'esp32-s3-touch-lcd-1.85c', 'Waveshare ESP32-S3-Touch-LCD-1.85C', 33, 1, NOW(), 1, NOW()),
|
||||||
|
(101034, 101, 'Waveshare ESP32-S3-Touch-LCD-1.85', 'esp32-s3-touch-lcd-1.85', 'Waveshare ESP32-S3-Touch-LCD-1.85', 34, 1, NOW(), 1, NOW()),
|
||||||
|
(101035, 101, 'Waveshare ESP32-S3-Touch-LCD-1.46', 'esp32-s3-touch-lcd-1.46', 'Waveshare ESP32-S3-Touch-LCD-1.46', 35, 1, NOW(), 1, NOW()),
|
||||||
|
(101036, 101, 'Waveshare ESP32-S3-Touch-LCD-3.5', 'esp32-s3-touch-lcd-3.5', 'Waveshare ESP32-S3-Touch-LCD-3.5', 36, 1, NOW(), 1, NOW()),
|
||||||
|
(101037, 101, '土豆子', 'tudouzi', '土豆子', 37, 1, NOW(), 1, NOW()),
|
||||||
|
(101038, 101, 'LILYGO T-Circle-S3', 'lilygo-t-circle-s3', 'LILYGO T-Circle-S3', 38, 1, NOW(), 1, NOW()),
|
||||||
|
(101039, 101, 'LILYGO T-CameraPlus-S3', 'lilygo-t-cameraplus-s3', 'LILYGO T-CameraPlus-S3', 39, 1, NOW(), 1, NOW()),
|
||||||
|
(101040, 101, 'Movecall Moji 小智AI衍生版', 'movecall-moji-esp32s3', 'Movecall Moji 小智AI衍生版', 40, 1, NOW(), 1, NOW()),
|
||||||
|
(101041, 101, 'Movecall CuiCan 璀璨·AI吊坠', 'movecall-cuican-esp32s3', 'Movecall CuiCan 璀璨·AI吊坠', 41, 1, NOW(), 1, NOW()),
|
||||||
|
(101042, 101, '正点原子DNESP32S3开发板', 'atk-dnesp32s3', '正点原子DNESP32S3开发板', 42, 1, NOW(), 1, NOW()),
|
||||||
|
(101043, 101, '正点原子DNESP32S3-BOX', 'atk-dnesp32s3-box', '正点原子DNESP32S3-BOX', 43, 1, NOW(), 1, NOW()),
|
||||||
|
(101044, 101, '嘟嘟开发板CHATX(wifi)', 'du-chatx', '嘟嘟开发板CHATX(wifi)', 44, 1, NOW(), 1, NOW()),
|
||||||
|
(101045, 101, '太极小派esp32s3', 'taiji-pi-s3', '太极小派esp32s3', 45, 1, NOW(), 1, NOW()),
|
||||||
|
(101046, 101, '无名科技星智0.85(WIFI)', 'xingzhi-cube-0.85tft-wifi', '无名科技星智0.85(WIFI)', 46, 1, NOW(), 1, NOW()),
|
||||||
|
(101047, 101, '无名科技星智0.85(ML307)', 'xingzhi-cube-0.85tft-ml307', '无名科技星智0.85(ML307)', 47, 1, NOW(), 1, NOW()),
|
||||||
|
(101048, 101, '无名科技星智0.96(WIFI)', 'xingzhi-cube-0.96oled-wifi', '无名科技星智0.96(WIFI)', 48, 1, NOW(), 1, NOW()),
|
||||||
|
(101049, 101, '无名科技星智0.96(ML307)', 'xingzhi-cube-0.96oled-ml307', '无名科技星智0.96(ML307)', 49, 1, NOW(), 1, NOW()),
|
||||||
|
(101050, 101, '无名科技星智1.54(WIFI)', 'xingzhi-cube-1.54tft-wifi', '无名科技星智1.54(WIFI)', 50, 1, NOW(), 1, NOW()),
|
||||||
|
(101051, 101, '无名科技星智1.54(ML307)', 'xingzhi-cube-1.54tft-ml307', '无名科技星智1.54(ML307)', 51, 1, NOW(), 1, NOW()),
|
||||||
|
(101052, 101, 'SenseCAP Watcher', 'sensecap-watcher', 'SenseCAP Watcher', 52, 1, NOW(), 1, NOW()),
|
||||||
|
(101053, 101, '四博智联AI陪伴盒子', 'doit-s3-aibox', '四博智联AI陪伴盒子', 53, 1, NOW(), 1, NOW()),
|
||||||
|
(101054, 101, '元控·青春', 'mixgo-nova', '元控·青春', 54, 1, NOW(), 1, NOW());
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- 初始化智能体聊天记录
|
||||||
|
DROP TABLE IF EXISTS ai_chat_history;
|
||||||
|
DROP TABLE IF EXISTS ai_chat_message;
|
||||||
|
DROP TABLE IF EXISTS ai_agent_chat_history;
|
||||||
|
CREATE TABLE ai_agent_chat_history
|
||||||
|
(
|
||||||
|
id BIGINT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY,
|
||||||
|
mac_address VARCHAR(50) COMMENT 'MAC地址',
|
||||||
|
agent_id VARCHAR(32) COMMENT '智能体id',
|
||||||
|
session_id VARCHAR(50) COMMENT '会话ID',
|
||||||
|
chat_type TINYINT(3) COMMENT '消息类型: 1-用户, 2-智能体',
|
||||||
|
content VARCHAR(1024) COMMENT '聊天内容',
|
||||||
|
audio_id VARCHAR(32) COMMENT '音频ID',
|
||||||
|
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL COMMENT '创建时间',
|
||||||
|
updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
INDEX idx_ai_agent_chat_history_mac (mac_address),
|
||||||
|
INDEX idx_ai_agent_chat_history_session_id (session_id),
|
||||||
|
INDEX idx_ai_agent_chat_history_agent_id (agent_id),
|
||||||
|
INDEX idx_ai_agent_chat_history_agent_session_created (agent_id, session_id, created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '智能体聊天记录表';
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS ai_agent_chat_audio;
|
||||||
|
CREATE TABLE ai_agent_chat_audio
|
||||||
|
(
|
||||||
|
id VARCHAR(32) COMMENT '主键ID' PRIMARY KEY,
|
||||||
|
audio LONGBLOB COMMENT '音频opus数据'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '智能体聊天音频数据表';
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- 添加百度ASR模型配置
|
||||||
|
delete from `ai_model_config` where `id` = 'ASR_BaiduASR';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('ASR_BaiduASR', 'ASR', 'BaiduASR', '百度语音识别', 0, 1, '{\"type\": \"baidu\", \"app_id\": \"\", \"api_key\": \"\", \"secret_key\": \"\", \"dev_pid\": 1537, \"output_dir\": \"tmp/\"}', NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
|
||||||
|
-- 添加百度ASR供应器
|
||||||
|
delete from `ai_model_provider` where `id` = 'SYSTEM_ASR_BaiduASR';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_ASR_BaiduASR', 'ASR', 'baidu', '百度语音识别', '[{"key":"app_id","label":"应用AppID","type":"string"},{"key":"api_key","label":"API Key","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"dev_pid","label":"语言参数","type":"number"},{"key":"output_dir","label":"输出目录","type":"string"}]', 7, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
|
||||||
|
-- 更新百度ASR配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.bce.baidu.com/ai-engine/old/#/ai/speech/app/list',
|
||||||
|
`remark` = '百度ASR配置说明:
|
||||||
|
1. 访问 https://console.bce.baidu.com/ai-engine/old/#/ai/speech/app/list
|
||||||
|
2. 创建新应用
|
||||||
|
3. 获取AppID、API Key和Secret Key
|
||||||
|
4. 填入配置文件中
|
||||||
|
查看资源额度:https://console.bce.baidu.com/ai-engine/old/#/ai/speech/overview/resource/list
|
||||||
|
语言参数说明:https://ai.baidu.com/ai-doc/SPEECH/0lbxfnc9b
|
||||||
|
' WHERE `id` = 'ASR_BaiduASR';
|
||||||
|
|
||||||
|
-- 更新豆包供应器字段
|
||||||
|
update `ai_model_provider` set `fields` =
|
||||||
|
'[{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"},{"key":"boosting_table_name","label":"热词文件名称","type":"string"},{"key":"correct_table_name","label":"替换词文件名称","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]'
|
||||||
|
where `id` = 'SYSTEM_ASR_DoubaoASR';
|
||||||
|
|
||||||
|
-- 更新豆包ASR配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://console.volcengine.com/speech/app',
|
||||||
|
`remark` = '豆包ASR配置说明:
|
||||||
|
1. 需要在火山引擎控制台创建应用并获取appid和access_token
|
||||||
|
2. 支持中文语音识别
|
||||||
|
3. 需要网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
申请步骤:
|
||||||
|
1. 访问 https://console.volcengine.com/speech/app
|
||||||
|
2. 创建新应用
|
||||||
|
3. 获取appid和access_token
|
||||||
|
4. 填入配置文件中
|
||||||
|
如需设置热词,请参考:https://www.volcengine.com/docs/6561/155738
|
||||||
|
' WHERE `id` = 'ASR_DoubaoASR';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 更新intent_llmM供应器
|
||||||
|
update `ai_model_provider` set fields = '[{"key":"llm","label":"LLM模型","type":"string"},{"key":"functions","label":"函数列表","type":"dict","dict_name":"functions"}]' where id = 'SYSTEM_Intent_intent_llm';
|
||||||
|
-- 更新ChatGLMLLM的意图识别配置
|
||||||
|
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\", \"functions\": \"get_weather;get_news_from_newsnow;play_music\"}' where id = 'Intent_intent_llm';
|
||||||
|
-- 更新函数调用意图识别配置
|
||||||
|
UPDATE `ai_model_config` SET config_json = REPLACE(config_json, ';get_news;', ';get_news_from_newsnow;') WHERE id = 'Intent_function_call';
|
||||||
@@ -79,3 +79,38 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504251422.sql
|
path: classpath:db/changelog/202504251422.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504291043
|
||||||
|
author: jiangkunyin
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504291043.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504301341
|
||||||
|
author: Goody
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504301341.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505022134
|
||||||
|
author: Goody
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505022134.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505081146
|
||||||
|
author: hrz
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505081146.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505091409
|
||||||
|
author: hrz
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505091409.sql
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="xiaozhi.modules.agent.dao.AiAgentChatHistoryDao">
|
||||||
|
<resultMap id="BaseResultMap" type="xiaozhi.modules.agent.entity.AgentChatHistoryEntity">
|
||||||
|
<!--@mbg.generated-->
|
||||||
|
<!--@Table ai_agent_chat_history-->
|
||||||
|
<id column="id" jdbcType="BIGINT" property="id" />
|
||||||
|
<result column="mac_address" jdbcType="VARCHAR" property="macAddress" />
|
||||||
|
<result column="agent_id" jdbcType="VARCHAR" property="agentId" />
|
||||||
|
<result column="session_id" jdbcType="VARCHAR" property="sessionId" />
|
||||||
|
<result column="sort" jdbcType="BIGINT" property="sort" />
|
||||||
|
<result column="chat_type" jdbcType="TINYINT" property="chatType" />
|
||||||
|
<result column="content" jdbcType="VARCHAR" property="content" />
|
||||||
|
<result column="audio" jdbcType="LONGVARCHAR" property="audio" />
|
||||||
|
<result column="audio_url" jdbcType="VARCHAR" property="audioUrl" />
|
||||||
|
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
|
||||||
|
<result column="updated_at" jdbcType="TIMESTAMP" property="updatedAt" />
|
||||||
|
</resultMap>
|
||||||
|
<sql id="Base_Column_List">
|
||||||
|
<!--@mbg.generated-->
|
||||||
|
id, mac_address, agent_id, session_id, sort, chat_type, content, audio, audio_url,
|
||||||
|
created_at, updated_at
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<delete id="deleteAudioByAgentId">
|
||||||
|
DELETE FROM ai_agent_chat_audio
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT audio_id
|
||||||
|
FROM ai_agent_chat_history
|
||||||
|
WHERE agent_id = #{agentId}
|
||||||
|
);
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<delete id="deleteHistoryByAgentId">
|
||||||
|
DELETE FROM ai_agent_chat_history
|
||||||
|
WHERE agent_id = #{agentId};
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="xiaozhi.modules.device.dao.DeviceDao">
|
||||||
|
<!-- 获取此智能体全部设备的最后连接时间 -->
|
||||||
|
<select id="getAllLastConnectedAtByAgentId" resultType="java.util.Date">
|
||||||
|
SELECT last_connected_at FROM ai_device
|
||||||
|
WHERE
|
||||||
|
agent_id = #{agentId}
|
||||||
|
order by
|
||||||
|
last_connected_at desc limit 0,1
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -2,12 +2,17 @@
|
|||||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
|
||||||
<mapper namespace="xiaozhi.modules.sys.dao.SysDictDataDao">
|
<mapper namespace="xiaozhi.modules.sys.dao.SysDictDataDao">
|
||||||
|
<select id="getDictDataByType" resultType="xiaozhi.modules.sys.vo.SysDictDataItem">
|
||||||
<select id="getDictDataList" resultType="xiaozhi.modules.sys.entity.DictData">
|
SELECT d.dict_label AS `name`, d.dict_value AS `key`
|
||||||
select dict_type_id, dict_label, dict_value from sys_dict_data order by dict_type_id, sort
|
FROM sys_dict_data d
|
||||||
|
LEFT JOIN sys_dict_type t ON d.dict_type_id = t.id
|
||||||
|
WHERE t.dict_type = #{dictType}
|
||||||
|
ORDER BY d.sort ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="getDataByTypeCode" resultType="xiaozhi.modules.sys.dto.SysDictDataDTO">
|
<select id="getTypeByTypeId" resultType="java.lang.String">
|
||||||
select * from sys_dict_data WHERE DICT_TYPE_ID =(SELECT id FROM SYS_DICT_TYPE WHERE DICT_TYPE = #{dictType}) order by sort;
|
SELECT dict_type
|
||||||
|
FROM sys_dict_type
|
||||||
|
WHERE id = #{dictTypeId}
|
||||||
</select>
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -3,8 +3,4 @@
|
|||||||
|
|
||||||
<mapper namespace="xiaozhi.modules.sys.dao.SysDictTypeDao">
|
<mapper namespace="xiaozhi.modules.sys.dao.SysDictTypeDao">
|
||||||
|
|
||||||
<select id="getDictTypeList" resultType="xiaozhi.modules.sys.entity.DictType">
|
|
||||||
select id, dict_type from sys_dict_type order by dict_type, sort
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package xiaozhi.modules.device;
|
package xiaozhi.modules.device;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -8,8 +11,9 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@@ -19,18 +23,37 @@ public class DeviceTest {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RedisUtils redisUtils;
|
private RedisUtils redisUtils;
|
||||||
|
@Autowired
|
||||||
|
private SysUserService sysUserService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaveUser() {
|
||||||
|
SysUserDTO userDTO = new SysUserDTO();
|
||||||
|
userDTO.setUsername("test");
|
||||||
|
userDTO.setPassword(UUID.randomUUID().toString());
|
||||||
|
sysUserService.save(userDTO);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("测试写入设备信息")
|
@DisplayName("测试写入设备信息")
|
||||||
public void testWriteDeviceInfo() {
|
public void testWriteDeviceInfo() {
|
||||||
log.info("开始测试写入设备信息...");
|
log.info("开始测试写入设备信息...");
|
||||||
|
|
||||||
// 模拟设备MAC地址
|
// 模拟设备MAC地址
|
||||||
String macAddress = "00:11:22:33:44:55";
|
String macAddress = "00:11:22:33:44:66";
|
||||||
// 模拟设备验证码
|
// 模拟设备验证码
|
||||||
String deviceCode = "123456";
|
String deviceCode = "123456";
|
||||||
|
|
||||||
String redisKey = RedisKeys.getDeviceCaptchaKey(deviceCode);
|
HashMap<String, Object> map = new HashMap<>();
|
||||||
|
map.put("mac_address", macAddress);
|
||||||
|
map.put("activation_code", deviceCode);
|
||||||
|
map.put("board", "硬件型号");
|
||||||
|
map.put("app_version", "0.3.13");
|
||||||
|
|
||||||
|
String safeDeviceId = macAddress.replace(":", "_").toLowerCase();
|
||||||
|
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
redisUtils.set(cacheDeviceKey, map, 300);
|
||||||
|
|
||||||
|
String redisKey = "ota:activation:code:" + deviceCode;
|
||||||
log.info("Redis Key: {}", redisKey);
|
log.info("Redis Key: {}", redisKey);
|
||||||
|
|
||||||
// 将设备信息写入Redis
|
// 将设备信息写入Redis
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
import admin from './module/admin.js'
|
import admin from './module/admin.js'
|
||||||
import agent from './module/agent.js'
|
import agent from './module/agent.js'
|
||||||
import device from './module/device.js'
|
import device from './module/device.js'
|
||||||
|
import dict from './module/dict.js'
|
||||||
import model from './module/model.js'
|
import model from './module/model.js'
|
||||||
|
import ota from './module/ota.js'
|
||||||
import timbre from "./module/timbre.js"
|
import timbre from "./module/timbre.js"
|
||||||
import user from './module/user.js'
|
import user from './module/user.js'
|
||||||
import ota from './module/ota.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接口地址
|
* 接口地址
|
||||||
* 开发时自动读取使用.env.development文件
|
* 开发时自动读取使用.env.development文件
|
||||||
@@ -32,5 +32,6 @@ export default {
|
|||||||
device,
|
device,
|
||||||
model,
|
model,
|
||||||
timbre,
|
timbre,
|
||||||
ota
|
ota,
|
||||||
|
dict
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,4 +97,50 @@ export default {
|
|||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
|
// 获取智能体会话列表
|
||||||
|
getAgentSessions(agentId, params, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/agent/${agentId}/sessions`)
|
||||||
|
.method('GET')
|
||||||
|
.data(params)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getAgentSessions(agentId, params, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 获取智能体聊天记录
|
||||||
|
getAgentChatHistory(agentId, sessionId, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/agent/${agentId}/chat-history/${sessionId}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getAgentChatHistory(agentId, sessionId, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 获取音频下载ID
|
||||||
|
getAudioId(audioId, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/agent/audio/${audioId}`)
|
||||||
|
.method('POST')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getAudioId(audioId, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { getServiceUrl } from '../api';
|
||||||
|
import RequestService from '../httpRequest';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 获取字典类型列表
|
||||||
|
getDictTypeList(params, callback) {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
dictType: params.dictType || '',
|
||||||
|
dictName: params.dictName || '',
|
||||||
|
page: params.page || 1,
|
||||||
|
limit: params.limit || 10
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/type/page?${queryParams}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取字典类型列表失败:', err)
|
||||||
|
this.$message.error(err.msg || '获取字典类型列表失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDictTypeList(params, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取字典类型详情
|
||||||
|
getDictTypeDetail(id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/type/${id}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取字典类型详情失败:', err)
|
||||||
|
this.$message.error(err.msg || '获取字典类型详情失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDictTypeDetail(id, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增字典类型
|
||||||
|
addDictType(data, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/type/save`)
|
||||||
|
.method('POST')
|
||||||
|
.data(data)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('新增字典类型失败:', err)
|
||||||
|
this.$message.error(err.msg || '新增字典类型失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.addDictType(data, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新字典类型
|
||||||
|
updateDictType(data, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/type/update`)
|
||||||
|
.method('PUT')
|
||||||
|
.data(data)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('更新字典类型失败:', err)
|
||||||
|
this.$message.error(err.msg || '更新字典类型失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.updateDictType(data, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除字典类型
|
||||||
|
deleteDictType(ids, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/type/delete`)
|
||||||
|
.method('POST')
|
||||||
|
.data(ids)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('删除字典类型失败:', err)
|
||||||
|
this.$message.error(err.msg || '删除字典类型失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.deleteDictType(ids, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取字典数据列表
|
||||||
|
getDictDataList(params, callback) {
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
dictTypeId: params.dictTypeId,
|
||||||
|
dictLabel: params.dictLabel || '',
|
||||||
|
dictValue: params.dictValue || '',
|
||||||
|
page: params.page || 1,
|
||||||
|
limit: params.limit || 10
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/data/page?${queryParams}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取字典数据列表失败:', err)
|
||||||
|
this.$message.error(err.msg || '获取字典数据列表失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDictDataList(params, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取字典数据详情
|
||||||
|
getDictDataDetail(id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/data/${id}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取字典数据详情失败:', err)
|
||||||
|
this.$message.error(err.msg || '获取字典数据详情失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDictDataDetail(id, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增字典数据
|
||||||
|
addDictData(data, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/data/save`)
|
||||||
|
.method('POST')
|
||||||
|
.data(data)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('新增字典数据失败:', err)
|
||||||
|
this.$message.error(err.msg || '新增字典数据失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.addDictData(data, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新字典数据
|
||||||
|
updateDictData(data, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/data/update`)
|
||||||
|
.method('PUT')
|
||||||
|
.data(data)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('更新字典数据失败:', err)
|
||||||
|
this.$message.error(err.msg || '更新字典数据失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.updateDictData(data, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除字典数据
|
||||||
|
deleteDictData(ids, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/data/delete`)
|
||||||
|
.method('POST')
|
||||||
|
.data(ids)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
callback(res)
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('删除字典数据失败:', err)
|
||||||
|
this.$message.error(err.msg || '删除字典数据失败')
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.deleteDictData(ids, callback)
|
||||||
|
})
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取字典数据列表
|
||||||
|
getDictDataByType(dictType) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/admin/dict/data/type/${dictType}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
if (res.data && res.data.code === 0) {
|
||||||
|
resolve(res.data)
|
||||||
|
} else {
|
||||||
|
reject(new Error(res.data?.msg || '获取字典数据列表失败'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取字典数据列表失败:', err)
|
||||||
|
reject(err)
|
||||||
|
}).send()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible="visible" @close="handleClose" width="400px" center>
|
<el-dialog :visible="visible" @close="handleClose" width="24%" center>
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center
|
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="57%" center
|
||||||
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
||||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注"
|
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
class="custom-input-bg"></el-input>
|
class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -271,7 +271,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.center-dialog .el-dialog {
|
.center-dialog .el-dialog {
|
||||||
margin: 4% 0 auto !important;
|
margin: 0 0 auto !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible="visible" @close="handleClose" width="400px" center @open="handleOpen">
|
<el-dialog :visible="visible" @close="handleClose" width="25%" center @open="handleOpen">
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
<div style="font-weight: 400;text-align: left;color: #3d4566;">
|
||||||
<div style="color: red;display: inline-block;">*</div> 智能体名称:
|
<div style="color: red;display: inline-block;">*</div> 智能体名称:
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<form>
|
<form>
|
||||||
<el-dialog :visible.sync="value" width="400px" center>
|
<el-dialog :visible.sync="dialogVisible" width="24%" center>
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
@@ -60,11 +60,20 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
dialogVisible: this.value,
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
confirmNewPassword: ""
|
confirmNewPassword: ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
value(val) {
|
||||||
|
this.dialogVisible = val;
|
||||||
|
},
|
||||||
|
dialogVisible(val) {
|
||||||
|
this.$emit('input', val);
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['logout']), // 引入Vuex的logout action
|
...mapActions(['logout']), // 引入Vuex的logout action
|
||||||
confirm() {
|
confirm() {
|
||||||
@@ -101,7 +110,7 @@ export default {
|
|||||||
this.$emit('input', false);
|
this.$emit('input', false);
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('input', false);
|
this.dialogVisible = false;
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
|
|||||||
@@ -0,0 +1,447 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :title="'与' + agentName + '的聊天记录' + (currentMacAddress ? '[' + currentMacAddress + ']' : '')"
|
||||||
|
:visible.sync="dialogVisible" width="80%" :before-close="handleClose" custom-class="chat-history-dialog">
|
||||||
|
<div class="chat-container">
|
||||||
|
<div class="session-list" @scroll="handleScroll">
|
||||||
|
<div v-for="session in sessions" :key="session.sessionId" class="session-item"
|
||||||
|
:class="{ active: currentSessionId === session.sessionId }" @click="selectSession(session)">
|
||||||
|
<img :src="getUserAvatar(session.sessionId)" class="avatar" />
|
||||||
|
<div class="session-info">
|
||||||
|
<div class="session-time">{{ formatTime(session.createdAt) }}</div>
|
||||||
|
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="loading">加载中...</div>
|
||||||
|
<div v-if="!hasMore" class="no-more">没有更多记录了</div>
|
||||||
|
</div>
|
||||||
|
<div class="chat-content">
|
||||||
|
<div v-if="currentSessionId" class="messages">
|
||||||
|
<div v-for="(message, index) in messagesWithTime" :key="message.id">
|
||||||
|
<div v-if="message.type === 'time'" class="time-divider">
|
||||||
|
{{ message.content }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="message-item" :class="{ 'user-message': message.chatType === 1 }">
|
||||||
|
<img :src="message.chatType === 1 ? getUserAvatar(currentSessionId) : require('@/assets/xiaozhi-logo.png')"
|
||||||
|
class="avatar" />
|
||||||
|
<div class="message-content">
|
||||||
|
{{ message.content }}
|
||||||
|
<i v-if="message.audioId" :class="getAudioIconClass(message)"
|
||||||
|
@click="playAudio(message)" class="audio-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-session-selected">
|
||||||
|
请选择会话查看聊天记录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChatHistoryDialog',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
agentId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
agentName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogVisible: false,
|
||||||
|
sessions: [],
|
||||||
|
messages: [],
|
||||||
|
currentSessionId: '',
|
||||||
|
currentMacAddress: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
loading: false,
|
||||||
|
hasMore: true,
|
||||||
|
scrollTimer: null,
|
||||||
|
isFirstLoad: true,
|
||||||
|
playingAudioId: null,
|
||||||
|
audioElement: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(val) {
|
||||||
|
this.dialogVisible = val;
|
||||||
|
if (val) {
|
||||||
|
this.resetData();
|
||||||
|
this.loadSessions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialogVisible(val) {
|
||||||
|
if (!val) {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
messagesWithTime() {
|
||||||
|
if (!this.messages || this.messages.length === 0) return [];
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
const TIME_INTERVAL = 60 * 1000; // 1分钟的时间间隔(毫秒)
|
||||||
|
|
||||||
|
// 添加第一条消息的时间标记
|
||||||
|
if (this.messages[0]) {
|
||||||
|
result.push({
|
||||||
|
type: 'time',
|
||||||
|
content: this.formatTime(this.messages[0].createdAt),
|
||||||
|
id: `time-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理消息列表
|
||||||
|
for (let i = 0; i < this.messages.length; i++) {
|
||||||
|
const currentMessage = this.messages[i];
|
||||||
|
result.push(currentMessage);
|
||||||
|
|
||||||
|
// 检查是否需要添加时间标记
|
||||||
|
if (i < this.messages.length - 1) {
|
||||||
|
const currentTime = new Date(currentMessage.createdAt).getTime();
|
||||||
|
const nextTime = new Date(this.messages[i + 1].createdAt).getTime();
|
||||||
|
|
||||||
|
if (nextTime - currentTime > TIME_INTERVAL) {
|
||||||
|
result.push({
|
||||||
|
type: 'time',
|
||||||
|
content: this.formatTime(this.messages[i + 1].createdAt),
|
||||||
|
id: `time-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetData() {
|
||||||
|
this.sessions = [];
|
||||||
|
this.messages = [];
|
||||||
|
this.currentSessionId = '';
|
||||||
|
this.currentMacAddress = '';
|
||||||
|
this.page = 1;
|
||||||
|
this.loading = false;
|
||||||
|
this.hasMore = true;
|
||||||
|
this.isFirstLoad = true;
|
||||||
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.dialogVisible = false;
|
||||||
|
},
|
||||||
|
loadSessions() {
|
||||||
|
if (this.loading || (!this.isFirstLoad && !this.hasMore)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
const params = {
|
||||||
|
page: this.page,
|
||||||
|
limit: this.limit
|
||||||
|
};
|
||||||
|
|
||||||
|
Api.agent.getAgentSessions(this.agentId, params, (res) => {
|
||||||
|
if (res.data && res.data.data && Array.isArray(res.data.data.list)) {
|
||||||
|
const list = res.data.data.list;
|
||||||
|
this.hasMore = list.length === this.limit;
|
||||||
|
|
||||||
|
this.sessions = [...this.sessions, ...list];
|
||||||
|
this.page++;
|
||||||
|
|
||||||
|
if (this.sessions.length > 0 && !this.currentSessionId) {
|
||||||
|
this.selectSession(this.sessions[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
this.isFirstLoad = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selectSession(session) {
|
||||||
|
this.currentSessionId = session.sessionId;
|
||||||
|
Api.agent.getAgentChatHistory(this.agentId, session.sessionId, (res) => {
|
||||||
|
if (res.data && res.data.data) {
|
||||||
|
this.messages = res.data.data;
|
||||||
|
if (this.messages.length > 0 && this.messages[0].macAddress) {
|
||||||
|
this.currentMacAddress = this.messages[0].macAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleScroll(e) {
|
||||||
|
if (this.scrollTimer) {
|
||||||
|
clearTimeout(this.scrollTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollTimer = setTimeout(() => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||||
|
// 当滚动到底部时加载更多
|
||||||
|
if (scrollHeight - scrollTop <= clientHeight + 50) {
|
||||||
|
this.loadSessions();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
formatTime(timestamp) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
if (date >= today) {
|
||||||
|
return `今天 ${hours}:${minutes}`;
|
||||||
|
} else if (date >= yesterday) {
|
||||||
|
return `昨天 ${hours}:${minutes}`;
|
||||||
|
} else {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAudioIconClass(message) {
|
||||||
|
if (this.playingAudioId === message.audioId) {
|
||||||
|
return 'el-icon-loading';
|
||||||
|
}
|
||||||
|
return 'el-icon-video-play';
|
||||||
|
},
|
||||||
|
playAudio(message) {
|
||||||
|
if (this.playingAudioId === message.audioId) {
|
||||||
|
// 如果正在播放当前音频,则停止播放
|
||||||
|
if (this.audioElement) {
|
||||||
|
this.audioElement.pause();
|
||||||
|
this.audioElement = null;
|
||||||
|
}
|
||||||
|
this.playingAudioId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止当前正在播放的音频
|
||||||
|
if (this.audioElement) {
|
||||||
|
this.audioElement.pause();
|
||||||
|
this.audioElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先获取音频下载ID
|
||||||
|
this.playingAudioId = message.audioId;
|
||||||
|
Api.agent.getAudioId(message.audioId, (res) => {
|
||||||
|
if (res.data && res.data.data) {
|
||||||
|
// 使用获取到的下载ID播放音频
|
||||||
|
this.audioElement = new Audio(Api.getServiceUrl() + `/agent/play/${res.data.data}`);
|
||||||
|
|
||||||
|
this.audioElement.onended = () => {
|
||||||
|
this.playingAudioId = null;
|
||||||
|
this.audioElement = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.audioElement.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getUserAvatar(sessionId) {
|
||||||
|
// 从 sessionId 中提取所有数字
|
||||||
|
const numbers = sessionId.match(/\d+/g);
|
||||||
|
if (!numbers) return require('@/assets/user-avatar1.png');
|
||||||
|
|
||||||
|
// 将所有数字相加
|
||||||
|
const sum = numbers.reduce((acc, num) => acc + parseInt(num), 0);
|
||||||
|
|
||||||
|
// 计算模5并加1,得到1-5之间的数字
|
||||||
|
const avatarIndex = (sum % 5) + 1;
|
||||||
|
|
||||||
|
// 返回对应的头像图片
|
||||||
|
return require(`@/assets/user-avatar${avatarIndex}.png`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chat-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-list {
|
||||||
|
width: 250px;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-item.active {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-time {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #272727;
|
||||||
|
float: left;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
/* 为消息数量留出空间 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-count {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #b4b4b4;
|
||||||
|
border-radius: 20px;
|
||||||
|
float: left;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item.user-message {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
max-width: 60%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin: 0 10px;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 20px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .message-content {
|
||||||
|
background-color: #1890ff;
|
||||||
|
color: white;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .audio-icon {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 10px 30px 10px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-session-selected {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-divider {
|
||||||
|
text-align: center;
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-divider::before,
|
||||||
|
.time-divider::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 30%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #eee;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chat-history-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 700px;
|
||||||
|
margin: 0 !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
height: 90vh;
|
||||||
|
max-width: 85vw;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-dialog .el-dialog__header {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-dialog .el-dialog__body {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
height: calc(90vh - 54px);
|
||||||
|
/* 减去标题栏的高度 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,9 +26,12 @@
|
|||||||
<div class="settings-btn" @click="handleDeviceManage">
|
<div class="settings-btn" @click="handleDeviceManage">
|
||||||
设备管理({{ device.deviceCount }})
|
设备管理({{ device.deviceCount }})
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-btn" @click="handleChatHistory">
|
||||||
|
聊天记录
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
<div>最近对话:{{ device.lastConnectedAt }}</div>
|
<div>最近对话:{{ formattedLastConnectedTime }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,6 +45,27 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return { switchValue: false }
|
return { switchValue: false }
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
formattedLastConnectedTime() {
|
||||||
|
if (!this.device.lastConnectedAt) return '暂未对话';
|
||||||
|
|
||||||
|
const lastTime = new Date(this.device.lastConnectedAt);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMinutes = Math.floor((now - lastTime) / (1000 * 60));
|
||||||
|
|
||||||
|
if (diffMinutes <= 1) {
|
||||||
|
return '刚刚';
|
||||||
|
} else if (diffMinutes < 60) {
|
||||||
|
return `${diffMinutes}分钟前`;
|
||||||
|
} else if (diffMinutes < 24 * 60) {
|
||||||
|
const hours = Math.floor(diffMinutes / 60);
|
||||||
|
const minutes = diffMinutes % 60;
|
||||||
|
return `${hours}小时${minutes > 0 ? minutes + '分钟' : ''}前`;
|
||||||
|
} else {
|
||||||
|
return this.device.lastConnectedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleDelete() {
|
handleDelete() {
|
||||||
this.$emit('delete', this.device.agentId)
|
this.$emit('delete', this.device.agentId)
|
||||||
@@ -51,6 +75,9 @@ export default {
|
|||||||
},
|
},
|
||||||
handleDeviceManage() {
|
handleDeviceManage() {
|
||||||
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
||||||
|
},
|
||||||
|
handleChatHistory() {
|
||||||
|
this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||||
|
<el-form :model="form" :rules="rules" ref="form" label-width="100px">
|
||||||
|
<el-form-item label="字典标签" prop="dictLabel">
|
||||||
|
<el-input v-model="form.dictLabel" placeholder="请输入字典标签"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字典值" prop="dictValue">
|
||||||
|
<el-input v-model="form.dictValue" placeholder="请输入字典值"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="sort">
|
||||||
|
<el-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSave">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DictDataDialog',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '新增字典数据'
|
||||||
|
},
|
||||||
|
dictData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
dictTypeId: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogVisible: this.visible,
|
||||||
|
form: {
|
||||||
|
id: null,
|
||||||
|
dictTypeId: null,
|
||||||
|
dictLabel: '',
|
||||||
|
dictValue: '',
|
||||||
|
sort: 0
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||||
|
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dictData: {
|
||||||
|
handler(val) {
|
||||||
|
if (val) {
|
||||||
|
this.form = { ...val }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
dictTypeId: {
|
||||||
|
handler(val) {
|
||||||
|
if (val) {
|
||||||
|
this.form.dictTypeId = val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
visible(val) {
|
||||||
|
this.dialogVisible = val;
|
||||||
|
},
|
||||||
|
dialogVisible(val) {
|
||||||
|
this.$emit('update:visible', val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.resetForm();
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
this.form = {
|
||||||
|
id: null,
|
||||||
|
dictTypeId: this.dictTypeId,
|
||||||
|
dictLabel: '',
|
||||||
|
dictValue: '',
|
||||||
|
sort: 0
|
||||||
|
}
|
||||||
|
this.$refs.form?.resetFields()
|
||||||
|
},
|
||||||
|
handleSave() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.$emit('save', this.form)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||||
|
<el-form :model="form" :rules="rules" ref="form" label-width="120px">
|
||||||
|
<el-form-item label="字典类型名称" prop="dictName">
|
||||||
|
<el-input v-model="form.dictName" placeholder="请输入字典类型名称"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="字典类型编码" prop="dictType">
|
||||||
|
<el-input v-model="form.dictType" placeholder="请输入字典类型编码"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSave">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'DictTypeDialog',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '新增字典类型'
|
||||||
|
},
|
||||||
|
dictTypeData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogVisible: this.visible,
|
||||||
|
form: {
|
||||||
|
id: null,
|
||||||
|
dictName: '',
|
||||||
|
dictType: ''
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
dictName: [{ required: true, message: '请输入字典类型名称', trigger: 'blur' }],
|
||||||
|
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(val) {
|
||||||
|
this.dialogVisible = val;
|
||||||
|
},
|
||||||
|
dialogVisible(val) {
|
||||||
|
this.$emit('update:visible', val);
|
||||||
|
},
|
||||||
|
dictTypeData: {
|
||||||
|
handler(val) {
|
||||||
|
if (val) {
|
||||||
|
this.form = { ...val }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.resetForm()
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
this.form = {
|
||||||
|
id: null,
|
||||||
|
dictName: '',
|
||||||
|
dictType: ''
|
||||||
|
}
|
||||||
|
this.$refs.form?.resetFields()
|
||||||
|
},
|
||||||
|
handleSave() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.$emit('save', this.form)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" :visible.sync="visible" width="500px" @close="handleClose" @open="handleOpen">
|
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose" @open="handleOpen">
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="固件名称" prop="firmwareName">
|
<el-form-item label="固件名称" prop="firmwareName">
|
||||||
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
||||||
@@ -38,7 +38,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import { FIRMWARE_TYPES } from '@/utils';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FirmwareDialog',
|
name: 'FirmwareDialog',
|
||||||
@@ -54,14 +53,19 @@ export default {
|
|||||||
form: {
|
form: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
firmwareTypes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
firmwareTypes: FIRMWARE_TYPES,
|
|
||||||
uploadProgress: 0,
|
uploadProgress: 0,
|
||||||
uploadStatus: '',
|
uploadStatus: '',
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
|
dialogVisible: this.visible,
|
||||||
rules: {
|
rules: {
|
||||||
firmwareName: [
|
firmwareName: [
|
||||||
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' }
|
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' }
|
||||||
@@ -85,9 +89,21 @@ export default {
|
|||||||
return !!this.form.id
|
return !!this.form.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
// 移除 getDictDataByType 调用
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(val) {
|
||||||
|
this.dialogVisible = val;
|
||||||
|
},
|
||||||
|
dialogVisible(val) {
|
||||||
|
this.$emit('update:visible', val);
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 移除 getFirmwareTypes 方法
|
||||||
handleClose() {
|
handleClose() {
|
||||||
this.$refs.form.clearValidate();
|
this.dialogVisible = false;
|
||||||
this.$emit('cancel');
|
this.$emit('cancel');
|
||||||
},
|
},
|
||||||
handleCancel() {
|
handleCancel() {
|
||||||
@@ -195,13 +211,17 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
::v-deep .el-dialog {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-demo {
|
.upload-demo {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-upload__tip {
|
.el-upload__tip {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
padding-top: 5px;
|
padding-top: 2%;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
|
|
||||||
<!-- 中间导航菜单 -->
|
<!-- 中间导航菜单 -->
|
||||||
<div class="header-center">
|
<div class="header-center">
|
||||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
<div class="equipment-management"
|
||||||
|
:class="{ 'active-tab': $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' }"
|
||||||
|
@click="goHome">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
||||||
:style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
智能体管理
|
智能体管理
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
||||||
@@ -26,18 +28,29 @@
|
|||||||
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
用户管理
|
用户管理
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSuperAdmin" class="equipment-management"
|
|
||||||
:class="{ 'active-tab': $route.path === '/params-management' }" @click="goParamManagement">
|
|
||||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
|
||||||
:style="{ filter: $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
|
||||||
参数管理
|
|
||||||
</div>
|
|
||||||
<div v-if="isSuperAdmin" class="equipment-management"
|
<div v-if="isSuperAdmin" class="equipment-management"
|
||||||
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
||||||
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
OTA管理
|
OTA管理
|
||||||
</div>
|
</div>
|
||||||
|
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
||||||
|
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' }">
|
||||||
|
<span class="el-dropdown-link">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||||
|
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
|
参数字典
|
||||||
|
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||||
|
</span>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click.native="goParamManagement">
|
||||||
|
参数管理
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="goDictManagement">
|
||||||
|
字典管理
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧元素 -->
|
<!-- 右侧元素 -->
|
||||||
@@ -114,6 +127,9 @@ export default {
|
|||||||
goOtaManagement() {
|
goOtaManagement() {
|
||||||
this.$router.push('/ota-management')
|
this.$router.push('/ota-management')
|
||||||
},
|
},
|
||||||
|
goDictManagement() {
|
||||||
|
this.$router.push('/dict-management')
|
||||||
|
},
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
fetchUserInfo() {
|
fetchUserInfo() {
|
||||||
userApi.getUserInfo(({ data }) => {
|
userApi.getUserInfo(({ data }) => {
|
||||||
@@ -343,4 +359,32 @@ export default {
|
|||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.equipment-management.more-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-management.more-dropdown .el-dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
min-width: 120px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dropdown-menu__item {
|
||||||
|
min-width: 60px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.equipment-management.more-dropdown .el-dropdown-menu {
|
||||||
|
position: fixed;
|
||||||
|
right: 10px;
|
||||||
|
top: 60px;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||