Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
919c2ffd46 | ||
|
|
e6d63a811e | ||
|
|
92227098b7 | ||
|
|
571080c1d6 | ||
|
|
d07feb837d | ||
|
|
63e378524b | ||
|
|
ea5f54e421 | ||
|
|
72f7514114 | ||
|
|
44e1f00ffc | ||
|
|
4a3ac2cfcd | ||
|
|
bbc31e01c4 | ||
|
|
770a198772 | ||
|
|
a26bee3696 | ||
|
|
abb8f4f963 | ||
|
|
43d2adff70 | ||
|
|
e2dffea423 | ||
|
|
3d5eaba46f | ||
|
|
1bb8fbd56f | ||
|
|
b699886953 | ||
|
|
12c957d48b | ||
|
|
48e890c1b1 | ||
|
|
05331f001a | ||
|
|
aa77bfdfc4 | ||
|
|
a2baef8911 | ||
|
|
3fcfb65d45 | ||
|
|
8f266bea0d | ||
|
|
0a765f4aac | ||
|
|
ee3f0555d1 | ||
|
|
9fc1285c09 | ||
|
|
e6e8ccee50 | ||
|
|
f9632af016 | ||
|
|
81359bf419 | ||
|
|
a144570ed7 | ||
|
|
0732b72e8f | ||
|
|
8332445a52 | ||
|
|
c9111d1bfc | ||
|
|
e7d999278d | ||
|
|
4043d20ea6 | ||
|
|
afe88ccfd6 | ||
|
|
56c3a04809 | ||
|
|
4530345a1f | ||
|
|
4a89ec8515 | ||
|
|
cb5f1c6485 | ||
|
|
15c0677a4b | ||
|
|
dc68b8148a | ||
|
|
1976034f12 | ||
|
|
727621fdac | ||
|
|
0f3806d358 | ||
|
|
dfb2bf3923 | ||
|
|
bf08a91191 | ||
|
|
f0e353cc1a | ||
|
|
456b1eddcc | ||
|
|
5e08f05437 | ||
|
|
9f9227bae4 | ||
|
|
76f47bce37 | ||
|
|
e1f54b30dc | ||
|
|
fc4e7e6c05 | ||
|
|
7aaa79b644 | ||
|
|
9b6f894be2 | ||
|
|
48f7ef2023 | ||
|
|
54692859fd | ||
|
|
1719244015 | ||
|
|
48f8c6c5b7 | ||
|
|
f0815e595b | ||
|
|
4f829017ed | ||
|
|
d2f8f05acb | ||
|
|
e8b1156bfd | ||
|
|
bf748773c8 | ||
|
|
a05b25ae90 | ||
|
|
3ceb3feccf | ||
|
|
b4944f23ef | ||
|
|
09c17b2dc5 | ||
|
|
6f7ff9f858 | ||
|
|
9e9af2a031 | ||
|
|
7d74e853ae | ||
|
|
1d627d570d | ||
|
|
9b0088f4f0 | ||
|
|
b50c7dbed4 | ||
|
|
f92c0313a6 | ||
|
|
233e1ee0a6 | ||
|
|
805b40ec76 | ||
|
|
86ca3e5be0 | ||
|
|
bb2143d9a0 | ||
|
|
ef662e0ccf | ||
|
|
30708a59f1 | ||
|
|
b0262cfe72 | ||
|
|
259886c1a8 | ||
|
|
f1b4b33727 | ||
|
|
38e8b690bd | ||
|
|
9a52b43e09 | ||
|
|
5e35cfc44a | ||
|
|
ac0ec6dfe6 | ||
|
|
361defc348 | ||
|
|
b9fc0da215 | ||
|
|
6ac8947168 | ||
|
|
5655930d12 | ||
|
|
18acc37324 | ||
|
|
e9155d719e | ||
|
|
16e77b02c2 | ||
|
|
adc1902649 | ||
|
|
1cc470bc5a | ||
|
|
b3f6efc7d0 | ||
|
|
d91fbf3ff4 | ||
|
|
e552593474 | ||
|
|
1f5a78f458 | ||
|
|
f490c15282 | ||
|
|
90a31debd1 | ||
|
|
416145a90e | ||
|
|
6285786282 | ||
|
|
f9f6a69c89 | ||
|
|
61da60bdf1 | ||
|
|
2d5ef3011a | ||
|
|
63dab6d224 | ||
|
|
38dd76dfb3 | ||
|
|
fe642de526 | ||
|
|
f4b7eefc38 | ||
|
|
5bbbad02b4 | ||
|
|
bd85dd5faf | ||
|
|
d91ba3d219 | ||
|
|
7992c40815 | ||
|
|
574b01db97 | ||
|
|
2d6049b7bd | ||
|
|
96d0f2e6f9 | ||
|
|
b46bd4d0dd | ||
|
|
244c76793c | ||
|
|
411fc46189 | ||
|
|
48427e330f | ||
|
|
5ddc2451c1 | ||
|
|
e357cad2bb | ||
|
|
9a5aaea8ca | ||
|
|
39fe057f9f | ||
|
|
c9eb640510 | ||
|
|
766a706250 | ||
|
|
f55d6c2e60 |
@@ -9,26 +9,24 @@ assignees: ''
|
||||
## 🐛 问题描述
|
||||
<!-- 清晰简洁地描述问题是什么 -->
|
||||
|
||||
## 🔍 复现步骤
|
||||
<!-- 详细描述复现问题的步骤 -->
|
||||
## 🖥️ 环境信息
|
||||
- 部署方式: 全模块部署 还是 单Server部署
|
||||
- 版本号: 例如 0.3.x
|
||||
|
||||
## 🔍 告诉我们,应该怎么复现这个问题
|
||||
<!-- 这个很重要,方便我们快速定位 -->
|
||||
1. 打开 '...'
|
||||
2. 点击 '...'
|
||||
3. 滚动到 '...'
|
||||
4. 看到错误
|
||||
|
||||
## 🤔 预期行为
|
||||
## 🤔 你原本希望是怎么样的
|
||||
<!-- 简要描述预期的正确行为 -->
|
||||
|
||||
## 😯 截图
|
||||
## 😯 提供一些截图
|
||||
<!-- 如果适用,添加问题的截图 -->
|
||||
|
||||
## 🖥️ 环境信息
|
||||
- 操作系统: [例如 Windows 10]
|
||||
- 浏览器: [例如 Chrome 89]
|
||||
- 项目版本: [例如 1.0.0]
|
||||
- Python版本: [例如 3.9.13]
|
||||
- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS]
|
||||
- Nodejs版本:[例如 v20.14.0]
|
||||
1. 比如日志截图,越多越好
|
||||
2. 比如界面反应
|
||||
|
||||
## 📋 其他信息
|
||||
<!-- 在此添加关于此问题的任何其他上下文信息 -->
|
||||
|
||||
@@ -165,3 +165,4 @@ main/manager-api/.vscode
|
||||
main/manager-web/.webpack_cache/
|
||||
main/xiaozhi-server/mysql
|
||||
uploadfile
|
||||
.vscode
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[](https://github.com/xinnan-tech/xiaozhi-esp32-server)
|
||||
|
||||
|
||||
<h1 align="center">小智后端服务xiaozhi-esp32-server</h1>
|
||||
|
||||
<p align="center">
|
||||
@@ -12,11 +11,10 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">English</a>
|
||||
· 简体中文
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
|
||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">反馈问题</a>
|
||||
· <a href="./docs/FAQ.md">常见问题</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/issues">反馈问题</a>
|
||||
· <a href="./README.md#%E9%83%A8%E7%BD%B2%E6%96%87%E6%A1%A3">部署文档</a>
|
||||
· <a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">更新日志</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/releases">
|
||||
@@ -132,17 +130,7 @@
|
||||
1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。
|
||||
建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。
|
||||
|
||||
2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件
|
||||
`config.yaml` 中开启防护:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
auth:
|
||||
# 开启防护
|
||||
enabled: true
|
||||
```
|
||||
|
||||
开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。
|
||||
2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。
|
||||
|
||||
---
|
||||
|
||||
@@ -156,21 +144,18 @@ server:
|
||||
|
||||
| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 |
|
||||
|---------|------|---------|---------|---------|
|
||||
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库,仅支持虾哥1.6.0及以下固件版本 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
|
||||
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验,支持虾哥最新固件 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
|
||||
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
|
||||
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
|
||||
|
||||
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
|
||||
|
||||
```
|
||||
智控台地址: https://2662r3426b.vicp.fun
|
||||
|
||||
服务测试工具: https://2662r3426b.vicp.fun/test/
|
||||
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
```
|
||||
---
|
||||
|
||||
## 常见问题 ❓
|
||||
|
||||
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
||||
|
||||
---
|
||||
## 功能清单 ✨
|
||||
@@ -246,7 +231,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
| 使用方式 | 支持平台 | 免费平台 |
|
||||
|:---:|:---:|:---:|
|
||||
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||
| 接口调用 | DoubaoASR | - |
|
||||
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ xiaozhi-server
|
||||
|
||||
下载完后,回到本教程继续往下。
|
||||
|
||||
##### 1.2.3.2 下载 config.yaml
|
||||
##### 1.2.3.2 创建 config.yaml
|
||||
|
||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||
|
||||
@@ -224,43 +224,36 @@ python app.py
|
||||
## 配置项目
|
||||
|
||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||
如果你的`data`下面没有`.config.yaml`文件,有两个方式,任选一种:
|
||||
|
||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
||||
第一个方式:你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`。在此文件上修改
|
||||
|
||||
第二个方式:你也可以创建在`data`目录下手动创建`.config.yaml`空文件,然后在这个文件中增加必要的配置信息,系统会优先读取`.config.yaml`文件的配置,如果`.config.yaml`没有配置的,系统会自动去加载`xiaozhi-server`目录下的`config.yaml`的配置。推荐使用这种方式,这种方式是最简洁的方式。
|
||||
|
||||
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||
|
||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
||||
以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例
|
||||
|
||||
```
|
||||
server:
|
||||
websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/
|
||||
prompt: |
|
||||
我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||
我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。
|
||||
我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。
|
||||
请你像一个人一样说话,请勿返回配置xml及其他特殊字符。
|
||||
|
||||
selected_module:
|
||||
VAD: SileroVAD
|
||||
ASR: FunASR
|
||||
LLM: ChatGLMLLM
|
||||
TTS: EdgeTTS
|
||||
# 默认不开启记忆,如需开启请看配置文件里的描述
|
||||
Memory: nomem
|
||||
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
||||
Intent: nointent
|
||||
```
|
||||
LLM: DoubaoLLM
|
||||
|
||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
||||
使用时,在`selected_module`修改成对应的如下LLM配置的名称:
|
||||
|
||||
```
|
||||
LLM:
|
||||
DeepSeekLLM:
|
||||
type: openai
|
||||
...
|
||||
ChatGLMLLM:
|
||||
type: openai
|
||||
...
|
||||
DifyLLM:
|
||||
type: dify
|
||||
...
|
||||
api_key: xxxxxxxxxxxxxxx.xxxxxx
|
||||
```
|
||||
|
||||
建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。
|
||||
比如你要换更换模型,修改`selected_module`下的配置就行。
|
||||
|
||||
## 模型文件
|
||||
|
||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
||||
@@ -276,21 +269,26 @@ LLM:
|
||||
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
||||
|
||||
```
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8002/xiaozhi/ota/
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======================================================
|
||||
```
|
||||
|
||||
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
||||
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
||||
|
||||
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
||||
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`。
|
||||
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8002/xiaozhi/ota/`。
|
||||
|
||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||
|
||||
接下来,你就可以开始 [编译1.6.0版本esp32固件](firmware-build.md)了。
|
||||
接下来,你就可以开始操作你的esp32设备了,你可以`自行编译esp32固件`也可以配置使用`虾哥编译好的1.6.1以上版本的固件`。两个任选一个
|
||||
|
||||
1、 [编译自己的esp32固件](firmware-build.md)了。
|
||||
|
||||
2、 [基于虾哥编译好的固件配置自定义服务器](firmware-setting.md)了。
|
||||
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
@@ -169,7 +169,7 @@ docker logs -f xiaozhi-esp32-server
|
||||
如果你能看到,类似以下日志,则是Server启动成功的标志。
|
||||
|
||||
```
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - Websocket地址是 ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||
@@ -213,7 +213,7 @@ CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unico
|
||||
如果还没有MySQL,你可以通过docker安装mysql
|
||||
|
||||
```
|
||||
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
||||
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -e TZ=Asia/Shanghai -d mysql:latest
|
||||
```
|
||||
|
||||
## 2.安装redis
|
||||
|
||||
@@ -106,10 +106,6 @@ VAD:
|
||||
|
||||
### 8、更多问题,可联系我们反馈 💬
|
||||
|
||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||
可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。
|
||||
|
||||
网盘里有"硬件烧录QQ群"、"开源服务端交流群"、"产品建议联系人" 三张图片,请根据需要选择加入。
|
||||
|
||||
- 硬件烧录QQ群:适用于硬件烧录问题
|
||||
- 开源服务端交流群:适用于服务端问题
|
||||
- 产品建议联系人:适用于产品功能、产品设计等建议
|
||||
也可以发邮件我们取得联系:huangrongzhuang@xin-nan.com
|
||||
@@ -1,118 +0,0 @@
|
||||
# 编译1.6.0版本esp32固件
|
||||
|
||||
## 第1步 配置环境
|
||||
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||
|
||||
## 第2步 打开配置文件
|
||||
配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
|
||||
|
||||
|
||||
从这里下载虾哥[xiaozhi-esp32项目1.6.0版本源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||
|
||||
从这里下载虾哥[xiaozhi-esp32项目1.6.0版本源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||
|
||||
从这里下载虾哥[xiaozhi-esp32项目1.6.0版本源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||
|
||||
下载后,解压缩包,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
||||
|
||||
|
||||
## 第3步 修改WEBSOCKET地址
|
||||
找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
|
||||
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
|
||||
|
||||
修改前:
|
||||
|
||||
```
|
||||
config WEBSOCKET_URL
|
||||
depends on CONNECTION_TYPE_WEBSOCKET
|
||||
string "Websocket URL"
|
||||
default "wss://api.tenclass.net/xiaozhi/v1/"
|
||||
help
|
||||
Communication with the server through websocket after wake up.
|
||||
```
|
||||
|
||||
修改后(示例):
|
||||
|
||||
```
|
||||
config WEBSOCKET_URL
|
||||
depends on CONNECTION_TYPE_WEBSOCKET
|
||||
string "Websocket URL"
|
||||
default "ws://192.168.1.25:8000/xiaozhi/v1/"
|
||||
help
|
||||
Communication with the server through websocket after wake up.
|
||||
```
|
||||
|
||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
||||
|
||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
||||
|
||||
注意:你的地址是`ws://`开头,不是`wss://`开头,一定不要写错了。
|
||||
|
||||
## 第4步 设置编译参数
|
||||
|
||||
设置编译参数
|
||||
|
||||
```
|
||||
# 终端命令行进入xiaozhi-esp32的根目录
|
||||
cd xiaozhi-esp32
|
||||
# 例如我使用的板子是esp32s3,所以设置编译目标为esp32s3,如果你的板子是其他型号,请替换成对应的型号
|
||||
idf.py set-target esp32s3
|
||||
# 进入菜单配置
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||

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

|
||||
|
||||
## 第5步 编译固件
|
||||
|
||||
```
|
||||
idf.py build
|
||||
```
|
||||
|
||||
## 第6步 打包bin固件
|
||||
|
||||
```
|
||||
cd scripts
|
||||
python release.py
|
||||
```
|
||||
|
||||
上面的打包命令执行完成后,会在项目根目录下的`build`目录下生成固件文件`merged-binary.bin`。
|
||||
这个`merged-binary.bin`就是要烧录到硬件上的固件文件。
|
||||
|
||||
注意:如果执行到第二命令后,报了“zip”相关的错误,请忽略这个错误,只要`build`目录下生成固件文件`merged-binary.bin`
|
||||
,对你没有太大影响,请继续。
|
||||
|
||||
## 第7步 烧录固件
|
||||
将esp32设备连接电脑,使用chrome浏览器,打开以下网址
|
||||
|
||||
```
|
||||
https://espressif.github.io/esp-launchpad/
|
||||
```
|
||||
|
||||
打开这个教程,[Flash工具/Web端烧录固件(无IDF开发环境)](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)。
|
||||
翻到:`方式二:ESP-Launchpad 浏览器WEB端烧录`,从`3. 烧录固件/下载到开发板`开始,按照教程操作。
|
||||
|
||||
烧录成功且联网成功后,通过唤醒词唤醒小智,留意server端输出的控制台信息。
|
||||
|
||||
## 常见问题
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||
|
||||
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||
|
||||
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||
|
||||
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||
|
||||
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||
|
||||
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||
@@ -1,14 +1,31 @@
|
||||
# esp32固件编译
|
||||
|
||||
## 第1步 准备你的ota地址
|
||||
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
||||
|
||||
如果你,使用的是本项目0.3.12版本,不管是简单Server部署还是全模块部署,都会有ota地址。
|
||||
|
||||
由于简单Server部署和全模块部署的OTA地址设置方式不一样,请你选择下面的具体方式:
|
||||
|
||||
### 如果你用的是简单Server部署
|
||||
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||
```
|
||||
http://192.168.1.25:8002/xiaozhi/ota/
|
||||
```
|
||||
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
|
||||
|
||||
你可以使用项目自带的`test_page.html`测试一下,是否能连上ota页面输出的websocket地址。
|
||||
|
||||
如果访问不到,你需要到配置文件`.config.yaml`里修改`server.websocket`的地址,重启后再重新测试,直到`test_page.html`能正常访问。
|
||||
|
||||
成功后,请往下进行第2步
|
||||
|
||||
### 如果你用的是全模块部署
|
||||
此刻,请你用浏览器打开你的ota地址,例如我的ota地址
|
||||
```
|
||||
http://192.168.1.25:8002/xiaozhi/ota/
|
||||
```
|
||||
|
||||
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下。
|
||||
如果显示“OTA接口运行正常,websocket集群数量:X”。那就往下进行2步。
|
||||
|
||||
如果显示“OTA接口运行不正常”,大概是你还没在`智控台`配置`Websocket`地址。那就:
|
||||
|
||||
@@ -69,14 +86,9 @@ idf.py set-target esp32s3
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||

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

|
||||
|
||||
## 第5步 编译固件
|
||||
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 149 KiB |
@@ -197,6 +197,10 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<!-- 阿里云maven仓库 -->
|
||||
|
||||
@@ -177,5 +177,5 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.3.11";
|
||||
public static final String VERSION = "0.4.1";
|
||||
}
|
||||
@@ -62,6 +62,13 @@ public class RedisKeys {
|
||||
return "agent:device:count:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体最后连接时间缓存key
|
||||
*/
|
||||
public static String getAgentDeviceLastConnectedAtById(String id) {
|
||||
return "agent:device:lastConnected:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置缓存key
|
||||
*/
|
||||
@@ -96,4 +103,18 @@ public class RedisKeys {
|
||||
public static String getOtaDownloadCountKey(String uuid) {
|
||||
return "ota:download:count:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据的缓存key
|
||||
*/
|
||||
public static String getDictDataByTypeKey(String dictType) {
|
||||
return "sys:dict:data:" + dictType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体音频ID的缓存key
|
||||
*/
|
||||
public static String getAgentAudioIdKey(String uuid) {
|
||||
return "agent:audio:id:" + uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +128,10 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
return SqlHelper.retBool(result);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<M> currentMapperClass() {
|
||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Class<T> currentModelClass() {
|
||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||
|
||||
@@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils;
|
||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
||||
implements CrudService<T, D> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<D> currentDtoClass() {
|
||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package xiaozhi.modules.agent.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||
|
||||
@Tag(name = "智能体聊天历史管理")
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/agent/chat-history")
|
||||
public class AgentChatHistoryController {
|
||||
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
||||
|
||||
/**
|
||||
* 小智服务聊天上报请求
|
||||
* <p>
|
||||
* 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。
|
||||
*
|
||||
* @param request 包含上传文件及相关信息的请求对象
|
||||
*/
|
||||
@Operation(summary = "小智服务聊天上报请求")
|
||||
@PostMapping("/report")
|
||||
public Result<Boolean> uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) {
|
||||
Boolean result = agentChatHistoryBizService.report(request);
|
||||
return new Result<Boolean>().ok(result);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,13 @@ package xiaozhi.modules.agent.controller;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -25,14 +30,20 @@ import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
@@ -46,6 +57,9 @@ public class AgentController {
|
||||
private final AgentService agentService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final DeviceService deviceService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取用户智能体列表")
|
||||
@@ -80,7 +94,7 @@ public class AgentController {
|
||||
@PostMapping
|
||||
@Operation(summary = "创建智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||
public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
||||
|
||||
// 获取默认模板
|
||||
@@ -108,7 +122,7 @@ public class AgentController {
|
||||
// ID、智能体编码和排序会在Service层自动生成
|
||||
agentService.insert(entity);
|
||||
|
||||
return new Result<>();
|
||||
return new Result<String>().ok(entity.getId());
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@@ -178,6 +192,8 @@ public class AgentController {
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
// 先删除关联的设备
|
||||
deviceService.deleteByAgentId(id);
|
||||
// 删除关联的聊天记录
|
||||
agentChatHistoryService.deleteByAgentId(id);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
return new Result<>();
|
||||
@@ -192,4 +208,71 @@ public class AgentController {
|
||||
return new Result<List<AgentTemplateEntity>>().ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/sessions")
|
||||
@Operation(summary = "获取智能体会话列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||
})
|
||||
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
|
||||
@PathVariable("id") String id,
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
params.put("agentId", id);
|
||||
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
|
||||
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/chat-history/{sessionId}")
|
||||
@Operation(summary = "获取智能体聊天记录")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentChatHistoryDTO>> getAgentChatHistory(
|
||||
@PathVariable("id") String id,
|
||||
@PathVariable("sessionId") String sessionId) {
|
||||
// 获取当前用户
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
// 检查权限
|
||||
if (!agentService.checkAgentPermission(id, user.getId())) {
|
||||
return new Result<List<AgentChatHistoryDTO>>().error("没有权限查看该智能体的聊天记录");
|
||||
}
|
||||
|
||||
// 查询聊天记录
|
||||
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/audio/{audioId}")
|
||||
@Operation(summary = "获取音频下载ID")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
|
||||
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||
if (audioData == null) {
|
||||
return new Result<String>().error("音频不存在");
|
||||
}
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId);
|
||||
return new Result<String>().ok(uuid);
|
||||
}
|
||||
|
||||
@GetMapping("/play/{uuid}")
|
||||
@Operation(summary = "播放音频")
|
||||
public ResponseEntity<byte[]> playAudio(@PathVariable("uuid") String uuid) {
|
||||
|
||||
String audioId = (String) redisUtils.get(RedisKeys.getAgentAudioIdKey(uuid));
|
||||
if (StringUtils.isBlank(audioId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||
if (audioData == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
redisUtils.delete(RedisKeys.getAgentAudioIdKey(uuid));
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"play.wav\"")
|
||||
.body(audioData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
|
||||
@@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao<AgentEntity> {
|
||||
* @return 设备数量
|
||||
*/
|
||||
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||
*
|
||||
* @param macAddress 设备MAC地址
|
||||
* @return 默认智能体信息
|
||||
*/
|
||||
@Select(" SELECT a.* FROM ai_device d " +
|
||||
" LEFT JOIN ai_agent a ON d.agent_id = a.id " +
|
||||
" WHERE d.mac_address = #{macAddress} " +
|
||||
" ORDER BY d.id DESC LIMIT 1")
|
||||
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||
|
||||
/**
|
||||
* {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/5/8
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiAgentChatAudioDao extends BaseMapper<AgentChatAudioEntity> {
|
||||
}
|
||||
@@ -0,0 +1,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.entity.AgentEntity;
|
||||
|
||||
/**
|
||||
* 智能体表处理service
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface AgentService extends BaseService<AgentEntity> {
|
||||
|
||||
/**
|
||||
* 管理员获取所有智能体列表(分页)
|
||||
* 获取管理员智能体列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 分页数据
|
||||
*/
|
||||
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 获取智能体详情
|
||||
* 根据ID获取智能体
|
||||
*
|
||||
* @param id 智能体ID
|
||||
* @return 智能体实体
|
||||
*/
|
||||
AgentEntity getAgentById(String id);
|
||||
|
||||
/**
|
||||
* 删除这个用户的所有
|
||||
*
|
||||
* @param userId
|
||||
* 插入智能体
|
||||
*
|
||||
* @param entity 智能体实体
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean insert(AgentEntity entity);
|
||||
|
||||
/**
|
||||
* 根据用户ID删除智能体
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
void deleteAgentByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 获取用户智能体列表
|
||||
*
|
||||
* @param userId
|
||||
* @return
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 智能体列表
|
||||
*/
|
||||
List<AgentDTO> getUserAgents(Long userId);
|
||||
|
||||
/**
|
||||
* 获取智能体的设备数量
|
||||
*
|
||||
* 根据智能体ID获取设备数量
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 设备数量
|
||||
*/
|
||||
Integer getDeviceCountByAgentId(String agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||
*
|
||||
* @param macAddress 设备MAC地址
|
||||
* @return 默认智能体信息,不存在时返回null
|
||||
*/
|
||||
AgentEntity getDefaultAgentByMacAddress(String macAddress);
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限访问智能体
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param userId 用户ID
|
||||
* @return 是否有权限
|
||||
*/
|
||||
boolean checkAgentPermission(String agentId, Long userId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.agent.service.biz;
|
||||
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||
|
||||
/**
|
||||
* 智能体聊天历史业务逻辑层
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface AgentChatHistoryBizService {
|
||||
|
||||
/**
|
||||
* 聊天上报方法
|
||||
*
|
||||
* @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象
|
||||
* 例如:设备MAC地址、文件类型、内容等
|
||||
* @return 上传结果,true表示成功,false表示失败
|
||||
*/
|
||||
Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO);
|
||||
}
|
||||
@@ -0,0 +1,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 org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
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.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||
private final AgentDao agentDao;
|
||||
|
||||
@Autowired
|
||||
private TimbreService timbreModelService;
|
||||
|
||||
@Autowired
|
||||
private ModelConfigService modelConfigService;
|
||||
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
public AgentServiceImpl(AgentDao agentDao) {
|
||||
this.agentDao = agentDao;
|
||||
}
|
||||
private final TimbreService timbreModelService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -101,9 +96,11 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
// 获取 TTS 音色名称
|
||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||
|
||||
// 获取智能体最近的最后连接时长
|
||||
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
||||
|
||||
// 获取设备数量
|
||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
@@ -130,4 +127,29 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
return deviceCount != null ? deviceCount : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentEntity getDefaultAgentByMacAddress(String macAddress) {
|
||||
if (StringUtils.isEmpty(macAddress)) {
|
||||
return null;
|
||||
}
|
||||
return agentDao.getDefaultAgentByMacAddress(macAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkAgentPermission(String agentId, Long userId) {
|
||||
// 获取智能体信息
|
||||
AgentEntity agent = getAgentById(agentId);
|
||||
if (agent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果是超级管理员,直接返回true
|
||||
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否是智能体的所有者
|
||||
return userId.equals(agent.getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package xiaozhi.modules.config.controller;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -8,15 +7,12 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.sys.dto.ConfigSecretDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* xiaozhi-server 配置获取
|
||||
@@ -29,33 +25,20 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
||||
@AllArgsConstructor
|
||||
public class ConfigController {
|
||||
private final ConfigService configService;
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
@PostMapping("server-base")
|
||||
@Operation(summary = "获取配置")
|
||||
public Result<Object> getConfig(@RequestBody ConfigSecretDTO dto) {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
checkSecret(dto.getSecret());
|
||||
public Result<Object> getConfig() {
|
||||
Object config = configService.getConfig(true);
|
||||
return new Result<Object>().ok(config);
|
||||
}
|
||||
|
||||
@PostMapping("agent-models")
|
||||
@Operation(summary = "获取智能体模型")
|
||||
public Result<Object> getAgentModels(@RequestBody AgentModelsDTO dto) {
|
||||
public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
checkSecret(dto.getSecret());
|
||||
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
||||
return new Result<Object>().ok(models);
|
||||
}
|
||||
|
||||
private void checkSecret(String secret) {
|
||||
String secretParam = sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||
// 验证密钥
|
||||
if (StringUtils.isBlank(secret) || !secret.equals(secretParam)) {
|
||||
throw new RenException("密钥错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@ import lombok.Data;
|
||||
@Data
|
||||
@Schema(description = "获取智能体模型配置DTO")
|
||||
public class AgentModelsDTO {
|
||||
@NotBlank(message = "密钥不能为空")
|
||||
@Schema(description = "密钥")
|
||||
private String secret;
|
||||
|
||||
@NotBlank(message = "设备MAC地址不能为空")
|
||||
@Schema(description = "设备MAC地址")
|
||||
|
||||
@@ -61,15 +61,15 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
|
||||
// 构建模块配置
|
||||
buildModuleConfig(
|
||||
agent.getAgentName(),
|
||||
agent.getSystemPrompt(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
agent.getLlmModelId(),
|
||||
agent.getTtsModelId(),
|
||||
agent.getMemModelId(),
|
||||
agent.getIntentModelId(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
result,
|
||||
isCache);
|
||||
|
||||
@@ -117,18 +117,6 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
||||
agent.setAsrModelId(null);
|
||||
}
|
||||
String alreadySelectedLlmModelId = (String) selectedModule.get("LLM");
|
||||
if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) {
|
||||
agent.setLlmModelId(null);
|
||||
}
|
||||
String alreadySelectedMemModelId = (String) selectedModule.get("Memory");
|
||||
if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) {
|
||||
agent.setMemModelId(null);
|
||||
}
|
||||
String alreadySelectedIntentModelId = (String) selectedModule.get("Intent");
|
||||
if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) {
|
||||
agent.setIntentModelId(null);
|
||||
}
|
||||
|
||||
// 构建模块配置
|
||||
buildModuleConfig(
|
||||
@@ -153,7 +141,6 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
* @param paramsList 系统参数列表
|
||||
* @return 配置信息
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object buildConfig(Map<String, Object> config) {
|
||||
|
||||
// 查询所有系统参数
|
||||
|
||||
@@ -28,7 +28,6 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.device.utils.NetworkUtil;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||
@@ -53,7 +52,7 @@ public class OTAController {
|
||||
clientId = deviceId;
|
||||
}
|
||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||
boolean macAddressValid = isMacAddressValid(macAddress);
|
||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||
@@ -102,4 +101,19 @@ public class OTAController {
|
||||
.contentLength(jsonBytes.length)
|
||||
.body(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单判断mac地址是否有效(非严格)
|
||||
*
|
||||
* @param macAddress
|
||||
* @return
|
||||
*/
|
||||
private boolean isMacAddressValid(String macAddress) {
|
||||
if (StringUtils.isBlank(macAddress)) {
|
||||
return false;
|
||||
}
|
||||
// MAC地址通常为12位十六进制数字,可以包含冒号或连字符分隔符
|
||||
String macPattern = "^([0-9A-Za-z]{2}[:-]){5}([0-9A-Za-z]{2})$";
|
||||
return macAddress.matches(macPattern);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,16 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||
/**
|
||||
* 获取此智能体全部设备的最后连接时间
|
||||
* @param agentId 智能体id
|
||||
* @return
|
||||
*/
|
||||
Date getAllLastConnectedAtByAgentId(String agentId);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package xiaozhi.modules.device.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
@@ -78,4 +79,13 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
* @return 激活码
|
||||
*/
|
||||
String geCodeByDeviceId(String deviceId);
|
||||
|
||||
/**
|
||||
* 获取这个智能体设备理的最近的最后连接时间
|
||||
* @param agentId 智能体id
|
||||
* @return 返回设备最近的最后连接时间
|
||||
*/
|
||||
Date getLatestLastConnectionTime(String agentId);
|
||||
|
||||
|
||||
}
|
||||
@@ -258,6 +258,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getLatestLastConnectionTime(String agentId) {
|
||||
// 查询是否有缓存时间,有则返回
|
||||
Date cachedDate = (Date) redisUtils.get(RedisKeys.getAgentDeviceLastConnectedAtById(agentId));
|
||||
if (cachedDate != null) {
|
||||
return cachedDate;
|
||||
}
|
||||
Date maxDate = deviceDao.getAllLastConnectedAtByAgentId(agentId);
|
||||
if (maxDate != null) {
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), maxDate);
|
||||
}
|
||||
return maxDate;
|
||||
}
|
||||
|
||||
private String getDeviceCacheKey(String deviceId) {
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||
|
||||
@@ -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表示单播地址,合法
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.service.impl;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -19,6 +20,8 @@ import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
@@ -36,6 +39,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
private final ModelConfigDao modelConfigDao;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final AgentDao agentDao;
|
||||
|
||||
@Override
|
||||
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
|
||||
@@ -75,6 +79,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
// 再保存供应器提供的模型
|
||||
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||
modelConfigEntity.setModelType(modelType);
|
||||
modelConfigEntity.setIsDefault(0);
|
||||
modelConfigDao.insert(modelConfigEntity);
|
||||
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||
}
|
||||
@@ -102,9 +107,64 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
// 查看是否是默认
|
||||
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
|
||||
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
|
||||
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
|
||||
}
|
||||
// 验证是否有引用
|
||||
checkAgentReference(id);
|
||||
checkIntentConfigReference(id);
|
||||
|
||||
modelConfigDao.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查智能体配置是否有引用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
private void checkAgentReference(String modelId) {
|
||||
List<AgentEntity> agents = agentDao.selectList(
|
||||
new QueryWrapper<AgentEntity>()
|
||||
.eq("vad_model_id", modelId)
|
||||
.or()
|
||||
.eq("asr_model_id", modelId)
|
||||
.or()
|
||||
.eq("llm_model_id", modelId)
|
||||
.or()
|
||||
.eq("tts_model_id", modelId)
|
||||
.or()
|
||||
.eq("mem_model_id", modelId)
|
||||
.or()
|
||||
.eq("intent_model_id", modelId));
|
||||
if (!agents.isEmpty()) {
|
||||
String agentNames = agents.stream()
|
||||
.map(AgentEntity::getAgentName)
|
||||
.collect(Collectors.joining("、"));
|
||||
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查意图识别配置是否有引用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
private void checkIntentConfigReference(String modelId) {
|
||||
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
|
||||
if (modelConfig != null
|
||||
&& "LLM".equals(modelConfig.getModelType() == null ? null : modelConfig.getModelType().toUpperCase())) {
|
||||
List<ModelConfigEntity> intentConfigs = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", "Intent")
|
||||
.like("config_json", "%" + modelId + "%"));
|
||||
if (!intentConfigs.isEmpty()) {
|
||||
throw new RenException("该LLM模型已被意图识别配置引用,无法删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelNameById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
|
||||
@@ -18,6 +18,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import jakarta.servlet.Filter;
|
||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||
import xiaozhi.modules.security.secret.ServerSecretFilter;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* Shiro的配置文件
|
||||
@@ -46,7 +48,7 @@ public class ShiroConfig {
|
||||
}
|
||||
|
||||
@Bean("shiroFilter")
|
||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
|
||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
|
||||
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
||||
config.setFilterOncePerRequest(true);
|
||||
|
||||
@@ -54,9 +56,11 @@ public class ShiroConfig {
|
||||
shiroFilter.setSecurityManager(securityManager);
|
||||
shiroFilter.setShiroFilterConfiguration(config);
|
||||
|
||||
// oauth过滤
|
||||
Map<String, Filter> filters = new HashMap<>();
|
||||
// oauth过滤
|
||||
filters.put("oauth2", new Oauth2Filter());
|
||||
// 服务密钥过滤
|
||||
filters.put("server", new ServerSecretFilter(sysParamsService));
|
||||
shiroFilter.setFilters(filters);
|
||||
|
||||
// 添加Shiro的内置过滤器
|
||||
@@ -79,8 +83,10 @@ public class ShiroConfig {
|
||||
filterMap.put("/user/login", "anon");
|
||||
filterMap.put("/user/pub-config", "anon");
|
||||
filterMap.put("/user/register", "anon");
|
||||
filterMap.put("/config/server-base", "anon");
|
||||
filterMap.put("/config/agent-models", "anon");
|
||||
// 将config路径使用server服务过滤器
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/play/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
@@ -98,4 +104,4 @@ public class ShiroConfig {
|
||||
advisor.setSecurityManager(securityManager);
|
||||
return advisor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package xiaozhi.modules.security.config;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
@@ -18,6 +19,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
@@ -53,10 +63,29 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
// 忽略未知属性
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
// 日期格式转换
|
||||
// mapper.setDateFormat(new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN));
|
||||
// 设置时区
|
||||
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
|
||||
|
||||
// 配置Java8日期时间序列化
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
javaTimeModule.addSerializer(java.time.LocalDateTime.class, new LocalDateTimeSerializer(
|
||||
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
|
||||
javaTimeModule.addSerializer(java.time.LocalDate.class, new LocalDateSerializer(
|
||||
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
|
||||
javaTimeModule.addSerializer(java.time.LocalTime.class,
|
||||
new LocalTimeSerializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||
javaTimeModule.addDeserializer(java.time.LocalDateTime.class, new LocalDateTimeDeserializer(
|
||||
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
|
||||
javaTimeModule.addDeserializer(java.time.LocalDate.class, new LocalDateDeserializer(
|
||||
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
|
||||
javaTimeModule.addDeserializer(java.time.LocalTime.class,
|
||||
new LocalTimeDeserializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||
mapper.registerModule(javaTimeModule);
|
||||
|
||||
// 配置java.util.Date的序列化和反序列化
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN);
|
||||
mapper.setDateFormat(dateFormat);
|
||||
|
||||
// Long类型转String类型
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||
|
||||
@@ -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.setPage((String) params.get(Constant.PAGE));
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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 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.vo.SysDictDataItem;
|
||||
|
||||
/**
|
||||
* 字典数据
|
||||
@@ -15,10 +14,13 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||
@Mapper
|
||||
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
||||
|
||||
/**
|
||||
* 字典数据列表
|
||||
*/
|
||||
List<DictData> getDictDataList();
|
||||
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||
|
||||
List<SysDictDataDTO> getDataByTypeCode(String dictType);
|
||||
/**
|
||||
* 根据字典类型ID获取字典类型编码
|
||||
*
|
||||
* @param dictTypeId 字典类型ID
|
||||
* @return 字典类型编码
|
||||
*/
|
||||
String getTypeByTypeId(Long dictTypeId);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package xiaozhi.modules.sys.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
|
||||
/**
|
||||
@@ -14,9 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
@Mapper
|
||||
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 = "更新时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date updateDate;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package xiaozhi.modules.sys.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 字典数据
|
||||
*/
|
||||
@Data
|
||||
public class DictData {
|
||||
@JsonIgnore
|
||||
private Long dictTypeId;
|
||||
private String dictLabel;
|
||||
private String dictValue;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package xiaozhi.modules.sys.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@Data
|
||||
public class DictType {
|
||||
@JsonIgnore
|
||||
private Long id;
|
||||
private String dictType;
|
||||
private List<DictData> dataList = new ArrayList<>();
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import xiaozhi.common.entity.BaseEntity;
|
||||
@TableName("sys_dict_type")
|
||||
public class SysDictTypeEntity extends BaseEntity {
|
||||
/**
|
||||
* 字典类型
|
||||
* 字典类型编码
|
||||
*/
|
||||
private String dictType;
|
||||
/**
|
||||
|
||||
@@ -1,25 +1,70 @@
|
||||
package xiaozhi.modules.sys.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||
|
||||
/**
|
||||
* 数据字典
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 更新数据字典项
|
||||
*
|
||||
* @param dto 数据字典项的更新数据传输对象
|
||||
*/
|
||||
void update(SysDictDataDTO dto);
|
||||
|
||||
/**
|
||||
* 删除数据字典项
|
||||
*
|
||||
* @param ids 要删除的数据字典项的ID数组
|
||||
*/
|
||||
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.service.BaseService;
|
||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||
|
||||
/**
|
||||
* 数据字典
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 更新字典类型信息
|
||||
*
|
||||
* @param dto 字典类型数据传输对象
|
||||
*/
|
||||
void update(SysDictTypeDTO dto);
|
||||
|
||||
/**
|
||||
* 删除字典类型信息
|
||||
*
|
||||
* @param ids 要删除的字典类型ID数组
|
||||
*/
|
||||
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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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.springframework.stereotype.Service;
|
||||
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.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
|
||||
implements SysDictDataService {
|
||||
private final SysUserDao sysUserDao;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public PageData<SysDictDataDTO> page(Map<String, Object> params) {
|
||||
IPage<SysDictDataEntity> page = baseDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
getWrapper(params));
|
||||
public PageData<SysDictDataVO> page(Map<String, Object> params) {
|
||||
IPage<SysDictDataEntity> page = baseDao.selectPage(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) {
|
||||
@@ -48,32 +66,118 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysDictDataDTO get(Long id) {
|
||||
public SysDictDataVO get(Long id) {
|
||||
SysDictDataEntity entity = baseDao.selectById(id);
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictDataDTO.class);
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictDataVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(SysDictDataDTO dto) {
|
||||
// 相同字典类型的标签不能相同
|
||||
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), null);
|
||||
|
||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||
|
||||
insert(entity);
|
||||
// 删除Redis缓存
|
||||
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(SysDictDataDTO dto) {
|
||||
// 相同字典类型的标签不能相同
|
||||
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), String.valueOf(dto.getId()));
|
||||
|
||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||
|
||||
updateById(entity);
|
||||
// 删除Redis缓存
|
||||
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long[] ids) {
|
||||
// 删除
|
||||
deleteBatchIds(Arrays.asList(ids));
|
||||
for (Long id : 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.List;
|
||||
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.springframework.stereotype.Service;
|
||||
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.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
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.SysUserEntity;
|
||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
|
||||
implements SysDictTypeService {
|
||||
private final SysDictDataDao sysDictDataDao;
|
||||
private final SysUserDao sysUserDao;
|
||||
private final SysDictDataService sysDictDataService;
|
||||
|
||||
@Override
|
||||
public PageData<SysDictTypeDTO> page(Map<String, Object> params) {
|
||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
getWrapper(params));
|
||||
public PageData<SysDictTypeVO> page(Map<String, Object> params) {
|
||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(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) {
|
||||
@@ -53,15 +63,21 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysDictTypeDTO get(Long id) {
|
||||
public SysDictTypeVO get(Long 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
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(SysDictTypeDTO dto) {
|
||||
// 字典类型编码不能重复
|
||||
checkDictTypeUnique(dto.getDictType(), null);
|
||||
|
||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||
|
||||
insert(entity);
|
||||
@@ -70,6 +86,9 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(SysDictTypeDTO dto) {
|
||||
// 字典类型编码不能重复
|
||||
checkDictTypeUnique(dto.getDictType(), String.valueOf(dto.getId()));
|
||||
|
||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||
|
||||
updateById(entity);
|
||||
@@ -78,27 +97,57 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long[] ids) {
|
||||
// 删除
|
||||
// 先删除对应的字典数据
|
||||
for (Long id : ids) {
|
||||
sysDictDataService.deleteByTypeId(id);
|
||||
}
|
||||
// 再删除字典类型
|
||||
deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DictType> getAllList() {
|
||||
List<DictType> typeList = baseDao.getDictTypeList();
|
||||
List<DictData> dataList = sysDictDataDao.getDictDataList();
|
||||
for (DictType type : typeList) {
|
||||
for (DictData data : dataList) {
|
||||
if (type.getId().equals(data.getDictTypeId())) {
|
||||
type.getDataList().add(data);
|
||||
}
|
||||
}
|
||||
public List<SysDictTypeVO> list(Map<String, Object> params) {
|
||||
List<SysDictTypeEntity> entityList = baseDao.selectList(getWrapper(params));
|
||||
List<SysDictTypeVO> sysDictTypeVOList = ConvertUtils.sourceToTarget(entityList, SysDictTypeVO.class);
|
||||
// 设置用户名
|
||||
setUserName(sysDictTypeVOList);
|
||||
|
||||
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
|
||||
public List<DictType> getDictTypeList() {
|
||||
return baseDao.getDictTypeList();
|
||||
private void checkDictTypeUnique(String dictType, String excludeId) {
|
||||
LambdaQueryWrapper<SysDictTypeEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SysDictTypeEntity::getDictType, dictType);
|
||||
if (StringUtils.isNotBlank(excludeId)) {
|
||||
queryWrapper.ne(SysDictTypeEntity::getId, excludeId);
|
||||
}
|
||||
boolean exists = baseDao.exists(queryWrapper);
|
||||
if (exists) {
|
||||
throw new RenException("字典类型编码重复");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -169,6 +169,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
|
||||
adminPageUserVO.setDeviceCount(deviceCount);
|
||||
adminPageUserVO.setStatus(user.getStatus());
|
||||
adminPageUserVO.setCreateDate(user.getCreateDate());
|
||||
return adminPageUserVO;
|
||||
}).toList();
|
||||
return new PageData<>(list, page.getTotal());
|
||||
|
||||
@@ -20,7 +20,9 @@ public class SysUserUtilServiceImpl extends BaseServiceImpl<SysUserDao, SysUserE
|
||||
@Override
|
||||
public void assignUsername(Long userId, Consumer<String> setter) {
|
||||
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){
|
||||
setter.accept(username);
|
||||
}else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.regex.Pattern;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||
import org.springframework.web.socket.client.WebSocketClient;
|
||||
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||
|
||||
@@ -45,8 +46,9 @@ public class WebSocketValidator {
|
||||
try {
|
||||
WebSocketClient client = new StandardWebSocketClient();
|
||||
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秒获取连接结果
|
||||
return future.get(5, TimeUnit.SECONDS);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package xiaozhi.modules.sys.vo;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -23,4 +25,7 @@ public class AdminPageUserVO {
|
||||
|
||||
@Schema(description = "用户id")
|
||||
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 '智能体聊天音频数据表';
|
||||
@@ -78,4 +78,25 @@ databaseChangeLog:
|
||||
changes:
|
||||
- sqlFile:
|
||||
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
|
||||
@@ -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">
|
||||
|
||||
<mapper namespace="xiaozhi.modules.sys.dao.SysDictDataDao">
|
||||
|
||||
<select id="getDictDataList" resultType="xiaozhi.modules.sys.entity.DictData">
|
||||
select dict_type_id, dict_label, dict_value from sys_dict_data order by dict_type_id, sort
|
||||
<select id="getDictDataByType" resultType="xiaozhi.modules.sys.vo.SysDictDataItem">
|
||||
SELECT d.dict_label AS `name`, d.dict_value AS `key`
|
||||
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 id="getDataByTypeCode" resultType="xiaozhi.modules.sys.dto.SysDictDataDTO">
|
||||
select * from sys_dict_data WHERE DICT_TYPE_ID =(SELECT id FROM SYS_DICT_TYPE WHERE DICT_TYPE = #{dictType}) order by sort;
|
||||
<select id="getTypeByTypeId" resultType="java.lang.String">
|
||||
SELECT dict_type
|
||||
FROM sys_dict_type
|
||||
WHERE id = #{dictTypeId}
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -3,8 +3,4 @@
|
||||
|
||||
<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>
|
||||
@@ -1,5 +1,8 @@
|
||||
package xiaozhi.modules.device;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -8,8 +11,9 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@@ -19,18 +23,37 @@ public class DeviceTest {
|
||||
|
||||
@Autowired
|
||||
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
|
||||
@DisplayName("测试写入设备信息")
|
||||
public void testWriteDeviceInfo() {
|
||||
log.info("开始测试写入设备信息...");
|
||||
|
||||
// 模拟设备MAC地址
|
||||
String macAddress = "00:11:22:33:44:55";
|
||||
String macAddress = "00:11:22:33:44:66";
|
||||
// 模拟设备验证码
|
||||
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);
|
||||
|
||||
// 将设备信息写入Redis
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import admin from './module/admin.js'
|
||||
import agent from './module/agent.js'
|
||||
import device from './module/device.js'
|
||||
import dict from './module/dict.js'
|
||||
import model from './module/model.js'
|
||||
import ota from './module/ota.js'
|
||||
import timbre from "./module/timbre.js"
|
||||
import user from './module/user.js'
|
||||
import ota from './module/ota.js'
|
||||
|
||||
/**
|
||||
* 接口地址
|
||||
* 开发时自动读取使用.env.development文件
|
||||
@@ -32,5 +32,6 @@ export default {
|
||||
device,
|
||||
model,
|
||||
timbre,
|
||||
ota
|
||||
ota,
|
||||
dict
|
||||
}
|
||||
|
||||
@@ -97,4 +97,50 @@ export default {
|
||||
});
|
||||
}).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 |
@@ -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">
|
||||
设备管理({{ device.deviceCount }})
|
||||
</div>
|
||||
<div class="settings-btn" @click="handleChatHistory">
|
||||
聊天记录
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<div>最近对话:{{ device.lastConnectedAt }}</div>
|
||||
<div>最近对话:{{ formattedLastConnectedTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -42,6 +45,27 @@ export default {
|
||||
data() {
|
||||
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: {
|
||||
handleDelete() {
|
||||
this.$emit('delete', this.device.agentId)
|
||||
@@ -51,6 +75,9 @@ export default {
|
||||
},
|
||||
handleDeviceManage() {
|
||||
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,105 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="visible" width="500px" @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 {
|
||||
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
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:visible', 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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="visible" width="500px" @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 {
|
||||
form: {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
},
|
||||
rules: {
|
||||
dictName: [{ required: true, message: '请输入字典类型名称', trigger: 'blur' }],
|
||||
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dictTypeData: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form = { ...val }
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:visible', 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;
|
||||
}
|
||||
</style>
|
||||
@@ -38,7 +38,6 @@
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import { FIRMWARE_TYPES } from '@/utils';
|
||||
|
||||
export default {
|
||||
name: 'FirmwareDialog',
|
||||
@@ -54,11 +53,14 @@ export default {
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
firmwareTypes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
firmwareTypes: FIRMWARE_TYPES,
|
||||
uploadProgress: 0,
|
||||
uploadStatus: '',
|
||||
isUploading: false,
|
||||
@@ -85,7 +87,11 @@ export default {
|
||||
return !!this.form.id
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 移除 getDictDataByType 调用
|
||||
},
|
||||
methods: {
|
||||
// 移除 getFirmwareTypes 方法
|
||||
handleClose() {
|
||||
this.$refs.form.clearValidate();
|
||||
this.$emit('cancel');
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
|
||||
<!-- 中间导航菜单 -->
|
||||
<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"
|
||||
: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 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' }" />
|
||||
用户管理
|
||||
</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"
|
||||
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
||||
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
OTA管理
|
||||
</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>
|
||||
|
||||
<!-- 右侧元素 -->
|
||||
@@ -114,6 +127,9 @@ export default {
|
||||
goOtaManagement() {
|
||||
this.$router.push('/ota-management')
|
||||
},
|
||||
goDictManagement() {
|
||||
this.$router.push('/dict-management')
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({ data }) => {
|
||||
@@ -343,4 +359,32 @@ export default {
|
||||
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>
|
||||
@@ -84,8 +84,14 @@ const routes = [
|
||||
title: 'OTA管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/dict-management',
|
||||
name: 'DictManagement',
|
||||
component: function () {
|
||||
return import('../views/DictManagement.vue')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
base: process.env.VUE_APP_PUBLIC_PATH || '/',
|
||||
routes
|
||||
|
||||
@@ -136,63 +136,3 @@ export function getUUID() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 固件类型选项
|
||||
*/
|
||||
export const FIRMWARE_TYPES = [
|
||||
{ "key": "bread-compact-wifi", "name": "面包板新版接线(WiFi)" },
|
||||
{ "key": "bread-compact-wifi-lcd", "name": "面包板新版接线(WiFi)+ LCD" },
|
||||
{ "key": "bread-compact-ml307", "name": "面包板新版接线(ML307 AT)" },
|
||||
{ "key": "bread-compact-esp32", "name": "面包板(WiFi) ESP32 DevKit" },
|
||||
{ "key": "bread-compact-esp32-lcd", "name": "面包板(WiFi+ LCD) ESP32 DevKit" },
|
||||
{ "key": "df-k10", "name": "DFRobot 行空板 k10" },
|
||||
{ "key": "esp32-cgc", "name": "ESP32 CGC" },
|
||||
{ "key": "esp-box-3", "name": "ESP BOX 3" },
|
||||
{ "key": "esp-box", "name": "ESP BOX" },
|
||||
{ "key": "esp-box-lite", "name": "ESP BOX Lite" },
|
||||
{ "key": "kevin-box-1", "name": "Kevin Box 1" },
|
||||
{ "key": "kevin-box-2", "name": "Kevin Box 2" },
|
||||
{ "key": "kevin-c3", "name": "Kevin C3" },
|
||||
{ "key": "kevin-sp-v3-dev", "name": "Kevin SP V3开发板" },
|
||||
{ "key": "kevin-sp-v4-dev", "name": "Kevin SP V4开发板" },
|
||||
{ "key": "kevin-yuying-313lcd", "name": "鱼鹰科技3.13LCD开发板" },
|
||||
{ "key": "lichuang-dev", "name": "立创·实战派ESP32-S3开发板" },
|
||||
{ "key": "lichuang-c3-dev", "name": "立创·实战派ESP32-C3开发板" },
|
||||
{ "key": "magiclick-2p4", "name": "神奇按钮 Magiclick_2.4" },
|
||||
{ "key": "magiclick-2p5", "name": "神奇按钮 Magiclick_2.5" },
|
||||
{ "key": "magiclick-c3", "name": "神奇按钮 Magiclick_C3" },
|
||||
{ "key": "magiclick-c3-v2", "name": "神奇按钮 Magiclick_C3_v2" },
|
||||
{ "key": "m5stack-core-s3", "name": "M5Stack CoreS3" },
|
||||
{ "key": "atoms3-echo-base", "name": "AtomS3 + Echo Base" },
|
||||
{ "key": "atoms3r-echo-base", "name": "AtomS3R + Echo Base" },
|
||||
{ "key": "atoms3r-cam-m12-echo-base", "name": "AtomS3R CAM/M12 + Echo Base" },
|
||||
{ "key": "atommatrix-echo-base", "name": "AtomMatrix + Echo Base" },
|
||||
{ "key": "xmini-c3", "name": "虾哥 Mini C3" },
|
||||
{ "key": "esp32s3-korvo2-v3", "name": "ESP32S3_KORVO2_V3开发板" },
|
||||
{ "key": "esp-sparkbot", "name": "ESP-SparkBot开发板" },
|
||||
{ "key": "esp-spot-s3", "name": "ESP-Spot-S3" },
|
||||
{ "key": "esp32-s3-touch-amoled-1.8", "name": "Waveshare ESP32-S3-Touch-AMOLED-1.8" },
|
||||
{ "key": "esp32-s3-touch-lcd-1.85c", "name": "Waveshare ESP32-S3-Touch-LCD-1.85C" },
|
||||
{ "key": "esp32-s3-touch-lcd-1.85", "name": "Waveshare ESP32-S3-Touch-LCD-1.85" },
|
||||
{ "key": "esp32-s3-touch-lcd-1.46", "name": "Waveshare ESP32-S3-Touch-LCD-1.46" },
|
||||
{ "key": "esp32-s3-touch-lcd-3.5", "name": "Waveshare ESP32-S3-Touch-LCD-3.5" },
|
||||
{ "key": "tudouzi", "name": "土豆子" },
|
||||
{ "key": "lilygo-t-circle-s3", "name": "LILYGO T-Circle-S3" },
|
||||
{ "key": "lilygo-t-cameraplus-s3", "name": "LILYGO T-CameraPlus-S3" },
|
||||
{ "key": "movecall-moji-esp32s3", "name": "Movecall Moji 小智AI衍生版" },
|
||||
{ "key": "movecall-cuican-esp32s3", "name": "Movecall CuiCan 璀璨·AI吊坠" },
|
||||
{ "key": "atk-dnesp32s3", "name": "正点原子DNESP32S3开发板" },
|
||||
{ "key": "atk-dnesp32s3-box", "name": "正点原子DNESP32S3-BOX" },
|
||||
{ "key": "du-chatx", "name": "嘟嘟开发板CHATX(wifi)" },
|
||||
{ "key": "taiji-pi-s3", "name": "太极小派esp32s3" },
|
||||
{ "key": "xingzhi-cube-0.85tft-wifi", "name": "无名科技星智0.85(WIFI)" },
|
||||
{ "key": "xingzhi-cube-0.85tft-ml307", "name": "无名科技星智0.85(ML307)" },
|
||||
{ "key": "xingzhi-cube-0.96oled-wifi", "name": "无名科技星智0.96(WIFI)" },
|
||||
{ "key": "xingzhi-cube-0.96oled-ml307", "name": "无名科技星智0.96(ML307)" },
|
||||
{ "key": "xingzhi-cube-1.54tft-wifi", "name": "无名科技星智1.54(WIFI)" },
|
||||
{ "key": "xingzhi-cube-1.54tft-ml307", "name": "无名科技星智1.54(ML307)" },
|
||||
{ "key": "sensecap-watcher", "name": "SenseCAP Watcher" },
|
||||
{ "key": "doit-s3-aibox", "name": "四博智联AI陪伴盒子" },
|
||||
{ "key": "mixgo-nova", "name": "元控·青春" }
|
||||
]
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@
|
||||
import Api from '@/apis/api';
|
||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import { FIRMWARE_TYPES } from "@/utils";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, AddDeviceDialog },
|
||||
@@ -118,6 +117,7 @@ export default {
|
||||
deviceList: [],
|
||||
loading: false,
|
||||
userApi: null,
|
||||
firmwareTypes: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -163,7 +163,19 @@ export default {
|
||||
this.fetchBindDevices(agentId);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getFirmwareTypes()
|
||||
},
|
||||
methods: {
|
||||
async getFirmwareTypes() {
|
||||
try {
|
||||
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
|
||||
this.firmwareTypes = res.data
|
||||
} catch (error) {
|
||||
console.error('获取固件类型失败:', error)
|
||||
this.$message.error(error.message || '获取固件类型失败')
|
||||
}
|
||||
},
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
@@ -282,20 +294,13 @@ export default {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.deviceList = data.data.map(device => {
|
||||
const bindDate = new Date(device.createDate);
|
||||
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
||||
let formattedLastConversation = '';
|
||||
if (device.lastConnectedAt) {
|
||||
const lastConvoDate = new Date(device.lastConnectedAt);
|
||||
formattedLastConversation = `${lastConvoDate.getFullYear()}-${(lastConvoDate.getMonth() + 1).toString().padStart(2, '0')}-${lastConvoDate.getDate().toString().padStart(2, '0')} ${lastConvoDate.getHours().toString().padStart(2, '0')}:${lastConvoDate.getMinutes().toString().padStart(2, '0')}:${lastConvoDate.getSeconds().toString().padStart(2, '0')}`;
|
||||
}
|
||||
return {
|
||||
device_id: device.id,
|
||||
model: device.board,
|
||||
firmwareVersion: device.appVersion,
|
||||
macAddress: device.macAddress,
|
||||
bindTime: formattedBindTime,
|
||||
lastConversation: formattedLastConversation,
|
||||
bindTime: device.createDate,
|
||||
lastConversation: device.lastConnectedAt,
|
||||
remark: device.alias,
|
||||
isEdit: false,
|
||||
otaSwitch: device.autoUpdate === 1,
|
||||
@@ -317,8 +322,8 @@ export default {
|
||||
return "";
|
||||
},
|
||||
getFirmwareTypeName(type) {
|
||||
const firmwareType = FIRMWARE_TYPES.find(item => item.key === type);
|
||||
return firmwareType ? firmwareType.name : type;
|
||||
const firmwareType = this.firmwareTypes.find(item => item.key === type)
|
||||
return firmwareType ? firmwareType.name : type
|
||||
},
|
||||
handleOtaSwitchChange(row) {
|
||||
Api.device.enableOtaUpgrade(row.device_id, row.otaSwitch ? 1 : 0, ({ data }) => {
|
||||
|
||||
@@ -0,0 +1,835 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">字典管理</h2>
|
||||
<div class="action-group">
|
||||
<div class="search-group">
|
||||
<el-input placeholder="请输入字典值标签查询" v-model="search" class="search-input" clearable
|
||||
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||
<el-button class="btn-search" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<!-- 左侧字典类型列表 -->
|
||||
<div class="dict-type-panel">
|
||||
<div class="dict-type-header">
|
||||
<el-button type="success" size="mini" @click="showAddDictTypeDialog">新增字典类型</el-button>
|
||||
<el-button type="danger" size="mini" @click="batchDeleteDictType"
|
||||
:disabled="selectedDictTypes.length === 0">
|
||||
批量删除字典类型
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table ref="dictTypeTable" :data="dictTypeList" style="width: 100%" v-loading="dictTypeLoading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" @row-click="handleDictTypeRowClick"
|
||||
@selection-change="handleDictTypeSelectionChange" :row-class-name="tableRowClassName"
|
||||
class="dict-type-table">
|
||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||
<el-table-column label="字典类型名称" prop="dictName" align="center"></el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click.stop="editDictType(scope.row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 右侧字典数据列表 -->
|
||||
<div class="content-area">
|
||||
<el-card class="dict-data-card" shadow="never">
|
||||
<el-table ref="dictDataTable" :data="dictDataList" style="width: 100%"
|
||||
v-loading="dictDataLoading" element-loading-text="拼命加载中"
|
||||
element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
@selection-change="handleDictDataSelectionChange" class="data-table"
|
||||
header-row-class-name="table-header">
|
||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||
<el-table-column label="字典标签" prop="dictLabel" align="center"></el-table-column>
|
||||
<el-table-column label="字典值" prop="dictValue" align="center"></el-table-column>
|
||||
<el-table-column label="排序" prop="sort" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180px">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="editDictData(scope.row)"
|
||||
class="edit-btn">
|
||||
修改
|
||||
</el-button>
|
||||
<el-button type="text" size="mini" @click="deleteDictData(scope.row)"
|
||||
class="delete-btn">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="table-footer">
|
||||
<div class="batch-actions">
|
||||
<el-button size="mini" type="primary" @click="selectAllDictData">
|
||||
{{ isAllDictDataSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" @click="showAddDictDataDialog" class="add-btn">
|
||||
新增字典数据
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDeleteDictData">
|
||||
批量删除字典数据
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
||||
:value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
上一页
|
||||
</button>
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||
下一页
|
||||
</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用字典类型编辑弹框组件 -->
|
||||
<DictTypeDialog :visible.sync="dictTypeDialogVisible" :title="dictTypeDialogTitle" :dictTypeData="dictTypeForm"
|
||||
@save="saveDictType" />
|
||||
|
||||
<!-- 使用字典数据编辑弹框组件 -->
|
||||
<DictDataDialog :visible.sync="dictDataDialogVisible" :title="dictDataDialogTitle" :dictData="dictDataForm"
|
||||
:dictTypeId="selectedDictType?.id" @save="saveDictData" />
|
||||
<el-footer style="flex-shrink:unset;">
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dictApi from '@/apis/module/dict'
|
||||
import DictDataDialog from '@/components/DictDataDialog.vue'
|
||||
import DictTypeDialog from '@/components/DictTypeDialog.vue'
|
||||
import HeaderBar from '@/components/HeaderBar.vue'
|
||||
import VersionFooter from '@/components/VersionFooter.vue'
|
||||
export default {
|
||||
name: 'DictManagement',
|
||||
components: {
|
||||
HeaderBar,
|
||||
DictTypeDialog,
|
||||
DictDataDialog,
|
||||
VersionFooter
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 字典类型相关
|
||||
dictTypeList: [],
|
||||
dictTypeLoading: false,
|
||||
selectedDictType: null,
|
||||
selectedDictTypes: [], // 恢复多选数组
|
||||
dictTypeDialogVisible: false,
|
||||
dictTypeDialogTitle: '新增字典类型',
|
||||
dictTypeForm: {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
},
|
||||
|
||||
// 字典数据相关
|
||||
dictDataList: [],
|
||||
dictDataLoading: false,
|
||||
selectedDictData: [],
|
||||
isAllDictDataSelected: false,
|
||||
dictDataDialogVisible: false,
|
||||
dictDataDialogTitle: '新增字典数据',
|
||||
dictDataForm: {
|
||||
id: null,
|
||||
dictTypeId: null,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
},
|
||||
search: '',
|
||||
// 添加分页相关数据
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadDictTypeList()
|
||||
},
|
||||
methods: {
|
||||
// 字典类型相关方法
|
||||
loadDictTypeList() {
|
||||
this.dictTypeLoading = true
|
||||
dictApi.getDictTypeList({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
dictName: this.search
|
||||
}, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.dictTypeList = data.data.list
|
||||
if (this.dictTypeList.length > 0) {
|
||||
this.selectedDictType = this.dictTypeList[0]
|
||||
this.loadDictDataList(this.dictTypeList[0].id)
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dictTypeTable.setCurrentRow(this.dictTypeList[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
this.dictTypeLoading = false
|
||||
})
|
||||
},
|
||||
handleDictTypeRowClick(row) {
|
||||
this.selectedDictType = row
|
||||
this.loadDictDataList(row.id)
|
||||
this.$refs.dictTypeTable.setCurrentRow(row)
|
||||
},
|
||||
handleDictTypeSelectionChange(val) {
|
||||
this.selectedDictTypes = val
|
||||
},
|
||||
tableRowClassName({ row }) {
|
||||
return row === this.selectedDictType ? 'current-row' : ''
|
||||
},
|
||||
showAddDictTypeDialog() {
|
||||
this.dictTypeDialogTitle = '新增字典类型'
|
||||
this.dictTypeForm = {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
}
|
||||
this.dictTypeDialogVisible = true
|
||||
},
|
||||
editDictType(row) {
|
||||
this.dictTypeDialogTitle = '编辑字典类型'
|
||||
this.dictTypeForm = { ...row }
|
||||
this.dictTypeDialogVisible = true
|
||||
},
|
||||
saveDictType(formData) {
|
||||
const api = formData.id ? dictApi.updateDictType : dictApi.addDictType
|
||||
api(formData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('保存成功')
|
||||
this.dictTypeDialogVisible = false
|
||||
this.loadDictTypeList()
|
||||
}
|
||||
})
|
||||
},
|
||||
batchDeleteDictType() {
|
||||
if (this.selectedDictTypes.length === 0) {
|
||||
this.$message.warning('请选择要删除的字典类型')
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm('确定要删除选中的字典类型吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const ids = this.selectedDictTypes.map(item => item.id)
|
||||
dictApi.deleteDictType(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.loadDictTypeList()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 字典数据相关方法
|
||||
loadDictDataList(dictTypeId) {
|
||||
if (!dictTypeId) return
|
||||
this.dictDataLoading = true
|
||||
dictApi.getDictDataList({
|
||||
dictTypeId,
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
dictLabel: this.search,
|
||||
dictValue: ''
|
||||
}, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.dictDataList = data.data.list
|
||||
this.total = data.data.total
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取字典数据失败')
|
||||
}
|
||||
this.dictDataLoading = false
|
||||
})
|
||||
},
|
||||
handleDictDataSelectionChange(val) {
|
||||
this.selectedDictData = val
|
||||
this.isAllDictDataSelected = val.length === this.dictDataList.length
|
||||
},
|
||||
selectAllDictData() {
|
||||
if (this.isAllDictDataSelected) {
|
||||
this.$refs.dictDataTable.clearSelection()
|
||||
} else {
|
||||
this.$refs.dictDataTable.toggleAllSelection()
|
||||
}
|
||||
},
|
||||
showAddDictDataDialog() {
|
||||
if (!this.selectedDictType) {
|
||||
this.$message.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
this.dictDataDialogTitle = '新增字典数据'
|
||||
this.dictDataForm = {
|
||||
id: null,
|
||||
dictTypeId: this.selectedDictType.id,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
}
|
||||
this.dictDataDialogVisible = true
|
||||
},
|
||||
editDictData(row) {
|
||||
this.dictDataDialogTitle = '编辑字典数据'
|
||||
this.dictDataForm = { ...row }
|
||||
this.dictDataDialogVisible = true
|
||||
},
|
||||
saveDictData(formData) {
|
||||
const api = formData.id ? dictApi.updateDictData : dictApi.addDictData
|
||||
api(formData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('保存成功')
|
||||
this.dictDataDialogVisible = false
|
||||
this.loadDictDataList(formData.dictTypeId)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteDictData(row) {
|
||||
this.$confirm('确定要删除该字典数据吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
dictApi.deleteDictData([row.id], ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.loadDictDataList(row.dictTypeId)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
batchDeleteDictData() {
|
||||
if (this.selectedDictData.length === 0) {
|
||||
this.$message.warning('请选择要删除的字典数据')
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm('确定要删除选中的字典数据吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const ids = this.selectedDictData.map(item => item.id)
|
||||
dictApi.deleteDictData(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('删除成功')
|
||||
this.loadDictDataList(this.selectedDictType.id)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
handleSearch() {
|
||||
if (!this.selectedDictType) {
|
||||
this.$message.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
this.currentPage = 1
|
||||
this.loadDictDataList(this.selectedDictType.id)
|
||||
},
|
||||
// 添加分页相关方法
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.loadDictDataList(this.selectedDictType?.id);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
visiblePages() {
|
||||
const pages = [];
|
||||
const maxVisible = 3;
|
||||
let start = Math.max(1, this.currentPage - 1);
|
||||
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
:deep(.search-input .el-input__inner) {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #DCDFE6;
|
||||
background-color: white;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
:deep(.search-input .el-input__inner:focus) {
|
||||
border-color: #6b8cff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.dict-type-panel {
|
||||
width: 300px;
|
||||
background: white;
|
||||
border-right: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dict-type-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dict-type-table {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dict-data-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
border-radius: 6px;
|
||||
overflow-y: auto;
|
||||
background-color: transparent !important;
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
:deep(.el-table__body-wrapper) {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
|
||||
:deep(.el-table__body) {
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
min-height: 60px;
|
||||
background: white;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 26px;
|
||||
|
||||
.el-button {
|
||||
min-width: 72px;
|
||||
height: 32px;
|
||||
padding: 7px 12px 7px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: #5f70f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-child(3),
|
||||
.pagination-btn:nth-last-child(2) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #d7dce6;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(245, 247, 250, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: #5f70f3 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #5f70f3 !important;
|
||||
|
||||
&:hover {
|
||||
background: #6d7cf5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.total-text {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.delete-btn {
|
||||
margin: 0 8px;
|
||||
color: #7079aa !important;
|
||||
font-size: 12px;
|
||||
padding: 7px 12px;
|
||||
height: 32px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: #5a64b5 !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.dict-type-header .el-button) {
|
||||
min-width: 72px;
|
||||
height: 32px;
|
||||
padding: 7px 12px 7px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row.current-row) {
|
||||
background-color: #5778ff !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row.current-row .el-button--text) {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row.current-row:hover) {
|
||||
background-color: #5778ff !important;
|
||||
}
|
||||
|
||||
:deep(.dict-type-table .el-table__row td) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.el-table thead) {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -96,8 +96,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑固件对话框 -->
|
||||
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm" @submit="handleSubmit"
|
||||
@cancel="dialogVisible = false" />
|
||||
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm"
|
||||
:firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
@@ -109,7 +109,6 @@ import Api from "@/apis/api";
|
||||
import FirmwareDialog from "@/components/FirmwareDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import { FIRMWARE_TYPES } from "@/utils";
|
||||
import { formatDate, formatFileSize } from "@/utils/format";
|
||||
|
||||
export default {
|
||||
@@ -136,10 +135,12 @@ export default {
|
||||
remark: "",
|
||||
firmwarePath: ""
|
||||
},
|
||||
firmwareTypes: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchFirmwareList();
|
||||
this.getFirmwareTypes();
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -387,9 +388,18 @@ export default {
|
||||
},
|
||||
formatDate,
|
||||
formatFileSize,
|
||||
async getFirmwareTypes() {
|
||||
try {
|
||||
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
|
||||
this.firmwareTypes = res.data
|
||||
} catch (error) {
|
||||
console.error('获取固件类型失败:', error)
|
||||
this.$message.error(error.message || '获取固件类型失败')
|
||||
}
|
||||
},
|
||||
getFirmwareTypeName(type) {
|
||||
const firmwareType = FIRMWARE_TYPES.find(item => item.key === type);
|
||||
return firmwareType ? firmwareType.name : type;
|
||||
const firmwareType = this.firmwareTypes.find(item => item.key === type)
|
||||
return firmwareType ? firmwareType.name : type
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,8 +25,19 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
|
||||
<el-table-column label="参数值" prop="paramValue" align="center"
|
||||
show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="参数值" prop="paramValue" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div v-if="isSensitiveParam(scope.row.paramCode)">
|
||||
<span v-if="!scope.row.showValue">{{ maskSensitiveValue(scope.row.paramValue)
|
||||
}}</span>
|
||||
<span v-else>{{ scope.row.paramValue }}</span>
|
||||
<el-button size="mini" type="text" @click="toggleSensitiveValue(scope.row)">
|
||||
{{ scope.row.showValue ? '隐藏' : '查看' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span v-else>{{ scope.row.paramValue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
@@ -100,6 +111,7 @@ export default {
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增参数",
|
||||
isAllSelected: false,
|
||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||
paramForm: {
|
||||
id: null,
|
||||
paramCode: "",
|
||||
@@ -152,7 +164,8 @@ export default {
|
||||
if (data.code === 0) {
|
||||
this.paramsList = data.data.list.map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
selected: false,
|
||||
showValue: false
|
||||
}));
|
||||
this.total = data.data.total;
|
||||
} else {
|
||||
@@ -314,7 +327,18 @@ export default {
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
isSensitiveParam(paramCode) {
|
||||
return this.sensitive_keys.some(key => paramCode.toLowerCase().includes(key.toLowerCase()));
|
||||
},
|
||||
maskSensitiveValue(value) {
|
||||
if (!value) return '';
|
||||
if (value.length <= 8) return '****';
|
||||
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
|
||||
},
|
||||
toggleSensitiveValue(row) {
|
||||
this.$set(row, 'showValue', !row.showValue);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
|
||||
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
|
||||
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
|
||||
<el-table-column label="注册时间" prop="createDate" align="center"></el-table-column>
|
||||
<el-table-column label="状态" prop="status" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 1" type="success">正常</el-tag>
|
||||
@@ -336,7 +337,7 @@ export default {
|
||||
}).catch(() => {
|
||||
// 用户取消操作
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -32,11 +32,7 @@
|
||||
</div>
|
||||
<div class="device-list-container">
|
||||
<template v-if="isLoading">
|
||||
<div
|
||||
v-for="i in skeletonCount"
|
||||
:key="'skeleton-'+i"
|
||||
class="skeleton-item"
|
||||
>
|
||||
<div v-for="i in skeletonCount" :key="'skeleton-' + i" class="skeleton-item">
|
||||
<div class="skeleton-image"></div>
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton-line"></div>
|
||||
@@ -46,14 +42,8 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<DeviceItem
|
||||
v-for="(item, index) in devices"
|
||||
:key="index"
|
||||
:device="item"
|
||||
@configure="goToRoleConfig"
|
||||
@deviceManage="handleDeviceManage"
|
||||
@delete="handleDeleteAgent"
|
||||
/>
|
||||
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" @configure="goToRoleConfig"
|
||||
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" @chat-history="handleShowChatHistory" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,6 +52,7 @@
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
<chat-history-dialog :visible.sync="showChatHistory" :agent-id="currentAgentId" :agent-name="currentAgentName" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -69,13 +60,14 @@
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
|
||||
import ChatHistoryDialog from '@/components/ChatHistoryDialog.vue';
|
||||
import DeviceItem from '@/components/DeviceItem.vue';
|
||||
import HeaderBar from '@/components/HeaderBar.vue';
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
|
||||
export default {
|
||||
name: 'HomePage',
|
||||
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar, VersionFooter },
|
||||
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar, VersionFooter, ChatHistoryDialog },
|
||||
data() {
|
||||
return {
|
||||
addDeviceDialogVisible: false,
|
||||
@@ -85,6 +77,9 @@ export default {
|
||||
searchRegex: null,
|
||||
isLoading: true,
|
||||
skeletonCount: localStorage.getItem('skeletonCount') || 8,
|
||||
showChatHistory: false,
|
||||
currentAgentId: '',
|
||||
currentAgentName: ''
|
||||
}
|
||||
},
|
||||
|
||||
@@ -177,6 +172,11 @@ export default {
|
||||
}
|
||||
});
|
||||
}).catch(() => { });
|
||||
},
|
||||
handleShowChatHistory({ agentId, agentName }) {
|
||||
this.currentAgentId = agentId;
|
||||
this.currentAgentName = agentName;
|
||||
this.showChatHistory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,7 +302,9 @@ export default {
|
||||
|
||||
/* 骨架屏动画 */
|
||||
@keyframes shimmer {
|
||||
100% { transform: translateX(100%); }
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-item {
|
||||
@@ -353,12 +355,10 @@ export default {
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255,255,255,0),
|
||||
rgba(255,255,255,0.3),
|
||||
rgba(255,255,255,0)
|
||||
);
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0.3),
|
||||
rgba(255, 255, 255, 0));
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar/>
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">角色配置</h2>
|
||||
@@ -26,93 +26,49 @@
|
||||
<div class="form-grid">
|
||||
<div class="form-column">
|
||||
<el-form-item label="助手昵称:">
|
||||
<el-input v-model="form.agentName" class="form-input"/>
|
||||
<el-input v-model="form.agentName" class="form-input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色模版:">
|
||||
<div class="template-container">
|
||||
<div
|
||||
v-for="(template, index) in templates"
|
||||
:key="`template-${index}`"
|
||||
class="template-item"
|
||||
:class="{ 'template-loading': loadingTemplate }"
|
||||
@click="selectTemplate(template)"
|
||||
>
|
||||
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
||||
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
||||
{{ template.agentName }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色介绍:">
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="5"
|
||||
resize="none"
|
||||
placeholder="请输入内容"
|
||||
v-model="form.systemPrompt"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
class="form-textarea"
|
||||
/>
|
||||
<el-input type="textarea" rows="12" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
||||
maxlength="2000" show-word-limit class="form-textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="语言编码:">
|
||||
<el-input
|
||||
v-model="form.langCode"
|
||||
placeholder="请输入语言编码,如:zh_CN"
|
||||
maxlength="10"
|
||||
show-word-limit
|
||||
class="form-input"
|
||||
/>
|
||||
<el-form-item label="语言编码:" style="display: none;">
|
||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
|
||||
class="form-input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="交互语种:">
|
||||
<el-input
|
||||
v-model="form.language"
|
||||
placeholder="请输入交互语种,如:中文"
|
||||
maxlength="10"
|
||||
show-word-limit
|
||||
class="form-input"
|
||||
/>
|
||||
<el-form-item label="交互语种:" style="display: none;">
|
||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
|
||||
class="form-input" />
|
||||
</el-form-item>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||
<div class="hint-text">
|
||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="">
|
||||
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||
<div class="hint-text">
|
||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="">
|
||||
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-column">
|
||||
<el-form-item
|
||||
v-for="(model, index) in models"
|
||||
:key="`model-${index}`"
|
||||
:label="model.label"
|
||||
class="model-item"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.model[model.key]"
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
class="form-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||
:key="`option-${index}-${optionIndex}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
||||
class="model-item">
|
||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select">
|
||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色音色:">
|
||||
<el-select
|
||||
v-model="form.ttsVoiceId"
|
||||
placeholder="请选择"
|
||||
class="form-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in voiceOptions"
|
||||
:key="`voice-${index}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
||||
:value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -133,7 +89,7 @@ import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
export default {
|
||||
name: 'RoleConfigPage',
|
||||
components: {HeaderBar},
|
||||
components: { HeaderBar },
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
@@ -154,12 +110,12 @@ export default {
|
||||
}
|
||||
},
|
||||
models: [
|
||||
{label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD'},
|
||||
{label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR'},
|
||||
{label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM'},
|
||||
{label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent'},
|
||||
{label: '记忆(Memory)', key: 'memModelId', type: 'Memory'},
|
||||
{label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS'},
|
||||
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
|
||||
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
|
||||
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
|
||||
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
|
||||
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
|
||||
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' },
|
||||
],
|
||||
modelOptions: {},
|
||||
templates: [],
|
||||
@@ -187,7 +143,7 @@ export default {
|
||||
language: this.form.language,
|
||||
sort: this.form.sort
|
||||
};
|
||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
|
||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '配置保存成功',
|
||||
@@ -232,7 +188,7 @@ export default {
|
||||
});
|
||||
},
|
||||
fetchTemplates() {
|
||||
Api.agent.getAgentTemplate(({data}) => {
|
||||
Api.agent.getAgentTemplate(({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.templates = data.data;
|
||||
} else {
|
||||
@@ -277,7 +233,7 @@ export default {
|
||||
};
|
||||
},
|
||||
fetchAgentConfig(agentId) {
|
||||
Api.agent.getDeviceConfig(agentId, ({data}) => {
|
||||
Api.agent.getDeviceConfig(agentId, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.form = {
|
||||
...this.form,
|
||||
@@ -298,7 +254,7 @@ export default {
|
||||
},
|
||||
fetchModelOptions() {
|
||||
this.models.forEach(model => {
|
||||
Api.model.getModelNames(model.type, '', ({data}) => {
|
||||
Api.model.getModelNames(model.type, '', ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$set(this.modelOptions, model.type, data.data.map(item => ({
|
||||
value: item.id,
|
||||
@@ -315,7 +271,7 @@ export default {
|
||||
this.voiceOptions = [];
|
||||
return;
|
||||
}
|
||||
Api.model.getModelVoices(modelId, '', ({data}) => {
|
||||
Api.model.getModelVoices(modelId, '', ({ data }) => {
|
||||
if (data.code === 0 && data.data) {
|
||||
this.voiceOptions = data.data.map(voice => ({
|
||||
value: voice.id,
|
||||
@@ -567,7 +523,6 @@ export default {
|
||||
background: none;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
bottom: -10%;
|
||||
right: 3%;
|
||||
}
|
||||
|
||||
@@ -597,5 +552,4 @@ export default {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
</style>
|
||||